import fs from "fs";

import camelize from "camelize";
import {coreOptions as comalCoreOptions} from "comal";
const comalCoreOptionNames = Object.keys(comalCoreOptions);
import isGlob from "is-glob";
import cloneDeep from "lodash/cloneDeep";
import includes from "lodash/includes";
import omit from "lodash/omit";
import map from "lodash/map";
import pick from "lodash/pick";
import subarg from "subarg";

import usage from "./usage";
import usageAdvanced from "./usage/comal";

import baseArgs from "./base-args";
import argsWithComalOpts from "./convertComalOpts";
import convertPluginOpts from "./convertPluginOpts";
import stdin from "./stdin";
import stdout from "./stdout";
import transformTree from "./transformTree";
import watchTree from "./watchTree";

export default class TransformCli {
  constructor(defaults = {}) {
    this.name = "unknown";
    this.logger = console;
    this.defaults = omit(defaults, "_");
    this.opts = cloneDeep(baseArgs);
    this.destExt = ".js";
    // TODO: collect unknown arguments in a separate object as args to pass to comal
    // this.opts.unknown;

    this.opts.boolean.push("watch", "serial");
    this.opts.string.push("outfile", "extensions");
    this.opts.alias["outfile"] = ["o"];
    this.opts.alias["watch"] = ["w"];
    this.opts.alias["extensions"] = ["x"];
    this.opts.alias["plugin"] = ["p"];
    this.opts.alias["generator"] = ["g"];
    this.args = {}
  }

  transformSync(code, opts) {
    // This method should be overridden by a subclass or instance
    return {code: code, opts: opts};
  }

  prepare() {
    // This method should be overridden by a subclass or instance
  }

  transformFile(file, opts, cb) {
    // This method should be overridden by a subclass or instance
    fs.readFile(file, opts, cb);
  }

  debug(...args) {
    if (this.args.verbose) this.logger.warn(...args);
  }

  log(...args) {
    if (!this.args.quiet) this.logger.log(...args);
  }

  parseArgs(argv) {
    const comalUnknownOptionNames = [];
    const minimistOpts = argsWithComalOpts(comalCoreOptions, {
      ...this.opts,
      default: {...this.defaults, ...this.opts.default},
      unknown(arg) {
        const match = /^--([^=]+)=/.exec(arg) || /^--(?:no-)?(.+)/.exec(arg);
        if (match) comalUnknownOptionNames.push(match[1]);
      }
    });
    const args = this.args = subarg(argv, minimistOpts);

    this.comalOpts = camelize(pick(args, comalCoreOptionNames, comalUnknownOptionNames));
  }

  run(argv, cb) {
    if (includes(argv, "--help") || includes(argv, "-h")) {
      if (subarg(argv, {alias: {"help": ["h"]}}).help === "advanced") {
        return usageAdvanced(comalCoreOptions, cb);
      } else {
        return usage(this.name, cb);
      }
    }

    this.parseArgs(argv);
    const args = this.args;

    // normalize input arguments
    this.infiles = args._;
    const useStdin = this.infiles.length === 0;

    // normalize output arguments
    this.outfiles = args.outfile == null ? [] : [].concat(args.outfile);
    const useStdout = this.outfiles.length === 0;

    // normalize arguments to pass to a comal api
    const comalOpts = this.comalOpts;

    if (args.plugin) {
      try { comalOpts.plugins = convertPluginOpts(args.plugin) }
      catch (e) { return cb(e); }
    }

    if (args.extensions && !useStdin) {
      comalOpts.only = (comalOpts.only ? comalOpts.only + "," : "") +
        map(args.extensions.split(","), (e) => "*" + e).join(",");
    }

    this.debug("will convert", useStdin ? '<stdin>' : this.infiles, "to", useStdout ? '<stdout>' : this.outfiles);

    if (useStdin || useStdout) {
      return this.runStdio(cb);
    }
    return this.runFiles(cb);
  }

  runStdio(cb) {
    const args = this.args;

    if (args.watch) return cb(new Error("Cannot use watch with stdio"));

    const useStdin = this.infiles.length === 0;
    const useStdout = this.outfiles.length === 0;

    if (useStdin) {
      // TODO: error if sourcemaps is true, i.e. is requested as a separate file.
    }
    if (useStdout && !useStdin && (this.infiles.length > 1 || isGlob(this.infiles[0]))) {
      return cb(new Error("Cannot use more than one input file with stdout"));
    }
    if (useStdin && !useStdout && this.infiles.length > 1) {
      return cb(new Error("Cannot write to more than one output file with stdin"));
    }

    // do the work
    const read = useStdin ? stdin : (cb) => { fs.readFile(this.infiles[0], 'utf8', cb) };
    const write = useStdout ? stdout : (data, cb) => { fs.writeFile(this.outfiles[0], data, 'utf8', cb); };

    if (!useStdin && !this.comalOpts.filename) {
      this.comalOpts.filename = this.infiles[0];
    }

    read((err, data) => {
      if (err) {
        if (err.code === "EISDIR") {
          return cb(new Error("Cannot use a directory as input with stdout"));
        }
        return cb(err)
      }

      write(this.transformSync(data, this.comalOpts).code, cb);
    });
  }

  runFiles(cb) {
    const args = this.args;

    if (this.outfiles.length !== 1 && this.outfiles.length !== this.infiles.length) {
      return cb(new Error("Number of input files must equal number of output files, or output to a directory"));
    }

    this.prepare();

    // TODO: move watch and transform into this class
    if (args.watch) {
      watchTree(this.transformFile.bind(this), {src: this.infiles, dest: this.outfiles, destExt: this.destExt}, {args, only: this.comalOpts.only}, cb);
    } else {
      transformTree(this.transformFile.bind(this), {src: this.infiles, dest: this.outfiles, destExt: this.destExt}, {args, only: this.comalOpts.only}, cb);
    }
  }
}
