#!/bin/bash

# omarchy:summary=Show, set, or adjust focused Hyprland monitor scaling
# omarchy:args=[up|down|SCALE]
# omarchy:examples=omarchy hyprland monitor scaling | omarchy hyprland monitor scaling 1.6 | omarchy hyprland monitor scaling up | omarchy hyprland monitor scaling down

SCALES=(1 1.25 1.6 2 3 4)
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/omarchy"
SCALE_LOG="$STATE_DIR/monitor-scaling.log"

usage() {
  echo "Usage: omarchy-hyprland-monitor-scaling [up|down|SCALE]"
}

focused_monitor_scale() {
  hyprctl monitors -j | jq -er '.[] | select(.focused == true) | .scale'
}

cmdline_for_pid() {
  local pid="$1"

  [[ -r /proc/$pid/cmdline ]] || return 0
  tr '\0\t\n' '   ' <"/proc/$pid/cmdline" | sed -E 's/[[:space:]]+/ /g; s/[[:space:]]+$//'
}

audit_scale_change() {
  local requested="$1"
  local active_monitor="$2"
  local current_scale="$3"
  local new_scale="$4"
  local parent_pid="$PPID"
  local grandparent_pid
  local parent_cmd
  local grandparent_cmd

  mkdir -p "$STATE_DIR" || return 0

  grandparent_pid=$(ps -o ppid= -p "$parent_pid" 2>/dev/null | tr -d ' ')
  parent_cmd=$(cmdline_for_pid "$parent_pid")
  grandparent_cmd=$(cmdline_for_pid "$grandparent_pid")

  printf 'at=%s\trequested=%s\tcurrent=%s\tnew=%s\tmonitor=%s\tpid=%s\tppid=%s\tparent=%s\tgppid=%s\tgrandparent=%s\n' \
    "$(date --iso-8601=seconds)" \
    "$requested" \
    "$current_scale" \
    "$new_scale" \
    "$active_monitor" \
    "$$" \
    "$parent_pid" \
    "$parent_cmd" \
    "$grandparent_pid" \
    "$grandparent_cmd" >>"$SCALE_LOG"
}

# Hyprland only accepts scales where the mode divides into whole logical
# pixels (in 1/120 steps), so clean scales are divisors of gcd(w*120, h*120).
# Round the requested scale up to the nearest clean value.
clean_scale() {
  awk -v scale="$1" -v width="$2" -v height="$3" '
    function gcd(a, b, t) { while (b) { t = a % b; a = b; b = t } return a }
    BEGIN {
      g = gcd(width * 120, height * 120)
      k = int(scale * 120 + 0.5)
      if (k > g) k = g
      while (g % k != 0) k++
      printf "%g\n", k / 120
    }'
}

normalize_scale() {
  awk 'NR == 1 { printf "%g\n", $0 }'
}

set_scale() {
  local requested_scale="$1"
  local requested="${2:-$requested_scale}"
  local monitors_json="$(hyprctl monitors -j)"
  local monitor_info="$(echo "$monitors_json" | jq -e -c '.[] | select(.focused == true)')"
  local active_monitor="$(echo "$monitor_info" | jq -r '.name')"
  local current_scale="$(echo "$monitor_info" | jq -r '.scale')"
  local width="$(echo "$monitor_info" | jq -r '.width')"
  local height="$(echo "$monitor_info" | jq -r '.height')"
  local refresh_rate="$(echo "$monitor_info" | jq -r '.refreshRate')"

  # active_monitor is written into the Lua string eval'd below, so only a plain
  # connector name may pass; a hostile output name could execute otherwise.
  if [[ ! $active_monitor =~ ^[A-Za-z0-9._-]+$ ]]; then
    echo "Refusing unsafe monitor name" >&2
    exit 1
  fi

  local new_scale="$(clean_scale "$requested_scale" "$width" "$height")"
  # GDK_SCALE is one value for the whole session, so it follows the lowest
  # monitor scale: XWayland apps end up a little small on denser screens rather
  # than oversized on the others. GTK only honors integers, so round it.
  local new_gdk_scale="$(echo "$monitors_json" | jq -r --arg name "$active_monitor" --argjson scale "$new_scale" '
    [.[] | select(.disabled != true) | if .name == $name then $scale else .scale end]
    | min | . + 0.5 | floor | if . < 1 then 1 else . end')"
  local monitor_lua="$HOME/.config/hypr/monitors.lua"

  hyprctl eval "hl.monitor({ output = \"$active_monitor\", mode = \"${width}x${height}@${refresh_rate}\", position = \"auto\", scale = $new_scale })" >/dev/null
  audit_scale_change "$requested" "$active_monitor" "$current_scale" "$new_scale"

  persist_scale "$monitor_lua" "$active_monitor" "$new_scale" "$new_gdk_scale"
}

# Persist the scale as a rule for this monitor alone, so other monitors keep
# theirs and the scale survives reboots. A rule already naming the monitor has
# its scale updated in place. Otherwise one is added after Omarchy's catch-all,
# which stays the default for monitors without a rule of their own. Only rules
# written on one line with a plain scale value are edited; a config holding
# anything else for these rules (a multi-line table, a scale expression, another
# rule naming the monitor) is left alone rather than risk broken Lua or a second
# rule competing with the user's own.
persist_scale() {
  local monitor_lua="$1"
  local output="$2"
  local scale="$3"
  local gdk_scale="$4"
  local target
  local tmp

  [[ -f $monitor_lua ]] || return 0

  # Hyprland reloads on every write, so build the new file beside the real one
  # and rename it into place, through any symlink, in one step.
  target="$(readlink -f "$monitor_lua")"
  tmp="$(mktemp "$target.XXXXXX")" || return 0
  if ! awk -v output="$output" -v scale="$scale" '
    function rule(name) { return "hl.monitor({ output = \"" name "\"," }
    function whole_line() { return $0 ~ /\}\)[[:space:]]*(--.*)?$/ }
    function names_output(    code, value) {
      code = $0
      sub(/--.*/, "", code)
      if (!match(code, /output[[:space:]]*=[[:space:]]*"[^"]*"/)) return 0
      value = substr(code, RSTART, RLENGTH)
      sub(/^[^"]*"/, "", value)
      sub(/"$/, "", value)
      return value == output
    }
    names_output() {
      if (updated || index($0, rule(output)) != 1 || !whole_line() ||
        !match($0, /[{, ]scale = ("[^"]*"|[A-Za-z0-9_.]+) *[,}]/)) {
        skip = 1
        exit
      }
      tail = substr($0, RSTART, RLENGTH)
      sub(/^[{, ]scale = ("[^"]*"|[A-Za-z0-9_.]+)/, "", tail)
      $0 = substr($0, 1, RSTART) "scale = " scale tail substr($0, RSTART + RLENGTH)
      updated = 1
    }
    !catch_all && index($0, rule("")) == 1 { catch_all = NR; catch_all_whole = whole_line() }
    { lines[NR] = $0 }
    END {
      if (skip || (!updated && !(catch_all && catch_all_whole))) exit 1
      for (i = 1; i <= NR; i++) {
        print lines[i]
        if (!updated && i == catch_all)
          print "hl.monitor({ output = \"" output "\", mode = \"preferred\", position = \"auto\", scale = " scale " })"
      }
    }
  ' "$target" >"$tmp"; then
    rm -f "$tmp"
    return 0
  fi

  # GDK_SCALE is a single session-wide value, set from the lowest monitor scale.
  sed -i -E \
    -e "s|^local omarchy_gdk_scale = .*|local omarchy_gdk_scale = ${gdk_scale}|" \
    -e 's|^hl\.env\("GDK_SCALE", "[^"]*"\)|hl.env("GDK_SCALE", "'"$gdk_scale"'")|' \
    "$tmp"
  chmod --reference="$target" "$tmp"
  mv "$tmp" "$target"
}

scale_from_current() {
  local direction="${1:-}"
  local width="${2:-}"
  local height="${3:-}"

  awk -v direction="$direction" -v list="${SCALES[*]}" -v width="$width" -v height="$height" '
    function gcd(a, b, t) { while (b) { t = a % b; a = b; b = t } return a }
    function clean(scale,    g, k) {
      g = gcd(width * 120, height * 120)
      k = int(scale * 120 + 0.5)
      if (k > g) k = g
      while (g % k != 0) k++
      return k / 120
    }
    NR == 1 { scale = $0; found = 1 }
    END {
      if (!found) exit 1

      preset_count = split(list, presets, " ")
      for (i = 1; i <= preset_count; i++) {
        effective = clean(presets[i])
        key = sprintf("%.8f", effective)
        distance = presets[i] - effective
        if (distance < 0) distance = -distance

        # Multiple presets can collapse to the same clean scale. Keep only the
        # closest label so stepping always moves to a distinct effective value.
        if (!(key in effective_index)) {
          effective_index[key] = ++n
          effective_scales[n] = effective
          scales[n] = presets[i]
          distances[n] = distance
        } else {
          idx = effective_index[key]
          if (distance < distances[idx]) {
            scales[idx] = presets[i]
            distances[idx] = distance
          }
        }
      }

      # Snap to the nearest effective scale first. Hyprland reports floating
      # point values, so exact comparisons can otherwise get stuck.
      best = 1; best_diff = 1e9
      for (i = 1; i <= n; i++) {
        diff = scale - effective_scales[i]; if (diff < 0) diff = -diff
        if (diff < best_diff) { best_diff = diff; best = i }
      }

      if (direction == "next") {
        print scales[(best < n ? best + 1 : n)]
      } else if (direction == "previous") {
        print scales[(best > 1 ? best - 1 : 1)]
      } else {
        print scales[best]
      }
    }'
}

case "${1:-}" in
"")
  focused_monitor_scale | normalize_scale
  ;;
-h | --help)
  usage
  ;;
up)
  monitor_info=$(hyprctl monitors -j | jq -e -c '.[] | select(.focused == true)')
  set_scale "$(echo "$monitor_info" | jq -r '.scale' | scale_from_current next \
    "$(echo "$monitor_info" | jq -r '.width')" "$(echo "$monitor_info" | jq -r '.height')")" "up"
  ;;
down)
  monitor_info=$(hyprctl monitors -j | jq -e -c '.[] | select(.focused == true)')
  set_scale "$(echo "$monitor_info" | jq -r '.scale' | scale_from_current previous \
    "$(echo "$monitor_info" | jq -r '.width')" "$(echo "$monitor_info" | jq -r '.height')")" "down"
  ;;
1 | 1.25 | 1.6 | 2 | 3 | 4)
  set_scale "$1" "$1"
  ;;
*)
  if [[ $1 =~ ^[0-9]+([.][0-9]+)?$ ]] &&
    awk -v scale="$1" 'BEGIN { exit !(scale >= 1 && scale <= 4) }'; then
    set_scale "$1" "$1"
  else
    usage >&2
    exit 1
  fi
  ;;
esac
