#!/usr/bin/perl -w
use strict;
use POSIX qw(strftime);

use Getopt::Long;
use POSIX qw(setsid);
use Sys::Syslog qw(:DEFAULT setlogsock);

sub logger($;@);
sub signalhandler();

my $APMISER_VERSION = 4.14;

my $HELP = 0;
my $VERBOSE = 0;
my $VERSION = 0;
my $SYSLOG = 0;
my $DAEMON = 0;
my $LOW_PERCENT = 35;
my $HIGH_PERCENT = 97;
my $TRENDINESS = 2.2;
my $TERM = 0.5;
my $JIFFIES = -1;

my $dlp = $LOW_PERCENT;
my $dhp = $HIGH_PERCENT;
my $dst = $TRENDINESS;
my $dlt = $TERM;

my $result = GetOptions(
	'daemon'           => \$DAEMON,
	'help'             => \$HELP,
	'syslog'           => \$SYSLOG,
	'verbose'          => \$VERBOSE,
	'version'          => \$VERSION,
	'low_threshold=f'  => \$LOW_PERCENT,
	'high_threshold=f' => \$HIGH_PERCENT,
	'term=f'           => \$TERM,
	'trendiness=f'     => \$TRENDINESS,
	'interval=i'       => \$JIFFIES
);

if ($HELP  || !$result) {
	print <<EOF;
Usage: apmiser [OPTIONS]
    --daemon	Become a daemon
    --help	Display usage description
    --syslog	Send messages to syslog.
    --verbose	Report power expenditure changes.
    --version	Report program's version number.
    --high_threshold PERCENT
		Set the CPU usage level above which to run at full speed
		Range: low_threshold .. 100.   Default: $dhp
    --low_threshold PERCENT
		Set the CPU usage level below which to start saving power
		Range: 0 .. high_threshold.   Default: $dlp
    --term SECONDS
                Set how many seconds later the CPU usage should still matter
		Range: 0.15 .. 5.   Default: $dlt
    --trendiness FACTOR
		Set how strongly recent CPU usage should be favored
		Range: 1 .. 10.   Default: $dst
EOF

	if ($result) {
		exit(0);
	} else {
		exit(1);
	}
}

if ($VERSION) {
	printf STDOUT "apmiser version $APMISER_VERSION\n";
	exit(0);
}

if ($LOW_PERCENT < 0 ) {
	printf STDERR "apmiser: Error: low_threshold < 0\n";
	exit(1);
}

if ($HIGH_PERCENT > 100 ) {
	printf STDERR "apmiser: Error: high_threshold > 100\n";
	exit(1);
}

if ($LOW_PERCENT > $HIGH_PERCENT ) {
	printf STDERR "apmiser: Error: low_threshold > high_threshold\n";
	exit(1);
}

if ($TRENDINESS < 1 || $TRENDINESS > 10) {
	printf STDERR "apmiser: Error: trendiness must be between 1 and 10\n";
	exit(1);
}

if ($JIFFIES > 0 && $TERM == $dlt ) {
	$TERM = $JIFFIES / 1000.;
}

if ($TERM < 0.15 || $TERM > 5) {
	printf STDERR "apmiser: Error: term must be between 0.15 and 5 seconds\n";
	exit(1);
}


if ($DAEMON) {
	# force syslogging on
	$SYSLOG = 1;
	
	# fork and exit to detach from parent
	fork && exit;

	# set up new process group and session
	setsid();

	fork && exit;

	# avoid blocking file systems
	chdir q(/);

	# well-defined file descriptors
	close STDIN;
	close STDOUT;
	close STDERR;
	open STDIN, "</dev/null";
	open STDOUT, ">/dev/null";
	open STDERR, ">/dev/null";

	open(PIDFILE,">/var/run/apmiser.pid");
	printf PIDFILE "$$\n";
	close(PIDFILE);
}

if ($SYSLOG) {
	setlogsock q(unix);
	openlog q(apmiser), q(nowait,pid), q(daemon);
}

logger "Automatic power miser $APMISER_VERSION started (daemon: %s, low: %g%%, high: %g%%, term: %gs, trendiness: %g)",
	($DAEMON ? q(yes) : q(no)), $LOW_PERCENT, $HIGH_PERCENT, $TERM, $TRENDINESS;

my $sample_ms = int( $TERM * 10 + .5 ) * 10;
if ($sample_ms < 50) { $sample_ms = 50; }
my $window = int( 1000 * $TERM / $sample_ms + .5 );
if ($window > 10) { $window = 10; }
#print "sample_ms: $sample_ms  window: $window  " . $sample_ms * $window . "\n";
my $asymptote = $TRENDINESS * $TRENDINESS;
my $sample_s = $sample_ms / 1000.;
my $tdelay = $sample_ms * $window;
my $tjiffies = $tdelay / 10.;
my $min_idle = $tjiffies * (1 - $HIGH_PERCENT/100.);
my $max_idle = $tjiffies * (1 - $LOW_PERCENT/100.);
my $bad_value = $max_idle * 1.5 / $window;

my @idle_t;
my @idle_coeff;

my $coeff_total = 1 + $asymptote;
$idle_t[$window-1] = 0;
$idle_coeff[$window-1] = 1;

for( my $i = $window-2; $i >= 0; $i-- ) {
	$idle_coeff[$i] = $TRENDINESS * $idle_coeff[$i + 1];
	$coeff_total += $idle_coeff[$i] + $asymptote;
	$idle_t[$i] = 0;
}

my $fact = ($#idle_t + 1) / $coeff_total;
for( my $i = 0; $i <= $#idle_coeff; $i++ ) {
	$idle_coeff[$i] = ($idle_coeff[$i] + $asymptote) * $fact;
}

if ($VERBOSE) {
	printf "coeff: ";
	foreach my $t (@idle_coeff) {
		printf "%6.3f ", $t;
	}
	print "\n";
}

my ($user,$nice,$sys,$idle);
my ($apmstate,$charge);
my $chargenumeral;
my $idle_total;
my $baselineidle = 0;
my $good_fullpower;

my ($batterypower,$fullpower);
my $fullpower_cnt = 0;
my $fullpower_bad_cnt = 0;

# Install Signal Handlers
$SIG{QUIT} = $SIG{HUP} = $SIG{PIPE} = $SIG{INT} = $SIG{USR1} = $SIG{USR2} = \&signalhandler;

# Set initial power expenditure modes
system("tpctl --quiet --pma=high > /dev/null");
system("tpctl --quiet --pmb=high > /dev/null");
$fullpower = 1;

open(STAT,"/proc/stat") or die $!;
open(APM,"/proc/apm") or die $!;

while (1) {
	# Get CPU status information
	seek(STAT, 0, 0) or die "can't rewind /proc/stat : $!";
	$_ = <STAT>;

	# Parse the CPU status information
	($user,$nice,$sys,$idle) = /^cpu\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/;

	# Get power status information
	seek(APM, 0, 0) or die "can't rewind /proc/apm : $!";
	$_ = <APM>;

	# Parse the power status information
	($apmstate,$charge) = /^\S+\s+\S+\s+\S+\s+(\S+)\s+\S+\s+\S+\s+(\S+)/;

	if ($apmstate eq '0x00') {
		$batterypower = 1;
	} else {
		$batterypower = 0;
	}

	# Calculate number of jiffies (well, not quite) we've been idling since last iteration.
	@idle_t[1 .. $#idle_t] = @idle_t[0 .. $#idle_t - 1];
	if ($baselineidle) {
		$idle_t[0] = $idle - $baselineidle;
	}
	$idle_total = 0;
	for( my $i = 0; $i <= $#idle_t; $i++ ) {
		$idle_total += $idle_coeff[$i] * $idle_t[$i];
	}

	if ($baselineidle) { 
		# This isn't the first iteration

		if ($fullpower) {
			$fullpower_cnt++;
			if ($idle_t[0] + .5 * $idle_t[1] >= $bad_value) {
				$fullpower_bad_cnt++;
			}
		}

		if ($idle_total <= $min_idle) { # We're using the CPU intensely
			if (!$fullpower) {	# But we're not at full power
				logger "Now going at full speed %5.1f%% CPU usage  @idle_t",
					100 * (1 - $idle_total / $tjiffies)  if $VERBOSE;
				if ($batterypower) {
					system("tpctl --quiet --pmb=high > /dev/null");
				} else {
					system("tpctl --quiet --pma=high > /dev/null");
				}
				$fullpower = 1;
			}

		} elsif ( $idle_total >= $max_idle ) { # We're not using the CPU intensely
			if ($fullpower) {		# But we're at full power
				$good_fullpower = ($fullpower_cnt - $fullpower_bad_cnt) * $sample_s;
				logger "Now saving power %5.1f%% CPU usage  @idle_t",
					100 * (1 - $idle_total / $tjiffies) if $VERBOSE;
				#logger "  %5.2fs - %.2fs = %.2fs%s",
				#	$fullpower_cnt * $sample_s, $fullpower_bad_cnt * $sample_s,
				#	$good_fullpower, ($good_fullpower <= 0.10 ? " *" : "") if $VERBOSE;
				if ($batterypower) {
					system("tpctl --quiet --pmb=auto > /dev/null");
				} else {
					system("tpctl --quiet --pma=auto > /dev/null");
				}
				$fullpower = 0;
				$fullpower_cnt = 0;
				$fullpower_bad_cnt = 0;
			}
		}
	}

	$baselineidle = $idle;

	select(undef, undef, undef, $sample_s);
}

sub signalhandler () {
	# It seems a signal was sent
	# Restore power expenditure modes to defaults
	logger "Signal caught. Restoring default power expenditure modes and exiting.";
	system("tpctl --quiet --pma=high > /dev/null");
	system("tpctl --quiet --pmb=auto > /dev/null");
	if ($DAEMON) {
		unlink '/var/run/apmiser.pid';
	}
	exit(0);
}

sub logger ($;@) {
	my $format = shift;
	my $now_string = strftime "apmiser: %H:%M:%S  ", localtime();
	if ($SYSLOG) {
		syslog q(info), sprintf $format, @_;
	}
	print $now_string;
	print sprintf $format, @_;
	print "\n";
}
