// Brightness, from whichever source this machine actually has.
//
// Two exist and they are not interchangeable:
//
//   The kernel backlight class, driven by brightnessctl. Laptop panels have it;
//   this desktop does not -- brightnessctl reports only keyboard and NIC LEDs.
//
//   DDC/CI, the channel the buttons on a monitor's bezel drive. That is the
//   only brightness an external display has, and it is per-monitor.
//
// A machine may have neither, either, or both, so this renders a row per source
// found and removes itself entirely when there are none, rather than sitting
// there as a dead control.
//
// Connector labels appear only when there is more than one row. A single
// slider needs no explanation of which screen it dims.

import QtQuick
import Quickshell
import Quickshell.Io
import qs.widgets
import qs.config
import qs.services

Item {
    id: root

    property bool backlightAvailable: false
    property real backlightValue: 0

    readonly property int rowCount: (root.backlightAvailable ? 1 : 0) + Brightness.displays.length
    readonly property bool labeled: root.rowCount > 1

    visible: root.rowCount > 0
    implicitHeight: rows.implicitHeight

    // Probing I2C takes on the order of a second, so it waits until the panel
    // is actually on screen rather than running at shell startup. Monitors do
    // not come and go, so once is enough.
    onVisibleChanged: if (visible && !Brightness.scanned) Brightness.refresh()
    Component.onCompleted: if (root.visible && !Brightness.scanned) Brightness.refresh()

    // `-m` is the machine-readable form: name,class,current,percent,max
    Process {
        running: true
        command: ["brightnessctl", "-m", "-c", "backlight", "-l"]
        stdout: StdioCollector {
            onStreamFinished: root.parseDevices(this.text)
        }
    }

    function parseDevices(text: string): void {
        for (const line of text.trim().split("\n")) {
            const fields = line.split(",");
            if (fields.length < 5 || fields[1] !== "backlight")
                continue;
            root.backlightAvailable = true;
            root.backlightValue = parseInt(fields[3]) / 100;
            return;
        }
    }

    function applyBacklight(v: real): void {
        root.backlightValue = v;
        // Never go fully dark: a 0% backlight looks like a broken shell.
        Quickshell.execDetached(["brightnessctl", "-c", "backlight", "-q", "set", Math.max(1, Math.round(v * 100)) + "%"]);
    }

    Column {
        id: rows
        anchors.left: parent.left
        anchors.right: parent.right
        spacing: 4

        BrightnessRow {
            width: rows.width
            visible: root.backlightAvailable
            label: "Built-in"
            value: root.backlightValue
            onMoved: v => root.applyBacklight(v)
        }

        Repeater {
            model: Brightness.displays

            BrightnessRow {
                required property var modelData
                width: rows.width
                // Hyprland already knows what each output is called, so the
                // name comes from there rather than from a second source that
                // could disagree with the Displays page. The connector is the
                // fallback, so a display is never an unlabeled slider.
                label: Displays.monitorNamed(modelData.connector)?.description || modelData.connector
                value: modelData.value / 100
                onMoved: v => Brightness.set(modelData.bus, Math.round(v * 100))
            }
        }
    }

    component BrightnessRow: Item {
        id: row

        property string label: ""
        property real value: 0
        signal moved(real value)

        implicitHeight: caption.height + control.height

        Text {
            id: caption
            anchors.left: parent.left
            anchors.right: parent.right
            anchors.leftMargin: 6
            anchors.rightMargin: 6
            anchors.top: parent.top
            visible: root.labeled
            height: visible ? implicitHeight + 2 : 0
            text: row.label
            color: Theme.fgDim
            font.family: Theme.fontFamily
            font.pixelSize: Theme.fontSizeSmall
            elide: Text.ElideRight
        }

        // The slider and its glyph share one strip so the two stay aligned
        // whether or not a caption sits above them. Anchoring the glyph to
        // both a caption and a center line instead would conflict, and an
        // anchor set to undefined is not released.
        Item {
            id: control
            anchors.left: parent.left
            anchors.right: parent.right
            anchors.top: caption.bottom
            height: 32

            // ValueSlider draws its own leading icon, but symbolic icons need
            // recoloring to be visible — see ThemedIcon.
            ThemedIcon {
                id: glyph
                anchors.left: parent.left
                anchors.leftMargin: 6
                anchors.verticalCenter: parent.verticalCenter
                size: 17
                icon: "display-brightness-symbolic"
            }

            ValueSlider {
                anchors.left: glyph.right
                anchors.leftMargin: 12
                anchors.right: parent.right
                anchors.rightMargin: 32
                anchors.verticalCenter: parent.verticalCenter
                value: row.value
                onMoved: v => row.moved(v)
            }
        }
    }
}
