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

export function loadFlameGraph(
    flameGraphData: data.FlameGraphNode,
    fnNames: data.FlameGraphFnNames,
    flameGraphDiv: HTMLDivElement,
) {
    flameGraphDiv.innerHTML = `
        <div id="fg-background">
            <div id="canvas-div">
                <canvas id="flame-graph-canvas"></canvas>
                <span id="flame-graph-tooltip" hidden></span>
            </div>
        </div>
        <div id="toolbar">
            <button id="flame-graph-reset">Reset</button>
        </div>
    `;

    const canvas = document.querySelector<HTMLCanvasElement>(
        "#flame-graph-canvas",
    )!;
    const resetButton = document.querySelector<HTMLButtonElement>(
        "#flame-graph-reset",
    )!;

    canvas.width = 1000;
    canvas.height = 500;

    const fnNameFont = "600 14px 'Roboto Mono'";

    const ctx = canvas.getContext("2d")!;
    ctx.font = fnNameFont;

    type CalcNode = {
        x: number;
        y: number;
        w: number;
        h: number;
        title: string;
        percent: string;
        fgNode: data.FlameGraphNode;
    };

    function calculateNodeRects(
        nodes: CalcNode[],
        node: data.FlameGraphNode,
        depth: number,
        totalAcc: data.FlameGraphNode["acc"],
        offsetAcc: data.FlameGraphNode["acc"],
    ) {
        const x = (offsetAcc / totalAcc) * canvas.width;
        const y = canvas.height - 30 * depth - 30;
        const w = ((node.acc + 1) / totalAcc) * canvas.width;
        const h = 30;

        const title = node.fn == 0
            ? "<program>"
            : fnNames[node.fn] ?? "<unknown>";
        const percent = `${(node.acc / totalAcc * 100).toFixed(1)}%`;
        nodes.push({ x, y, w, h, title, percent, fgNode: node });

        const totalChildrenAcc = node.children.reduce(
            (acc, child) => acc + child.acc,
            0,
        );
        let newOffsetAcc = offsetAcc + (node.acc - totalChildrenAcc) / 2;
        for (const child of node.children) {
            calculateNodeRects(nodes, child, depth + 1, totalAcc, newOffsetAcc);
            newOffsetAcc += child.acc;
        }
    }

    function drawTextCanvas(node: CalcNode): HTMLCanvasElement {
        const { w, h, title } = node;
        const textCanvas = document.createElement("canvas");
        textCanvas.width = Math.max(w - 8, 1);
        textCanvas.height = h;
        const textCtx = textCanvas.getContext("2d")!;
        textCtx.font = fnNameFont;
        textCtx.fillStyle = "black";
        textCtx.fillText(
            title,
            ((w - 10) / 2 - ctx.measureText(title).width / 2) + 5 - 4,
            20,
        );
        return textCanvas;
    }

    function renderNodes(nodes: CalcNode[], hoverFnId?: number) {
        for (const node of nodes) {
            const { x, y, w, h } = node;
            ctx.fillStyle = "rgb(227, 178, 60)";
            ctx.fillRect(x + 2, y + 2, w - 4, h - 4);

            const textCanvas = drawTextCanvas(node);
            ctx.drawImage(textCanvas, x + 4, y);

            const edgePadding = 4;
            const edgeWidth = 8;

            const leftGradient = ctx.createLinearGradient(
                x + 2 + edgePadding,
                0,
                x + 2 + edgeWidth,
                0,
            );
            leftGradient.addColorStop(1, "rgba(227, 178, 60, 0.0)");
            leftGradient.addColorStop(0, "rgba(227, 178, 60, 1.0)");
            ctx.fillStyle = leftGradient;
            ctx.fillRect(x + 2, y + 2, Math.min(edgeWidth, (w - 4) / 2), h - 4);

            const rightGradient = ctx.createLinearGradient(
                x + w - 2 - edgeWidth,
                0,
                x + w - 2 - edgePadding,
                0,
            );
            rightGradient.addColorStop(0, "rgba(227, 178, 60, 0.0)");
            rightGradient.addColorStop(1, "rgba(227, 178, 60, 1.0)");
            ctx.fillStyle = rightGradient;
            ctx.fillRect(
                x + w - 2 - Math.min(edgeWidth, (w - 4) / 2),
                y + 2,
                Math.min(edgeWidth, (w - 4) / 2),
                h - 4,
            );

            if (hoverFnId === node.fgNode.fn) {
                ctx.strokeStyle = canvas.style.backgroundColor;
                ctx.lineWidth = 1;
                ctx.strokeRect(x + 2, y + 2, w - 4, h - 4);
            }
        }
        const tooltip = document.getElementById("flame-graph-tooltip")!;

        const mousemoveEvent = (e: MouseEvent) => {
            const x = e.offsetX;
            const y = e.offsetY;
            const node = nodes.find((node) =>
                x >= node.x && x < node.x + node.w && y >= node.y &&
                y < node.y + node.h
            );

            if (!node) {
                tooltip.hidden = true;
                canvas.style.cursor = "default";
                return;
            }
            tooltip.innerText = `${node.title} ${node.percent}`;
            tooltip.style.left = `${e.clientX + 20}px`;
            tooltip.style.top = `${e.clientY + 20}px`;
            tooltip.hidden = false;
            canvas.style.cursor = "pointer";

            canvas.removeEventListener("mousemove", mousemoveEvent);
            canvas.removeEventListener("mouseleave", mouseleaveEvent);
            canvas.removeEventListener("mousedown", mousedownEvent);
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            renderNodes(nodes, node.fgNode.fn);
        };
        const mouseleaveEvent = () => {
            tooltip.hidden = true;
            canvas.style.cursor = "default";
        };
        const mousedownEvent = (e: MouseEvent) => {
            const x = e.offsetX;
            const y = e.offsetY;
            const node = nodes.find((node) =>
                x >= node.x && x < node.x + node.w && y >= node.y &&
                y < node.y + node.h
            );
            if (!node) {
                return;
            }
            tooltip.hidden = true;
            canvas.style.cursor = "default";
            const newNodes: CalcNode[] = [];
            calculateNodeRects(
                newNodes,
                node.fgNode,
                0,
                node.fgNode.acc,
                0,
            );
            canvas.removeEventListener("mousemove", mousemoveEvent);
            canvas.removeEventListener("mouseleave", mouseleaveEvent);
            canvas.removeEventListener("mousedown", mousedownEvent);
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            renderNodes(newNodes);
        };

        canvas.addEventListener("mousemove", mousemoveEvent);
        canvas.addEventListener("mouseleave", mouseleaveEvent);
        canvas.addEventListener("mousedown", mousedownEvent);
    }

    resetButton.addEventListener("click", () => {
        const nodes: CalcNode[] = [];
        calculateNodeRects(nodes, flameGraphData, 0, flameGraphData.acc, 0);
        renderNodes(nodes);
    });

    const nodes: CalcNode[] = [];
    calculateNodeRects(nodes, flameGraphData, 0, flameGraphData.acc, 0);
    renderNodes(nodes);
}
