'use strict';

var fs = require('fs');
var shell = require('shelljs');
var grunt = require('grunt');
var spawn = require('npm-run').spawn;

var CSP_CSS_HEADER = '/* Include this file in your html if you are using the CSP mode. */\n\n';


module.exports = {

  startKarma: function(config, singleRun, done) {
    var browsers = grunt.option('browsers');
    var reporters = grunt.option('reporters');
    var noColor = grunt.option('no-colors');
    var port = grunt.option('port');
    var p = spawn('karma', ['start', config,
      singleRun ? '--single-run=true' : '',
      reporters ? '--reporters=' + reporters : '',
      browsers ? '--browsers=' + browsers : '',
      noColor ? '--no-colors' : '',
      port ? '--port=' + port : ''
    ]);
    p.stdout.pipe(process.stdout);
    p.stderr.pipe(process.stderr);
    p.on('exit', function(code) {
      if (code !== 0) grunt.fail.warn('Karma test(s) failed. Exit code: ' + code);
      done();
    });
  },


  updateWebdriver: function(done) {
    if (process.env.TRAVIS) {
      // Skip the webdriver-manager update on Travis, since the browsers will
      // be provided remotely.
      done();
      return;
    }
    var p = spawn('webdriver-manager', ['update']);
    p.stdout.pipe(process.stdout);
    p.stderr.pipe(process.stderr);
    p.on('exit', function(code) {
      if (code !== 0) grunt.fail.warn('Webdriver failed to update');
      done();
    });
  },

  startProtractor: function(config, done) {
    var sauceUser = grunt.option('sauceUser');
    var sauceKey = grunt.option('sauceKey');
    var tunnelIdentifier = grunt.option('capabilities.tunnel-identifier');
    var sauceBuild = grunt.option('capabilities.build');
    var browser = grunt.option('browser');
    var specs = grunt.option('specs');
    var args = [config];
    if (sauceUser) args.push('--sauceUser=' + sauceUser);
    if (sauceKey) args.push('--sauceKey=' + sauceKey);
    if (tunnelIdentifier) args.push('--capabilities.tunnel-identifier=' + tunnelIdentifier);
    if (sauceBuild) args.push('--capabilities.build=' + sauceBuild);
    if (specs) args.push('--specs=' + specs);
    if (browser) {
      args.push('--browser=' + browser);
    }


    var p = spawn('protractor', args);
    p.stdout.pipe(process.stdout);
    p.stderr.pipe(process.stderr);
    p.on('exit', function(code) {
      if (code !== 0) grunt.fail.warn('Protractor test(s) failed. Exit code: ' + code);
      done();
    });
  },


  wrap(src, name) {
    return [`src/${name}.prefix`, ...src, `src/${name}.suffix`];
  },


  addStyle: function(src, styles, minify) {
    styles = styles.reduce(processCSS.bind(this), {
      js: [src],
      css: []
    });
    return {
      js: styles.js.join('\n'),
      css: styles.css.join('\n')
    };

    function processCSS(state, file) {
      var css = fs.readFileSync(file).toString(),
        js;
      state.css.push(css);

      if (minify) {
        css = css
          .replace(/\r?\n/g, '')
          .replace(/\/\*.*?\*\//g, '')
          .replace(/:\s+/g, ':')
          .replace(/\s*\{\s*/g, '{')
          .replace(/\s*\}\s*/g, '}')
          .replace(/\s*,\s*/g, ',')
          .replace(/\s*;\s*/g, ';');
      }
      //escape for js
      css = css
        .replace(/\\/g, '\\\\')
        .replace(/'/g, '\\\'')
        .replace(/\r?\n/g, '\\n');
      js = '!window.angular.$$csp().noInlineStyle && window.angular.element(document.head).prepend(\'<style type="text/css">' + css + '</style>\');';
      state.js.push(js);

      return state;
    }
  },


  process: function(src, NG_VERSION, strict) {
    var processed = src
      .replace(/(['"])NG_VERSION_FULL\1/g, NG_VERSION.full)
      .replace(/(['"])NG_VERSION_MAJOR\1/, NG_VERSION.major)
      .replace(/(['"])NG_VERSION_MINOR\1/, NG_VERSION.minor)
      .replace(/(['"])NG_VERSION_DOT\1/, NG_VERSION.patch)
      .replace(/(['"])NG_VERSION_CDN\1/, NG_VERSION.cdn)
      .replace(/(['"])NG_VERSION_CODENAME\1/, NG_VERSION.codeName);
    if (strict !== false) processed = this.singleStrict(processed, '\n\n', true);
    return processed;
  },


  build: function(config, fn) {
    var files = grunt.file.expand(config.src);
    // grunt.file.expand might reorder the list of files
    // when it is expanding globs, so we use prefix and suffix
    // fields to ensure that files are at the start of end of
    // the list (primarily for wrapping in an IIFE).
    if (config.prefix) {
      files = grunt.file.expand(config.prefix).concat(files);
    }
    if (config.suffix) {
      files = files.concat(grunt.file.expand(config.suffix));
    }
    var styles = config.styles;
    var processedStyles;
    //concat
    var src = files.map(function(filepath) {
      return grunt.file.read(filepath);
    }).join(grunt.util.normalizelf('\n'));
    //process
    var processed = this.process(src, grunt.config('NG_VERSION'), config.strict);
    if (styles) {
      processedStyles = this.addStyle(processed, styles.css, styles.minify);
      processed = processedStyles.js;
      if (config.styles.generateCspCssFile) {
        grunt.file.write(removeSuffix(config.dest) + '-csp.css', CSP_CSS_HEADER + processedStyles.css);
      }
    }
    //write
    grunt.file.write(config.dest, processed);
    grunt.log.ok('File ' + config.dest + ' created.');
    fn();

    function removeSuffix(fileName) {
      return fileName.replace(/\.js$/, '');
    }
  },


  singleStrict: function(src, insert) {
    return src
      .replace(/\s*("|')use strict("|');\s*/g, insert) // remove all file-specific strict mode flags
      .replace(/(\(function\([^)]*\)\s*\{)/, '$1\'use strict\';'); // add single strict mode flag
  },


  sourceMap: function(mapFile, fileContents) {
    var sourceMapLine = '//# sourceMappingURL=' + mapFile + '\n';
    return fileContents + sourceMapLine;
  },


  min: function(file, done) {
    var classPathSep = (process.platform === 'win32') ? ';' : ':';
    var minFile = file.replace(/\.js$/, '.min.js');
    var mapFile = minFile + '.map';
    var mapFileName = mapFile.match(/[^/]+$/)[0];
    var errorFileName = file.replace(/\.js$/, '-errors.json');
    var versionNumber = grunt.config('NG_VERSION').full;
    var compilationLevel = (file === 'build/angular-message-format.js') ?
        'ADVANCED_OPTIMIZATIONS' : 'SIMPLE_OPTIMIZATIONS';
    shell.exec(
        'java ' +
            this.java32flags() + ' ' +
            this.memoryRequirement() + ' ' +
            '-cp vendor/closure-compiler/compiler.jar' + classPathSep +
            'vendor/ng-closure-runner/ngcompiler.jar ' +
            'org.angularjs.closurerunner.NgClosureRunner ' +
            '--compilation_level ' + compilationLevel + ' ' +
            '--language_in ECMASCRIPT5_STRICT ' +
            '--minerr_pass ' +
            '--minerr_errors ' + errorFileName + ' ' +
            '--minerr_url http://errors.angularjs.org/' + versionNumber + '/ ' +
            '--source_map_format=V3 ' +
            '--create_source_map ' + mapFile + ' ' +
            '--js ' + file + ' ' +
            '--js_output_file ' + minFile,
      function(code) {
        if (code !== 0) grunt.fail.warn('Error minifying ' + file);

        // closure creates the source map relative to build/ folder, we need to strip those references
        grunt.file.write(mapFile, grunt.file.read(mapFile).replace('"file":"build/', '"file":"').
                                                           replace('"sources":["build/','"sources":["'));

        // move add use strict into the closure + add source map pragma
        grunt.file.write(minFile, this.sourceMap(mapFileName, this.singleStrict(grunt.file.read(minFile), '\n')));
        grunt.log.ok(file + ' minified into ' + minFile);
        done();
    }.bind(this));
  },

  memoryRequirement: function() {
    return (process.platform === 'win32') ? '' : '-Xmx2g';
  },


  //returns the 32-bit mode force flags for java compiler if supported, this makes the build much faster
  java32flags: function() {
    if (process.platform === 'win32') return '';
    if (shell.exec('java -d32 -version 2>&1', {silent: true}).code !== 0) return '';
    return ' -d32 -client';
  },


  //collects and combines error messages stripped out in minify step
  collectErrors: function() {
    var combined = {
      id: 'ng',
      generated: new Date().toString(),
      errors: {}
    };
    grunt.file.expand('build/*-errors.json').forEach(function(file) {
      var errors = grunt.file.readJSON(file),
        namespace;
      Object.keys(errors).forEach(function(prop) {
        if (typeof errors[prop] === 'object') {
          namespace = errors[prop];
          if (combined.errors[prop]) {
            Object.keys(namespace).forEach(function(code) {
              if (combined.errors[prop][code] && combined.errors[prop][code] !== namespace[code]) {
                grunt.warn('[collect-errors] Duplicate minErr codes don\'t match!');
              } else {
                combined.errors[prop][code] = namespace[code];
              }
            });
          } else {
            combined.errors[prop] = namespace;
          }
        } else {
          if (combined.errors[prop] && combined.errors[prop] !== errors[prop]) {
            grunt.warn('[collect-errors] Duplicate minErr codes don\'t match!');
          } else {
            combined.errors[prop] = errors[prop];
          }
        }
      });
    });
    grunt.file.write('build/errors.json', JSON.stringify(combined));
    grunt.file.expand('build/*-errors.json').forEach(grunt.file.delete);
  },


  //csp connect middleware
  conditionalCsp: function() {
    return function(req, res, next) {
      var CSP = /\.csp\W/;

      if (CSP.test(req.url)) {
        res.setHeader('X-WebKit-CSP', 'default-src \'self\';');
        res.setHeader('X-Content-Security-Policy', 'default-src \'self\'');
        res.setHeader('Content-Security-Policy', 'default-src \'self\'');
      }
      next();
    };
  },


  //rewrite connect middleware
  rewrite: function() {
    return function(req, res, next) {
      var REWRITE = /\/(guide|api|cookbook|misc|tutorial|error).*$/,
          IGNORED = /(\.(css|js|png|jpg|gif|svg)$|partials\/.*\.html$)/,
          match;

      if (!IGNORED.test(req.url) && (match = req.url.match(REWRITE))) {
        console.log('rewriting', req.url);
        req.url = req.url.replace(match[0], '/index.html');
      }
      next();
    };
  },

  // Our Firebase projects are in subfolders, but Travis expects them in the root,
  // so we need to modify the upload folder path and copy the file into the root
  firebaseDocsJsonForTravis: function() {
    var docsScriptFolder = 'scripts/docs.angularjs.org-firebase';

    var fileName = docsScriptFolder + '/firebase.json';
    var json = grunt.file.readJSON(fileName);

    (json.hosting || (json.hosting = {})).public = 'deploy/docs';
    (json.functions || (json.functions = {})).source = docsScriptFolder + '/functions';

    grunt.file.write('firebase.json', JSON.stringify(json));
  }
};
