// Applications and session startup.

import Quickshell
import QtQuick
import qs.services

SettingsPage {
    id: root

    objectName: "applications"
    title: "Applications"
    lede: "Choose what opens your files and links, and what starts with your session."

    property string expandedRole: ""
    property bool addingAutostart: false

    // Which entry has been asked to be removed. Removal deletes a file, so
    // it never happens on a first press.
    property string confirmingAutostartRemoval: ""
    readonly property var applications: DesktopEntries.applications.values
    // Each role governs a whole family of types, not one representative: setting
    // "Images" writes PNG, JPEG, WebP and the rest together, so a file manager
    // can never open one image in a viewer and its neighbour in an editor.
    // The detail line names the family the way someone would describe it.
    readonly property var roles: [
        { key: "browser", label: "Browser", detail: "Web links and HTML pages", categorySets: [["webbrowser"]], terms: ["web browser", "browser"] },
        { key: "mail", label: "Mail", detail: "Email links", categorySets: [["email"]], terms: ["mail client", "email client"] },
        { key: "files", label: "Files", detail: "Folders and file locations", categorySets: [["filemanager"]], terms: ["file manager"] },
        { key: "terminal", label: "Terminal", detail: "Terminal links and command-line handoffs", categorySets: [["terminalemulator"]], terms: ["terminal emulator", "terminal"] },
        { key: "images", label: "Images", detail: "PNG, JPEG, GIF, WebP, SVG and other pictures", categorySets: [], terms: ["image viewer", "image editor", "photo viewer", "photo editor", "picture viewer"] },
        { key: "music", label: "Music", detail: "MP3, FLAC, Ogg and other audio", categorySets: [["music"], ["audio", "player"]], terms: ["music player", "audio player"] },
        { key: "video", label: "Video", detail: "MP4, MKV, WebM and other video", categorySets: [["video"]], terms: ["video player", "movie player"] },
        { key: "documents", label: "Documents", detail: "PDF and EPUB documents", categorySets: [["office", "viewer"]], terms: ["document viewer", "pdf viewer", "ebook", "e-book"] },
        { key: "text", label: "Text", detail: "Plain text, Markdown, and source files", categorySets: [["texteditor"]], terms: ["text editor", "code editor"] },
        { key: "archives", label: "Archives", detail: "Zip, tar, and other archives", categorySets: [["archiving"], ["filemanager"]], terms: ["archive manager", "file roller", "file manager"] }
    ]

    function desktopId(entry: var): string {
        const entryId = String(entry?.id ?? "");
        return entryId.endsWith(".desktop") ? entryId : entryId + ".desktop";
    }

    function displayName(entry: var): string {
        return String(entry?.name || entry?.genericName || root.desktopId(entry));
    }

    function currentHandler(role: string): string {
        return String(DefaultApps.handlers[role] ?? "");
    }

    function currentEntry(role: string): var {
        const handler = root.currentHandler(role);
        return root.applications.find(entry => root.desktopId(entry) === handler) ?? null;
    }

    function matchesRole(entry: var, role: var): bool {
        // DesktopEntries hands back a QML list, not a JavaScript array, so
        // Array.isArray is false for it. The old code took that as "this is a
        // string", stringified the list into "Network,WebBrowser" and then split
        // on ";" only -- producing the single token "network,webbrowser", which
        // matches no category at all.
        //
        // Nothing failed loudly. Browsers still appeared because their generic
        // name contains "web browser", so the terms fallback carried the role
        // by itself. Archives matched NOTHING, which meant that row could only
        // ever offer the application it already had.
        //
        // Joining first and splitting on both separators handles the list form
        // and a plain string equally.
        const raw = entry.categories;
        const joined = Array.isArray(raw) ? raw.join(";") : String(raw ?? "");
        const categories = [];
        for (const value of joined.split(/[;,]/)) {
            const category = value.trim().toLowerCase();
            if (category !== "")
                categories.push(category);
        }
        const metadata = [entry.name, entry.genericName]
            .map(value => String(value ?? "").toLowerCase())
            .join(" ");
        return role.categorySets.some(set => set.every(category => categories.includes(category)))
            || role.terms.some(term => metadata.includes(term));
    }

    function choicesForRole(role: var): var {
        const choices = root.applications.filter(entry => root.matchesRole(entry, role));
        const currentEntry = root.currentEntry(role.key);
        if (currentEntry && !choices.some(entry => root.desktopId(entry) === root.desktopId(currentEntry)))
            choices.push(currentEntry);
        return choices.sort((left, right) => root.displayName(left).localeCompare(root.displayName(right)));
    }

    TextRow {
        visible: DefaultApps.lastError !== ""
        label: "Application settings need attention"
        detail: DefaultApps.lastError
        value: ""
        divider: false
    }

    SettingsCard {
        title: "Default applications"
        subtitle: "Open a row to choose from applications that advertise the matching role."

        Repeater {
            model: root.roles

            delegate: Column {
                id: roleBlock

                required property var modelData
                required property int index
                readonly property var choices: root.choicesForRole(roleBlock.modelData)
                readonly property var selectedEntry: root.currentEntry(roleBlock.modelData.key)

                width: parent.width

                SettingRow {
                    id: roleRow

                    readonly property bool open: root.expandedRole === roleBlock.modelData.key

                    label: roleBlock.modelData.label
                    detail: roleBlock.modelData.detail
                    activatable: roleBlock.choices.length > 0 && !DefaultApps.busy
                    controlWidth: 210

                    // Drawn rather than left to SettingRow's plain value text, so
                    // the row carries the same chevron a PickerRow does. These
                    // open a chooser but looked completely inert without it.
                    Row {
                        anchors.right: parent.right
                        anchors.verticalCenter: parent.verticalCenter
                        spacing: 9

                        Text {
                            anchors.verticalCenter: parent.verticalCenter
                            text: DefaultApps.busy ? "Loading…" : (
                                roleBlock.selectedEntry
                                    ? root.displayName(roleBlock.selectedEntry)
                                    : (root.currentHandler(roleBlock.modelData.key) || "Not set")
                            )
                            color: Theme.fgDim
                            font.family: Theme.fontFamily
                            font.pixelSize: Theme.fontSize
                            elide: Text.ElideRight
                        }

                        Text {
                            anchors.verticalCenter: parent.verticalCenter
                            visible: roleBlock.choices.length > 0
                            text: roleRow.open ? "\u25B4" : "\u25BE"
                            color: Theme.fgMuted
                            font.family: Theme.fontFamily
                            font.pixelSize: Theme.fontSizeSmall
                        }
                    }

                    divider: root.expandedRole !== roleBlock.modelData.key && roleBlock.index < root.roles.length - 1
                    onActivated: {
                        root.expandedRole = root.expandedRole === roleBlock.modelData.key
                            ? ""
                            : roleBlock.modelData.key;
                    }
                }

                Column {
                    width: parent.width
                    visible: root.expandedRole === roleBlock.modelData.key

                    Repeater {
                        model: roleBlock.choices

                        delegate: SettingRow {
                            id: candidateRow

                            required property var modelData
                            required property int index

                            readonly property string candidateId: root.desktopId(candidateRow.modelData)
                            readonly property bool selected: candidateRow.candidateId === root.currentHandler(roleBlock.modelData.key)

                            label: root.displayName(candidateRow.modelData)
                            detail: String(candidateRow.modelData.genericName || candidateRow.modelData.comment || candidateRow.candidateId)
                            value: candidateRow.selected ? "Current" : ""
                            activatable: !candidateRow.selected && !DefaultApps.busy
                            divider: candidateRow.index < roleBlock.choices.length - 1 || roleBlock.index < root.roles.length - 1
                            onActivated: {
                                DefaultApps.setDefault(roleBlock.modelData.key, candidateRow.candidateId);
                                root.expandedRole = "";
                            }
                        }
                    }
                }
            }
        }
    }

    // GNOME's Search panel, answered honestly.
    //
    // It configures which applications provide results in gnome-shell's
    // overview and which folders are indexed. gnome-shell does not run here,
    // so those settings would do nothing. Under Panama searching is the
    // launcher's job, and Vicinae carries its own preferences -- reimplementing
    // them here would give two places to change one thing.
    SettingsCard {
        title: "Search"
        subtitle: "Applications, files, the calculator, clipboard history, emoji, and open windows are all searched from the launcher."

        TextEntryRow { setting: "webSearchUrl"; placeholder: "https://duckduckgo.com/?q=" }

        TextRow {
            label: "Launcher"
            detail: SystemSettings.vicinaeActive
                ? "Running as a user service"
                : "Not running — Super+Shift+R opens the fallback launcher"
            value: "Vicinae"
        }

        TextRow {
            label: "Open search"
            detail: "Three keys open it, because Super+A and Super+R were GNOME's app grid and run dialog"
            value: "Super+Space"
        }

        ActionRow {
            label: "Change these shortcuts"
            detail: "Every launcher chord is rebindable, including clipboard history and emoji"
            action: "Open keyboard"
            divider: false
            onTriggered: ShellState.openSettings("shortcuts")
        }
    }

    SettingsCard {
        title: "User autostart"
        subtitle: "Choose what starts with your session. Entries live in your user configuration, not the compositor."

        ActionRow {
            label: "Add an application"
            detail: root.addingAutostart
                ? "Search the applications installed on this machine"
                : "Start another installed application when you sign in"
            action: root.addingAutostart ? "Close" : "Choose"
            divider: !root.addingAutostart || DefaultApps.autostartEntries.length > 0
            enabled: !DefaultApps.busy
            onTriggered: root.addingAutostart = !root.addingAutostart
        }

        AutostartAppPicker {
            visible: root.addingAutostart
            width: parent.width
            existing: DefaultApps.autostartEntries.map(entry => entry.id)
            onPicked: id => {
                DefaultApps.addAutostart(id);
                root.addingAutostart = false;
            }
        }

        TextRow {
            visible: !DefaultApps.busy && DefaultApps.autostartEntries.length === 0
            label: "No user autostart entries"
            detail: "Applications can add entries to ~/.config/autostart."
            value: ""
            divider: false
        }

        Repeater {
            model: DefaultApps.autostartEntries

            delegate: SettingRow {
                id: autostartRow

                required property var modelData
                required property int index

                readonly property bool confirming:
                    root.confirmingAutostartRemoval === String(autostartRow.modelData.id)

                label: autostartRow.modelData.name
                detail: autostartRow.confirming
                    ? "Removing deletes this entry. Turning it off instead is reversible."
                    : autostartRow.modelData.id
                divider: autostartRow.index < DefaultApps.autostartEntries.length - 1
                controlWidth: 210

                // A switch, not the words "Enabled"/"Disabled". The row always
                // toggled on click, but read as static text, so a control that
                // worked looked like a status nobody could change.
                Row {
                    anchors.right: parent.right
                    anchors.verticalCenter: parent.verticalCenter
                    spacing: 9

                    SettingsButton {
                        anchors.verticalCenter: parent.verticalCenter
                        visible: autostartRow.confirming
                        text: "Remove it"
                        tone: "danger"
                        enabled: !DefaultApps.busy
                        onClicked: {
                            root.confirmingAutostartRemoval = "";
                            DefaultApps.removeAutostart(String(autostartRow.modelData.id));
                        }
                    }

                    SettingsButton {
                        anchors.verticalCenter: parent.verticalCenter
                        text: autostartRow.confirming ? "Keep" : "Remove…"
                        enabled: !DefaultApps.busy
                        onClicked: root.confirmingAutostartRemoval =
                            autostartRow.confirming ? "" : String(autostartRow.modelData.id)
                    }

                    SettingsToggle {
                        anchors.verticalCenter: parent.verticalCenter
                        checked: autostartRow.modelData.enabled
                        onToggled: value => DefaultApps.setAutostart(autostartRow.modelData.id, value)
                    }
                }
            }
        }
    }

    SettingsCard {
        title: "Compositor autostart"
        subtitle: "These are started from the Hyprland configuration. They are read-only here."

        TextRow {
            visible: !DefaultApps.busy && DefaultApps.luaAutostartEntries.length === 0
            label: "No compositor entries found"
            detail: "No hl.exec_cmd entries were found in config/dot/hypr/autostart.lua."
            value: ""
            divider: false
        }

        Repeater {
            model: DefaultApps.luaAutostartEntries

            delegate: TextRow {
                id: luaRow

                required property var modelData
                required property int index

                label: luaRow.modelData.name
                detail: luaRow.modelData.command
                value: "Hyprland"
                divider: luaRow.index < DefaultApps.luaAutostartEntries.length - 1
            }
        }
    }

    SettingsCard {
        title: "Refresh"

        ActionRow {
            label: "Reload application settings"
            detail: "Re-read desktop entries, defaults, and user autostart files"
            action: DefaultApps.busy ? "Refreshing…" : "Refresh"
            enabled: !DefaultApps.busy
            divider: false
            onTriggered: DefaultApps.refresh()
        }
    }
}
