import type { ProjectFilePath } from '@learnitive/project-storage';

export type ProjectGitDiffLine = {
  kind: 'context' | 'add' | 'remove';
  text: string;
};

export type ProjectGitDiff = {
  path: ProjectFilePath;
  binary: boolean;
  oldSize: number;
  newSize: number;
  lines?: readonly ProjectGitDiffLine[];
};

const MAX_DIFF_MATRIX_CELLS = 2_000_000;

function splitLines(text: string): string[] {
  if (text === '') {
    return [];
  }
  return text.replace(/\r\n/g, '\n').split('\n');
}

function simplifiedDiff(
  oldLines: string[],
  newLines: string[],
): ProjectGitDiffLine[] {
  let prefix = 0;
  while (
    prefix < oldLines.length &&
    prefix < newLines.length &&
    oldLines[prefix] === newLines[prefix]
  ) {
    prefix += 1;
  }

  let suffix = 0;
  while (
    suffix < oldLines.length - prefix &&
    suffix < newLines.length - prefix &&
    oldLines[oldLines.length - 1 - suffix] ===
      newLines[newLines.length - 1 - suffix]
  ) {
    suffix += 1;
  }

  return [
    ...oldLines.slice(0, prefix).map((text) => ({
      kind: 'context' as const,
      text,
    })),
    ...oldLines.slice(prefix, oldLines.length - suffix).map((text) => ({
      kind: 'remove' as const,
      text,
    })),
    ...newLines.slice(prefix, newLines.length - suffix).map((text) => ({
      kind: 'add' as const,
      text,
    })),
    ...(suffix === 0
      ? []
      : newLines.slice(newLines.length - suffix).map((text) => ({
          kind: 'context' as const,
          text,
        }))),
  ];
}

/**
 * Produces a stable line diff without pulling a second diff implementation
 * into the app. Normal source files use LCS; very large comparisons retain
 * common prefix/suffix and mark the changed middle as remove/add blocks.
 */
export function createProjectTextDiff(
  oldText: string,
  newText: string,
): ProjectGitDiffLine[] {
  const oldLines = splitLines(oldText);
  const newLines = splitLines(newText);

  if (oldLines.length * newLines.length > MAX_DIFF_MATRIX_CELLS) {
    return simplifiedDiff(oldLines, newLines);
  }

  const width = newLines.length + 1;
  const lcs = new Uint32Array((oldLines.length + 1) * width);
  for (let oldIndex = oldLines.length - 1; oldIndex >= 0; oldIndex -= 1) {
    for (let newIndex = newLines.length - 1; newIndex >= 0; newIndex -= 1) {
      const index = oldIndex * width + newIndex;
      lcs[index] =
        oldLines[oldIndex] === newLines[newIndex]
          ? 1 + (lcs[(oldIndex + 1) * width + newIndex + 1] ?? 0)
          : Math.max(
              lcs[(oldIndex + 1) * width + newIndex] ?? 0,
              lcs[oldIndex * width + newIndex + 1] ?? 0,
            );
    }
  }

  const result: ProjectGitDiffLine[] = [];
  let oldIndex = 0;
  let newIndex = 0;
  while (oldIndex < oldLines.length && newIndex < newLines.length) {
    if (oldLines[oldIndex] === newLines[newIndex]) {
      result.push({ kind: 'context', text: oldLines[oldIndex] ?? '' });
      oldIndex += 1;
      newIndex += 1;
    } else if (
      (lcs[(oldIndex + 1) * width + newIndex] ?? 0) >=
      (lcs[oldIndex * width + newIndex + 1] ?? 0)
    ) {
      result.push({ kind: 'remove', text: oldLines[oldIndex] ?? '' });
      oldIndex += 1;
    } else {
      result.push({ kind: 'add', text: newLines[newIndex] ?? '' });
      newIndex += 1;
    }
  }
  while (oldIndex < oldLines.length) {
    result.push({ kind: 'remove', text: oldLines[oldIndex] ?? '' });
    oldIndex += 1;
  }
  while (newIndex < newLines.length) {
    result.push({ kind: 'add', text: newLines[newIndex] ?? '' });
    newIndex += 1;
  }
  return result;
}

export function decodeGitText(bytes: Uint8Array): string | undefined {
  const sampleLength = Math.min(bytes.length, 8 * 1024);
  for (let index = 0; index < sampleLength; index += 1) {
    if (bytes[index] === 0) {
      return undefined;
    }
  }
  try {
    return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
  } catch {
    return undefined;
  }
}
