// Type definitions for log4js

type Format =
  | string
  | ((req: any, res: any, formatter: (str: string) => string) => string);

export interface Log4js {
  getLogger(category?: string): Logger;
  configure(filename: string): Log4js;
  configure(config: Configuration): Log4js;
  addLayout(
    name: string,
    config: (a: any) => (logEvent: LoggingEvent) => string
  ): void;
  connectLogger(
    logger: Logger,
    options: { format?: Format; level?: string; nolog?: any }
  ): any; // express.Handler;
  levels: Levels;
  shutdown(cb?: (error?: Error) => void): void;
}

export function getLogger(category?: string): Logger;

export function configure(filename: string): Log4js;
export function configure(config: Configuration): Log4js;

export function addLayout(
  name: string,
  config: (a: any) => (logEvent: LoggingEvent) => any
): void;

export function connectLogger(
  logger: Logger,
  options: {
    format?: Format;
    level?: string;
    nolog?: any;
    statusRules?: any[];
    context?: boolean;
  }
): any; // express.Handler;

export function recording(): Recording;

export const levels: Levels;

export function shutdown(cb?: (error?: Error) => void): void;

export interface BasicLayout {
  type: 'basic';
}

export interface ColoredLayout {
  type: 'colored' | 'coloured';
}

export interface MessagePassThroughLayout {
  type: 'messagePassThrough';
}

export interface DummyLayout {
  type: 'dummy';
}

export interface Level {
  isEqualTo(other: string): boolean;
  isEqualTo(otherLevel: Level): boolean;
  isLessThanOrEqualTo(other: string): boolean;
  isLessThanOrEqualTo(otherLevel: Level): boolean;
  isGreaterThanOrEqualTo(other: string): boolean;
  isGreaterThanOrEqualTo(otherLevel: Level): boolean;
  colour: string;
  level: number;
  levelStr: string;
}
/**
 * A parsed CallStack from an `Error.stack` trace
 */
export interface CallStack {
  functionName: string;
  fileName: string;
  lineNumber: number;
  columnNumber: number;
  /**
   * The stack string after the skipped lines
   */
  callStack: string;
}
export interface LoggingEvent extends Partial<CallStack> {
  categoryName: string; // name of category
  level: Level; // level of message
  data: any[]; // objects to log
  startTime: Date;
  pid: number;
  context: any;
  cluster?: {
    workerId: number;
    worker: number;
  };
  /**
   * The first Error object in the data if there is one
   */
  error?: Error;
  serialise(): string;
}

export type Token = ((logEvent: LoggingEvent) => string) | string;

export interface PatternLayout {
  type: 'pattern';
  // specifier for the output format, using placeholders as described below
  pattern: string;
  // user-defined tokens to be used in the pattern
  tokens?: { [name: string]: Token };
}

export interface CustomLayout {
  [key: string]: any;
  type: string;
}

export type Layout =
  | BasicLayout
  | ColoredLayout
  | MessagePassThroughLayout
  | DummyLayout
  | PatternLayout
  | CustomLayout;

/**
 * Category Filter
 *
 * @see https://log4js-node.github.io/log4js-node/categoryFilter.html
 */
export interface CategoryFilterAppender {
  type: 'categoryFilter';
  // the category (or categories if you provide an array of values) that will be excluded from the appender.
  exclude?: string | string[];
  // the name of the appender to filter. see https://log4js-node.github.io/log4js-node/layouts.html
  appender?: string;
}

/**
 * No Log Filter
 *
 * @see https://log4js-node.github.io/log4js-node/noLogFilter.html
 */
export interface NoLogFilterAppender {
  type: 'noLogFilter';
  // the regular expression (or the regular expressions if you provide an array of values)
  // will be used for evaluating the events to pass to the appender.
  // The events, which will match the regular expression, will be excluded and so not logged.
  exclude: string | string[];
  // the name of an appender, defined in the same configuration, that you want to filter.
  appender: string;
}

/**
 * Console Appender
 *
 * @see https://log4js-node.github.io/log4js-node/console.html
 */
export interface ConsoleAppender {
  type: 'console';
  // (defaults to ColoredLayout)
  layout?: Layout;
}

export interface FileAppender {
  type: 'file';
  // the path of the file where you want your logs written.
  filename: string;
  // (defaults to undefined) the maximum size (in bytes) for the log file. If not specified or 0, then no log rolling will happen.
  maxLogSize?: number | string;
  // (defaults to 5) the number of old log files to keep (excluding the hot file).
  backups?: number;
  // (defaults to BasicLayout)
  layout?: Layout;
  // (defaults to utf-8)
  encoding?: string;
  // (defaults to 0o600)
  mode?: number;
  // (defaults to a)
  flags?: string;
  // (defaults to false) compress the backup files using gzip (backup files will have .gz extension)
  compress?: boolean;
  // (defaults to false) preserve the file extension when rotating log files (`file.log` becomes `file.1.log` instead of `file.log.1`).
  keepFileExt?: boolean;
  // (defaults to .) the filename separator when rolling. e.g.: abc.log`.`1 or abc`.`1.log (keepFileExt)
  fileNameSep?: string;
}

export interface SyncfileAppender {
  type: 'fileSync';
  // the path of the file where you want your logs written.
  filename: string;
  // (defaults to undefined) the maximum size (in bytes) for the log file. If not specified or 0, then no log rolling will happen.
  maxLogSize?: number | string;
  // (defaults to 5) the number of old log files to keep (excluding the hot file).
  backups?: number;
  // (defaults to BasicLayout)
  layout?: Layout;
  // (defaults to utf-8)
  encoding?: string;
  // (defaults to 0o600)
  mode?: number;
  // (defaults to a)
  flags?: string;
}

export interface DateFileAppender {
  type: 'dateFile';
  // the path of the file where you want your logs written.
  filename: string;
  // (defaults to yyyy-MM-dd) the pattern to use to determine when to roll the logs.
  /**
   * The following strings are recognised in the pattern:
   *  - yyyy : the full year, use yy for just the last two digits
   *  - MM   : the month
   *  - dd   : the day of the month
   *  - hh   : the hour of the day (24-hour clock)
   *  - mm   : the minute of the hour
   *  - ss   : seconds
   *  - SSS  : milliseconds (although I'm not sure you'd want to roll your logs every millisecond)
   *  - O    : timezone (capital letter o)
   */
  pattern?: string;
  // (defaults to BasicLayout)
  layout?: Layout;
  // (defaults to utf-8)
  encoding?: string;
  // (defaults to 0o600)
  mode?: number;
  // (defaults to a)
  flags?: string;
  // (defaults to false) compress the backup files using gzip (backup files will have .gz extension)
  compress?: boolean;
  // (defaults to false) preserve the file extension when rotating log files (`file.log` becomes `file.2017-05-30.log` instead of `file.log.2017-05-30`).
  keepFileExt?: boolean;
  // (defaults to .) the filename separator when rolling. e.g.: abc.log`.`2013-08-30 or abc`.`2013-08-30.log (keepFileExt)
  fileNameSep?: string;
  // (defaults to false) include the pattern in the name of the current log file.
  alwaysIncludePattern?: boolean;
  // (defaults to 1) the number of old files that matches the pattern to keep (excluding the hot file).
  numBackups?: number;
}

export interface LogLevelFilterAppender {
  type: 'logLevelFilter';
  // the name of an appender, defined in the same configuration, that you want to filter
  appender: string;
  // the minimum level of event to allow through the filter
  level: string;
  // (defaults to FATAL) the maximum level of event to allow through the filter
  maxLevel?: string;
}

export interface MultiFileAppender {
  type: 'multiFile';
  // the base part of the generated log filename
  base: string;
  // the value to use to split files (see below).
  property: string;
  // the suffix for the generated log filename.
  extension: string;
}

export interface MultiprocessAppender {
  type: 'multiprocess';
  // controls whether the appender listens for log events sent over the network, or is responsible for serialising events and sending them to a server.
  mode: 'master' | 'worker';
  // (only needed if mode == master) the name of the appender to send the log events to
  appender?: string;
  // (defaults to 5000) the port to listen on, or send to
  loggerPort?: number;
  // (defaults to localhost) the host/IP address to listen on, or send to
  loggerHost?: string;
}

export interface RecordingAppender {
  type: 'recording';
}

export interface StandardErrorAppender {
  type: 'stderr';
  // (defaults to ColoredLayout)
  layout?: Layout;
}

export interface StandardOutputAppender {
  type: 'stdout';
  // (defaults to ColoredLayout)
  layout?: Layout;
}

/**
 * TCP Appender
 *
 * @see https://log4js-node.github.io/log4js-node/tcp.html
 */
export interface TCPAppender {
  type: 'tcp';
  // (defaults to 5000)
  port?: number;
  // (defaults to localhost)
  host?: string;
  // (defaults to __LOG4JS__)
  endMsg?: string;
  // (defaults to a serialized log event)
  layout?: Layout;
}

export interface CustomAppender {
  type: string | AppenderModule;
  [key: string]: any;
}

/**
 * Mapping of all Appenders to allow for declaration merging
 * @example
 * declare module 'log4js' {
 *   interface Appenders {
 *     StorageTestAppender: {
 *       type: 'storageTest';
 *       storageMedium: 'dvd' | 'usb' | 'hdd';
 *     };
 *   }
 * }
 */
export interface Appenders {
  CategoryFilterAppender: CategoryFilterAppender;
  ConsoleAppender: ConsoleAppender;
  FileAppender: FileAppender;
  SyncfileAppender: SyncfileAppender;
  DateFileAppender: DateFileAppender;
  LogLevelFilterAppender: LogLevelFilterAppender;
  NoLogFilterAppender: NoLogFilterAppender;
  MultiFileAppender: MultiFileAppender;
  MultiprocessAppender: MultiprocessAppender;
  RecordingAppender: RecordingAppender;
  StandardErrorAppender: StandardErrorAppender;
  StandardOutputAppender: StandardOutputAppender;
  TCPAppender: TCPAppender;
  CustomAppender: CustomAppender;
}

export interface AppenderModule {
  configure: (
    config?: Config,
    layouts?: LayoutsParam,
    findAppender?: () => AppenderFunction,
    levels?: Levels
  ) => AppenderFunction;
}

export type AppenderFunction = (loggingEvent: LoggingEvent) => void;

// TODO: Actually add types here...
// It's supposed to be the full config element
export type Config = any;

export interface LayoutsParam {
  basicLayout: LayoutFunction;
  messagePassThroughLayout: LayoutFunction;
  patternLayout: LayoutFunction;
  colouredLayout: LayoutFunction;
  coloredLayout: LayoutFunction;
  dummyLayout: LayoutFunction;
  addLayout: (name: string, serializerGenerator: LayoutFunction) => void;
  layout: (name: string, config: PatternToken) => LayoutFunction;
}

export interface PatternToken {
  pattern: string; // TODO type this to enforce good pattern...
  tokens: { [tokenName: string]: () => any };
}

export type LayoutFunction = (loggingEvent: LoggingEvent) => string;

export type Appender = Appenders[keyof Appenders];

export interface Levels {
  ALL: Level;
  MARK: Level;
  TRACE: Level;
  DEBUG: Level;
  INFO: Level;
  WARN: Level;
  ERROR: Level;
  FATAL: Level;
  OFF: Level;
  levels: Level[];
  getLevel(level: Level | string, defaultLevel?: Level): Level;
  addLevels(customLevels: object): void;
}

export interface Configuration {
  appenders: { [name: string]: Appender };
  categories: {
    [name: string]: {
      appenders: string[];
      level: string;
      enableCallStack?: boolean;
    };
  };
  pm2?: boolean;
  pm2InstanceVar?: string;
  levels?:
    | Levels
    | {
        [name: string]: {
          value: number;
          colour: string;
        };
      };
  disableClustering?: boolean;
}

export interface Recording {
  configure(): AppenderFunction;
  replay(): LoggingEvent[];
  playback(): LoggingEvent[];
  reset(): void;
  erase(): void;
}

export interface Logger {
  new (name: string): Logger;

  readonly category: string;
  level: Level | string;

  log(level: Level | string, ...args: any[]): void;

  isLevelEnabled(level?: string): boolean;

  isTraceEnabled(): boolean;
  isDebugEnabled(): boolean;
  isInfoEnabled(): boolean;
  isWarnEnabled(): boolean;
  isErrorEnabled(): boolean;
  isFatalEnabled(): boolean;

  _log(level: Level, data: any): void;

  addContext(key: string, value: any): void;

  removeContext(key: string): void;

  clearContext(): void;

  /**
   * Replace the basic parse function with a new custom one
   * - Note that linesToSkip will be based on the origin of the Error object in addition to the callStackLinesToSkip (at least 1)
   * @param parseFunction the new parseFunction. Use `undefined` to reset to the base implementation
   */
  setParseCallStackFunction(
    parseFunction: (error: Error, linesToSkip: number) => CallStack | undefined
  ): void;

  /**
   * Adjust the value of linesToSkip when the parseFunction is called.
   *
   * Cannot be less than 0.
   */
  callStackLinesToSkip: number;

  trace(message: any, ...args: any[]): void;

  debug(message: any, ...args: any[]): void;

  info(message: any, ...args: any[]): void;

  warn(message: any, ...args: any[]): void;

  error(message: any, ...args: any[]): void;

  fatal(message: any, ...args: any[]): void;

  mark(message: any, ...args: any[]): void;
}
