import {
  ParamDefinition,
  ParamDefinitionAny,
  ParamDefinitions,
  ParamDefinitionsMap,
  TasksMap
} from "../../types";
import { PolarError } from "../core/errors";
import { ERRORS } from "../core/errors-list";
import { cmpStr } from "../util/strings";
import { ArgumentsParser } from "./arguments-parser";

const getMax = (a: number, b: number): number => Math.max(a, b);

export class HelpPrinter {
  constructor (
    private readonly _programName: string,
    private readonly _version: string,
    private readonly _paramDefs: ParamDefinitions,
    private readonly _tasks: TasksMap
  ) {}

  public printGlobalHelp (includeInternalTasks = false): void {
    console.log(`${this._programName} version ${this._version}\n`);

    console.log(
      `Usage: ${this._programName} [GLOBAL OPTIONS] <TASK> [TASK OPTIONS]\n`
    );

    console.log("GLOBAL OPTIONS:\n");

    this._printParamDetails(this._paramDefs);

    console.log("\n\nAVAILABLE TASKS:\n");

    const tasksToShow: TasksMap = {};
    for (const [taskName, taskDefinition] of Object.entries(this._tasks)) {
      if (includeInternalTasks || !taskDefinition.isInternal) {
        tasksToShow[taskName] = taskDefinition;
      }
    }

    const nameLength = Object.keys(tasksToShow)
      .map((n) => n.length)
      .reduce((a, b) => Math.max(a, b), 0);

    for (const name of Object.keys(tasksToShow).sort(cmpStr)) {
      const { description = "" } = this._tasks[name];

      console.log(`  ${name.padEnd(nameLength)}\t${description}`);
    }

    console.log("");

    console.log(
      `To get help for a specific task run: ${this._programName} help [task]\n`
    );
  }

  public printTaskHelp (taskName: string): void {
    const taskDefinition = this._tasks[taskName];

    if (taskDefinition === undefined) {
      throw new PolarError(ERRORS.ARGUMENTS.UNRECOGNIZED_TASK, {
        task: taskName
      });
    }

    const {
      description = "",
      name,
      paramDefinitions,
      positionalParamDefinitions
    } = taskDefinition;

    console.log(`${this._programName} version ${this._version}\n`);

    const paramsList = this._getParamsList(paramDefinitions);
    const positionalParamsList = this._getPositionalParamsList(
      positionalParamDefinitions
    );

    console.log(
      `Usage: ${this._programName} [GLOBAL OPTIONS] ${name}${paramsList}${positionalParamsList}\n`
    );

    if (Object.keys(paramDefinitions).length > 0) {
      console.log("OPTIONS:\n");

      this._printParamDetails(paramDefinitions);

      console.log("");
    }

    if (positionalParamDefinitions.length > 0) {
      console.log("POSITIONAL ARGUMENTS:\n");

      this._printPositionalParamDetails(positionalParamDefinitions);

      console.log("");
    }

    console.log(`${name}: ${description}\n`);

    console.log(`For global options help run: ${this._programName} help\n`);
  }

  private _getParamValueDescription<T>(paramDefinition: ParamDefinition<T>): string {
    return `<${paramDefinition.type.name.toUpperCase()}>`;
  }

  private _getParamsList (paramDefinitions: ParamDefinitionsMap): string {
    let paramsList = "";

    for (const name of Object.keys(paramDefinitions).sort(cmpStr)) {
      const definition = paramDefinitions[name];
      const { defaultValue, isFlag } = definition;

      paramsList += " ";

      if (defaultValue !== undefined) {
        paramsList += "[";
      }

      paramsList += `${ArgumentsParser.paramNameToCLA(name)}`;

      if (!isFlag) {
        paramsList += ` ${this._getParamValueDescription(definition)}`;
      }

      if (defaultValue !== undefined) {
        paramsList += "]";
      }
    }

    return paramsList;
  }

  private _getPositionalParamsList (
    positionalParamDefinitions: ParamDefinitionAny[]
  ): string {
    let paramsList = "";

    for (const definition of positionalParamDefinitions) {
      const { defaultValue, isVariadic, name } = definition;

      paramsList += " ";

      if (defaultValue !== undefined) {
        paramsList += "[";
      }

      if (isVariadic) {
        paramsList += "...";
      }

      paramsList += name;

      if (defaultValue !== undefined) {
        paramsList += "]";
      }
    }

    return paramsList;
  }

  private _printParamDetails (paramDefinitions: ParamDefinitionsMap): void {
    const paramKeys = Object.keys(paramDefinitions);
    const shortParamsNameLength = paramKeys
      .map((n) => ArgumentsParser.shortParamNameToCLA(paramDefinitions[n].shortName).length)
      .reduce(getMax, 0);
    const paramsNameLength = paramKeys
      .map((n) => ArgumentsParser.paramNameToCLA(n).length)
      .reduce(getMax, 0);

    for (const name of paramKeys.sort(cmpStr)) {
      const {
        description,
        defaultValue,
        isOptional,
        isFlag,
        shortName
      } = paramDefinitions[name];

      const paddedShortName = ArgumentsParser.shortParamNameToCLA(shortName)
        .padEnd(shortParamsNameLength) + (shortName ? "," : " ");

      let msg = `  ${paddedShortName} `;

      msg += `${ArgumentsParser.paramNameToCLA(name).padEnd(
        paramsNameLength
      )}\t`;

      if (description !== undefined) {
        msg += `${description} `;
      }

      if (isOptional && defaultValue !== undefined && !isFlag) {
        msg += `(default: ${JSON.stringify(defaultValue)})`;
      }

      console.log(msg);
    }
  }

  private _printPositionalParamDetails (
    positionalParamDefinitions: ParamDefinitionAny[]
  ): void {
    const paramsNameLength = positionalParamDefinitions
      .map((d) => d.name.length)
      .reduce((a, b) => Math.max(a, b), 0);

    for (const definition of positionalParamDefinitions) {
      const { name, description, isOptional, defaultValue } = definition;

      let msg = `  ${name.padEnd(paramsNameLength)}\t`;

      if (description !== undefined) {
        msg += `${description} `;
      }

      if (isOptional && defaultValue !== undefined) {
        msg += `(default: ${JSON.stringify(defaultValue)})`;
      }

      console.log(msg);
    }
  }
}
