#!/usr/bin/perl
#  Copyright (C) 2008  Stanislav Sinyagin
#
#  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 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.

# Stanislav Sinyagin <ssinyagin@k-open.com>

# Combine SUM or MAX from several service IDs and create a new one


use strict;
use warnings;
BEGIN { require '/usr/share/torrus/conf_defaults/torrus-config.pl'; }

use POSIX qw(floor);
use Getopt::Long;
use Date::Parse;
use Date::Format;
use Math::BigFloat;

use Torrus::Log;
use Torrus::ServiceID;
use Torrus::SQL::SrvExport;

my $startdate;
my $enddate;
my $onemonth;

my $function;
my @input;
my $output;

my $step = 300;

my $debug;
my $verbose;
my $help_needed;

my %known_functions = ('MAX' => \&function_max, 'SUM' => \&function_sum);


my $ok = GetOptions(
                    'start=s'      => \$startdate,
                    'end=s'        => \$enddate,
                    'month'        => \$onemonth,
                    'func=s'       => \$function,
                    'in=s'         => \@input,
                    'out=s'        => \$output,
                    'step=i'       => \$step,
                    'verbose'      => \$verbose,
                    'debug'        => \$debug,
                    'help'         => \$help_needed,
                    );

if( $help_needed or not $ok or (not $startdate) or
    (defined($enddate) + defined($onemonth) != 1) or
    not $function or
    (scalar(@input) < 2 and scalar( @ARGV ) < 2) or
    not $output )
{
    print STDERR "Usage: $0 TIMESPAN OUTPUT FUNCTION SOURCES...\n\n",
    "TIMESPAN:\n",
    "  --start=YYYY-MM-DD   [mandatory] Start date\n",
    "  --month              [optional] Calendar month timespan\n",
    "  --end=YYYY-MM-DD     [optional] Next day after the timespan end\n",
    "  Either --month or --end option must be defined\n\n",
    "OUTPUT:\n",    
    "  --out=SERVICEID      [mandatory] Output Service ID\n\n",
    "FUNCTION:\n",
    "  --func=MAX|SUM       [mandatory] Aggregation function\n\n",
    "SOURCES:\n",
    "  Either --in=SERVICEID, or Service ID names as command line ",
    "arguments.\n",
    "  Minimum 2 sources required\n\n",
    "Options:\n",
    " --step=N              [300] service data interval\n",
    " --verbose             print extra information\n",
    " --debug               print debugging information\n",   
    " --help                print usage information\n",   
    "\n";
    
    exit 1;
}

push(@input, @ARGV);

if( not defined($known_functions{$function}) )
{
    printf STDERR ("Unknown function: %s. Must be onne of: %s\n",
                   $function, join(', ', sort keys %known_functions));
    exit 1;
}



if( $debug )
{
    Torrus::Log::setLevel('debug');
}
elsif( $verbose )
{
    Torrus::Log::setLevel('verbose');
}


my $starttime = str2time( $startdate );
if( not defined($starttime) )
{
    Error('Cannot parse start date: ' . $startdate);
    exit 1;
}

# Canonize the date
$startdate = time2str('%Y-%m-%d', $starttime);

my $endtime;

if( defined($enddate) )
{
    $endtime = str2time( $enddate ); 
    if( not defined($endtime) )
    {
        Error('Cannot parse end date: ' . $enddate);
        exit 1;
    }

    if( $endtime < $starttime )
    {
        Error('End date is earlier than start date');
        exit 1;
    }        
}
else
{
    # Calculate +1 calendar month

    my ($ss,$mm,$hh,$day,$month,$year,$zone) =
        strptime( $startdate );
    $year += 1900;
    $month++;
    $day++;
  
    my $endyear = $year;
    my $endmonth = $month + 1;
    
    if( $endmonth > 12 )
    {
        $endmonth = 1;
        $endyear++;
    }

    $endtime = str2time( sprintf('%.4d-%.2d-%.2d',
                                 $endyear, $endmonth, $day) );
    if( not defined($endtime) )
    {
        # oops, it was past the end of the month
        $day++;
        $endmonth++;
        if( $endmonth > 12 )
        {
            $endmonth = 1;
            $endyear++;
        }
        
        $endtime = str2time( sprintf('%.4d-%.2d-%.2d',
                                     $endyear, $endmonth, $day) );
        
        if( not defined($endtime) )
        {
            Error('Cannot determine the end date');
            exit 1;
        }
    }
}

# Canonize the date
$enddate = time2str('%Y-%m-%d', $endtime);


Verbose('Start time: ', scalar(localtime($starttime)));
Verbose('End time: ', scalar(localtime($endtime)));

my $srvExp = Torrus::SQL::SrvExport->new();
if( not defined( $srvExp ) )
{
    Error('Cannot connect to the SQL database');
    exit 1;
}

my $srvIDs = $srvExp->getServiceIDs();

foreach my $serviceid ( @input )
{
    if( not grep {$serviceid eq $_} @{$srvIDs} )
    {
        Error('Input service ID not found in the database: ' . $serviceid);
        exit 1;
    }
}

&Torrus::DB::setSafeSignalHandlers();

# Check if the output ServiceID exists in the local database
# The database contains only IDs generated from datasource trees.

my $srvIDParams = new Torrus::ServiceID();
if( $srvIDParams->idExists( $output ) )
{
    Error('Output service ID was previously created from Torrus ' .
          'datasource tree. Cannot override it: ' . $output);
    exit 1;
}
&Torrus::DB::cleanupEnvironment();
&Torrus::DB::setUnsafeSignalHandlers();


my %in_data;

foreach my $serviceid ( @input )
{
    $in_data{$serviceid} =
        $srvExp->getIntervalData( $startdate, $enddate, $serviceid );

    Verbose(sprintf('Loaded %d rows of data for %s',
                    scalar( @{$in_data{$serviceid}} ),
                    $serviceid));
}

my $n_points = floor( ($endtime - $starttime) / $step );

my %aligned_data;

foreach my $serviceid ( @input )
{
    my @aligned = ();
    $#aligned = $n_points;

    # Fill in the aligned array. For each interval by modulo(step),
    # we take the sum of values that get into that interval
    
    foreach my $row ( @{$in_data{$serviceid}} )
    {
        my $rowtime = str2time( $row->{'srv_date'} . 'T' .
                                $row->{'srv_time'} );
        
        my $pos = floor( ($rowtime - $starttime) / $step );
        my $value = Math::BigFloat->new( $row->{'value'} );
        if( $value->is_nan() )
        {
            $value->bzero();
        }
            
        if( not defined($aligned[$pos]))
        {
            $aligned[$pos] = $value;
        }
        else
        {
            $aligned[$pos]->badd($value);
        }
    }

    $aligned_data{$serviceid} = \@aligned;
}
    
Verbose(sprintf('Aligned data into %d intervals', $n_points));

# Store the derived data
my $dbh = Torrus::SQL::SrvExport->dbh();
if( not defined( $dbh ) )
{
    Error('Lost SQL connection');
    exit 1;
}

my $sth = $dbh->prepare( Torrus::SQL::SrvExport->sqlInsertStatement() );
if( not defined( $sth ) )
{
    Error('Error preparing the SQL statement: ' . $dbh->errstr);
}



for( my $pos = 0; $pos < $n_points; $pos++ )
{
    my @args;
    foreach my $serviceid ( @input )
    {
        my $val = $aligned_data{$serviceid}->[$pos];
        if( defined( $val ) )
        {
            push( @args, $val );
        }
    }

    if( scalar( @args ) > 0 )
    {
        my $value = &{$known_functions{$function}}(@args);

        my $timestamp = $starttime + $pos * $step;
        my $datestr = time2str('%Y-%m-%d', $timestamp);
        my $timestr = time2str('%H:%M:%S', $timestamp);
        
        if( isDebug() )
        {
            Debug('Updating SQL database: ' .
                  join(', ', $datestr, $timestr,
                       $output, $value, $step ));
        }
    
        if( not $sth->execute( $datestr, $timestr,
                               $output, $value, $step ) )
        {
            Error('Error executing SQL: ' . $dbh->errstr);
            exit 1;
        }
    }
}

$dbh->commit() or Error('Error committing to SQL database: ' . $dbh->errstr);
$dbh->disconnect();

Verbose('Database update finished');

exit($ok ? 0:1);


sub function_max
{
    my $value = Math::BigFloat->new();
    $value->binf('-');

    foreach my $a ( @_ )
    {
        if( $value->bcmp($a) < 0 )
        {
            $value = Math::BigFloat->new($a);
        }
    }

    return $value;
}


sub function_sum
{
    my $value = Math::BigFloat->new(0);

    foreach my $a ( @_ )
    {
        $value->badd($a);
    }

    return $value;
}
    


# Local Variables:
# mode: perl
# indent-tabs-mode: nil
# perl-indent-level: 4
# End:
