#!/bin/sh
# tlp - rf switch functions
#
# Copyright (c) 2015 Thomas Koch <linrunner at gmx.net>
# This software is licensed under the GPL v2 or later.

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

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

readonly ALLDEV="bluetooth wifi wwan"

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

get_devc () { # get control device for radio type
              # $1: rftype bluetooth/wifi/wwan
              # retval $devc: sysdev,
              #     $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 [ -f $i/type ] && [ "$(cat $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 [ -f $i/type ] && [ "$(cat $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=$(cat $devc)
        [ "$rfkdev" = "0" ] && devs=$(($devs ^ $devoff))
    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 nm $1 $2 > /dev/null 2>&1; rc=$?
    echo_debug "rf" "invoke_nmcli($1, $2).nm: rc=$rc"
    if [ $rc -eq 2 ]; then
        # option "nm" is invalid for this NM version, try "radio" instead
        $NMCLI radio $1 $2 > /dev/null 2>&1; rc=$?
        echo_debug "rf" "invoke_nmcli($1, $2).radio: rc=$rc"
    fi

    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: on/off/toggle;
                   # retval $devc, $devs: 0=off/1=on

    local curst newst devn

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

    get_devc $1

    # quit if no device
    [ -z "$devc" ] && return 0

    # quit if invalid operation
    if ! wordinlist $2 "on 1 off 0 toggle"; then
        return 0
    fi

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

    # quit if device is hard blocked
    [ "$curst" = "2" ] && return 0

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

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

    # wifi, wwan: before rfkill -- notify NM of desired state
    if [ "$X_NO_NMCLI" != "1" ] && [ "$X_NMCLI_FIRST" = "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
    fi

    # wifi, wwan: quit if rfkill disabled
    [ "$X_NMCLI_ONLY" = "1" ] && [ "$1" != "bluetooth" ] return 0

    # if current state does not match desired state:
    # switch rfkill device state
    if [ "$curst" != "$newst" ]; then
        if [ "$rfkdev" = "1" ] && cmd_exists $RFKILL ; then
            if test_rfkill_perms ; then
                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
                err_no_root_priv "device_switch($1, $2).rfkill"
            fi
        else
            if test_root ; then
                echo_debug "rf" "device_switch($1, $2).devc"
                echo -n $devn > $devc
                # record device state after direct write
                get_devs $1
            else
                err_no_root_priv "device_switch($1, $2).devc"
            fi
        fi
    fi # if state does not match

    # wifi, wwan: after rfill -- notify NM of desired state
    if [ "$X_NO_NMCLI" != "1" ] && [ "$X_NMCLI_FIRST" != "1" ] \
        && [ "$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

    return 0
}

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 = unknown state \"$2\""
    esac

    return 0
}

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

    # create empty state file
    mkdir -p $STATEDIR 2> /dev/null 1>&2
    : > $RFSTATEFILE

    # iterate over all possible devices -> save state in file
    for dev in $devlist; do
        device_state $dev
        echo "$dev $devs" >> $RFSTATEFILE
    done

    return 0
}

restore_device_states () { # restore radio type states
    local sline

    echo_debug "rf" "restore_device_states"

    if [ -f $RFSTATEFILE ]; then
        # read state file
        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:-0}
    else
        restore=0
    fi

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

            stop)
                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"

                # NM workaround: when not explicitly configured,
                # re-enable wifi on shutdown to prepare for startup
                if ! wordinlist "wifi" "$devs2disable $devs2enable"; then
                    devs2enable="wifi $devs2enable"
                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
                    case $dev in
                        bluetooth) any_bluetooth_in_use ;;
                        wifi) any_wifi_in_use ;;
                        wwan) any_wwan_in_use ;;
                    esac
                    # if device is not connected and not in list yet --> add to disable list
                    [ $? = 0 ] || 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
}
