var async = require('async');
var HappyThreadPool = require('./HappyThreadPool');
var HappyForegroundThreadPool = require('./HappyForegroundThreadPool');
var WebpackUtils = require('./WebpackUtils');
var OptionParser = require('./OptionParser');
var JSONSerializer = require('./JSONSerializer');
var SourceMapSerializer = require('./SourceMapSerializer');
var fnOnce = require('./fnOnce');
var ErrorSerializer = require('./ErrorSerializer');
var pick = require('./pick');
var pkg = require('../package.json');

function HappyPlugin(userConfig) {
  if (!(this instanceof HappyPlugin)) {
    return new HappyPlugin(userConfig);
  }

  this.name = 'HappyPack';
  this.state = {
    loaders: [],
    baseLoaderRequest: '',
    foregroundThreadPool: null,
    verbose: false,
    debug: false,
  };

  this.config = OptionParser(userConfig, {
    id:                       { type: 'string', default: '1' },
    compilerId:               { type: 'string', default: 'default' },
    tempDir:                  { deprecated: true },
    threads:                  { type: 'number', default: 3 },
    threadPool:               { type: 'object', default: null },
    cache:                    { deprecated: true },
    cachePath:                { deprecated: true },
    cacheContext:             { deprecated: true },
    cacheSignatureGenerator:  { deprecated: true },
    verbose:                  { type: 'boolean', default: true },
    verboseWhenProfiling:     { type: 'boolean', default: false },
    debug:                    { type: 'boolean', default: process.env.DEBUG === '1' },
    enabled:                  { deprecated: true },
    // we don't want this to be documented / exposed since it's an
    // implementation detail + not having it on means a bug, but we're making it
    // configurable for testing purposes
    bufferedMessaging:        { type: 'boolean', default: process.platform === 'win32' },
    loaders:                  {
      alias: 'use',
      validate: value => {
        if (!Array.isArray(value)) {
          return 'Loaders must be an array!';
        }
        else if (value.length === 0) {
          return 'You must specify at least one loader!';
        }
        else if (value.some(function(loader) {
          return typeof loader !== 'string' && !loader.path && !loader.loader;
        })) {
          return 'Loader must have a @path or @loader property or be a string.'
        }
      }
    }
  }, "HappyPack");

  this.id = this.config.id;

  return this;
}

HappyPlugin.prototype.apply = function(compiler) {
  var that, engageWatchMode;

  that = this;

  this.state.verbose = isVerbose(compiler, this);
  this.state.debug = isDebug(compiler, this);

  this.threadPool = this.config.threadPool || HappyThreadPool({
    id: this.id,
    size: this.config.threads,
    verbose: this.state.verbose,
    debug: this.state.debug,
    bufferedMessaging: this.config.bufferedMessaging,
  });

  engageWatchMode = fnOnce(function() {
    // Once the initial build has completed, we create a foreground worker and
    // perform all compilations in this thread instead:
    WebpackUtils.tapInto(compiler, 'done', function() {
      that.state.foregroundThreadPool = HappyForegroundThreadPool({
        loaders: that.state.loaders,
      });

      that.state.foregroundThreadPool.start(that.config.compilerId, compiler, '{}', Function.prototype);
    });
  });

  WebpackUtils.tapAsyncInto(compiler, 'watch-run', function(_, done) {
    if (engageWatchMode() === fnOnce.ALREADY_CALLED) {
      done();
    }
    else {
      that.start(compiler, done);
    }
  });

  WebpackUtils.tapAsyncInto(compiler, 'run', that.start.bind(that));

  // cleanup hooks:
  WebpackUtils.tapInto(compiler, 'done', that.stop.bind(that));

  if (compiler.options.bail) {
    WebpackUtils.tapInto(compiler, 'compilation', function(compilation) {
      WebpackUtils.tapInto(compilation, 'failed-module', that.stop.bind(that));
    });
  }
};

HappyPlugin.prototype.start = function(compiler, done) {
  var that = this;

  if (that.state.verbose) {
    console.log('Happy[%s]: Version: %s. Threads: %d%s',
      that.id, pkg.version,
      that.threadPool.size,
      that.config.threadPool ? ' (shared pool)' : ''
    );
  }

  async.series([
    function resolveLoaders(callback) {
      var loaderSpecs = that.config.loaders || that.config.use;
      var normalLoaders = loaderSpecs.reduce(function(list, entry) {
        return list.concat(WebpackUtils.normalizeLoader(entry));
      }, []);

      var loaderPaths = normalLoaders.map(function(loader) {
        return loader.path;
      });

      WebpackUtils.resolveLoaders(compiler, loaderPaths, function(err, resolvedPaths) {
        if (err) return callback(err);

        var withResolvedPaths = normalLoaders.map(function(loader, index) {
          var resolvedPath = resolvedPaths[index];

          return Object.assign({}, loader, {
            path: resolvedPath,
            request: loader.query ? (loader.path + loader.query) : loader.path
          })
        })

        that.state.loaders = withResolvedPaths;
        that.state.baseLoaderRequest = withResolvedPaths.map(function(loader) {
          return loader.path + (loader.query || '');
        }).join('!');

        callback();
      });
    },

    function launchAndConfigureThreads(callback) {
      var serializedOptions;
      var compilerOptions = pick(HappyPlugin.SERIALIZABLE_OPTIONS)(compiler.options);

      try {
        serializedOptions = JSONSerializer.serialize(compilerOptions);
      }
      catch(e) {
        console.error('Happy[%s]: Internal error; unable to serialize options!!!', that.id);
        console.error(compilerOptions);

        return callback(e);
      }

      that.threadPool.start(that.config.compilerId, compiler, serializedOptions, callback);
    },

    function announceReadiness(callback) {
      if (that.state.verbose) {
        console.log('Happy[%s]: All set; signaling webpack to proceed.', that.id);
      }

      callback();
    }
  ], done);
};

HappyPlugin.prototype.stop = function() {
  this.threadPool.stop(this.config.compilerId);
};

HappyPlugin.prototype.compile = function(loader, loaderContext, done) {
  var threadPool = this.state.foregroundThreadPool || this.threadPool;

  threadPool.compile(loaderContext.remoteLoaderId, loader, {
    loaders: this.state.loaders,
    loaderContext: loaderContext,
  }, function(err, result) {
    if (err) {
      done(ErrorSerializer.deserialize(err));
    }
    else {
      done(null,
        result.compiledSource || '',
        SourceMapSerializer.deserialize(result.compiledMap)
      );
    }
  });
};

HappyPlugin.prototype.generateRequest = function(resource) {
  return this.state.baseLoaderRequest + '!' + resource;
};

// export this so that users get to override if needed
HappyPlugin.SERIALIZABLE_OPTIONS = [
  'amd',
  'bail',
  'cache',
  'context',
  'entry',
  'externals',
  'debug',
  'devtool',
  'devServer',
  'loader',
  'module',
  'node',
  'output',
  'profile',
  'recordsPath',
  'recordsInputPath',
  'recordsOutputPath',
  'resolve',
  'resolveLoader',
  'target',
  'watch',
];

// convenience accessor to relieve people from requiring the file directly:
HappyPlugin.ThreadPool = HappyThreadPool;

function isVerbose(compiler, plugin) {
  return plugin.config.verbose && (
    !compiler.options.profile ||
    plugin.config.verboseWhenProfiling
  );
};

function isDebug(compiler, plugin) {
  return plugin.config.debug && (
    !compiler.options.profile ||
    plugin.config.verboseWhenProfiling
  );
};

module.exports = HappyPlugin;
