var CachingWriter = require('broccoli-caching-writer');
var sriToolbox = require('sri-toolbox');
var fs = require('fs');
var crypto = require('crypto');
var symlinkOrCopy = require('symlink-or-copy').sync;
var Promise = require('rsvp').Promise; // node 0.10
var path = require('path');

var STYLE_CHECK = /\srel=["\'][^"]*stylesheet[^"]*["\']/;
var SRC_CHECK = /\ssrc=["\']([^"\']+)["\']/;
var HREF_CHECK = /\shref=["\']([^"\']+)["\']/;
var BASE_CHECK = new RegExp('<base[^>]*href=["\']([^"]*)["\'][^>]*>', 'g');
var SCRIPT_CHECK = new RegExp('<script[^>]*src=["\']([^"]*)["\'][^>]*>', 'g');
var LINT_CHECK = new RegExp('<link[^>]*href=["\']([^"]*)["\'][^>]*>', 'g');
var INTEGRITY_CHECK = new RegExp('integrity=["\']');
var CROSS_ORIGIN_CHECK = new RegExp('crossorigin=["\']([^"\']+)["\']');
var MD5_CHECK = /^(.*)[-]([a-z0-9]{32})([.].*)$/;
var mkdirp = require('mkdirp');

function SRIHashAssets(inputNodes, options) {
  if (!(this instanceof SRIHashAssets)) {
    return new SRIHashAssets(inputNodes, options);
  }

  this.options = options || {};
  this.context = this.options.context || {};
  var nodes = inputNodes;
  if (!Array.isArray(nodes)) {
    nodes = [nodes];
  }

  CachingWriter.call(this, nodes, {
    // disabled to ensure all files are synced forward
    // I suspect additions to BCW are needed, or a slightly different plugin
    // to handle this more elegantly.
    // Leaving this comment here as a reminder. -sp
    //
    // cacheInclude: [
    //   /\.html$/,
    //   /\.js$/,
    //   /\.css$/
    // ]
  });

  this.options.paranoiaCheck = this.options.paranoiaCheck || true;

  if ('origin' in this.options) {
    if ('prefix' in this.options && !('crossorigin' in this.options)) {
      if (this.options.prefix.indexOf(this.options.origin, 0) === 0) {
        this.options.crossorigin = false;
      }
    }
  }
}

SRIHashAssets.prototype = Object.create(CachingWriter.prototype);
SRIHashAssets.prototype.constructor = SRIHashAssets;

SRIHashAssets.prototype.getBaseHREF = function getBaseHREF(string) {
  var baseTag = string.match(BASE_CHECK);
  if (baseTag && baseTag[0]) {
    var href = baseTag[0].match(HREF_CHECK);
    var relativePath = href && href[1];

    if (!relativePath) { return null; }

    // do not support `<base href="../../">`
    if (!relativePath[0] === '/') { return null; }

    return this.inputPaths[0] + relativePath;
  }

  return null;
};

SRIHashAssets.prototype.addSRI = function addSRI(string, srcDir) {
  var plugin = this;
  var base = this.getBaseHREF(string);

  return string.replace(SCRIPT_CHECK, function srcMatch(match) {

    var src = match.match(SRC_CHECK);
    var filePath;

    if (!src) {
      return match;
    }

    filePath = src[1];

    return plugin.mungeOutput(match, filePath, base || srcDir);
  }).replace(LINT_CHECK, function hrefMatch(match) {
    var href = match.match(HREF_CHECK);
    var isStyle = STYLE_CHECK.test(match);
    var filePath;

    if (!isStyle || !href) {
      return match;
    }

    filePath = href[1];

    return plugin.mungeOutput(match, filePath, base || srcDir);
  });
};

SRIHashAssets.prototype.readFile = function readFile(dirname, file) {
  var assetSource;

  try {
    assetSource = fs.readFileSync(dirname + '/' + file).toString();
  } catch(e) {
    return null;
  }

  return assetSource;
};

/*
  This check is always called (at the moment).
  If 'paranoiaCheck' is enabled then it will check a file only contains ASCII characters
    - will return true if paranoiaCheck is disabled
    - will return true if ASCII only
    - will return false if non ASCII chars are present
  This relates to an issue that is either within OpenSSL or Chrome itself due to an encoding issue:
   https://code.google.com/p/chromium/issues/detail?id=527286
*/
SRIHashAssets.prototype.paranoiaCheck = function paranoiaCheck(assetSource) {
  var i;
  var checkResult = true;

  if (this.options.paranoiaCheck === true) {
    for (i = 0; i < assetSource.length; i++) {
      if (assetSource.charCodeAt(i) > 127) {
        checkResult = false;
        break;
      }
    }
  }

  return checkResult;
};

SRIHashAssets.prototype.generateIntegrity = function generateIntegrity(output, file, dirname, external) {
  var assetSource = this.readFile(dirname, file);
  var selfCloseCheck = /\s*\/>$/;
  var integrity;
  var append;
  var outputWithIntegrity;

  if (assetSource === null) {
    return output;
  }

  if (this.paranoiaCheck(assetSource) === false) {
    return output;
  }


  integrity = sriToolbox.generate({
    algorithms: ['sha256', 'sha512']
  }, assetSource);

  append = ' integrity="' + integrity + '"';

  if (external && this.options.crossorigin) {
    if (!CROSS_ORIGIN_CHECK.test(output)) {
      append = append + ' crossorigin="' + this.options.crossorigin + '" ';
    }
  }

  if (selfCloseCheck.test(output)) {
    outputWithIntegrity = output.replace(selfCloseCheck, append + ' />');
  } else {
    outputWithIntegrity = output.replace(/\s*[>]$/, append + ' >');
  }
  return outputWithIntegrity;
};

SRIHashAssets.prototype.checkExternal = function checkExternal(output, file, dirname) {
  var md5Matches = file.match(MD5_CHECK);
  var md5sum = crypto.createHash('md5');
  var assetSource;
  var filePath;

  if (!('prefix' in this.options) || !('crossorigin' in this.options) || md5Matches === null) {
    return output;
  }

  filePath = file.replace(this.options.prefix, '');

  if (filePath === file) {
    return output;
  }

  assetSource = this.readFile(dirname, filePath);
  if (assetSource === null) {
    filePath = md5Matches[1].replace(this.options.prefix, '') + md5Matches[3];
    assetSource = this.readFile(dirname, filePath);
    if (assetSource === null) {
      return output;
    }
  }

  md5sum.update(assetSource);
  if (md5Matches[2] === md5sum.digest('hex')) {
    return this.generateIntegrity(output, filePath, dirname, true);
  }
  return output;
};

SRIHashAssets.prototype.mungeOutput = function mungeOutput(output, filePath, srcDir) {
  var newOutput = output;

  if (/^https?:\/\//.test(filePath)) {
    return this.checkExternal(output, filePath, srcDir);
  }

  if (!INTEGRITY_CHECK.test(output)) {
    newOutput = this.generateIntegrity(output, filePath, srcDir);
  }

  return newOutput;
};

SRIHashAssets.prototype.processHTMLFile = function processFile(entry) {
  var srcDir = path.dirname(entry.fullPath);
  var fileContent = this.addSRI(fs.readFileSync(entry.fullPath,'UTF-8'), srcDir);
  var fullPath = this.outputPath + '/' + entry.relativePath;

  mkdirp.sync(path.dirname(fullPath));

  fs.writeFileSync(fullPath, fileContent);
};

SRIHashAssets.prototype.processOtherFile = function(entry) {
  var fullPath = this.outputPath + '/' + entry.relativePath;
  mkdirp.sync(path.dirname(fullPath));
  symlinkOrCopy(entry.fullPath, fullPath);
};

SRIHashAssets.prototype.build = function () {
  var html = [];
  var other = [];

  this.listEntries().forEach(function(entry) {
    if (/\.html$/.test(entry.relativePath)) {
      html.push(entry);
    } else {
      other.push(entry);
    }
  });

  return Promise.all([
    Promise.all(html.map(this.processHTMLFile.bind(this))),
    Promise.all(other.map(this.processOtherFile.bind(this)))
  ]);
};

module.exports = SRIHashAssets;
