=head1 NAME

  CFX5::Simulation::Configuration::Partition - 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::Partition;

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;

=head1 Partition

=cut

sub _Do {
  my $self=shift;
  
  return 1 unless $self->prep_cfx();

  # 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;

  export_conf(qw(CFX_DATA_DIR));

  #
  # what do we need for the Partitioner?
  #
  # * a working directory
  # * an out file
  # * an input def file
  # * possibly an output par file
  # * either
  #    o an input par file
  #    o a number of partitions
  # * the partitioner executable
  #

  # we check the executable first, before we bother making directories
  # or anything
  my $exec=$self->partitioner_exec();
  my $name=$self->partitioner_exec_name();
  # we could use _check_defined but the error message is slightly
  # misleading
  unless (defined $exec) {
    if (defined $Config{CFX5ARCH}) {
      _error("A $name executable is required for this run, but none can be found.  You seem to have CFX5ARCH set to \"$Config{CFX5ARCH}\": if this setting is required, ensure that it is correct.");
    } elsif ($name=~'int64') {
      _error("A $name executable is required for this run, but none is available.  Please be aware that the large problem partitioner is only supported on 64bit platforms.");
    } else {
      _error("A $name executable is required for this run, but none is available.");
    }
    return 0;
  }
  my $xtra='';
  if (defined $Config{CFX5ARCH} and $self->partitioner_exec_arch eq $Config{CFX5ARCH}) {
    $xtra=qq( (forced by the CFX5ARCH=$Config{CFX5ARCH} environment variable));
  }
  CFX5::FileTests::execfile($exec, "Executable $exec for the $name$xtra") or return 0;

  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, "partitioner output file" or return 0;
    CFX5::FileTests::datafile_w($outfile, "partitioner output file") or return 0;
    $self->StartOutFile() or return 0;
  }

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

  my $parfile_in;
  # by default, we ignore input partitioning files while partitioning
  # now (see I.10-831)
  if ($Config{CFX5_PARTITION_USES_PARFILE}) {
    # if we've got an input partition file, stick it into the directory.
    # This may or may not be the same as the output partition file, but
    # it should be relatively small, so copy it just in case.
    unless ($self->HaveFile('par')) {
      $parfile_in=$self->parfile_in();
      if (defined $parfile_in) {
        CFX5::FileTests::datafile($parfile_in, "source partition file $parfile_in")
            or return 0;
        if ($do and !$workdir->have_file('par')) {
          $workdir->input_file_copy('par' => $parfile_in);
        }
      }
    } else {
      $parfile_in='provided';
    }
  }

  #
  # make sure the host list is set up fully, we may need it for the
  # partition count
  #
  $self->SetupPPEHostList()
    or return 0;

  # we need either an input partition file or a number of partitions
  my $partitions=$self->partitions();
  unless (defined $parfile_in or defined $partitions) {
    # TODO: partitions has a default, as does parfile_in; methinks this is incorrect
    _error("No partition count has been specified");
    return 0;
  }

  # is the number of partitions OK, if it's defined?
  if (defined $partitions and !defined $parfile_in) {
    if ($partitions !~ m/^[0-9]+$/) {
      _error("The number of partitions must be a positive integer");
      return 0;
    } elsif ($partitions < 2) {
      _error("You cannot create fewer than two partitions");
      return 0;
    }
  }

  if ($self->save_parfile()) {
    my $parfile_out=$self->parfile_out();
    if (defined $parfile_out) {
      # we want to save the results
      CFX5::FileTests::datafile_w($parfile_out, "destination partition file") 
	or return 0;
      if ($do) {
	$workdir->creatable_file('par');
	$workdir->mark_output_file('par' => $parfile_out);
      }
    }
  } else {
    if ($do) {
      $workdir->creatable_file('par');
      $workdir->mark_output_file_ignore('par');
    }
  }

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

  if ($do) {
    # a job file can be created if there is an error in the
    # partitioner
    $workdir->creatable_file('job');
    # The partitioner may create a rad file for Monte Carlo radiation cases.
    # This is used by the solver, but we don't want to save it.
    $workdir->creatable_file('rad');
    $workdir->mark_output_file_ignore('rad');
  }

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

  # fill in the required parts of the CCL.
  if ($self->part_mode() eq 'MeTiS') {
    $self->part_metis_type();
  }
  $self->part_size_rule();

  # see if we can re-order the host list.
  #
  # we can only do this if we are re-partitioning, as if we have an
  # input partition file, the order of the partitions is fixed.  Not
  # that we will do anything sensible in that case, but at least we
  # can leave the data consistent.
  #
  # we must do this before calculating the weights if the rule is
  # Automatic
  my $ppe=$self->ppe_obj();
  if ($self->part_size_rule() eq 'Specified') {
    # we need to take special care, but we can do it
    my $weights=$self->part_weights();
    unless (defined $weights) {
      _error("Partition weights not specified with rule \"Specified\"");
      return 0;
    }
    my @weights=split(/\s*,\s*/, $weights);
    unless (@weights == $ppe->partitions()) {
      _error("Partition weight factors contains", scalar(@weights), "entries, but this run uses", scalar($ppe->partitions()), "partitions.");
      return 0;
    }
    my @hl=$ppe->consolidated_partitions();
    unless ($ppe->is_master($hl[0]->[1])) {
      # the first partition is not for the master host, re-order
      $ppe->put_master_first(\@weights);
      # no sprintf, we'll keep as string and use whatever the user
      # said
      $self->part_weights(join(", ", @weights));
    }
  } else {
    $ppe->put_master_first();
  }

  #
  # if requested, calculate the partition sizes
  #
  if ($self->part_size_rule() eq 'Automatic') {
    # we know the master is the first now, so the list will not need
    # reordering.

    my @parts=$ppe->consolidated_partitions();

    # count the "total speed"
    my $total=0;
    for my $r (@parts) {
      $total += $r->[0] * $r->[1]->speed();
    }

    # this turns easily into a list of fractions, we just need to make
    # the list now
    my @factors;
    for my $r (@parts) {
      push @factors, ($r->[1]->speed() / $total) x $r->[0];
    }

    $self->part_weights(join(", ", map { sprintf("%.5f",$_) } @factors));
  }

  #
  # if the user explicitly requested uniform partitioning, we'll
  # remove any weight factors with a warning.
  #
  if ($self->part_size_rule() eq 'Uniform') {
    my $weights=$self->part_weights();
    if (defined $weights) {
      _warn("Removing partition weight factors for size rule \"Uniform\"");
      $self->delete_ccl_parm("/SIMULATION CONTROL/EXECUTION CONTROL/PARTITIONER STEP CONTROL/PARTITIONING TYPE/Partition Weight Factors");
    }
  }

  1;
}

=head4 Run

This runs the partitioner after setting it up as above.

=cut

sub Run {
  my $self=shift;

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

    $self->OutFileHeader('Partitioning');

    my @args;

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

    if (defined $self->cat_factor_part()) {
      if (defined (my $f=$self->cat_factor_part())) {
        if ($f =~ s!x$!!i) {
          # "1.5x" actually maps to a different partitioner 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/PARTITIONER 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}_part";
      if (defined (my $f=$self->$m())) {
        if ($f =~ s!x$!!i) {
          # "1.5x" actually maps to a different partitioner 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/PARTITIONER STEP CONTROL/MEMORY CONTROL/$pairs{$t} Memory Override = $f");
            return 1;
          }
        }
       }
    }

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

    push @args, '-part';
    # this has to work, we checked it
    push @args, $self->partitions() 
      unless ($Config{CFX5_PARTITION_USES_PARFILE}
              and defined $self->parfile_in());

    $self->RunningComponent('partitioner');
    $rundir->enter();
    my $rv=$self->_exec(@args);
    $rundir->leave();
    $self->RunningComponent('');

    # if we've got a mon file, it's no use to anybody at this stage
    # anyway, so mark it to be ignored.  If the partitioner is also run,
    # that will change this setting if necessary.
    if ($self->HaveFile('mon')) {
      $rundir->notice_file('mon');
      $rundir->mark_output_file_ignore('mon');
    }

    if (!$rv->is_success()) {
      my $rfmsg='';
      if ($self->HaveFile('par')) {
        # if we got an error, the partitioning file is likely useless,
        # junk it
        $rundir->mark_output_file_ignore('par');
      }
      _error($rv->errstr("The ANSYS CFX partitioner") . $rfmsg);
      return 0;
    } elsif (not $self->HaveFile('par')) {
      _error("The ANSYS CFX partitioner has terminated without writing a partitioning information file.");
      return 0;
    }

    return 1;
  } else {
    return 0;
  }
}

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

  if ($self->lets_pretend()) {
    # dummy stuff
    require CFX5::Simulation::Configuration::Debug;
    return $self->CFX5::Simulation::Configuration::Debug::dummy_solver($self->partitioner_exec(),@args);
  }

  my $rv=do_termerr_cmd($self->partitioner_exec(),@args);
  return $rv;
}


1;
