/* eslint max-classes-per-file: ["error", 2] */
/* eslint no-underscore-dangle: ["error", { "allow": ["_getLocationKeys"] }] */

const flatted = require('flatted');
const levels = require('./levels');

class SerDe {
  constructor() {
    const deserialise = {
      __LOG4JS_undefined__: undefined,
      __LOG4JS_NaN__: Number('abc'),
      __LOG4JS_Infinity__: 1 / 0,
      '__LOG4JS_-Infinity__': -1 / 0,
    };
    this.deMap = deserialise;
    this.serMap = {};
    Object.keys(this.deMap).forEach((key) => {
      const value = this.deMap[key];
      this.serMap[value] = key;
    });
  }

  canSerialise(key) {
    if (typeof key === 'string') return false;
    return key in this.serMap;
  }

  serialise(key) {
    if (this.canSerialise(key)) return this.serMap[key];
    return key;
  }

  canDeserialise(key) {
    return key in this.deMap;
  }

  deserialise(key) {
    if (this.canDeserialise(key)) return this.deMap[key];
    return key;
  }
}
const serde = new SerDe();

/**
 * @name LoggingEvent
 * @namespace Log4js
 */
class LoggingEvent {
  /**
   * Models a logging event.
   * @constructor
   * @param {string} categoryName name of category
   * @param {Log4js.Level} level level of message
   * @param {Array} data objects to log
   * @param {Error} [error]
   * @author Seth Chisamore
   */
  constructor(categoryName, level, data, context, location, error) {
    this.startTime = new Date();
    this.categoryName = categoryName;
    this.data = data;
    this.level = level;
    this.context = Object.assign({}, context); // eslint-disable-line prefer-object-spread
    this.pid = process.pid;
    this.error = error;

    if (typeof location !== 'undefined') {
      if (!location || typeof location !== 'object' || Array.isArray(location))
        throw new TypeError(
          'Invalid location type passed to LoggingEvent constructor'
        );

      this.constructor._getLocationKeys().forEach((key) => {
        if (typeof location[key] !== 'undefined') this[key] = location[key];
      });
    }
  }

  /** @private */
  static _getLocationKeys() {
    return [
      'fileName',
      'lineNumber',
      'columnNumber',
      'callStack',
      'className',
      'functionName',
      'functionAlias',
      'callerName',
    ];
  }

  serialise() {
    return flatted.stringify(this, (key, value) => {
      // JSON.stringify(new Error('test')) returns {}, which is not really useful for us.
      // The following allows us to serialize errors (semi) correctly.
      if (value instanceof Error) {
        // eslint-disable-next-line prefer-object-spread
        value = Object.assign(
          { message: value.message, stack: value.stack },
          value
        );
      }
      // JSON.stringify({a: Number('abc'), b: 1/0, c: -1/0}) returns {a: null, b: null, c: null}.
      // The following allows us to serialize to NaN, Infinity and -Infinity correctly.
      // JSON.stringify([undefined]) returns [null].
      // The following allows us to serialize to undefined correctly.
      return serde.serialise(value);
    });
  }

  static deserialise(serialised) {
    let event;
    try {
      const rehydratedEvent = flatted.parse(serialised, (key, value) => {
        if (value && value.message && value.stack) {
          const fakeError = new Error(value);
          Object.keys(value).forEach((k) => {
            fakeError[k] = value[k];
          });
          value = fakeError;
        }
        return serde.deserialise(value);
      });
      this._getLocationKeys().forEach((key) => {
        if (typeof rehydratedEvent[key] !== 'undefined') {
          if (!rehydratedEvent.location) rehydratedEvent.location = {};
          rehydratedEvent.location[key] = rehydratedEvent[key];
        }
      });
      event = new LoggingEvent(
        rehydratedEvent.categoryName,
        levels.getLevel(rehydratedEvent.level.levelStr),
        rehydratedEvent.data,
        rehydratedEvent.context,
        rehydratedEvent.location,
        rehydratedEvent.error
      );
      event.startTime = new Date(rehydratedEvent.startTime);
      event.pid = rehydratedEvent.pid;
      if (rehydratedEvent.cluster) {
        event.cluster = rehydratedEvent.cluster;
      }
    } catch (e) {
      event = new LoggingEvent('log4js', levels.ERROR, [
        'Unable to parse log:',
        serialised,
        'because: ',
        e,
      ]);
    }

    return event;
  }
}

module.exports = LoggingEvent;
