import { CodeCovRender, loadCodeCoverage } from "./code_coverage.ts";
import * as data from "./data.ts";
import { loadFlameGraph } from "./flamegraph.ts";

function countLines(code: string) {
    let lines = 0;
    for (const char of code) {
        if (char === "\n") lines += 1;
    }
    return lines;
}

function createLineElement(code: string): HTMLPreElement {
    const lines = countLines(code) + 1;
    const maxLineWidth = lines.toString().length;
    let text = "";
    for (let i = 1; i < lines; ++i) {
        const node = i.toString().padStart(maxLineWidth);
        text += node;
        text += "\n";
    }
    const lineElement = document.createElement("pre");
    lineElement.classList.add("code-lines");
    lineElement.innerText = text;
    return lineElement;
}

async function checkStatus(): Promise<"running" | "done"> {
    const status = await data.status();

    if (status.running) {
        return "running";
    }
    const statusHtml = document.querySelector<HTMLSpanElement>(
        "#status",
    )!;
    statusHtml.innerText = "Done";
    document.body.classList.remove("status-waiting");
    document.body.classList.add("status-done");
    return "done";
}

function syntaxHighlight(code: string): string {
    const colors = {
        colorBackground: "#282828",
        colorForeground: "#fbf1c7",
        colorKeyword: "#fb4934",
        colorIdentifier: "#83a598",
        colorOperator: "#fe8019",
        colorSpecial: "#fe8019",
        colorType: "#fabd2f",
        colorBoolean: "#d3869b",
        colorNumber: "#d3869b",
        colorString: "#b8bb26",
        colorComment: "#928374",
        colorFunction: "#b8bb26",
        colorLineNumber: "#7c6f64",
    } as const;

    /*
    Keyword = { link = "GruvboxRed" },
    Identifier = { link = "GruvboxBlue" },
    Operator = { fg = colors.orange, italic = config.italic.operators },
    Special = { link = "GruvboxOrange" },
    Type = { link = "GruvboxYellow" },
    Boolean = { link = "GruvboxPurple" },
    Number = { link = "GruvboxPurple" },
    String = { fg = colors.green, italic = config.italic.strings },
    Comment = { fg = colors.gray, italic = config.italic.comments },
    Function = { link = "GruvboxGreenBold" },
    */

    let matches: {
        index: number;
        length: number;
        color: string;
        extra: string;
    }[] = [];

    function addMatches(color: string, re: RegExp, extra = "") {
        for (const match of code.matchAll(re)) {
            matches.push({
                index: match.index,
                length: match[1].length,
                color,
                extra,
            });
        }
    }
    function addKeywordMatches(color: string, keywords: string[]) {
        addMatches(
            color,
            new RegExp(
                `(?<!\\w)(${
                    keywords.map((kw) => `(?:${kw})`).join("|")
                })(?!\\w)`,
                "g",
            ),
        );
    }

    for (let i = 0; i < code.length; ++i) {
        if (code[i] !== '"') {
            continue;
        }
        let last = code[i];
        const index = i;
        i += 1;
        while (i < code.length && !(code[i] === '"' && last !== "\\")) {
            last = code[i];
            i += 1;
        }
        if (i < code.length) {
            i += 1;
        }
        matches.push({
            index,
            length: i - index,
            color: colors.colorString,
            extra: "font-style: italic;",
        });
    }

    {
        let last = "";
        for (let i = 0; i < code.length; ++i) {
            if (last === "/" && code[i] === "/") {
                const index = i - 1;
                while (i < code.length && code[i] !== "\n") {
                    i += 1;
                }
                matches.push({
                    index,
                    length: i - index,
                    color: colors.colorComment,
                    extra: "font-style: italic;",
                });
            }
            last = code[i];
        }
    }

    addKeywordMatches(
        colors.colorKeyword,
        [
            "break",
            "return",
            "let",
            "fn",
            "if",
            "else",
            "struct",
            "import",
            "or",
            "and",
            "not",
            "loop",
            "while",
            "for",
            "in",
        ],
    );
    addKeywordMatches(colors.colorSpecial, ["null"]);
    addKeywordMatches(colors.colorType, ["int", "string", "bool"]);
    addKeywordMatches(colors.colorBoolean, ["false", "true"]);
    addMatches(
        colors.colorOperator,
        new RegExp(
            `(${
                [
                    "\\+=",
                    "\\-=",
                    "\\+",
                    "\\->",
                    "\\-",
                    "\\*",
                    "/",
                    "==",
                    "!=",
                    "<=",
                    ">=",
                    "=",
                    "<",
                    ">",
                    "\\.",
                    "::<",
                    "::",
                    ":",
                ].map((kw) => `(?:${kw})`).join("|")
            })`,
            "g",
        ),
    );
    addMatches(
        colors.colorNumber,
        /(0|(?:[1-9][0-9]*)|(?:0[0-7]+)|(?:0x[0-9a-fA-F]+)|(?:0b[01]+))/g,
    );
    addMatches(
        colors.colorFunction,
        /([a-zA-Z_]\w*(?=\())/g,
        "font-weight: 700;",
    );
    addMatches(colors.colorIdentifier, /([a-z_]\w*)/g);
    addMatches(colors.colorType, /([A-Z_]\w*)/g);

    matches = matches.reduce<typeof matches>(
        (acc, match) =>
            acc.find((m) => m.index === match.index) === undefined
                ? (acc.push(match), acc)
                : acc,
        [],
    );
    matches.sort((a, b) => a.index - b.index);

    let highlighted = "";
    let i = 0;
    for (const match of matches) {
        if (match.index < i) {
            continue;
        }
        while (i < match.index) {
            highlighted += code[i];
            i += 1;
        }
        const section = code.slice(match.index, match.index + match.length);
        highlighted +=
            `<span style="color: ${match.color};${match.extra}">${section}</span>`;
        i += section.length;
    }
    while (i < code.length) {
        highlighted += code[i];
        i += 1;
    }

    return highlighted;
}

function sourceCode(view: Element, codeData: string) {
    const outerContainer = document.createElement("div");
    outerContainer.classList.add("code-container");

    const innerContainer = document.createElement("div");
    innerContainer.classList.add("code-container-inner");

    const lines = createLineElement(codeData);
    const code = document.createElement("pre");

    code.classList.add("code-source");
    code.innerHTML = syntaxHighlight(codeData);
    innerContainer.append(lines, code);
    outerContainer.append(innerContainer);
    view.replaceChildren(outerContainer);
}

function createRadio(
    id: string,
    content: string,
    checked: boolean,
): [HTMLDivElement, HTMLInputElement] {
    const label = document.createElement("label");
    label.htmlFor = id;
    label.innerText = content;
    const input = document.createElement("input");
    input.id = id;
    input.name = "coverage-radio";
    input.type = "radio";
    input.hidden = true;
    input.checked = checked;
    const container = document.createElement("div");
    container.classList.add("coverage-radio-group");
    container.append(input, label);
    return [container, input];
}

async function codeCoverage(view: Element, codeData: string) {
    const codeCoverageData = await data.codeCoverageData();

    const outerContainer = document.createElement("div");
    outerContainer.classList.add("code-container");
    outerContainer.classList.add("code-coverage");

    const innerContainer = document.createElement("div");
    innerContainer.classList.add("code-container-inner");

    const [perfGroup, perfInput] = createRadio(
        "performance-coverage",
        "Performance view",
        true,
    );
    const [testGroup, testInput] = createRadio(
        "test-coverage",
        "Test view",
        false,
    );

    function load(mode: CodeCovRender, tooltip: HTMLDivElement) {
        const lines = createLineElement(codeData);
        const code = loadCodeCoverage(
            codeData,
            codeCoverageData,
            tooltip,
            mode,
        );
        innerContainer.replaceChildren(lines, code);
    }

    const radios = document.createElement("div");
    radios.append(perfGroup, testGroup);
    radios.classList.add("coverage-radio");
    const tooltip = document.createElement("div");
    tooltip.id = "covers-tooltip";
    tooltip.hidden = true;
    outerContainer.append(innerContainer);
    view.replaceChildren(outerContainer, tooltip, radios);

    if (perfInput.checked) {
        load("performance", tooltip);
    } else if (testInput.checked) {
        load("test-coverage", tooltip);
    }

    perfInput.addEventListener("input", () => load("performance", tooltip));
    testInput.addEventListener("input", () => load("test-coverage", tooltip));
}

async function main() {
    type RenderFns = {
        "source-code": () => void;
        "code-coverage": () => void;
        "flame-graph": () => void;
    };

    const codeData = await data.codeData();

    const view = document.querySelector("#view")!;
    const renderFunctions: RenderFns = {
        "source-code": () => sourceCode(view, codeData),
        "code-coverage": async () => await codeCoverage(view, codeData),
        "flame-graph": async () => {
            const flameGraphData = await data.flameGraphData();
            const flameGraphFnNames = await data.flameGraphFnNames();

            const container = document.createElement("div");
            container.classList.add("flame-graph");
            container.id = "flame-graph";
            const view = document.querySelector("#view")!;
            view.replaceChildren(container);
            loadFlameGraph(flameGraphData, flameGraphFnNames, container);
        },
    };

    const viewRadios: NodeListOf<HTMLInputElement> = document.querySelectorAll(
        'input[name="views"]',
    );
    for (const input of viewRadios) {
        input.addEventListener("input", (ev) => {
            const target = ev.target as HTMLInputElement;
            const value = target.value as keyof RenderFns;
            renderFunctions[value]();
        });
        if (input.checked) {
            const value = input.value as keyof RenderFns;
            renderFunctions[value]();
        }
    }

    checkStatus().then((status) => {
        if (status == "done") {
            return;
        }
        const interval = setInterval(async () => {
            const status = await checkStatus();
            if (status == "done") {
                clearInterval(interval);
            }
        }, 500);
    });
}

main();
