import { diffWordsWithSpace } from 'diff';

import { AlignedLyric } from '../types';
import { getPlaintextLyrics } from './getPlaintextLyrics';
import {
  getLyricEndSeconds,
  getLyricStartSeconds,
} from './refineAlignedLyrics';

const getEditedLyric = (
  alignedLyrics: AlignedLyric[],
  editCharOffset: number,
  endOpen: boolean = false
): { lyric: AlignedLyric; offset: number } => {
  let accumulatedOffset = 0;
  if (editCharOffset === 0) {
    return { lyric: alignedLyrics[0], offset: 0 };
  }
  for (let i = 0; i < alignedLyrics.length; i++) {
    accumulatedOffset += alignedLyrics[i].text.length;
    if (
      endOpen
        ? accumulatedOffset > editCharOffset
        : accumulatedOffset >= editCharOffset
    ) {
      return {
        lyric: alignedLyrics[i],
        offset:
          editCharOffset - (accumulatedOffset - alignedLyrics[i].text.length),
      };
    }
  }
  return {
    lyric: alignedLyrics[alignedLyrics.length - 1],
    offset: alignedLyrics[alignedLyrics.length - 1].text.length,
  };
};

const getNextLyricStartSeconds = (
  alignedLyrics: AlignedLyric[],
  index: number
) => {
  let currentIndex = index;
  while (currentIndex < alignedLyrics.length) {
    if (alignedLyrics[currentIndex].timing) {
      return getLyricStartSeconds(alignedLyrics[currentIndex]);
    }
    currentIndex++;
  }
  return Infinity;
};

export default function getChangedAlignedLyrics(
  alignedLyricsBefore: AlignedLyric[],
  text: string
): AlignedLyric[] {
  const plaintextAlignedLyrics = getPlaintextLyrics(alignedLyricsBefore);
  const diff = diffWordsWithSpace(plaintextAlignedLyrics, text);
  let accumulatedChars = 0;
  if (alignedLyricsBefore.length === 0) {
    return [{ text, dirty: true, timing: null }];
  }
  const operationsByLyric: Map<
    AlignedLyric,
    (
      | {
          type: 'delete';
          range: { from: number; to: number };
        }
      | {
          type: 'insert';
          text: string;
          offset: number;
        }
    )[]
  > = new Map();

  const pushOperation = (lyric: AlignedLyric, operation: any) => {
    let list = operationsByLyric.get(lyric);
    if (!list) {
      list = [];
      operationsByLyric.set(lyric, list);
    }
    list.push(operation);
  };

  diff.forEach((part) => {
    if (!part.removed && !part.added) {
      accumulatedChars += part.value.length;
    } else if (part.removed) {
      const firstAffectedLyric = getEditedLyric(
        alignedLyricsBefore,
        accumulatedChars,
        true
      );
      accumulatedChars += part.value.length;
      const lastAffectedLyric = getEditedLyric(
        alignedLyricsBefore,
        accumulatedChars
      );
      if (firstAffectedLyric.lyric === lastAffectedLyric.lyric) {
        pushOperation(firstAffectedLyric.lyric, {
          type: 'delete',
          meta: 'single',
          full: part.value,
          range: {
            from: firstAffectedLyric.offset,
            to: lastAffectedLyric.offset,
          },
        });
      } else {
        if (firstAffectedLyric.offset < firstAffectedLyric.lyric.text.length) {
          pushOperation(firstAffectedLyric.lyric, {
            type: 'delete',
            meta: 'first',
            full: part.value,
            range: {
              from: firstAffectedLyric.offset,
              to: firstAffectedLyric.lyric.text.length,
            },
          });
        }
        for (
          let i = alignedLyricsBefore.indexOf(firstAffectedLyric.lyric) + 1;
          i < alignedLyricsBefore.indexOf(lastAffectedLyric.lyric);
          i++
        ) {
          pushOperation(alignedLyricsBefore[i], {
            type: 'delete',
            meta: 'in-range',
            full: part.value,
            range: { from: 0, to: alignedLyricsBefore[i].text.length },
          });
        }
        if (lastAffectedLyric.offset > 0) {
          pushOperation(lastAffectedLyric.lyric, {
            type: 'delete',
            meta: 'last',
            full: part.value,
            range: { from: 0, to: lastAffectedLyric.offset },
          });
        }
      }
    } else if (part.added) {
      const firstAffectedLyric = getEditedLyric(
        alignedLyricsBefore,
        accumulatedChars,
        true
      );
      pushOperation(firstAffectedLyric.lyric, {
        type: 'insert',
        text: part.value,
        offset: firstAffectedLyric.offset,
      });
    }
  });

  const result: AlignedLyric[] = [];
  let lastLyricEnd = 0;
  let nextLyricStart = 0;
  let currentLyricStart = 0;
  let currentLyricEnd = 0;
  alignedLyricsBefore.forEach((lyric, index, list) => {
    const operations = operationsByLyric.get(lyric);
    currentLyricStart = getLyricStartSeconds(lyric, lastLyricEnd);
    currentLyricEnd = getLyricEndSeconds(lyric, currentLyricStart);

    if (!operations) {
      result.push(lyric);
      lastLyricEnd = currentLyricEnd;
      nextLyricStart = getNextLyricStartSeconds(list, index);

      return;
    }
    let text = lyric.text;
    let offset = 0;

    operations.forEach((operation, index, list) => {
      if (operation.type === 'delete') {
        text =
          text.slice(0, operation.range.from + offset) +
          text.slice(operation.range.to + offset);
        offset -= operation.range.to - operation.range.from;
      } else if (operation.type === 'insert') {
        let currentOffset = operation.offset;
        let currentText = operation.text;
        if (
          index === 0 &&
          operation.offset === 0 &&
          operation.text.trimEnd() !== operation.text
        ) {
          // if the first operation is:
          // - an insert
          // - at the start of the lyric
          // - and the inserted content ends with whitespace
          // then the inserted content should be a new lyric, not an adjustment of the existing lyric.
          result.push({
            timing: {
              type: 'range',
              startSeconds: lastLyricEnd,
              endSeconds: currentLyricStart,
            },
            text: operation.text,
            dirty: true,
          });
          currentOffset = 0;
          currentText = '';
        } else if (
          index === list.length - 1 &&
          operation.offset === lyric.text.length &&
          operation.text.trimStart() !== operation.text
        ) {
          // if the last operation is:
          // - an insert
          // - at the end of the lyric
          // - and the inserted content starts with whitespace
          // then the inserted content should be a new lyric, not an adjustment of the existing lyric.
          if (text.length > 0) {
            result.push({ ...lyric, text, dirty: text !== lyric.text });
          }
          result.push({
            timing: {
              type: 'range',
              startSeconds: currentLyricEnd,
              endSeconds: nextLyricStart,
            },
            text: operation.text,
            dirty: true,
          });
          text = '';
          offset = 0;
          return;
        }
        text =
          text.slice(0, currentOffset + offset) +
          currentText +
          text.slice(currentOffset + offset);
        offset += operation.text.length;
      }
    });
    lastLyricEnd = currentLyricEnd;
    nextLyricStart = getNextLyricStartSeconds(list, index);
    if (text.length > 0) {
      result.push({ ...lyric, text, dirty: text !== lyric.text });
    }
  });

  if (result[result.length - 1].dirty && result[result.length - 1].timing) {
    result[result.length - 1] = {
      ...result[result.length - 1],
      timing: {
        type: 'range',
        startSeconds: getLyricStartSeconds(result[result.length - 1]),
        endSeconds: Infinity,
      },
      dirty: true,
    };
  }

  return result;
}
