import cluster from 'cluster';
import { WrappingLogger } from './Logger';
import type { Logger } from './Logger';
import type { LoggerFactory } from './LoggerFactory';
import type { LogLevel } from './LogLevel';

/**
 * Temporary {@link LoggerFactory} that buffers log messages in memory
 * until the {@link TemporaryLoggerFactory#switch} method is called.
 */
class TemporaryLoggerFactory implements LoggerFactory {
  private bufferSpaces: number;
  private readonly wrappers: { wrapper: WrappingLogger; label: string }[] = [];
  private readonly buffer: { logger: Logger; level: LogLevel; message: string }[] = [];

  public constructor(bufferSize = 1024) {
    this.bufferSpaces = bufferSize;
  }

  public createLogger(label: string): WrappingLogger {
    const wrapper = new WrappingLogger({
      log: (level: LogLevel, message: string): Logger =>
        this.bufferLogEntry(wrapper, level, message),
    });
    this.wrappers.push({ wrapper, label });
    return wrapper;
  }

  private bufferLogEntry(logger: WrappingLogger, level: LogLevel, message: string): Logger {
    // Buffer the message if spaces are still available
    if (this.bufferSpaces > 0) {
      this.bufferSpaces -= 1;
      // If this is the last space, instead generate a warning through a new logger
      if (this.bufferSpaces === 0) {
        logger = this.createLogger('LazyLoggerFactory');
        level = 'warn';
        message = `Memory-buffered logging limit of ${this.buffer.length + 1} reached`;
      }
      this.buffer.push({ logger, level, message });
    }
    return logger;
  }

  /**
   * Swaps all lazy loggers to new loggers from the given factory,
   * and emits any buffered messages through those actual loggers.
   */
  public switch(loggerFactory: LoggerFactory): void {
    // Instantiate an actual logger within every lazy logger
    for (const { wrapper, label } of this.wrappers.splice(0, this.wrappers.length)) {
      wrapper.logger = loggerFactory.createLogger(label);
    }
    // Emit all buffered log messages
    for (const { logger, level, message } of this.buffer.splice(0, this.buffer.length)) {
      logger.log(level, message, { isPrimary: cluster.isMaster, pid: process.pid });
    }
  }
}

/**
 * Wraps around another {@link LoggerFactory} that can be set lazily.
 * This is useful when objects are instantiated (and when they create loggers)
 * before the logging system has been fully instantiated,
 * as is the case when using a dependency injection framework such as Components.js.
 *
 * Loggers can be created even before a {@link LoggerFactory} is set;
 * any log messages will be buffered and re-emitted.
 */
export class LazyLoggerFactory implements LoggerFactory {
  private factory: LoggerFactory;

  public constructor(options: { bufferSize?: number } = {}) {
    this.factory = new TemporaryLoggerFactory(options.bufferSize);
  }

  public get loggerFactory(): LoggerFactory {
    if (this.factory instanceof TemporaryLoggerFactory) {
      throw new Error('Logger factory not yet set.');
    }
    return this.factory;
  }

  public set loggerFactory(loggerFactory: LoggerFactory) {
    if (this.factory instanceof TemporaryLoggerFactory) {
      this.factory.switch(loggerFactory);
    }
    this.factory = loggerFactory;
  }

  public createLogger(label: string): Logger {
    return this.factory.createLogger(label);
  }
}
