/*
 * Copyright (C) 2019-2020 StApps
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation, version 3.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program. If not, see <https://www.gnu.org/licenses/>.
 */
import {stringify} from 'flatted';
import {isNodeEnvironment, isProductiveEnvironment, isProductiveNodeEnvironment} from './common.js';
import {Transformation} from './transformation.js';
import {AddLogLevel} from './transformations/add-log-level.js';
import {Transport} from './transport.js';

/**
 * Check if something has property STAPPS_LOG_LEVEL
 * @param something Something to check
 */
// tslint:disable-next-line:completed-docs
function hasStAppsLogLevel(something: object): something is {STAPPS_LOG_LEVEL: number} {
  return 'STAPPS_LOG_LEVEL' in something;
}

/**
 * Check if something has property STAPPS_EXIT_LEVEL
 * @param something Something to check
 */
// tslint:disable-next-line:completed-docs
function hasStAppsExitLevel(something: object): something is {STAPPS_EXIT_LEVEL: number} {
  return 'STAPPS_EXIT_LEVEL' in something;
}

/**
 * A level descriptor for either log or exit level
 */
export type Level = 'LOG' | 'EXIT';

/**
 * A log level
 */
export type LogLevel = 'INFO' | 'LOG' | 'WARN' | 'ERROR' | 'OK';

/**
 * A logger with transports and transformations
 *
 * Log level can be defined by setting the environment variable STAPPS_LOG_LEVEL to a valid log level. Log levels are
 * set in a binary way. For example STAPPS_LOG_LEVEL=12 does result in logs only for `Logger.warn` and `Logger.error`.
 *
 * Log levels in that order are:
 * ```
 * INFO: 1
 * LOG: 2
 * WARN: 4
 * ERROR: 8
 * OK: 16
 * ```
 */
export class Logger {
  /**
   * Base of binary system
   */
  private static readonly binaryBase = 2;

  /**
   * Log levels
   */
  private static readonly logLevels: LogLevel[] = ['INFO', 'LOG', 'WARN', 'ERROR', 'OK'];

  /**
   * Log level sum, equivalent to all log levels enabled
   */
  private static readonly logLevelSum = Math.pow(Logger.binaryBase, Logger.logLevels.length) - 1;

  /**
   * Transformers for log output
   */
  private static transformations?: Transformation[] = [new AddLogLevel()];

  /**
   * Transport for errors
   */
  private static transport?: Transport;

  /**
   * Apply transformations to an output
   * Will strip newlines in production environment
   * @param logLevel Log level of the output
   * @param output Output to apply transformations to
   */
  private static applyTransformers(logLevel: LogLevel, output: string): string {
    if (isProductiveEnvironment()) {
      output = output.replaceAll(/[\n\r]/g, ' ');
    }

    if (!Array.isArray(Logger.transformations) || Logger.transformations.length === 0) {
      return output;
    }

    let transformedOutput = output;
    for (const transformation of Logger.transformations) {
      transformedOutput = transformation.transform(logLevel, transformedOutput);
    }

    return transformedOutput;
  }

  /**
   * Check if intended exit level is allowed in environment exit level
   * @param exitLevel Log level to check
   */
  private static checkExitLevel(exitLevel: LogLevel): boolean {
    if (Logger.getLevel('EXIT') === 0) {
      return false;
    }

    // tslint:disable-next-line:no-bitwise
    return (Logger.getLevel('EXIT') & Logger.logLevelNumber(exitLevel)) === Logger.logLevelNumber(exitLevel);
  }

  /**
   * Check if intended log level is allowed in environment log level
   * @param logLevel Log level to check
   */
  private static checkLogLevel(logLevel: LogLevel): boolean {
    // tslint:disable-next-line:no-bitwise
    return (Logger.getLevel('LOG') & Logger.logLevelNumber(logLevel)) === Logger.logLevelNumber(logLevel);
  }

  /**
   * Notify about exit and end process
   */
  private static exit(): void {
    if (isProductiveNodeEnvironment()) {
      return;
    }
    // eslint-disable-next-line no-console
    console.error(
      Logger.applyTransformers('ERROR', `exiting as of used exit level ${Logger.getLevel('EXIT')} !`),
    );
    process.exit(-1);
  }

  /**
   * Return log level from environment
   */
  private static getLevel(level: Level): number {
    if (typeof window !== 'undefined') {
      // browser environment exists
      if (hasStAppsLogLevel(window)) {
        return window.STAPPS_LOG_LEVEL;
      }

      if (hasStAppsExitLevel(window)) {
        return window.STAPPS_EXIT_LEVEL;
      }
    }

    const environmentLevel = level === 'LOG' ? process.env.STAPPS_LOG_LEVEL : process.env.STAPPS_EXIT_LEVEL;

    if (isNodeEnvironment() && environmentLevel !== undefined) {
      // Node.js environment exists
      return Number.parseInt(environmentLevel, 10);
    }

    // Fallback to log everything, or not exiting
    switch (level) {
      case 'LOG': {
        return Logger.logLevelSum;
      }
      case 'EXIT': {
        return 0;
      }
    }
  }

  /**
   * Get number of specific log level
   * @param logLevel Log level to check
   */
  private static logLevelNumber(logLevel: LogLevel): number {
    return Math.pow(Logger.binaryBase, Logger.logLevels.indexOf(logLevel));
  }

  /**
   * Log an error
   * @param arguments_ Arguments to log
   */
  public static async error(...arguments_: unknown[]): Promise<string | void> {
    if (!Logger.checkLogLevel('ERROR')) {
      return;
    }

    // eslint-disable-next-line no-console
    console.error(Logger.applyTransformers('ERROR', Logger.stringifyArguments(...arguments_)));

    if (isProductiveNodeEnvironment()) {
      if (Logger.transport !== undefined) {
        return Logger.transport.send('Error', Logger.stringifyArguments(...arguments_));
      }

      if (process.env.ALLOW_NO_TRANSPORT !== 'true') {
        throw new Error(
          `Error couldn't be transported! Please set a transport or set ALLOW_NO_TRANSPORT='true'.`,
        );
      }
    }
    if (Logger.checkExitLevel('ERROR')) {
      Logger.exit();
    }
  }

  /**
   * Log an information
   * @param arguments_ Arguments to log
   */
  public static info(...arguments_: unknown[]): void {
    if (!Logger.checkLogLevel('INFO')) {
      return;
    }

    // eslint-disable-next-line no-console
    console.info(Logger.applyTransformers('INFO', Logger.stringifyArguments(...arguments_)));
    if (Logger.checkExitLevel('INFO')) {
      Logger.exit();
    }
  }

  /**
   * Check if the logger is initialized correctly
   */
  public static initialized(): void {
    if (isProductiveNodeEnvironment() && Logger.transport === undefined) {
      if (process.env.ALLOW_NO_TRANSPORT !== 'true') {
        throw new Error(`Productive environment doesn't set a transport for error notifications.`);
      }

      Logger.warn(`Productive environment doesn't set a transport for error notifications.`);
    }
  }

  /**
   * Log something
   * @param arguments_ Arguments to log
   */
  public static log(...arguments_: unknown[]): void {
    if (!Logger.checkLogLevel('LOG')) {
      return;
    }

    // eslint-disable-next-line no-console
    console.log(Logger.applyTransformers('LOG', Logger.stringifyArguments(...arguments_)));
    if (Logger.checkExitLevel('LOG')) {
      Logger.exit();
    }
  }

  /**
   * Log something successful
   * @param arguments_ Arguments to log
   */
  public static ok(...arguments_: unknown[]): void {
    if (!Logger.checkLogLevel('OK')) {
      return;
    }

    // eslint-disable-next-line no-console
    console.log(Logger.applyTransformers('OK', Logger.stringifyArguments(...arguments_)));
    if (Logger.checkExitLevel('OK')) {
      Logger.exit();
    }
  }

  /**
   * Set transformations for log output
   * @param transformations List of transformations
   */
  public static setTransformations(transformations: Transformation[]) {
    const transforms = transformations.filter(transform =>
      isProductiveEnvironment() ? transform.useInProduction === true : true,
    );
    Logger.transformations = transforms;
  }

  /**
   * Set a transport
   * @param transport Transport to set
   */
  public static setTransport(transport?: Transport) {
    Logger.transport = transport;
  }

  /**
   * Stringify a list of arguments
   * @param arguments_ Arguments to stringify
   */
  public static stringifyArguments(...arguments_: unknown[]): string {
    const result: string[] = [];

    for (const argument of arguments_) {
      if (typeof argument === 'string' || typeof argument === 'number') {
        result.push(argument.toString());
      } else if (argument instanceof Error) {
        result.push(argument.message);
        if (argument.stack !== undefined) {
          result.push(argument.stack);
        }
      } else {
        result.push(stringify(argument, undefined, 2));
      }
    }

    return result.join(', ');
  }

  /**
   * Log a warning
   * @param arguments_ Arguments to log
   */
  public static warn(...arguments_: unknown[]): void {
    if (!Logger.checkLogLevel('WARN')) {
      return;
    }

    // eslint-disable-next-line no-console
    console.warn(Logger.applyTransformers('WARN', Logger.stringifyArguments(...arguments_)));
    if (Logger.checkExitLevel('WARN')) {
      Logger.exit();
    }
  }
}
