#! /usr/bin/perl -w


use Getopt::Long;
use File::Basename;
use Cwd;


sub zeropad {
  my ($value) = @_;
  if ($value>=10) {
    return $value;
  }
  return "0".$value;
}

sub get_date_strings {
  my ($sec,$min,$hour,$day,$month,$year,,,)=localtime(time);
  $year+=1900;
  $month++;
  $sec=zeropad($sec);
  $min=zeropad($min);
  $hour=zeropad($hour);
  $month=zeropad($month);
  $day=zeropad($day);
  return ($sec,$min,$hour,$day,$month,$year);
}

sub timestamp_file {
  my ($sec,$min,$hour,$day,$month,$year)=get_date_strings();
  return $year."-".$month."-".$day."_".$hour."-".$min."-".$sec;
}

sub timestamp_human {
  my ($sec,$min,$hour,$day,$month,$year)=get_date_strings();
  return $year."-".$month."-".$day." ".$hour.":".$min.":".$sec;
}


$myname=basename($0);

if ( !GetOptions("param=s",\$param,"paramfile=s",\$paramfile,"nice=i",\$nice,
		 "host=s",\$host,"sge",\$sge,"sge-options=s",\$sge_options,
		 "dir=s",\$dir,"subdir=s",\$subdir,
		 "help",\$help)
  || defined($help) ) {
  print "usage: $myname [--param=PARAMS | --paramfile=PARAMFILE] [--nice=NICE] [--host=HOST | --sge ] [--sge-options=QSUBOPTIONS] [--dir=DIR] [--subdir=SUBDIR] [--help] script\n";
  print "Rstart version 1.0. Jan-Moritz Peter Franosch 2003 <mail\@Franosch.org>.\n";
  print "Starts a script on a remote machine or submits it to Sun Grid Engine (SGE).\n";
  print "\n";
  print "PARAMS = Parameter string in the format 'param1=value1 param2=value2 ...'.\n";
  print "         All strings '&param1&' etc. in the 'script' are substituted\n";
  print "         by the given values.\n";
  print "NICE   = The script is started with 'nice -n NICE'.\n";
  print "HOST   = The remote host on which the script is started.\n";
  print "DIR    = The directory name on the remote host in which output is created.\n";
  print "         Default is the current working directory.\n";
  print "SUBDIR = The name of the newly created directory. Default is 'Result_TIMESTAMP_XXXXXX'.\n";
  print "\n";
  print "'script' is the name of the script to be started.\n";
  print "\n";
  print "--help displays this help message.\n";
  print "\n";
  print "In the directory DIR, creates a new directory named 'Result...', followed by a time stamp\n";
  print "(format year-month-day_hours-minutes-seconds), followed by a unique mark.\n";
  print "Writes a file 'Result.../parameters.txt' with the parameters and a time stamp.\n";
  print "Substitutes the parameters in 'script' and writes the result to 'Result.../start.sh'.\n";
  print "\n";
  print "Creates a script 'Result_.../execute.sh', writing the machine name, the output\n";
  print "and the execution time to 'Result_.../'\n";
  print "Standard output of 'execute.sh' is written to 'stdout.txt' and\n";
  print "standard error output to stderr.txt.\n";
  print "\n";
  print "In the execution environment of the script, the environment variable SCRIPTDIR\n";
  print "contains the directory, where the script resides, as an absolute pathname.\n";
  print "\n";
  print "The default directory 'Result...' may be overwritten by giving SUBDIR.\n";
  print "A string 'TIMESTAMP' in SUBDIR is substituted by a time stamp as described above,\n";
  print "a string 'XXXXXX' in SUBDIR is substituted by a unique mark.\n";
  print "\n";
  print "If --host is given, 'start.sh' is executed on the remote host HOST via ssh.\n";
  print "\n";
  print "If --sge is given, then 'execute.sh' is created under 'Result.../' and\n";
  print "submitted to the Sun Grid Engine (SGE) by qsub. The options QSUBOPTIONS are given to\n";
  print "qsub, if specified.\n";
  print "\n";
  print "Note that if either --host or --sge is given, the directory DIR must exist on the host\n";
  print "where $myname is executed as well as on the remote host(s) and must be the same directory.\n";
  print "\n";
  print "The file 'state' contains the current state of the job.\n";
  print "The state 'create' means the necessary files described above are created,\n";
  print "'submitted' means the job has been submitted to SGE,\n";
  print "'running' means the job is currently running,\n";
  print "'FAILED' means the job has finished with exit status other than zero, and\n";
  print "'success' means the job has finished with exit status zero.\n";
  print "\n";
  print "If 'script' is a directory, the directory is copied and parameters in all\n";
  print ".template-files are substituted. Then, 'start.sh' is executed or 'make' is executed\n";
  print "in case a 'Makefile' is found.\n";
  print "\n";
  print "In PARAMS, more than one parameter value for each parameter, separated by a comma,\n";
  print "may be given, e.g. --param='param1=value11,value12 param2=value21,value22,value23'\n";
  print "Then, the script is executed with all combinations of the parameters, i.e.\n";
  print "in the example above with all six combinations (param1=value11,param2=value21),\n";
  print "(param1=value12,param2=value21), (param1=value11,param2=value22), ...\n";
  print "Because for each combination of the parameters, a new script 'start.sh' in a new directory\n";
  print "is created, SUBDIR must contain 'XXXXXX' so that each of the newly created directory names\n";
  print "will be unique. In case --sge is given, all jobs are submitted parallelly to SGE.\n";
  exit 1;
}

if ( $#ARGV!=0 ) {
  $nargs=$#ARGV+1;
  die "Exactly one script or directory must be specified (specified $nargs). Try --help.\n";
}
$scriptname=$ARGV[0];

if ( dirname($scriptname) =~ /^\// ) {
} else {
  $scriptname=cwd()."/".$scriptname;
}

if ( $scriptname =~ /\/$/ ) {
  chop($scriptname);
}

if ( defined($param) && defined($paramfile) ) {
  die "Both --param and --paramfile specified. Try --help.\n";
}

if ( defined($host) && defined($sge) ) {
  die "Specifying both --host and --sge makes not much sense. Try --help.\n";
}

if ( !defined($sge_options) ) {
  $sge_options="";
}

if ( !defined($nice) ) {
  $nice=0;
}

if ( !defined($subdir) ) {
  $subdir="Result_TIMESTAMP_XXXXXX";
}

if ( defined($param) ) {
  if ( !($param =~ /^\s*(\w*=\S+\s*)*$/ ) ) {
    die "--param ('$param') must have the format 'parameter1=value1 parameter2=value2 ...'.";
  }
}

if ( !defined($dir) ) {
  $dir=cwd();
}

if ( defined($paramfile) ) {
  if ( $paramfile =~ /^\// ) {
  } else {
    $paramfile=cwd()."/".$paramfile;
  }
}

$timestamp=timestamp_file();

# if ( defined($host) ) {
#   print `ssh -f $host 'cd $dir && LOGFILE=\`mktemp "$myname".log.$timestamp.XXXXXX\` && echo "Logfile \$LOGFILE" && $0 --param="$param" --paramfile="$paramfile" --nice="$nice" --dir="$dir" --subdir="$subdir" "$scriptname" &> \$LOGFILE &' 2>&1`;
#   print "Job submitted to host '$host'.\n";
#   exit 0;
# }


# Read parameter file if specified.
$linenr=0;
if ( defined($paramfile) ) {
  open(PARAMFILE,"< $paramfile") || die "Could not open '$paramfile'.";
  while ( defined($line = <PARAMFILE>) ) {
    if ( $line =~ /^ *\#/ || $line =~ /^\s*$/) {
      # Kommentarzeilen und leere Zeilen ueberspringen.
      next;
    }
    if ( !defined(@parameternames) ) {
      # erste Zeile
      @parameternames=split ' ',$line;
      next;
    }
    @{$parametervalues[$linenr]} = split ' ',$line;
    $nparametervalues = $#{$parametervalues[$linenr]};
    ++$linenr;
    if ( $#parameternames != $nparametervalues ) {
      die "Number of parameter names ($#parameternames) must match number of values ($nparametervalues) in line $linenr of paremeter file $paramfile."
    }
  }
  if ( !defined(@parameternames) ) {
    die "First non-comment line of '$paramfile' must contain parameter names."
  }
  if ( !defined(@parametervalues) ) {
    die "No parameter values for parameters '".join(' ',@parameternames)."' found.";
  }
}


# Read --param if specified.
if ( defined($param) ) {
  @paramstrings= $param =~ /(\w*=\S+)/g;
  $p=0; # number of the parameter
  foreach $pair (@paramstrings) {
    ($parameter,$value) = ( $pair =~ /(\w*)=(\S+)/ );
    $parameternames[$p]=$parameter;
    $parametervalues[0][$p]=$value;
    ++$p;
  }
}

if ( !defined($param) && !defined($paramfile) ) {
  # only one set of parameters, the empty set
  @parameternames = ();
  @{$parametervalues[0]} = ();
}

# Expand the comma separated parameters
$combinationnr=0; # number of the combination
for ($i=0; $i<=$#parametervalues; ++$i) {

  # For all parameters ...
  for ($p=0; $p<=$#parameternames; ++$p) {
    # save the comma separated values.
    @{$all_values[$p]} = split(',',$parametervalues[$i][$p]);
  }

  # How many combinations do we get?
  $ncombinations=1;
  for ($p=0; $p<=$#parameternames; ++$p) {
    $ncombinations *= ($#{$all_values[$p]}+1);
  }

  # We repeat the first parameters in blocks, then the second etc.
  $nrepeats=$ncombinations;
  $nblocks=1; # number of blocks
  for ($p=0; $p<=$#parameternames; ++$p) {
    $parametername = $parameternames[$p];
    $nvalues = ($#{$all_values[$p]}+1); # number of different values for the parameter
    $nrepeats /= $nvalues; # number of repeats for each value
    for ($blocknr=0; $blocknr<$nblocks; ++$blocknr) {
      for ($valuenr=0; $valuenr<$nvalues; ++$valuenr) {
	for ($repeatnr=0; $repeatnr<$nrepeats; ++$repeatnr) {
	  $expanded_parametervalues[$combinationnr]{$parametername}=$all_values[$p][$valuenr];
	  ++$combinationnr;
	}
      }
    }
    $nblocks *= $nvalues;
    $combinationnr -= $ncombinations;
  }

  $combinationnr += $ncombinations;

}


if ( $ncombinations>1 ) {
  if ( $subdir !~ /XXXXXX/ ) {
    die "You have specified more than one combination of parameters, thus --subdir=$subdir must contain the placeholder 'XXXXXX'"
  }

  # print all combinations
  print "Combinations of parameters:\n";
  for ($p=0; $p<=$#parameternames; ++$p) {
    print $parameternames[$p]," ";
  }
  print "\n";
  for ($combinationnr=0; $combinationnr<=$#expanded_parametervalues; ++$combinationnr) {
    for ($p=0; $p<=$#parameternames; ++$p) {
      print $expanded_parametervalues[$combinationnr]{$parameternames[$p]}," ";
    }
    print "\n";
  }

}


# For all combinations of parameters ...
for ($combinationnr=0; $combinationnr<$ncombinations; ++$combinationnr) {

  $timestamp2=timestamp_human();
  $timestamp=timestamp_file();

  $template=$dir."/".$subdir;
  $template =~ s/TIMESTAMP/$timestamp/;
  if ($template =~ /XXXXXX/) {
    $directory=`mktemp -d $template`;
    chomp($directory);
    if ($directory eq "") {
      die "Could not create temporary directory.\n";
    }
    chmod 0755,"$directory" || die "Could not change mode of '$directory' to 0755.\n";
  } else {
    $directory=$template;
    mkdir $directory,0755 || die "Could not create directory '$directory'.\n";
  }

  open(STATE,"> $directory/state") || die "Could not open file '$directory/state' for writing.";
  print STATE "create\n";
  close STATE;

  if ( -d $scriptname ) {
    # $scriptname is a directory
    # Everything under this directory is recursively copied to the (Results-) SUBDIR
    # Then in files named *.template, parameters are substituted.
    $scriptdir=$scriptname;
    ! system("cp -a $scriptname/* $directory")
      || die "Could not copy files from $scriptname to $directory.";
  } else {
    $scriptdir=dirname($scriptname);
    ! system("cp -a $scriptname $directory/start.sh.template")
      || die "Could not copy script $scriptname to $directory/start.sh.template.";
  }

  if ( defined($last_build_directory) ) {
    # Ein 'make' ist schon gemacht.
    # "Rette" die Ergebnisse durch Kopieren.
    ! system("cp -a $last_build_directory/* $directory")
      || die "Could not copy files from '$last_build_directory' to '$directory'.";
    # Notwendig, damit die substituierten Template-Dateien juenger sind
    # als alles Dateien vom alten make in $last_build_directory.
    sleep 1;
  }

  open(PARAMETERS,">".$directory."/parameters.txt") 
    || die "Could not open '$directory/parameters.txt' for writing.\n";
  print PARAMETERS "script ".basename($scriptname)."\n";
  print PARAMETERS "timestamp $timestamp2\n";
  for ($p=0; $p<=$#parameternames; ++$p) {
    $parameter = $parameternames[$p];
    $value = $expanded_parametervalues[$combinationnr]{$parameter};
    $parameters{$parameter}=$value;
    print PARAMETERS "$parameter $value\n";
  }
  close(PARAMETERS);

  print "\nParameters:\n";
  print `cat $directory/parameters.txt`;
  print "\n";

  $templatedir=$directory."/templates";
  mkdir $templatedir,0755 || die "Could not create directory '$directory'.\n";

  opendir(DIRECTORY,$directory) || die "Could not open directory '$directory'.\n";
  @files=readdir DIRECTORY;
  for $file (@files) {
    if ( ! ($file =~ /\.template$/ ) ) {
      next;
    }
    #    print "Substituting '$file'.\n";
    open(TEMPLATE,"< $directory/$file")
      || die "Could not open file '$directory/$file' for reading.";
    $subst_template=basename($file,".template");
    open(SUBST_TEMPLATE,"> $templatedir/$subst_template") 
      || die "Could not open file '$templatedir/$subst_template' for writing.";
    $linenr=0;
    while ( defined($line = <TEMPLATE>) ) {
      $linenr++;
      foreach $parameter (keys %parameters) {
	$line =~ s/&$parameter&/$parameters{$parameter}/g;
      }
      if ( $line =~ /&\w*&/ ) {
	$line2=$line;
	chomp($line2);
	die "Line $linenr ('$line2') in file '$template' probably contains unspecified parameters.\n";
      }
      print SUBST_TEMPLATE $line;
    }
    close SUBST_TEMPLATE;
  }

  # Alle substituierten templates, die von schon vorhandenen Files verschieden sind, moven.
  opendir(TEMPLATEDIRECTORY,"$templatedir") ||
    die "Could not open directory '$templatedir'.\n";
  @files=readdir TEMPLATEDIRECTORY;
  for $file (@files) {
    if ( ! -f "$templatedir/$file" ) {
      # Keine Datei.
      next;
    }
    if ( -e "$directory/$file" ) {
      # Datei existiert.
      if ( ! system("cmp --quiet $templatedir/$file $directory/$file") ) {
	# Datei ist unveraendert.
	unlink "$templatedir/$file" || die "Could not remove '$templatedir/$file'.";
	next;
      }
    }
    ! system("mv $templatedir/$file $directory/$file") ||
      die "Could not move '$templatedir/$file' to '$directory/$file'.";
  }

  rmdir $templatedir || die "Could not remove '$templatedir'.";


  chdir $directory || die "Could not change to directory '$directory'.\n";

  print "Output will be in $directory.\n";

  if ( -e "start.sh" ) {
    chmod 0755,"start.sh" || die "Could not change mode of 'start.sh' to 0755.\n";
    $command="./start.sh";
  } elsif ( -e "Makefile" ) {
    $command="make";
    if ( !defined($sge) ) {
      $last_build_directory=$directory;
      # After 'make' has been made, copy the results in
      # $last_build_directory to the new directory, in the hope the
      # results are partly usable. In case we submit to sge, do not
      # copy because we don't know when 'make' has finished.
    }
  } else {
    die "No start.sh and no Makefile found."
  }

#     open(OUTPUT,"> output.txt") || die "Could not open output.txt for writing.";
#     $hostname=`hostname`;
#     chomp($hostname);
#     $date=`date`;
#     chomp($date);
#     print OUTPUT "Begin of execution on $hostname at $date.\n";
#     print OUTPUT "---------------- output\n";
#     close OUTPUT;
#     print `export SCRIPTDIR=$scriptdir ; /usr/bin/time nice -n $nice $command >> output.txt 2>&1`;
#     open(OUTPUT,">> output.txt") || die "Could not open output.txt.";
#     $date=`date`;
#     chomp($date);
#     print OUTPUT "---------------- end of output\n";
#     print OUTPUT "End of execution at $date.\n";
#     close OUTPUT;

  # create the script to submit
  $jobname = basename($directory);
  open(SCRIPT,"> execute.sh") || die "Could not open execute.sh for writing.";
  print SCRIPT "#!/bin/bash\n\n";
  print SCRIPT "echo -n 'Begin of execution of job '\n";
  print SCRIPT "echo -n $jobname\n";
  print SCRIPT "echo -n ' on host '\n";
  print SCRIPT "echo -n `hostname`\n";
  print SCRIPT "echo -n ' by '\n";
  print SCRIPT "echo -n \$USER\n";
  if ( defined($sge) ) {
    print SCRIPT "echo -n ' in queue '\n";
    print SCRIPT "echo -n \$QUEUE\n";
  }
  print SCRIPT "echo -n ' at '\n";
  print SCRIPT "echo -n `date`\n";
  print SCRIPT "echo '.'\n";
  print SCRIPT "echo '---------------- output'\n";
  print SCRIPT "export SCRIPTDIR=$scriptdir\n";
  print SCRIPT "echo running > state\n";
  print SCRIPT "/usr/bin/time $command\n";
  print SCRIPT "STATUS=\$?\n";
  print SCRIPT "echo '---------------- end of output'\n";
  print SCRIPT "echo -n 'End of execution at '\n";
  print SCRIPT "echo -n `date`\n";
  print SCRIPT "echo '.'\n";
  print SCRIPT "if [ \"\$STATUS\" != \"0\" ] ; then\n";
  print SCRIPT "  echo FAILURE!\n";
  print SCRIPT "  echo FAILED > state\n";
  print SCRIPT "  exit 1\n";
  print SCRIPT "else\n";
  print SCRIPT "  echo Success.\n";
  print SCRIPT "  echo success > state\n";
  print SCRIPT "fi\n";
  close SCRIPT;
  chmod 0755,"execute.sh" || die "Could not change mode of 'execute.sh' to 0755.\n";

  if ( defined($sge) ) {
    # submit job to SGE
    open(STATE,"> state") || die "Could not open file 'state' for writing.";
    print STATE "submitted\n";
    close STATE; # File must be written _before_ the submit to avoid a race condition
    print `qsub -N $jobname -cwd -o stdout.txt -e stderr.txt -S /bin/bash $sge_options execute.sh`;
    if ( $? != 0 ) {
      open(STATE,"> state") || die "Could not open file 'state' for writing.";
      print STATE "create\n";
      close STATE;
      die "Could not submit job to SGE.\n";
    }
  } else {
    if ( defined($host) ) {
      print `ssh -f $host 'cd $directory && ./execute.sh > stdout.txt 2> stderr.txt'`;
    } else {
      print `cd $directory && ./execute.sh > stdout.txt 2> stderr.txt`;
    }
  }

}
