=head1 NAME

  CFX5::Simulation::Configuration::CombDef - Solver Input File Input handling for CFX5::Simulation::Configuration

=head1 SYNOPSIS

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

=head1 DESCRIPTION

$Id$

=cut

package CFX5::Simulation::Configuration::CombDef;

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

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

use CFX5::PathManip qw(dirname basename fullpath join_file split_mult_files
                       normalize_filename is_absolute is_unc);
use CFX5::FileOps qw(create_empty_file create_directory
                     append_to_file set_file_contents copy_file move_file
                     link_files concatenate_files);

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

use CFX5::CCLI;

use CFX5::Commands qw(get_cmd_output);

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

use CFX5::PkgLoader qw(cfx5_require);

use Carp;

=head3 CombDef

"CombDef" is the name I have just made up to refer to the process of
getting the original .def file into the run directory.  This includes
handling initial values files, and ensuring that CCL data is turned
into MMS data correctly.

The designation of this as a component is slightly arbitary, but it
does fit in with a little thought!  It is a separate operation, and
being run only once, has different semantics from both the C<Solve>
and C<Partition> components.

Because the CCL handling may produce output, this component does
require an output file.

None of the other components should have any knowledge of creating
definition files like this.

This component should only be used once, at (or near) the beginning of
a run.  It will give an error if called when either somebody has
C<ProvideFile>d C<'def'>, or there is a file C<'def'> in the working
directory already.

=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 or 2") 
    unless defined $do and ($do eq 0 or $do eq 1 or $do eq 2);

  my $whichCall=shift ;
  unless (defined $whichCall) {
    $whichCall = "Initial" ;
  }
#
#===============================================================================
#  Setup and Validation
#===============================================================================
#
  my $workdir;
#
# Validate whichCall 
#
  my $initCall   = 0; 
  my $remeshCall = 0;
  if    ( $whichCall eq "Initial" ) {
    $initCall = 1; 
  }
  elsif ( $whichCall eq "Remeshing") {
    $remeshCall = 1; 
  }
  else {
    internal_error("Unrecognised call to CombDef: $whichCall")  ; 
  }

#
# Setup the run directory and define the output file
# --> Note run directory is created by accessing rundir_obj()
#
  my $rundir=$self->rundir();
  if ($do) {
    _check_defined $rundir, "working directory" or return 0;
    $workdir = $self->rundir_obj();
    return 0 unless (defined $workdir);
  }
  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;
  }
#
# For the initial call:
# --> Ensure required CCL rules are loaded
# --> Create the out file
#
  if ($initCall) {
    # This will be called by read_def_ccl, which will always have been
    # called by this point.  Apart from, for example, in some of the
    # cut-down testing, where we deliberately skip it ... so just in
    # case, make sure everything is set up.  (We need to do this to read
    # the start methods library, at least).
    $self->ensure_ccl_rules_loaded();

    # 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;
      # Force parameter containing res/par file name to be written to CCL
      $self->resfile_ccl() if ($self->save_resfile);
      $self->parfile_out_ccl() if ($self->save_parfile);
      # Start the output file
      $self->StartOutFile() or return 0;
    }
  }
#
# If the working directory is a UNC path and the shell does not
# support UNC paths, issue a warning.
#
  if ($do and $Config{windows_nt} and is_unc($rundir))
  {
    unless ($Config{unc_shell_support})
    {
      my $msgtxt = <<'!EOW!';
The working directory is a UNC path, but the Windows command shell is
currently not configured to support UNC paths. This may cause failure
of the CFX Solver.

Command shell support for UNC paths can be enabled by setting the Windows
registry key DisableUNCCheck.
!EOW!
      _warn($msgtxt)
    }
  }
#
#-------------------------------------------------------------------------------
# Input File Handling
#-------------------------------------------------------------------------------
#
  my ($deffile,@iniFiles) ; 
#
  if    ($initCall) {
    if ($self->HaveFile('def')) {
      internal_error("There is already a definition file, refusing to create a new one") 
    }
    
    $deffile     = $self->deffile() ;
    if ($self->parent()->singleconfig()) {
      @iniFiles    = $self->GetIniFiles() ; 
    } else  {
      @iniFiles    = $self->GetIniFiles() unless $self->parent()->norun() ; 
    }
  }
  elsif ($remeshCall) {
    
    # Replace def and ini files
    $deffile     = $self->defRemesh();
    push (@iniFiles,$self->resRemesh()); 
  }

  # Make the files we expect to exist actually do
  _check_defined $deffile, "definition file" or return 0;
  CFX5::FileTests::datafile($deffile, "ANSYS CFX Solver Input File") or return 0;

  foreach my $file (@iniFiles) {
    unless (CFX5::FileTests::datafile($file, "A valid Initial Values file is required for the run. ANSYS CFX Initial Values file $file")) {
      return 0 unless ($self->parent()->check_for_gui());
      reset_error_indicator();
    }
  }

  # Multiple initial values cannot be used with a def file earlier than CFX-5.4
  my $def=new CFX5::DefFile($deffile);
  my $defv=$def->cfx5_version();
  
  if (scalar(@iniFiles) > 1 and $defv < 4.00 ) {
    _error("You cannot use a CFX-5.3 definition file with multiple initial values files.");
    return 0;
  }

  # Check def file is compatible with initial values file(s)
  my $bothFilesV53 = 0 ; 
  foreach my $iniFile (@iniFiles) {
    if (test_f $iniFile) {
      my $ini=new CFX5::DefFile($iniFile);
      my $iniv=$ini->cfx5_version();
      if    ($defv < 4.00 and $iniv < 4.00) {
        $bothFilesV53 = 1 ;
      } 
      elsif ($defv < 4.00 ) {
        _error("You cannot use a CFX-5.3 solver input file with the CFX-5.4 initial values file: $iniFile");
        return 0;
      }
    }
  }

  #
  # if we're going to need it later, we check/start the parallel
  # environment here, so we get errors sooner.
  # Check the Ansys solver settings too
  #
  $self->CheckAnsys() or return 0;
  $self->CheckCCL2MF() or return 0;
  if ($do) {
    # we won't start it until after we've displayed the CCL, but we
    # will check the architectures etc of the hosts in the list
    $self->InterrogatePPE() or return 0;
  } else {
    $self->CheckPPE() or return 0;
  }

  #
  # if we're not going to actually do anything, this is all we need to do
  #
  $self->ProvideFile('def');
  return 1 unless $do;

  #
  # if we're here for real, we'll do whatever needs doing to the CCL
  # before trying to, eg, start the parallel environment.
  #
  # We always have CCL now, even for 5.3 runs; the difference is that
  # for 5.3 runs, it's not got any physics in it.
  #

  #
  # Log any custom data files
  #
  if(defined $Config{CFX_CUSTOM_DATA_DIR}){
    $self->OutFileHeader("Custom Data Files");
    $self->outobj->print("Location:    ".$Config{CFX_CUSTOM_DATA_DIR});
    $self->outobj->print("Materials:   ".$Config{CFX5_MATERIALS_DIR});
    $self->outobj->print("Reactions:   ".$Config{CFX5_REACTIONS_DIR});
    my @ccllibs=split(/$Config{path_sep}/, $Config{CFX5_CCL_LIBS});
    $self->outobj->print("CCL RULES:   ".$ccllibs[0]);
    $self->outobj->print("EXECRULES:   ".$ccllibs[1]);
    $self->outobj->print("VARIABLES:   ".$ccllibs[2]);
    $self->outobj->print("TT FILE:     ".(defined $Config{CFX5_TT_FILE} ? $Config{CFX5_TT_FILE} : $Config{CFX5_TTPROC_FILE}));

    # Not controled directly by the scripts.
    # Just figuring out which units ccl2flow should pickup based
    # on the logic used in the source (ignoring files in current direcotry)
    $self->outobj->print("UNITS:       ".(-f join_file($Config{CFX_CUSTOM_DATA_DIR}, 'units.cfx') ? join_file($Config{CFX_CUSTOM_DATA_DIR}, 'units.cfx')  : join_file($Config{CFX_DATA_DIR}, 'units.cfx')));
    $self->outobj->print("COMMONUNITS: ".(-f join_file($Config{CFX_CUSTOM_DATA_DIR}, 'common_units.cfx') ? join_file($Config{CFX_CUSTOM_DATA_DIR}, 'common_units.cfx')  : join_file($Config{CFX_DATA_DIR}, 'common_units.cfx')));
  }
 
  #
  # Fix up the physics CCL for the new version of CFX-5, if
  # necessary.  TODO: at a later date, this is where we want to
  # validate the physics CCL too.
  #
  my $cclupdatelogobj = $self->cclupdatelog();
  if ($cclupdatelogobj->have_data()) {
    $self->OutFileHeader('CFX Command Language Upgrade');
    while (my $text = $cclupdatelogobj->get_next()) {
      _trace('Individual CCL Upgrade for Configuration');
      $self->outobj->print($text);
    }
  }
 
  # Run a ccl upgrade anyway, the user may have specified ccl on the command line
  $self->update_physics_ccl()
    or return 0;

  #
  # Check existence of any profile data files and update relative file paths
  #
  $self->init_profile_data() or return '';

  #
  # Check existence of any user location data files and update relative file paths
  #
  $self->init_userlocation_data() or return '';

  #
  # record the version of the software used for this run.
  #
  {
    # strip off leading non-numeric chars, and then all chars after
    # the first trailing non-numeric char
    (my $basever=$Config{CFXBUILDID_MAJOR_MINOR}) =~ s!^[^0-9.]+!!gi;
    $basever =~ s![^0-9.].*$!!;
    $self->set_ccl_parm('/COMMAND FILE/Results Version', $basever);
  }

  $self->start_ansys();
  $self->run_mode();
  $self->RunCCL2MF() or return 0;

  $self->RunAnsys() or return 0;

  return 1 unless $self->prep_cfx();

  if ($initCall) {
    $self->OutFileHeader("CFX Command Language for Run");

    # add it to the out file.  We must do this after interrogating the
    # PPE above in order to get the filled-in host definitions.
    my $outobj=$self->outobj();
    if (my $fh=$outobj->open()) {
      print $fh map { " $_\n" } ($self->get_current_ccl(), '');
      $outobj->close();
    }
  }
  
  $self->validate_full_ccl()
    or return 0;

  $self->PreparePPE() or return 0;

  #
  # OK, check there isn't already a file called def in the working
  # directory
  #
  if ($workdir->have_file('def')) {
    _error("A definition file already exists in the working directory,\nrefusing to create a new one");
    return 0;
  }

  #
  # If we have an initial values file, merge it with the definition
  # file.
  #
  # See the comments in the TODO section about getting CCL datasets
  # from ini files.
  #
  if (defined $iniFiles[0]) {
  
  #
  # First report any initial values files that are derived from configuration results
  $self->ReportIniFilesFromConfigs() unless $remeshCall;
#
#===============================================================================
# All file versions < 5.4
#===============================================================================
#
    if (scalar(@iniFiles)==1 and $bothFilesV53) {
      # both 5.3: we concatenate them
      my $newfile=$workdir->creatable_file('def');
      unless (concatenate_files($iniFiles[0], $deffile => $newfile)) {
        _error("Unable to merge initial values file with definition file: $!");
        return 0;
      }        
      # we want to ignore this file, we don't care about it
      $workdir->mark_output_file_ignore('def');

      my $newdef=new CFX5::DefFile($newfile);

      _trace("New file has version 5." . $newdef->cfx5_version());

    } 
#
#===============================================================================
# Def or Ini file with version >= 5.4
#===============================================================================
#
    else {
#
#-------------------------------------------------------------------------------
# No Interpolation (and reinitialize simulation)
# --> Always interpolate if remeshing 
# --> Transfer control data from def file to ini file & run with ini file mesh
#-------------------------------------------------------------------------------
#
      if ($self->use_iv_mesh() && !$remeshCall) {
        # Cannot copy the physics if multiple initial values files
        # Stop and warn the user to interpolate.
        if (scalar(@iniFiles) > 1) {
          _error("You must interpolate multiple initial values files onto the definition file.");
          return 0;
        }
        # use the mesh from the Initial Values file.  This is the old
        # way that 5.6 and previous worked -- just copy the physics
        # information across.

        $self->QueryIniFile() || return 0;

        # we copy some datasets from the def file
        $workdir->input_file_copy('def' => $iniFiles[0]);
        # we want to ignore this file, we don't care about it once
        # it's done with
        $workdir->mark_output_file_ignore('def');

        # now, copy the requisite datasets
        my $intdef=new CFX5::DefFile($workdir->file_intpath('def'));
        my $count=$def->copy_datasets($intdef,
                                      map { [ $_, '*', 0 ] } 
                                      qw(G/COMMANDS G/ADAPT/ADAPTING  
                                         G/ADAPT/ADAPTING_STATE
                                         G/ADAPT/ADAPTING_TO_GEOMETRY
                                         G/ADAPT/SAVE_INTERMEDIATE_FILES
                                         G/ROTAXIS
                                         M/REFINER/PARAMETERS/BUILD
                                         G/ADAPT/SOLVER_PARAMETERS
                                         G/PRESTATE
                                        ));
        unless ($count) {
          _error("No simulation control datasets were transferd to the initial ".
                 "values file.  Either none of the expected datasets were ".
                 "present, or an error was encountered during the copy.");
          return 0;
        }
      }
#
#-------------------------------------------------------------------------------
# Interpolation - Either reinit. or continue sim. using def file mesh
#-------------------------------------------------------------------------------
#
      else {

        if ($initCall) {
          $self->OutFileHeader("Interpolation of Initial Values");
        }
        else {
          $self->OutFileHeader("Interpolation of Values to New Mesh");
        }

        $workdir->checkpoint("Before interpolation setup");
#
# Figure out the interpolator executable and make sure it exists
#
        # Check if the interpolator executable
        my $interp_exec=$self->interpolator_exec();

#
# Setup command line options
#      
        # Base interpolator arguments
        my @interp_args ;
        push  @interp_args,  '-interp', '-stdout' ;

        # Conditionally enable run continuation mode
        if ($self->continue_history() or $remeshCall) {
          push  @interp_args,  '-rest' ;
        }

        # Record number of initial values files  
        push @interp_args, '-nini', scalar(@iniFiles) ;

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

        # Memory factor
        if (defined $self->memory_factor_interp()) {
          push @interp_args, '-s', $self->memory_factor_interp();
        }

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

        # 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}_interp";
          if (defined (my $f=$self->$m())) {
            if ($f =~ s!x$!!i) {
              # "1.5x" actually maps to a different interpolator argument
              (my $a=$t) =~ s!^n!s!;
              push @interp_args, "-$a", $f;
            } else {
              if ($f =~ m/^\d+[KkMm]?$/) {
                push @interp_args, "-$t", $f;
              } else {
                _error("Invalid syntax for parameter /SIMULATION CONTROL/EXECUTION CONTROL/INTERPOLATOR STEP CONTROL/MEMORY CONTROL/$pairs{$t} Memory Override = $f");
                return 0;
              }
            }
          }
        }

#
# Copy def file for use by the interpolator
# --> mark file to be ignored later
#
        unless ($workdir->input_file_copy('cfx5.def' => $deffile)) {
          _error("Required file $deffile not found.") ;
          return 0;
        }
        $workdir->mark_output_file_ignore('cfx5.def') ;

#
# Copy (or possibly link) initial values files
# --> local copies are named cfx5_###.ini for convenience in the 
#     interpolator (could have used direct paths w. cmd line args.)
#      
        my $count = 0;
        foreach my $file (@iniFiles) {
          # increase the file count.
          $count++ ;
          my $cfx5ini = sprintf("cfx5_%03d.ini",$count);
          unless ($workdir->input_file($cfx5ini => $file) or 1) {
            _error("Required file, $file, not copied to $cfx5ini") ;
            return 0;
          }
        }

#
# Create mod.dat file to tell the interpolator what to do
# --> we always want to "interpolate"; "differencing" makes no sense!
# --> mark file to be ignored later
#
        my $name=$workdir->creatable_file('mode.dat');
        set_file_contents($name, "interpolate\n");
        $workdir->mark_output_file_ignore('mode.dat');
#
# create a ccl file for ccl2flow to read later.  This file is
# currently not used, and will simply be overwritten, but at a
# later date ccl2flow should be modified to pick it up and use
# it instead of the CCL from G/COMMANDS.
#
        my $f=$workdir->creatable_file('cfx5interp.ccl');
        my @ccl=$self->get_current_ccl();
        # what if the file already exists?  We unlink it just in case,
        # lacking a write_file call.
        unlink($f);
        append_to_file($f,map { "$_\n" } @ccl);
#
# Execute the interpolator
#
        $workdir->checkpoint("Before interpolation");
        my $res=$self->run_and_redirect($interp_exec, 'interpolation',@interp_args);
        unless ($res->is_success()) {
          _error("Error interpolating results onto the new mesh:", $res->errstr());
          return 0;
        }
        $workdir->checkpoint("After interpolation");
#
# Post interpolation cleanup
#
        # First rename the definition file back to 'def'
        $workdir->rename_file('cfx5.def' => 'def')
          or return 0;

        # Now delete the initial values files
        $count = 0 ; 
        foreach my $file (@iniFiles) {
          $count++ ;
          my $cfx5ini = sprintf("cfx5_%03d.ini",$count);
          $workdir->remove_file($cfx5ini);
        }

        $workdir->checkpoint("After interpolation tidyup");
      }
#
#-------------------------------------------------------------------------------
# Post processing 
#-------------------------------------------------------------------------------
#
# Single Ini file with version < 5.4
#
      if (scalar(@iniFiles) == 1) {
        my $ini=new CFX5::DefFile($iniFiles[0]);
        if ($ini->cfx5_version() < 4.00 and 
            not $ini->have_dataset(what => 'G/NAMEMAP')) {
          #
          # a 5.3 initial values file has no G/NAMEMAP dataset.  If this
          # is indeed the case (not sure why it might not be, but I
          # guess the user might want to provide this herself), we can
          # create one for them.  Otherwise, ccl2flow mis-generates it,
          # and all the boundaries end up in the wrong places!
          #
          _note "Generating new name map for 5.3 results file";
          # get the name map
          my @namemap=$ini->create_namemap_53();
          return 0 unless @namemap;
          # make a dataset for it.  The easiest way is to start with the
          # one from the def file, and replace the data!
          unless ($def->have_dataset(what => 'G/NAMEMAP', when => 0)) {
            _error("The CFX-5.4 definition file does not contain a name map");
            return 0;
          }
          my $ds=$def->read_dataset(what => 'G/NAMEMAP', when => 0);
          defined $ds or return 0;
          $ds->data(@namemap);
          # now write the namemap into the new def file.  We know it's
          # not there, we checked that above.
          my $intdef=new CFX5::DefFile($workdir->file_intpath('def'));
          $intdef->write_dataset($ds);
          # if the new file has no TOPCAT, write it there too
          unless ($intdef->have_dataset(what => 'G/TOPCAT', when => 0)) {
            $ds->header(what => 'G/TOPCAT');
            $intdef->write_dataset($ds);
          }
        }
      }
#
# User has requested that we NOT continue run history
#
      if (not ($self->continue_history() or $remeshCall)) {
        my $intdef=new CFX5::DefFile($workdir->file_intpath('def'));
        foreach my $ds (qw(G/MON
                           G/TRANSIENT
                           G/RES_INFO
                           S/TIMESTEPS)) {
          $intdef->delete_dataset(what => $ds)
        }
      }
    } 
  }  
  else {
    # make a new def file from the old one only 
    $workdir->input_file_copy('def' => $deffile);
    # we want to ignore this file, we don't care about it
    $workdir->mark_output_file_ignore('def');
    ## link the old def file
    #$workdir->input_file('def' => $deffile);
  }

  return 0 if error_pending();
  if (!$remeshCall) {

    #
    # Try to extract mon data so the SM can start showing graphs
    # --> Not required for remesh calls because a mon file already exists
    #
    # we always create it, even if it's going to be empty ... we write
    # it to a different name and then rename it, so the SM should be
    # less likely to pick it up half way through the write.
    my $monold=$workdir->creatable_file('mon.old');
    my $montmp=$workdir->creatable_file('mon.old.tmp');
    if ($Config{CFX5_SOLVE_NO_OLD_MONFILE}) {
      set_file_contents($montmp, '');
    } elsif ($Config{CFX5_SOLVE_OLD_MON_HANDLING}) {
      my @mon=map {
        "$_\n"
      } CFX5::MonFile->read_from_res($workdir->file_intpath('def'));
      set_file_contents($montmp,@mon);
    } else {
      CFX5::MonFile->extract_from_res($workdir->file_intpath('def'), $montmp)
          or _error("Error extracting monitor point data from definition file");
      unless (-f $montmp) {
        set_file_contents($montmp, '');
      }
    }
    rename($montmp, $monold) 
      or _error("Error renaming $montmp to $monold: $!");
    $workdir->mark_output_file_ignore('mon.old');
    $self->TraceLog("write-mon-old");
  }
  #
  # CCL processing if necessary
  #
  if ((my $intdef=new CFX5::DefFile($workdir->file_intpath('def')))
      ->cfx5_version() < 4.00) {
    # 5.3 file, do nothing
    1;
  } else {
    #
    # now we've gone through all the CCL updates etc, we're ready to
    # write it back into the def file, and process it, etc.  Before
    # that, check for people running from mesh-only mode definition
    # files from 5.5.1, which contain an empty G/COMMANDS dataset.
    # This should be allowed, so long as they provide the CCL
    # themselves.
    #
    # TODO: we should do this earlier, but it's safer here.  Move it
    # for 5.7.  (Or delete it again, 5.6 doesn't produce .def in
    # mesh-only mode.)
    #
    if ($self->deffile_no_ccl() 
        and not $self->ccl_api_obj()->haveObject("/".$self->flow_name())) {
      # don't mention the possibility of doing the CCL themselves,
      # it'll only confuse them.
      _error("The ANSYS CFX Solver Input File", $self->deffile(),
             "contains no physics information.  Definition files from",
             "CFX-Build Mesh Only Mode cannot be run directly in",
             "the solver.");
      return 0;
    }

    #
    # we always write the CCL into the new definition file if it's got
    # CCL in already.  This is safe, as we currently always copy it
    # anyway, as we're going to write the MMS dataset in as well - if
    # this is ever changed (which is looking more and more unlikely)
    # this part should probably be revisited too.
    #
    my @ccl=$self->get_current_ccl();
    CFX5::CCLI::writeccl($workdir->file_intpath('def'), \@ccl)
        or return 0;

    if ($self->process_ccl() ne 'never' and $do and $do == 2) {
      # get it from this def file, write it to this def file, put any
      # output into the out file, and don't bother deleting the files.
      my $rv;

      # we don't give a header, as the process is usually quiet ...
      #$self->OutFileHeader("CFX Command Language Setup");

      $self->RunningComponent('ccl');

      if ($self->lets_pretend()) {
        $rv=1;
      } else {
        # create a ccl file for ccl2flow to read later.  This file is
        # currently not used, and will simply be overwritten, but at a
        # later date ccl2flow should be modified to pick it up and use
        # it instead of the CCL from G/COMMANDS.
        {
          my $f=$workdir->creatable_file('ccl');
          my @ccl=$self->get_current_ccl();
          # what if the file already exists?  We unlink it just in case,
          # lacking a write_file call.
          unlink($f);
          append_to_file($f,map { "$_\n" } @ccl);
        }

        # we know these will be created
        for my $f (qw(cfx5.tt mms cfx5.mms mms.setup mms.setup.attrb)) {
          $workdir->creatable_file($f);
        }

        $workdir->checkpoint('before ccl2flow et al');

        setconf('CFX5_CCL2FLOW_EXEC',$self->ccl2flow_exec());
        setconf('CFX5_CCLSETUP_EXEC',$self->cclsetup_exec());

        $rv=CFX5::CCLI::prepare_def_file($workdir->file_intpath('def'),
                                         $workdir->file_intpath('def'),
                                         fullpath($self->outfile()),
                                         0,$remeshCall);

        $workdir->checkpoint('after ccl2flow et al');
      }

      $self->RunningComponent('');

      # check that we found CCL if we have to
      if ($self->process_ccl() eq 'always' and $rv < 0) {
        _error("Definition file appears not to contain CCL, but CCL processing\nhas been forced on:\n\t$deffile");
        return 0;
      } elsif (!$rv) {
        # an error has occurred while processing CCL.  The CCLI
        # routines should have signalled an error anyway, but just in
        # case we check, and give an error if one is needed.
        unless(error_pending()) {
          _error("An error was encountered in CCL processing.");
        }
        # in any case, we've failed ...
        return 0;
      }
    }
  }

  #
  # if necessary, store profile data in the def file, and print it to
  # the outfile if it's small.
  #
  $self->store_profile_data()
    or return '';

  #
  # If part of remesh call, then we can migrate the oldmesh.res file 
  # into the final directory along w. the other trn/bak files
  # --> the newmesh.def file is not handled here, although it could be
  #     (but this would need some more cleanup in other places)
  #
  if ($remeshCall) {
    $self->CleanupFiles(basename($self->resRemesh())) ;
    $self->CleanupFiles(basename($self->outRemesh())) ;
  }

  #
  # that's it
  #
  1;
}

=head3 get_userlocation_list

Return a hash of User locations whose values are the
location file names.

=cut

sub get_userlocation_list {
  my $self=shift;

  my $api=$self->ccl_api_obj();

  my $root='/LIBRARY/USER LOCATION DEFINITIONS';

  return () unless $api->haveObject($root);

  my @kids;
  my $rv=$api->getChildObjectList($root, \@kids);
  if (defined $rv) {
    _error("Error finding children of $root:",
           $self->format_ccl_error($rv));
    return ();
  }

  my %userlocation_files;
  for my $func (grep(m/^USER SURFACE:/, @kids)) {
    if ($api->haveParameter("$root/$func/Input File")) {
      my $file;
      my $rv=$api->getStringValue("$root/$func/Input File", \$file);
      if (defined $rv) {
        _error("Error retrieving parameter $root/$func/Input File from the Command Language database:",
               $self->format_ccl_error($rv));
        return ();
      }

      # since the values are user-supplied, there might be multiple
      # versions of the same filename, eg with different separators.
      $file=normalize_filename($file);

      $userlocation_files{$func} = $file;
    }
  }

  return %userlocation_files;
}

=head3 init_userlocation_data

Make sure the User Location files exist and for any relative paths, change the file
name in the CCL to use an absolute path, so that ccl2flow can find the files.
Report any changes in a similar way to a CCL upgrade.

=cut

sub init_userlocation_data {
  my $self=shift;
  my %userlocation_files = $self->get_userlocation_list();
  my $root='/LIBRARY/USER LOCATION DEFINITIONS';

  for my $func (keys %userlocation_files)
  {
    my $file = $userlocation_files{$func};

    if (-f $file)
    {
      unless (is_absolute($file))
      {
        my $file_abs = fullpath($file);
        $self->set_ccl_parm("$root/$func/Input File", $file_abs);
        $self->outobj()->print("Changed file path for User Location definition $root/$func from relative path: $file to absolute path: $file_abs\n\n");
      }
    }
    else
    {
      _error("Error opening User Location definition file $file: $!");
      return '';
    }
  }
 
  1;
}


=head3 get_profile_list

Return a hash of profile function names whose values are the
profile file names.

=cut

sub get_profile_list {
  my $self=shift;

  my $api=$self->ccl_api_obj();

  my $root='/LIBRARY/CEL';

  return () unless $api->haveObject($root);

  my @kids;
  my $rv=$api->getChildObjectList($root, \@kids);
  if (defined $rv) {
    _error("Error finding children of $root:",
           $self->format_ccl_error($rv));
    return ();
  }

  my %profile_files;
  for my $func (grep(m/^FUNCTION:/, @kids)) {
    if ($api->haveParameter("$root/$func/File Name")) {
      my $file;
      my $rv=$api->getStringValue("$root/$func/File Name", \$file);
      if (defined $rv) {
        _error("Error retrieving parameter $root/$func/File Name from the Command Language database:",
               $self->format_ccl_error($rv));
        return ();
      }

      # since the values are user-supplied, there might be multiple
      # versions of the same filename, eg with different separators.
      $file=normalize_filename($file);

      $profile_files{$func} = $file;
    }
  }

  return %profile_files;
}

=head3 init_profile_data

Make sure the profile files exist and for any relative paths, change the file
name in the CCL to use an absolute path, so that ccl2flow can find the files.
Report any changes in a similar way to a CCL upgrade.

=cut

sub init_profile_data {
  my $self=shift;
  my %profile_files = $self->get_profile_list();
  my $root='/LIBRARY/CEL';

  for my $func (keys %profile_files)
  {
    my $file = $profile_files{$func};

    if (-f $file)
    {
      unless (is_absolute($file))
      {
        my $file_abs = fullpath($file);
        $self->set_ccl_parm("$root/$func/File Name", $file_abs);
        $self->outobj()->print("Changed file path for profile function $root/$func from relative path: $file to absolute path: $file_abs\n\n");
      }
    }
    else
    {
      _error("Error opening profile data file $file: $!");
      return '';
    }
  }
 
  1;
}

=head3 store_profile_data

Check for profile data, and if we have any, include the details in
the input definition file.

=cut

use CFX5::Config::def CFX5_PROFILE_OUTPUT_SIZE => sub { 16*1024 };

sub store_profile_data {
  my $self=shift;

  # A profile file may be referenced by multiple profile functions.
  # Create a unique list of profile files.
  my %profiles = $self->get_profile_list();
  my %profile_files = reverse %profiles;
  return 1 unless keys %profile_files;

  #
  # OK, now we know there's something to do.  We will do two things:
  #  * store the file in the res file
  #  * output a summary of the file to the output file
  #

  my $have_outfile_header = 0;
  my $outfile=$self->outobj();
  my $deffile=new CFX5::DefFile($self->rundir_obj()->file_intpath('def'));

  cfx5_require('CFX5::DefFile::ProfileInterface') or return '';
  my $profile_int=CFX5::DefFile::ProfileInterface->new($deffile);

  # remove existing ones, so that on a restart we end up with just the
  # new ones.
  $profile_int->delete_all();

  my $isok=1;
  for my $file (keys %profile_files) {

    unless ($have_outfile_header) {
      $self->OutFileHeader("Profile Data");
      ++$have_outfile_header;
    }
    #
    # write file name, size and a limited file contents to the output file
    #
    local *FILE;
    if (open FILE, "<$file") {
      my $fs = -s $file;
      my $unit;
      for my $u ('bytes','kB','MB','GB','TB')
      {
        $unit = $u;
        last if ($fs < 1024);
        $fs /= 1024;
      }
      my $unitfmt = ($unit eq 'bytes' ? '%d' : '%.1f');
      my $header = sprintf ("File size: $unitfmt %s. Summary shown.", $fs, $unit);
      $outfile->output('print','info',
		       "Profile Data File: $file",
		       $header);
      my $contents;
      read FILE,$contents,$Config{CFX5_PROFILE_OUTPUT_SIZE};
      close FILE;
      # at least the example file has lots of spurious ^Ms, and some
      # which are meant to be ends-of-lines.  Looks like dodgy
      # writing code.  So tidy up -- CRs with LFs are squished, CRs
      # on their own become LFs.  On Windows, we will then print in
      # text mode, which will replace CRLF pairs where necessary.
      $contents =~ s!\r*\n\r*!\n!g;
      $contents =~ s!\r+!\n!g;

      my $fh=$outfile->open();
      my $i = 0;

      # Truncate to 70 characters. If truncated, end in ...
      # Print whole fields only
      for my $line (split '\n', $contents) {
        my $pos = length($line);
        if ($pos > 70) {
          while ($pos > 65) {
            $pos = rindex($line, ',', $pos-1);
            if ($pos < 0) { $pos = 65; }
          }
          printf $fh " %s ...\n", substr($line,0,$pos+1);
        } else {
          printf $fh " %s\n", $line;
        }

        # Limit number of lines printed, indicating if incomplete
        if (++$i > 20)
        {
          printf $fh " ...\n ...\n";
          last;
        }
      }
      $outfile->close();

      # and write it into the def file
      $profile_int->insert_file($file)
        or $isok='';

    } else {
      _error("Error opening profile data file $file: $!");
      $isok='';
    }
  }

  return $isok;
}

=head3 run_and_redirect

  my $rv=$job->run_and_redirect($exec, $component_name, @args);
  unless ($rv->is_success()) {
    ...
  }

Runs the specified executable, within the working directory, and with
its output redirected to the output file for the run.  The
$component_name is the bin to which the CPU time will be allocated.

The return value is the CFX5::Commands::Result object representing the
return status.

=cut

sub run_and_redirect {
  my $self=shift;

  my $exec=shift;
  my $component=shift;
  my @args=@_;

  cfx5_require('CFX5::Commands') or return undef;

  $self->RunningComponent($component);

  my $rundir=$self->rundir_obj();
  $rundir->enter();

  # this isn't nice, but it fits in well with the rest of the
  # code.  We have to ensure that _exec doesn't try to
  # print any messages which should go to the screen.

  local (*SAVEDOUT,*SAVEDERR);
  if (CFX5::Output::have_console()
      or CFX5::Output::have_stdout_file()) {
    # if we have any FDs, save them.
    open SAVEDOUT, ">&STDOUT" 
      or _warn "Cannot dup STDOUT";
    open SAVEDERR, ">&STDERR"
      or _warn "Cannot dup STDERR";
  }
  open STDOUT, ">>".$self->outfile()
    or _warn "Cannot redirect STDOUT";
  open STDERR, ">&STDOUT"
    or _warn "Cannot redirect STDERR";

  # it appears that (with Perl 5.8.0?) we have to send something
  # to the output file after messing around with FDs, to realign
  # the OS position with the Perl one, or something.  Otherwise,
  # output from the refiner overwrites the top of the output file.
  print " ";

  my $rv=CFX5::Commands::parse_rv(CFX5::Commands::do_cmd_quiet($exec, @args));

  if (CFX5::Output::have_console()
      or CFX5::Output::have_stdout_file()) {
    open STDERR, ">&SAVEDERR"
      or _warn "Cannot restore STDERR";
    open STDOUT, ">&SAVEDOUT"
      or _warn "Cannot restore STDOUT";
    close SAVEDERR
      or _warn "Cannot close saved STDERR";
    close SAVEDOUT
      or _warn "Cannot close saved STDOUT";
  } else {
    # they were closed to start with, close them again
    close STDOUT
      or _warn "Cannot close STDOUT";
    close STDERR
      or _warn "Cannot close STDERR";
  }
  $rundir->leave();
  $self->RunningComponent('');

  $rv;
}

=head4 _RunCheckIV

Runs the CheckIV program from the old Solver Manager with the names of
the current definition and initial values files, and returns a hash
representing the output.

  my %rv=$self->_RunCheckIV();

This call should not be used to get or set partitioning information.

The keys in the hash are as follows.

=over

=item ini_file

This can be set to "Require", "Allow" or "None".  If "Require", the
definition file is missing something fundamental, and will not run on
its own.  If "Allow", the definition file may use an initial values
file if one is supplied, but if no file is supplied the problem will
start OK.  If "None", an initial values file should not be supplied.

Note that there is currently no way to obtain "None" in this parameter
with a 5.4 definition file.  The old Solver Manager would give this
behaviour if a 5.4 definition file contained a G/MMS dataset, but I
can find no design for this behaviour, and it seems strange to me.  In
future, this parameter may be set to "None" if, for example, all
variables are set to "VALUE" or "DEFAULT" in the CCL.  It is still
possible with 5.3 files if they have no datasets with HOW=FILE (?).

=item adapt_to_geometry

This item will be set to TRUE if the definition file has adaption
switched on, or FALSE if it is switched off.

=item adapt_db_def

If present, this will be set to the default database for adaption read
from the definition file.  Note that this will be set whether or not
the database exists, and may be set regardless of the setting of
adaption in the definition file.

=item error_list

If present, this will point to an anonymous array of error messages.

=item warning_list

If present, this will point to an anonymous array of warning messages.

=back

=cut

sub _RunCheckIV {
  my $self=shift;

  my @args;

  my $deffile=$self->deffile();
  internal_error("No definition file has yet been specified")
    unless defined $deffile;
  push @args, -inputfile => $deffile;

  my $ename='CheckIV.exe';
  my $exec=join_file($Config{CFX5EXECDIR},$ename);
  CFX5::FileTests::execfile($exec, "CheckIV executable $exec")
      or return ();

  my @out=get_cmd_output($exec, @args);

  my @iniFiles=$self->GetIniFiles();
  if (scalar(@iniFiles) == 1 and
      not $self->interpolate_iv_int()=~m!yes|on|1!i) {
    # if we've got an initial values file, and we aren not interpolating
    # we also check to see that this file is compatible. 
    # The old SM only ever used to do one or
    # the other, but it would presumably cache the information in
    # between so that when it got an initial values file, it
    # remembered whether or not it was required.  Here we don't have
    # that luxury.
    push @args, -optionfile => $iniFiles[0];
    push @out, get_cmd_output($exec, @args);
  }

  my %rv;

  for my $line (@out) {
    if ($line =~ m!Require Initial Values File *= *(yes|no)!) {
      $rv{smr_ini_file}=($1 eq 'yes' ? 1 : 0);
    } elsif ($line =~ m!Mesh *= *(yes|no)!) {
      $rv{smr_mesh}=($1 eq 'yes' ? 1 : 0);
    } elsif ($line =~ m!Version *= *(\d+)!) {
      $rv{smr_version}=$1;
    } elsif ($line =~ s!^ *Warning: *!!) {
      push @{$rv{warning_list}}, $line;
    } elsif ($line =~ s!^ *Error: *!!) {
      push @{$rv{error_list}}, $line;
    } elsif ($line =~ m!^ *database *= *$!) {
      # an empty database= line means that we are adapting to
      # geometry, but no database was specified.
      $rv{adapt_to_geometry}=1;
    } elsif ($line =~ m!^ *database *= *(.*)$!) {
      (my $db=$1) =~ s![\s\r\n]*$!!;
      $rv{adapt_db_def}=$db;
      $rv{adapt_to_geometry}=1;
    } elsif ($line =~ s!^ *DBG: *!!) {
      _debug("From CheckIV: $line");
    } else {
      _warn("Unrecognised output $line from CheckIV, ignoring");
    }
  }

  # composite (ie, sensible ;-) values based on the output from
  # CheckIV
  if (exists $rv{smr_mesh} and !$rv{smr_mesh}) {
    $rv{ini_file}='Require';
  } elsif ($rv{smr_ini_file}) {
    $rv{ini_file}='Allow';
  } else {
    # if an initial values file and a res file are passed, it returns
    # (not unreasonably) "None".  This is not what we want, though.
    #$rv{ini_file}='None';
    $rv{ini_file}='Allow';
  }

  %rv;
}

=head3 QueryIniFile

  $self->QueryIniFile();

Interrogates the currently set-up set of files, with regard to the
necessity or not of an initial values file.  The return value is false when
an error occurs. Any errors/warnings are displayed in the outfile.

=cut

sub QueryIniFile {
  my $self=shift;

  my %info=$self->_RunCheckIV();

  if (exists $info{warning_list}) {
    for my $w (@{$info{warning_list}}) {
      $self->outobj()->warn($w);
    }
  }
  if (exists $info{error_list}) {
    for my $w (@{$info{error_list}}) {
      $self->outobj()->error($w);
    }
  }

  return not exists $info{error_list};
}

1;
