var _ = require('lodash');
var path = require('path');
var resolve = require('resolve');
var glob = require('glob');

module.exports = GruntBrowserifyRunner;

function GruntBrowserifyRunner(options) {
  this.browserify = options.browserify || require('browserify');
  this.watchify = options.watchify || require('watchify');
  this.logger = options.logger;
  this.writer = options.writer;
}

GruntBrowserifyRunner.prototype = _.create(GruntBrowserifyRunner.prototype, {
  run: function (files, destination, options, next) {
    var self = this;

    //determine watchify or browserify
    var engine = options.watch? this.watchify: this.browserify;

    //set constructor options and instantiate
    var bOpts = options.browserifyOptions || {};
    bOpts.entries = files;
    var b = engine(bOpts);

    b.on('error', function (err) {
      self.logger.fail.warn(err);
    });


    //define default bundle options
    var ignoredAliases = [];
    options.bundleOptions = (options.bundleOptions || {});
    options.bundleOptions.filter = function (requireId) {
      return (ignoredAliases.indexOf(requireId) < 0);
    };


    if (options.alias) {
      _.forEach(options.alias, function (aliasPair) {
        if (!aliasPair.match(':')) {
          self.logger.fail.warn('Aliases must be split with a colon');
        }
        else {
          //first, try the module path as it is since
          //it may be a node module name
          var modulePath;
          aliasPair = aliasPair.split(':');
          try {
            modulePath = resolve.sync(aliasPair[0]);
          }
          catch (e) {
            //if resolution fails, the path is probably not
            //a node module so resolve the full module path
            modulePath = resolve.sync(path.resolve(aliasPair[0]));
          }

          b.require(resolve.sync(modulePath), {expose: aliasPair[1]});
        }
      });

    }

    if (options.require) {
      _.forEach(options.require, function (file) {
        b.require(file);
      });
    }

    if (options.exclude) {
      _.forEach(options.exclude, function (file) {
        runOptionForGlob(b, 'exclude', file);
      });
    }

    if (options.ignore) {
      _.forEach(options.ignore, function (file) {
        runOptionForGlob(b, 'ignore', file);
      });
    }

    if (options.external) {
      _.forEach(options.external, function (id) {
        //allow externalizing of alias lists
        if (id.match(':')) {
          id = id.split(':')[1];
        }

        if (/\*/.test(id)) {
          runOptionForGlob(b, 'external', id);
        }
        else {
          try {
            require.resolve(id);
          }
          catch (e) {
            //it's an arbitary alias that can't be resolved,
            //add it to the ignore list
            ignoredAliases.push(id);
          }
          b.external(id);
        }
      });
    }

    if (options.transform) {
      _.forEach(options.transform, function (transformer) {
        if (typeof transformer !== 'object') {
          b.transform(transformer);
        }
        else {
          b.transform(transformer[1], transformer[0]);
        }
      });
    }

    if (options.plugin) {
      _.forEach(options.plugin, function (plugin) {
        if (typeof plugin !== 'object') {
          b.plugin(plugin);
        }
        else {
          b.plugin(plugin[0], plugin[1]);
        }
      });
    }


    var destPath = this.createDestDir(destination);
    var keepAlive = this.keepAliveFn.bind(this, options);
    var done = options.keepAlive? keepAlive : next;
    var bundleComplete = this.onBundleComplete(destination, done);

    if (options.watch) {
      var bundleUpdate = this.onBundleComplete(destination, keepAlive, {allowErrors: true});
      b.on('update', function (ids) {
        ids.forEach(function (id) {
          self.logger.log.ok(id + ' changed, updating browserify bundle.');
        });
        doBundle(b, options, bundleUpdate);
      });
    }

    doBundle(b, options, bundleComplete);
  },

  createDestDir: function (destination) {
    var destPath = path.dirname(path.resolve(destination));
    if (!this.writer.exists(destPath)) {
      this.writer.mkdir(destPath);
    }
    return destPath;
  },

  keepAliveFn: function (options) {
    this.logger.log.ok('Watchifying...');
  },

  onBundleComplete: function (destination, next, options) {
    var self = this;
    return function (err, src) {
      if (err) {
        self.logger.log.error(err);
        if (!options || !options.allowErrors) {
          self.logger.fail.warn('Error running grunt-browserify.');
        }
      }

      self.writer.write(destination, src + ';');
      next();
    };
  }
});

function doBundle(browserifyInstance, opts, bundleComplete) {
  if (opts.preBundleCB) {
    opts.preBundleCB(browserifyInstance);
  }

  browserifyInstance.bundle((opts.bundleOptions || {}), function (err, src) {
    if (opts.postBundleCB) {
      opts.postBundleCB(err, src, bundleComplete);
    }
    else {
      bundleComplete(err, src);
    }
  });
}

function runOptionForGlob(browserifyInstance, method, pattern) {
  var files = glob.sync(pattern);
  if (!files || files.length < 1) {
    //it's not a glob, it's a file path
    files = [pattern];
  }
  files.forEach(function (f) {
    browserifyInstance[method].call(browserifyInstance, path.resolve(f));
  });
}

@@ -4660,14 +4660,8 @@
  src
- + ';'
 );%0A 

Fix #185: Get rid of semicolon