# $Id$
package OAR::Schedulers::QuotaStorage;
require Exporter;
use POSIX qw(strftime);
use Storable qw(dclone);
use OAR::Schedulers::GanttHoleStorage_with_quotas;
use warnings;
use strict;

# Prototypes
# quota data management
sub new($);
sub read_conf_file($);
sub update_accounting_counters($$$$$$$$);
sub check_quotas($$$$$$$$$$$);
sub pretty_print($);
###############################################################################
my $Security_time_overhead;

# Creates an accounting data structure for quotas
sub new($){
    $Security_time_overhead = shift;

    my $accounting_counters_init;
    # $accounting_counters_init->{'queue'}->{'project'}->{'type'}->{'user'} = [nb_used_resources, nb_running_jobs, resourcetime]
    $accounting_counters_init->{'*'}->{'*'}->{'*'}->{'*'} = [0,0,0];

    return( [                       # Accounting data storage for quotas: an array of array
                [                   # This is a chronological stack with:
                    0,              # - t_start: counters are valid from this date to the next array entry
                    $accounting_counters_init # - counters: hash ref of the accounting data    
                ],
                [
                    OAR::Schedulers::GanttHoleStorage_with_quotas::get_infinity_value(),      # next array entry
                    undef
                ]
            ]);
}

# Read configuration file
sub read_conf_file($){
    my ($quota_file) = @_;

    my $msg;
    my $Gantt_quotas;
    # By default, no quota
    $Gantt_quotas->{'*'}->{'*'}->{'*'}->{'*'} = [-1,-1,-1];
    if (open(QUOTAFILE, "< $quota_file")){
        my $oldslurpmode = $/;
        undef $/;
        my $quota_file_content = <QUOTAFILE>;
        $/ = $oldslurpmode;
        close(QUOTAFILE);
        eval($quota_file_content);
        if ($@) {
            $msg = "Syntax error in file $quota_file: $@";
        }
    }else{
        $msg = "Cannot open file $quota_file: $!";
    }
    # Check if the values are just an integer (nb resources) or an array ref
    foreach my $q (keys(%{$Gantt_quotas})){
        foreach my $p (keys(%{$Gantt_quotas->{$q}})){
            foreach my $t (keys(%{$Gantt_quotas->{$q}->{$p}})){
                foreach my $u (keys(%{$Gantt_quotas->{$q}->{$p}->{$t}})){
                    if (! ref($Gantt_quotas->{$q}->{$p}->{$t}->{$u})){
                        $Gantt_quotas->{$q}->{$p}->{$t}->{$u} = [$Gantt_quotas->{$q}->{$p}->{$t}->{$u}, -1, -1];
                    }elsif ($#{$Gantt_quotas->{$q}->{$p}->{$t}->{$u}} < 2){
                        $Gantt_quotas->{$q}->{$p}->{$t}->{$u}->[2] = -1;
                    }
                }
            }
        }
    }

    return($Gantt_quotas, $msg);
}

# Return a string with the quota accounting data
sub pretty_print($){
    my ($accounting) = @_;
   
    my $str = "Accounting data for quotas:\n";
    my $index = 0;
    while ($index < $#{$accounting}){
        my $step_ref = $accounting->[$index];
        my $next_step_time = $accounting->[$index+1]->[0];
        $str .= "  $index From $step_ref->[0](".strftime("%F %T",localtime($step_ref->[0])).") To $next_step_time(".strftime("%F %T",localtime($next_step_time))."):\n";
        foreach my $i (sort(keys(%{$step_ref->[1]}))){
            foreach my $j (sort(keys(%{$step_ref->[1]->{$i}}))){
                foreach my $k (sort(keys(%{$step_ref->[1]->{$i}->{$j}}))){
                    foreach my $l (sort(keys(%{$step_ref->[1]->{$i}->{$j}->{$k}}))){
                        $str .= sprintf("    %16.16s > %16.16s > %10.10s > %10.10s = %i resources, %i jobs, %f resourcesXhours\n", $i, $j, $k, $l, $step_ref->[1]->{$i}->{$j}->{$k}->{$l}->[0], $step_ref->[1]->{$i}->{$j}->{$k}->{$l}->[1], $step_ref->[1]->{$i}->{$j}->{$k}->{$l}->[2]);
                    }
                }
            }
        }
        $index++;
    }
    return($str);
}

# Update accounting data
sub update_accounting_counters($$$$$$$$){
    my ( $accounting,
         $nb_resources,
         $date_start,
         $duration,
         $job_queue,
         $job_project,
         $job_types_arrayref,
         $job_user) = @_;

    my $array_id_to_insert1 = -1;
    my $array_id_to_insert2 = -1;
    my $date_stop = $date_start + $duration + 1;
    for (my $stackid = 0; $stackid < $#{$accounting}; $stackid++){
        # Update existing slots with the new job
        if (($accounting->[$stackid]->[0] >= $date_start) and ($accounting->[$stackid+1]->[0] <= $date_stop)){
            my $resourcesXhours = $nb_resources * (($date_stop - 1 - $accounting->[$stackid]->[0] - $Security_time_overhead) / 3600);
            update_accounting_slot_data($accounting->[$stackid]->[1], $job_queue, $job_project, $job_types_arrayref, $job_user, $nb_resources, $resourcesXhours);
        }
        # Get the index of the first slot to add
        if (($accounting->[$stackid]->[0] < $date_start) and ($accounting->[$stackid+1]->[0] > $date_start)){
            $array_id_to_insert1 = $stackid;
        }
        # Get the index of the last slot to add
        if (($accounting->[$stackid]->[0] < $date_stop) and ($accounting->[$stackid+1]->[0] > $date_stop)){
            $array_id_to_insert2 = $stackid;
            last();
        }
    }
    # Add new slot (end of job)
    if ($array_id_to_insert2 >= 0){
        splice(@{$accounting}, $array_id_to_insert2 + 1, 0, [ $date_stop , dclone($accounting->[$array_id_to_insert2]->[1]) ]);
        if ($accounting->[$array_id_to_insert2]->[0] >= $date_start){
            my $resourcesXhours = $nb_resources * (($date_stop - 1 - $accounting->[$array_id_to_insert2]->[0] - $Security_time_overhead) / 3600);
            update_accounting_slot_data($accounting->[$array_id_to_insert2]->[1], $job_queue, $job_project, $job_types_arrayref, $job_user, $nb_resources, $resourcesXhours);
        }
    }
    # Add new slot (start of job)
    if ($array_id_to_insert1 >= 0){
        splice(@{$accounting}, $array_id_to_insert1 + 1, 0, [ $date_start , dclone($accounting->[$array_id_to_insert1]->[1]) ]);
        my $resourcesXhours = $nb_resources * (($date_stop - 1 - $accounting->[$array_id_to_insert1+1]->[0] - $Security_time_overhead) / 3600);
        update_accounting_slot_data($accounting->[$array_id_to_insert1+1]->[1], $job_queue, $job_project, $job_types_arrayref, $job_user, $nb_resources, $resourcesXhours);
    }
}

# Update the hash of a slot for accounting data
# Internal function
sub update_accounting_slot_data($$$$$$$){
    my ( $counter_hashref,
         $queue,
         $project,
         $types_arrayref,
         $user,
         $nbresources,
         $resourcesXhours) = @_;

    $resourcesXhours = 0 if ($resourcesXhours < 0);
    foreach my $t (@{$types_arrayref},'*'){
        # Update the number of used resources
        $counter_hashref->{'*'}->{'*'}->{$t}->{'*'}->[0] += $nbresources;
        $counter_hashref->{'*'}->{'*'}->{$t}->{$user}->[0] += $nbresources;
        $counter_hashref->{'*'}->{$project}->{$t}->{'*'}->[0] += $nbresources;
        $counter_hashref->{$queue}->{'*'}->{$t}->{'*'}->[0] += $nbresources;
        $counter_hashref->{$queue}->{$project}->{$t}->{$user}->[0] += $nbresources;
        $counter_hashref->{$queue}->{$project}->{$t}->{'*'}->[0] += $nbresources;
        $counter_hashref->{$queue}->{'*'}->{$t}->{$user}->[0] += $nbresources;
        $counter_hashref->{'*'}->{$project}->{$t}->{$user}->[0] += $nbresources;
        # Update the number of running jobs
        $counter_hashref->{'*'}->{'*'}->{$t}->{'*'}->[1] += 1;
        $counter_hashref->{'*'}->{'*'}->{$t}->{$user}->[1] += 1;
        $counter_hashref->{'*'}->{$project}->{$t}->{'*'}->[1] += 1;
        $counter_hashref->{$queue}->{'*'}->{$t}->{'*'}->[1] += 1;
        $counter_hashref->{$queue}->{$project}->{$t}->{$user}->[1] += 1;
        $counter_hashref->{$queue}->{$project}->{$t}->{'*'}->[1] += 1;
        $counter_hashref->{$queue}->{'*'}->{$t}->{$user}->[1] += 1;
        $counter_hashref->{'*'}->{$project}->{$t}->{$user}->[1] += 1;
        # Update the resource X hours occupation (=~cputime)
        $counter_hashref->{'*'}->{'*'}->{$t}->{'*'}->[2] += $resourcesXhours;
        $counter_hashref->{'*'}->{'*'}->{$t}->{$user}->[2] += $resourcesXhours;
        $counter_hashref->{'*'}->{$project}->{$t}->{'*'}->[2] += $resourcesXhours;
        $counter_hashref->{$queue}->{'*'}->{$t}->{'*'}->[2] += $resourcesXhours;
        $counter_hashref->{$queue}->{$project}->{$t}->{$user}->[2] += $resourcesXhours;
        $counter_hashref->{$queue}->{$project}->{$t}->{'*'}->[2] += $resourcesXhours;
        $counter_hashref->{$queue}->{'*'}->{$t}->{$user}->[2] += $resourcesXhours;
        $counter_hashref->{'*'}->{$project}->{$t}->{$user}->[2] += $resourcesXhours;
    }
}

# Check if the job fit the quotas and when
# return the date when the quotas are satisfied
sub check_quotas($$$$$$$$$$$){
    my ( $accounting,
         $gantt_quotas,
         $current_time,
         $gantt_hole_date_stop,
         $gantt_next_hole_date_start,
         $duration,
         $job_queue,
         $job_project,
         $job_types_arrayref,
         $job_user,
         $nbresources_occupied_by_other_groups) = @_;

    my $resourcesXhours = $nbresources_occupied_by_other_groups * ($duration - $Security_time_overhead) / 3600; 
    my $comment = "quota_ok";
    my $qindex = 0;
#    print(pretty_print($accounting));
    
    # Check if quotas are satisfied in this hole and when
    while (($qindex < $#{$accounting})
            and ($accounting->[$qindex]->[0] < $gantt_hole_date_stop) # quota beginning slot inside the hole
            and ($current_time + $duration < $gantt_hole_date_stop) # quota slot not outside the hole
            and ($accounting->[$qindex]->[0] - $current_time < $duration) # continue until the quota slot is big enough
            and (($gantt_next_hole_date_start > 0) or ($gantt_next_hole_date_start > $current_time))
          ){
        if ($current_time < $accounting->[$qindex+1]->[0]){
            # Check the whole quotas
            my $q_counter; my $p_counter; my $u_counter;
            OUTER_LOOP:
            foreach my $q (keys(%{$gantt_quotas})){
                if (($q eq $job_queue) or ($q eq '*') or ($q eq '/')){
                    if (($q ne $job_queue) and (defined($gantt_quotas->{$job_queue}))){  # check if another rule is more specific
                        my $skip = 0;
                        foreach my $tmp_t (@{$job_types_arrayref},'*'){
                            if (defined($gantt_quotas->{$job_queue}->{$job_project}->{$tmp_t}->{$job_user})
                                or defined($gantt_quotas->{$job_queue}->{$job_project}->{$tmp_t}->{'*'})
                                or defined($gantt_quotas->{$job_queue}->{$job_project}->{$tmp_t}->{'/'})
                                or defined($gantt_quotas->{$job_queue}->{'*'}->{$tmp_t}->{$job_user})
                                or defined($gantt_quotas->{$job_queue}->{'/'}->{$tmp_t}->{$job_user})
                                or defined($gantt_quotas->{$job_queue}->{'*'}->{$tmp_t}->{'*'})
                                or defined($gantt_quotas->{$job_queue}->{'*'}->{$tmp_t}->{'/'})
                                or defined($gantt_quotas->{$job_queue}->{'/'}->{$tmp_t}->{'*'})
                                or defined($gantt_quotas->{$job_queue}->{'/'}->{$tmp_t}->{'/'})
                                ){
                                $skip = 1;
                                last;
                            }
                        }
                        next if ($skip == 1);
                    }
                    if ($q eq '/'){
                        $q_counter = $job_queue;
                    }else{
                        $q_counter = $q;
                    }
                    foreach my $p (keys(%{$gantt_quotas->{$q}})){
                        if (($p eq $job_project) or ($p eq '*') or ($p eq '/')){
                            if (($p ne $job_project) and (defined($gantt_quotas->{$q}->{$job_project}))){  # check if another rule is more specific
                                my $skip = 0;
                                foreach my $tmp_t (@{$job_types_arrayref},'*'){
                                    if (defined($gantt_quotas->{$q}->{$job_project}->{$tmp_t}->{$job_user})
                                        or defined($gantt_quotas->{$q}->{$job_project}->{$tmp_t}->{'*'})
                                        or defined($gantt_quotas->{$q}->{$job_project}->{$tmp_t}->{'/'})
                                        ){
                                        $skip = 1;
                                        last;
                                    }
                                }
                                next if ($skip == 1);
                            }
                            if ($p eq '/'){
                                $p_counter = $job_project;
                            }else{
                                $p_counter = $p;
                            }
                            foreach my $t (keys(%{$gantt_quotas->{$q}->{$p}})){
                                if ($t eq '*'){  # check if another rule is more specific
                                    my $skip = 0;
                                    foreach my $tmp_t (@{$job_types_arrayref}){
                                        if (defined($gantt_quotas->{$q}->{$p}->{$tmp_t}->{$job_user})
                                            or defined($gantt_quotas->{$q}->{$p}->{$tmp_t}->{'*'})
                                            or defined($gantt_quotas->{$q}->{$p}->{$tmp_t}->{'/'})
                                            ){
                                            $skip = 1;
                                            last;
                                        }
                                    }
                                    next if ($skip == 1);
                                }
                                foreach my $job_t (@{$job_types_arrayref},'*'){
                                    if ($t eq $job_t){
                                        foreach my $u (keys(%{$gantt_quotas->{$q}->{$p}->{$t}})){
                                            if (($u ne $job_user) and defined($gantt_quotas->{$q}->{$p}->{$t}->{$job_user})){  # check if another rule is more specific
                                                next;
                                            }
                                            if (($u eq $job_user) or ($u eq '*') or ($u eq '/')){
                                                if ($u eq '/'){
                                                    $u_counter = $job_user;
                                                }else{
                                                    $u_counter = $u;
                                                }
                                                if (($gantt_quotas->{$q}->{$p}->{$t}->{$u}->[0] >= 0) or
                                                    ($gantt_quotas->{$q}->{$p}->{$t}->{$u}->[1] >= 0) or
                                                    ($gantt_quotas->{$q}->{$p}->{$t}->{$u}->[2] >= 0)
                                                   ){
                                                    # Get previous nb resources used by the previous group of the job
                                                    my $tmp_account = $nbresources_occupied_by_other_groups;
                                                    my $tmp_nbjobs_account = 1;
                                                    my $tmp_resourcesXhours_account = $resourcesXhours;
                                                    if (defined($accounting->[$qindex]->[1]->{$q_counter}->{$p_counter}->{$t}->{$u_counter})){
                                                        # Add existing accounting data from the other jobs
                                                        $tmp_account += $accounting->[$qindex]->[1]->{$q_counter}->{$p_counter}->{$t}->{$u_counter}->[0];
                                                        $tmp_nbjobs_account += $accounting->[$qindex]->[1]->{$q_counter}->{$p_counter}->{$t}->{$u_counter}->[1];
                                                        $tmp_resourcesXhours_account += $accounting->[$qindex]->[1]->{$q_counter}->{$p_counter}->{$t}->{$u_counter}->[2];
                                                        if ($current_time > $accounting->[$qindex]->[0]){
                                                            $tmp_resourcesXhours_account -= ($accounting->[$qindex]->[1]->{$q_counter}->{$p_counter}->{$t}->{$u_counter}->[0] * ($current_time - $accounting->[$qindex]->[0])) / 3600;
                                                        }
                                                    }
                                                    if ((($gantt_quotas->{$q}->{$p}->{$t}->{$u}->[0] < $tmp_account) and ($gantt_quotas->{$q}->{$p}->{$t}->{$u}->[0] >= 0)) or
                                                        (($gantt_quotas->{$q}->{$p}->{$t}->{$u}->[1] < $tmp_nbjobs_account) and ($gantt_quotas->{$q}->{$p}->{$t}->{$u}->[1] >= 0)) or
                                                        (($gantt_quotas->{$q}->{$p}->{$t}->{$u}->[2] < $tmp_resourcesXhours_account) and ($gantt_quotas->{$q}->{$p}->{$t}->{$u}->[2] >= 0))
                                                       ){
                                                        $tmp_resourcesXhours_account = sprintf("%.2f", $tmp_resourcesXhours_account);
                                                        $comment = "quota_exceeded:Gantt_quotas->{$q}->{$p}->{$t}->{$u}=\[$tmp_account,$tmp_nbjobs_account,$tmp_resourcesXhours_account\]>\[$gantt_quotas->{$q}->{$p}->{$t}->{$u}->[0],$gantt_quotas->{$q}->{$p}->{$t}->{$u}->[1],$gantt_quotas->{$q}->{$p}->{$t}->{$u}->[2]\]";
                                                        $current_time = $accounting->[$qindex+1]->[0];
                                                        last OUTER_LOOP;
                                                    }else{
                                                        $comment = "quota_ok";
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
        $qindex++;
    }
    return($current_time, $comment);
}

return 1;
