// A numeric setting, bound to a schema key by name.
//
//   SliderRow { setting: "windowRounding" }
//
// Range, step, label, explanation, and unit all come from PreferenceSchema.
//
// Three behaviors worth knowing:
//
//   * The row is responsive. The Settings window is a normal tiled window, so
//     its width is whatever the layout gives it -- anywhere from a half-screen
//     split to the full 4500px display, and `implicitWidth` is only a hint.
//     Below a usable inline width the slider moves onto its own line under the
//     label instead of squeezing the explanation into a five-line column.
//   * The readout follows the drag immediately, but the value is only committed
//     after a short quiet period. Compositor-backed settings are applied and
//     verified one batch at a time, and a slider fires dozens of changes per
//     second -- committing each would spend the whole drag rejecting
//     overlapping writes.
//   * Between commits the row shows what you are dragging; once settled it
//     shows what is actually stored. If the compositor refuses a value the row
//     falls back to the stored one rather than displaying a value nothing
//     accepted.
//
// This does not extend SettingRow: that component fixes the control to a
// trailing column of a set width, which is the layout this row needs to be able
// to abandon. The label, explanation, and divider match it exactly.

import QtQuick
import qs.config
import qs.services
import qs.widgets

Item {
    id: root

    required property string setting

    property bool divider: true
    property string zeroLabel: ""

    readonly property var spec: PreferenceSchema.spec(root.setting)
    readonly property string label: root.spec ? root.spec.label : root.setting
    readonly property string detail: root.spec ? root.spec.detail : ""
    readonly property real minimum: root.spec && root.spec.min !== undefined ? root.spec.min : 0
    readonly property real maximum: root.spec && root.spec.max !== undefined ? root.spec.max : 100
    readonly property real step: root.spec && root.spec.step !== undefined ? root.spec.step : 1
    readonly property string unit: root.spec && root.spec.unit !== undefined ? root.spec.unit : ""

    readonly property real stored: {
        const value = DesktopPreferences.get(root.setting);
        return typeof value === "number" ? value : root.minimum;
    }

    // Shown while dragging; null means "nothing pending, show what is stored".
    // Not -1: several schema entries (e.g. pointerSensitivity) are legitimately
    // negative, and -1 would be indistinguishable from a real committed value.
    property var pending: null
    readonly property real shown: root.pending !== null ? root.pending : root.stored

    // Below this the label and a usable slider cannot share a line without one
    // of them becoming useless.
    readonly property bool inline: width >= 520

    // A fixed trailing width rather than a share of the row. A proportional
    // control looks reasonable at 900px and absurd at 4500px, where the slider
    // would be a meter long next to a two-word label -- and this window is
    // tiled, so it really can be that wide.
    readonly property int controlSpan: 300

    width: parent ? parent.width : 620
    implicitHeight: root.inline
        ? Math.max(56, copy.implicitHeight + 20)
        : copy.implicitHeight + 32 + 30

    function quantise(ratio: real): real {
        const raw = root.minimum + ratio * (root.maximum - root.minimum);
        const snapped = Math.round(raw / root.step) * root.step;
        const clamped = Math.max(root.minimum, Math.min(root.maximum, snapped));
        // Steps below 1 are fractional (opacity, pointer speed); rounding to two
        // places keeps 0.8500000000000001 out of the readout and the store.
        return root.step < 1 ? Math.round(clamped * 100) / 100 : clamped;
    }

    function display(value: real): string {
        if (value === 0 && root.zeroLabel !== "")
            return root.zeroLabel;
        const text = root.step < 1 ? value.toFixed(2) : String(value);
        return root.unit === "" ? text : `${text} ${root.unit}`;
    }

    // Both children are positioned explicitly rather than by anchors. Binding
    // an anchor to `undefined` to switch layouts does not reliably release it,
    // which left the slider anchored to both edges and the label squeezed into
    // whatever was left.
    Column {
        id: copy

        x: 0
        y: root.inline ? (root.height - height) / 2 : 10
        width: root.inline ? root.width - root.controlSpan - 20 : root.width
        spacing: 3

        Text {
            width: parent.width
            text: root.label
            color: Theme.fg
            font.family: Theme.fontFamily
            font.pixelSize: Theme.fontSize
            font.weight: Font.Medium
            elide: Text.ElideRight
        }

        Text {
            width: parent.width
            visible: root.detail !== ""
            text: root.detail
            color: Theme.fgDim
            font.family: Theme.fontFamily
            font.pixelSize: Theme.fontSizeSmall
            wrapMode: Text.WordWrap
        }
    }

    Item {
        id: control

        width: root.inline ? root.controlSpan : root.width
        height: 32
        x: root.inline ? root.width - width : 0
        y: root.inline ? (root.height - height) / 2 : copy.y + copy.height + 10

        ValueSlider {
            id: slider

            anchors.left: parent.left
            anchors.right: readout.left
            anchors.rightMargin: 12
            anchors.verticalCenter: parent.verticalCenter

            value: root.maximum > root.minimum
                ? (root.shown - root.minimum) / (root.maximum - root.minimum)
                : 0

            onMoved: ratio => {
                root.pending = root.quantise(ratio);
                commitTimer.restart();
            }
        }

        Text {
            id: readout

            anchors.right: parent.right
            anchors.verticalCenter: parent.verticalCenter
            width: 58
            horizontalAlignment: Text.AlignRight
            text: root.display(root.shown)
            color: Theme.fgDim
            font.family: Theme.fontFamily
            // The readout changes digit by digit under the pointer; tabular
            // figures stop it twitching sideways as it does.
            font.features: Theme.tabularFigures
            font.pixelSize: Theme.fontSize
            elide: Text.ElideRight
        }
    }

    Rectangle {
        anchors.left: parent.left
        anchors.right: parent.right
        anchors.bottom: parent.bottom
        height: 1
        visible: root.divider
        color: Theme.alpha(Theme.fg, 0.065)
    }

    Timer {
        id: commitTimer
        interval: 140
        onTriggered: {
            if (root.pending === null)
                return;
            SystemSettings.commitPreference(root.setting, root.pending);
            // Hand the display back to the stored value. If the write was
            // refused, the row snaps back to what is really in effect.
            releaseTimer.restart();
        }
    }

    Timer {
        id: releaseTimer
        interval: 160
        onTriggered: root.pending = null
    }
}
