import * as data from "./data.ts";

type Color = { r: number; g: number; b: number };

function lerp2(ratio: number, start: number, end: number) {
    return (1 - ratio) * start + ratio * end;
}

function lerp3(ratio: number, start: number, middle: number, end: number) {
    return (1 - ratio) * lerp2(ratio, start, middle) +
        ratio * lerp2(ratio, middle, end);
}

function colorLerp(
    ratio: number,
    start: Color,
    middle: Color,
    end: Color,
): Color {
    return {
        r: lerp3(ratio, start.r, middle.r, end.r),
        g: lerp3(ratio, start.g, middle.g, end.g),
        b: lerp3(ratio, start.b, middle.b, end.b),
    };
}

function colorToString(color: Color): string {
    return `rgb(${color.r}, ${color.g}, ${color.b})`;
}

const GREEN = { r: 42, g: 121, b: 82 };
const YELLOW = {
    r: 247,
    g: 203,
    b: 21,
};
const RED = {
    r: 167,
    g: 29,
    b: 49,
};

export type CodeCovRender = "performance" | "test-coverage";

export function loadCodeCoverage(
    text: string,
    input: data.CodeCovEntry[],
    tooltip: HTMLElement,
    mode: CodeCovRender,
): HTMLPreElement {
    const container = document.createElement("pre");
    container.classList.add("code-source");
    if (input.length === 0) {
        return container;
    }
    const entries = input.toSorted((
        a: data.CodeCovEntry,
        b: data.CodeCovEntry,
    ) => b.index - a.index);
    const charEntries: { [key: string]: data.CodeCovEntry } = {};
    const elements: HTMLElement[] = [];
    let line = 1;
    let col = 1;
    const maxPerfCovers = entries.map((v) => v.covers).reduce((acc, v) =>
        acc > Math.log10(v) ? acc : Math.log10(v)
    );
    for (let index = 0; index < text.length; ++index) {
        if (text[index] === "\n") {
            col = 1;
            line += 1;
            const newlineSpan = document.createElement("span");
            newlineSpan.innerText = "\n";
            elements.push(newlineSpan);
            continue;
        }
        const entry = entries.find((entry) => index >= entry.index);
        if (!entry) {
            throw new Error("unreachable");
        }
        charEntries[`${line}-${col}`] = entry;

        const perfColor = (ratio: number) => {
            const clr = colorLerp(ratio, GREEN, YELLOW, RED);
            return colorToString(clr);
        };

        const span = document.createElement("span");
        span.style.backgroundColor = mode === "performance"
            ? perfColor(
                Math.log10(entry.covers) / maxPerfCovers,
            )
            : entry.covers > 0
            ? colorToString(GREEN)
            : colorToString(RED);
        span.innerText = text[index];
        span.dataset.covers = entry.covers.toString();
        elements.push(span);
        col += 1;
    }
    container.append(...elements);
    document.addEventListener("mousemove", (event) => {
        const [x, y] = [event.clientX, event.clientY];
        const outerBox = container.getBoundingClientRect();
        if (!positionInBox([x, y], outerBox)) {
            tooltip.hidden = true;
            return;
        }
        const element = elements.find((element) => {
            if (typeof element === "string") {
                return false;
            }
            if (!element.dataset.covers) {
                return false;
            }
            const isIn = positionInBox([x, y], element.getBoundingClientRect());
            return isIn;
        });
        if (!element) {
            tooltip.hidden = true;
            return;
        }
        const maybeCovers = element.dataset.covers;
        if (!maybeCovers) {
            throw new Error("unreachable");
        }
        const covers = parseInt(maybeCovers);
        tooltip.hidden = false;
        tooltip.style.left = `${event.clientX + 20}px`;
        tooltip.style.top = `${event.clientY + 20}px`;
        tooltip.innerText = `Ran ${covers} time${covers !== 1 ? "s" : ""}`;
    });
    return container;
}

function positionInBox(
    position: [number, number],
    boundingRect: {
        left: number;
        top: number;
        right: number;
        bottom: number;
    },
) {
    const [x, y] = position;
    const outside = x < boundingRect.left ||
        x >= boundingRect.right || y < boundingRect.top ||
        y >= boundingRect.bottom;
    return !outside;
}
