/**
 * @license
 * Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
 * This code may only be used under the BSD style license found at
 * http://polymer.github.io/LICENSE.txt
 * The complete set of authors may be found at
 * http://polymer.github.io/AUTHORS.txt
 * The complete set of contributors may be found at
 * http://polymer.github.io/CONTRIBUTORS.txt
 * Code distributed by Google as part of the polymer project is also
 * subject to an additional IP rights grant found at
 * http://polymer.github.io/PATENTS.txt
 */

// Be mindful of adding imports here, as this is on the hot path of all
// commands.

import * as commandLineArgs from 'command-line-args';
import {sep as pathSeperator} from 'path';
import * as logging from 'plylog';
import {ProjectConfig, ProjectOptions} from 'polymer-project-config';

import {globalArguments, mergeArguments} from './args';
import {AnalyzeCommand} from './commands/analyze';
import {BuildCommand} from './commands/build';
import {Command} from './commands/command';
import {HelpCommand} from './commands/help';
import {InitCommand} from './commands/init';
import {InstallCommand} from './commands/install';
import {LintCommand} from './commands/lint';
import {ServeCommand} from './commands/serve';
import {TestCommand} from './commands/test';
import {dashToCamelCase} from './util';

import commandLineCommands = require('command-line-commands');
import {ParsedCommand} from 'command-line-commands';

const logger = logging.getLogger('cli.main');

process.on('uncaughtException', (error: null|undefined|Partial<Error>) => {
  logger.error(`Uncaught exception: ${error}`);
  if (error && error.stack) {
    logger.error(error.stack);
  }
  process.exit(1);
});

process.on('unhandledRejection', (error: null|undefined|Partial<Error>) => {
  logger.error(`Promise rejection: ${error}`);
  if (error && error.stack) {
    logger.error(error.stack);
  }
  process.exit(1);
});

/**
 * CLI arguments are in "hyphen-case" format, but our configuration is in
 * "lowerCamelCase". This helper function converts the special
 * `command-line-args` data format (with its hyphen-case flags) to an easier to
 *  use options object with lowerCamelCase properties.
 */
// tslint:disable-next-line: no-any Super hacky scary code.
function parseCLIArgs(commandOptions: any): {[name: string]: string} {
  commandOptions = commandOptions && commandOptions['_all'];
  const parsedOptions = Object.assign({}, commandOptions);

  if (commandOptions['extra-dependencies']) {
    parsedOptions.extraDependencies = commandOptions['extra-dependencies'];
  }
  if (commandOptions.fragment) {
    parsedOptions.fragments = commandOptions.fragment;
  }

  return parsedOptions;
}

/**
 * Shallowly copies an object, converting keys from dash-case to camelCase.
 */
function objectDashToCamelCase<V>(input: {[key: string]: V}) {
  const output: {[key: string]: V} = {};
  for (const key of Object.keys(input)) {
    output[dashToCamelCase(key)] = input[key];
  }
  return output;
}


export class PolymerCli {
  commands: Map<string, Command> = new Map();
  args: string[];
  defaultConfigOptions: ProjectOptions;

  constructor(args: string[], configOptions?: ProjectOptions) {
    // If the "--quiet"/"-q" flag is ever present, set our global logging
    // to quiet mode. Also set the level on the logger we've already created.
    if (args.indexOf('--quiet') > -1 || args.indexOf('-q') > -1) {
      logging.setQuiet();
    }

    // If the "--verbose"/"-v" flag is ever present, set our global logging
    // to verbose mode. Also set the level on the logger we've already created.
    if (args.indexOf('--verbose') > -1 || args.indexOf('-v') > -1) {
      logging.setVerbose();
    }

    this.args = args;
    logger.debug('got args:', {args: args});

    if (typeof configOptions !== 'undefined') {
      this.defaultConfigOptions = configOptions;
      logger.debug(
          'got default config from constructor argument:',
          {config: this.defaultConfigOptions});
    } else {
      this.defaultConfigOptions =
          ProjectConfig.loadOptionsFromFile('polymer.json')!;
      if (this.defaultConfigOptions) {
        logger.debug(
            'got default config from polymer.json file:',
            {config: this.defaultConfigOptions});
      } else {
        logger.debug('no polymer.json file found, no config loaded');
      }
    }

    // This is a quick fix to make sure that "webcomponentsjs" files are
    // included in every build, since some are imported dynamically in a way
    // that our analyzer cannot detect.
    // TODO(fks) 03-07-2017: Remove/refactor when we have a better plan for
    // support (either here or inside of polymer-project-config).
    this.defaultConfigOptions = this.defaultConfigOptions || {};
    this.defaultConfigOptions.extraDependencies =
        this.defaultConfigOptions.extraDependencies || [];
    this.defaultConfigOptions.extraDependencies.unshift(
        `bower_components${pathSeperator}webcomponentsjs${pathSeperator}*.js`);

    this.addCommand(new AnalyzeCommand());
    this.addCommand(new BuildCommand());
    this.addCommand(new HelpCommand(this.commands));
    this.addCommand(new InitCommand());
    this.addCommand(new InstallCommand());
    this.addCommand(new LintCommand());
    this.addCommand(new ServeCommand());
    this.addCommand(new TestCommand());
  }

  addCommand(command: Command) {
    logger.debug('adding command', command.name);
    this.commands.set(command.name, command);

    command.aliases.forEach((alias) => {
      logger.debug('adding alias', alias);
      this.commands.set(alias, command);
    });
  }

  async run() {
    const helpCommand = this.commands.get('help')!;
    const commandNames = Array.from(this.commands.keys());
    let parsedArgs: ParsedCommand;
    logger.debug('running...');

    // If the "--version" flag is ever present, just print
    // the current version. Useful for globally installed CLIs.
    if (this.args.indexOf('--version') > -1) {
      console.log(require('../package.json').version);
      return Promise.resolve();
    }

    try {
      parsedArgs = commandLineCommands(commandNames, this.args);
    } catch (error) {
      // Polymer CLI needs a valid command name to do anything. If the given
      // command is invalid, run the generalized help command with default
      // config. This should print the general usage information.
      if (error.name === 'INVALID_COMMAND') {
        if (error.command) {
          logger.warn(`'${error.command}' is not an available command.`);
        }
        return helpCommand.run(
            {command: error.command},
            new ProjectConfig(this.defaultConfigOptions));
      }
      // If an unexpected error occurred, propagate it
      throw error;
    }

    const commandName = parsedArgs.command;
    const commandArgs = parsedArgs.argv;
    const command = this.commands.get(commandName)!;
    if (command == null) {
      throw new TypeError('command is null');
    }

    logger.debug(
        `command '${commandName}' found, parsing command args:`,
        {args: commandArgs});

    const commandDefinitions = mergeArguments([command.args, globalArguments]);
    const commandOptionsRaw =
        commandLineArgs(commandDefinitions, {argv: commandArgs});
    const commandOptions = parseCLIArgs(commandOptionsRaw);
    logger.debug(`command options parsed from args:`, commandOptions);

    const mergedConfigOptions = {
      ...this.defaultConfigOptions,
      ...objectDashToCamelCase(commandOptions),
    };
    logger.debug(`final config options:`, mergedConfigOptions);

    const config = new ProjectConfig(mergedConfigOptions);
    logger.debug(`final project configuration generated:`, config);

    // Help is a special argument for displaying help for the given command.
    // If found, run the help command instead, with the given command name as
    // an option.
    if (commandOptions['help']) {
      logger.debug(
          `'--help' option found, running 'help' for given command...`);
      return helpCommand.run({command: commandName}, config);
    }

    logger.debug('Running command...');
    return command.run(commandOptions, config);
  }
}
