import path from 'path';
import neon_new from './ops/neon_new';
import neon_build from './ops/neon_build';
import neon_clean from './ops/neon_clean';
import * as style from './style';
import cliCommands = require('command-line-commands');
import cliArgs = require('command-line-args');
import cliUsage = require('command-line-usage');
import log from './log';
import { setup as setupLogging } from './log';
import * as JSON from 'ts-typed-json';
import { Toolchain } from './rust';

let metadata = JSON.loadSync(path.resolve(__dirname, '..', 'package.json'));

function commandUsage(command: string) {
  if (!spec[command]) {
    let e = new Error();
    (e as any).command = command;
    e.name = 'INVALID_COMMAND';
    throw e;
  }
  console.error(cliUsage(spec[command].usage));
}

function logIf(multiple: boolean, action: string, cwd: string, module: string) {
  if (multiple) {
    log(action + " Neon package at " + (path.relative(cwd, module) || "."));
  }
}

function parseArgv(argv: string[]): { cli: string[], extra: string[] } {
  let splitAt = argv.indexOf('--');

  // No additional arguments provided
  if (splitAt < 0) {
    return {
      cli: argv,
      extra: [],
    };
  }

  return {
    cli: argv.slice(0, splitAt),
    extra: argv.slice(splitAt + 1)
  };
}

function parseModules(cwd: string, names: string[], paths: boolean) {
  let modules = names.length
      ? names.map(m => paths ? path.resolve(cwd, m)
                             : path.resolve(cwd, 'node_modules', m))
      : [cwd];

  return {
    modules,
    multiple: modules.length > 1
  };
}

type Action = (this: CLI, options: Record<string, unknown>, usage: string) => void;

type Command = {
  args: cliArgs.OptionDefinition[],
  usage: cliUsage.Sections,
  action: Action
};

type Spec = Record<string, Command>;

const spec: Spec = {

  null: {
    args: [{ name: "version", alias: "v", type: Boolean },
           { name: "help", alias: "h", type: String, defaultValue: null }],
    usage: [{
      header: "Neon",
      content: "Neon is a tool for building native Node.js modules with Rust."
    }, {
      header: "Synopsis",
      content: "$ neon [options] <command> -- [cargo options]"
    }, {
      header: "Command List",
      content: [{ name: "new", summary: "Create a new Neon project." },
                { name: "build", summary: "(Re)build a Neon project." },
                { name: "clean", summary: "Remove build artifacts from a Neon project." },
                { name: "version", summary: "Display the Neon version." },
                { name: "help", summary: "Display help information about Neon." }]
    }],
    action: function(options, usage) {
      if (options.version && options.help === undefined) {
        spec.version.action.call(this, options);
      } else if (options.help !== undefined) {
        commandUsage(options.help as string);
      } else {
        console.error(usage);
      }
    }
  },

  help: {
    args: [{ name: "command", type: String, defaultOption: true },
           { name: "help", alias: "h", type: Boolean }],
    usage: [{
      header: "neon help",
      content: "Get help about a Neon command"
    }, {
      header: "Synopsis",
      content: "$ neon help [command]"
    }],
    action: function(options) {
      if (options && options.command) {
        commandUsage(options.command as string);
      } else if (options && options.help) {
        commandUsage('help');
      } else {
        console.error(cliUsage(spec.null.usage));
      }
    }
  },

  new: {
    args: [{ name: "name", type: String, defaultOption: true },
           { name: "neon", alias: "n", type: String },
           { name: "features", alias: "f", type: String },
           { name: "no-default-features", type: Boolean },
           { name: "help", alias: "h", type: Boolean }],
    usage: [{
      header: "neon new",
      content: "Create a new Neon project."
    }, {
      header: "Synopsis",
      content: "$ neon new [options] [@<scope>/]<name>"
    }, {
      header: "Options",
      optionList: [{
        name: "neon",
        alias: "n",
        type: String,
        description: "Specify a semver version of Neon or path to a local Neon repository."
      }, {
        name: "features",
        alias: "f",
        type: String,
        description: "Space-separated list of experimental Neon features to enable."
      }, {
        name: "no-default-features",
        type: Boolean,
        description: "Do not activate the `default` Neon feature."
      }]
    }],
    action: function(options) {
      if (options.help) {
        commandUsage('new');
      } else if (!options.name) {
        console.error(cliUsage(spec.new.usage));
      } else {
        return neon_new(this.cwd,
                        options.name as string,
                        (options.neon || null) as (string | null),
                        (options.features || null) as (string | null),
                        !!options['no-default-features']);
      }
      return;
    }
  },

  build: {
    args: [{ name: "release", alias: "r", type: Boolean },
           { name: "path", alias: "p", type: Boolean },
           { name: "modules", type: String, multiple: true, defaultOption: true },
           { name: "help", alias: "h", type: Boolean }],
    usage: [{
      header: "neon build",
      content: "(Re)build a Neon project."
    }, {
      header: "Synopsis",
      content: ["$ neon build [options]",
                "$ neon build [options] {underline module} ..."]
    }, {
      header: "Options",
      optionList: [{
        name: "release",
        alias: "r",
        type: Boolean,
        description: "Release build."
      }, {
        name: "path",
        alias: "p",
        type: Boolean,
        description: "Specify modules by path instead of name."
      }]
    }],
    action: async function(options) {
      if (options.help) {
        commandUsage('build');
        return;
      }

      let extra = options.extra as string[];
      let { modules, multiple } = parseModules(this.cwd,
                                               (options.modules || []) as string[],
                                               !!options.path);

      for (let module of modules) {
        logIf(multiple, "building", this.cwd, module);

        await neon_build(module, this.toolchain, !!options.release, extra);
      }
    }
  },

  clean: {
    args: [{ name: "path", alias: "p", type: Boolean },
           { name: "modules", type: String, multiple: true, defaultOption: true },
           { name: "help", alias: "h", type: Boolean }],
    usage: [{
      header: "neon clean",
      content: "Remove build artifacts from a Neon project."
    }, {
      header: "Synopsis",
      content: ["$ neon clean [options]",
                "$ neon clean [options] {underline module} ..."]
    }, {
      header: "Options",
      optionList: [{
        name: "path",
        alias: "p",
        type: Boolean,
        description: "Specify modules by path instead of name."
      }]
    }],
    action: async function(options) {
      if (options.help) {
        commandUsage('clean');
        return;
      }

      let { modules, multiple } = parseModules(this.cwd,
                                               (options.modules || []) as string[],
                                               !!options.path);

      for (let module of modules) {
        logIf(multiple, "cleaning", this.cwd, module);

        await neon_clean(module);
      }
    }
  },

  version: {
    args: [{ name: "help", alias: "h", type: Boolean }],
    usage: [{
      header: "neon version",
      content: "Display the Neon version."
    }, {
      header: "Synopsis",
      content: "$ neon version"
    }],
    action: function(options) {
      if (options.help) {
        commandUsage('version');
        return;
      }

      console.log((JSON.asObject(metadata)).version);
    }
  }

};

export default class CLI {
  readonly toolchain: Toolchain;
  readonly argv: string[];
  readonly cwd: string;

  constructor(argv: string[], cwd: string) {
    // Check for a toolchain argument in the style of Rust tools (e.g., `neon +nightly build`).
    if (argv.length > 2 && argv[2].trim().startsWith('+')) {
      this.toolchain = argv[2].substring(1).trim() as Toolchain;
      this.argv = argv.slice(3);
    } else {
      this.toolchain = 'default';
      this.argv = argv.slice(2);
    }
    this.cwd = cwd;
  }

  async exec() {
    setupLogging(msg => { console.log(style.info(msg)); });

    let parsed;

    try {
      parsed = cliCommands([ null, 'help', 'new', 'build', 'clean', 'version' ], this.argv);
    } catch (e) {
      spec.help.action.call(this);

      switch (e.name) {
        case 'INVALID_COMMAND':
          console.error(style.error("No manual entry for `neon " + e.command + "`"));
          break;

        default:
          console.error(style.error(e.message));
          break;
      }

      process.exit(1);
    }

    try {
      let { command, argv } = parsed;
      let { cli, extra } = parseArgv(argv);
      let options = { extra, ...cliArgs(spec[command].args, { argv: cli }) };
      await spec[command].action.call(this,
                                      options,
                                      cliUsage(spec[command].usage));
    } catch (e) {
      console.error(style.error(e.message));
      console.error();
      console.error(e.stack);
      throw e;
    }
  }
}
