#!/usr/bin/perl

#
# $Id$
#


use strict;
use locale;
use POSIX qw( locale_h );
use Getopt::Std;
use GD;
use USBLCD;
use XML::Simple qw( :strict );
use Time::HiRes qw( gettimeofday );
use App::Daemon qw( daemonize );

use vars qw/ %opt /;



###
### Globals

use constant MODULES_DIR		=> ".";

use constant UPDATE_PERIOD_MS 		=> 1000;

use constant M_FOCUS_ACCEPT		=> 1;
use constant M_FOCUS_DISCARD		=> 2;

use constant M_EVENT_DISCARD		=> 0;
use constant M_EVENT_PROCESS		=> 1;
use constant M_EVENT_UNFOCUS		=> 3;

use constant SELECT_LIST_SCROLL_SIZE	=> 5;



my %G_USBLCD_ROTATE_DICT = (
    '0'		=> USBLCD_ROTATE0,
    '90'	=> USBLCD_ROTATE90,
    '180'	=> USBLCD_ROTATE180,
    '270'	=> USBLCD_ROTATE270
);


my $G_VERBOSE = undef;
my $CONT = 1;
my $G_USBLCD_MODE_REF;
my $G_USBLCD_LEDS_REF;
my $G_USBLCD_KEYS_REF;
my $G_USBLCD_ROTATE;

my $G_CONFIG_REF = undef;
my $G_DEFAULT_UPDATE = &UPDATE_PERIOD_MS;
my @G_MODULES_ORDER;

my $G_MODULES_DIR = &MODULES_DIR;
my %G_MODULES;
my @G_MODULES_GRAPH_LIST;
my @G_MODULES_ALARM_LIST;
my @G_MODULES_EVENT_LIST;

my $G_MODULE_INDEX;
my $G_MODULE_NAME;
my $G_MODULE_FOCUS;
my $G_MODULE_UPDATE;
my($G_SECONDS, $G_MICROSECONDS);



###
### Common code

sub _dump;
sub _dump($;) {
    my $ref = shift;
    my $level = shift || 0;

    if(ref $ref eq "HASH") {
	print "HASH\n";
	foreach my $key (sort keys %{$ref}) {
	    for(my $i = 0; $i < $level; $i++) {
		print "    ";
	    }
	    print "$key -> ";
	    _dump($ref->{$key}, $level+1);
	}
    }
    elsif(ref $ref eq "ARRAY") {
	print "ARRAY\n";
	foreach my $item (@{$ref}) {
	    for(my $i = 0; $i < $level; $i++) {
		print "    ";
	    }
	    _dump($item, $level + 1);
	}
	return;
    }
    else {
	print "%$ref%\n";
	return;
    }
}

sub chart($$$$$$) {
    my $w = shift;
    my $h = shift;
    my $data_ref = shift;
    my $xi = shift;
    my $x_step = shift;
    my $max_value = shift;

    my $data_length = $w - 2;
    my $max = $h - 2;

    my $im = new GD::Image($w, $h);
    my $white = $im->colorAllocate(255, 255, 255);
    my $black = $im->colorAllocate(0, 0, 0);

    $im->rectangle(0, 0, $w - 1, $h - 1, $black);

    $im->setStyle($black, $white, $white);
    $im->line(0, $h * 1 / 4,  $w, $h * 1 / 4, gdStyled);
    $im->setStyle($black, $white, $white);
    $im->line(0, $h * 2 / 4,  $w, $h * 2 / 4, gdStyled);
    $im->setStyle($black, $white, $white);
    $im->line(0, $h * 3 / 4,  $w, $h * 3 / 4, gdStyled);
    for(my $i = $w - 2 - $xi; $i > 0; $i -= $x_step) {
	$im->line($i, 0 + 1, $i, $h - 2, gdStyled);
    }

    for(my $i = 0; $i < $data_length; $i++) {
	my $d = $data_ref->[$i];

	if(defined $d) {
	    my $d_prev = ($i > 0 and defined $data_ref->[$i - 1]) ?
		$data_ref->[$i - 1] : 0;

	    my $y = $max - int($d * $max / $max_value) + 1;
	    my $y_prev = $max - int($d_prev * $max / $max_value) + 1;

	    if($i == 0) {
		$y_prev = $y;
	    }
	    elsif($y_prev < $y and $y_prev < $max) {
		$y_prev++;
	    }
	    elsif($y_prev > $y and $y_prev > 0) {
		$y_prev--;
	    }

	    $im->line($i + 1, $y, $i + 1, $y_prev, $black);
	}
    }

    return $im;
}


# add value to chart data
sub chart_add($$$) {
    my $value = shift;
    my $chart_data_ref = shift;
    my $data_length = shift;
    my $max = 0;
    my $i;

    if(defined $value) {
	for($i = 1; $i < $data_length; $i++) {
	    $chart_data_ref->[$i - 1] = $chart_data_ref->[$i];
	    $max = $chart_data_ref->[$i - 1] if $max < $chart_data_ref->[$i - 1];
	}
	$chart_data_ref->[$i - 1] = $value;
	$max = $chart_data_ref->[$i - 1] if $max < $chart_data_ref->[$i - 1];
    }

    return $max;
}


# draw select list input
sub draw_select_list($$$;$$) {
    my $w = shift;
    my $h = shift;
    my $list_ref = shift;
    my $start = shift || 0;
    my $select = shift;

    $start = scalar @{$list_ref} - 1 if $start >= scalar @{$list_ref};

    my $im = new GD::Image($w, $h);
    my $white = $im->colorAllocate(255, 255, 255);
    my $black = $im->colorAllocate(0, 0, 0);

    my $select_width = $im->width - 1;
    my $window_size = $h / gdTinyFont->height;
    if ($window_size < scalar @{$list_ref}) {
	my $logical_size = scalar @{$list_ref} - $window_size;
	my $physical_size = $im->height - &SELECT_LIST_SCROLL_SIZE;
	my $y = ($logical_size > $start) ? $physical_size * $start / $logical_size : $physical_size;
	$im->line($im->width - &SELECT_LIST_SCROLL_SIZE / 2, 0,
	    $im->width - &SELECT_LIST_SCROLL_SIZE / 2, $im->height, $black);
	$im->filledEllipse($im->width - &SELECT_LIST_SCROLL_SIZE / 2, $y + &SELECT_LIST_SCROLL_SIZE / 2,
	    &SELECT_LIST_SCROLL_SIZE - 1, &SELECT_LIST_SCROLL_SIZE - 1, $black);
	$select_width -= &SELECT_LIST_SCROLL_SIZE;
    }

    my $y = 0;
    for (my $i = $start; $i < scalar @{$list_ref}; $i++) {
	my $element = $list_ref->[$i];
	if ($i == $select) {
	    $im->filledRectangle(0, $y, $select_width, $y + gdTinyFont->height, $black);
	    $im->string(gdTinyFont, 2, $y, $element, $white);
	}
	else {
	    $im->string(gdTinyFont, 2, $y, $element, $black);
	}
	$y += gdTinyFont->height;
	last if $y >= $h;
    }

    return $im;
}



###
### Helpers

# load supported modules
sub modules_load($$) {
    my $rotate = shift;
    my $mode_ref = shift;
    my %modules;

    my %modules_order_index;
    for (my $i = 0; $i < scalar @G_MODULES_ORDER; $i++) {
	$modules_order_index{$G_MODULES_ORDER[$i]} = $i;
    }
    my $out_of_order_idx = 1000;

    my($screen_width, $screen_height);
    if ($rotate == USBLCD_ROTATE0 or $rotate == USBLCD_ROTATE180) {
	($screen_width, $screen_height) = ($mode_ref->{'width'}, $mode_ref->{'height'});
    } else {
	($screen_width, $screen_height) = ($mode_ref->{'height'}, $mode_ref->{'width'});
    }

    eval "use lib \"".$G_MODULES_DIR."\";";

    opendir DIR, $G_MODULES_DIR or die "Can't open modules directory \"".$G_MODULES_DIR."\": $!\n";

    while (my $filename = readdir(DIR)) {
	next unless -f $G_MODULES_DIR."/$filename" && $filename =~ /^mod_.*\.pm$/;
	my ($module_name) = ($filename =~ /^mod_(.*)\.pm$/);
	eval 'use mod_'.$module_name.';';
	my %init_params = (
	    'verbose' => $G_VERBOSE,
	    'width' => $screen_width,
	    'height' => $screen_height
	);

	if (exists $G_CONFIG_REF->{'modules'}->{$module_name}) {
	    my $module_param_ref = $G_CONFIG_REF->{'modules'}->{$module_name};
	    map { $init_params{$_} = $module_param_ref->{$_} } keys %{$module_param_ref};
	}

	my %module_info;
	eval '%module_info = mod_'.$module_name.'::init(%init_params);';

	$modules{$module_name} = {} unless exists $modules{$module_name};
	$modules{$module_name}->{'order'} = exists $modules_order_index{$module_name}
	    ? $modules_order_index{$module_name}
	    : $out_of_order_idx++;
	$modules{$module_name}->{'graph'} = exists $module_info{'graph'}
	    ? $module_info{'graph'} : undef;
	$modules{$module_name}->{'event'} = exists $module_info{'event'}
	    ? $module_info{'event'} : undef;
	$modules{$module_name}->{'update'} = (exists $module_info{'update'} and $module_info{'update'} =~ /\d+/)
	    ? $module_info{'update'} : &UPDATE_PERIOD_MS;
    }

    close DIR;

    return %modules;
}


# switch to module accordinf index in $G_MODULE_INDEX
sub module_switch($) {
    my $usblcd = shift;

    eval 'mod_'.$G_MODULE_NAME.'::leave($usblcd);' if defined $G_MODULE_NAME;

    $G_MODULE_NAME = $G_MODULES_GRAPH_LIST[$G_MODULE_INDEX];
    $G_MODULE_UPDATE = $G_MODULES{$G_MODULE_NAME}->{'update'};
    ($G_SECONDS, $G_MICROSECONDS) = gettimeofday();
    $G_MODULE_FOCUS = 0;
    eval 'mod_'.$G_MODULE_NAME.'::enter($usblcd);';
}


# send event to all 'event' modules
sub module_send_event($$) {
    my $usblcd = shift;
    my $event_ref = shift;

    print "send unprocessed event: type $event_ref->{'type'}, value $event_ref->{'value'}\n" if $G_VERBOSE;
    foreach my $module_name (@G_MODULES_EVENT_LIST) {
	print "    to module $module_name\n" if $G_VERBOSE;
	my $ret;
	eval '$ret = mod_'.$module_name.'::event($usblcd, $event_ref);';
    }
}


# show usage message
sub usage() {
    print "usage: wrtmond.pl [-v] [-c <config file>] -d\n";
    print "       wrtmond.pl -h\n";
    exit(255);
}



### Entry point
###

setlocale(LC_ALL, "C");

getopts("c:hvd", \%opt ) or usage();
usage() if $opt{'h'};
$G_VERBOSE = 1 if defined $opt{'v'};

# get configuration
$G_CONFIG_REF = XMLin($opt{'c'}, ForceArray => 0, KeyAttr => []) if $opt{'c'} and -f $opt{'c'};

$G_DEFAULT_UPDATE = $G_CONFIG_REF->{'main'}->{'default_update'} if $G_CONFIG_REF->{'main'}->{'default_update'} > 0;
$G_MODULES_DIR = $G_CONFIG_REF->{'main'}->{'modules_dir'} || &MODULES_DIR;
@G_MODULES_ORDER = split /[ ,;]/, $G_CONFIG_REF->{'main'}->{'modules_order'};
$G_USBLCD_ROTATE = $G_USBLCD_ROTATE_DICT{$G_CONFIG_REF->{'main'}->{'rotate'}} || USBLCD_ROTATE0;

# daemonize
daemonize() if $opt{'d'};

# get USBLCS device
my $usblcd = new USBLCD;
die "Can't claim the USB LCD device.\n" unless defined $usblcd;

$G_USBLCD_MODE_REF = $usblcd->getmode();
$G_USBLCD_LEDS_REF = $usblcd->getleds();
$G_USBLCD_KEYS_REF = $usblcd->getkeys();

# clear screen
$usblcd->fillrect(0, 0, $G_USBLCD_MODE_REF->{'width'}, $G_USBLCD_MODE_REF->{'height'}, 0);

# set screen orientation
$usblcd->setrotate($G_USBLCD_ROTATE); # FixMe: get rotate from configuration

# loading and initialization  modules
%G_MODULES = modules_load($G_USBLCD_ROTATE, $G_USBLCD_MODE_REF);
@G_MODULES_GRAPH_LIST = ();
map { push @G_MODULES_GRAPH_LIST, $_ if $G_MODULES{$_}->{'graph'} } sort { $G_MODULES{$a}->{'order'} <=> $G_MODULES{$b}->{'order'} } keys %G_MODULES;
@G_MODULES_ALARM_LIST = ();
map { push @G_MODULES_ALARM_LIST, $_ if $G_MODULES{$_}->{'alarm'} } keys %G_MODULES;
@G_MODULES_EVENT_LIST = ();
map { push @G_MODULES_EVENT_LIST, $_ if $G_MODULES{$_}->{'event'} } keys %G_MODULES;

# init module
$G_MODULE_NAME = undef;
$G_MODULE_INDEX = 0;
module_switch($usblcd);

# turn on display
$usblcd->setpower(USBLCD_POWER_ON);

# set signal handler
$SIG{'INT'} = sub(){ $CONT = undef ;};

# main loop
while ($CONT) {
    my $poll_time;

    for (;;) {
	my ($seconds, $microseconds) = gettimeofday();
	my $period = (($seconds - $G_SECONDS) * 1000 + ($microseconds - $G_MICROSECONDS) / 1000);
	$poll_time = int($G_MODULE_UPDATE - $period);
	last if $poll_time <= 0;

	my $event_ref = $usblcd->usblcd_pollevent($poll_time);
	if ($event_ref) {
	    my $event_process = undef;
	    if ($G_MODULE_FOCUS == 0) {
		if ($event_ref->{'type'} == USBLCD_POLLEVENT_KEYDOWN) {
		    if ($event_ref->{'value'} == USBLCD_KEY_OK) {
			my $ret;
			eval '$ret = mod_'.$G_MODULE_NAME.'::focus($usblcd);';
			$G_MODULE_FOCUS = 1 if $ret == &M_FOCUS_ACCEPT;
			$event_process = 1;
		    } elsif ($event_ref->{'value'} == USBLCD_KEY_UP) {
			$G_MODULE_INDEX = scalar @G_MODULES_GRAPH_LIST if $G_MODULE_INDEX == 0;
			$G_MODULE_INDEX--;
			module_switch($usblcd);
			$event_process = 1;
			last;
		    } elsif ($event_ref->{'value'} == USBLCD_KEY_DOWN) {
			$G_MODULE_INDEX = ($G_MODULE_INDEX + 1) % scalar @G_MODULES_GRAPH_LIST;
			module_switch($usblcd);
			$event_process = 1;
			last;
		    }
		}
	    }
	    else {
		my $ret;
		eval '$ret = mod_'.$G_MODULE_NAME.'::event($usblcd, $event_ref);';
		$G_MODULE_FOCUS = 0 if $ret == &M_EVENT_UNFOCUS;
		$event_process = 1 if $ret != &M_EVENT_DISCARD;
	    }

	    unless ($event_process) {
		# send unprocessed event to "event" modules
		module_send_event($usblcd, $event_ref);
	    }
	}
    }

    eval 'mod_'.$G_MODULE_NAME.'::update($usblcd);';

    ($G_SECONDS, $G_MICROSECONDS) = ($G_SECONDS + $G_MODULE_UPDATE / 1000, $G_MICROSECONDS + ($G_MODULE_UPDATE % 1000) * 1000);
}

eval 'mod_'.$G_MODULE_NAME.'::leave($usblcd);';
