package Statusmon::Grapher;

use strict;
use Statusmon::Statusmon;
use cfgfile;

@Statusmon::Grapher::ISA = qw(Statusmon::Statusmon);


sub new
{

    my ($this) = @_;
    my  $class = ref($this) || $this;
    my  $self = {};

    bless($self, $class);

    # I want to receive "statuschange" events
    $self->register( "perf" );
    $self->register( "config" );
    $self->sched_frequency( 1 );

    $self->{"templates"} = {};
    $self->{"creators"} = {};
    $self->{"matchcreators"} = {};
    $self->{"inputwatches"} = {};
    $self->{"inputvarwatches"} = {};
    $self->{"varwatches"} = {};
    $self->{"cfgfile"} = new cfgfile( "graphtemplates" );
    $self->{"graphdir"} = $BigSister::common::fs{'var'}."/graphs";
    $self->{"wwwdir"} = $BigSister::common::fs{'web'}."/graphs";
    $self->{"create_indexes"} = {};
    $self->{"displayconfig"} = {};
    $self->{"negativecache"} = {};

    $self->{"active"} = [];

    $self->add_feature_provider( "graphprinter" );

    return($self);
}


sub start {
    my( $self ) = @_;

    if( $self->getarg( "config" ) ) {
	$self->{"cfgfile"} = new cfgfile( $self->getarg( "config" ) );
    }
    if( $self->getarg( "graphs" ) ) {
	my $file = BigSister::common::find_file( $self->getarg( "graphs" ) );
	$self->{"graphdir"} = $file;
    }
    unless( -d $self->{"graphdir"} ) {
	unless( mkdir( $self->{"graphdir"}, 0755 ) ) {
	    BigSister::common::log( "warn", "cannot create directory ".($self->{"graphdir"})." for storing graphics" );
	}
    }
    unless( -d $self->{"wwwdir"} ) {
	unless( mkdir( $self->{"wwwdir"}, 0755 ) ) {
	    BigSister::common::log( "warn", "cannot create directory ".($self->{"wwwdir"})." for storing html index files" );
	}
    }


    my $config = $self->get_feature_provider( "displayconfig" );
    if( $config ) { $self->read_displayconfig( $config->get_current_config() ) };

    $self->read_config();
    $self->read_graphdefs();
    foreach my $host ( keys %{$self->{"inputwatches"}} ) {
	next if( -f $self->{"wwwdir"}."/$host.html" );
	$self->{"create_indexes"}->{$host} = 1;
    }
}

    

sub rrd {
    my( $self ) = @_;

    if( $self->{"rrd"} ) { return( $self->{"rrd"} ); }
    $self->{"rrd"} = $self->get_feature_provider( "rrd" );
    return $self->{"rrd"};
}


sub read_config {
    my( $self ) = @_;
    my $vars = {};

    my $cfg = $self->{"cfgfile" };
    $cfg->rewind();
    $self->{"templates"} = {};

    my $current;
    my $currentname;
    while( $_ = $cfg->nextline() ) {
	chomp;
	next unless( $_ );
	if( /^graph (.*)/ ) {
	    $currentname = $1;
	    $current = {
		"graph" => $currentname,
		"vars" => { "GRAPH" => $currentname },
		"input-var" => [],
		"accept" => [],
		"graph-id" => "\$HOST.\$GRAPH",
		"input-interval" => 300,
		"value-type" => "gauge",
		"consolidate" => "average",
		"format" => "png",
                "grtype" => "line"
	    };
	    $self->{"templates"}->{$currentname} = $current;
	    next;
	}

	unless( $current ) {
	    unless( /^([a-z]+)=(.*)$/i ) {
		BigSister::common::log( "warn", "Grapher: config outside graph: $_" );
	    }
	    my( $var, $cont ) = ($1,$2);
	    $vars->{$var} = $self->expand( $cont, $vars, 1 );
	    next;
	}

	if( /^[\s\t]*(input|value-identifier|graph-id|legend|title|value-type|input-limits|input-interval|input-deadtime|maxcombine|scale|unit-label|unit-base|size|format|grtype|logarithmic)[\s\t]+(.*)/ ) {
	    my( $var, $cont ) = ($1,$2);
	    $cont = $self->expand( $cont, $vars, 1 );
	    $current->{$var} = $cont;
	}
	elsif( /^[\s\t]*(input-var)[\s\t]+([^\s\t]+)[\s\t]+(.*)/ ) {
	    push( @{$current->{"input-var"}}, [ $2, $3 ] );
	}
	elsif( /^[\s\t]*(include|exclude)[\s\t]+(.*)/ ) {
	    push( @{$current->{"accept"}}, [ $1, $2 ] );
	}
    }

    $self->init_watchers();
}



sub init_watchers() {
    my( $self ) = @_;

    $self->{"creators"} = {};
    $self->{"matchcreators"} = {};
    while( my( $id, $template ) = each %{$self->{"templates"}} ) {
	my $input = $template->{"input"};
	unless( $input ) {
	    BigSister::common::log( "warn", "Grapher: graph $id: no input variable" );
	    next;
	}
	if( $input =~ /\(.*\)/ ) {
	    $self->{"matchcreators"}->{$input} = $template;
	}
	else {
	    $self->{"creators"}->{$input} = $template;
	}
    }
}



sub read_displayconfig {
    my( $self, $newconfig, $oldconfig ) = @_;

    $self->{"displayconfig"} = $newconfig;
}


        
sub perf {
    my( $self, $cmd ) = @_;

    my( $host, $variable, $value ) = @$cmd{"host","variable","value"};
    my $id = "$host $variable";
    if( defined $self->{"negativecache"}->{$id} ) {
        my $timeout = $self->{"negativecache"}->{$id};
	return if( time < $timeout );
	delete $self->{"negativecache"}->{"$host $variable"};
    }

    if( defined $self->{"inputvarwatches"}->{$host}
      && defined $self->{"inputvarwatches"}->{$host}->{$variable} ) {
        foreach my $def (@{$self->{"inputvarwatches"}->{$host}->{$variable}}) {
	    my $graph = $def->{"graph"};
	    my $oldval = $graph->{"vars"}->{$def->{"name"}};
	    if( $oldval ne $value ) {
		$graph->{"vars"}->{$def->{"name"}} = $value;
		my $id = $self->expand( $graph->{"value-identifier-template"}, $graph->{"vars"} );
		next unless( $id );

		if( $id ne $graph->{"value-identifier"} ) {
		    $self->recalibrate( $graph, $id );
		}
	    }
	}
    }
     
    if( defined $self->{"inputwatches"}->{$host} 
      && defined $self->{"inputwatches"}->{$host}->{$variable} ) {
	my $graph = $self->{"inputwatches"}->{$host}->{$variable};
	return if( (defined $graph->{"nologging"}) || ! defined $graph->{"fileid"} );
	my $rrd = $self->rrd();
	if( $rrd ) {
	    $rrd->rrd_update(
		Platform::filepath( ($self->{"graphdir"})."/".($graph->{"fileid"}).".rrd" ),
		($cmd->{"perftime"}).":".$value
	    )
	}
	return ;
    }

    if( defined $self->{"varwatches"}->{$host} 
      && defined $self->{"varwatches"}->{$host}->{$variable} ) {
	my $suref = $self->{"varwatches"}->{$host}->{$variable};
	foreach my $ref (@$suref) {
	    my( $name, $template ) = @$ref;
	    $template->{"vars"}->{$name} = $value;
	    delete $self->{"varwatches"}->{$host}->{$variable};
	    my @vars = grep( $_->[0] ne $name, @{$template->{"input-var"}} );
	    $template->{"input-var"} = \@vars;
	    $self->check_pending( $template, $cmd->{'perftime'} );
	}
	return;
    }


    my $template;
    my( $group, $group2, $group3 );
    if( defined $self->{"creators"}->{$variable} ) {
	$template = $self->{"creators"}->{$variable};
    }
    else {
	while( my( $input, $templ ) = each %{$self->{"matchcreators"}} ) {
	    if( $variable =~ /^$input$/ ) {
		$template = $templ;
		($group,$group2,$group3) = ($1,$2,$3);
	    }
	}
    }
    unless( $template ) {
        $self->{"negativecache"}->{$id} = time + 3600;
	return;
    }
    print STDERR "Grapher: creating new graph using template "
               . ($template->{"graph"})
	       . " at time $cmd->{'perftime'}"
	       . "\n" 
	                                                if( $BigSister::common::dl > 1 );
    # magic: add the variable to the negative cache
    # it will be cleared out as soon as the graph is
    # created. Meanwhile we prevent us from creating
    # new (pending) graphs.
    $self->{"negativecache"}->{$id} = time + 48*3600;

    my %pending = %$template;
    $pending{"input"} = $variable;
    # copy graph variables!
    my %vars = %{$pending{"vars"}};
    $pending{"vars"} = \%vars;
    $pending{"vars"}->{"HOST"} = $host;
    ($pending{"vars"}->{"GROUP"} = $group) if( defined $group );
    ($pending{"vars"}->{"GROUP2"} = $group2) if( defined $group2 );
    ($pending{"vars"}->{"GROUP3"} = $group3) if( defined $group3 );

    if( defined $pending{"input-var"} ) {
	foreach my $var ( @{$pending{"input-var"}} ) {
	    unless( defined $self->{"varwatches"}->{$host} ) {
		$self->{"varwatches"}->{$host} = {};
	    }
	    my $key = $self->expand( $var->[1], $pending{"vars"} );
	    unless( defined $self->{"varwatches"}->{$host}->{$key} ) {
		$self->{"varwatches"}->{$host}->{$key} = [];
	    }
	    push( @{$self->{"varwatches"}->{$host}->{$key}}, [ $var->[0], \%pending ] );
	    delete $self->{"negativecache"}->{"$host $key"};
	}
    }
    unless( defined $self->{"inputwatches"}->{$host} ) {
	$self->{"inputwatches"}->{$host} = {};
    }
    $self->check_pending( \%pending, $cmd->{'perftime'} );
    $self->setup_watches();
}



sub recalibrate {
    my( $self, $graph, $newid ) = @_;

    my $class = $graph->{"graph"};
    $graph->{"value-identifier"} = $newid;
    foreach my $i (@{$self->{"active"}}) {
        if( ($i != $graph) && ($i->{"graph"} eq "$class") && ($i->{"value-identifier"} eq $newid) ) {
	    #swap
	    my %swap = %$i;
	    foreach my $attr ("input", "input-vars", "group", "group2", "group3" ) {
	        $i->{$attr} = $graph->{$attr};
		$graph->{$attr} = $swap{$attr};
	    }
	    $self->save( $i );
	    $self->setup_watches();
	    BigSister::common::log( "warn", "recalibrated graph binding for ".($graph->{"input"}) );
	    last;
	}
    }
    $self->save( $graph );
}


sub expand {
    my( $self, $text, $vars, $preexpand ) = @_;

    my $ret = "";
    while( $text =~ /\$([\$a-zA-Z0-9]+)/ ) {
	$ret .= $`;
	$text = $';
	if( $1 eq "\$" ) {
	    $ret .= $preexpand?"\$\$":"\$";
	}
	else {
	    if( defined $vars->{$1} ) {
		$ret .= $vars->{$1};
	    }
	    elsif( $preexpand ) {
	        $ret .= "\$$1";
	    }
	}
    }

    return ($ret.$text);
}



sub check_pending {
    my( $self, $template, $perfTime ) = @_;

    return if( (defined $template->{"input-var"})
	&& ($#{$template->{"input-var"}} >= 0) );
    $self->create( $template, $perfTime );
}



sub create {
    my( $self, $template, $perfTime ) = @_;

    foreach my $var (qw( value-identifier value-type input-limits input-interval
	input-deadtime graph-id legend maxcombine scale unit-label unit-base size format title grtype)) {
	if( defined $template->{$var} ) {
	    $template->{$var} = $self->expand( $template->{$var}, $template->{"vars"} );
	}
    }
    $self->enrich( $template );

    unless( defined $template->{"value-identifier"} ) {
	my $id = ($template->{"vars"}->{"HOST"}).".".($template->{"graph"});
	foreach my $suffix ("","2", "3") {
	    if( defined $template->{"vars"}->{"GROUP$suffix"} ) {
		$id .= ".".($template->{"vars"}->{"GROUP$suffix"});
	    }
	}
	$template->{"value-identifier"} = $id;
    }

    $template->{"host"} = $template->{"vars"}->{"HOST"};
    foreach my $suffix ("",2,3) {
	$template->{"group$suffix"} = $template->{"vars"}->{"GROUP$suffix"};
    }

    foreach my $active (@{$self->{"active"}}) {
	if( $active->{"value-identifier"} eq $template->{"value-identifier"} ) {
	    while( my( $var, $val ) = each %$template ) {
		$active->{$var} = $val;
	    }
	    $self->save( $active );
	    $self->setup_watches();
	    BigSister::common::log( "notice", "Grapher: input source altered for ".($template->{"graph"})." database for ".($template->{"vars"}->{"HOST"}) );
	    return;
	}
    }


    BigSister::common::log( "notice", "Grapher: creating ".($template->{"graph"})." database for ".($template->{"vars"}->{"HOST"}) );
    $self->post_event( {
        "cmd_group" => "config",
	"cmd" => "graphcreated",
	"host" => ($template->{"vars"}->{"HOST"})
    } );

    my $fileid = $template->{"value-identifier"};
    $fileid =~ s/[\\\/\s\t:]/_/g;
    $template->{"fileid"} = $fileid;

    my @args = ();
    my $interval = $template->{"input-interval"};
    push( @args, "--step", $interval );

    # Are we skewed by a time zone?  If so, start the graph with an earlier
    # time.
    if ( $perfTime < time() )
    {
    	my $timeArg = sprintf 'now-%ds', ( time() - $perfTime + 1800 );
    	push( @args, "--start" );
    	push( @args, $timeArg  );
    };

    my $dst = $template->{"value-type"};
    if( $dst =~ /^(gauge|counter|derive|absolute)$/i ) {
	$dst = uc( $dst );
    }
    else {
	BigSister::common::log( "err", "graph ".($template->{"graph"}).": unknown value-type $dst" );
	$dst = "GAUGE";
    }
    my( $min, $max );
    if( $template->{"input-limits"} =~ /(\d*)\.\.(\d*)/ ) {
	($min,$max) = ($1,$2);
    }
    my $heartbeat = $template->{"input-deadtime"};
    unless( $heartbeat ) {
	$heartbeat = 5 * $template->{"input-interval"};
    }
#    push( @args, "DS:a:$dst:$heartbeat".(length($min)?":$min":(length($max)?":":"")).(
#	length($max)?":$max":"") );
    push( @args, "DS:a:$dst:$heartbeat:".(length($min)?$min:"U").":".(length($max)?$max:"U") );

    my $twodays = 48*3600/$interval;
    if( $twodays > 600 ) { $twodays = $twodays/2; }
    if( $twodays < 100 ) { $twodays = 2 * $twodays; }
    $twodays = int($twodays);
    push( @args, "RRA:AVERAGE:0.5:1:$twodays" );

    my $cf = $template->{"consolidate"};
    if( $cf =~ /^(average|min|max|last)$/ ) {
	$cf = uc( $cf );
    }
    else {
	BigSister::common::log( "err", "graph ".($template->{"graph"}).": unknown consolidation function $cf" );
	$cf = "AVERAGE";
    }
	
    my $week = 7*24*3600/10/$interval; 
    if( $week > 500 ) { $week = 500; }
    if( $week < 100 ) { $week = 100; }
    $week = int($week);
    my $number = int(7*24*3600/$week/$interval+1);
    push( @args, "RRA:$cf:0.5:$number:$week" );

    my $month = 30*24*3600/10/$interval; 
    if( $month > 500 ) { $month = 500; }
    if( $month < 200 ) { $month = 200; }
    $month = int($month);
    my $number = int(30*24*3600/$month/$interval+1);
    push( @args, "RRA:$cf:0.5:$number:$month" );

    my $year = 365; 
    my $number = int(365*24*3600/$month/$interval+1);
    push( @args, "RRA:$cf:0.5:$number:$year" );

    my $rrd = $self->rrd();
    unless( $rrd
      && $rrd->rrd_create( Platform::filepath(($self->{"graphdir"})."/".($template->{"fileid"}).".rrd" ), @args ) ) {
	BigSister::common::log( "err", "RRD creation failed for graph ".($template->{"graph"}) );
	$template->{"nologging"} = "yes";
	return;
    }
    $self->save( $template );
    push( @{$self->{"active"}}, $template );
    $self->save_index();
    $self->setup_watches();

    $self->create_indexfile( $template->{"host"} );
}



sub save {
    my( $self, $template ) = @_;

    open( OUT, ">".($self->{"graphdir"})."/".($template->{"fileid"}).".graph" );
    while( my( $var, $contents ) = each %$template ) {
	next if( ref $contents );
	$contents =~ s/\n/ /g;
	print OUT "$var=$contents\n";
    }
    close OUT;
}



sub setup_watches {
    my( $self ) = @_;
    my $watches = {};

    $self->{"inputwatches"} = $watches;
    my $vwatches = $self->{"inputvarwatches"} = {};
    foreach my $active (@{$self->{"active"}}) {
	my $host = $active->{"host"};
	unless( defined $watches->{$host} ) {
	    $watches->{$host} = {};
	}
	$watches->{$host}->{$active->{"input"}} = $active;
	delete $self->{"negativecache"}->{"$host ".($active->{"input"})};

        $vwatches->{$host} = {} unless( defined $vwatches->{$host} );
	if( defined $active->{"input-vars"} ) {
	    foreach my $var ( split( " :: ", $active->{"input-vars"} ) ) {
	        next unless( $var =~ /^(.*?) (.*)$/ );
		my( $name, $var ) = ($1,$2);
		$vwatches->{$host}->{$var} = [] unless( defined $vwatches->{$host}->{$var} );
		push( @{$vwatches->{$host}->{$var}}, { 
		    "graph" => $active,
		    "name" => $name
		} );
		delete $self->{"negativecache"}->{"$host $var"};
	    }
	}
    }
}



sub read_graphdefs {
    my( $self ) = @_;

    opendir( DIR, $self->{"graphdir"} );
    foreach my $file (readdir( DIR )) {
	next unless( $file =~ /\.graph$/ );
	my $current = $self->read_graphdef( $file );
	next unless( $current->{"graph"} );
	push( @{$self->{"active"}}, $current );
    }
    closedir( DIR );
    $self->setup_watches();
    $self->save_index();
}



sub save_index {
    my( $self ) = @_;

    open( OUT, ">".($self->{"graphdir"})."/index.$$" );
    foreach my $graph (@{$self->{"active"}}) {
	print OUT ($graph->{"host"})." || ".($graph->{"graph-id"})." || ".($graph->{"fileid"})."\n";
    }
    close OUT;
    unless( Platform::replace( ($self->{"graphdir"})."/index.$$", ($self->{"graphdir"})."/index" ) ) {
	sleep 1;
	unless( Platform::replace( ($self->{"graphdir"})."/index.$$", ($self->{"graphdir"})."/index" ) ) {
	    BigSister::common::log( "err", "cannot overwrite graph index file" );
	    unlink( ($self->{"graphdir"})."/index.$$" );
	}
    }
}



sub create_indexfile {
    my( $self, $host ) = @_;

    my $disp = $self->get_feature_provider( "display" );
    my $cfg = $self->get_feature_provider( "displayconfig" );
    return unless( $disp && $cfg );
    my $skin = $cfg->get_current_config()->{"defaultskin"};
    my @text = ();
    $skin->{"_BASE"} = "..";
    my %ids = ();
    foreach my $template ( @{$self->{"active"}} ) {
	next unless( $template->{"host"} eq $host );
	next if( $ids{$template->{"graph-id"}} );
	$ids{$template->{"graph-id"}} = 1;
	push( @text, $disp->replace_vars( $skin, {
	    "HOST" => $host,
	    "ENC_HOST" => $disp->encode($host),
	    "TITLE" => $template->{"title"},
	    "ID" => $template->{"graph-id"},
	    "ENC_ID" => $disp->encode($template->{"graph-id"})
	    }, $skin->{"graph_index_entry.proto"} ) );
    }

    $disp->replace_vars_file( $skin, {
	"TEXT" => join( "", @text ),
	"HOST" => $host
	}, $skin->{"graph_index.proto"},
	$self->{"wwwdir"}."/$host.html" );

    delete $skin->{"_BASE"};
}



sub event {
    my( $self, $cmd ) = @_;
    my $text;

    print STDERR "Statusmon::Grapher got an event\n" if( $BigSister::common::dl > 6 );
    $self->{"current_event"} = $cmd;
    if( $cmd->{"cmd"} eq "perf" ) {
	$self->perf( $cmd );
    }
    elsif( $cmd->{"cmd"} eq "displayconfig" ) {
	$self->read_displayconfig( $cmd->{"config"}, $cmd->{"previous_config"} );
    }
}




sub run {
    my( $self ) = @_;

    if( $self->{"cfgfile"}->has_changed() ) {
	$self->read_config();
    }

    foreach my $host (keys %{$self->{"create_indexes"}}) {
	$self->create_indexfile( $host );
	delete $self->{"create_indexes"}->{$host};
    }
}


# ======================= Grapher feature provider =========================


sub get_index {
    my( $self, $host, $graphid ) = @_;
    my @result = ();

    open( INDEX, "<".($self->{"graphdir"})."/index" );
    while( <INDEX> ) {
	chomp;
	my( $ihost, $igraphid, $ifileid ) = split( / \|\| / );
	next if( 
	    ( (defined $host) && ($host ne $ihost) )
	    || ( (defined $graphid) && ($graphid ne $igraphid) )
	);
	push( @result, {
	    "host" => $ihost,
	    "graph-id" => $igraphid,
	    "fileid" => $ifileid
	} );
    }
    close INDEX;
    return @result;
}


sub read_graphdef {
    my( $self, $file ) = @_;

    unless( $file =~ /\.graph$/ ) {
	$file .= ".graph";
    }
    open( IN, "<".($self->{"graphdir"})."/$file" );
    my %current = ();
    while( <IN> ) {
	chomp;
	if( /^(.*?)=(.*)$/ ) {
	    $current{$1} = $2;
	}
    }
    close( IN );
    if( $current{"graph"} ) {
	if( $self->enrich( \%current ) ) {
	    $self->save( \%current );
	}
    }
    return \%current;
}



sub enrich {
    my( $self, $graph ) = @_;
    return(0) unless( defined $graph->{"graph"} );
    my $name = $graph->{"graph"};
    print STDERR "  enrich($name)\n" if( $BigSister::common::dl > 4 );
    my $template;
    $template = $graph->{"template"} = $self->{"templates"}->{$name} if( defined $self->{"templates"}->{$name} );
    print STDERR "    template: $template\n" if( $BigSister::common::dl > 5 );
    return(0) unless( $template );

    my $altered = 0;

    $graph->{"vars"} = {} unless( defined $graph->{"vars"} );
    foreach my $var ("graph", "host","group","group2","group3") {
	$graph->{"vars"}->{uc($var)} = $graph->{$var} if( defined $graph->{$var} );
    }

    if( (defined $template->{"input-var"}) && ! defined $graph->{"input-vars"} ) {
	print STDERR "      adding input-var\n" if( $BigSister::common::dl > 6 );
	$altered++;
        my $vardef = "";
	foreach my $var (@{$template->{"input-var"}}) {
	    $vardef .= " :: " if( $vardef );
	    $vardef .= ($var->[0])." ".($self->expand( $var->[1], $graph->{"vars"} ));
	}
	$graph->{"input-vars"} = $vardef;
    }

    if( defined $template->{"value-identifier"} && ! defined $graph->{"value-identifier-template"} ) {
        $graph->{"value-identifier-template"} = $template->{"value-identifier"};
	$altered++;
    }

    return $altered;
}

        
sub rrd_file {
    my( $self, $graphorindex ) = @_;

    return( ($self->{"graphdir"})."/".($graphorindex->{"fileid"}).".rrd" );
}


1;
