.pragma library

const STOPWORDS = [
    "a",
    "an",
    "and",
    "are",
    "for",
    "in",
    "not",
    "notification",
    "of",
    "on",
    "or",
    "out",
    "the",
    "to",
];

const FIELD_WEIGHT = {
    title: 1.0,
    keywords: 0.4,
};
const SKIP_LABELS = ["Muted", "None"];

function cleanLabel(text) {
    return String(text ?? "")
        .replace(/\s*\(?%\d+\)?/g, "")
        .trim();
}

const PAGE_NAME_RE = /^\s*name:\s*qsTr\("([^"]+)"\)/;
const ROW_RE =
    /^\s*(ToggleRow|SliderRow|SelectRow|SpinRow|NavRow|RowButton|InfoRow|PopupRow|DefaultRow|TextFieldRow|IconTextButton|TimeDialogSelect)\s*\{/;
const NAV_ROW_TYPES = ["NavRow", "IconTextButton"];
const LABEL_RE = /^\s*(?:label|text):\s*qsTr\("([^"]+)"\)/;
const ANCHOR_RE = /^\s*(?:property\s+string\s+)?settingAnchor:\s*"([^"]+)"/;
const CHECKED_RE = /^\s*checked:\s*(?:GlobalConfig|Config)\.([\w.]+)\s*$/;
const ONTOGGLED_RE =
    /^\s*onToggled:\s*(?:GlobalConfig|Config)\.([\w.]+)\s*=\s*checked\s*$/;
const ICON_RE = /^\s*icon:\s*"([^"]+)"/;
const SUBTEXT_RE = /^\s*(?:subtext|status):\s*qsTr\("([^"]+)"\)/;
const SECTION_RE = /^\s*SectionHeader\s*\{/;

function tokenize(text) {
    const toks = [];
    for (const word of text.toLowerCase().split(/\s+/)) {
        if (!word) continue;
        const parts = word.split(/[^a-z0-9]+/).filter((p) => p);
        for (const p of parts) {
            if (!STOPWORDS.includes(p) && !toks.includes(p)) toks.push(p);
        }
        if (parts.length > 1) {
            const joined = parts.join("");
            if (!toks.includes(joined)) toks.push(joined);
        }
    }
    return toks;
}

function discoverFiles(settingsDir, listFiles) {
    const files = {};
    for (const p of listFiles(`${settingsDir}/Pages`, ".qml")) {
        const name = p.slice(p.lastIndexOf("/") + 1).replace(/\.qml$/, "");
        files[name] = p;
    }
    return files;
}

function parsePageRegistry(settingsDir, readLines) {
    const lines = readLines(`${settingsDir}/PageRegistry.qml`);
    const out = [];
    let inArray = false;
    let depth = 0;
    let label = null;
    let icon = null;
    for (const line of lines) {
        const s = line.trim();
        if (s.includes("pages:") && s.includes("[")) {
            inArray = true;
            continue;
        }
        if (!inArray) continue;
        if (s.startsWith("//")) continue;
        if (s.startsWith("]")) break;
        if (s.startsWith("{")) {
            depth++;
            label = icon = null;
            continue;
        }
        if (s.startsWith("}")) {
            if (label !== null) out.push([icon || "tune", label]);
            depth--;
            continue;
        }
        if (depth >= 1) {
            const m = PAGE_NAME_RE.exec(line);
            if (m && label === null) label = m[1];
            const mi = ICON_RE.exec(line);
            if (mi && icon === null) icon = mi[1];
        }
    }
    return out;
}

function parsePageComps(settingsDir, readFile) {
    const text = readFile(`${settingsDir}/PageCompRegistry.qml`);
    const start = text.indexOf("pageComps:");
    if (start === -1) return [];
    const comps = [];
    let current = null;
    let depth = 0;
    for (const raw of text.slice(start).split("\n")) {
        const line = raw.split("//")[0];
        const s = line.trim();
        if (s.startsWith("]")) break;
        const atTop = depth === 0;
        depth +=
            (line.match(/\{/g) ?? []).length -
            (line.match(/\}/g) ?? []).length;
        if (atTop && /^Component\s*\{/.test(s)) {
            current = [];
            comps.push(current);
            continue;
        }
        if (current !== null) {
            const m = /(^|\s)([A-Z][A-Za-z0-9]*)\s*\{\s*\}/.exec(s);
            if (m) current.push(m[2]);
        }
    }
    return comps;
}

function dedupCrumbs(labels, icons) {
    const outLabels = [];
    const outIcons = [];
    for (let i = 0; i < labels.length; i++) {
        if (
            outLabels.length > 0 &&
            outLabels[outLabels.length - 1] === labels[i]
        )
            continue;
        outLabels.push(labels[i]);
        outIcons.push(icons[i]);
    }
    return [outLabels, outIcons];
}

function buildNavMap(settingsDir, files, readFile, readLines) {
    const comps = parsePageComps(settingsDir, readFile);
    const registry = parsePageRegistry(settingsDir, readLines);

    const navChildren = {};
    for (const names of comps) {
        for (const name of names) {
            const pf = files[name];
            if (!pf) continue;
            let pendingIcon = null;
            let pendingLabel = null;
            let section = "";
            let expectSection = false;
            for (const ln of readLines(pf)) {
                if (SECTION_RE.test(ln)) {
                    expectSection = true;
                    continue;
                }
                const ml = LABEL_RE.exec(ln);
                if (ml) {
                    if (expectSection) {
                        section = ml[1];
                        expectSection = false;
                    } else {
                        pendingLabel = ml[1];
                    }
                    continue;
                }
                const mi = ICON_RE.exec(ln);
                if (mi) pendingIcon = mi[1];
                const mo = /openSubPage\((\d+)\)/.exec(ln);
                if (mo) {
                    const pos = parseInt(mo[1], 10);
                    if (!navChildren[name]) navChildren[name] = {};
                    navChildren[name][pos] = [
                        pendingIcon || "tune",
                        pendingLabel || "",
                        section,
                    ];
                    pendingIcon = pendingLabel = null;
                }
            }
        }
    }

    const nav = {};
    for (let topIdx = 0; topIdx < comps.length; topIdx++) {
        const names = comps[topIdx];
        if (names.length === 0) continue;
        const main = names[0];
        const [mainIcon, mainLabel] = registry[topIdx] ?? ["tune", main];
        nav[main] = {
            pageIdx: topIdx,
            subPath: [],
            crumbIcons: [mainIcon],
            crumbLabels: [mainLabel],
        };
        const children = Object.assign({}, navChildren[main] ?? {});
        const openedViaSubpage = [];
        for (const owner in navChildren) {
            const ownerGroup = comps.find((ns) => ns.includes(owner));
            if (!ownerGroup) continue;
            for (const kpos in navChildren[owner]) {
                const k = parseInt(kpos, 10);
                if (k < ownerGroup.length) openedViaSubpage.push(ownerGroup[k]);
            }
        }
        for (let pos = 1; pos < names.length; pos++) {
            if (!(pos in children) && !openedViaSubpage.includes(names[pos])) {
                let label = names[pos].replace(/(Detail)?Page$/, "");
                label = label.replace(/(?<!^)(?=[A-Z])/g, " ");
                children[pos] = [mainIcon, label, ""];
            }
        }
        for (const posKey in children) {
            const pos = parseInt(posKey, 10);
            if (pos >= names.length) continue;
            const [icon, label, section] = children[posKey];
            const child = names[pos];
            let labels = [mainLabel]
                .concat(section ? [section] : [])
                .concat([label]);
            let icons = [mainIcon].concat(section ? [icon] : []).concat([icon]);
            [labels, icons] = dedupCrumbs(labels, icons);
            nav[child] = {
                pageIdx: topIdx,
                subPath: [pos],
                crumbIcons: icons,
                crumbLabels: labels,
            };
            const grandChildren = navChildren[child] ?? {};
            for (const gposKey in grandChildren) {
                const gpos = parseInt(gposKey, 10);
                if (gpos >= names.length) continue;
                const [gicon, glabel, gsection] = grandChildren[gposKey];
                let glabels = labels
                    .concat(gsection ? [gsection] : [])
                    .concat([glabel]);
                let gicons = icons
                    .concat(gsection ? [gicon] : [])
                    .concat([gicon]);
                [glabels, gicons] = dedupCrumbs(glabels, gicons);
                nav[names[gpos]] = {
                    pageIdx: topIdx,
                    subPath: [pos, gpos],
                    crumbIcons: gicons,
                    crumbLabels: glabels,
                };
            }
        }
    }
    return nav;
}

function findBlockEnd(lines, start) {
    let depth = 0;
    for (let i = start; i < lines.length; i++) {
        const line = lines[i].split("//")[0];
        depth +=
            (line.match(/\{/g) ?? []).length -
            (line.match(/\}/g) ?? []).length;
        if (i > start && depth <= 0) return i;
    }
    return lines.length;
}

function extractSettings(files, nav, comps, readLines) {
    const entries = [];
    for (const comp in nav) {
        const meta = nav[comp];
        const pf = files[comp];
        if (!pf) continue;
        const lines = readLines(pf);
        let section = "";
        for (let i = 0; i < lines.length; i++) {
            if (SECTION_RE.test(lines[i])) {
                for (let j = i + 1; j < Math.min(i + 4, lines.length); j++) {
                    const m = LABEL_RE.exec(lines[j]);
                    if (m) {
                        section = m[1];
                        break;
                    }
                }
            }
            const rowMatch = ROW_RE.exec(lines[i]);
            if (!rowMatch) continue;
            const rowType = rowMatch[1];
            let label = null;
            let anchor = null;
            let subtext = null;
            let checkedPath = null;
            let toggledPath = null;
            let targetPos = null;
            const rowEnd = findBlockEnd(lines, i);
            for (let j = i + 1; j < rowEnd; j++) {
                if (label === null) {
                    const m = LABEL_RE.exec(lines[j]);
                    if (m) label = m[1];
                }
                if (anchor === null) {
                    const a = ANCHOR_RE.exec(lines[j]);
                    if (a) anchor = a[1];
                }
                if (subtext === null) {
                    const st = SUBTEXT_RE.exec(lines[j]);
                    if (st) subtext = st[1];
                }
                if (checkedPath === null) {
                    const ch = CHECKED_RE.exec(lines[j]);
                    if (ch) checkedPath = ch[1];
                }
                if (toggledPath === null) {
                    const tg = ONTOGGLED_RE.exec(lines[j]);
                    if (tg) toggledPath = tg[1];
                }
                if (NAV_ROW_TYPES.includes(rowType) && targetPos === null) {
                    const sp = /openSubPage\((\d+)\)/.exec(lines[j]);
                    if (sp) targetPos = parseInt(sp[1], 10);
                }
            }
            const togglePath =
                rowType === "ToggleRow" &&
                checkedPath &&
                checkedPath === toggledPath
                    ? checkedPath
                    : "";
            if (label && !SKIP_LABELS.includes(label) && anchor) {
                const extra =
                    meta.crumbLabels.join(" ") +
                    " " +
                    section +
                    " " +
                    (subtext && !/%\d/.test(subtext) ? subtext : "");
                const group = comps[meta.pageIdx] ?? [];
                const targetSubPath =
                    targetPos !== null && targetPos < group.length
                        ? meta.subPath.concat([targetPos])
                        : [];
                entries.push({
                    rowType: rowType,
                    pageIdx: meta.pageIdx,
                    subPath: meta.subPath,
                    targetSubPath: targetSubPath,
                    crumbIcons: meta.crumbIcons,
                    crumbLabels: meta.crumbLabels,
                    trailKey: meta.crumbLabels.join("/"),
                    title: cleanLabel(label),
                    anchor: anchor,
                    section: section,
                    subtext: subtext && !/%\d/.test(subtext) ? subtext : "",
                    togglePath: togglePath,
                });
            }
        }
    }
    return mergeInfoRows(entries);
}

function mergeInfoRows(entries) {
    const out = [];
    const merged = {};

    for (const entry of entries) {
        const isInfo = entry.rowType === "InfoRow";
        delete entry.rowType;

        if (!isInfo || !entry.section) {
            out.push(entry);
            continue;
        }

        const key =
            entry.anchor.split("-")[0] +
            "/" +
            entry.trailKey +
            "/" +
            entry.section;
        const existing = merged[key];
        if (existing === undefined) {
            entry.keywords = entry.title;
            entry.title = entry.section;
            merged[key] = entry;
            out.push(entry);
        } else {
            existing.keywords += " " + entry.title;
        }
    }
    return out;
}

function buildInvertedAndRanking(entries) {
    const inverted = {};
    const ranking = {};
    for (let idx = 0; idx < entries.length; idx++) {
        const e = entries[idx];
        const extra =
            e.crumbLabels.join(" ") +
            " " +
            e.section +
            " " +
            e.subtext +
            " " +
            (e.keywords ?? "");
        const fields = {
            title: e.title,
            keywords: tokenize(e.title + " " + extra)
                .sort()
                .join(" "),
        };
        for (const field in fields) {
            const weight = FIELD_WEIGHT[field] ?? 0.2;
            for (const tok of tokenize(fields[field])) {
                if (!inverted[tok]) inverted[tok] = [];
                if (!inverted[tok].includes(idx)) inverted[tok].push(idx);
                if (!ranking[tok]) ranking[tok] = {};
                ranking[tok][idx] = Math.max(ranking[tok][idx] ?? 0.0, weight);
            }
        }
    }
    for (const tok in inverted)
        inverted[tok].sort((a, b) => ranking[tok][b] - ranking[tok][a]);
    return [inverted, ranking];
}

function buildIndex(settingsDir, readFile, listFiles) {
    const lineCache = {};
    const readLines = (path) => {
        if (!(path in lineCache)) lineCache[path] = readFile(path).split("\n");
        return lineCache[path];
    };
    const files = discoverFiles(settingsDir, listFiles);
    const nav = buildNavMap(settingsDir, files, readFile, readLines);
    const comps = parsePageComps(settingsDir, readFile);
    const entries = extractSettings(files, nav, comps, readLines);
    const [inverted, ranking] = buildInvertedAndRanking(entries);
    return {
        version: 3,
        entries: entries,
        inverted: inverted,
        ranking: ranking,
    };
}
