The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!perl 

use strict;
use warnings;

use Carp qw(carp croak);
use FindBin qw($RealBin); 

use File::Spec::Functions;
use File::Path;
use File::Copy;

use lib 'libcpan';
use Data::Dump qw(dump);
use File::Copy::Recursive qw(dircopy);

use lib 'lib';
use Watchdog qw(sys sys_for_watchdog);
use SVNShell qw(svnversion svnup);

use lib "$FindBin::Bin/lib";

use Term::ReadKey;
ReadMode('cbreak');

my $ver = 5;

print "Working path: '" . $RealBin . "'\n" if $ver > 3;

print "Loading config file.\n" if $ver > 1;

my $fp_conf = catfile( $RealBin, 'conf.pl' );
print "Config file path: '" . $fp_conf . "'\n" if $ver > 1;
my $conf = require $fp_conf;

dump( $conf ) if $ver > 4;

print "Validating config.\n" if $ver > 1;
foreach my $c_num ( 0..$#$conf ) {
    my $ck = $conf->[$c_num];
    print "num: $c_num\n" if $ver > 3;

    # global keys
    foreach my $mck ( qw(name repository commands) ) {
        croak "Option '$mck' not found for '$ck'!\n" unless defined $ck->{$mck};
        print "$mck: '" . $ck->{$mck} . "'\n" if !ref($ck->{$mck}) && $ver > 3;
    }

    # commands keys
    foreach my $ac_num ( 0..$#{$ck->{commands}} ) {
        my $ack = $ck->{commands}->[$ac_num];
        print "  num: $ac_num\n" if $ver > 3;
        foreach my $mack ( qw(name cmd) ) {
            croak "Option '$mack' not found for '$ack'!\n" unless defined $ack->{$mack};
            print "  $mack: '" . $ack->{$mack} . "'\n" if !ref($ack->{$mack}) && $ver > 3;
        }
    }
    $ck->{src_dn} = $ck->{name} . '-src' unless exists $ck->{src_dn};
    $ck->{temp_dn} = $ck->{name} . '-temp' unless exists $ck->{temp_dn};
    $ck->{results_dn} = $ck->{name} . '-results' unless exists $ck->{results_dn};
    $ck->{src_add_dn} = $ck->{name} . '-src-add' unless exists $ck->{src_add_dn};
    
    # todo
    $ck->{temp_dn_back} = '..' unless exists $ck->{temp_dn_back};

    # todo
    $ck->{rm_temp_dir} = 1 unless exists $ck->{rm_temp_dir};
    $ck->{rewrite_temp_dir} = 1  unless exists $ck->{rewrite_temp_dir};
}
print "\n" if $ver > 3;

dump( $conf ) if $ver > 3;
print "\n" if $ver > 3;

# todo - load from dump

sub default_cmd_state {
    return {
        'cmd_num' => 0,
        'before_done' => 0,
        'cmd_done' => 0,
        'after_done' => 0,
    };
}

sub default_state {
    return {
        'svnup_done' => 0,
        'rm_temp_dir_done' => 0,
        'copy_src_dir_done' => 0,
        'after_temp_copied_done' => 0,
        'cmd' => default_cmd_state(),
    };
}


my $fp_state = catfile( $RealBin, 'state.pl' );
print "Runstate config file path: '" . $fp_state . "'\n" if $ver > 1;
my $state;
if ( -e $fp_state ) {
    $state = require $fp_state;
} else {
    $state = default_state();
}


my $conf_last = $#$conf;
my $conf_first = ( exists $state->{ck_num} ) ? $state->{ck_num} : 0;
my $first_time = 1;

while ( 1 ) {
    NEXT_CONF: foreach my $ck_num ( $conf_first..$conf_last ) {
        if ( $first_time ) {
            $conf_first = 0;
        } else {
            $state = default_state();
        }
        $first_time = 0;

        my $ck = $conf->[$ck_num];
        # todo
        #   $state->{ck_hash} = hash($ck)
        #   $conf_a->{ignore_ck_hash}
        $state->{ck_num} = $ck_num;

        print 
            ( $ck->{skip} ? 'Skipping' : 'Running' ) .
            " testing for configuration: '$ck->{name}' (" .
            ($ck_num+1) . " of " . ($conf_last+1). ")\n"
        if $ver > 1;
        next if $ck->{skip};

        # 'svn co' if needed
        unless ( -d $ck->{src_dn} ) {
            print "Source dir not found: 'svn co'\n";
            my $cmd = 'svn co "' . $ck->{repository} . '" "' . $ck->{src_dn} . '"';
            my ( $cmd_rc, $out ) = sys_for_watchdog( 
                $cmd, 
                $ck->{results_dn} . '/svn_co.txt' 
            );
            if ( $cmd_rc ) {
                print "svn co failed, return code: $cmd_rc\n" if $ver > 1;
                print "svn co output: '$out'\n" if $ver > 2;
                next;
            }
            $state->{svnup_done} = 1;
        } else {
            print "Source dir found: '$ck->{src_dn}'\n" if $ver > 3;
        }

        # get revision num
        unless ( $state->{svnup_done} ) {
            print "Getting revision number for src dir.\n" if $ver > 1;
            my ( $o_rev, $o_log ) = svnversion( $ck->{src_dn} );
            die "svn info failed: $o_log" unless defined $o_rev;
            $state->{src_rev} = $o_rev;
            print "Src revision number: $state->{src_rev}\n" if $ver > 1;
            if ( $state->{src_rev} !~ /^(\d+)$/ ) {
                print "Bad revision number. No clean src dir.\n" if $ver > 1;
                next NEXT_CONF;
            }
        }
        # svn up
        while ( not $state->{svnup_done} ) {    
            my $to_rev = 'HEAD';
            # $to_rev = $state->{src_rev} + 1;
            my $slt = 5*30;

            my ( $up_ok, $o_log, $new_rev ) = svnup( 
                $ck->{src_dn}, 
                $to_rev, 
                'README'
            );
            if ( $up_ok ) {
                if ( $new_rev > $state->{src_rev} ) {            
                    $state->{svnup_done} = 1;
                } else {
                    print "Newer revision not found in repository!\n" if $ver > 1;
                }
            } else {
                print "org dir svn up to $to_rev failed:\n" if $ver > 1;
                print "'$o_log'\n" if $ver > 2 && defined($o_log);
                print "waiting for $slt s ...\n";
            }

            if ( !$state->{svnup_done} ) {
                next NEXT_CONF if $conf_last > $conf_first; 
                sleep $slt;
                next;
            }

            print "Src dir svn up to $to_rev done\n" if $ver > 1;
            print "New src revision number: $new_rev \n" if $ver > 1;
            $state->{src_rev} = $new_rev;
        }

        if ( $ck->{rm_temp_dir} ) {
            if ( $state->{rm_temp_dir_done} ) {
                print "SKIP: Remove temp dir.\n" if $ver > 2;
            } else {
                print "Removing temp dir '$ck->{temp_dn}' ...\n" if $ver > 2;
                if ( -d $ck->{temp_dn} ) {
                    rmtree( $ck->{temp_dn} ) or die $!;
                    while ( -d $ck->{temp_dn} ) {
                        print "Remove temp dir failed.\n" if $ver > 1;
                        my $wait_time = 10;
                        print "Waiting for ${wait_time}s.\n" if $ver > 1;
                        sleep $wait_time;
                        rmtree( $ck->{temp_dn} ) or die $!;
                    }
                    print "Remove temp dir done.\n" if $ver > 1;
                } else {
                    print "Temp dir not found.\n" if $ver > 1;
                }
                $state->{rm_temp_dir_done} = 1;
            }
        }

        if ( $ck->{rm_temp_dir} || $ck->{rewrite_temp_dir} ) {
            if ( $state->{copy_src_dir_done} ) {
                print "SKIP: Copy src dir.\n" if $ver > 2;
            } else {
                print "Copying src '$ck->{src_dn}' to temp '$ck->{temp_dn}' ...\n" if $ver > 2;
                dircopy( $ck->{src_dn}, $ck->{temp_dn} ) or die "$!\n$@";
                print "Copy src dir to temp dir done.\n" if $ver > 1;
                $state->{copy_src_dir_done} = 1;
            }
        }

        if ( $ck->{after_temp_copied} ) {
            if ( $state->{after_temp_copied_done} ) {
                print "SKIP: after_temp_copied hook.\n" if $ver > 1; 
            } else {
                print "Running after_temp_copied hook.\n" if $ver > 2; 
                my $after_temp_copied_ret_code = $ck->{after_temp_copied}->( $ck, $state, $ver );
                print "After_temp_copied hook return: $after_temp_copied_ret_code\n" if $ver > 2;
                unless ( $after_temp_copied_ret_code ) {
                    print "Running after_temp_copied hook failed." if $ver > 1; 
                    next;
                }
            }
        }

        my $cmd_last = $#{ $ck->{commands} };
        my $cmd_first;
        if ( $state->{cmd} ) {
            $cmd_first = $state->{cmd}->{cmd_num};
            die "cmd_first > cmd_last" if $cmd_first > $cmd_last;

        } else {
            $cmd_first = 0;
        }

        chdir( $ck->{temp_dn} ) or die $!;

        my ( $o_rev, $o_log );
        # get revision num
        ( $o_rev, $o_log ) = svnversion( '.' );
        die "svn info failed: $o_log" unless defined $o_rev;
        $state->{temp_rev} = $o_rev;
        print "Revision number: $state->{temp_rev}\n" if $ver > 1;

        my $timestamp = time();
        $state->{results_path_prefix} = 
            $RealBin . '/' 
            . $ck->{results_dn} . '/'
            . 'r' . $state->{temp_rev} . '-' . $timestamp .  '/'
        ;
        unless ( mkpath( $state->{results_path_prefix} ) ) {
            print "Can't create results dir '$state->{results_path_prefix}'.\n" if $ver > 1;
            next;
        }
        print "Results dir: '$state->{results_path_prefix}'.\n" if $ver > 3;

        my $cmd_first_time = 1;
        foreach my $cmd_num ( $cmd_first..$cmd_last ) {
            if ( $cmd_first_time ) {
                $cmd_first_time = 0;
            } else {
                $state->{cmd} = default_cmd_state();
            }

            my $cmd_conf = $ck->{commands}->[$cmd_num];
            my $cmd = $cmd_conf->{cmd};
            my $cmd_name = $cmd_conf->{name};

            my $cmd_say = "'$cmd_name' (" . ($cmd_num+1) . " of " . ($cmd_last+1). ")";
            print "Command: $cmd_say\n" if $ver > 1;

            # max_time for watchdog
            my $cmd_mt = $cmd_conf->{mt};

            $state->{cmd}->{num} = $cmd_num;

            if ( $cmd_conf->{before} ) {
                if ( $state->{cmd}->{before_done} ) {
                    print "SKIP: before_cmd hook.\n" if $ver > 2;
                } else {
                    print "Running before_cmd hook.\n" if $ver > 2; 
                    my $before_ret_code = $cmd_conf->{before}->( $ck, $state, $ver );
                    print "Before cmd hook return: $before_ret_code\n" if $ver > 2;
                    unless ( $before_ret_code ) {
                        print "Running before_cmd hook failed.\n" if $ver > 1; 
                        next;
                    }
                }
            }
            $state->{cmd}->{before_done} = 1;

            if ( $state->{cmd}->{cmd_done} ) {
                print "SKIP: after_cmd hook.\n" if $ver > 2;
            } else {
                my $cmd_log_fp = 
                     $state->{results_path_prefix} 
                     . ($cmd_num+1) . '-' .  $cmd_name . '.out'
                ;
                # todo, .out to configure
                my ( $cmd_rc, $out ) = sys_for_watchdog( 
                    $cmd, 
                    $cmd_log_fp,
                    $cmd_mt
                );
                print "Command '$cmd_name' return $cmd_rc.\n" if $ver > 3;
            }
            $state->{cmd}->{cmd_done} = 1;

            if ( $cmd_conf->{after} ) {
                if ( $state->{cmd}->{after_done} ) {
                    print "SKIP: after_cmd hook.\n" if $ver > 2;
                } else {
                    print "Running after_cmd hook.\n" if $ver > 2; 
                    my $after_ret_code = $cmd_conf->{after}->( $ck, $state, $ver );
                    print "After_cmd hook return: $after_ret_code\n" if $ver > 3;
                    unless ( $after_ret_code ) {
                        print "Running after_cmd hook failed.\n" if $ver > 1; 
                    }
                }
            }
            $state->{cmd}->{after_done} = 1;
        }

        chdir( $ck->{temp_dn_back} ) or die $!;
    }

    my $char = undef;
    while ( defined ($char = ReadKey(-1)) ) { 1; }
    if ( $char ) {
        print "User press '$char'.\n" if $ver > 3; 
        $char = uc( $char );
        # TODO
        if ( $char eq 'P' ) {
            print "User press pause key.\n" if $ver > 2;

        } elsif ( $char eq 'C' ) {
            print "User press continue key.\n" if $ver > 2;

        } elsif ( $char eq 'Q' || $char eq 'E' ) {
            print "User press exit key.\n" if $ver > 2;
            exit;
        }
    }
    print "\n" if $ver > 1;
}