#!/usr/bin/env bash

# What this machine is, asked one yes-or-no question at a time.
#
#   panama-hw laptop && echo "portable"
#   panama-hw clamshell && panama-lid close
#
# Every subcommand exits 0 for yes and 1 for no, prints nothing, and answers
# correctly on a machine that has none of the hardware in question. That last
# part is the whole point: a desktop must be able to ask "am I in clamshell
# mode" and get a calm no rather than an error, because the scripts and
# services that ask are shared between machines.
#
# `--json` answers everything at once, for the health page and for contracts.
#
# Detection reads sysfs directly rather than shelling out to lspci or upower:
# lspci touches PCI config space and wakes a runtime-suspended GPU, which is a
# real cost to pay for a question asked at every login.
#
# Paths are overridable (PANAMA_HW_SYS, PANAMA_HW_ACPI) so the contract can
# drive fixture trees. Nothing else should set them.

set -uo pipefail

SYS="${PANAMA_HW_SYS:-/sys}"
ACPI="${PANAMA_HW_ACPI:-/proc/acpi}"

# SMBIOS chassis types that mean "carried around": Portable, Laptop, Notebook,
# Hand Held, Sub Notebook, Tablet, Convertible, Detachable. A machine that
# reports something else, or reports nothing, is treated as stationary --
# guessing "laptop" on an unknown chassis would put battery chrome on a desktop.
readonly PORTABLE_CHASSIS=" 8 9 10 11 14 30 31 32 "

is_laptop() {
    local type_file="$SYS/class/dmi/id/chassis_type" chassis
    [[ -r "$type_file" ]] || return 1
    chassis="$(cat "$type_file" 2>/dev/null)" || return 1
    [[ "$PORTABLE_CHASSIS" == *" $chassis "* ]]
}

# The first SYSTEM battery, or nothing. Named rather than assumed to be BAT0:
# the second battery in a ThinkPad is BAT1, and a machine with only BAT1
# exists. The scope check is what keeps a desktop a desktop: a wireless mouse
# or a game controller publishes type=Battery with scope=Device, and counting
# one turned a tower into a "laptop" whose battery readout was the mouse's.
battery_path() {
    local supply type scope
    for supply in "$SYS"/class/power_supply/*; do
        [[ -r "$supply/type" ]] || continue
        type="$(cat "$supply/type" 2>/dev/null)"
        [[ "$type" == "Battery" ]] || continue
        scope="$(cat "$supply/scope" 2>/dev/null || echo System)"
        [[ "$scope" == "Device" ]] && continue
        printf '%s\n' "$supply"
        return 0
    done
    return 1
}

has_battery() { battery_path >/dev/null; }

# On wall power. A machine with no mains supply at all and no system battery
# is a desktop, and a desktop is always on wall power -- answering "no" there
# would make every battery-aware timing apply to a machine that cannot run
# out of power. But "no Mains" alone is not "desktop": hardware charged only
# over USB-PD exposes type=USB supplies and no Mains at all, and reading that
# as permanently-on-AC meant its battery timings never engaged while it ran
# down. When no Mains exists but a system battery does, the battery's own
# status is the answer: Discharging means battery, everything else means fed.
on_ac() {
    local supply type online found=1 battery status
    for supply in "$SYS"/class/power_supply/*; do
        [[ -r "$supply/type" ]] || continue
        type="$(cat "$supply/type" 2>/dev/null)"
        [[ "$type" == "Mains" ]] || continue
        found=0
        online="$(cat "$supply/online" 2>/dev/null || echo 0)"
        [[ "$online" == "1" ]] && return 0
    done
    # Mains exists and none of it is online: genuinely on battery.
    (( found == 0 )) && return 1
    if battery="$(battery_path)"; then
        status="$(cat "$battery/status" 2>/dev/null || echo Unknown)"
        [[ "$status" == "Discharging" ]] && return 1
    fi
    return 0
}

# ACPI first, logind second. Some platforms expose the lid only as an evdev
# switch with no /proc/acpi/button entry; logind watches the switch either
# way, so its LidClosed property is the fallback that keeps clamshell
# detection honest there. No logind (a container, a test tree) means the
# fallback quietly answers open, which is the safe direction.
lid_closed() {
    local state
    for state in "$ACPI"/button/lid/*/state; do
        [[ -r "$state" ]] || continue
        grep -qi closed "$state" && return 0
        return 1
    done
    [[ -d "$ACPI/button/lid" ]] && return 1
    busctl get-property org.freedesktop.login1 /org/freedesktop/login1 \
        org.freedesktop.login1.Manager LidClosed 2>/dev/null | grep -q 'b true'
}

# A connected output that is not the built-in panel. eDP, LVDS and DSI are the
# internal ones; everything else arrived through a cable.
has_external_monitor() {
    local status connector
    for status in "$SYS"/class/drm/card*-*/status; do
        [[ -r "$status" ]] || continue
        [[ "$(cat "$status" 2>/dev/null)" == "connected" ]] || continue
        connector="$(basename "$(dirname "$status")")"
        case "$connector" in
            *eDP*|*LVDS*|*DSI*) continue ;;
            *) return 0 ;;
        esac
    done
    return 1
}

# The one definition the rest of the laptop work hangs on: the lid is shut and
# there is still a screen to use. Closing the lid on a dock must not suspend;
# closing it on a train must.
is_clamshell() { lid_closed && has_external_monitor; }

has_touchpad() {
    local name
    for name in "$SYS"/class/input/*/name; do
        [[ -r "$name" ]] || continue
        grep -qi touchpad "$name" && return 0
    done
    return 1
}

# Vendor 0x10de on a display-class device. Read from sysfs rather than lspci
# so an idle discrete GPU is not woken to answer.
has_nvidia() {
    local device vendor class
    for device in "$SYS"/bus/pci/devices/*; do
        [[ -r "$device/vendor" && -r "$device/class" ]] || continue
        vendor="$(cat "$device/vendor" 2>/dev/null)"
        [[ "$vendor" == "0x10de" ]] || continue
        class="$(cat "$device/class" 2>/dev/null)"
        [[ "$class" == 0x03* ]] && return 0
    done
    return 1
}

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

cmd_json() {
    printf '{"laptop":%s,"battery":%s,"ac":%s,"lidClosed":%s,"externalMonitor":%s,"clamshell":%s,"touchpad":%s,"nvidia":%s}\n' \
        "$(answer is_laptop)" "$(answer has_battery)" "$(answer on_ac)" \
        "$(answer lid_closed)" "$(answer has_external_monitor)" \
        "$(answer is_clamshell)" "$(answer has_touchpad)" "$(answer has_nvidia)"
}

case "${1:-}" in
    laptop)           is_laptop ;;
    battery)          has_battery ;;
    battery-path)     battery_path ;;
    ac)               on_ac ;;
    lid-closed)       lid_closed ;;
    external-monitor) has_external_monitor ;;
    clamshell)        is_clamshell ;;
    touchpad)         has_touchpad ;;
    nvidia)           has_nvidia ;;
    --json)           cmd_json ;;
    -h|--help|"")
        cat <<'USAGE'
usage: panama-hw <predicate>

Exits 0 for yes, 1 for no, and prints nothing.

  laptop            a portable chassis
  battery           a battery is present
  battery-path      print the first battery's sysfs path (0 if found)
  ac                on wall power (a machine with no mains is always yes)
  lid-closed        the lid is shut
  external-monitor  a connected output that is not the built-in panel
  clamshell         lid shut AND an external monitor: docked, keep working
  touchpad          a touchpad is present
  nvidia            an NVIDIA display device is present

  --json            every answer at once
USAGE
        ;;
    *) printf 'panama-hw: unknown predicate: %s\n' "$1" >&2; exit 2 ;;
esac
