const debug = require("debug")("workflow-compile");
const mkdirp = require("mkdirp");
const { callbackify, promisify } = require("util");
const Config = require("truffle-config");
const solcCompile = require("truffle-compile");
const vyperCompile = require("truffle-compile-vyper");
const externalCompile = require("truffle-external-compile");
const expect = require("truffle-expect");
const Resolver = require("truffle-resolver");
const Artifactor = require("truffle-artifactor");
const OS = require("os");

const SUPPORTED_COMPILERS = {
  solc: solcCompile,
  vyper: vyperCompile,
  external: externalCompile
};

function prepareConfig(options) {
  expect.options(options, ["contracts_build_directory"]);

  expect.one(options, ["contracts_directory", "files"]);

  // Use a config object to ensure we get the default sources.
  const config = Config.default().merge(options);

  config.compilersInfo = {};

  if (!config.resolver) config.resolver = new Resolver(config);

  if (!config.artifactor) {
    config.artifactor = new Artifactor(config.contracts_build_directory);
  }

  return config;
}

function multiPromisify(func) {
  return (...args) =>
    new Promise((accept, reject) => {
      const callback = (err, ...results) => {
        if (err) reject(err);

        accept(results);
      };

      func(...args, callback);
    });
}

var Contracts = {
  // contracts_directory: String. Directory where .sol files can be found.
  // contracts_build_directory: String. Directory where .sol.js files can be found and written to.
  // all: Boolean. Compile all sources found. Defaults to true. If false, will compare sources against built files
  //      in the build directory to see what needs to be compiled.
  // network_id: network id to link saved contract artifacts.
  // quiet: Boolean. Suppress output. Defaults to false.
  // strict: Boolean. Return compiler warnings as errors. Defaults to false.
  compile: callbackify(async function(options) {
    const config = prepareConfig(options);

    const compilers = config.compiler
      ? [config.compiler]
      : Object.keys(config.compilers);

    this.reportCompilationStarted(options);

    // convert to promise to compile+write
    const compilations = await this.compileSources(config, compilers);

    const numberOfCompiledContracts = compilations.reduce(
      (number, compilation) => {
        return number + Object.keys(compilation.contracts).length;
      },
      0
    );
    if (numberOfCompiledContracts === 0) {
      return this.reportNothingToCompile(options);
    }

    const collect = async compilations => {
      let result = { outputs: {}, contracts: {} };

      for (let compilation of await Promise.all(compilations)) {
        let { compiler, output, contracts } = compilation;

        result.outputs[compiler] = output;

        for (let [name, abstraction] of Object.entries(contracts)) {
          result.contracts[name] = abstraction;
        }
      }

      return result;
    };

    this.reportCompilationFinished(options, config);
    return await collect(compilations);
  }),

  compileSources: async function(config, compilers) {
    return Promise.all(
      compilers.map(async compiler => {
        const compile = SUPPORTED_COMPILERS[compiler];
        if (!compile) throw new Error("Unsupported compiler: " + compiler);

        const compileFunc =
          config.all === true || config.compileAll === true
            ? compile.all
            : compile.necessary;

        let [contracts, output, compilerUsed] = await multiPromisify(
          compileFunc
        )(config);

        if (compilerUsed) {
          config.compilersInfo[compilerUsed.name] = {
            version: compilerUsed.version
          };
        }

        if (contracts && Object.keys(contracts).length > 0) {
          await this.writeContracts(contracts, config);
        }

        return { compiler, contracts, output };
      })
    );
  },

  reportCompilationStarted: options => {
    const logger = options.logger || console;
    if (!options.quiet) {
      logger.log(OS.EOL + `Compiling your contracts` + OS.EOL);
    }
  },

  reportCompilationFinished: (options, config) => {
    const logger = options.logger || console;
    const { compilersInfo } = config;
    if (!options.quiet) {
      logger.log(
        `    > artifacts written to ${options.contracts_build_directory}`
      );
      if (Object.keys(compilersInfo).length > 0) {
        logger.log(OS.EOL + `Compiled successfully using:` + OS.EOL);
        for (const name in compilersInfo) {
          logger.log(`    > ${name}: ${compilersInfo[name].version}`);
        }
      } else {
        logger.log(OS.EOL + `Compilation successful`);
      }
      logger.log();
    }
  },

  reportNothingToCompile: options => {
    const logger = options.logger || console;
    if (!options.quiet) {
      logger.log(
        `Everything is up to date, there is nothing to compile.` + OS.EOL
      );
    }
  },

  writeContracts: async (contracts, options) => {
    await promisify(mkdirp)(options.contracts_build_directory);
    const extra_opts = { network_id: options.network_id };
    await options.artifactor.saveAll(contracts, extra_opts);
  }
};

module.exports = Contracts;

@@ -1345,25 +1345,440 @@
 %0A%7D%0A%0A
-var Contracts = %7B
+const Contracts = %7B%0A  collectCompilations: async compilations =%3E %7B%0A    let result = %7B outputs: %7B%7D, contracts: %7B%7D %7D;%0A%0A    for (let compilation of await Promise.all(compilations)) %7B%0A      let %7B compiler, output, contracts %7D = compilation;%0A%0A      result.outputs%5Bcompiler%5D = output;%0A%0A      for (let %5Bname, abstraction%5D of Object.entries(contracts)) %7B%0A        result.contracts%5Bname%5D = abstraction;%0A      %7D%0A    %7D%0A%0A    return result;%0A  %7D,%0A
 %0A  /
@@ -2572,51 +2572,8 @@
 );%0A%0A
-    // convert to promise to compile+write%0A
     
@@ -2819,24 +2819,25 @@
    0%0A    );%0A
+%0A
     if (numb
@@ -2868,23 +2868,8 @@
 = 0)
- %7B%0A      return
  thi
@@ -2906,444 +2906,8 @@
 ns);
-%0A    %7D%0A%0A    const collect = async compilations =%3E %7B%0A      let result = %7B outputs: %7B%7D, contracts: %7B%7D %7D;%0A%0A      for (let compilation of await Promise.all(compilations)) %7B%0A        let %7B compiler, output, contracts %7D = compilation;%0A%0A        result.outputs%5Bcompiler%5D = output;%0A%0A        for (let %5Bname, abstraction%5D of Object.entries(contracts)) %7B%0A          result.contracts%5Bname%5D = abstraction;%0A        %7D%0A      %7D%0A%0A      return result;%0A    %7D;
 %0A%0A  
@@ -2974,23 +2974,40 @@
 n await 
+this.
 collect
+Compilations
 (compila

Move some code from compile method to its own