import { diffFileName, type DiffFile, type NiceDiff, type TextFragment } from "./diff";
import { hexFromBytes } from "./repo";

// port of appview/repo/rawdiff.go

const OP_CHARS = [" ", "-", "+"] as const;

const opChar = (op: number): string => OP_CHARS[op] ?? "?";

const noEol = (line: string): boolean => line.length === 0 || !line.endsWith("\n");

const fragmentHeader = (fragment: TextFragment): string => {
	const header = `@@ -${fragment.OldPosition ?? 0},${fragment.OldLines ?? 0} +${fragment.NewPosition ?? 0},${fragment.NewLines ?? 0} @@`;
	return fragment.Comment ? `${header} ${fragment.Comment}` : header;
};

const fragmentString = (fragment: TextFragment): string => {
	let out = `${fragmentHeader(fragment)}\n`;
	for (const line of fragment.Lines ?? []) {
		out += opChar(line.Op) + line.Line;
		if (noEol(line.Line)) out += "\n\\ No newline at end of file\n";
	}
	return out;
};

export const fileStats = (file: DiffFile): { insertions: number; deletions: number } => {
	let insertions = 0;
	let deletions = 0;
	for (const fragment of file.text_fragments ?? []) {
		insertions += fragment.LinesAdded ?? 0;
		deletions += fragment.LinesDeleted ?? 0;
	}
	return { insertions, deletions };
};

export const renderUnifiedDiff = (niceDiff: NiceDiff | null | undefined): string => {
	if (!niceDiff) return "";
	let out = "";
	for (const file of niceDiff.diff ?? []) {
		const oldName = file.name.old || file.name.new;
		const newName = file.name.new || file.name.old;

		out += `diff --git a/${oldName} b/${newName}\n`;
		if (file.is_new) {
			out += "new file mode 100644\n";
			out += "--- /dev/null\n";
			out += `+++ b/${newName}\n`;
		} else if (file.is_delete) {
			out += "deleted file mode 100644\n";
			out += `--- a/${oldName}\n`;
			out += "+++ /dev/null\n";
		} else if (file.is_rename) {
			out += `rename from ${oldName}\n`;
			out += `rename to ${newName}\n`;
			out += `--- a/${oldName}\n`;
			out += `+++ b/${newName}\n`;
		} else {
			out += `--- a/${oldName}\n`;
			out += `+++ b/${newName}\n`;
		}

		for (const fragment of file.text_fragments ?? []) {
			out += fragmentString(fragment);
		}
	}
	return out;
};

const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] as const;
const MONTHS = [
	"Jan",
	"Feb",
	"Mar",
	"Apr",
	"May",
	"Jun",
	"Jul",
	"Aug",
	"Sep",
	"Oct",
	"Nov",
	"Dec"
] as const;

const pad = (value: number): string => String(value).padStart(2, "0");

// RFC1123Z, always utc
const formatPatchDate = (input: string): string => {
	const date = new Date(input);
	// a missing When is go's zero time, formats to this exact string
	if (Number.isNaN(date.getTime())) return "Mon, 01 Jan 0001 00:00:00 +0000";
	return (
		`${WEEKDAYS[date.getUTCDay()]}, ${pad(date.getUTCDate())} ${MONTHS[date.getUTCMonth()]} ` +
		`${String(date.getUTCFullYear()).padStart(4, "0")} ${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:${pad(date.getUTCSeconds())} +0000`
	);
};

export const renderFormatPatch = (niceDiff: NiceDiff | null | undefined): string => {
	if (!niceDiff) return "";
	const commit = niceDiff.commit ?? {};
	const message = commit.message ?? "";

	// format-patch takes the first LINE as the subject, unlike splitMessage's
	// first paragraph
	const newline = message.indexOf("\n");
	const subject = newline === -1 ? message : message.slice(0, newline);

	const separator = message.indexOf("\n\n");
	const body = separator === -1 ? "" : message.slice(separator + 2).replace(/\n+$/, "");

	const hash = commit.this || hexFromBytes(commit.hash ?? []);

	let out = `From ${hash} Mon Sep 17 00:00:00 2001\n`;
	out += `From: ${commit.author?.Name ?? ""} <${commit.author?.Email ?? ""}>\n`;
	out += `Date: ${formatPatchDate(commit.author?.When ?? "")}\n`;
	out += `Subject: [PATCH] ${subject}\n`;
	out += "\n";
	if (body !== "") out += `${body}\n`;
	out += "---\n";

	for (const file of niceDiff.diff ?? []) {
		const name = diffFileName(file);
		const { insertions, deletions } = fileStats(file);
		out += ` ${name} | ${insertions + deletions} ${"+".repeat(insertions)}${"-".repeat(deletions)}\n`;
	}
	const stat = niceDiff.stat ?? { insertions: 0, deletions: 0, files_changed: 0 };
	out += ` ${stat.files_changed} file(s) changed, ${stat.insertions} insertion(s)(+), ${stat.deletions} deletion(s)(-)\n\n`;

	out += renderUnifiedDiff(niceDiff);
	out += "\n--\ntangled.sh\n";
	return out;
};
