'use strict';

var Plugin = require('broccoli-caching-writer');
var walk = require('walk-sync');
var path = require('path');
var fs = require('fs-extra');

var DEFAULT_SUPPORTED_TYPES = [ 'js', 'css' ];

/**
 * A Broccoli plugin to generate an asset manifest from a given input tree.
 * You can also provide a second tree that is a tree containing an existing
 * `asset-manifest.json` file to merge with.
 *
 * @class AssetManifestGenerator
 * @extends BroccoliCachingWriter
 */
function AssetManifestGenerator(inputTrees, options) {
  options = options || {};

  this.prepend = options.prepend || '';
  this.filesToIgnore = options.filesToIgnore || [];
  this.supportedTypes = options.supportedTypes || DEFAULT_SUPPORTED_TYPES;
  this.generateURI = options.generateURI || function generateURI(filePath) {
    return filePath;
  };

  Plugin.call(this, inputTrees, {
    annotation: options.annotation
  });
}

AssetManifestGenerator.prototype = Object.create(Plugin.prototype);
AssetManifestGenerator.prototype.constructor = AssetManifestGenerator;

/**
 * Generate an asset manifest from an inputTree assuming it has the following
 * convention for structure:
 *
 *   root/
 *     <bundle-name>/
 *       dependencies.manifest.json
 *       <asset>.<type>
 *       assets/
 *         <asset>.<type>
 *
 * This means that for each entry in the inputTree:
 *
 *   1. If the entry is a root-level directory, create a new bundle
 *   2. If the entry is an asset named "dependencies.manifest.json", add its
 *      contents as the "dependencies" for the bundle
 *   3. If the entry is an asset within a bundle, add it to the bundles "assets"
 *   4. Otherwise, do nothing
 *
 * One important note is that bundles should be flattened in the inputTree. If
 * a directory is found within a bundle, it will not be treated as an additional
 * bundle.
 */
AssetManifestGenerator.prototype.build = function() {
  var supportedTypes = this.supportedTypes;
  var generateURI = this.generateURI;
  var prepend = this.prepend;
  var filesToIgnore = this.filesToIgnore;
  var inputPath = this.inputPaths[0];
  var existingManifestPath = this.inputPaths[1];
  var existingManifest;

  // Check to see if we have an exisiting manifest. If not, than use a new
  // empty manifest.
  try {
    existingManifest = fs.readJsonSync(path.join(existingManifestPath, 'asset-manifest.json'))
  } catch (err) {
    existingManifest = { bundles: {} };
  }

  var manifest = walk(inputPath).reduce(function(manifest, entry) {
    var bundleInfo = bundleInfoFromAssetEntry(entry);
    var bundleName = bundleInfo.bundleName;
    var assetName = bundleInfo.assetName;
    var assetType = bundleInfo.assetType;

    if (!bundleInfo.isValid) {
      return manifest;
    }

    if (bundleInfo.isNewBundle) {
      if (manifest.bundles[bundleName]) {
        throw new Error('Attempting to add bundle "' + bundleName + '" to manifest but a bundle with that name already exists.');
      }

      manifest.bundles[bundleName] = {
        assets: []
      };
    }

    // If the asset is named "dependencies.manifest.json" then we should read
    // the json in and set it as "dependencies" on the corresponding bundle.
    else if (assetName === 'dependencies.manifest.json') {
      var dependencies = fs.readJsonSync(path.join(inputPath, entry));
      manifest.bundles[bundleName].dependencies = dependencies;
    }

    // If the asset is in a bundle, then attempt to add it to the manifest by
    // checking if it is a supported type.
    else if (supportedTypes.indexOf(assetType) !== -1 && !stringOrRegexMatch(filesToIgnore, entry)) {

      // only add non-empty assets
      let fullPath = path.join(inputPath, entry);
      let contents = fs.readFileSync(fullPath, 'utf-8');

      if (contents.trim() !== '') {
        manifest.bundles[bundleName].assets.push({
          uri: generateURI(path.posix.join(prepend, entry)),
          type: assetType
        });
      }
    }

    return manifest;
  }, existingManifest);

  var manifestFile = path.join(this.outputPath, 'asset-manifest.json');

  fs.writeJsonSync(manifestFile, manifest, { spaces: 2 });
};

function stringOrRegexMatch(matchers, value) {
  for (var i = 0; i < matchers.length; i++) {
    var matcher = matchers[i];

    if (typeof matcher === 'string' && matcher === value) {
        return true;
    } else if (matcher instanceof RegExp && matcher.test(value)) {
      return true;
    }
  }

  return false;
}

/**
 * @typedef {Object} BundleInfo
 * @property {boolean} isValid - true if we have a valid new bundle name or a bundle and asset name
 * @property {boolean} isNewBundle - true if this entry is just a bundle name
 * @property {string} bundleName
 * @property {string=} assetName
 * @property {string=} assetType - asset extension
 *
 * @private
 * @param {string} entry
 * @returns {BundleInfo}
 */
function bundleInfoFromAssetEntry(entry) {
  var pathParts = entry.split('/').filter(Boolean);

  var bundleName = pathParts.shift();
  // If the bundle name starts with a "@", then we have a scoped package[1]
  // so we want to grab the next path part to complete the bundle name.
  // [1]:https://docs.npmjs.com/misc/scope
  if (bundleName && bundleName[0] === '@') {
    if (!pathParts.length) {
      return { isValid: false };
    }
    bundleName = [bundleName, pathParts.shift()].join('/');
  }
  var assetName = pathParts.pop();

  // If there is no assetName, then we have a directory per the rules of
  // walk-sync. And, if there are no pathParts left we have a root directory.
  // https://github.com/joliss/node-walk-sync/blob/master/README.md#usage
  var isNewBundle = !assetName && pathParts.length === 0;

  // Find the file extension and strip off the leading '.'
  var assetType;
  if (assetName) {
    assetType = path.extname(assetName).replace(/^\./, '') || undefined;
  }

  return {
    isValid: isNewBundle || Boolean(bundleName && assetType),
    isNewBundle,
    bundleName,
    assetName,
    assetType
  };
}

module.exports = AssetManifestGenerator;

// export for testing
module.exports._bundleInfoFromAssetEntry = bundleInfoFromAssetEntry;

@@ -2196,17 +2196,16 @@
  an exis
-i
 ting man

Correct the spelling of "existing".