#!/usr/bin/env bash
# Read-only RK3588 media hardware inventory with optional short workloads.

set -uo pipefail
export LC_ALL=C

JSON=0
QUICK=0
STRICT=0
MEDIA_DIR=""

SYS_ROOT=${RK1_SYSFS_ROOT:-/sys}
PROC_ROOT=${RK1_PROCFS_ROOT:-/proc}
DEV_ROOT=${RK1_DEV_ROOT:-/dev}
RKNN_HOME=${RKNN_HOME:-/opt/rknn/current}

declare -a CHECK_GROUP=()
declare -a CHECK_NAME=()
declare -a CHECK_STATUS=()
declare -a CHECK_MESSAGE=()

usage() {
    cat <<'EOF'
Usage: rk1-media-selftest [OPTIONS]

Options:
  --json               Emit one JSON document instead of readable text.
  --quick              Inventory only; skip GPU, codec, and NPU workloads.
  --strict             Return nonzero for warnings as well as failures.
  --media-dir PATH     Exercise RKMPP decoding with named sample files.
                       Recognized names contain h264, hevc, vp9, or av1.
  -h, --help           Show this help.

The default run is non-destructive. It executes only short inference, Vulkan,
and encoder probes when the relevant runtime and devices are present. It never
changes clocks, governors, display modes, storage, or network configuration.
EOF
}

invocation_error() {
    printf 'rk1-media-selftest: %s\n' "$*" >&2
    exit 2
}

while (($#)); do
    case "$1" in
        --json)
            JSON=1
            shift
            ;;
        --quick)
            QUICK=1
            shift
            ;;
        --strict)
            STRICT=1
            shift
            ;;
        --media-dir)
            (($# >= 2)) || invocation_error "--media-dir requires a path"
            MEDIA_DIR=$2
            shift 2
            ;;
        -h|--help)
            usage
            exit 0
            ;;
        *)
            invocation_error "unknown option: $1"
            ;;
    esac
done

if [[ -n "$MEDIA_DIR" && ! -d "$MEDIA_DIR" ]]; then
    invocation_error "media directory is not a directory: $MEDIA_DIR"
fi

add_check() {
    CHECK_GROUP+=("$1")
    CHECK_NAME+=("$2")
    CHECK_STATUS+=("$3")
    CHECK_MESSAGE+=("$4")
}

read_text() {
    local path=$1
    if [[ -r "$path" ]]; then
        tr -d '\000' <"$path" 2>/dev/null || true
    fi
}

short_message() {
    local value=$1
    value=${value//$'\r'/}
    value=${value//$'\n'/'; '}
    value=${value//$'\t'/ }
    printf '%s' "${value:0:320}"
}

driver_binding_count() {
    local pattern=$1
    local directory entry base
    local count=0
    shopt -s nullglob
    for directory in "$SYS_ROOT"/bus/platform/drivers/$pattern; do
        [[ -d "$directory" ]] || continue
        for entry in "$directory"/*; do
            base=${entry##*/}
            case "$base" in
                bind|unbind|uevent|module|new_id|remove_id) continue ;;
            esac
            if [[ -L "$entry" || -d "$entry" ]]; then
                ((count++))
            fi
        done
    done
    shopt -u nullglob
    printf '%d' "$count"
}

find_sample() {
    local token=$1
    [[ -n "$MEDIA_DIR" ]] || return 1
    find "$MEDIA_DIR" -maxdepth 1 -type f \
        \( -iname "*${token}*.mkv" -o -iname "*${token}*.mp4" \
           -o -iname "*${token}*.webm" -o -iname "*${token}*.ts" \) \
        -print -quit 2>/dev/null
}

# System identity
machine=$(uname -m 2>/dev/null || printf unknown)
if [[ "$machine" == "aarch64" ]]; then
    add_check system architecture pass "aarch64 userspace"
else
    add_check system architecture fail "expected aarch64, found $machine"
fi

model=$(read_text "$PROC_ROOT/device-tree/model")
if [[ "$model" == *"Turing"*"RK1"* ]]; then
    add_check system board pass "$model"
elif [[ -n "$model" ]]; then
    add_check system board warn "device-tree model is $model"
else
    add_check system board warn "device-tree model is unavailable"
fi

# HDMI / DRM connector state
shopt -s nullglob
hdmi_connectors=("$SYS_ROOT"/class/drm/card*-HDMI-A-*)
shopt -u nullglob
if ((${#hdmi_connectors[@]} == 0)); then
    add_check hdmi connector warn "no DRM HDMI connector is registered"
else
    connected_count=0
    for connector in "${hdmi_connectors[@]}"; do
        connector_name=${connector##*/}
        state=$(read_text "$connector/status")
        case "$state" in
            connected)
                ((connected_count++))
                add_check hdmi "$connector_name" pass "connected"
                if [[ -s "$connector/edid" ]]; then
                    edid_bytes=$(wc -c <"$connector/edid" 2>/dev/null || printf 0)
                    add_check hdmi "$connector_name-edid" pass \
                        "$edid_bytes bytes of EDID data"
                else
                    add_check hdmi "$connector_name-edid" warn \
                        "connected but EDID is empty or unreadable"
                fi
                if [[ -r "$connector/modes" ]] &&
                   grep -qx '3840x2160' "$connector/modes" 2>/dev/null; then
                    add_check hdmi "$connector_name-4k" pass \
                        "EDID advertises a 3840x2160 mode"
                else
                    add_check hdmi "$connector_name-4k" warn \
                        "3840x2160 is not present in the connector mode list"
                fi
                ;;
            disconnected)
                add_check hdmi "$connector_name" warn \
                    "disconnected (expected when no display is attached)"
                ;;
            *)
                add_check hdmi "$connector_name" warn \
                    "connector state is ${state:-unknown}"
                ;;
        esac
    done
fi

hdmi_driver_count=$(driver_binding_count 'dwhdmi*')
hdmi_phy_count=$(driver_binding_count '*hdptx*hdmi*')
if ((hdmi_driver_count > 0 && hdmi_phy_count > 0)); then
    add_check hdmi drivers pass \
        "$hdmi_driver_count controller and $hdmi_phy_count HDMI PHY binding(s)"
else
    add_check hdmi drivers warn \
        "HDMI controller bindings=$hdmi_driver_count, PHY bindings=$hdmi_phy_count"
fi

# GPU / Vulkan
gpu_driver=""
for candidate in panthor panfrost mali; do
    if (($(driver_binding_count "$candidate") > 0)); then
        gpu_driver=$candidate
        break
    fi
done
case "$gpu_driver" in
    panthor)
        add_check gpu kernel-driver pass "Panthor is bound to the GPU"
        ;;
    panfrost)
        add_check gpu kernel-driver warn "Panfrost is bound; the image expects Panthor"
        ;;
    mali)
        add_check gpu kernel-driver warn "proprietary Mali kernel driver is bound"
        ;;
    *)
        add_check gpu kernel-driver warn "no bound Mali GPU driver was found"
        ;;
esac

shopt -s nullglob
gpu_devfreq=("$SYS_ROOT"/class/devfreq/*.gpu)
render_nodes=("$DEV_ROOT"/dri/renderD*)
shopt -u nullglob
if ((${#gpu_devfreq[@]} > 0)); then
    add_check gpu devfreq pass "${#gpu_devfreq[@]} GPU devfreq device(s)"
else
    add_check gpu devfreq warn "GPU devfreq node is missing"
fi
if ((${#render_nodes[@]} > 0)); then
    add_check gpu render-node pass "${#render_nodes[@]} DRM render node(s)"
else
    add_check gpu render-node warn "no accessible DRM render node"
fi

if ((QUICK)); then
    add_check gpu vulkan skip "active Vulkan probe disabled by --quick"
elif command -v vulkaninfo >/dev/null 2>&1; then
    vulkan_output=$(timeout 20s vulkaninfo --summary 2>&1)
    vulkan_rc=$?
    if ((vulkan_rc == 0)); then
        gpu_name=$(printf '%s\n' "$vulkan_output" |
            sed -n 's/^[[:space:]]*deviceName[[:space:]]*=[[:space:]]*//p' |
            head -1)
        add_check gpu vulkan pass "${gpu_name:-vulkaninfo completed}"
    else
        add_check gpu vulkan fail \
            "vulkaninfo failed: $(short_message "$vulkan_output")"
    fi
else
    add_check gpu vulkan skip "vulkaninfo is not installed"
fi

# VPU bindings and FFmpeg integration
decoder_bindings=$(driver_binding_count 'mpp_rkvdec*')
encoder_bindings=$(driver_binding_count 'mpp_rkvenc*')
av1_bindings=$(driver_binding_count '*av1*')
if ((decoder_bindings > 0)); then
    add_check vpu decoder-driver pass "$decoder_bindings RKMPP decoder binding(s)"
else
    add_check vpu decoder-driver warn "RKMPP decoder bindings are missing"
fi
if ((encoder_bindings > 0)); then
    add_check vpu encoder-driver pass "$encoder_bindings RKMPP encoder binding(s)"
else
    add_check vpu encoder-driver warn "RKMPP encoder bindings are missing"
fi
if ((av1_bindings > 0)); then
    add_check vpu av1-driver pass "$av1_bindings AV1 decoder binding(s)"
else
    add_check vpu av1-driver warn "a separate AV1 driver binding was not found"
fi

FFMPEG=""
for candidate in /opt/rkmedia/bin/ffmpeg-rk /usr/local/bin/ffmpeg-rk; do
    if [[ -x "$candidate" ]]; then
        FFMPEG=$candidate
        break
    fi
done
if [[ -z "$FFMPEG" ]] && command -v ffmpeg-rk >/dev/null 2>&1; then
    FFMPEG=$(command -v ffmpeg-rk)
fi

if [[ -z "$FFMPEG" ]]; then
    add_check vpu ffmpeg-rkmpp skip "ffmpeg-rk is not installed"
    add_check rga ffmpeg-filter skip "ffmpeg-rk is not installed"
else
    hwaccels=$($FFMPEG -hide_banner -hwaccels 2>&1)
    if grep -qE '(^|[[:space:]])rkmpp($|[[:space:]])' <<<"$hwaccels"; then
        add_check vpu ffmpeg-rkmpp pass "$FFMPEG advertises the rkmpp hwaccel"
    else
        add_check vpu ffmpeg-rkmpp fail "$FFMPEG does not advertise rkmpp"
    fi

    encoders=$($FFMPEG -hide_banner -encoders 2>&1)
    for codec in h264 hevc mjpeg; do
        if grep -q "${codec}_rkmpp" <<<"$encoders"; then
            add_check vpu "encode-$codec-capability" pass \
                "${codec}_rkmpp is registered"
        else
            add_check vpu "encode-$codec-capability" fail \
                "${codec}_rkmpp is not registered"
        fi
    done

    filters=$($FFMPEG -hide_banner -filters 2>&1)
    if grep -q 'scale_rkrga' <<<"$filters"; then
        add_check rga ffmpeg-filter pass "scale_rkrga is registered"
    else
        add_check rga ffmpeg-filter fail "scale_rkrga is not registered"
    fi

    if ((QUICK)); then
        add_check vpu encode-workload skip "active encoder probe disabled by --quick"
    else
        for codec in h264 hevc mjpeg; do
            encode_output=$(timeout 30s "$FFMPEG" -nostdin -v error \
                -f lavfi -i 'color=size=320x240:rate=30:duration=0.2' \
                -frames:v 3 -c:v "${codec}_rkmpp" -f null - 2>&1)
            encode_rc=$?
            if ((encode_rc == 0)); then
                add_check vpu "encode-$codec-workload" pass \
                    "three frames submitted successfully"
            else
                add_check vpu "encode-$codec-workload" fail \
                    "hardware encode failed: $(short_message "$encode_output")"
            fi
        done
    fi

    if [[ -n "$MEDIA_DIR" && $QUICK -eq 0 ]]; then
        first_decode_sample=""
        for codec in h264 hevc vp9 av1; do
            sample=$(find_sample "$codec" || true)
            if [[ -z "$sample" ]]; then
                add_check vpu "decode-$codec-workload" skip \
                    "no sample containing '$codec' in $MEDIA_DIR"
                continue
            fi
            [[ -n "$first_decode_sample" ]] || first_decode_sample=$sample
            decode_output=$(timeout 180s "$FFMPEG" -nostdin -v error \
                -hwaccel rkmpp -i "$sample" -map 0:v:0 -f null - 2>&1)
            decode_rc=$?
            if ((decode_rc == 0)); then
                add_check vpu "decode-$codec-workload" pass \
                    "decoded ${sample##*/} through RKMPP"
            else
                add_check vpu "decode-$codec-workload" fail \
                    "decode failed: $(short_message "$decode_output")"
            fi
        done

        if [[ -n "$first_decode_sample" ]] && grep -q 'scale_rkrga' <<<"$filters"; then
            rga_output=$(timeout 60s "$FFMPEG" -nostdin -v error \
                -hwaccel rkmpp -hwaccel_output_format drm_prime \
                -i "$first_decode_sample" -map 0:v:0 -frames:v 30 \
                -vf 'scale_rkrga=w=1280:h=720:format=nv12' -f null - 2>&1)
            rga_rc=$?
            if ((rga_rc == 0)); then
                add_check rga workload pass "RKMPP-to-RGA scaling completed"
            else
                add_check rga workload fail \
                    "RGA workload failed: $(short_message "$rga_output")"
            fi
        else
            add_check rga workload skip "no decode sample is available for RGA"
        fi
    elif [[ -z "$MEDIA_DIR" ]]; then
        add_check vpu decode-workload skip "use --media-dir to test hardware decoding"
        add_check rga workload skip "use --media-dir to test zero-copy RGA scaling"
    fi
fi

rga2_bindings=$(driver_binding_count 'rga2')
rga3_bindings=$(driver_binding_count 'rga3')
if ((rga2_bindings + rga3_bindings > 0)); then
    add_check rga kernel-driver pass \
        "RGA2 bindings=$rga2_bindings, RGA3 bindings=$rga3_bindings"
else
    add_check rga kernel-driver warn "no RGA2/RGA3 binding was found"
fi

# NPU runtime and per-core inference
npu_bindings=$(driver_binding_count 'rknpu')
((npu_bindings += $(driver_binding_count 'RKNPU')))
shopt -s nullglob
npu_devfreq=("$SYS_ROOT"/class/devfreq/*.npu)
shopt -u nullglob
if ((npu_bindings > 0 || ${#npu_devfreq[@]} > 0)); then
    add_check npu kernel-driver pass \
        "RKNPU bindings=$npu_bindings, devfreq nodes=${#npu_devfreq[@]}"
    npu_present=1
else
    add_check npu kernel-driver warn "RKNPU driver/device was not found"
    npu_present=0
fi

runtime_so="$RKNN_HOME/lib/librknnrt.so"
model_path="$RKNN_HOME/share/models/rk3588/mobilenet_v1.rknn"
npu_test="$RKNN_HOME/bin/rknn-inference-test"
if [[ -r "$runtime_so" ]]; then
    runtime_hash=$(sha256sum "$runtime_so" 2>/dev/null)
    runtime_hash=${runtime_hash%% *}
    if [[ "$runtime_hash" == \
          "d31fc19c85b85f6091b2bd0f6af9d962d5264a4e410bfb536402ec92bac738e8" ]]; then
        add_check npu runtime pass "RKNN Runtime 2.3.2 checksum matches"
    else
        add_check npu runtime fail "unexpected librknnrt.so checksum $runtime_hash"
    fi
else
    add_check npu runtime warn "RKNN runtime is not installed at $runtime_so"
fi

if ((QUICK)); then
    add_check npu inference skip "active inference disabled by --quick"
elif ((npu_present == 0)); then
    add_check npu inference skip "NPU driver is absent; inference was not attempted"
elif [[ ! -x "$npu_test" || ! -r "$model_path" ]]; then
    add_check npu inference skip "test binary or pinned RK3588 model is unavailable"
else
    for core in 0 1 2 all; do
        npu_output=$(timeout 45s "$npu_test" --model "$model_path" \
            --core "$core" --iterations 1 2>&1)
        npu_rc=$?
        if ((npu_rc == 0)) && grep -q 'RKNN_RESULT status=pass' <<<"$npu_output"; then
            add_check npu "core-$core" pass "$(short_message "$npu_output")"
        else
            add_check npu "core-$core" fail \
                "inference failed: $(short_message "$npu_output")"
        fi
    done
fi

# HDMI ALSA devices
asound_cards=$(read_text "$PROC_ROOT/asound/cards")
if [[ -n "$asound_cards" ]] && grep -qiE 'hdmi|rockchiphdmi' <<<"$asound_cards"; then
    add_check audio hdmi-card pass "an HDMI ALSA card is registered"
elif [[ -n "$asound_cards" ]]; then
    add_check audio hdmi-card warn "ALSA cards exist, but none is labeled HDMI"
else
    add_check audio hdmi-card warn "ALSA card inventory is unavailable"
fi
if command -v aplay >/dev/null 2>&1; then
    aplay_output=$(aplay -l 2>&1)
    if grep -qi 'hdmi' <<<"$aplay_output"; then
        add_check audio pcm-device pass "aplay lists an HDMI PCM device"
    else
        add_check audio pcm-device warn \
            "aplay did not list an HDMI PCM device: $(short_message "$aplay_output")"
    fi
else
    add_check audio pcm-device skip "aplay is not installed"
fi

# Link state only: no pings, DNS requests, or network mutations.
shopt -s nullglob
interfaces=("$SYS_ROOT"/class/net/*)
shopt -u nullglob
interface_names=()
up_names=()
for interface in "${interfaces[@]}"; do
    name=${interface##*/}
    [[ "$name" == lo ]] && continue
    interface_names+=("$name")
    state=$(read_text "$interface/operstate")
    carrier=$(read_text "$interface/carrier")
    if [[ "$state" == up || "$carrier" == 1 ]]; then
        up_names+=("$name")
    fi
done
if ((${#interface_names[@]} == 0)); then
    add_check network interfaces warn "no non-loopback interface was found"
elif ((${#up_names[@]} > 0)); then
    add_check network link pass "up: ${up_names[*]}"
else
    add_check network link warn \
        "interfaces present but down: ${interface_names[*]}"
fi

# Root filesystem, eMMC, and NVMe presence. No serial numbers are read.
if command -v findmnt >/dev/null 2>&1; then
    root_source=$(findmnt -n -o SOURCE / 2>/dev/null || true)
    root_fstype=$(findmnt -n -o FSTYPE / 2>/dev/null || true)
    if [[ -n "$root_source" ]]; then
        add_check storage rootfs pass "$root_source ($root_fstype)"
    else
        add_check storage rootfs warn "root filesystem source is unavailable"
    fi
else
    add_check storage rootfs skip "findmnt is not installed"
fi

shopt -s nullglob
emmc_devices=("$SYS_ROOT"/block/mmcblk*)
nvme_devices=("$SYS_ROOT"/block/nvme*n1)
shopt -u nullglob
if ((${#emmc_devices[@]} > 0)); then
    add_check storage emmc pass "${#emmc_devices[@]} MMC block device(s)"
else
    add_check storage emmc warn "no MMC/eMMC block device was found"
fi
if ((${#nvme_devices[@]} > 0)); then
    add_check storage nvme pass "${#nvme_devices[@]} NVMe namespace(s)"
else
    add_check storage nvme warn "no NVMe namespace was found"
fi

pass_count=0
warn_count=0
fail_count=0
skip_count=0
for status in "${CHECK_STATUS[@]}"; do
    case "$status" in
        pass) ((pass_count++)) ;;
        warn) ((warn_count++)) ;;
        fail) ((fail_count++)) ;;
        skip) ((skip_count++)) ;;
    esac
done

if ((fail_count > 0)); then
    overall=fail
elif ((warn_count > 0)); then
    overall=warn
else
    overall=pass
fi

json_escape() {
    local value=$1
    value=${value//\\/\\\\}
    value=${value//\"/\\\"}
    value=${value//$'\n'/\\n}
    value=${value//$'\r'/\\r}
    value=${value//$'\t'/\\t}
    printf '%s' "$value"
}

if ((JSON)); then
    printf '{"schema_version":1,"overall":"%s","quick":%s,' \
        "$overall" "$([[ $QUICK -eq 1 ]] && printf true || printf false)"
    printf '"summary":{"pass":%d,"warn":%d,"fail":%d,"skip":%d},' \
        "$pass_count" "$warn_count" "$fail_count" "$skip_count"
    printf '"checks":['
    for ((i = 0; i < ${#CHECK_NAME[@]}; ++i)); do
        ((i == 0)) || printf ','
        printf '{"group":"%s","name":"%s","status":"%s","message":"%s"}' \
            "$(json_escape "${CHECK_GROUP[i]}")" \
            "$(json_escape "${CHECK_NAME[i]}")" \
            "${CHECK_STATUS[i]}" \
            "$(json_escape "${CHECK_MESSAGE[i]}")"
    done
    printf ']}\n'
else
    printf '%-9s %-24s %-6s %s\n' GROUP CHECK STATUS DETAIL
    printf '%-9s %-24s %-6s %s\n' '---------' '------------------------' \
        '------' '------'
    for ((i = 0; i < ${#CHECK_NAME[@]}; ++i)); do
        printf '%-9s %-24s %-6s %s\n' \
            "${CHECK_GROUP[i]}" "${CHECK_NAME[i]}" \
            "${CHECK_STATUS[i]^^}" "${CHECK_MESSAGE[i]}"
    done
    printf '\nOverall: %s (pass=%d warn=%d fail=%d skip=%d)\n' \
        "${overall^^}" "$pass_count" "$warn_count" "$fail_count" "$skip_count"
fi

if ((fail_count > 0 || (STRICT && warn_count > 0))); then
    exit 1
fi
exit 0
