import { ValueInterface } from '../value/value.interface';
import { DebugContent, LoggerInterface } from '../types/log-provider.interface';
import { LogContent, LogMessage, LogMessageType } from './log-message';

export class Logger implements LoggerInterface {
  #messages: LogMessage[] = [];
  #started: number | null = null;
  #ended: number | null = null;

  public get started() {
    return this.#started;
  }

  public get ended() {
    return this.#ended;
  }

  public get duration(): number {
    this.enforceHasEnded();
    return (this.ended || 0) - (this.started || 0);
  }

  public get length(): number {
    return this.#messages.length;
  }

  public get lastLine(): LogMessage {
    return this.#messages[this.#messages.length - 1];
  }

  public lastLineIsBreak(): boolean {
    return (
      this.lastLine.type == 'lineBreak' || this.lastLine.text.trim().length == 0
    );
  }

  public start() {
    this.enforceNotStarted();
    this.#started = Date.now();
  }

  public end() {
    this.enforceStartedButNotEnded();
    this.#ended = Date.now();
  }

  public get messages(): LogMessage[] {
    return this.#messages;
  }

  public count(type: LogMessageType | LogMessageType[]): number {
    return this.filter(type).length;
  }

  public filter(type: LogMessageType | LogMessageType[]): LogMessage[] {
    type = Array.isArray(type) ? type : [type];
    return this.#messages.filter(message => type.includes(message.type));
  }

  public get failed(): boolean {
    return this.#messages.some(message => message.type == 'fail');
  }

  protected enforceNotStarted() {
    if (this.started) throw 'Already started.';
    if (this.ended) throw 'Already ended.';
  }

  protected enforceStartedButNotEnded() {
    if (!this.started) throw 'Not yet started.';
    if (this.ended) throw 'Already ended.';
  }

  protected enforceHasEnded() {
    if (!this.started) throw 'Not yet started.';
    if (!this.ended) throw 'Not yet ended.';
  }

  protected messageToString(message: DebugContent) {
    if (
      typeof message == 'string' ||
      typeof message == 'number' ||
      typeof message == 'boolean'
    ) {
      return String(message);
    }
    return message.string.$;
  }

  public log(type: LogMessageType, content: LogContent | string) {
    this.enforceStartedButNotEnded();
    const message = new LogMessage(
      type,
      typeof content == 'string' ? { text: content } : content,
    );
    this.#messages.push(message);
    return message;
  }

  public debug(content: DebugContent | DebugContent[]) {
    const messages = Array.isArray(content) ? content : [content];
    messages.forEach(message => {
      this.log('info', { text: this.messageToString(message) });
    });
  }

  public horizontalRule() {
    this.log('horizontalRule', '-------');
  }

  public lineBreak() {
    this.log('lineBreak', '');
  }

  public info(content: string | string[]) {
    const messages = Array.isArray(content) ? content : [content];
    messages.forEach(message => {
      this.log('info', { text: this.messageToString(message) });
    });
  }

  public heading(content: string | string[]) {
    const messages = Array.isArray(content) ? content : [content];
    messages.forEach(message => {
      this.log('heading', { text: this.messageToString(message) });
    });
  }

  public action(name: string, target?: ValueInterface, text?: string) {
    this.log('action', {
      name,
      target: target?.name,
      text: text || '',
    });
  }

  public pass(content: LogContent | string | LogContent[]) {
    const messages = Array.isArray(content) ? content : [content];
    messages.forEach(message => this.log('pass', message));
  }

  public fail(
    content: LogContent | string | LogContent[],
    isOptional: boolean = false,
  ) {
    const messages = Array.isArray(content) ? content : [content];
    messages.forEach(message => {
      this.log(isOptional ? 'fail' : 'optionalFail', message);
    });
  }
}
