import type { ChangeContent, ContextContent, FileDiffMetadata, Hunk } from "@pierre/diffs";
import { LineOp, type DiffFile } from "$lib/api/diff";

// old alone collides when one file is deleted and another renamed onto its
// path in the same commit so we need new too
export const diffRowKey = (file: DiffFile): string => `${file.name.old}→${file.name.new}`;

// pierre parses patch text into FileDiffMetadata, but the wire already hands
// us hunks, so we can skip the text roundtrip by mapping
export const toFileDiffMetadata = (file: DiffFile): FileDiffMetadata => {
	const fragments = file.text_fragments ?? [];

	const hunks: Hunk[] = [];
	const deletionLines: string[] = [];
	const additionLines: string[] = [];
	let splitLineCount = 0;
	let unifiedLineCount = 0;
	let lastHunkEnd = 0;

	for (const fragment of fragments) {
		const additionStart = fragment.NewPosition ?? 0;
		const additionCount = fragment.NewLines ?? 0;
		const deletionStart = fragment.OldPosition ?? 0;
		const deletionCount = fragment.OldLines ?? 0;

		const collapsedBefore = Math.max(additionStart - 1 - lastHunkEnd, 0);
		lastHunkEnd = additionStart + additionCount - 1;

		const hunk: Hunk = {
			collapsedBefore,
			additionStart,
			additionCount,
			additionLines: 0,
			additionLineIndex: additionLines.length,
			deletionStart,
			deletionCount,
			deletionLines: 0,
			deletionLineIndex: deletionLines.length,
			hunkContent: [],
			hunkContext: fragment.Comment,
			hunkSpecs: `@@ -${deletionStart},${deletionCount} +${additionStart},${additionCount} @@`,
			splitLineStart: 0,
			splitLineCount: 0,
			unifiedLineStart: 0,
			unifiedLineCount: 0,
			noEOFCRDeletions: false,
			noEOFCRAdditions: false
		};

		let current: ContextContent | ChangeContent | null = null;
		let lastOp: LineOp | null = null;
		for (const { Op: op, Line: raw } of fragment.Lines ?? []) {
			if (op === LineOp.Context) {
				if (current?.type !== "context") {
					current = {
						type: "context",
						lines: 0,
						additionLineIndex: additionLines.length,
						deletionLineIndex: deletionLines.length
					};
					hunk.hunkContent.push(current);
				}
				current.lines++;
				additionLines.push(raw);
				deletionLines.push(raw);
			} else {
				if (current?.type !== "change") {
					current = {
						type: "change",
						additions: 0,
						deletions: 0,
						additionLineIndex: additionLines.length,
						deletionLineIndex: deletionLines.length
					};
					hunk.hunkContent.push(current);
				}
				if (op === LineOp.Add) {
					current.additions++;
					hunk.additionLines++;
					additionLines.push(raw);
				} else {
					current.deletions++;
					hunk.deletionLines++;
					deletionLines.push(raw);
				}
			}
			lastOp = op;
		}

		// a line without a trailing newline is the patch's "\ No newline"
		// marker, only possible at the end of the file
		const last = fragment.Lines?.[fragment.Lines.length - 1];
		if (last && !last.Line.endsWith("\n")) {
			if (last.Op === LineOp.Context) {
				hunk.noEOFCRAdditions = true;
				hunk.noEOFCRDeletions = true;
			} else if (last.Op === LineOp.Delete) {
				hunk.noEOFCRDeletions = true;
			} else {
				hunk.noEOFCRAdditions = true;
			}
			if (lastOp === LineOp.Add || lastOp === LineOp.Context) {
				additionLines[additionLines.length - 1] = last.Line;
			}
			if (lastOp === LineOp.Delete || lastOp === LineOp.Context) {
				deletionLines[deletionLines.length - 1] = last.Line;
			}
		}

		for (const content of hunk.hunkContent) {
			if (content.type === "context") {
				hunk.splitLineCount += content.lines;
				hunk.unifiedLineCount += content.lines;
			} else {
				hunk.splitLineCount += Math.max(content.additions, content.deletions);
				hunk.unifiedLineCount += content.deletions + content.additions;
			}
		}
		hunk.splitLineStart = splitLineCount + collapsedBefore;
		hunk.unifiedLineStart = unifiedLineCount + collapsedBefore;
		splitLineCount += collapsedBefore + hunk.splitLineCount;
		unifiedLineCount += collapsedBefore + hunk.unifiedLineCount;

		hunks.push(hunk);
	}

	const type: FileDiffMetadata["type"] = file.is_new
		? "new"
		: file.is_delete
			? "deleted"
			: file.is_rename
				? "rename-changed"
				: "change";

	return {
		name: file.name.new || file.name.old,
		prevName: type === "rename-changed" ? file.name.old : undefined,
		type,
		hunks,
		splitLineCount,
		unifiedLineCount,
		isPartial: true,
		deletionLines,
		additionLines
	};
};
