#!/bin/sh -efu
#
# Copyright (C) 2020  Paul Wolneykien <manowar@altlinux.org>
#
# This file is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
  
. shell-config

PROG="${0##*/}"
PROG_VERSION=0.1.1

FANFIX_CONFIG="${FANFIX_CONFIG:-/etc/sysconfig/fanfix.conf}"
HWMONDIR="/sys/devices/platform/nct6775.3712/hwmon/hwmon2"

show_help()
{
	cat <<EOF
Usage: $PROG [options] [FAN_NUMBER | T_INPUT ...]

$PROG is a tool to read and modify (fix) the fan driver parameters
on boot and after waking up (resume).

$PROG uses $FANFIX_CONFIG configuration file.

Main options:

  -d HWMONDIR, --dir=HWMONDIR    use specified hwmon sysfs directory;

  -f, --fix      modify the parameters of the specified or all fans
                 in accordance with the given options or the
                 configuration file $FANFIX_CONFIG;

  -r MODULES, --reload=MODULES    reload specified kernel modules
                                before applying changes.

Fan options:

  -m MODE, --mode=MODE    switch fan control to the specified mode;

  -p VAL, --pwm=VAL    set fan PWM value;

  -t TIN, --temp-source=TIN    select the temperature source for the
                             fan (either by index or by label);

  -w TIN, --weight-source=TIN    select the secondary (weight)
                               temperature source for the fan (either
                             by index or by label);

  -W WPROFILE, --weight-profile=WPROFILE    set weight profile;

  -P PROFILE, --profile=PROFILE    set Smart Fan IV profile;

  -n,--dry-run    just print out which modifications would be made.

Temperature options:

  -o OFFS, --offset=OFFS    set offset for temperature sensor T_INPUT.

Other options:

  -v,--verbose   print information about the actual changes;

  -V,--version        print program version and exit;
  -h,--help           show this text and exit.


With no -f|--fix given $PROG prints out the current configuration of
all or the specified fans.

Report bugs to http://bugs.altlinux.ru/

EOF
}

print_version()
{
	cat <<EOF
$PROG version $PROG_VERSION
Written by Paul Wolneykien <manowar@altlinux.org>

Copyright (C) 2020 Paul Wolneykien <manowar@altlinux.org>
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE.
EOF
}

show_usage()
{
	cat <<EOF
Usage: $PROG [options] [FAN_NUMBER]

Run $PROG -h to see the help page.
EOF
    exit 1
}

set_val()
{
    local file="$1"
    local val="$2"
    local comment="${3:-}"

    if [ $dry_run -ne 0 ]; then
        echo -n "Would set $file to $val"
        [ -z "$comment" ] || echo -n " ($comment)"
        echo
    else
        if [ $verbose -ne 0 ]; then
            echo -n "Set $file to $val" >&2
            [ -z "$comment" ] || echo -n " ($comment)" >&2
            echo >&2
        fi
        echo "$val" > "$file"
    fi
}

set_mode()
{
    local pwmnum="$1"
    local mode="$2"
    local modename="$3"

    set_val "$HWMONDIR/pwm${pwmnum}_enable" "$mode" "$modename"
}

percent_to_byte()
{
    echo $((($1 * 255) / 100))
}

byte_to_percent()
{
    echo $((($1 * 100) / 255))
}

set_temp_val()
{
    local file="$1"
    local tempval="$2"
    local comment="${3:-}"

    set_val "$file" "$((tempval * 1000))" "$comment"
}

set_pwm_val()
{
    local file="$1"
    local pwmval="$2"
    local comment="${3:-}"

    set_val "$file" "$(percent_to_byte "$pwmval")" "$comment"
}

fix_smart_fan()
{
    local pwmnum="$1"
    local profile="$2"

    if [ -z "$profile" ]; then
        echo "Please, specify or configure the profile for fan $pwmnum" >&2
        return 1
    fi

    if [ ! -d "$HWMONDIR" ]; then
        echo "HWMON directory not found: $HWMONDIR" >&2
        return 1
    fi

    local i=1
    local pwmpoint=
    local temppoint=
    for pval in $profile; do
        pwmpoint="$HWMONDIR/pwm${pwmnum}_auto_point${i}_pwm"
        temppoint="$HWMONDIR/pwm${pwmnum}_auto_point${i}_temp"
        if [ ! -e "$pwmpoint" ]; then
            echo "PWM point file not found: $pwmpoint" >&2
            return 2
        fi
        if [ ! -e "$temppoint" ]; then
            echo "Temperature point file not found: $temppoint" >&2
            return 2
        fi

        local tempval="${pval%:*}"
        set_temp_val "$temppoint" "$tempval"

        local pwmval="${pval#*:}"
        set_pwm_val "$pwmpoint" "$pwmval"
        i=$((i+1))
    done

    set_mode "$pwmnum" 5 "Smart Fan IV"
}

fix_manual_fan()
{
    local pwmnum="$1"
    local pwmval="$2"

    if [ -z "$pwmval" ]; then
        echo "Please, specify or configure the PWM value for fan $pwmnum" >&2
        return 1
    fi

    set_mode "$pwmnum" 1 "manual mode"

    local pwmfile="$HWMONDIR/pwm$pwmnum"
    set_pwm_val "$pwmfile" "$pwmval"
}

show_smart_fan()
{
    local pwmnum="$1"

    echo -n "Profile: "
    local i=1
    local pwmpoint=
    local temppoint=
    local pwmval=
    local tempval=
    while :; do
        pwmpoint="$HWMONDIR/pwm${pwmnum}_auto_point${i}_pwm"
        temppoint="$HWMONDIR/pwm${pwmnum}_auto_point${i}_temp"
        if [ -e "$pwmpoint" -a -e "$temppoint" ]; then
            tempval="$(cat "$temppoint")"
            tempval=$((tempval / 1000))
            pwmval="$(cat "$pwmpoint")"
            pwmval="$(byte_to_percent "$pwmval")"
            [ $i -eq 1 ] || echo -n ' '
            echo -n "$tempval:$pwmval"
        else
            echo
            break
        fi
        i=$((i+1))
    done
}

show_stub()
{
    local mode="${1:-$MODE}"
    echo "Sorry, mode $mode isn't supported yet"
}

show_fan_speed()
{
    local pwmnum="$1"

    local pwmfile="$HWMONDIR/pwm$pwmnum"
    local pwmval="$(cat "$pwmfile")"
    pwmval="$(byte_to_percent "$pwmval")"
    echo "PWM: ${pwmval}%"
}

show_temp_label()
{
    local temp_idx="$1"
    if [ "$temp_idx" -ne 0 ]; then
        cat "$HWMONDIR/temp${temp_idx}_label"
    else
        echo '-'
    fi
}

show_temp_sel()
{
    local pwmnum="$1"

    local temp_sel="$(cat "$HWMONDIR/pwm${pwmnum}_temp_sel")"
    echo "Main source: $(show_temp_label "$temp_sel")"

    local weight_temp_sel="$(cat "$HWMONDIR/pwm${pwmnum}_weight_temp_sel")"
    echo "Weight source: $(show_temp_label "$weight_temp_sel")"

    echo -n "Weight profile: $(($(cat "$HWMONDIR/pwm${pwmnum}_weight_temp_step_base")/1000))/$(($(cat "$HWMONDIR/pwm${pwmnum}_weight_temp_step")/1000))"
    local temp_step_tol="$(cat "$HWMONDIR/pwm${pwmnum}_weight_temp_step_tol")"
    [ "$temp_step_tol" -eq 0 ] || echo -n "+-$((temp_step_tol/1000))"
    echo ":$(byte_to_percent $(cat "$HWMONDIR/pwm${pwmnum}_weight_duty_base"))/$(byte_to_percent $(cat "$HWMONDIR/pwm${pwmnum}_weight_duty_step"))%"
}

show_fan()
{
    local pwmnum="$1"

    local modefile="$HWMONDIR/pwm${pwmnum}_enable"
    local mode="$(cat "$modefile")"

    show_fan_speed "$pwmnum"

    echo -n "Mode: "
    case "$mode" in
        0)
            echo "0 (control disabled)"
            ;;
        1)
            echo "1 (manual mode)"
            ;;
        2)
            echo "2 (Thermal Cruise mode)"
            show_stub "$mode"
            ;;
        3)
            echo "3 (Speed Cruise mode)"
            show_stub "$mode"
            ;;
        4)
            echo "4 (Smart Fan III)"
            show_stub "$mode"
            ;;
        5)
            echo "5 (Smart Fan IV)"
            show_smart_fan "$pwmnum"
            ;;
        *)
            echo "$mode (unknown mode)"
            ;;
    esac

    show_temp_sel "$pwmnum"
}

list_pwm_nums()
{
    local sep=
    find "$HWMONDIR" -name 'pwm[0-9]*_enable' | \
        while read f; do
            i="${f#*pwm}"; i="${i%_enable}"
            echo -n "$sep$i"
            sep=' '
        done
    echo
}

show_fans()
{
    local pwmnums="${1:-$(list_pwm_nums)}"

    if [ -z "$pwmnums" ]; then
        echo "No fans found in $HWMONDIR"
        return 0
    fi

    local first=1
    for i in $pwmnums; do
        [ -n "$first" ] || echo
        echo "FAN $i"
        show_fan "$i"
        first=
    done
}

read_config()
{
    local _HWMONDIR="$(shell_config_get "$FANFIX_CONFIG" 'HWMONDIR' '[[:space:]]*=[[:space:]]*')"
    HWMONDIR="${_HWMONDIR:-$HWMONDIR}"
    RELOAD_MODULES="$(shell_config_get "$FANFIX_CONFIG" 'RELOAD_MODULES' '[[:space:]]*=[[:space:]]*')"
}

read_mode()
{
    local pwmnum="$1"
    shell_config_get "$FANFIX_CONFIG" "MODE$pwmnum" '[[:space:]]*=[[:space:]]*'
}

read_pwm_value()
{
    local pwmnum="$1"
    shell_config_get "$FANFIX_CONFIG" "PWM$pwmnum" '[[:space:]]*=[[:space:]]*'
}

read_profile()
{
    local pwmnum="$1"
    shell_config_get "$FANFIX_CONFIG" "PROFILE$pwmnum" '[[:space:]]*=[[:space:]]*'
}

read_temp_sel()
{
    local pwmnum="$1"
    shell_config_get "$FANFIX_CONFIG" "TEMP_SOURCE$pwmnum" '[[:space:]]*=[[:space:]]*'
}

read_weight_sel()
{
    local pwmnum="$1"
    shell_config_get "$FANFIX_CONFIG" "WEIGHT_TEMP_SOURCE$pwmnum" '[[:space:]]*=[[:space:]]*'
}

read_weight_profile()
{
    local pwmnum="$1"
    shell_config_get "$FANFIX_CONFIG" "WEIGHT_PROFILE$pwmnum" '[[:space:]]*=[[:space:]]*'
}

set_temp_sel()
{
    local pwmnum="$1"
    local tempsuf="$2"
    local sel="$3"

    local tindex=
    case "$sel" in
        [0-9]|[0-9][0-9])
            tindex="$sel"
            ;;
        *)
            tindex="$(set +f; grep -lxF "$sel" "$HWMONDIR/"temp*_label | head -1)" ||:
            if [ -n "$tindex" ]; then
                tindex="${tindex##*/temp}"
                tindex="${tindex%_label}"
            fi
            ;;
    esac

    if [ -z "$tindex" ]; then
        echo "Temperature source $sel not found" >&2
        return 1
    fi

    local name=
    [ "$tindex" -eq 0 ] || \
        name="$(cat "$HWMONDIR/temp${tindex}_label")"
    local tempfile="$HWMONDIR/pwm${pwmnum}_$tempsuf"
    set_val "$tempfile" "$tindex" "$name"
}

set_main_temp_sel()
{
    set_temp_sel "$1" 'temp_sel' "$2"
}

set_weight_temp_sel()
{
    set_temp_sel "$1" 'weight_temp_sel' "$2"
}

fix_weight()
{
    local pwmnum="$1"
    local weight_sel="$2"
    local weight_profile="$3"

    set_weight_temp_sel "$pwmnum" "$weight_sel"

    local temp_base=0
    local temp_step=0
    local temp_step_tol=0
    local duty_base=0
    local duty_step=0
    if [ -n "$weight_profile" ]; then
        temp_base="${weight_profile%%:*}"
        temp_step="${temp_base#*/}"
        case "$temp_step" in
            *-+*)
                temp_step_tol="${temp_step#*-+}"
                temp_step="${temp_step%-+*}"
                ;;
            *+-*)
                temp_step_tol="${temp_step#*+-}"
                temp_step="${temp_step%+-*}"
                ;;
            *[+-]*)
                temp_step_tol="${temp_step#*[+-]}"
                temp_step="${temp_step%[+-]*}"
                ;;
            *)
                temp_step_tol=0
                ;;
        esac
        temp_base="${temp_base%/*}"
        duty_base="${weight_profile#*:}"
        duty_step="${duty_base#*/}"
        duty_base="${duty_base%/*}"
    fi

    set_temp_val "$HWMONDIR/pwm${pwmnum}_weight_temp_step_base" \
                 "$temp_base"
    set_temp_val "$HWMONDIR/pwm${pwmnum}_weight_temp_step" \
                 "$temp_step"
    set_temp_val "$HWMONDIR/pwm${pwmnum}_weight_temp_step_tol" \
                 "$temp_step_tol"
    set_pwm_val "$HWMONDIR/pwm${pwmnum}_weight_duty_base" \
                "$duty_base"
    set_pwm_val "$HWMONDIR/pwm${pwmnum}_weight_duty_step" \
                "$duty_step"
}

fix_fan()
{
    local pwmnum="$1"
    local mode="${2:-${MODE:-$(read_mode "$pwmnum")}}"

    local tempsel="${TEMP_SEL:-$(read_temp_sel "$pwmnum")}"
    if [ -n "$tempsel" ]; then
        set_main_temp_sel "$pwmnum" "$tempsel"
    fi

    if [ -n "$mode" ]; then
        case "$mode" in
            0)
                set_mode "$pwmnum" 0 "control disabled"
                ;;
            1)
                fix_manual_fan "$pwmnum" "${PWM_VAL:-$(read_pwm_value "$pwmnum")}"
                ;;
            5)
                fix_smart_fan "$pwmnum" "${PROFILE:-$(read_profile "$pwmnum")}"
                ;;
            *)
                show_stub "$mode"
                return 3
                ;;
        esac
    fi

    local weight_sel="${WEIGHT_SEL:-$(read_weight_sel "$pwmnum")}"
    local weight_profile="${WEIGHT_PROFILE:-$(read_weight_profile "$pwmnum")}"
    if [ -n "$weight_sel" ]; then
        fix_weight "$pwmnum" "$weight_sel" "$weight_profile"
    fi
}

fix_fans()
{
    local pwmnums="${1:-$(list_pwm_nums)}"

    if [ -z "$pwmnums" ]; then
        echo "No fans found in $HWMONDIR" >&2
        return 0
    fi

    for i in $pwmnums; do
        fix_fan "$i"
    done
}

reload_modules()
{
    local modules="${1:-$RELOAD_MODULES}"

    if [ -n "$modules" ]; then
        for m in $modules; do
            if [ $dry_run -ne 0 ]; then
                echo "Would remove module $m from the kernel" >&2
            else
                if [ $verbose -ne 0 ]; then
                    echo "Removing $m from the kernel..." >&2
                fi
                /sbin/modprobe -r "$m"
            fi
            if [ $dry_run -ne 0 ]; then
                echo "Would insert module $m into the kernel" >&2
            else
                if [ $verbose -ne 0 ]; then
                    echo "Inserting $m into the kernel..." >&2
                fi
                /sbin/modprobe "$m"
            fi
        done
    fi
}

list_tins()
{
    local sep=
    find "$HWMONDIR" -name 'temp[0-9]*_input' | \
        while read f; do
            i="${f#*temp}"; i="${i%_input}"
            echo -n "$sep$i"
            sep=' '
        done
    echo
}

set_offset()
{
    local tins="$1"
    local offs="${2:-${OFFSET:-}}"

    if [ -n "$offs" ]; then
        for tin in $tins; do
            set_temp_val "$HWMONDIR/temp${tin}_offset" "$offs"
        done
    fi
}

fix_offsets()
{
    local offs=
    local label=
    for tin in $(list_tins); do
        offs="$(shell_config_get "$FANFIX_CONFIG" "TEMP${tin}_OFFSET" '[[:space:]]*=[[:space:]]*')"
        [ -z "$offs" ] || set_offset "$tin" "$offs"
        label="$(cat "$HWMONDIR/temp${tin}_label")"
        offs="$(shell_config_get "$FANFIX_CONFIG" "${label}_OFFSET" '[[:space:]]*=[[:space:]]*')"
        [ -z "$offs" ] || set_offset "$tin" "$offs"
    done
}

show_offs()
{
    local tins="${1:-$(list_tins)}"

    if [ -z "$tins" ]; then
        echo "No temperature inputs found in $HWMONDIR"
        return 0
    fi

    local first=1
    local offs=
    for i in $tins; do
        offs="$([ ! -e "$HWMONDIR/temp${i}_offset" ] || cat "$HWMONDIR/temp${i}_offset")"
        if [ "${offs:-0}" -ne 0 ]; then
            [ -z "$first" ] || echo
            echo "$(cat "$HWMONDIR/temp${i}_label") offset: $offs"
            first=
        fi
    done
}

OPTS=`getopt -n $PROG -o d:,f,m:,p:t:,w:,W:,P:,n,r:,o:,v,V,h \
             -l dir:,fix,mode:,pwm:,temp-source:,weight-source:,weight-profile:,profile:,dry-run,reload:,offset:,verbose,version,help -- "$@"` || show_usage
eval set -- "$OPTS"

dry_run=0
verbose=0
run_fix=0

MODE=
PWM_VAL=
TEMP_SEL=
WEIGHT_SEL=
WEIGHT_PROFILE=
PROFILE=
RELOAD_MODULES=
OFFSET=

read_config

manual=

while :; do
	case "$1" in
        -d|--dir)
            shift
            HWMONDIR="$1"
            ;;
        -f|--fix)
            run_fix=1
            ;;
        -m|--mode)
            shift
            MODE="$1"
            manual=1
            ;;
        -p|--pwm)
            shift
            PWM_VAL="$1"
            manual=1
            ;;
        -t|--temp-source)
            shift
            TEMP_SEL="$1"
            manual=1
            ;;
        -w|--weight-source)
            shift
            WEIGHT_SEL="$1"
            manual=1
            ;;
        -W|--weight-profile)
            shift
            WEIGHT_PROFILE="$1"
            manual=1
            ;;
        -P|--profile)
            shift
            PROFILE="$1"
            manual=1
            ;;
        -n|--dry-run)
            dry_run=1
            ;;
        -r|--reload)
            shift
            RELOAD_MODULES="$1"
            ;;
        -o|--offset)
            shift
            OFFSET="$1"
            ;;
        -v|--verbose)
            verbose=1
            ;;
		-V|--version)
            print_version
            exit 0
            ;;
		-h|--help)
            show_help
            exit 0
            ;;
        --)
            shift
            break
            ;;
		*)
            echo "Unrecognized option: $1" >&2
            exit 1
            ;;
	esac
	shift
done

if [ $run_fix -ne 0 ]; then
    if [ -n "$RELOAD_MODULES" ]; then
        reload_modules "$RELOAD_MODULES"
    fi
    if [ -n "$manual" ]; then
        if [ -n "$OFFSET" ]; then
            echo "Can't mix fan and temperature parameters" >&2
            exit 3
        fi
        if [ $# -gt 0 ]; then
            fix_fans "$*"
        else
            echo "In order to set parameters on the command line to multiple fans, please, specify their numbers explicitly" >&2
            exit 3
        fi
    elif [ -n "$OFFSET" ]; then
        if [ $# -gt 0 ]; then
            set_offset "$*"
        else
            echo "Please, specify the temperature input number" >&2
            exit 3
        fi
    else
        fix_offsets
        fix_fans
    fi
else
    show_fans "$*"
    show_offs "$*"
fi
