#!/usr/bin/env bash

# What closing the lid should do.
#
#   panama-lid status    what this machine would do right now, as JSON
#   panama-lid guard     hold the inhibitor while clamshell use is possible
#
# The rule is one sentence: closing the lid should suspend, UNLESS there is an
# external monitor, in which case the machine is docked and should keep working.
#
# The implementation is deliberately not a lid watcher. logind already handles
# the lid perfectly well; what it cannot do is notice that an external display
# makes a closed lid mean something different, because its own "docked" test
# looks for an ACPI docking station that modern hardware does not have.
#
# So Panama does not take the lid over. It holds a `handle-lid-switch`
# inhibitor while an external monitor is connected, and releases it when the
# last one goes away. logind does everything else, which has three properties
# worth having:
#
#   * Nothing has to watch the lid, and no polling loop exists.
#   * If this ever fails or is killed, the inhibitor is released and the
#     machine goes back to logind's default. The failure mode is "a docked
#     laptop suspends", not "the lid does nothing at all", which is the failure
#     mode a logind drop-in would have.
#   * Locking before sleep is already handled: hypridle's before_sleep_cmd
#     runs `loginctl lock-session`, so a suspend from a closed lid is a locked
#     suspend without anything here being involved.
#
# Started and stopped by services/LidPolicy.qml on display topology changes.
#
# NOT YET VERIFIED ON A LAPTOP. The logic and the inhibitor are testable on any
# machine and are covered by tests/setup/lid-contract, but the end-to-end
# behavior of closing a real lid on a docked machine needs a machine with a
# lid. Everything here fails toward logind's default, so the untested path is
# "keeps working while docked" rather than anything that could strand a session.

set -uo pipefail

PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
HW="$PANAMA_PATH/bin/panama-hw"

answer() { "$@" && printf 'true' || printf 'false'; }

cmd_status() {
    local inhibited=false
    systemd-inhibit --list 2>/dev/null | grep -q 'panama-lid' && inhibited=true
    printf '{"laptop":%s,"lidClosed":%s,"externalMonitor":%s,"clamshell":%s,"inhibited":%s}\n' \
        "$(answer "$HW" laptop)" \
        "$(answer "$HW" lid-closed)" \
        "$(answer "$HW" external-monitor)" \
        "$(answer "$HW" clamshell)" \
        "$inhibited"
}

# Holds the inhibitor for as long as it runs. Exits immediately, holding
# nothing, when this machine has no reason to want one -- a desktop has no lid
# to inhibit and an undocked laptop should suspend normally.
cmd_guard() {
    "$HW" laptop || exit 0
    "$HW" external-monitor || exit 0

    exec systemd-inhibit \
        --what=handle-lid-switch \
        --who=panama-lid \
        --why="An external display is connected, so a closed lid is a docked machine rather than one being put away" \
        --mode=block \
        sleep infinity
}

# The internal panel's connector name, from the compositor: eDP, LVDS and
# DSI are built in, everything else arrived through a cable -- the same rule
# panama-hw and LidPolicy use.
internal_connector() {
    hyprctl -j monitors all 2>/dev/null \
        | jq -r '[.[] | select(.name | test("^(eDP|LVDS|DSI)"; "i"))][0].name // empty'
}

# Bound to the lid switch by keybinds.lua. Closing a DOCKED lid turns the
# internal panel off, so nothing renders inside a closed shell and no
# workspace strands on an invisible output; an undocked lid does nothing here
# because logind is already suspending the machine. Failure direction: if the
# disable never runs, the panel stays lit inside the shell -- wasteful, not
# harmful.
cmd_close() {
    "$HW" laptop || exit 0
    "$HW" external-monitor || exit 0
    local internal
    internal="$(internal_connector)"
    [[ -n "$internal" ]] || exit 0
    hyprctl eval "hl.monitor({ output = \"$internal\", disable = true })" >/dev/null
}

# The stored display record for one output, rendered as hl.monitor arguments.
#
# This exists because the four-key version it replaces was a clobber. Opening
# the lid emitted mode, position="auto" and scale and nothing else -- and an
# hl.monitor call REPLACES the rule for that output whole, so every other field
# the person had chosen for the panel went with it. Transform, VRR, bit depth,
# colour profile, SDR trim, mirroring and, worst of all, the position: a panel
# arranged to the left of an external display jumped back to automatic
# placement, taking its workspaces with it, every time the lid was opened.
#
# So the rule is rebuilt from the same store config/dot/hypr/monitors.lua reads
# at startup, with the same validation, so that opening the lid produces the
# rule a reload would have produced. Mirroring that file field for field is the
# point; the two must not be able to disagree about what a saved record means.
#
# The two failure directions are deliberately different, exactly as they are
# there:
#
#   * mode, scale or transform unreadable -- or a half-written position, where
#     a record carries some layout fields but not a valid pair -- refuses the
#     WHOLE record. Nothing is printed and the caller falls back to the panel's
#     preferred mode. Guessing a position can strand an output where no cursor
#     can reach it.
#   * an unreadable colour, VRR, SDR or mirror value drops only itself. The
#     geometry survives, and the worst it costs is a wrong shade.
#
# Every value that reaches the emitted string is validated first -- modes and
# positions against a numeric pattern, connector names against the same
# `[%w_.-]` class monitors.lua uses, colour profiles against a fixed set -- so
# nothing from the settings file can carry a quote into the Lua that is built
# from it.
monitor_rule() {
    local name="$1" settings="${XDG_CONFIG_HOME:-$HOME/.config}/panama/settings.json"
    [[ -r "$settings" ]] || return 0
    jq -r --arg name "$name" '
        def numeric: if type == "number" and (isnan | not) and (isinfinite | not)
                     then . else null end;
        def whole: numeric | if . != null and . == floor then . else null end;

        . as $root
        | ($root.displays[$name] // null) as $e
        | if ($e | type) != "object" then "" else

        # ── Geometry: all of it, or none of it ───────────────────────────────
          ($e.mode | if type == "string"
                        and test("^[0-9]+x[0-9]+@[0-9]+(\\.[0-9]+)?$")
                     then . else null end) as $mode
        | ($mode | if . == null then null
                   else capture("^(?<w>[0-9]+)x(?<h>[0-9]+)@(?<r>[0-9.]+)$")
                        | [(.w | tonumber), (.h | tonumber), (.r | tonumber)]
                   end) as $dim
        | (if $dim == null or $dim[0] <= 0 or $dim[1] <= 0 or $dim[2] <= 0
           then null else $mode end) as $mode

        # A scale is only valid if it divides the mode into whole logical
        # pixels; Hyprland refuses the rest, and monitors.lua refuses them here
        # first so the two agree about which records are usable.
        | ($e.scale | numeric | if . != null and . > 0 and . <= 4
                                then . else null end) as $rawScale
        | (if $mode == null or $rawScale == null then null
           else ($dim[0] / $rawScale) as $lw
                | ($dim[1] / $rawScale) as $lh
                | if ((($lw - ($lw | round)) | fabs) < 0.0001)
                     and ((($lh - ($lh | round)) | fabs) < 0.0001)
                  then $rawScale else null end
           end) as $scale
        | ($e.transform | whole
           | if . != null and . >= 0 and . <= 3 then . else null end) as $transform

        # Legacy records carry no layout fields at all and keep automatic
        # placement. A record that carries SOME of them and gets one wrong is
        # refused outright rather than half-honoured.
        | (($e.x != null) or ($e.y != null) or ($e.primary != null)) as $hasLayout
        | ($e.x | whole | if . != null and . >= -100000 and . <= 100000
                          then . else null end) as $x
        | ($e.y | whole | if . != null and . >= -100000 and . <= 100000
                          then . else null end) as $y
        | ($hasLayout
           and ($x == null or $y == null or ($e.primary | type) != "boolean")) as $layoutBroken

        # ── Extended fields: each one drops on its own ───────────────────────
        | ($e.bitdepth | if . == 8 or . == 10 then . else null end) as $bitdepth
        | ($e.colorProfile
           | if . == "auto" or . == "srgb" or . == "wide" or . == "hdr"
             then . else null end) as $cm
        # -1 means "follow the global VRR policy", which is said by leaving the
        # key out; 3 is the global policy value and not a per-display choice.
        | ($e.vrrMode | whole | if . != null and . >= 0 and . <= 2
                                then . else null end) as $vrr
        # Neutral is 1.0 and is left out rather than written: naming it pins the
        # display to it, which is not the same as leaving the trim alone.
        | ($e.sdrBrightness | numeric
           | if . != null and . >= 0.8 and . <= 2.0 and (((. - 1) | fabs) >= 0.001)
             then . else null end) as $sdrBrightness
        | ($e.sdrSaturation | numeric
           | if . != null and . >= 0.8 and . <= 1.2 and (((. - 1) | fabs) >= 0.001)
             then . else null end) as $sdrSaturation
        # A mirror needs a target that is not itself and not another mirror --
        # Hyprland has no chain to follow -- and the primary may not mirror at
        # all, since the arrangement is anchored on it.
        | ($e.mirrorOf
           | if type == "string" and . != "" and . != $name
                and test("^[A-Za-z0-9_.-]+$")
             then . else null end) as $mirrorName
        | (if $mirrorName == null or $e.primary == true then null
           else ($root.displays[$mirrorName] // null) as $target
                | if ($target | type) == "object"
                     and ($target.mirrorOf | type) == "string"
                     and $target.mirrorOf != ""
                  then null else $mirrorName end
           end) as $mirror

        # A mirror shows its target picture in its target place, so the saved
        # position is not ours to ask for.
        | (if $mirror != null then "auto"
           elif $hasLayout then "\($x)x\($y)"
           else "auto" end) as $position

        | if $mode == null or $scale == null or $transform == null or $layoutBroken
          then ""
          else ([ "mode = \"\($mode)\"",
                  "position = \"\($position)\"",
                  "scale = \($scale)",
                  "transform = \($transform)" ]
                + (if $bitdepth == null then [] else ["bitdepth = \($bitdepth)"] end)
                + (if $cm == null then [] else ["cm = \"\($cm)\""] end)
                + (if $vrr == null then [] else ["vrr = \($vrr)"] end)
                + (if $sdrBrightness == null then []
                   else ["sdrbrightness = \($sdrBrightness)"] end)
                + (if $sdrSaturation == null then []
                   else ["sdrsaturation = \($sdrSaturation)"] end)
                + (if $mirror == null then [] else ["mirror = \"\($mirror)\""] end)
               ) | join(", ")
          end
        end
    ' "$settings" 2>/dev/null || true
}

# Re-enable on open, restoring what the person chose for this panel in Settings
# -- the whole record, not the three fields that used to survive -- and falling
# back to the panel's preferred mode when there is no usable record.
cmd_open() {
    "$HW" laptop || exit 0
    local internal rule
    internal="$(internal_connector)"
    [[ -n "$internal" ]] || exit 0
    # The connector name is interpolated into Lua too, and it comes from
    # hyprctl rather than from us. Same class monitors.lua accepts.
    [[ "$internal" =~ ^[A-Za-z0-9_.-]+$ ]] || exit 0

    rule="$(monitor_rule "$internal")"
    [[ -n "$rule" ]] || rule='mode = "preferred", position = "auto", scale = "auto"'
    hyprctl eval "hl.monitor({ output = \"$internal\", $rule })" >/dev/null
}

case "${1:-status}" in
    status) cmd_status ;;
    guard)  cmd_guard ;;
    close)  cmd_close ;;
    open)   cmd_open ;;
    -h|--help)
        cat <<'USAGE'
usage: panama-lid [status|guard|close|open]

  status   what closing the lid would do right now, as JSON
  guard    hold a handle-lid-switch inhibitor while an external display is
           connected; exits immediately on a machine that needs none
  close    docked lid closed: turn the internal panel off (bound to the lid
           switch by keybinds.lua); does nothing undocked
  open     lid opened: turn the internal panel back on, restoring the whole
           display record chosen in Settings -- position, mode, scale,
           transform, and the colour, VRR and mirror fields when they are set
USAGE
        ;;
    *) printf 'panama-lid: unknown command: %s\n' "$1" >&2; exit 2 ;;
esac
