#!/bin/bash
#
# hobot-audio-profile - Audio Tuning Profile engine for RDK S100
#
# Applies per-board audio tuning profiles (mixer state, codec register
# scripts, PulseAudio config) selected via srpi-config.
#
# The engine has NO knowledge of which physical sub-board is attached.
# Board selection is stored by srpi-config in:
#   /etc/hobot_audio_config/cur_audio_hat   (board display, informational)
#   /etc/hobot_audio_config/cur_profile     (profile id, the apply source)
#
# Copyright (C) 2026 Horizon Robotics / D-Robotics
# License: GPL-2.0
#

PROFILES_DIR="/etc/hobot_audio_config/profiles"
CUR_HAT_FILE="/etc/hobot_audio_config/cur_audio_hat"
CUR_PROFILE_FILE="/etc/hobot_audio_config/cur_profile"
BACKUP_PA="/etc/hobot_audio_config/original_default.pa"
SYSFS_ES7210_CHIP0="/sys/bus/i2c/devices/5-0040/es7210_debug/es7210"
SYSFS_ES7210_CHIP1="/sys/bus/i2c/devices/5-0042/es7210_debug/es7210"
SND_CTL="/dev/snd/controlC0"

# Register whitelist for PROFILE_REGS writes (via es7210 sysfs debug node).
# - ALC sel/common/max-gain, HPF: these survive stream open or are harmless
#   to re-write; anything outside is rejected.
# - NEVER allow 0x00-0x0F (clock/reset chain) or 0x3D-0x3F (read-only ID).
ALLOWED_REGS_ES7210="16 17 18 19 1a 1b 1c 1d 1e 20 21 22 23"

log()  { echo "hobot-audio-profile: $*"; }
err()  { echo "hobot-audio-profile: ERROR: $*" >&2; }
die()  { err "$*"; exit 1; }

usage() {
	cat <<EOF
Usage: hobot-audio-profile <command> [args]

Commands:
  list                     List available profiles
  status                   Show current board/profile selection and state
  validate <profile-id>    Validate a profile (config + state + regs)
  select <profile-id>      Set current profile and apply it live
  apply [profile-id]       Apply current (or given) profile
  deploy <profile-id>      Set current profile without applying
  import <file>            Validate and install a profile into $PROFILES_DIR
  unset                    Clear selection and restore stock config

Environment/overrides: none. Run as root.
EOF
}

# ---------------------------------------------------------------- helpers

wait_sound_card() {
	local timeout="${1:-15}" waited=0
	[ -e "$SND_CTL" ] && return 0
	while [ $waited -lt "$timeout" ]; do
		sleep 1; waited=$((waited + 1))
		[ -e "$SND_CTL" ] && return 0
	done
	return 1
}

load_profile() {
	local id="$1" file="$PROFILES_DIR/$id.profile"
	[ -f "$file" ] || die "profile not found: $file"
	# shellcheck disable=SC1090
	. "$file"
	[ -n "$PROFILE_ID" ] || die "$file: PROFILE_ID not set"
	[ "$PROFILE_ID" = "$id" ] || die "$file: PROFILE_ID($PROFILE_ID) != requested($id)"
}

list_profile_ids() {
	local f id
	for f in "$PROFILES_DIR"/*.profile; do
		[ -f "$f" ] || continue
		id="$(basename "$f" .profile)"
		unset PROFILE_DISPLAY PROFILE_ID
		# shellcheck disable=SC1090
		. "$f" 2>/dev/null
		echo "$id	${PROFILE_DISPLAY:-?}"
	done
}

# Validate one register spec: chip:reg=value (all hex, 8-bit)
check_reg_spec() {
	local spec="$1" chip reg val
	chip="${spec%%:*}"
	spec="${spec#*:}"
	reg="${spec%%=*}"
	val="${spec#*=}"
	case "$chip" in 0|1) ;; *) err "reg spec '$spec': chip must be 0 or 1"; return 1 ;; esac
	reg="${reg#0x}"; val="${val#0x}"
	if ! [[ "$reg" =~ ^[0-9a-fA-F]{1,2}$ ]] || ! [[ "$val" =~ ^[0-9a-fA-F]{1,2}$ ]]; then
		err "reg spec: bad reg/value: $reg=$val"; return 1
	fi
	reg="$(printf '%d' "0x$reg")"
	# whitelist check (decimal compare)
	local a
	for a in $ALLOWED_REGS_ES7210; do
		[ "$reg" = "$((16#$a))" ] && return 0
	done
	err "register 0x$(printf '%02x' "$reg") not in whitelist; allowed: $ALLOWED_REGS_ES7210"
	return 1
}

validate_profile() {
	local id="$1"
	load_profile "$id"
	[ -n "$PROFILE_DISPLAY" ] || die "$id: PROFILE_DISPLAY not set"
	[ -n "$PROFILE_VERSION" ] || die "$id: PROFILE_VERSION not set"
	[ -f "$PROFILES_DIR/$PROFILE_STATE" ] || die "$id: state file missing: $PROFILE_STATE"
	local spec
	for spec in $PROFILE_REGS; do
		check_reg_spec "$spec" || die "$id: invalid reg spec: $spec"
	done
	log "profile '$id' OK (display=\"$PROFILE_DISPLAY\" ver=$PROFILE_VERSION state=$PROFILE_STATE regs=${PROFILE_REGS:-none})"
	return 0
}

# Write one reg via the es7210 sysfs debug node.
# Node protocol (es7210.c es7210_store): value = (count<<16)|(reg<<8)|val, hex.
write_es7210_reg() {
	local chip="$1" reg="$2" val="$3" node w
	if [ "$chip" = "0" ]; then node="$SYSFS_ES7210_CHIP0"; else node="$SYSFS_ES7210_CHIP1"; fi
	[ -e "$node" ] || { err "sysfs node missing ($node); driver not loaded?"; return 1; }
	w="$(printf '%05x' $(( (1 << 16) | (0x$reg << 8) | 0x$val )))"
	echo "$w" > "$node" 2>/dev/null || { err "write failed: chip$chip 0x$reg=0x$val"; return 1; }
	log "reg chip$chip: 0x$(printf '%02x' "0x$reg") <- 0x$(printf '%02x' "0x$val")"
	return 0
}

# ---------------------------------------------------------------- commands

cmd_list() {
	list_profile_ids
}

cmd_status() {
	echo "board selection : $(cat "$CUR_HAT_FILE" 2>/dev/null || echo '(none)')"
	echo "current profile : $(cat "$CUR_PROFILE_FILE" 2>/dev/null || echo '(none)')"
	echo "sound card      : $([ -e "$SND_CTL" ] && echo present || echo absent)"
	echo "available profiles:"
	list_profile_ids | sed 's/^/  /'
}

cmd_validate() {
	[ -n "$1" ] || die "validate: missing profile id"
	validate_profile "$1"
}

cmd_deploy() {
	local id="$1"
	[ -n "$id" ] || die "deploy: missing profile id"
	validate_profile "$id"
	mkdir -p "$(dirname "$CUR_PROFILE_FILE")"
	echo "$id" > "$CUR_PROFILE_FILE"
	# stage mixer state for boot-time alsactl restore
	cp -f "$PROFILES_DIR/$PROFILE_STATE" /var/lib/alsa/asound.state
	log "deployed profile '$id'"
	return 0
}

cmd_apply() {
	local id="${1:-$(cat "$CUR_PROFILE_FILE" 2>/dev/null)}"
	[ -n "$id" ] || { log "no profile selected; nothing to apply"; return 0; }
	validate_profile "$id" || die "apply aborted: profile invalid"

	if ! wait_sound_card "${APPLY_WAIT:-10}"; then
		err "sound card controlC0 not present; is the audio driver loaded?"
		return 1
	fi

	# 1) mixer state (all codec controls: PGA, ALC gains, DAC volume, HP...)
	alsactl restore -f "$PROFILES_DIR/$PROFILE_STATE" 0 2>/dev/null \
		|| alsactl restore -f "$PROFILES_DIR/$PROFILE_STATE" 0
	log "mixer state restored from $PROFILE_STATE"

	# 2) register script (whitelisted, best-effort)
	local spec chip reg val rc=0
	for spec in $PROFILE_REGS; do
		chip="${spec%%:*}"; spec="${spec#*:}"
		reg="${spec%%=*}";  val="${spec#*=}"
		reg="${reg#0x}"; val="${val#0x}"
		write_es7210_reg "$chip" "$reg" "$val" || rc=1
	done

	# 3) pulseaudio config (guarded; headless boards may not run PA)
	if [ -n "$PROFILE_PA" ] && [ -f "$PROFILES_DIR/$PROFILE_PA" ]; then
		if [ ! -f "$BACKUP_PA" ]; then
			cp -f /etc/pulse/default.pa "$BACKUP_PA" 2>/dev/null
		fi
		cp -f "$PROFILES_DIR/$PROFILE_PA" /etc/pulse/default.pa
		if pgrep -x pulseaudio >/dev/null 2>&1; then
			killall pulseaudio 2>/dev/null
			log "pulseaudio config applied from $PROFILE_PA (PA restarted by session)"
		else
			log "pulseaudio config staged from $PROFILE_PA (PA not running)"
		fi
	fi

	[ "$rc" -eq 0 ] || err "some register writes failed (see above)"
	log "profile '$id' applied"
	return "$rc"
}

cmd_select() {
	local id="$1"
	[ -n "$id" ] || die "select: missing profile id"
	cmd_deploy "$id" && cmd_apply "$id"
}

cmd_import() {
	local src="$1"
	[ -f "$src" ] || die "import: file not found: $src"
	local id
	id="$(basename "$src" .profile)"
	# validate in a sandbox dir first
	mkdir -p /tmp/.hap_import
	cp -f "$src" "/tmp/.hap_import/$id.profile"
	# accompanying state file, if referenced
	unset PROFILE_STATE PROFILE_REGS PROFILE_ID PROFILE_DISPLAY PROFILE_VERSION
	# shellcheck disable=SC1090
	. "/tmp/.hap_import/$id.profile"
	if [ -n "$PROFILE_STATE" ] && [ -f "$(dirname "$src")/$PROFILE_STATE" ]; then
		cp -f "$(dirname "$src")/$PROFILE_STATE" "/tmp/.hap_import/$PROFILE_STATE"
	fi
	local saved_dir="$PROFILES_DIR"
	PROFILES_DIR="/tmp/.hap_import"
	validate_profile "$id"; local vrc=$?
	PROFILES_DIR="$saved_dir"
	[ $vrc -eq 0 ] || { rm -rf /tmp/.hap_import; die "import: validation failed"; }
	mkdir -p "$PROFILES_DIR"
	cp -f "/tmp/.hap_import/$id.profile" "$PROFILES_DIR/"
	[ -n "$PROFILE_STATE" ] && cp -f "/tmp/.hap_import/$PROFILE_STATE" "$PROFILES_DIR/" 2>/dev/null
	rm -rf /tmp/.hap_import
	log "imported profile '$id'"
}

cmd_unset() {
	rm -f "$CUR_PROFILE_FILE" "$CUR_HAT_FILE"
	rm -f /var/lib/alsa/asound.state
	if [ -f "$BACKUP_PA" ]; then
		cp -f "$BACKUP_PA" /etc/pulse/default.pa 2>/dev/null
		rm -f "$BACKUP_PA"
	fi
	log "selection cleared, stock config restored"
}

# ---------------------------------------------------------------- main

case "$1" in
	list)        shift; cmd_list "$@" ;;
	status)      shift; cmd_status "$@" ;;
	validate)    shift; cmd_validate "$@" ;;
	deploy)      shift; cmd_deploy "$@" ;;
	apply)       shift; APPLY_WAIT="${APPLY_WAIT:-10}"; cmd_apply "$@" ;;
	select)      shift; cmd_select "$@" ;;
	import)      shift; cmd_import "$@" ;;
	unset)       shift; cmd_unset "$@" ;;
	-h|--help|*) usage; [ "$1" = "-h" -o "$1" = "--help" ] && exit 0 || exit 1 ;;
esac
