#!/usr/bin/env bash

set -euo pipefail

sink_name="${REFORGER_EQ_SINK_NAME:-${REFORGER_SURROUND_SINK_NAME:-reforger_eq_51}}"
sink_description="${REFORGER_EQ_DESCRIPTION:-${REFORGER_SURROUND_DESCRIPTION:-ReforgerEQ 5.1}}"
app_name="${REFORGER_EQ_APP_NAME:-${REFORGER_SURROUND_APP_NAME:-Arma Reforger}}"
steam_uri="${REFORGER_EQ_STEAM_URI:-${REFORGER_SURROUND_STEAM_URI:-steam://rungameid/1874880}}"
wait_seconds="${REFORGER_EQ_WAIT_SECONDS:-${REFORGER_SURROUND_WAIT_SECONDS:-90}}"
state_dir="${XDG_STATE_HOME:-$HOME/.local/state}/reforger-eq"
module_id_file="${state_dir}/module-id"
previous_default_sink_file="${state_dir}/previous-default-sink"

usage() {
  cat <<'EOF'
Usage:
  reforger-eq create
  reforger-eq up
  reforger-eq launch [command...]
  reforger-eq expose
  reforger-eq pin
  reforger-eq restore
  reforger-eq down
  reforger-eq status
  reforger-eq check

Commands:
  create   Create the temporary 5.1 sink without changing the default sink.
  up       Create the 5.1 sink, save the current default, and make it default.
  launch   Run up, launch Reforger, wait for its stream, then restore.
  expose   Create the 5.1 sink and move a running Reforger stream into it.
  pin      Alias for expose.
  restore  Restore the saved previous default sink, then expose Reforger if running.
  down     Restore the saved previous default sink and unload the 5.1 sink.
  status   Show default sink, virtual sink state, and detected Reforger stream.
  check    Report whether a running Reforger stream negotiated stereo or surround.

Environment:
  REFORGER_EQ_SINK_NAME     Override sink name. Default: reforger_eq_51
  REFORGER_EQ_DESCRIPTION   Override visible sink name. Default: ReforgerEQ 5.1
  REFORGER_EQ_APP_NAME      Override app name. Default: Arma Reforger
  REFORGER_EQ_STEAM_URI     Override Steam URI. Default: steam://rungameid/1874880
  REFORGER_EQ_WAIT_SECONDS  Override launch wait. Default: 90

Legacy REFORGER_SURROUND_* variables are still honored as fallbacks.
EOF
}

require_command() {
  local command="$1"

  if ! command -v "${command}" >/dev/null 2>&1; then
    printf 'Missing required command: %s\n' "${command}" >&2
    exit 127
  fi
}

ensure_dependencies() {
  require_command pactl
  require_command awk
  require_command sed
}

require_pulse_connection() {
  if ! pactl info >/dev/null 2>&1; then
    printf 'Could not connect to PulseAudio/PipeWire through pactl.\n' >&2
    printf 'Check that PipeWire and pipewire-pulse are running for this user session.\n' >&2
    exit 1
  fi
}

ensure_state_dir() {
  mkdir -p "${state_dir}"
}

current_default_sink() {
  pactl get-default-sink 2>/dev/null || true
}

sink_exists() {
  pactl list sinks short | awk -v sink_name="${sink_name}" '$2 == sink_name { found = 1 } END { exit !found }'
}

find_module_id() {
  pactl list short modules | awk -v sink_name="${sink_name}" '
    $2 == "module-null-sink" && index($0, "sink_name=" sink_name) {
      print $1
      exit
    }
  '
}

stored_module_id() {
  [[ -s "${module_id_file}" ]] && sed -n '1p' "${module_id_file}"
}

create_sink() {
  ensure_state_dir

  local module_id
  module_id="$(find_module_id || true)"
  if [[ -n "${module_id}" ]]; then
    printf '%s\n' "${module_id}" > "${module_id_file}"
    printf 'Using existing 5.1 sink: %s (module %s)\n' "${sink_name}" "${module_id}"
    return 0
  fi

  if sink_exists; then
    printf '5.1 sink already exists: %s\n' "${sink_name}"
    return 0
  fi

  module_id="$(pactl load-module module-null-sink \
    "sink_name=${sink_name}" \
    "sink_properties=device.description=\"${sink_description}\"" \
    "channels=6" \
    "channel_map=front-left,front-right,front-center,lfe,rear-left,rear-right" \
    "rate=48000")"

  printf '%s\n' "${module_id}" > "${module_id_file}"
  printf 'Created 5.1 sink: %s (module %s)\n' "${sink_name}" "${module_id}"
}

save_previous_default() {
  ensure_state_dir

  local default_sink
  default_sink="$(current_default_sink)"
  if [[ -n "${default_sink}" && "${default_sink}" != "${sink_name}" ]]; then
    printf '%s\n' "${default_sink}" > "${previous_default_sink_file}"
    printf 'Saved previous default sink: %s\n' "${default_sink}"
  fi
}

set_sink_default() {
  pactl set-default-sink "${sink_name}"
  printf 'Set launch default sink: %s\n' "${sink_name}"
}

prepare_launch_default() {
  create_sink
  save_previous_default
  set_sink_default
}

up() {
  prepare_launch_default
  cat <<EOF

Launch Arma Reforger now. After the game has created its audio stream, run:
  reforger-eq restore
  reforger-eq check
EOF
}

start_game() {
  if [[ "$#" -gt 0 ]]; then
    "$@" &
    printf 'Launched command: %s\n' "$*"
    return 0
  fi

  require_command steam
  steam "${steam_uri}" >/tmp/reforger-eq-steam.log 2>&1 &
  printf 'Requested Steam launch: %s\n' "${steam_uri}"
}

wait_for_app_stream() {
  local input_id elapsed

  for ((elapsed=0; elapsed<wait_seconds; elapsed++)); do
    input_id="$(find_app_sink_input_id || true)"
    if [[ -n "${input_id}" ]]; then
      printf '%s\n' "${input_id}"
      return 0
    fi
    sleep 1
  done

  return 1
}

launch_game() {
  local input_id

  prepare_launch_default
  start_game "$@"

  if ! input_id="$(wait_for_app_stream)"; then
    printf 'Timed out waiting for application.name="%s".\n' "${app_name}" >&2
    printf 'Leaving %s as default so late stream creation can still negotiate 5.1.\n' "${sink_name}" >&2
    printf 'Run `reforger-eq restore` after the game audio stream appears.\n' >&2
    return 1
  fi

  printf 'Detected %s sink-input #%s\n' "${app_name}" "${input_id}"
  restore_with_pin
  print_check
}

expose_app() {
  local input_id sample_spec sink_after

  create_sink
  input_id="$(find_app_sink_input_id || true)"

  if [[ -z "${input_id}" ]]; then
    printf 'No running sink-input found for application.name="%s".\n' "${app_name}" >&2
    return 1
  fi

  sample_spec="$(app_stream_sample_spec "${input_id}")"
  pactl move-sink-input "${input_id}" "${sink_name}"
  sink_after="$(app_stream_sink_name "${input_id}" || true)"
  printf 'Exposed %s sink-input #%s to %s\n' "${app_name}" "${input_id}" "${sink_name}"
  printf 'Current stream sink: %s\n' "${sink_after:-unknown}"

  case "${sample_spec}" in
    *" 6ch "*)
      printf 'Stream is 6ch; Reforger should now stay on the 5.1 sink.\n'
      ;;
    *" 2ch "*)
      printf 'Warning: stream is already stereo-only. Pinning will not make Reforger renegotiate surround.\n' >&2
      ;;
    *)
      printf 'Warning: unknown stream channel count; run reforger-eq check.\n' >&2
      ;;
  esac
}

pin_app_if_running() {
  local input_id
  input_id="$(find_app_sink_input_id || true)"

  if [[ -n "${input_id}" ]]; then
    expose_app
  else
    printf 'No %s stream found to pin; restoring only the system default.\n' "${app_name}"
  fi
}

restore_default() {
  local restore_sink
  restore_sink="$(sed -n '1p' "${previous_default_sink_file}" 2>/dev/null || true)"

  if [[ -z "${restore_sink}" ]]; then
    printf 'No saved previous default sink found; nothing to restore.\n'
    return 0
  fi

  if ! pactl list sinks short | awk -v sink_name="${restore_sink}" '$2 == sink_name { found = 1 } END { exit !found }'; then
    printf 'Saved previous default sink no longer exists: %s\n' "${restore_sink}" >&2
    return 1
  fi

  pactl set-default-sink "${restore_sink}"
  printf 'Restored default sink: %s\n' "${restore_sink}"
}

restore_with_pin() {
  restore_default || true
  pin_app_if_running
}

unload_sink() {
  local module_id
  module_id="$(find_module_id || true)"
  if [[ -z "${module_id}" ]]; then
    module_id="$(stored_module_id || true)"
  fi

  if [[ -z "${module_id}" ]]; then
    printf 'No %s module found to unload.\n' "${sink_name}"
    return 0
  fi

  pactl unload-module "${module_id}" 2>/dev/null || true
  rm -f "${module_id_file}"
  printf 'Unloaded 5.1 sink module: %s\n' "${module_id}"
}

down() {
  restore_default || true
  unload_sink
}

find_app_sink_input_id() {
  pactl list sink-inputs | awk -v app_name="${app_name}" '
    /^Sink Input #/ {
      id = substr($3, 2)
    }
    index($0, "application.name = \"" app_name "\"") {
      print id
      exit
    }
  '
}

app_stream_sink_name() {
  local input_id="$1"

  pactl list sink-inputs | awk -v target="Sink Input #${input_id}" '
    $0 == target {
      in_input = 1
      next
    }
    in_input && $1 == "Sink:" {
      print $2
      exit
    }
    in_input && /^Sink Input #/ {
      exit
    }
  ' | while read -r sink_id; do
    pactl list sinks short | awk -v sink_id="${sink_id}" '$1 == sink_id { print $2; exit }'
  done
}

app_stream_lines() {
  local input_id="$1"

  pactl list sink-inputs | awk -v target="Sink Input #${input_id}" '
    $0 == target {
      in_input = 1
      next
    }
    in_input && ($1 == "Sink:" || ($1 == "Sample" && $2 == "Specification:") || ($1 == "Channel" && $2 == "Map:")) {
      print
    }
    in_input && /^Sink Input #/ {
      exit
    }
  '
}

app_stream_sample_spec() {
  local input_id="$1"

  pactl list sink-inputs | awk -v target="Sink Input #${input_id}" '
    $0 == target {
      in_input = 1
      next
    }
    in_input && $1 == "Sample" && $2 == "Specification:" {
      print $0
      exit
    }
    in_input && /^Sink Input #/ {
      exit
    }
  '
}

app_stream_channel_map() {
  local input_id="$1"

  pactl list sink-inputs | awk -v target="Sink Input #${input_id}" '
    $0 == target {
      in_input = 1
      next
    }
    in_input && $1 == "Channel" && $2 == "Map:" {
      print $0
      exit
    }
    in_input && /^Sink Input #/ {
      exit
    }
  '
}

print_check() {
  local input_id sample_spec channel_map sink
  input_id="$(find_app_sink_input_id || true)"

  if [[ -z "${input_id}" ]]; then
    printf 'No running sink-input found for application.name="%s".\n' "${app_name}" >&2
    return 1
  fi

  sample_spec="$(app_stream_sample_spec "${input_id}")"
  channel_map="$(app_stream_channel_map "${input_id}")"
  sink="$(app_stream_sink_name "${input_id}" || true)"

  printf 'Detected %s sink-input #%s\n' "${app_name}" "${input_id}"
  printf 'Sink: %s\n' "${sink:-unknown}"
  printf '%s\n' "${sample_spec}"
  printf '%s\n' "${channel_map}"

  case "${sample_spec}" in
    *" 6ch "*)
      printf 'Result: success, Reforger negotiated surround output.\n'
      printf 'Expected useful channels: FL, FR, FC, RL, RR. LFE may be silent.\n'
      ;;
    *" 2ch "*)
      printf 'Result: stereo-only. Restart Reforger with the 5.1 sink selected/default before launch.\n'
      return 2
      ;;
    *)
      printf 'Result: unknown channel count; inspect the sample specification and channel map above.\n'
      return 3
      ;;
  esac
}

print_status() {
  local module_id input_id
  module_id="$(find_module_id || true)"
  input_id="$(find_app_sink_input_id || true)"

  printf 'default sink: %s\n' "$(current_default_sink)"
  printf '5.1 sink: %s\n' "${sink_name}"
  printf '5.1 module: %s\n' "${module_id:-not loaded}"
  printf 'saved previous default: %s\n' "$(sed -n '1p' "${previous_default_sink_file}" 2>/dev/null || printf 'none')"

  if sink_exists; then
    pactl list sinks short | awk -v sink_name="${sink_name}" '$2 == sink_name { print }'
  fi

  if [[ -z "${input_id}" ]]; then
    printf '%s stream: not found\n' "${app_name}"
  else
    printf '%s stream: sink-input #%s\n' "${app_name}" "${input_id}"
    app_stream_lines "${input_id}"
  fi
}

main() {
  local command="${1:-}"
  if [[ -z "${command}" ]]; then
    usage >&2
    exit 1
  fi
  shift || true

  case "${command}" in
    -h|--help|help)
      usage
      ;;
    create)
      ensure_dependencies
      require_pulse_connection
      create_sink
      ;;
    up)
      ensure_dependencies
      require_pulse_connection
      up
      ;;
    launch)
      ensure_dependencies
      require_pulse_connection
      launch_game "$@"
      ;;
    expose)
      ensure_dependencies
      require_pulse_connection
      expose_app
      ;;
    pin)
      ensure_dependencies
      require_pulse_connection
      expose_app
      ;;
    restore)
      ensure_dependencies
      require_pulse_connection
      restore_with_pin
      ;;
    down)
      ensure_dependencies
      require_pulse_connection
      down
      ;;
    status)
      ensure_dependencies
      require_pulse_connection
      print_status
      ;;
    check)
      ensure_dependencies
      require_pulse_connection
      print_check
      ;;
    *)
      usage >&2
      exit 1
      ;;
  esac
}

main "$@"
