'use strict';

const util = require('util');
const path = require('path');
const levels = require('./levels');
const layouts = require('./layouts');
const appenderAdapter = require('./appender-adapter');
const debug = require('debug')('log4js:configuration');

let cluster;
try {
  cluster = require('cluster'); // eslint-disable-line global-require
} catch (e) {
  debug('Clustering support disabled because require(cluster) threw an error: ', e);
}

const validColours = [
  'white', 'grey', 'black',
  'blue', 'cyan', 'green',
  'magenta', 'red', 'yellow'
];

function not(thing) {
  return !thing;
}

function anObject(thing) {
  return thing && typeof thing === 'object' && !Array.isArray(thing);
}

function validIdentifier(thing) {
  return /^[A-Za-z][A-Za-z0-9_]*$/g.test(thing);
}

function anInteger(thing) {
  return thing && typeof thing === 'number' && Number.isInteger(thing);
}

class Configuration {
  throwExceptionIf(checks, message) {
    const tests = Array.isArray(checks) ? checks : [checks];
    tests.forEach((test) => {
      if (test) {
        throw new Error(`Problem with log4js configuration: (${util.inspect(this.candidate, { depth: 5 })})` +
          ` - ${message}`);
      }
    });
  }

  tryLoading(modulePath) {
    debug('Loading module from ', modulePath);
    try {
      return require(modulePath); //eslint-disable-line
    } catch (e) {
      // if the module was found, and we still got an error, then raise it
      this.throwExceptionIf(
        e.code !== 'MODULE_NOT_FOUND',
        `appender "${path}" could not be loaded (error was: ${e})`
      );
      return undefined;
    }
  }

  loadAppenderModule(type) {
    return this.tryLoading(`./appenders/${type}`) ||
      this.tryLoading(type) ||
      this.tryLoading(path.join(path.dirname(require.main.filename), type)) ||
      this.tryLoading(path.join(process.cwd(), type));
  }

  createAppender(name, config) {
    const appenderModule = this.loadAppenderModule(config.type);

    if (appenderAdapter[config.type]) {
      appenderAdapter[config.type](config);
    }

    this.throwExceptionIf(
      not(appenderModule),
      `appender "${name}" is not valid (type "${config.type}" could not be found)`
    );
    if (appenderModule.appender) {
      debug(`DEPRECATION: Appender ${config.type} exports an appender function.`);
    }
    if (appenderModule.shutdown) {
      debug(`DEPRECATION: Appender ${config.type} exports a shutdown function.`);
    }

    if (this.disableClustering || cluster.isMaster || (this.pm2 && process.env[this.pm2InstanceVar] === '0')) {
      debug(`cluster.isMaster ? ${cluster.isMaster}`);
      debug(`pm2 enabled ? ${this.pm2}`);
      debug(`pm2InstanceVar = ${this.pm2InstanceVar}`);
      debug(`process.env[${this.pm2InstanceVar}] = ${process.env[this.pm2InstanceVar]}`);

      const appender = appenderModule.configure(
        config,
        layouts,
        this.configuredAppenders.get.bind(this.configuredAppenders),
        this.configuredLevels
      );

      if (appender.deprecated && this.deprecationWarnings) {
        console.error(`Appender "${name}" uses a deprecated type "${config.type}", ` + // eslint-disable-line
          'which will be removed in log4js v3. ' +
          `You should change it to use "${appender.deprecated}". ` +
          'To turn off this warning add "deprecationWarnings: false" to your config.');
      }

      return appender;
    }
    return () => {};
  }

  get appenders() {
    return this.configuredAppenders;
  }

  set appenders(appenderConfig) {
    const appenderNames = Object.keys(appenderConfig);
    this.throwExceptionIf(not(appenderNames.length), 'must define at least one appender.');

    this.configuredAppenders = new Map();
    appenderNames.forEach((name) => {
      this.throwExceptionIf(
        not(appenderConfig[name].type),
        `appender "${name}" is not valid (must be an object with property "type")`
      );

      debug(`Creating appender ${name}`);
      this.configuredAppenders.set(name, this.createAppender(name, appenderConfig[name]));
    });
  }

  get categories() {
    return this.configuredCategories;
  }

  set categories(categoryConfig) {
    const categoryNames = Object.keys(categoryConfig);
    this.throwExceptionIf(not(categoryNames.length), 'must define at least one category.');

    this.configuredCategories = new Map();
    categoryNames.forEach((name) => {
      const category = categoryConfig[name];
      this.throwExceptionIf(
        [
          not(category.appenders),
          not(category.level)
        ],
        `category "${name}" is not valid (must be an object with properties "appenders" and "level")`
      );

      this.throwExceptionIf(
        not(Array.isArray(category.appenders)),
        `category "${name}" is not valid (appenders must be an array of appender names)`
      );

      this.throwExceptionIf(
        not(category.appenders.length),
        `category "${name}" is not valid (appenders must contain at least one appender name)`
      );

      const appenders = [];
      category.appenders.forEach((appender) => {
        this.throwExceptionIf(
          not(this.configuredAppenders.get(appender)),
          `category "${name}" is not valid (appender "${appender}" is not defined)`
        );
        appenders.push(this.appenders.get(appender));
      });

      this.throwExceptionIf(
        not(this.configuredLevels.getLevel(category.level)),
        `category "${name}" is not valid (level "${category.level}" not recognised;` +
        ` valid levels are ${this.configuredLevels.levels.join(', ')})`
      );

      debug(`Creating category ${name}`);
      this.configuredCategories.set(
        name,
        { appenders: appenders, level: this.configuredLevels.getLevel(category.level) }
      );
    });

    this.throwExceptionIf(not(categoryConfig.default), 'must define a "default" category.');
  }

  get levels() {
    return this.configuredLevels;
  }

  set levels(levelConfig) {
    // levels are optional
    if (levelConfig) {
      this.throwExceptionIf(not(anObject(levelConfig)), 'levels must be an object');
      const newLevels = Object.keys(levelConfig);
      newLevels.forEach((l) => {
        this.throwExceptionIf(
          not(validIdentifier(l)),
          `level name "${l}" is not a valid identifier (must start with a letter, only contain A-Z,a-z,0-9,_)`
        );
        this.throwExceptionIf(not(anObject(levelConfig[l])), `level "${l}" must be an object`);
        this.throwExceptionIf(not(levelConfig[l].value), `level "${l}" must have a 'value' property`);
        this.throwExceptionIf(not(anInteger(levelConfig[l].value)), `level "${l}".value must have an integer value`);
        this.throwExceptionIf(not(levelConfig[l].colour), `level "${l}" must have a 'colour' property`);
        this.throwExceptionIf(
          not(validColours.indexOf(levelConfig[l].colour) > -1),
          `level "${l}".colour must be one of ${validColours.join(', ')}`
        );
      });
    }
    this.configuredLevels = levels(levelConfig);
  }

  constructor(candidate) {
    this.candidate = candidate;

    this.throwExceptionIf(not(anObject(candidate)), 'must be an object.');
    this.throwExceptionIf(not(anObject(candidate.appenders)), 'must have a property "appenders" of type object.');
    this.throwExceptionIf(not(anObject(candidate.categories)), 'must have a property "categories" of type object.');

    this.disableClustering = this.candidate.disableClustering || !cluster;
    this.deprecationWarnings = true;
    if ('deprecationWarnings' in this.candidate) {
      this.deprecationWarnings = this.candidate.deprecationWarnings;
    }

    this.pm2 = this.candidate.pm2;
    this.pm2InstanceVar = this.candidate.pm2InstanceVar || 'NODE_APP_INSTANCE';

    this.levels = candidate.levels;
    this.appenders = candidate.appenders;
    this.categories = candidate.categories;
  }
}

module.exports = Configuration;