#!/bin/sh
# tlp-func-rf-sw - Radio Switch Functions
#
# Copyright (c) 2019 Thomas Koch <linrunner at gmx.net> and others.
# This software is licensed under the GPL v2 or later.

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

# shellcheck disable=SC2086

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

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

readonly ALLDEV="bluetooth wifi wwan"

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

readonly DEFAULT_RESTORE_DEVICE_STATE_ON_STARTUP=0

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

get_devc () { # get control device for radio type
              # $1: rftype bluetooth/wifi/wwan
              # retval $_devc: sysdev, $_devs: device state
              #     $_rfkdev: 1/0=is/is not an rfkill device,
              #     $_devon, $_devoff: value to write directly to the sysdev
              #     to achieve the desired switch state

    local i

    check_sysfs "get_devc" "/sys/class/rfkill"

    # preset retvals
    _devc=""
    _devs=254
    _rfkdev="1"
    _devon="1"
    _devoff="0"

    case $1 in
        wwan|bluetooth)
            for i in /sys/class/rfkill/rfkill* ; do
                if [ "$(read_sysf $i/type)" = "$1" ]; then
                    _devc="$i/state"
                    echo_debug "rf" "get_devc($1) = $_devc"
                    return 0
                fi
            done
            ;;

        wifi)
            for i in /sys/bus/pci/drivers/ipw2?00/*/rf_kill; do
                if [ -f "$i" ]; then
                    _devc="$i"
                    _rfkdev="0"
                    _devon="0"
                    _devoff="1"
                    echo_debug "rf" "get_devc($1) = $_devc"
                    return 0
                fi
            done

            for i in /sys/class/rfkill/rfkill* ; do
                if [ "$(read_sysf $i/type)" = "wlan" ]; then
                    _devc="$i/state"
                    echo_debug "rf" "get_devc($1) = $_devc"
                    return 0
                fi
            done
            ;;

        *)
            echo "Error: unknown device type \"$1\"" 1>&2
            echo_debug "rf" "get_devc($1).unknown_type"
            return 0
            ;;
    esac

    echo_debug "rf" "get_devc($1).not_present"

    return 0
}

get_devs () { # get radio device state -- $1: rftype; retval $_devs: 0=off/1=on
    if [ -n "$_devc" ]; then
        _devs="$(read_sysf $_devc)"
        case "$_devs" in
            0|1) # invert state when not a rfkill device
                [ "$_rfkdev" = "0" ] && _devs=$((_devs ^ _devoff))
                ;;
            2) ;; # hard blocked device
            *) _devs=3 # invalid state
        esac
    fi

    echo_debug "rf" "get_devs($1) = $_devs"

    return 0
}

err_no_root_priv () { # check root privilege
    echo "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 running and nmcli is installed

    cmd_exists $NMCLI
}

invoke_nmcli () { # call nmcli with radio option according to the program version
                  # $1: rftype, $2: on/off, $3: caller; rc: last 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_state () { # get radio type state -- $1: rftype; retval $_devc, $_devs: 0=off/1=on
    echo_debug "rf" "device_state($1)"

    get_devc $1
    get_devs $1
}

device_switch () { # switch radio type 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 newst devn

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

    get_devc $1

    # quit if no device
    if [ -z "$_devc" ]; then
        echo_debug "rf" "device_switch($1, $2).no_device: rc=1"
        return 1
    fi

    # 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

    # get current device state
    get_devs $1
    curst=$_devs

    # 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

    # wifi, wwan: before rfkill (only if X_configured) -- notify NM of desired state
    if [ "$X_USE_NMCLI" = "1" ] && [ "$1" != "bluetooth" ]; then
        case $newst in
            1) invoke_nmcli $1 on  ;;
            0) invoke_nmcli $1 off ;;
        esac
        # record device state after nmcli
        get_devs $1
        # update current state
        curst=$_devs

        # 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
    fi

    # 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"

    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

        # determine value for direct write
        case $newst in
            1) devn=$_devon  ;;
            0) devn=$_devoff ;;
        esac

        # switch device state when either rfkill isn't disabled
        # or it's a bluetooth device
        if [ "$X_USE_RFKILL" != "0" ] || [ "$1" != "bluetooth" ]; then
            if [ "$_rfkdev" = "1" ] && cmd_exists $RFKILL ; then
                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_devs $1
                else
                    # missing permission to rfkill
                    err_no_root_priv "device_switch($1, $2).rfkill"
                fi
            else
                # use direct write
                if test_root ; then
                    write_sysf "$devn" $_devc
                    echo_debug "rf" "device_switch($1, $2).devc: rc=$?"
                    # record device state after direct write
                    get_devs $1
                else
                    err_no_root_priv "device_switch($1, $2).devc"
                fi
            fi
        fi # rfkill not disabled or bluetooth
    fi # states did not match

    # wifi, wwan: after rkfill (default) -- notify NM of desired state
    if [ "$X_USE_NMCLI" != "0" ] && [ "$1" != "bluetooth" ]; then
        case $newst in
            1) invoke_nmcli $1 on  ;;
            0) invoke_nmcli $1 off ;;
        esac
        # record final device state
        get_devs $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

    # 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 type state -- $1: rftype, $2: state
    case $1 in
        bluetooth)
            devstr="bluetooth"
            ;;

        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
    local dev
    local devlist="${1:-$ALLDEV}" # when arg empty -> use all

    echo_debug "rf" "save_device_states($devlist): $RFSTATEFILE"

    # create empty state file
    create_rundir
    { : > $RFSTATEFILE; } 2> /dev/null

    # iterate over all possible devices -> save state in file
    for dev in $devlist; do
        device_state $dev
        { printf '%s\n' "$dev $_devs" >> $RFSTATEFILE; } 2> /dev/null
    done

    return 0
}

restore_device_states () { # restore radio type states
    local sline

    echo_debug "rf" "restore_device_states: $RFSTATEFILE"

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

        return 0
    else
        return 1
    fi
}

set_radio_device_states () { # set/initialize all radio states
    # $1: start/stop/1/0/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:-${DEFAULT_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="$DEVICES_TO_DISABLE_ON_SHUTDOWN"
                devs2enable="$DEVICES_TO_ENABLE_ON_SHUTDOWN"

                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
                ;;

            1) # 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 ;;
                        wifi) any_wifi_in_use ;;
                        wwan) any_wwan_in_use ;;
                    esac } || wordinlist $dev "$devs2disable" || devs2disable="$dev $devs2disable"
                done
                devs2disable="${devs2disable# }"
                ;;

            0) # 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" ] || echo -n "Disabling radios:"
            for dev in $devs2disable; do
                [ "$quiet" = "1" ] || echo -n " $dev"
                device_switch $dev off
            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 wifi wwan; do
                    if ! wordinlist "$dev" "$devs2enable"; then
                        device_switch $dev off
                    fi
                done
            else
                # start mode: enable listed radios
                [ "$quiet" = "1" ] || echo -n "Enabling radios:"
                for dev in $devs2enable; do
                    [ "$quiet" = "1" ] || echo -n " $dev"
                    device_switch $dev on
                done
                [ "$quiet" = "1" ] || echo "."
            fi
        fi

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

    return 0
}
