import type { Chunk, Highlight } from "./types";

export interface Span {
	text: string;
	match?: boolean;
}

export interface Line {
	num: number;
	/** empty for a blank line */
	spans: Span[];
	/** true when any part of this line matched */
	highlight: boolean;
}

const encoder = new TextEncoder();
const decoder = new TextDecoder();

/** sorts and merges overlapping or adjacent ranges */
function mergeRanges(highlights: Highlight[]): Highlight[] {
	const sorted = highlights.filter((h) => h.end > h.start).sort((a, b) => a.start - b.start);
	const out: Highlight[] = [];
	for (const h of sorted) {
		const last = out.at(-1);
		if (last && h.start <= last.end) {
			last.end = Math.max(last.end, h.end);
		} else {
			out.push({ ...h });
		}
	}
	return out;
}

function lineAt(
	bytes: Uint8Array,
	start: number,
	end: number,
	num: number,
	ranges: Highlight[]
): Line {
	const slice = (from: number, to: number) => decoder.decode(bytes.subarray(from, to));
	const spans: Span[] = [];
	let highlight = false;
	let pos = start;

	// ranges are sorted, so walking them once keeps `pos` monotonic
	for (const r of ranges) {
		const s = Math.max(r.start, start);
		const e = Math.min(r.end, end);
		if (s >= e) continue;
		highlight = true;
		if (s > pos) spans.push({ text: slice(pos, s) });
		spans.push({ text: slice(s, e), match: true });
		pos = e;
	}
	if (pos < end) spans.push({ text: slice(pos, end) });

	return { num, spans, highlight };
}

/**
 * Renders a chunk's content into per-line spans for display.
 *
 * Highlights are byte offsets into `content` and a single range may span
 * newlines, so the split happens in byte space — UTF-16 string indices would
 * skew on any line containing non-ASCII. Inverse of `chunkHighlights` in
 * appview/xrpc/search.go.
 */
export function chunkLines(chunk: Chunk): Line[] {
	const ranges = mergeRanges(chunk.highlights ?? []);
	// trim a single trailing newline so we don't emit a spurious empty line
	const bytes = encoder.encode(chunk.content.replace(/\n$/, ""));
	const firstNum = Math.max(1, chunk.lineStart);

	const lines: Line[] = [];
	let start = 0;
	for (let i = 0; i <= bytes.length; i++) {
		if (i < bytes.length && bytes[i] !== 0x0a) continue;
		lines.push(lineAt(bytes, start, i, firstNum + lines.length, ranges));
		start = i + 1;
	}
	return lines;
}

/** total match count across a file result's chunks, for the stats line */
export const matchCount = (chunks: Chunk[]) =>
	chunks.reduce((n, c) => n + (c.highlights?.length ?? 0), 0);
