#!/bin/bash
#
# Hammerspoon 一鍵設定（macOS）
# 說明：https://oawu.tw/blog/macos/hammerspoon/
#
# 用法：
#   curl -fsSL -o /tmp/hammerspoon-install.sh https://oawu.tw/blog/macos/hammerspoon/files/install.sh
#   bash /tmp/hammerspoon-install.sh
#
# ── 會做的事 ─────────────────────────────────────
#   1. 安裝 /Applications/Hammerspoon.app（從 GitHub releases 下載，不需要 Homebrew；已安裝則略過）
#   2. 下載 init.lua 到 ~/.hammerspoon/init.lua
#   3. 建立 ~/bin/hs，並在 ~/.zshrc 結尾加一行 PATH（已有則略過）
#   4. 啟動 Hammerspoon；設定檔有變更時會重新啟動
#   5. 檢查輔助使用權限
#   6. 驗證：權限、開機自啟、快捷鍵數量、Caps Lock 延遲
#
#   設定檔內容不同時，舊檔先改名成 *.bak.<時間> 再覆蓋，不會直接蓋掉。
#
# ── 需要人工的步驟 ───────────────────────────────
#   輔助使用權限：macOS 不允許程式授予。尚未授權時，腳本會開啟設定頁並以
#   結束代碼 2 結束。請使用者在「系統設定 ❯ 隱私權與安全性 ❯ 輔助使用」
#   打開 Hammerspoon 的開關，再執行一次本腳本。
#
# ── 結束代碼 ─────────────────────────────────────
#   0  完成，驗證全數通過
#   1  失敗（下載失敗、無法啟動、驗證未通過等），訊息會說明原因
#   2  需要人工步驟，完成後重新執行本腳本
#
# ── 可重複執行 ───────────────────────────────────
#   已完成的步驟會略過，重跑是安全的。
#
# ── 其他 ─────────────────────────────────────────
#   設定檔從 BASE_URL 下載。測試時可用環境變數指到別處，例如本機的開發伺服器：
#     BASE_URL=http://localhost:4321/blog/macos/hammerspoon/files bash install.sh
#

set -euo pipefail

BASE_URL="${BASE_URL:-https://oawu.tw/blog/macos/hammerspoon/files}"

APP="/Applications/Hammerspoon.app"
HS_CLI="$APP/Contents/Frameworks/hs/hs"
CONFIG_DIR="$HOME/.hammerspoon"
BIN_DIR="$HOME/bin"
ZSHRC="$HOME/.zshrc"
PATH_LINE='export PATH="$PATH:$HOME/bin"'

TMP_DIR="$(mktemp -d)"
trap 'rm -rf "$TMP_DIR"' EXIT

# 沒有終端機（例如由其他程式代為執行）時不上色，輸出才不會夾雜控制碼
if [ -t 1 ]; then
  BOLD=$'\033[1m' BLUE=$'\033[1;34m' GREEN=$'\033[32m' GRAY=$'\033[90m' YELLOW=$'\033[33m' RED=$'\033[31m' RESET=$'\033[0m'
else
  BOLD='' BLUE='' GREEN='' GRAY='' YELLOW='' RED='' RESET=''
fi
# 下載進度條同理，沒有終端機時改成安靜模式（錯誤訊息仍會顯示）
if [ -t 2 ]; then PROGRESS='-#'; else PROGRESS='-sS'; fi
step() { printf '\n%s==>%s %s%s%s\n' "$BLUE" "$RESET" "$BOLD" "$1" "$RESET"; }
ok()   { printf '    %s✔%s %s\n' "$GREEN" "$RESET" "$1"; }
skip() { printf '    %s- %s%s\n' "$GRAY" "$1" "$RESET"; }
warn() { printf '    %s!%s %s\n' "$YELLOW" "$RESET" "$1"; }
fail() { printf '\n%s✘ %s%s\n' "$RED" "$1" "$RESET" >&2; exit 1; }

# 需要人工處理的步驟：說明要做什麼後以結束代碼 2 結束，做完再重跑。
# 不用 read 等待按鍵，因為由其他程式代為執行時沒有終端機可讀
manual() {
  printf '\n%s需要人工處理：%s\n' "$YELLOW" "$RESET"
  printf '    %s\n' "$@"
  printf '\n完成後重新執行本腳本，已完成的步驟會自動略過。\n'
  exit 2
}

# 從 BASE_URL 下載設定檔，輸出暫存檔路徑
fetch() {
  local name="$1" out="$TMP_DIR/$1"
  curl -fsSL "$BASE_URL/$(printf '%s' "$name" | sed 's/ /%20/g')" -o "$out" || fail "下載失敗：$BASE_URL/$name"
  printf '%s' "$out"
}

# 放置檔案：內容相同就略過，不同則先備份再覆蓋。有變更時回傳 0
place() {
  local src="$1" dest="$2" label
  label="$(basename "$dest")"
  if [ -f "$dest" ] && cmp -s "$src" "$dest"; then
    skip "$label 已是最新"
    return 1
  fi
  if [ -f "$dest" ]; then
    local bak="$dest.bak.$(date +%Y%m%d%H%M%S)"
    mv "$dest" "$bak"
    warn "已備份舊檔：$(basename "$bak")"
  fi
  cp "$src" "$dest"
  ok "$label"
}

is_running() { pgrep -x Hammerspoon >/dev/null 2>&1; }

# 對 Hammerspoon 執行 Lua，印出結果
# stdin 導向 /dev/null：以 curl | bash 執行時，hs 會把其餘腳本當輸入吃掉
hs_eval() { "$HS_CLI" -t 5 -c "$1" </dev/null 2>/dev/null; }

# 等待 hs 指令可與 Hammerspoon 通訊（init.lua 內 require("hs.ipc")）
wait_ipc() {
  local i
  for i in $(seq 1 30); do
    [ "$(hs_eval 'print(1)')" = "1" ] && return 0
    sleep 1
  done
  return 1
}

restart_app() {
  if is_running; then
    osascript -e 'quit app "Hammerspoon"' >/dev/null 2>&1 || true
    for _ in $(seq 1 20); do is_running || break; sleep 0.5; done
  fi
  open "$APP"
  wait_ipc || fail "無法與 Hammerspoon 通訊，請手動重新啟動 Hammerspoon 後再執行"
}

is_trusted() { [ "$(hs_eval 'print(hs.accessibilityState())')" = "true" ]; }

[ "$(uname)" = "Darwin" ] || fail "此腳本只支援 macOS"

# 1. 安裝 Hammerspoon
step "安裝 Hammerspoon"
if [ -d "$APP" ]; then
  skip "已安裝"
else
  url="$(curl -fsSL https://api.github.com/repos/Hammerspoon/hammerspoon/releases/latest | sed -n 's/.*"browser_download_url": *"\([^"]*\.zip\)".*/\1/p' | head -n 1)"
  [ -n "$url" ] || fail "無法取得 Hammerspoon 最新版本"
  curl -fL "$PROGRESS" "$url" -o "$TMP_DIR/hs.zip"
  ditto -x -k "$TMP_DIR/hs.zip" "$TMP_DIR/hs"
  mv "$TMP_DIR/hs/Hammerspoon.app" "$APP"
  ok "$(basename "$url" .zip)"
fi

# 2. 設定檔
step "設定 init.lua"
mkdir -p "$CONFIG_DIR"
f="$(fetch "init.lua")"
changed=0
place "$f" "$CONFIG_DIR/init.lua" && changed=1

# 3. 命令列 hs
step "設定 hs 指令"
mkdir -p "$BIN_DIR"
ln -sfn "$HS_CLI" "$BIN_DIR/hs"
ok "$BIN_DIR/hs"
touch "$ZSHRC"
if grep -qxF "$PATH_LINE" "$ZSHRC"; then
  skip "~/.zshrc 已包含 ~/bin"
else
  printf '\n# My bin files\n%s\n' "$PATH_LINE" >> "$ZSHRC"
  ok "~/.zshrc 加入 ~/bin"
fi

# 4. 啟動（設定檔有變更時重新啟動，確保載入新設定）
step "啟動 Hammerspoon"
if is_running && [ "$changed" = 0 ] && wait_ipc; then
  skip "執行中"
else
  restart_app
  ok "已啟動"
fi

# 5. 輔助使用權限
step "輔助使用權限"
if is_trusted; then
  skip "已授權"
else
  # 剛打開開關時，要重新啟動 Hammerspoon 才會生效。
  # 這是使用者授權後重跑本腳本的情況，先重啟再判斷一次
  restart_app
  if is_trusted; then
    ok "已授權"
  else
    # 要先由 App 主動請求，Hammerspoon 才會出現在輔助使用的清單裡
    hs_eval 'hs.accessibilityState(true)' >/dev/null || true
    open "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"
    manual "已開啟「系統設定 ❯ 隱私權與安全性 ❯ 輔助使用」，請打開 Hammerspoon 的開關。" \
           "清單中沒有 Hammerspoon 時，按左下角 ＋ 選取 /Applications/Hammerspoon.app。"
  fi
fi

# 6. 驗證
step "驗證"
failed=0
check() {
  if [ "$2" = "$3" ]; then
    ok "$1：$2"
  else
    warn "$1：${2:-無回應}（預期 $3）"
    failed=$((failed + 1))
  fi
}
check "輔助使用權限" "$(hs_eval 'print(hs.accessibilityState())')" "true"
check "開機自動啟動" "$(hs_eval 'print(hs.autoLaunch())')" "true"
check "已註冊快捷鍵" "$(hs_eval 'print(#hs.hotkey.getHotkeys())')" "1"
check "Caps Lock 延遲" "$(hidutil property --get CapsLockDelayOverride 2>/dev/null | tr -d '[:space:]')" "0"
[ "$failed" = 0 ] || fail "驗證有 $failed 項未通過"

step "完成！"
echo "    按 command + option + ↑ 可將目前視窗移到下一個螢幕並填滿"
