const _ = require('lodash');
const { default: ow } = require('ow');

/**
 * Logging levels for the JSDoc logger. The default logging level is
 * {@link module:@jsdoc/cli.LOG_LEVELS.WARN}.
 *
 * @alias module:@jsdoc/cli.LOG_LEVELS
 * @enum
 * @type {number}
 */
const LEVELS = {
  /**
   * Do not log any messages.
   *
   * @alias module:@jsdoc/cli.LOG_LEVELS.SILENT
   */
  SILENT: 0,
  /**
   * Log fatal errors that prevent JSDoc from running.
   *
   * @alias module:@jsdoc/cli.LOG_LEVELS.FATAL
   */
  FATAL: 10,
  /**
   * Log all errors, including errors from which JSDoc can recover.
   *
   * @alias module:@jsdoc/cli.LOG_LEVELS.ERROR
   */
  ERROR: 20,
  /**
   * Log the following messages:
   *
   * + Warnings
   * + Errors
   *
   * @alias module:@jsdoc/cli.LOG_LEVELS.WARN
   */
  WARN: 30,
  /**
   * Log the following messages:
   *
   * + Informational messages
   * + Warnings
   * + Errors
   *
   * @alias module:@jsdoc/cli.LOG_LEVELS.INFO
   */
  INFO: 40,
  /**
   * Log the following messages:
   *
   * + Debugging messages
   * + Informational messages
   * + Warnings
   * + Errors
   *
   * @alias module:@jsdoc/cli.LOG_LEVELS.DEBUG
   */
  DEBUG: 50,
  /**
   * Log all messages.
   *
   * @alias module:@jsdoc/cli.LOG_LEVELS.VERBOSE
   */
  VERBOSE: 1000,
};

const DEFAULT_LEVEL = LEVELS.WARN;
const FUNCS = {
  [LEVELS.DEBUG]: 'debug',
  [LEVELS.ERROR]: 'error',
  [LEVELS.INFO]: 'info',
  [LEVELS.FATAL]: 'error',
  [LEVELS.VERBOSE]: 'debug',
  [LEVELS.WARN]: 'warn',
};
const LEVELS_BY_NUMBER = _.invert(LEVELS);
const PREFIXES = {
  [LEVELS.DEBUG]: 'DEBUG: ',
  [LEVELS.ERROR]: 'ERROR: ',
  [LEVELS.FATAL]: 'FATAL: ',
  [LEVELS.WARN]: 'WARNING: ',
};

// Add a prefix to a log message if necessary.
function addPrefix(level, args) {
  const prefix = PREFIXES[level];

  if (prefix && _.isString(args[0])) {
    args[0] = prefix + args[0];
  }

  return args;
}

class Logger {
  constructor(opts) {
    ow(opts, ow.object);
    // We validate `opts.level` in the setter, so no need to validate it here.
    ow(
      opts.emitter,
      ow.object.partialShape({
        off: ow.function,
        on: ow.function,
        once: ow.function,
      })
    );

    this._console = opts._console || console;
    const emitter = (this._emitter = opts.emitter);

    this.level = opts.level || DEFAULT_LEVEL;

    for (const levelName of Object.keys(LEVELS)) {
      let levelNameLower;
      let levelNumber;

      // `logger:silent` events are not a thing.
      if (levelName === 'SILENT') {
        continue;
      }

      levelNameLower = levelName.toLowerCase();
      levelNumber = LEVELS[levelName];

      emitter.on(`logger:${levelNameLower}`, (...args) => this._maybeLog(levelNumber, args));
    }
  }

  _maybeLog(level, args) {
    if (this._level >= level) {
      args = addPrefix(level, args);
      this._console[FUNCS[level]](...args);
    }
  }

  get level() {
    return this._level;
  }

  set level(level) {
    let errorMsg;

    if (_.isUndefined(LEVELS_BY_NUMBER[level])) {
      errorMsg = `Unrecognized logging level ${level}. Known levels are: `;
      errorMsg += Object.keys(LEVELS)
        .map((k) => `${k}: ${LEVELS[k]}`)
        .join(', ');

      throw new TypeError(errorMsg);
    }

    this._level = level;
  }
}

module.exports = {
  LEVELS,
  Logger,
};
