#!/usr/bin/env tclsh
#
# i8kmon -- Monitor the temperature on Dell laptops.
#
# Copyright (C) 2013-2017  Vitor Augusto <vitorafsr@gmail.com>
# Copyright (C) 2001-2005  Massimo Dal Zotto <dz@debian.org>
#
# This program 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, 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.

array set config {
    sysconfig    /etc/i8kmon.conf
    userconfig   ~/.i8kmon
    i8kfan       /usr/bin/i8kfan
    acpi         "acpi"
    use_conf     1
    verbose      0
    timeout      2
    t_high       80
    num_configs  5
    0            {{0 0}  -1  60  -1  65}
    1            {{1 0}  50  70  55  75}
    2            {{1 1}  60  80  65  85}
    3            {{2 1}  65  85  70  90}
    4            {{2 2}  70 128  75 128}
}

array set status {
    leftspeed   {}
    rightspeed  {}
    nfans       2
    acpi_timer  0
    state       0
    temp        0
    lstate      -2
    rstate      -2
    ac          0
    t_low       0
    t_high      0
}

proc read_config {} {
    global config
    global status

    # read system config file
    if {[file exists $config(sysconfig)]} {
        source $config(sysconfig)
        if {$config(verbose) > 0} {
            puts "reading system config file"
        }
    }

    # read user config file
    # as it is read last, the options here have precedence
    if {$config(use_conf) != 0} {
        if {[file exists $config(userconfig)]} {
            source $config(userconfig)
            if {$config(verbose) > 0} {
                puts "reading user config file"
            }
        }
    }

    for {set key 0}  {$key < $config(num_configs)} {incr key} {
        set fans  [lindex $config($key) 0]
        set lo_ac [lindex $config($key) 1]
        set hi_ac [lindex $config($key) 2]
        set lo_bt [lindex $config($key) 3]
        set hi_bt [lindex $config($key) 4]

        # check that for each key hi temp > lo temp
        if {$lo_ac > $hi_ac} {set hi_ac [expr $lo_ac + 5]}
        if {$lo_bt > $hi_bt} {set hi_bt [expr $lo_bt + 5]}

        # set temperature to -1 to lo_ac and lo_bt at config(0)
        if {$key == 0} {
            set lo_ac -1
            set lo_bt -1
        }

        # set temperature to 128 to hi_ac and hi_bt at config(3)
        if {$key == [expr $config(num_configs)-1]} {
            set hi_ac 128
            set hi_bt 128
        }

        set config($key) [list $fans $lo_ac $hi_ac $lo_bt $hi_bt]
    }
}

proc status_timer {} {
    global config
    global status

    # Reschedule status timer
    after [expr $config(timeout)*1000] {status_timer}

    check_status
}

proc check_status {} {
    global config
    global status

    if {![read_i8k_status]} {return}

    fan_control
}

proc read_i8k_status {} {
    global config
    global status

    set status(temp) [eval exec "i8kctl temp"]

    set speed [eval exec "i8kctl speed"]
    set lspeed [lindex $speed 0]
    set rspeed [lindex $speed 1]

    # Calculate fan state from probed fans speeds.
    # It improve performance as it does not requires reading values all the time from the system,
    # since reading fan speeds is a slow call that freezes the computer in some Dell laptops models.
    set status(lstate) 0
    set status(rstate) 0
    set ldiff_speed "+inf"
    set rdiff_speed "+inf"

    # Search for nearest value
    for {set idx_speed 0}  {$idx_speed < 4} {incr idx_speed} {
        if {[expr abs($lspeed - [lindex $status(leftspeed) $idx_speed]) < $ldiff_speed]} {
            set ldiff_speed [expr abs($lspeed - [lindex $status(leftspeed) $idx_speed])]
            set status(lstate) $idx_speed
        }

        if {[expr abs($rspeed - [lindex $status(rightspeed) $idx_speed]) < $rdiff_speed]} {
            set rdiff_speed [expr abs($rspeed - [lindex $status(rightspeed) $idx_speed])]
            set status(rstate) $idx_speed
        }
    }

    # If AC status is not available read it from procfs
    set ac [eval exec "i8kctl ac"]
    if {$ac < 0} {
        read_ac_status
    }

    if {$config(verbose) > 0} {
        puts "temp, \[fan state\], \[fan speed\], ac state:  $status(temp) \[$status(lstate) $status(rstate)\] \[$lspeed $rspeed\] $status(ac)"
    }

    return 1
}

proc read_ac_status {} {
    global config
    global status

    # Read ac status once per minute
    if {[incr status(acpi_timer) -1] > 0} {
        return 1
    }

    set status(acpi_timer) [expr 60 / $config(timeout)]

    set acpi_ac [exec {*}$config(acpi)]
    if {[string match *on-line* $acpi_ac] || [string match *online* $acpi_ac]} {
        set status(ac) 1
    } else {
        set status(ac) 0
    }

    if {$config(verbose) > 0} {
        puts "[clock seconds] acpi: $acpi_ac"
    }

    return 0
}

proc fan_control {} {
    global config
    global status

    set index [expr $status(ac) ? 1 : 3]
    set state $status(state)
    set temp  $status(temp)

    set status(t_low)  [lindex $config($state) $index]
    set status(t_high) [lindex $config($state) [expr $index+1]]

    while {$temp < 128 && $temp >= $status(t_high)} {
        if {$config(verbose) > 0} {
            puts -nonewline "# ($temp>=$status(t_high)), "
        }
        incr state
        set status(t_low)  [lindex $config($state) $index]
        set status(t_high) [lindex $config($state) [expr $index+1]]
        if {$config(verbose) > 0} {
            puts "state=$state, low=$status(t_low), high=$status(t_high)"
        }
    }

    while {$temp > 0 && $temp <= $status(t_low)} {
        if {$config(verbose) > 0} {
            puts -nonewline "# ($temp<=$status(t_low)), "
        }
        incr state -1
        set status(t_low)  [lindex $config($state) $index]
        set status(t_high) [lindex $config($state) [expr $index+1]]
        if {$config(verbose) > 0} {
            puts "state=$state, low=$status(t_low), high=$status(t_high)"
        }
    }

    set_fan $state
}

proc set_fan {state} {
    global config
    global status

    set status(state) $state
    set args [lindex $config($state) 0]

    set left [lindex $args 0]
    set right [lindex $args 1]

    if {$status(lstate) == -1 || $left == $status(lstate)} {
        set left "-"
    } else {
        set status(lstate) $left
    }

    if {$status(rstate) == -1 || $right == $status(rstate)} {
        set right "-"
    } else {
        set status(rstate) $right
    }

    if {$status(nfans) < 2} { set right "-" }

    if {$left == "-" && $right == "-"} {return}

    if {$config(verbose) > 0} {
        puts "$config(i8kfan) $left $right"
    }

    exec $config(i8kfan) $left $right
}

proc usage {} {
    global argv0

    regsub -all {^.*/} $argv0 {} progname
    puts "Usage:  $progname \[<options>...]\n"
    puts "Options:\n"
    puts "   -nc|--nouserconfig       don\x27t read \$HOME/.i8kmon"
    puts "    -v|--verbose            report log to stdout"
    puts "    -t|--timeout <secs>     set poll timeout\n"
}

proc parse_options {} {
    global config
    global status
    global argv

    for {set i 0} {$i < [llength $argv]} {incr i} {
        set arg [lindex $argv $i]
        switch -- $arg {
            -\? - -h - -help - --help {
                usage
                exit
            }
            --nouserconfig - -nc {
                set config(use_conf) 0
            }
            --verbose - -v {
                set config(verbose) 1
            }
            --timeout - -t {
                set config(timeout) [lindex $argv [incr i]]
            }
            -- {
                continue
            }
            default {
                puts stderr "invalid option: $arg"
                exit 1
            }
        }
    }

    if {$config(verbose) > 0} {
        puts "i8kmon"
        parray config
        parray status
    }
}

proc probe_fan_speed {} {
    global config
    global status

    if {$status(leftspeed) != {} || $status(rightspeed) != {}} {return}

    exec $config(i8kfan) 1 1
    after 5000
    set speeds [eval exec "i8kctl speed"]
    set lspeed1 [lindex $speeds 0]
    set rspeed1 [lindex $speeds 1]

    exec $config(i8kfan) 2 2
    after 5000
    set speeds [eval exec "i8kctl speed"]
    set lspeed2 [lindex $speeds 0]
    set rspeed2 [lindex $speeds 1]

    exec $config(i8kfan) 3 3
    after 5000
    set speeds [eval exec "i8kctl speed"]
    set lspeed3 [lindex $speeds 0]
    set rspeed3 [lindex $speeds 1]

    set status(leftspeed) "0 $lspeed1 $lspeed2 $lspeed3"
    set status(rightspeed) "0 $rspeed1 $rspeed2 $rspeed3"

    if {$config(verbose) > 0} {
        puts "Probed fan speeds are left=$status(leftspeed), right=status(rightspeed)"
    }
}

# Probe external tools
proc probe_tools {} {
    # The possibility of choosing 'acpi' or 'acpitool' is for compatibility between different architectures: amd64, i386, kFreeBSD.
    # This code below is strictly related on package dependency stated at keyword 'Depends:' on file 'debian/control'.
    if {![catch {exec acpi}]} {
        set config(acpi) "acpi"
    } elseif {[catch {exec acpitool}]} {
        set config(acpi) "acpitool"
    } else {
        puts stderr "Package dependency problem: neither 'acpi' nor 'acpitool' package is installed."
    }
}

proc main {} {
    global status

    read_config
    probe_fan_speed
    probe_tools
    parse_options
    status_timer
}

if {$tcl_interactive == 0} {
    main
    vwait forever
}
