const codeCoverageDiv = document.querySelector("#code-coverage");
const flameGraphDiv = document.querySelector("#flame-graph");

function drawText(text, codeCoverageData) {
    const tooltip = document.getElementById("covers-tooltip");
    const entries = codeCoverageData.toSorted((a, b) => b.index - a.index);
    const charEntries = {};
    const elements = [];
    let line = 1;
    let col = 1;
    for (let index = 0; index < text.length; ++index) {
        if (text[index] == "\n") {
            col = 1;
            line += 1;
            elements.push("\n");
            continue;
        }
        const entry = entries.find((entry) => index >= entry.index);
        charEntries[`${line}-${col}`] = entry;

        const color = (ratio) =>
            `rgba(${255 - 255 * ratio}, ${255 * ratio}, 125, 0.5)`;

        const span = document.createElement("span");
        span.style.backgroundColor = color(Math.min(entry.covers / 25, 1));
        span.innerText = text[index];
        span.dataset.covers = entry.covers;
        elements.push(span);
        col += 1;
    }
    function positionInBox(position, boundingRect) {
        const [x, y] = position;
        const outside = x < boundingRect.left ||
            x >= boundingRect.right || y < boundingRect.top ||
            y >= boundingRect.bottom;
        return !outside;
    }
    testytestytesty.append(...elements);
    document.addEventListener("mousemove", (event) => {
        const [x, y] = [event.clientX, event.clientY];
        const outerBox = testytestytesty.getBoundingClientRect();
        if (!positionInBox([x, y], outerBox)) {
            console.log("+");
            return;
        }
        const element = elements.find((element) => {
            if (!element.dataset?.covers) {
                return false;
            }
            const isIn = positionInBox([x, y], element.getBoundingClientRect());
            return isIn;
        });
        if (!element) {
            tooltip.hidden = true;
            return;
        }
        const covers = parseInt(element.dataset.covers);
        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" : ""}`;
    });
}

function loadCodeCoverage(text, codeCoverageData) {
    codeCoverageDiv.innerHTML = `
        <canvas id="code-coverage-canvas"></canvas>
        <pre><code>${text}</code></pre>
        <span id="covers-tooltip" hidden></span>
    `;

    /** @type { HTMLCanvasElement } */
    const canvas = document.querySelector("#code-coverage-canvas");
    canvas.width = 1000;
    canvas.height = 500;

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

    ctx.font = "20px monospace";

    const { width: chWidth } = ctx.measureText("-");
    const chHeight = 26;

    const color = (ratio) =>
        `rgba(${255 - 255 * ratio}, ${255 * ratio}, 125, 0.5)`;

    const entries = codeCoverageData.toSorted((a, b) => b.index - a.index);

    const charEntries = {};

    let line = 1;
    let col = 1;
    for (let index = 0; index < text.length; ++index) {
        if (text[index] == "\n") {
            col = 1;
            line += 1;
            continue;
        }
        const entry = entries.find((entry) => index >= entry.index);
        charEntries[`${line}-${col}`] = entry;
        ctx.fillStyle = color(Math.min(entry.covers / 25, 1));
        ctx.fillRect(
            (col - 1) * chWidth,
            (line - 1) * chHeight,
            chWidth,
            chHeight,
        );
        col += 1;
    }

    const tooltip = document.getElementById("covers-tooltip");

    canvas.addEventListener("mousemove", (e) => {
        const col = Math.floor(e.offsetX / chWidth + 1);
        const line = Math.floor(e.offsetY / chHeight + 1);
        const key = `${line}-${col}`;
        if (!(key in charEntries)) {
            tooltip.hidden = true;
            return;
        }
        const entry = charEntries[key];
        tooltip.innerText = `Ran ${entry.covers} time${
            entry.covers !== 1 ? "s" : ""
        }`;
        tooltip.style.left = `${e.clientX + 20}px`;
        tooltip.style.top = `${e.clientY + 20}px`;
        tooltip.hidden = false;
    });
    canvas.addEventListener("mouseleave", () => {
        tooltip.hidden = true;
    });
}

function loadFlameGraph(flameGraphData, fnNames) {
    flameGraphDiv.innerHTML = `
        <canvas id="flame-graph-canvas"></canvas>
        <span id="flame-graph-tooltip" hidden></span>
    `;

    /** @type { HTMLCanvasElement } */
    const canvas = document.querySelector("#flame-graph-canvas");
    canvas.width = 1000;
    canvas.height = 500;

    const ctx = canvas.getContext("2d");
    ctx.font = "16px monospace";

    const nodes = [];

    function calculateNodeRects(node, depth, totalAcc, offsetAcc) {
        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 = fnNames[node.fn];
        const percent = `${(node.acc / totalAcc * 100).toFixed(1)}%`;
        nodes.push({ x, y, w, h, title, percent });

        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(child, depth + 1, totalAcc, newOffsetAcc);
            newOffsetAcc += child.acc;
        }
    }
    calculateNodeRects(flameGraphData, 0, flameGraphData.acc, 0);

    for (const node of nodes) {
        const { x, y, w, h, title } = node;
        ctx.fillStyle = "rgb(255, 125, 0)";
        ctx.fillRect(
            x + 1,
            y + 1,
            w - 2,
            h - 2,
        );
        ctx.fillStyle = "black";
        ctx.fillText(
            title,
            (x + (w - 10) / 2 - ctx.measureText(title).width / 2) + 5,
            y + 20,
        );
    }

    const tooltip = document.getElementById("flame-graph-tooltip");

    canvas.addEventListener("mousemove", (e) => {
        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;
            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.addEventListener("mouseleave", () => {
        tooltip.hidden = true;
    });
}

const codeData = `\
fn add(a, b) {
    + a b
}

let result = 0;
let i = 0;
loop {
    if >= i 10 {
        break;
    }
    result = add(result, 5);
    i = + i 1;
}
`;

const codeCoverageData = JSON.parse(
    `[{"index":0,"line":1,"col":1,"covers":2},{"index":28,"line":5,"col":1,"covers":1},{"index":44,"line":6,"col":1,"covers":1},{"index":55,"line":7,"col":1,"covers":1},{"index":66,"line":8,"col":5,"covers":11},{"index":104,"line":11,"col":5,"covers":10},{"index":19,"line":2,"col":5,"covers":10},{"index":133,"line":12,"col":5,"covers":10},{"index":87,"line":9,"col":9,"covers":1}]`,
);

const flameGraphData = JSON.parse(
    `{"fn":0,"acc":257,"parent":0,"children":[{"fn":18,"acc":251,"parent":0,"children":[{"fn":12,"acc":30,"parent":1,"children":[]}]}]}`,
);

loadCodeCoverage(codeData, codeCoverageData);
loadFlameGraph(flameGraphData, {
    0: "<entry>",
    12: "add",
    18: "main",
});
drawText(codeData, codeCoverageData);