#!/bin/sh
# tlp-func-rf-sw - Radio Switch Functions
#
# Copyright (c) 2025 Thomas Koch <linrunner at gmx.net> and others.
# SPDX-License-Identifier: GPL-2.0-or-later

# Needs: tlp-func-base, tlp-func-rf

# shellcheck disable=SC2034

# ----------------------------------------------------------------------------
# Constants

readonly NMCLI=nmcli
readonly RFKILL="rfkill"
readonly RFKD="/dev/rfkill"

readonly ALLDEV="bluetooth nfc wifi wwan"

readonly RDW_NM_LOCK="rdw_nm"
readonly RDW_DOCK_LOCK="rdw_dock"
readonly RDW_NM_LOCKTIME=2
readonly RDW_KILL="rdw_kill"

readonly RFSTATEFILE="$VARDIR/rfkill_saved"

# ----------------------------------------------------------------------------
# Functions

get_rf_dev_state () {
    # get radio device state
    # $1: rftype
    # rc: 0=ok/1=no device (or unknown type)
    # retval $_devs: 0=off/1=on/254=no device (or unknown type)
    #        $_devc: sysfs rkill state

    local rfki rfks

    # preset: no device
    _devs=254
    _devc=""

    # step 1: get control device for radio type
    case "$1" in
        wwan|bluetooth|nfc)
            for rfki in /sys/class/rfkill/rfkill* ; do
                if [ "$(read_sysf "$rfki/type")" = "$1" ]; then
                    _devc="$rfki/state"
                fi
            done
            ;;

        wifi)
            for rfki in /sys/class/rfkill/rfkill* ; do
                if [ "$(read_sysf "$rfki/type")" = "wlan" ]; then
                    _devc="$rfki/state"
                fi
            done
            ;;

        *)
            # unknown radio type -> quit
            cecho "Error: unknown device type $1" 1>&2
            echo_debug "rf" "get_rf_dev_state($1).unknown_type"
            return 1
            ;;
    esac

    if [ -z "$_devc" ]; then
        # no device of this radio type -> quit
        echo_debug "rf" "get_rf_dev_state($1).not_present"
        return 1
    fi

    # step 2: get state for radio type
    if check_nm && wordinlist "$1" "wifi wwan";  then
        # read state from rfkill sysfs first ...
        rfks="$(read_sysf "$_devc")"
        # ... then get state from NM
        case "$($NMCLI radio "$1" 2> /dev/null)" in
            enabled)
                # assumption: the case NM(on), rfkill(off) does not occur
                _devs=1
                ;;

            disabled)
                case "$rfks" in
                    0) _devs=0 ;; # rfkill soft blocked
                    1) _devs=0 ;; # NM=off and rfkill=unblocked i.e. states are *not* in sync
                                  # -> return off(soft blocked) to keep things going
                    2) _devs=2 ;; # rfkill hard blocked
                esac
                ;;
            *) _devs=3 # unknown state
        esac
        echo_debug "rf" "get_rf_dev_state.nmcli($1) = $_devs; devc=$_devc; rfkill=$rfks"
    else
        # get state from rfkill sysfs
        _devs="$(read_sysf "$_devc")"
        case "$_devs" in
            0|1|2)     ;; # 0=soft blocked(off)/1=unblocked(on)/2=hard blocked(off)
            *) _devs=3 ;; # unknown state
        esac
        echo_debug "rf" "get_rf_dev_state.sysfs($1) = $_devs; devc=$_devc"
    fi

    return 0
}

err_no_root_priv () {
    # check root privilege

    cecho "Error: missing root privilege." 1>&2
    echo_debug "rf" "$1.missing_root_privilege"

    return 0
}

test_rfkill_perms () {
    # test if either root priv or rfkill device writable

    test_root || [ -w $RFKD ]
}

check_nm () {
    # test if NetworkManager is installed

    [ "$X_USE_NMCLI" != "0" ] && cmd_exists $NMCLI
}

invoke_nmcli () {
    # call nmcli to switch radio
    # $1: rftype, $2: on/off, $3: caller
    # rc: nmcli rc

    local rc

    check_nm || return 0 # return if NetworkManager not running

    $NMCLI radio "$1" "$2" > /dev/null 2>&1; rc=$?
    echo_debug "rf" "invoke_nmcli($1, $2).radio: rc=$rc"

    return $rc
}

device_switch () {
    # switch radio state
    # $1: rftype, $2: 1/on/0/off/toggle
    # $3: lock id, $4: lock duration
    # rc: 0=switched/1=invalid device or operation/
    #     2=hard blocked/3=invalid state/4=no change
    # retval $_devc, $_devs: 0=off/1=on

    local curst devn newst nmrc

    echo_debug "rf" "device_switch($1, $2, $3, $4)"

    # get current device state
    if ! get_rf_dev_state "$1"; then
        #  no device -> quit
        echo_debug "rf" "device_switch($1, $2).no_device: rc=1"
        return 1
    fi
    curst="$_devs"

    # quit if invalid operation
    if ! wordinlist "$2" "on 1 off 0 toggle"; then
        echo_debug "rf" "device_switch($1, $2).invalid_op: rc=1"
        return 1
    fi

    # quit if device state is hard blocked or invalid
    if [ "$_devs" -ge 2 ]; then
        case "$_devs" in
            2) echo_debug "rf" "device_switch($1, $2).hard_blocked: rc=$_devs" ;;
            *) echo_debug "rf" "device_switch($1, $2).invalid_state: rc=$_devs" ;;
        esac
        return "$_devs"
    fi

    # determine desired device state
    case "$2" in
        1|on)   newst=1 ;;
        0|off)  newst=0 ;;
        toggle) newst=$((curst ^ 1)) ;;
    esac

    # compare current and desired device state
    if [ "$curst" = "$newst" ]; then
        # desired matches current state --> do nothing
        echo_debug "rf" "device_switch($1, $2).desired_state"
        return 0
    else
        # desired does not match current state --> do switch

        # set timed lock if required
        [ -n "$3" ] && [ -n "$4" ] && [ "$1" != "bluetooth" ] && \
            set_timed_lock "$3" "$4"

        if check_nm && wordinlist "$1" "wifi wwan"; then
            # switch device with NetworkManager
            case "$newst" in
                1) invoke_nmcli "$1" on;  nmrc=$? ;;
                0) invoke_nmcli "$1" off; nmrc=$? ;;
            esac
            # record device state after nmcli
            get_rf_dev_state "$1"

            # interactive command only: check if failed due to missing privileges
            if [ -z "$3" ] && [ "$nmrc" = "1" ] && ! test_root; then
                err_no_root_priv "device_switch($1, $2).nmcli"
            fi

        elif cmd_exists $RFKILL ; then
            # switch device with rfkill
            if test_rfkill_perms ; then
                # use rfkill
                echo_debug "rf" "device_switch($1, $2).rfkill"
                case "$newst" in
                    1) $RFKILL unblock "$1" > /dev/null 2>&1 ;;
                    0) $RFKILL block "$1"   > /dev/null 2>&1 ;;
                    *) ;;
                esac
                # record device state after rfkill
                get_rf_dev_state "$1"
            else
                # missing permission to rfkill
                err_no_root_priv "device_switch($1, $2).rfkill"
            fi
        else
            # switch device with direct write
            # TODO: can't we remove that?
            if test_root ; then
                write_sysf "$newst" "$_devc"
                echo_debug "rf" "device_switch($1, $2).devc: rc=$?"
                # record device state after direct write
                get_rf_dev_state "$1"
            else
                err_no_root_priv "device_switch($1, $2).devc"
            fi
        fi
    fi # states did not match

    # quit if device state is hard blocked or invalid
    if [ "$_devs" -ge 2 ]; then
        case "$_devs" in
            2) echo_debug "rf" "device_switch($1, $2).hard_blocked: rc=$_devs" ;;
            *) echo_debug "rf" "device_switch($1, $2).invalid_state: rc=$_devs" ;;
        esac
        return "$_devs"
    fi

    # compare old and new device state
    if [ "$curst" = "$_devs" ]; then
        # state did not change
        echo_debug "rf" "device_switch($1, $2).no_change: rc=4"
        return 4
    else
        echo_debug "rf" "device_switch($1, $2).ok: rc=0"
        return 0
    fi
}

echo_device_state () {
    # print radio state -- $1: rftype, $2: state
    # prerequisite: get_rf_dev_state()
    #
    case "$1" in
        bluetooth)
            devstr="bluetooth"
            ;;

        nfc)
            devstr="nfc      "
            ;;

        wifi)
            devstr="wifi     "
            ;;

        wwan)
            devstr="wwan     "
            ;;

        *)
            devstr=$1
            ;;
    esac

    case "$2" in
        0)
            echo "$devstr = off (software)"
            ;;

        1)
            echo "$devstr = on"
            ;;

        2)
            echo "$devstr = off (hardware)"
            ;;

        254)
            echo "$devstr = none (no device)"
            ;;

        *)
            echo "$devstr = invalid state"
    esac

    return 0
}

# shellcheck disable=SC2120
save_device_states () {
    # save radio states
    # $1: list of rftypes
    # rc: 0=ok/1=create failed/2=write failed

    local dev
    local devlist="${1:-$ALLDEV}" # when arg empty -> use all
    local rc=0

    # create empty state file
    if [ -d "$VARDIR" ] && { : > "$RFSTATEFILE"; } 2> /dev/null; then
        # iterate over all possible devices -> save state in file
        for dev in $devlist; do
            get_rf_dev_state "$dev"
            { printf '%s\n' "$dev $_devs" >> "$RFSTATEFILE"; } 2> /dev/null || rc=2
        done
    else
        # create failed
        rc=1
    fi

    echo_debug "rf" "save_device_states($devlist): $RFSTATEFILE; rc=$rc"
    return $rc
}

restore_device_states () {
    # restore radio states
    # rc: 0=ok/1=state file nonexistent

    local sline
    local rc=0

    if [ -f "$RFSTATEFILE" ]; then
        # read state file
        # shellcheck disable=SC2162
        while read -r sline; do
            # shellcheck disable=SC2086
            set -- $sline # read dev, state into $1, $2
            device_switch "$1" "$2"
        done < "$RFSTATEFILE"
    else
        # state file nonexistent
        rc=1
    fi

    echo_debug "rf" "restore_device_states: $RFSTATEFILE; rc=$rc"
    return $rc
}

set_radio_device_states () {
    # set/initialize all radio states
    # $1: start/stop/PP_PRF=0/PP_BAL=1/PP_SAV=2/radiosw
    # called from init scripts or upon change of power source

    local dev devs2disable devs2enable restore
    local quiet=0

    # save/restore mode is disabled by default
    if [ "$1" != "radiosw" ]; then
        restore="$RESTORE_DEVICE_STATE_ON_STARTUP"
    else
        restore=0
    fi

    if [ "$restore" = "1" ]; then
        # "save/restore" mode
        echo_debug "rf" "set_radio_device_states($1).restore"
        case $1 in
            start)
                if restore_device_states; then
                    echo "Radio device states restored."
                else
                    echo "No saved radio device states found."
                fi
                ;;

            stop)
                # shellcheck disable=SC2119
                save_device_states
                echo "Radio device states saved."
                ;;
        esac
    else
        # "disable/enable on startup/shutdown or bat/ac" or "radiosw" mode
        case $1 in
            start) # system startup
                devs2disable="$DEVICES_TO_DISABLE_ON_STARTUP"
                devs2enable="$DEVICES_TO_ENABLE_ON_STARTUP"
                ;;

            stop) # system shutdown
                devs2disable=""
                devs2enable=""

                if [ "$X_WIFI_ON_SHUTDOWN" != "0" ]; then
                    # NM workaround: if
                    # 1. disable wifi is configured somehow, and
                    # 2. wifi is not explicitly configured for shutdown
                    # then re-enable wifi on shutdown to prepare for startup
                    if wordinlist "wifi" "$DEVICES_TO_DISABLE_ON_BAT
                                          $DEVICES_TO_DISABLE_ON_BAT_NOT_IN_USE
                                          $DEVICES_TO_DISABLE_ON_LAN_CONNECT
                                          $DEVICES_TO_DISABLE_ON_WIFI_CONNECT
                                          $DEVICES_TO_DISABLE_ON_WWAN_CONNECT" && \
                       ! wordinlist "wifi" "$devs2disable $devs2enable"; then
                        devs2enable="wifi $devs2enable"
                    fi
                fi
                ;;

            "$PP_BAL"|"$PP_SAV") # balanced/power-saver profile: battery power --> build disable list
                quiet=1 # do not display progress
                devs2enable=""
                devs2disable="${DEVICES_TO_DISABLE_ON_BAT:-}"

                # check configured list for connected devices
                for dev in ${DEVICES_TO_DISABLE_ON_BAT_NOT_IN_USE:-}; do
                    # if device is not connected and not in list yet --> add to disable list
                    { case $dev in
                        bluetooth) any_bluetooth_in_use ;;
                        nfc) any_nfc_in_use ;;
                        wifi) any_wifi_in_use ;;
                        wwan) any_wwan_in_use ;;
                    esac } || wordinlist "$dev" "$devs2disable" || devs2disable="$dev $devs2disable"
                done
                devs2disable="${devs2disable# }"
                ;;

            "$PP_PRF") # performance profile: AC power --> build enable list
                quiet=1 # do not display progress
                devs2enable="${DEVICES_TO_ENABLE_ON_AC:-}"
                devs2disable=""
                ;;

            radiosw)
                devs2disable=""
                devs2enable="$DEVICES_TO_ENABLE_ON_RADIOSW"
                ;;
        esac

        echo_debug "rf" "set_radio_device_states($1): enable=$devs2enable disable=$devs2disable"

        # disable configured radios
        if [ -n "$devs2disable" ]; then
            [ "$quiet" = "1" ] || printf "Disabling radios:"
            for dev in bluetooth nfc wifi wwan; do
                if wordinlist "$dev" "$devs2disable"; then
                    [ "$quiet" = "1" ] || printf ' %s' "$dev"
                    device_switch $dev off
                fi
            done
            [ "$quiet" = "1" ] || echo "."
        fi

        # enable configured radios
        if [ -n "$devs2enable" ]; then
            if [ "$1" = "radiosw" ]; then
                # radiosw mode: disable radios not listed
                for dev in bluetooth nfc wifi wwan; do
                    if ! wordinlist "$dev" "$devs2enable"; then
                        device_switch $dev off
                    fi
                done
            else
                # start mode: enable listed radios
                [ "$quiet" = "1" ] || printf "Enabling radios:"
                for dev in bluetooth nfc wifi wwan; do
                    if wordinlist "$dev" "$devs2enable"; then
                        [ "$quiet" = "1" ] || printf ' %s' "$dev"
                        device_switch $dev on
                    fi
                done
                [ "$quiet" = "1" ] || echo "."
            fi
        fi

        # clean up: discard state file
        rm -f "$RFSTATEFILE" 2> /dev/null
    fi

    return 0
}
