import most from "most";
import escodegen from "escodegen";
import _ from "lodash";

import * as Pluggable from "../pluggable";
import { CONTINUE } from "../pluggable";
import { constructBundle } from "./construct";
import compileModules from "./modules/compile";
import bootstrapBundles from "./bundles/bootstrap";
import hashBundle from "./bundles/hash";
import interpolateFilename from "./bundles/interpolate-filename";
import dedupeExplicit from "./bundles/dedupe-explicit";
import dedupeImplicit from "./bundles/dedupe-implicit";


export const bootstrapCompilation = Pluggable.promise(function bootstrapCompilation (opts) {
  return {
    cache: {
      modulesByAbsPath: Object.create(null)
    },
    opts: Object.freeze(opts)
  };
});

export const getModuleMaps = Pluggable.promise(function getModuleMaps (seedModules) {
  return this.compileModules(seedModules)
    .reduce((moduleMaps, module) => {
      moduleMaps.byHash[module.hash] = module;
      moduleMaps.byAbsPath[module.path] = module;
      return moduleMaps;
    }, {
      byHash: {},
      byAbsPath: {}
    });
}, { compileModules });

export const getBundles = Pluggable.promise(function getBundles (bootstrappedBundles, moduleMapsP) {
  const explicitDedupedBundlesP = moduleMapsP
    .then(moduleMaps => this.dedupeExplicit(bootstrappedBundles, moduleMaps.byAbsPath));

  return most
    .fromPromise(Promise.all([moduleMapsP, explicitDedupedBundlesP]))
    .take(1)
    // TODO: The below flapMap method is being invoked twice, even with the take(1) above.
    // All subsequent methods in the chain fire 2x what they should. Identify the reason why
    // this is happening.
    .flatMap(resolved => {
      const [{byHash}, explicitBundles] = resolved;
      return this.dedupeImplicit(explicitBundles)
        // Populate bundles with module objects.
        .map(bundle => Object.assign({}, bundle, {
          modules: bundle.moduleHashes.map(hash => byHash[hash])
        }));
    })
    .map(bundle => this.hashBundle(bundle)
      .then(hash => Object.assign({}, bundle, { hash })))
    .await()
    .map(this.interpolateFilename)
    .await();

}, { dedupeExplicit, dedupeImplicit, hashBundle, interpolateFilename });

export const getUrls = Pluggable.promise(function getUrls (bundles) {
  return bundles.reduce((urls, bundle) => {
    bundle.moduleHashes.forEach(hash => urls[hash] = bundle.dest);
    return urls;
  }, {});
});

export const emitRawBundles = Pluggable.stream(function emitRawBundles (bundles, urls) {
  return bundles
    .map(bundle => constructBundle({
      modules: bundle.modules,
      includeRuntime: bundle.includeRuntime,
      urls: bundle.entry ? urls : null,
      entryModuleHash: bundle.entryModuleHash || null,
      cacheMode: this.opts.cacheMode
    }).then(bundleAst => escodegen.generate(bundleAst, {
      format: { indent: { style: "  " } },
      sourceMap: !!this.opts.sourceMaps,
      sourceMapWithCode: true,
      comment: !!this.opts.includeComments
    })).then(genBundle => [bundle, genBundle]))
    .flatMap(most.fromPromise)
    .flatMap(([bundle, {code, map}]) => {
      const outputBundle = Object.assign({}, bundle, { raw: code });
      const mapDest = bundle.dest + ".map";
      return this.opts.sourceMaps === true ?
        most.from([outputBundle, { raw: map, dest: mapDest }]) :
        most.of(outputBundle);
    });
});

/**
 * Given a stream of bundles, reduces those bundles down into a promise that
 * resolves into a final compilation object.
 *
 * This compilation object will have three key/value pairs:
 *
 * - cache:    populated cache from the compilation
 * - bundles:  a mapping of destination paths to `raw` code
 * - opts:     the original options passed to the compilation)
 *
 * @param  {Stream} bundles   Bundles generated by [getBundles](#getbundles).
 *
 * @return {Promise}          Compilation object.
 */
export const buildOutput = Pluggable.promise(function buildOutput (bundles) {
  return this.getUrls(bundles).then(urls => {
    return this.emitRawBundles(bundles, urls)
      .reduce((output, bundle) => {
        output.bundles[bundle.dest] = bundle;
        return output;
      }, {
        cache: this.cache,
        bundles: {},
        opts: this.opts
      });
  });
}, { getUrls, emitRawBundles });

/**
 * Performs and end-to-end compilation.  Its return value is a promise
 * that will resolve to the output of [buildOutput](#buildoutput).
 *
 * @return {Promise}  compilation      Resolves to the compilation output.
 */
const compile = Pluggable.promise(function compile () {
  const bootstrappedBundles = this.bootstrapBundles(this.opts.entry, this.opts.split);
  const seedModules = bootstrappedBundles.map(bundle => bundle.module);
  const moduleMapsPromise = this.getModuleMaps(seedModules);
  return this.getBundles(bootstrappedBundles, moduleMapsPromise)
    .then(bundles => {
      return this.buildOutput(bundles);
    });
}, { bootstrapBundles, getModuleMaps, getBundles, buildOutput });

function addPluginsToContext (compilationContext) {
  compilationContext.__pluggables__ = { override: {}, transform: {} };
  const overrides = compilationContext.__pluggables__.override;
  const transforms = compilationContext.__pluggables__.transform;

  (compilationContext.opts.plugins || []).forEach(plugin => {
    function override (pluggableFnName, overrideFn) {
      overrides[pluggableFnName] = (overrides[pluggableFnName] || []).concat(overrideFn);
    }
    function transform (pluggableFnName, transformFn) {
      transforms[pluggableFnName] = (transforms[pluggableFnName] || []).concat(transformFn);
    }

    _.extend(override, { CONTINUE });
    plugin(override, transform);
  });
}

export default function (opts) {
  return bootstrapCompilation(opts)
    .then(compilationContext => {
      addPluginsToContext(compilationContext);
      return compile.call(compilationContext);
    });
}

@@ -2731,46 +2731,8 @@
 null
-,%0A      cacheMode: this.opts.cacheMode
 %0A   

Remove defunct cacheMode config.