=head1 NAME

  CFX5::Simulation::Configuration::Solve - Solver Input File Input handling for CFX5::Job

=head1 SYNOPSIS

This object is used by L<CFX5::Job>, to manage the process of setting
up the initial problem definition files.

=head1 DESCRIPTION

$Id$

=cut

package CFX5::Simulation::Configuration::Solve;

use base 'CFX5::Simulation::Subtask';

use strict;
use CFX5::Output qw(:all error_text);
use CFX5::FileTests;

use CFX5::PathManip qw(dirname basename fullpath join_file);
use CFX5::FileOps qw(create_empty_file create_directory
		     append_to_file set_file_contents copy_file move_file
		     link_files concatenate_files append_to_file);
use CFX5::Commands qw(do_cmd do_cmd_quiet get_cmd_output parse_rv do_termerr_cmd);

use CFX5::Config qw(%Config export_conf);

use CFX5::CCLI;

use CFX5::Simulation::Util '_check_defined';

use Carp;

=head3 Solve

Sets up and runs the solver, given the relevant settings.

=cut

sub _Do {
  my $self=shift;

  # make sure we've got something sensible here
  my $do=shift;
  internal_error("First argument must be 0 or 1") 
    unless defined $do and $do eq 0 or $do eq 1;

  return 1 unless $self->start_cfx();

  export_conf(qw(CFX_DATA_DIR CFX5_REACTIONS_DIR CFX5_MATERIALS_DIR));

  #
  # what do we need for the Solver?
  #
  # * a working directory
  # * an out file
  # * an input def file
  #

  my $rundir=$self->rundir();
  _check_defined $rundir, "working directory" or return 0 if $do;
  # No test, we just hope.
  # The following will create the directory.
  my $workdir;
  if ($do) {
    $workdir = $self->rundir_obj();
    return 0 unless (defined $workdir);
  }

  # StartOutFile does the requisite checks, but only under some
  # circumstances, and if we're just checking we don't really want to
  # create it anyway ...
  my $outfile=$self->outfile();
  if ($do) {
    _check_defined $outfile, "solver output file" or return 0;
    CFX5::FileTests::datafile_w($outfile, "solver output file") or return 0;
    $self->StartOutFile() or return 0;
  }

  unless ($self->HaveFile('def')) {
    _error("Definition file has not been set up");
    return 0;
  }

  # to allow us to run with the CFX-5.3 solver on UNIX, which handled
  # out files differently, we make a symlink in the working directory
  # pointing to the actual out file.  Thus any programs which try to
  # write to it will succeed, although we don't encourage this.
  #
  # If the current machine does not support symlinks, the solver can
  # never have worked in this way, so we just don't bother.
  if ($do and !$self->HaveFile('out') and CFX5::FileOps::can_symlink()) {
    # we can't directly use a method on the WorkDir object such as
    # input_file, as the semantics of that method are not guaranteed.
    my $inout=$workdir->creatable_file('out');
    link_files($outfile,$inout);
    $workdir->mark_output_file_ignore('out');
  }

  my $resfile=$self->resfile();
  if ($do) {
    _check_defined $resfile, "ANSYS CFX results file" or return 0;
    CFX5::FileTests::datafile_w($resfile, "ANSYS CFX results file") or return 0;
    # we want to save the results
    $workdir->creatable_file('res');
    $workdir->mark_output_file('res' => $resfile);
  }

  # the new 5.4-style job file
	my $jobfile ;
  if ($do) {
		$jobfile=$self->jobfile();
		if ($self->save_jobfile()) {
			_check_defined $jobfile, "ANSYS CFX Solver job file" or return 0;
			CFX5::FileTests::datafile_w($jobfile, "ANSYS CFX Solver job file") or return 0;
		}
	}
  if ($do) {
    $workdir->creatable_file('job');
    if ($self->save_jobfile()) {
      # we want to save the results
      $workdir->mark_output_file('job' => $jobfile);
    } else {
      # we don't want to save the results, but we'd rather not give an
      # error either
      $workdir->mark_output_file_ignore('job');
    }
  }

  # and the same again for the 5.3-style job file
  my $jobfile_53=$self->jobfile_53();
  if ($self->save_jobfile_53()) {
    _check_defined $jobfile_53, "ANSYS CFX Solver old-style job file" or return 0;
    CFX5::FileTests::datafile_w($jobfile_53, "ANSYS CFX Solver old-style job file")
      or return 0;
  }
  if ($do) {
    $workdir->creatable_file('job_53_style');
    if ($self->save_jobfile_53()) {
      # we want to save the results
      $workdir->mark_output_file('job_53_style' => $jobfile_53);
    } else {
      # we don't want to save the results, but we'd rather not give an
      # error either
      $workdir->mark_output_file_ignore('job_53_style');
    }
  }

  # we may get a stop file ... and if the solver stops before it
  # starts, it may even be called 'stp'!
  if ($do) {
    $workdir->creatable_file('solver.sta');
    $workdir->creatable_file('stp');
  }

  #
  # set up the PPE
  #
  if ($do) {
    $self->PreparePPE()
      or return 0;
  } else {
    $self->CheckPPE()
      or return 0;
  }

  #
  # input partitioning
  #
  if ($self->processes() > 1) {
    # if we aren't partitioning on this run, we need an input parfile
    unless ($self->HaveFile('par')) {
      my $parfile_in=$self->parfile_in();
      _check_defined $parfile_in, "partitioning file" or return 0;
      CFX5::FileTests::datafile($parfile_in, "source partition file $parfile_in")
	or return 0;

      # check that the number of partitions is consistent
      my $par=CFX5::Files::Partition->new($parfile_in);
      if (defined $par) {
        my $parts=$par->num_parts();
        if (defined $parts) {
          # we've managed to work it out; for now, we don't give an
          # error if we fail, but we will check if we can.
          my $parts_ccl=$self->partitions();
          if (defined $parts_ccl and $parts_ccl != $parts) {
            _error("The input partition file $parfile_in defines $parts partitions, but this run requires $parts_ccl partitions.");
          }
        }
      }

      if ($do and !$workdir->have_file('par')) {
	$workdir->input_file('par' => $parfile_in);
      }
    }

  }

  # what are our outputs?
  $self->ProvideFile('res');

  1;
}

sub Run {
  my $self=shift;

  if ($self->Prepare()) {
    # ready to go!
    my $rundir=$self->rundir_obj();

    $self->OutFileHeader('Solver');

    # for the Portland Fortran compiler
    $ENV{NO_STOP_MESSAGE}='1';

    my @args;

    if ($self->processes() > 1) {
      push @args, "-par";
    }
		
    if ($self->solver_debug_prompt()) {
      push @args, "-debug-prompt";
    }

    if ($self->solver_debug_prompt_user()) {
      push @args, "-debug-prompt-user";
    }

    if (defined $self->memory_factor()) {
      push @args, '-s', $self->memory_factor();
    }

    if (defined $self->cat_factor()) {
      if (defined (my $f=$self->cat_factor())) {
        if ($f =~ s!x$!!i) {
          # "1.5x" actually maps to a different solver argument
          push @args, "-smms", $f;
        } else {
          if ($f =~ m/^\d+[KkMm]?$/) {
            push @args, "-nmms", $f;
          } else {
            _error("Invalid syntax for parameter /SIMULATION CONTROL/EXECUTION CONTROL/SOLVER STEP CONTROL/MEMORY CONTROL/Catalogue Size Override = $f");
            return 1;
          }
        }
      }
    }

    # calculate the memory flags to use
    my %pairs=(
               nr => 'Real',
               ni => 'Integer',
               nd => 'Double Precision',
               nc => 'Character',
               nl => 'Logical',
              );
    for my $t (keys %pairs) {
      my $m="memory_${t}";
      if (defined (my $f=$self->$m())) {
        if ($f =~ s!x$!!i) {
          # "1.5x" actually maps to a different solver argument
          (my $a=$t) =~ s!^n!s!;
          push @args, "-$a", $f;
        
        } else {
          if ($f =~ m/^\d+[KkMm]?$/) {
            push @args, "-$t", $f;
          } else {
            _error("Invalid syntax for parameter /SIMULATION CONTROL/EXECUTION CONTROL/SOLVER STEP CONTROL/MEMORY CONTROL/$pairs{$t} Memory Override = $f");
            return 1;
          }
        }
      }
    }

    {
      my %pri=(
               Idle => 0,
               Low => 1,
               Standard => 2,
               High => 3,
              );
      if (defined $self->solver_priority()) {
        push @args, '-pri', $pri{$self->solver_priority()};
      }
    }

    # set the verbosity of the solver output
    if ($self->first_solver_run()) {
      push @args, '-outopt' => 0;
    } else {
      push @args, '-outopt' => 1;
    }

    unless ($self->save_jobfile()) {
      push @args, '-nojob';
    }

    $self->RunningComponent('solver');
    $rundir->enter();
    # we always run inside the PPE now, even for serial runs
    my $rv=$self->_exec_PPE(@args);
    $rundir->leave();
    $self->RunningComponent('');

    # Append any handled errors to the output file
    $self->AppendHandledErrors();

    # If the return code was non-zero and the run finalized correctly
    # continue normally, but issue a warning
    unless ($rv->is_success()) {
      my $finalized = 0;
      if (open my $fh, $self->outfile) {
        while (my $line = readline($fh)) {
          $finalized = 0 if ($line =~ m/\s+\|\s+Solver\s+\|/);
          $finalized = 1 if ($line =~ m/Final synchronization point reached by all partitions/);
        }
        close($fh);
      }

      if ($finalized) {
        _warn('The solver exited abnormally, but reached the final synchronization point. Continuing.');
        $rv->reparse_rv(0);
      }
    }

    # Check return code
    if (!$rv->is_success()) {
      my $rfmsg='';
      if (not $self->HaveFile('res')) {
        $rfmsg="  No results file has been created.";
      }
      _error($rv->errstr("The ANSYS CFX solver") . $rfmsg);
    } elsif (not $self->HaveFile('res')) {

      # this may happen normally if no res file is expected
      # or the user stopped the solver before
      # it really got started, in which case stop_now will be 1 (user
      # stop).  (We need to check if it's true before doing a numeric
      # comparison to avoid warnings).
      _error("The ANSYS CFX solver has terminated without writing a results file. ",
             $rv->errstr())
        unless (!$self->save_resfile() or ($self->stop_now() and $self->stop_now() == 1));
    }

    # write a line to the out file indicating that we've finished.
    my $fh=$self->outobj()->open();
    if ($fh) {
      print $fh "End of solution stage.\n";
      $self->outobj()->close()
	or _error("Cannot finish writing terminator to out file: $!");
    } else {
      _error("Cannot write terminator to out file: $!");
    }

    # register that the solver has run at least once.
    $self->first_solver_run('');

    # now grab the transient and backup files if possible
    $self->TransientCleanup();

    # make sure the mon file is saved into the results file now
    if ($self->HaveFile('mon')) {
      $rundir->notice_file('mon');
      my $dest;
      if ($self->HaveFile('res')) {
        $dest='res';
      } elsif ($self->HaveFile('crash')) {
        $dest='crash';
      }
      if (defined $dest) {
        _debug("Writing monitor point data to $dest");
        my @files;
        if ($self->HaveFile('mon.old')) {
          push @files, $rundir->file_intpath('mon.old')
        }
        push @files,$rundir->file_intpath('mon');
        $self->TraceLog('insert-mon', $dest, map { basename($_) } @files);
        CFX5::MonFile->insert_into_res($rundir->file_intpath($dest),
                                       @files)
          or undef $dest;
      }
      if (defined $dest) {
        # tried to save it, and it worked
        _debug("OK");
        $rundir->mark_output_file_ignore('mon');
        $self->TraceLog('insert-mon-ok');
      } elsif (!$self->save_resfile()) {
        _debug("No res file for mon");
        $rundir->mark_output_file_ignore('mon');
      } else {
        # it failed, so we'll copy it out at the end of the run
        _debug("Failed");
        $rundir->mark_output_file_leave('mon');
      }
    }

    # Write the parallel job info to the results file
    $self->WriteParInfo();
    
    return $rv->is_success();
  } else {
    return 0;
  }
}

=head4 TransientCleanup

In a transient run, the solver writes files 1.trn, 2.trn, and so on
which we need to save somewhere.  This routine -- called by RunSolve
-- does this cleanup, and also a similar one for '.bak' files.

=cut

sub TransientCleanup {
  my $self=shift;

  #
  # get the working directory object
  #
  my $workdir = $self->rundir_obj();
  return 0 unless (defined $workdir);

  #
  # do we have any transient files (that we don't know what to do
  # with)?
  #
  my @trn;
  my @unknownflist;
  for my $f ($workdir->list_unknown_files()) {
    if ($f =~ m/\.(trn|bak)$/i and $f !~ m!^tmp\.!i) {
     push @trn, "$f";
    } else { 
     push @unknownflist, "$f";
    }
  }

  @trn = reverse sort @trn;

  return 1 unless @trn;

  #
  # Delete the Post monitoring file, and then notice any lock files
  # 
  my $lckflag = 0;
  foreach my $f (@unknownflist) {
    unlink(join_file($workdir->dir(),$f)) if $f =~ /monpost/i ;
    $lckflag = $lckflag || ($f =~ m/\.lck$/i) ;
  }

  #
  # If lock exist, try to move files into the output directory now
  #
  my $fildir;
  if ($lckflag){
    _debug(".lck files found, so moving files before write");
    if ($self->CleanupFiles(@trn)) {
      $self->outobj()->message("The following transient and backup files written by the ANSYS CFX solver have been saved in the directory", $self->outdir().":\n\n".
                             join(", ", @trn));
    }
    $fildir = $self->outdir();
  } else {
    $fildir = $workdir->dir();
  }

  #
  # Process bak and trn files, in their current location
  #
 TRNFILE: for my $f (@trn) {
  
    # Insert monitor data if it exist
    (my $mon=$f) =~ s!(_[a-z]+)?\.[a-z]+!.mon!gi;
    if ($workdir->have_file($mon)) {
      $workdir->notice_file($mon);
      my @files;
      if ($self->HaveFile('mon.old')) {
        push @files, $workdir->file_intpath('mon.old')
      }
      push @files,$workdir->file_intpath($mon);
      $self->TraceLog('insert-mon', $f, map { basename($_) } @files);
      unless (CFX5::MonFile->insert_into_res($workdir->file_intpath($f),
                                             @files)) {
        # this might be because we're out of disc space, in which case
        # (due to an IO module bug) we've just corrupted that file.
        # Make sure we don't corrupt any more!
        _warn("Unable to insert monitor point data into file $f, see the preceding messages for more details.  The remaining files will not be modified.");
        last TRNFILE;
      }
      $workdir->mark_output_file_ignore($mon);
      $self->TraceLog('insert-mon-ok');
    }
    
    # Add resinfo data to trn and bak files
    my $currentConfig = $self->parent()->{currentconfig} ;
    my $simStep  = $self->parent()->{simstep} ; 
    my $confName = $self->parent()->{configmap}{$currentConfig}{name};
    my $confStep = $self->parent()->{configmap}{$currentConfig}{nextstep}-1;
    
    my $pathLevels = ($self->parent()->singleconfig() ? 2 : 3); 
    my $resInfoPath = basename(join_file($fildir,$f),$pathLevels);
    $resInfoPath =~ s/\.dir//gi; 
    $self->resInfo()->AddEntry($simStep,$confStep,$confName,$resInfoPath) ;
    $self->resInfo()->WriteToFile(join_file($fildir,$f)) ;
    $self->resInfo()->ClearEntry($simStep,$confStep,$confName,$resInfoPath); 
  }

  #
  # If we didnt move them earlier, move files into the output directory now
  #
  unless ($lckflag){
    if ($self->CleanupFiles(@trn)) {
      $self->outobj()->message("The following transient and backup files written by the ANSYS CFX solver have been saved in the directory", $self->outdir().":\n\n".
                               join(", ", @trn));
      return 1;
    }
  }

  return 0;

}

=head4 AppendHandledErrors

  $job->AppendHandledErrors();

Append contents of message handler file to output file, if it exists.
Once the contents is successfully appended, the file is deleted.

=cut

sub AppendHandledErrors {
  my $self = shift;

  my $rundir = $self->rundir_obj();
  return 0 unless (defined $rundir);

  my $handled_errors_file = 'handled_errors.txt';
  return 0 unless ($self->HaveFile($handled_errors_file));

  my $fh = $self->outobj()->open();
  if ($fh) {
    my $fullname = join_file($rundir->dir(),$handled_errors_file);
    my $cont = CFX5::FileOps::get_file_contents($fullname);
    if (defined $cont) {
      print $fh $cont;
    } else {
      _error("Error reading file: $handled_errors_file : $!");
    }
    if ($self->outobj()->close()) {
      $rundir->remove_file($handled_errors_file) if (defined $cont);
    } else {
      _error("Cannot finish writing contents of $handled_errors_file to output file: $!");
    }
  } else {
    _error("Cannot write contents of $handled_errors_file to output file: $!");
  }
}

=head4 WriteParInfo

  $job->WriteParInfo();

Write contents of 'pids' file to results file, if both files exist.
Once the contents is successfully written, the 'pids' file is deleted.

=cut

sub WriteParInfo {
  my $self = shift;

  my $rundir = $self->rundir_obj();
  return 0 unless (defined $rundir);

  my $pid_file = 'pids';
  return 0 unless ($self->HaveFile($pid_file));

  my $res_file = 'res';
  return 0 unless ($self->HaveFile($res_file));

  my $res = CFX5::DefFile->new(join_file($rundir->dir(),$res_file));
  if (ref $res) {
    my %ds_sig = (what => 'G/PAR_INFO', where => 'EVERY', when => 0);
    my $rv = $res->store_text_dataset(join_file($rundir->dir(),$pid_file), %ds_sig);
    $rundir->remove_file($pid_file) if ($rv);
  }
}

=head4 _exec_PPE

  $job->_exec_PPE('-par');

Runs the solver executable inside the PPE.

=cut

sub _exec_PPE {
  my $self=shift;
  my @args=@_;

  my $ppe=$self->ppe_obj();
  unless (defined $ppe) {
    internal_error("Running inside PPE before it is created!");
  }

  unless ($self->lets_pretend()) {
    return $ppe->do_cmd(@args);
  }

  # if we're pretending, fall back to the normal mode
  if ($self->write_debug()) {
    $ppe->log_environment($self->debug_obj());
  }

  # dummy stuff
  require CFX5::Simulation::Configuration::Debug;
  return $self->CFX5::Simulation::Configuration::Debug::dummy_solver($ppe->master_host()->actual_executable($ppe->start_method()), @args);
}

1;
