import type { ParseOptions } from './lrc';

// match `[12:30.1][12:30.2]`
export const SQUARE_TAGS_REGEXP = /^(?:\s*\[[^\]]+\])+/;

// match `ti: The Title`
export const INFO_REGEXP = /^\s*(\w+)\s*:(.*)$/;

// match `512:34.1`
export const TIME_REGEXP = /^\s*(\d+)\s*:\s*(\d+(\s*[.:]\s*\d+)?)\s*$/;

// match `<12:30.1> word` (A2 extension) | `[12:30.1] word` (Foobar2000)
export const ENHANCED_TAG_WORD_REGEXP = /[<[](\d+:\d+(?:\.\d+)?)[>\]]([^[<]*)/;

export enum LineType {
  INVALID = 'INVALID',
  INFO = 'INFO',
  TIME = 'TIME',
}

export interface InvalidLine {
  type: LineType.INVALID;
}

interface TimeWordTimestamp {
  timestamp: number;
  content: string;
}

export interface TimeLine {
  type: LineType.TIME;
  timestamps: number[];
  wordTimestamps?: TimeWordTimestamp[];
  rawContent: string;
  content: string;
}

export interface InfoLine {
  type: LineType.INFO;
  key: string;
  value: string;
}

export function parseSquareTags(
  line: string,
): null | { tags: string[]; rawContent: string } {
  line = line.trim();
  const matches = SQUARE_TAGS_REGEXP.exec(line);
  if (matches === null) return null;
  const tag = matches[0];
  const content = line.slice(tag.length);
  return {
    tags: tag.slice(1, -1).split(/\]\s*\[/),
    rawContent: content,
  };
}

function parseTimestamp(str: string): number | null {
  const matches = TIME_REGEXP.exec(str);
  if (!matches) return null;
  const minuteStr = matches[1] ?? '0';
  const secondStr = matches[2] ?? '0';
  const minutes = parseFloat(minuteStr);
  const seconds = parseFloat(secondStr.replace(/\s+/g, '').replace(':', '.'));
  return minutes * 60 + seconds;
}

export function parseEnhancedWords(
  timestamps: number[],
  rawContent: string,
): TimeLine | null {
  const wordTimestamps: TimeWordTimestamp[] = [];
  let stripContent = '';
  let stripIndex = 0;
  const pushContent = (timestamp: number, wordContent: string) => {
    if (!wordContent.trim()) return;
    if (stripContent.endsWith(' ') && wordContent.startsWith(' ')) {
      wordContent = wordContent.trimStart();
    }
    stripContent += wordContent;
    wordTimestamps.push({
      timestamp,
      content: wordContent,
    });
  };

  const firstTimestamp = timestamps[timestamps.length - 1];
  if (!firstTimestamp) return null;
  const firstMatches = ENHANCED_TAG_WORD_REGEXP.exec(rawContent);
  const firstContent = firstMatches
    ? rawContent.slice(0, firstMatches.index)
    : rawContent;
  pushContent(firstTimestamp, firstContent);

  if (firstMatches)
    while (stripIndex < rawContent.length) {
      const wordMatches = ENHANCED_TAG_WORD_REGEXP.exec(
        rawContent.slice(stripIndex),
      );
      if (!wordMatches) break;
      stripIndex += wordMatches.index + wordMatches[0].length;
      const timestamp = parseTimestamp(wordMatches[1]!);
      if (timestamp === null) continue;
      const wordContent = wordMatches[2]!;
      pushContent(timestamp, wordContent);
    }

  return {
    type: LineType.TIME,
    timestamps,
    content: stripContent.trim(),
    rawContent,
    wordTimestamps,
  };
}

export function parseTime(
  tags: string[],
  rawContent: string,
  { enhanced = true }: ParseOptions = {},
): TimeLine {
  const timestamps = tags
    .map((tag) => parseTimestamp(tag))
    .filter((it) => it !== null);
  rawContent = rawContent.trim();

  if (enhanced) {
    const parsedWords = parseEnhancedWords(timestamps, rawContent);
    if (parsedWords) return parsedWords;
  }

  return {
    type: LineType.TIME,
    timestamps,
    rawContent,
    content: rawContent,
  };
}

export function parseInfo(tag: string): InfoLine | null {
  const matches = INFO_REGEXP.exec(tag);
  if (!matches) return null;
  const key = matches[1] ?? '';
  const value = matches[2] ?? '';
  return {
    type: LineType.INFO,
    key: key.trim(),
    value: value.trim(),
  };
}

const parseLineInner = (
  line: string,
  options?: ParseOptions,
): InfoLine | TimeLine | null => {
  const parsedTags = parseSquareTags(line);
  if (!parsedTags) return null;
  const { tags, rawContent } = parsedTags;
  const firstTag = tags[0];
  if (!firstTag) return null;
  if (TIME_REGEXP.test(firstTag)) {
    return parseTime(tags, rawContent, options);
  } else {
    return parseInfo(firstTag);
  }
};

/**
 * line parse lrc of timestamp
 * @example
 * const lp = parseLine('[ti: Song title]')
 * lp.type === LineParser.TYPE.INFO
 * lp.key === 'ti'
 * lp.value === 'Song title'
 *
 * const lp = parseLine('[10:10.10]hello')
 * lp.type === LineParser.TYPE.TIME
 * lp.timestamps === [10*60+10.10]
 * lp.content === 'hello'
 *
 * const lp = parseLine('[10:10.10] <10:10.12> hello <10:11.02> world')
 * lp.type === LineParser.TYPE.TIME
 * lp.timestamps === [10*60+10.10]
 * lp.content === 'hello world'
 * lp.wordTimestamps === [
 *  { timestamp: 10*60+10.12, content: 'hello' },
 *  { timestamp: 10*60+11.02, content: 'world' }
 * ]
 */
export function parseLine(
  line: string,
  options?: ParseOptions,
): InfoLine | TimeLine | InvalidLine {
  const result = parseLineInner(line, options);
  return result
    ? result
    : {
        type: LineType.INVALID,
      };
}
