#!/bin/bash

# omarchy:summary=Display Hyprland keybindings defined in your configuration using walker for an interactive search menu.

declare -A KEYCODE_SYM_MAP
# Hyprland reports XKB keycodes for code: bindings. Keep a small fallback for
# common keys so the menu remains readable if xkbcli cannot resolve a symbol.
declare -A FALLBACK_KEYCODE_SYM_MAP=(
  [10]="1"
  [11]="2"
  [12]="3"
  [13]="4"
  [14]="5"
  [15]="6"
  [16]="7"
  [17]="8"
  [18]="9"
  [19]="0"
  [20]="MINUS"
  [21]="EQUAL"
  [61]="SLASH"
)

# Hyprland's Lua config provider currently reports code:... binds from hl.bind()
# as key="" and keycode=0 in `hyprctl -j binds`. Keep a lightweight
# source-derived key cache so the menu can still show those bindings.
declare -A LUA_BIND_KEY_MAP

build_keymap_cache() {
  local keymap
  keymap="$(xkbcli compile-keymap)" || {
    echo "Failed to compile keymap" >&2
    return 1
  }

  while IFS=, read -r code sym; do
    [[ -z $code || -z $sym ]] && continue
    KEYCODE_SYM_MAP["$code"]="$sym"
  done < <(
    awk '
      BEGIN { sec = "" }
      /xkb_keycodes/ { sec = "codes"; next }
      /xkb_symbols/  { sec = "syms";  next }
      sec == "codes" {
        if (match($0, /<([A-Za-z0-9_]+)>\s*=\s*([0-9]+)\s*;/, m)) code_by_name[m[1]] = m[2]
      }
      sec == "syms" {
        if (match($0, /key\s*<([A-Za-z0-9_]+)>\s*\{\s*\[\s*([^, \]]+)/, m)) sym_by_name[m[1]] = m[2]
      }
      END {
        for (k in code_by_name) {
          c = code_by_name[k]
          s = sym_by_name[k]
          if (c != "" && s != "" && s != "NoSymbol") print c "," s
        }
      }
    ' <<<"$keymap"
  )
}

lookup_keycode_cached() {
  local symbol
  symbol="${KEYCODE_SYM_MAP[$1]}"

  if [[ -z $symbol ]]; then
    symbol="${FALLBACK_KEYCODE_SYM_MAP[$1]}"
  fi

  if [[ -z $symbol ]]; then
    symbol="code:$1"
  fi

  printf '%s\n' "${symbol^^}"
}

parse_keycodes() {
  local start end elapsed
  [[ ${DEBUG:-0} == "1" ]] && start=$(date +%s.%N)
  while IFS= read -r line; do
    if [[ $line =~ code:([0-9]+) ]]; then
      code="${BASH_REMATCH[1]}"
      if [[ $code == "201" ]]; then
        echo "${line/SUPER SHIFT,code:201/,COPILOT KEY}"
      else
        symbol=$(lookup_keycode_cached "$code" "$XKB_KEYMAP_CACHE")
        echo "${line/code:${code}/$symbol}"
      fi
    elif [[ $line =~ mouse:([0-9]+) ]]; then
      code="${BASH_REMATCH[1]}"

      case "$code" in
        272) symbol="LEFT MOUSE BUTTON" ;;
        273) symbol="RIGHT MOUSE BUTTON" ;;
        274) symbol="MIDDLE MOUSE BUTTON" ;;
        *)   symbol="mouse:${code}" ;;
      esac

      echo "${line/mouse:${code}/$symbol}"
    else
      echo "$line"
    fi
  done

  if [[ $DEBUG == "1" ]]; then
    end=$(date +%s.%N)
    # fall back to awk if bc is missing
    if command -v bc >/dev/null 2>&1; then
      elapsed=$(echo "$end - $start" | bc)
    else
      elapsed=$(awk -v s="$start" -v e="$end" 'BEGIN{printf "%.6f", (e - s)}')
    fi
    echo "[DEBUG] parse_keycodes elapsed: ${elapsed}s" >&2
  fi
}

# Supplement `hyprctl -j binds` for Lua-only binds that Hyprland currently
# reports without their original key, such as hl.bind("SUPER + code:10", ...).
build_lua_bind_key_cache() {
  local modmask description key

  omarchy-cmd-present lua || return 0

  while IFS=$'\t' read -r modmask description key; do
    [[ -z $modmask || -z $description || -z $key ]] && continue
    LUA_BIND_KEY_MAP["$modmask,$description"]="$key"
  done < <(
    lua <<'LUA'
local modifiers = { SHIFT = 1, CTRL = 4, CONTROL = 4, ALT = 8, SUPER = 64 }

local function split_keys(keys)
  local modmask = 0
  local key = ""

  for part in string.gmatch(keys, "[^+]+") do
    local value = part:gsub("^%s+", ""):gsub("%s+$", "")
    local modifier = modifiers[string.upper(value)]

    if modifier then
      modmask = modmask + modifier
    else
      key = value
    end
  end

  return modmask, key
end

local function proxy()
  local p = {}

  return setmetatable(p, {
    __index = function()
      return p
    end,
    __call = function()
      return p
    end,
  })
end

local p = proxy()

hl = setmetatable({
  dsp = p,
  bind = function(keys, _, opts)
    opts = opts or {}
    if opts.description and opts.description ~= "" then
      local modmask, key = split_keys(keys)
      print(modmask .. "\t" .. opts.description .. "\t" .. key)
    end
    return p
  end,
  unbind = function() end,
  config = function() end,
  env = function() end,
  monitor = function() end,
  window_rule = function() end,
  gesture = function() end,
  animation = function() end,
  curve = function() end,
  exec_cmd = function() end,
}, {
  __index = function()
    return function()
      return p
    end
  end,
})

local config = (os.getenv("HOME") or "") .. "/.config/hypr/hyprland.lua"
local file = io.open(config, "r")

if file then
  file:close()
  local ok, err = pcall(dofile, config)
  if not ok and os.getenv("DEBUG") == "1" then
    io.stderr:write("[DEBUG] lua bind scan failed: " .. tostring(err) .. "\n")
  end
end
LUA
  )
}

modmask_to_text() {
  case "$1" in
    0)  printf '' ;;
    1)  printf 'SHIFT' ;;
    4)  printf 'CTRL' ;;
    5)  printf 'SHIFT CTRL' ;;
    8)  printf 'ALT' ;;
    9)  printf 'SHIFT ALT' ;;
    12) printf 'CTRL ALT' ;;
    13) printf 'SHIFT CTRL ALT' ;;
    64) printf 'SUPER' ;;
    65) printf 'SUPER SHIFT' ;;
    68) printf 'SUPER CTRL' ;;
    69) printf 'SUPER SHIFT CTRL' ;;
    72) printf 'SUPER ALT' ;;
    73) printf 'SUPER SHIFT ALT' ;;
    76) printf 'SUPER CTRL ALT' ;;
    77) printf 'SUPER SHIFT CTRL ALT' ;;
    *)  printf '%s' "$1" ;;
  esac
}

# Fetch dynamic keybindings from Hyprland.
#
# Also do some pre-processing:
# - Fill missing Lua code:... keys from the Lua source cache
# - Remove standard Omarchy bin path prefix
# - Map numeric modifier key mask to a textual rendition
# - Output comma-separated values that the parser can understand
dynamic_bindings() {
  local modmask key keycode description dispatcher arg modifiers

  hyprctl -j binds |
    jq -r '.[] | [.modmask, (.key // ""), (.keycode // 0), (.description // ""), (.dispatcher // ""), (.arg // "") | tostring] | join("\u001f")' |
    while IFS=$'\x1f' read -r modmask key keycode description dispatcher arg; do
      if [[ -z $key && $keycode != "0" ]]; then
        key="code:$keycode"
      fi

      if [[ -z $key && -n $description ]]; then
        key="${LUA_BIND_KEY_MAP["$modmask,$description"]}"
      fi

      [[ -z $description && $dispatcher == "__lua" ]] && continue

      modifiers=$(modmask_to_text "$modmask")
      arg="${arg//~\/.local\/share\/omarchy\/bin\//}"
      arg="${arg//uwsm app -- /}"
      arg="${arg//uwsm-app -- /}"

      printf '%s,%s,%s,%s,%s\n' "$modifiers" "$key" "$description" "$dispatcher" "$arg"
    done
}

# Hardcoded bindings, like the copy-url extension and such
static_bindings() {
  echo "SHIFT ALT,L,Copy URL from Web App,sendshortcut,SHIFT ALT,L,"
}

# Parse and format keybindings
#
# `awk` does the heavy lifting:
# - Set the field separator to a comma ','.
# - Joins the key combination (e.g., "SUPER + Q").
# - Joins the command that the key executes.
# - Prints display text and dispatch metadata as tab-separated fields.
parse_binding_records() {
  awk -F, '
{
    # Combine the modifier and key (first two fields)
    key_combo = $1 " + " $2;

    # Clean up: strip leading "+" if present, trim spaces
    gsub(/^[ \t]*\+?[ \t]*/, "", key_combo);
    gsub(/[ \t]+$/, "", key_combo);

    # Use description, if set
    action = $3;
    dispatcher = $4;

    # Reconstruct the dispatcher arg from the remaining fields
    arg = "";
    for (i = 5; i <= NF; i++) {
        arg = arg $i (i < NF ? "," : "");
    }

    if (action == "") {
        # Reconstruct the command from the remaining fields
        for (i = 4; i <= NF; i++) {
            action = action $i (i < NF ? "," : "");
        }

        # Clean up trailing commas, remove leading "exec, ", and trim
        sub(/,$/, "", action);
        gsub(/(^|,)[[:space:]]*exec[[:space:]]*,?/, "", action);
        gsub(/(^|[[:space:]])uwsm(-app| app)[[:space:]]+--[[:space:]]+/, "", action);
        gsub(/^[ \t]+|[ \t]+$/, "", action);
        gsub(/[ \t]+/, " ", key_combo);  # Collapse multiple spaces to one

        # Escape XML entities
        gsub(/&/, "\\&amp;", action);
        gsub(/</, "\\&lt;", action);
        gsub(/>/, "\\&gt;", action);
        gsub(/"/, "\\&quot;", action);
        gsub(/'"'"'/, "\\&apos;", action);
    }

    if (action != "") {
        printf "%-35s → %s\t%s\t%s\n", key_combo, action, dispatcher, arg;
    }
}'
}

prioritize_entries() {
  awk -F '\t' '
  {
    line = $1
    prio = 50
    if (match(line, /Terminal/)) prio = 0
    if (match(line, /Tmux/)) prio = 1
    if (match(line, /Browser/) && !match(line, /Browser[[:space:]]*\(/) && !match(line, /SUPER SHIFT.*\+.*B.*→.*Browser/)) prio = 2
    if (match(line, /File manager/) && !match(line, /File manager \(cwd\)/))  prio = 3
    if (match(line, /Launch apps/))  prio = 4
    if (match(line, /Omarchy menu/))  prio = 5
    if (match(line, /System menu/))  prio = 6
    if (match(line, /Theme menu/))  prio = 7
    if (match(line, /Full screen/))  prio = 8
    if (match(line, /Full width/))  prio = 9
    if (match(line, /Close window/))  prio = 10
    if (match(line, /Close all windows/))  prio = 11
    if (match(line, /Lock system/))  prio = 12
    if (match(line, /Toggle window floating/))  prio = 13
    if (match(line, /Toggle window split/))  prio = 14
    if (match(line, /Pop window/))  prio = 15
    if (match(line, /Universal/))  prio = 16
    if (match(line, /Clipboard/))  prio = 17
    if (match(line, /Audio controls/))  prio = 18
    if (match(line, /Bluetooth controls/))  prio = 19
    if (match(line, /Wifi controls/))  prio = 20
    if (match(line, /Emoji picker/))  prio = 21
    if (match(line, /Color picker/))  prio = 22
    if (match(line, /Screenshot/))  prio = 23
    if (match(line, /Screenrecording/))  prio = 24
    if (match(line, /SUPER SHIFT.*\+.*B.*→.*Browser/)) prio = 25
    if (match(line, /File manager \(cwd\)/))  prio = 26
    if (match(line, /(Switch|Next|Former|Previous).*workspace/))  prio = 27
    if (match(line, /Move window to workspace/))  prio = 28
    if (match(line, /Move window silently to workspace/))  prio = 29
    if (match(line, /Swap window/))  prio = 30
    if (match(line, /Focus/))  prio = 31
    if (match(line, /Move window$/))  prio = 32
    if (match(line, /Resize window/))  prio = 33
    if (match(line, /Expand window/))  prio = 34
    if (match(line, /Shrink window/))  prio = 35
    if (match(line, /scratchpad/))  prio = 36
    if (match(line, /notification/))  prio = 37
    if (match(line, /Toggle window transparency/))  prio = 38
    if (match(line, /Toggle workspace gaps/))  prio = 39
    if (match(line, /Toggle nightlight/))  prio = 40
    if (match(line, /Toggle locking/))  prio = 41
    if (match(line, /group/))  prio = 94
    if (match(line, /Scroll active workspace/))  prio = 95
    if (match(line, /Cycle to/))  prio = 96
    if (match(line, /Reveal active/))  prio = 97
    if (match(line, /Apple Display/))  prio = 98
    if (match(line, /XF86/))  prio = 99

    # print "priority<TAB>record"
    printf "%d\t%s\n", prio, $0
  }' |
  sort -k1,1n -k2,2 |
  cut -f2-
}

output_binding_records() {
  build_keymap_cache
  build_lua_bind_key_cache

  {
    dynamic_bindings
    static_bindings
  } |
    sort -u |
    parse_keycodes |
    parse_binding_records |
    prioritize_entries
}

output_keybindings() {
  output_binding_records | cut -f1
}

dispatch_binding() {
  local dispatcher="$1"
  local arg="$2"

  case "$dispatcher" in
    exec)
      [[ -n $arg ]] && hyprctl dispatch exec "$arg"
      ;;
    "") return 1 ;;
    *)
      if [[ -n $arg ]]; then
        hyprctl dispatch "$dispatcher" "$arg"
      else
        hyprctl dispatch "$dispatcher"
      fi
      ;;
  esac
}

if [[ $1 == "--print" || $1 == "-p" ]]; then
  output_keybindings
else
  records=$(output_binding_records)
  monitor_height=$(hyprctl monitors -j | jq -r '.[] | select(.focused == true) | .height')
  menu_height=$((monitor_height * 40 / 100))
  selection=$(cut -f1 <<<"$records" |
    walker --dmenu -p 'Keybindings' --width 800 --height "$menu_height")

  if [[ -n $selection ]]; then
    record=$(awk -F '\t' -v selection="$selection" '$1 == selection { print; exit }' <<<"$records")
    dispatcher=$(cut -f2 <<<"$record")
    arg=$(cut -f3- <<<"$record")

    dispatch_binding "$dispatcher" "$arg"
  fi
fi
