#!/bin/sh
# tlp-func-stat - tlp-stat Helper 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, 15-tlp-func-disk, 35-tlp-func-batt

# shellcheck disable=SC1004,SC2059,SC2086,SC2154

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

readonly INITCTL=initctl
readonly SMARTCTL=smartctl
readonly SYSTEMCTL=systemctl
readonly SYSTEMD=systemd

readonly RE_AC_QUIRK='^UNDEFINED$'
readonly RE_ATA_ERROR='ata[0-9]+: SError: {.*CommWake }'

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

# --- Checks

check_systemd () { # check if systemd is the active init system (PID 1) and systemctl is installed
                   # rc: 0=yes, 1=no
    [ -d /run/systemd/system ] && cmd_exists $SYSTEMCTL
}

check_upstart () { # check if upstart is active init system (PID 1)
                   # rc: 0=yes, 1=no
    cmd_exists $INITCTL && $INITCTL --version | grep -q upstart
}

check_openrc () { # check if openrc is the active init system (PID 1)
                  # rc: 0=yes, 1=no
    [ -e /run/openrc/softlevel ]
}

check_ac_quirk () { # check for hardware known not to expose AC device
                    # $1: model string; rc: 0=yes, 1=no
    printf '%s' "$1" | grep -E -q "${RE_AC_QUIRK}"
}

# --- Formatted Output

printparm () { # formatted output of sysfile - general
    # $1: format, $2: sysfile, $3: n/a message, $4: cutoff
    local format="$1"
    local sysf="$2"
    local namsg="$3"
    local cutoff="$4"
    local val=""

    if val=$(read_sysf $sysf); then
        # sysfile read successful
        if [ -n "$cutoff" ]; then
            val=${val%$cutoff}
        fi
    fi

    if [ -z "$val" ]; then
        # replace empty value with n/a text
        if [ -n "$namsg" ]; then
            if [ "$namsg" != "_" ]; then
                # use specific n/a text
                format=$(echo $format | sed -r -e "s/##(.*)##/($namsg)/" -e "s/\[.*\]//")
            else
                # _ = skip
                sysf=""
            fi
        else
            # empty n/a text, use default text
            format=$(echo $format | sed -r -e "s/##(.*)##/(not available)/" -e "s/\[.*\]//")
        fi
        # output n/a text or skip
        [ -n "$sysf" ] && printf "$format\n" "$sysf"
    else
        # non empty value: strip delimiters from format str
        format=$(echo $format | sed -r "s/##(.*)##/\1/")
        printf "$format\n" "$sysf" "$val"
    fi

    return 0
}

printparm_i915 () { # formatted output of sysfile - i915 kernel module variant
    # $1: sysfile; $2: alternative; $3: 1=psr/0=other
    local sysf val

    # Check if sysfile or alternative exist
    if [ -f "$1" ]; then
        sysf=$1
    elif [ -f "$2" ]; then
        sysf=$2
    else
        # neither file exists --> quit silently
        return 0
    fi

    if val=$(read_sysf $sysf); then
        # sysfile exists and is actually readable, output content
        printf "%-44s = %2d " "$sysf" "$val"
        # Explain content
        if [ "$val" = "-1" ]; then
            echo "(use per-chip default)"
        else
            echo -n "("
            if [ "$3" = "1" ]; then
                # enable_psr
                case $val in
                    0) echo -n "disabled" ;;
                    1) echo -n "enabled" ;;
                    2) echo -n "force link-standby mode" ;;
                    3) echo -n "force link-off mode" ;;
                    *) echo -n "unknown" ;;
                esac
            else
                # other parms
                if [ $((val & 1)) -ne 0 ]; then
                    echo -n "enabled"
                else
                    echo -n "disabled"
                fi
                [ $((val & 2)) -ne 0 ] && echo -n " + deep"
                [ $((val & 4)) -ne 0 ] && echo -n " + deepest"
            fi
            echo ")"
        fi
    else
        # sysfile was not readable
        printf "%-44s = (not available)\n" "$sysf"
    fi

    return 0
}

print_sysf () { # formatted output of a sysfile
    # $1: format; $2: sysfile
    local val

    if val=$(read_sysf $2); then
         # sysfile readable
        printf "$1" "$val"
    else
        # sysfile not readable
        printf "$1" "(not available)"
    fi

    return 0
}

print_sysf_trim () { # formatted output of a sysfile, trim leading and trailing
    # blanks -- $1: format; $2: sysfile
    local val

    if val=$(read_sysf $2); then
         # sysfile readable
        printf "$1" "$(printf "%s" "$val" | sed -r 's/^[[:blank:]]*//;s/[[:blank:]]*$//')"
    else
        # sysfile not readable
        printf "$1" "(not available)"
    fi

    return 0
}

print_file_modtime_and_age () { # show a file's last modification time
    #  and age in secs -- $1: file
    local mtime age

    if [ -f "$1" ]; then
        mtime=$(date +%X -r $1)
        age=$(( $(date +%s) - $(date +%s -r $1) ))
        printf '%s, %6d sec(s) ago' "$mtime" "$age"
    else
        printf "unknown"
    fi
}

print_saved_powerstate () { # read and print saved state
    case "$(read_sysf $PWRRUNFILE)" in
        0) printf "AC" ;;
        1) printf "battery" ;;
        *) printf "unknown" ;;
    esac

    # add manual mode
    get_manual_mode
    case "$_manual_mode" in
        0|1) printf " (manual)\n" ;;
        *)   printf "\n" ;;
    esac

    return 0
}

# --- Storage Devices

print_disk_model () { # print disk model -- $1: dev
    local model vendor

    model=$($HDPARM -I /dev/$1 2> /dev/null | grep 'Model Number' | \
      cut -f2 -d: | sed -r 's/^ *//' )

    if [ -z "$model" ]; then
        # hdparm -I not supported --> try udevadm approach
        vendor="$($UDEVADM info -n /dev/$1 -q property 2>/dev/null | sed -n 's/^ID_VENDOR=//p')"
        model="$( $UDEVADM info -n /dev/$1 -q property 2>/dev/null | sed -n 's/^ID_MODEL=//p' )"
        model=$(printf "%s %s" "$vendor" "$model" | sed -r 's/_/ /g; s/-//g; s/[[:space:]]+$//')
    fi

    printf '%s\n' "${model:-unknown}"

    return 0
}

print_disk_firmware () { # print firmware version --- $1: dev
    local firmware

    firmware=$($HDPARM -I /dev/$1 2> /dev/null | grep 'Firmware Revision' | \
      cut -f2 -d: | sed -r 's/^ *//' )
    printf '%s\n' "${firmware:-unknown}"

    return 0
}

get_disk_state () { # get disk power state -- $1: dev; retval: $_disk_state
    _disk_state=$($HDPARM -C /dev/$1 2> /dev/null | awk -F ':' '/drive state is/ { gsub(/ /,"",$2); print $2; }')
    [ -z "$_disk_state" ] && _disk_state="(not available)"

    return 0
}

get_disk_apm_level () { # get disk apm level -- $1: dev; rc: apm
    local apm

    apm=$($HDPARM -I /dev/$1 2> /dev/null | grep 'Advanced power management level' | \
          cut -f2 -d: | grep -E '^ *[0-9]+ *$')
    if [ -n "$apm" ]; then
        return $apm
    else
        return 0
    fi

}

get_disk_trim_capability () { # check for trim capability
    # $1: dev; rc: 0=no, 1=yes, 254=no ssd device

    local trim

    if $HDPARM -I /dev/$1 2> /dev/null | grep -q 'Solid State Device'; then
        if $HDPARM -I /dev/$1 2> /dev/null | grep -q 'TRIM supported'; then
            trim=1
        else
            trim=0
        fi
    else
        trim=255
    fi

    return $trim
}

check_ata_errors () { # check kernel log for ata errors
    # (possibly) caused by SATA_LINKPWR_ON_AC/BAT != max_performance
    # stdout: error count

    if wordinlist $SATA_LINKPWR_ON_BAT "min_power medium_power" || \
       wordinlist $SATA_LINKPWR_ON_AC "min_power medium_power"; then
        # config values != max_performance exist --> check kernel log

        # count matching error lines
        dmesg | grep -E -c "${RE_ATA_ERROR}" 2> /dev/null
    else
        # no values in question configured
        echo "0"
    fi

    return 0
}

show_disk_data () { # formatted output of NVMe / SATA disk data
    # $1: disk device

    # translate disk name
    get_disk_dev $1

    # check if block device
    if [ ! -b /dev/$_disk_dev ]; then
        # no block device for disk name --> we're done
        printf     "\n%s: not present.\n" /dev/$_disk_dev
        return 1
    fi

    # --- show general data
    case "$_disk_type" in
        nvme) # NVMe disk
            printf     "\n%s:\n" /dev/$_disk_dev
            printf     "  Type      = NVMe\n"
            [ -n "$_disk_id" ] && printf "  Disk ID   = %s\n" $_disk_id
            print_sysf "  Model     = %s\n" /sys/block/$_disk_dev/device/model
            print_sysf "  Firmware  = %s\n" /sys/block/$_disk_dev/device/firmware_rev
            # TODO: more features?
            ;;

        ata|usb|ieee1394)
            # ATA/USB/IEEE1394 disk
            printf     "\n%s:\n" /dev/$_disk_dev
            [ -n "$_disk_id" ] && printf "  Disk ID   = %s\n" $_disk_id

            printf     "  Type      = %s\n" "$(toupper $_disk_type)"

            # save spindle state
            get_disk_state $_disk_dev

            printf "  Model     = "
            print_disk_model $_disk_dev

            printf "  Firmware  = "
            print_disk_firmware $_disk_dev

            get_disk_apm_level $_disk_dev; local apm=$?
            printf "  APM Level = "
            case $apm in
                0|255)
                    printf "none/disabled\n"
                    ;;

                *)
                    printf "%s" $apm
                    if wordinlist "$_disk_type" "$DISK_TYPES_NO_APM_CHANGE"; then
                        printf " (changes not supported)\n"
                    else
                        printf "\n"
                    fi
                    ;;
            esac

            printf "  Status    = %s\n" $_disk_state

            get_disk_trim_capability $_disk_dev; local trim=$?
            case $trim in
                0) printf "  TRIM      = not supported\n" ;;
                1) printf "  TRIM      = supported\n" ;;
            esac

            # restore standby state
            [ "$_disk_state" = "standby" ] && spindown_disk $_disk_dev
            ;;

        *)
            printf     "\n%s: Device type \"%s\" ignored.\n" /dev/$_disk_dev $_disk_type
            return 1
            ;;
    esac

    if [ -f /sys/block/$_disk_dev/queue/scheduler ]; then
        if [ -d /sys/block/$_disk_dev/mq ]; then
            print_sysf_trim "  Scheduler = %s (multi queue)\n" /sys/block/$_disk_dev/queue/scheduler
        else
            print_sysf_trim "  Scheduler = %s (single queue)\n" /sys/block/$_disk_dev/queue/scheduler
        fi
    fi

    local pdev=/sys/block/$_disk_dev/device/power
    if [ -f $pdev/control ]; then
        echo
        print_sysf "  Runtime PM: control = %s, " $pdev/control
        print_sysf "autosuspend_delay_ms = %4s\n"    $pdev/autosuspend_delay_ms
    fi

    # --- show SMART data
    # skip if smartctl not installed or disk not SMART capable
    cmd_exists $SMARTCTL && $SMARTCTL /dev/$_disk_dev > /dev/null 2>&1 || return 0

    case "$_disk_type" in
        nvme)
            # NVMe disk
            printf "\n  SMART info:\n"
            $SMARTCTL -A /dev/$_disk_dev | \
                grep -E -e '^(Critical Warning|Temperature:|Available Spare)' \
                        -e '^(Percentage Used:|Data Units Written:|Power|Unsafe)' \
                        -e 'Integrity Errors' | \
                    sed 's/^/    /'
            ;;

        ata|usb)
            printf "\n  SMART info:\n"
            $SMARTCTL -A /dev/$_disk_dev | grep -v '<==' | \
                awk -F ' ' '$2 ~ /Power_Cycle_Count|Start_Stop_Count|Load_Cycle_Count|Reallocated_Sector_Ct/ \
                                { printf "    %3d %-25s = %8d \n", $1, $2, $10 } ; \
                          $2 ~ /Used_Rsvd_Blk_Cnt_Chip|Used_Rsvd_Blk_Cnt_Tot|Unused_Rsvd_Blk_Cnt_Tot/ \
                                { printf "    %3d %-25s = %8d \n", $1, $2, $10 } ; \
                          $2 ~ /Power_On_Hours/ \
                                { printf "    %3d %-25s = %8d %s\n", $1, $2, $10, "[h]" } ; \
                          $2 ~ /Temperature_Celsius/ \
                                { printf "    %3d %-25s = %8d %s %s %s %s\n", $1, $2, $10, $11, $12, $13, "[°C]" } ; \
                          $2 ~ /Airflow_Temperature_Cel/ \
                                { printf "    %3d %-25s = %8d %s\n", $1, $2, $10, "[°C]" } ; \
                          $2 ~ /G-Sense_Error_Rate/ \
                                { printf "    %3d %-25s = %8d \n", $1, $2, $10 } ; \
                          $2 ~ /Host_Writes/ \
                                { printf "    %3d %-25s = %8.3f %s\n", $1, $2, $10 / 32768.0, "[TB]" } ; \
                          $2 ~ /Total_LBAs_Written/ \
                                { printf "    %3d %-25s = %8.3f %s\n", $1, $2, $10 / 2147483648.0, "[TB]" } ; \
                          $2 ~ /NAND_Writes_1GiB/ \
                                { printf "    %3d %-25s = %8d %s\n", $1, $2, $10, "[GB]" } ; \
                          $2 ~ /Available_Reservd_Space|Media_Wearout_Indicator|Wear_Leveling_Count/ \
                                { printf "    %3d %-25s = %8d %s\n", $1, $2, $4, "[%]" }'
            ;;

        *) # unkown disk type
            ;;
    esac

    return 0
}

# --- Graphics

show_intel_gpu_data () { # show Intel GPU data

    check_intel_gpu || return 0

    # power managment data
    echo "+++ Intel Graphics"
    printparm_i915 $_intel_gpu_parm/powersave
    printparm_i915 $_intel_gpu_parm/enable_rc6 $_intel_gpu_parm/i915_enable_rc6
    printparm_i915 $_intel_gpu_parm/enable_dc
    printparm_i915 $_intel_gpu_parm/enable_fbc $_intel_gpu_parm/i915_enable_fbc
    printparm_i915 $_intel_gpu_parm/enable_psr "" 1
    printparm_i915 $_intel_gpu_parm/lvds_downclock
    printparm_i915 $_intel_gpu_parm/modeset
    printparm_i915 $_intel_gpu_parm/semaphores
    echo

    # frequency parameters
    if readable_sysf $_intel_gpu_drm/$IGPU_MIN_FREQ; then
        printparm "%-44s = ##%5d## [MHz]" $_intel_gpu_drm/$IGPU_MIN_FREQ
        printparm "%-44s = ##%5d## [MHz]" $_intel_gpu_drm/$IGPU_MAX_FREQ
        printparm "%-44s = ##%5d## [MHz]" $_intel_gpu_drm/$IGPU_BOOST_FREQ
        if readable_sysf $_intel_gpu_dbg/$IGPU_FREQ_TABLE; then
            # available frequencies
            printf "%s: " $_intel_gpu_dbg/$IGPU_FREQ_TABLE
            awk -F ' ' '{ if (NR >= 2) { printf "%d ", $1 } }; ' $_intel_gpu_dbg/$IGPU_FREQ_TABLE
            printf "[MHz] \n"
        fi
        echo
    fi

    return 0
}

# --- Battery Features

print_methods_per_driver () { # show features provided by a battery driver
    # $1: driver = natacpi, tpacpi, tpsmapi
    local bm m mlist=""

    for bm in _bm_read _bm_thresh _bm_dischg; do
        if [ "$(eval echo \$$bm)" = "$1" ]; then
            # method matches driver
            case $bm in
                _bm_read)   m="data" ;;
                _bm_thresh) m="thresholds" ;;
                _bm_dischg) m="discharge" ;;
            esac
            # concat method to output
            if [ -n "$mlist" ]; then
                mlist="$mlist, $m"
            else
                mlist="$m"
            fi
        fi
    done

    if [ -n "$mlist" ]; then
        printf "(%s)\n" "$mlist"
    else
        printf "(none)\n"
    fi

    return 0
}

print_batstate () { # print battery charging state with
    # an explanation when a threshold inhibits charging
    # $1: sysfile
    local sysf val

    # check if bat state sysfile exists
    if [ -f "$1" ]; then
        sysf=$1
    else
        # sysfile non-existent
        printf "%-59s = (not available)\n" "$1"
        return 0
    fi

    if val=$(read_sysf $sysf); then
        # sysfile was readable, output content
        printf "%-59s = %s" "$sysf" "$val"
        # Explain content if necessary
        case $val in
            "Unknown"|"Not charging") # a threshold forbids charging
                printf " (threshold effective)\n"
                ;;

            *) # Nothing to do
                printf "\n"
                ;;
        esac
    else
        # sysfile was not readable
        printf "%-59s = (not available)\n" "$sysf"
    fi

    return 0
}

print_thresholds () { # formatted output of ThinkPad charging thresholds
    # $1: BAT0/BAT1
    # global param: $_bm_thresh, $_bat_idx, $_bf_start, $_bf_stop
    local bsys sp thresh

    for sp in start stop; do
        get_threshold $sp; thresh=$?

        case $sp in
            start) bsys=$_bf_start ;;
            stop)  bsys=$_bf_stop ;;
        esac

        if [ $thresh -ne 255 ]; then
            # valid threshold read
            case $_bm_thresh in
                natacpi|tpsmapi)
                    printf "%-59s = %6d [%%]\n" "$bsys" "$thresh"
                    ;;

                tpacpi)
                    printf "%-59s = %6d [%%]\n" "tpacpi-bat.${1}.${sp}Threshold" "$thresh"
                    ;;

                none) ;; # nothing to show
            esac
        else
            # threshold read failed
            case $_bm_thresh in
                natacpi|tpsmapi)
                    printf "%-59s = (not available)\n" "$bsys"
                    ;;

                tpacpi)
                    printf "%-59s = (not available)\n" "tpacpi-bat.${1}.${sp}Threshold"
                    ;;

                none) ;; # nothing to show
            esac
        fi
    done # for sp

    return 0
}

print_discharge () { # formatted output of ThinkPad force_discharge
    # $1: BAT0/BAT1
    # global param: $_bm_dischg, $_bat_idx, $_bf_dischg
    local force

    get_force_discharge $1; force=$?

    if [ $force -lt 2 ]; then
        # valid force_discharge read
        case $_bm_dischg in
            natacpi|tpsmapi)
                printf "%-59s = %6d\n" "$_bf_dischg" "$force"
                ;;

            tpacpi)
                printf "%-59s = %6d\n" "tpacpi-bat.${1}.forceDischarge" "$force"
                ;;

            none) ;; # nothing to show
        esac
    else
        # force_discharge read failed
        case $_bm_dischg in
            natacpi|tpsmapi)
                printf "%-59s = (not available)\n" "$_bf_dischg"
                ;;

            tpacpi)
                printf "%-59s = %6d\n" "tpacpi-bat.${1}.forceDischarge" "(not available)"
                ;;

            none) ;; # nothing to show
        esac
    fi

    return 0
}

