/*
Copyright 2024 New Vector Ltd.
Copyright 2021 The Matrix.org Foundation C.I.C.

SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/

import type { CssNode, Rule, StyleSheet } from "css-tree";
import customCSS from "./exportCustomCSS.css?raw";

const cssSelectorTextClassesRegex = /\.[\w-]+/g;
const appLayerName = "app-web";

function mutateCssText(css: string): string {
    // replace used fonts so that we don't have to bundle Inter & Fira Code
    const sansFont = `-apple-system, BlinkMacSystemFont, avenir next,
            avenir, segoe ui, helvetica neue, helvetica, Ubuntu, roboto, noto, arial, sans-serif`;
    return css
        .replace(/font-family: ?(Inter|'Inter'|"Inter")/g, `font-family: ${sansFont}`)
        .replace(/--cpd-font-family-sans: ?(Inter|'Inter'|"Inter")/g, `--cpd-font-family-sans: ${sansFont}`)
        .replace(
            /font-family: ?Fira Code/g,
            "font-family: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace",
        );
}

function includeRule(rule: Rule, usedClasses: Set<string>): boolean {
    if (rule.prelude.type === "Raw") {
        // cull empty rules
        if (rule.block.children.isEmpty) return false;

        return rule.prelude.value.split(",").some((subselector) => {
            const classes = subselector.trim().match(cssSelectorTextClassesRegex);
            if (classes && !classes.every((c) => usedClasses.has(c.substring(1)))) {
                return false;
            }
            return true;
        });
    }
    return true;
}

function includeNode(node: CssNode, usedClasses: Set<string>): boolean {
    if (node.type === "Atrule") {
        if (node.name === "font-face") {
            return false;
        }

        if (node.block) {
            node.block.children = node.block.children.filter((child) => includeNode(child, usedClasses));
            return !node.block.children.isEmpty;
        }
    }

    return node.type !== "Rule" || includeRule(node, usedClasses);
}

// naively culls unused css rules based on which classes are present in the html,
// doesn't cull rules which won't apply due to the full selector not matching but gets rid of a LOT of cruft anyway.
// We cannot use document.styleSheets as it does not handle variables in shorthand properties sanely,
// see https://github.com/element-hq/element-web/issues/26761
const getExportCSS = async (usedClasses: Set<string>): Promise<string> => {
    const csstree = await import("css-tree");

    // only include bundle.css and light theme styling
    const hrefs = ["bundle.css", "theme-light.css"].map((name) => {
        return document.querySelector<HTMLLinkElement>(`link[rel="stylesheet"][href$="${name}"]`)?.href;
    });

    let css = "";

    for (const href of hrefs) {
        if (!href) continue;
        const res = await fetch(href);
        const text = await res.text();

        const ast = csstree.parse(text, {
            context: "stylesheet",
            parseAtrulePrelude: false,
            parseRulePrelude: false,
            parseValue: false,
            parseCustomProperty: false,
        }) as StyleSheet;

        for (const rule of ast.children) {
            if (!includeNode(rule, usedClasses)) {
                continue;
            }

            css += mutateCssText(csstree.generate(rule));
        }
    }

    return `${css}@layer ${appLayerName} {${customCSS}}`;
};

export default getExportCSS;
