#!/usr/bin/perl 
use strict;
use warnings;
use Config::IniFiles;
use Set::CrossProduct;
use Getopt::Long;
use Pod::Usage;
use Data::Dumper;

##########################################################
# Initialization
##########################################################
my $varyParamsConf = "conf/varyParams.conf";
my $varyParamsLog  = "/tmp/varyParams.log";
my $verbose        = 0;
my $help           = 0;
my $man            = 0;

Getopt::Long::Configure ("bundling");
my $result = GetOptions( 
	"f|file=s"   => \$varyParamsConf,
	"l|log=s"    => \$varyParamsLog,
	"v|verbose+" => \$verbose,
	"h|help"     => \$help,
	"m|man"      => \$man,
) or pod2usage(2);
pod2usage(1)  if $help;
pod2usage({-exitval => 0, -verbose => 2})  if $man;
pod2usage({-message => "File $varyParamsConf not found", -exitval => 2})  unless -e $varyParamsConf;

print "Logging to $varyParamsLog\n" if $verbose;
open LOG, ">>$varyParamsLog" or die $!;

my %cfg;
tie %cfg, 'Config::IniFiles', ( -file => $varyParamsConf, -allowcontinue => 1 );

##########################################################
# Run
##########################################################

my $gAtStart       = $cfg{Global}{at_start} || " ";
my $gAtEnd         = $cfg{Global}{at_end}   || " ";
my $gPreRun        = $cfg{Global}{pre_run}  || " ";
my $gPostRun       = $cfg{Global}{post_run} || " ";
my $gDefaultCfg    = $cfg{Global}{default_cfg_file};
my $gProgRun       = $cfg{Global}{run}      || " ";

my $startTime  = localtime;
my $gStartTime = time;
print LOG "\n# Start of $0 at $startTime, PID=$$\n"; 
exec_cmd($gAtStart);

$_ = $cfg{Global}{run_blocks};
die "Error: No Global::run_blocks defined in $varyParamsConf!\n" unless $_;
my @blocks = split /\s*,\s*/;

$_ = $cfg{Global}{settings};
if($_){
	my @settings = split /\s+/;
	foreach my $s (@settings){
		my ($fn,$sec,$p,$v) = interprete_line($gDefaultCfg,$s);
		set_val($fn,$sec,$p,$v);
	}
}


run_block($_) foreach (@blocks);

exec_cmd($gAtEnd);

##########################################################
# interprete_line: determine file, section, parameter
# and value, substituting defaults.
##########################################################
sub interprete_line{
	my $defaultcfg = shift;
	my $line = shift;
	my (@files,@secs,@params);
	unless($line =~ m|^\s*(.*)=(.*?)\s*$|){
		die "Error: Could not parse line ``$line''\n";
	}
	my $params = $1;
	my $values = $2;
	foreach my $p (split /,/,$params){
		if(0){
		}elsif($p =~ m|^(\w+)::(\w+)$|){
			push @files, $defaultcfg;
			push @secs, $1;
			push @params, $2;
		}elsif($p =~ m|^([\w/.]+)::(\w+)::(\w+)$|){
			push @files, $1;
			push @secs, $2;
			push @params, $3;
		}else{
			die "Error: Could not parse line ``$line''\n";
		}
	}
	if(scalar @files == 1){
		return ($files[0],$secs[0],$params[0],$values);
	}
	return (\@files,\@secs,\@params,$values);
}


##########################################################
# run_block: run an experiment described by a block
# in the .ini-file
##########################################################
sub run_block{
	my $b = shift;
	my $cfg = $cfg{$b};
	unless($cfg and %$cfg){
		warn "Warning: Section $b is not defined/empty in $varyParamsConf!\n";
		return;
	}
	my %cfg = %$cfg;
	my $defaultCfg    = $cfg{default_cfg_file} || $gDefaultCfg;
	my $atStart       = $cfg{at_start} || " ";
	my $atEnd         = $cfg{at_end}   || " ";
	my $preRun        = $cfg{pre_run}  || " ";
	my $postRun       = $cfg{post_run} || " ";
	my $progRun       = $cfg{run} || $gProgRun;

	my $startTime = localtime;
	print LOG "\n# Start of Block $b at $startTime\n";
	print     "\n# Start of Block $b at $startTime\n" if $verbose;
	exec_cmd($atStart,$b);

	# apply general settings for block
	$_ = $cfg{settings};
	if($_){
		my @settings = split /\s+/;
		foreach my $s (@settings){
			my ($fn,$sec,$p,$v) = interprete_line($defaultCfg,$s);
			set_val($fn,$sec,$p,$v);
		}
	}

	### Now, the actual varying starts.
	#### Read what to vary...
	my $vary = $cfg{vary};
	unless($vary){
		warn "Warning: Nothing to vary for ``$b''\n";
		return;
	}
	my (@params,@values);
	foreach (split/\s+/,$vary){
		my ($fn,$sec,$p,$v) = interprete_line($defaultCfg,$_);
		$v = join(',', glob($v)) if($v =~ /\*/);
		my %d = ("file" => $fn, "sec" => $sec,"param" => $p,"val"=> $v);
		push @values, [ split/\s*,\s*/,$d{val} ];
		$d{val}     = [ split/\s*,\s*/,$d{val} ];
		push @params, \%d;
	}

	#### Create all combinations
	my $it    = Set::CrossProduct->new(\@values);
	my $tuple;
	my %subst;
	while ($tuple = $it->get){
		my %substs = ();
		foreach my $t (0..(scalar @$tuple-1)){
			my $v = $$tuple[$t];
			$substs{$params[$t]{param}} = $v;
			if(ref $params[$t]{file})
			{
				# all of the following variables must get the same value
				my $max = $#{$params[$t]{file}};
				foreach(0..$max){
					set_val($params[$t]{file}->[$_],$params[$t]{sec}->[$_],$params[$t]{param}->[$_],$v);
					$subst{$params[$t]{param}->[$_]} = $v;
				}
			}
			else
			{
				# just a plain, single variable
				set_val($params[$t]{file},$params[$t]{sec},$params[$t]{param},$v);
				$subst{$params[$t]{param}} = $v;
			}
		}
		# execute all that stuff
		exec_cmd($gPreRun, $b,\%subst);
		exec_cmd($preRun,  $b,\%subst);
		exec_cmd($progRun, $b,\%subst);
		exec_cmd($postRun, $b,\%subst);
		exec_cmd($gPostRun,$b,\%subst);
	}

	exec_cmd($atEnd,$b);
	print LOG "\n# End of Block $b at $startTime\n";
}

##########################################################
# set_val: write a value to a config-file
##########################################################
sub set_val{
	my ($file,$sec,$param,$val) = @_;
	my $cfg = new Config::IniFiles( -file => $file );
	print "Setting $file ${sec}::$param to $val\n" if($verbose);
	$cfg->setval($sec,$param,$val);
	$cfg->RewriteConfig;
	$cfg->Delete;
}

##########################################################
# exec_cmd: prepare and execute a command for a hook
##########################################################
sub exec_cmd{
	my $cmd   = shift;
	my $block = shift || "Global";
	$_ = shift || { " " => " "};
	my %subst = %$_;
	$cmd =~ s/{time}/$gStartTime/g;
	$cmd =~ s/{block}/$block/g;
	foreach (keys %subst){
		if(-e $subst{$_}){ 
			# treat filenames specially
			$subst{$_} =~ s|.*/||g;  # remove path
			$subst{$_} =~ s|\..*||g; # remove suffix
		}
		$cmd =~ s/{$_}/$subst{$_}/g;
	}
	print LOG qx($cmd);
}

__END__

=head1 NAME

varyParams2.pl - Varying parameters in ini-style config files

=head1 SYNOPSIS

varyParams2.pl [options]

  Options: 
    -f | --file configfile        load this configuration file
    -l | --log  logfile           log everything to this file
    -v | --verbose                increase verbosity
    -h | --help                   this message
    -m | --man                    man page

=head1 DESCRIPTION

This script helps you if you have a program for which
many parameters and their combinations have to be tested.

To use it, make sure your program reads the parameters
from INI-style config files.

The next step is to create yet another .ini file, which
controls which parameters vary and what is executed when.

The following example shall hopefully give you an idea on what 
to do:

Write a config file, which contains any or all of the following:

	[Global]
	# this is the program to start
	run              = ./my_program

	# executed once
	at_start         = echo start of block {block} at time {time}
	at_end           = echo end of block {block} at time {time}

	# executed once per run
	pre_run          = 
	post_run         = 

	# if not specified otherwise, use this file
	default_cfg_file = conf/default.cfg

	# run the following blocks:
	run_blocks       = Exp1,Exp2

	# apply these settings for all blocks
	settings         = General::interactive=false


	[Exp1]
	# apply these settings for all runs
	settings = General::interactive=false

	# overwrites global default_cfg_file
	default_cfg_file = conf/default.cfg

	# executed after global at_start. 
	# Similar: at_end, pre_run, post_run
	at_start = echo Starting Block {block} at {time}

	# the parameters to vary. Values are separated by ",".
	vary = Blubber::blo=1,2,3 \
		   Blubber::bla=x,y,z 

	[Exp2]
	# vary parameters in another config file
	vary = conf/other.cfg::Blubber::blo=true,false

	[Exp3]
	# iterate over all jpg files in a folder
	vary = Blubber::image=images/*.jpg

	[Exp4]
	# vary two variables, but make sure they have the same value
	# at all times
	vary    = Blubber::var_a,Bibber::var_b=1,2,3

	# print the value of a variable
	pre_run = echo Starting run with var_a={var_a}

Caveats:

- You must make sure that there are at least two variables to vary.
If you want to vary just one variable, add a second one with just 
one value.

=cut

