import tokenize from '@nxmix/tokenize-ansi';
import { FontWeight } from '@xterm/xterm';

import {
  colors,
  BACKGROUND_COLORS_BY_ANSI,
  FOREGROUND_COLORS_BY_ANSI,
  RGBColor,
} from './colors';
import { formatJSONLine } from './formatJSONLogs';
import { formatZerologLogs, ZerologRegex } from './formatZerologLogs';
import { Token, Span, TIMESTAMP_LENGTH, FormattedLine } from './types';

type FormatOptions = {
  stripHeaders?: boolean;
  withTimestamps?: boolean;
  splitter?: string;
};

const defaultOptions: FormatOptions = {
  splitter: '\n',
};

export function formatLogs(
  rawLogs: string,
  {
    stripHeaders,
    withTimestamps,
    splitter = '\n',
  }: FormatOptions = defaultOptions
) {
  let logs = rawLogs;
  if (stripHeaders) {
    logs = stripHeadersFunc(logs);
  }
  // if JSON logs come serialized 2 times, parse them once to unwrap them
  if (logs.startsWith('"')) {
    try {
      logs = JSON.parse(logs);
    } catch (error) {
      // noop, throw error away if logs cannot be parsed
    }
  }

  const tokens: Token[][] = tokenize(logs);
  const formattedLogs: FormattedLine[] = [];

  let fgColor: string | undefined;
  let bgColor: string | undefined;
  let fontWeight: FontWeight | undefined;
  let line = '';
  let spans: Span[] = [];

  tokens.forEach((token) => {
    const [type] = token;

    const fgAnsi = FOREGROUND_COLORS_BY_ANSI[type];
    const bgAnsi = BACKGROUND_COLORS_BY_ANSI[type];

    if (fgAnsi) {
      fgColor = cssColorFromRgb(fgAnsi);
    } else if (type === 'moreColor') {
      fgColor = extendedColorForToken(token);
    } else if (type === 'fgDefault') {
      fgColor = undefined;
    } else if (bgAnsi) {
      bgColor = cssColorFromRgb(bgAnsi);
    } else if (type === 'bgMoreColor') {
      bgColor = extendedColorForToken(token);
    } else if (type === 'bgDefault') {
      bgColor = undefined;
    } else if (type === 'reset') {
      fgColor = undefined;
      bgColor = undefined;
      fontWeight = undefined;
    } else if (type === 'bold') {
      fontWeight = 'bold';
    } else if (type === 'normal') {
      fontWeight = 'normal';
    } else if (type === 'text') {
      const tokenLines = (token[1] as string).split(splitter);

      tokenLines.forEach((tokenLine, idx) => {
        if (idx && line) {
          formattedLogs.push({ line, spans });
          line = '';
          spans = [];
        }

        const text = stripEscapeCodes(tokenLine);
        try {
          if (
            (!withTimestamps && text.startsWith('{')) ||
            (withTimestamps && text.substring(TIMESTAMP_LENGTH).startsWith('{'))
          ) {
            const lines = formatJSONLine(text, withTimestamps);
            formattedLogs.push(...lines);
          } else if (
            (!withTimestamps && ZerologRegex.test(text)) ||
            (withTimestamps &&
              ZerologRegex.test(text.substring(TIMESTAMP_LENGTH)))
          ) {
            const lines = formatZerologLogs(text, withTimestamps);
            formattedLogs.push(...lines);
          } else {
            spans.push({ fgColor, bgColor, text, fontWeight });
            line += text;
          }
        } catch (error) {
          // in case parsing fails for whatever reason, push the raw logs and continue
          spans.push({ fgColor, bgColor, text, fontWeight });
          line += text;
        }
      });
    }
  });
  if (line) {
    formattedLogs.push({ line, spans });
  }

  return formattedLogs;
}

function stripHeadersFunc(logs: string) {
  return logs.substring(8).replace(/\r?\n(.{8})/g, '\n');
}

function stripEscapeCodes(logs: string) {
  return logs.replace(
    // eslint-disable-next-line no-control-regex
    /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
    ''
  );
}

function cssColorFromRgb(rgb: RGBColor) {
  const [r, g, b] = rgb;

  return `rgb(${r}, ${g}, ${b})`;
}

// assuming types based on original JS implementation
// there is not much type definitions for the tokenize library
function extendedColorForToken(token: Token[]) {
  const [, colorMode, colorRef] = token as [undefined, number, number];

  if (colorMode === 2) {
    return cssColorFromRgb(token.slice(2) as RGBColor);
  }

  if (colorMode === 5 && colors[colorRef]) {
    return cssColorFromRgb(colors[colorRef]);
  }

  return '';
}
