=head1 NAME

  CFX5::CCLI - CCL toolchain interface routines

=head1 DESCRIPTION

The CCL toolchain -- ccl2flow, cclsetup, and the like -- are expected
to be used in more than just the solver script; for example, for
checking the validity of def files.  This module is designed to give a
standard way of accessing this functionality.

This is especially important in this case, since it is expected that
some of the error handling will be done by the Perl rather than the
C/Fortran, so we need to make sure it is done consistently.

This module assumes that it is being used from a ANSYS CFX script, and is
fairly verbose about error conditions; as well as returning exit
stati, it will display messages via C<_error>/C<_warn>/... .  This
should possibly change in the future.

=cut

package CFX5::CCLI;

use strict;
use Carp;
use lib;

use Exporter;
use vars qw(@ISA @EXPORT_OK);
push @EXPORT_OK, qw(export_parser_conf ttprocess getcclds setcclds writeccl format_ccl_error);
push @ISA, qw(Exporter);

use CFX5::Config qw(export_conf %Config query_conf unexport_conf unsetconf);
use CFX5::Output qw(:all);

use CFX5::PathManip qw(join_file split_file dirname basename fullpath
		       list_to_path change_cwd);

use CFX5::Commands qw(do_cmd do_cmd_quiet bg_cmd get_cmd_output
		      parse_rv handle_rv do_output_cmd);

use CFX5::FileTests;

use CFX5::FileOps qw(copy_file_for_read copy_file move_file append_to_file);

use CFX5::DefFile;

use CFX5::PkgLoader qw(cfx5_require);

#
# make the CCL parser directory available for the compiled code.
#
use lib;
sub _load_ccl_parser_load {
  my $from=shift;

  _debug("Attempting to load CCL parser from $from.");
  #_debug("Before use lib, \@INC=", @INC);
  'lib'->import($from);
  # on NT under the Perl compiler, the lib->import fails.  Not sure
  # why.  So we also put the directory onto the lib path.
  unshift @INC, $from;
  #_debug("After use lib, \@INC=", @INC);
  eval { require CCL::API; };
  if ($@) {
    _abort "Error loading CFX Command Language parser from $from:\n\n $@";
  }
  _debug("About to import ...");
  CCL::API->import();
  _debug("Imported.");
  1;
}
{
  my $parser_loaded=0;
  sub _load_ccl_parser {
    if ($parser_loaded) {
      return 1;
    } elsif (exists $ENV{CFX5_CCL_PARSER}) {
      # manual override
      _load_ccl_parser_load($ENV{CFX5_CCL_PARSER})
        and $parser_loaded=1;
    } elsif (exists $ENV{CFX5LIBDIR} and -d "$ENV{CFX5LIBDIR}/../ccl") {
      _load_ccl_parser_load("$ENV{CFX5LIBDIR}/../ccl")
        and $parser_loaded=1;
    } elsif ($^C) {
      warn "Not trying to calculate variables as we're compiling only";
    } elsif (defined $Config{CFX5_CCL_PARSER}) {
      _load_ccl_parser_load($Config{CFX5_CCL_PARSER})
        and $parser_loaded=1;
    } elsif (defined $Config{CFX5LIBDIR} and -d "$Config{CFX5LIBDIR}/../ccl") {
      _load_ccl_parser_load("$Config{CFX5LIBDIR}/../ccl")
        and $parser_loaded=1;
    }
    return $parser_loaded;
  }
}
BEGIN {
  if ($^C) {
    _load_ccl_parser() or die;
  }
}

=head1 CCL API access

=head2 new_api_object

  my $api=CFX5::CCLI::new_api_object();

Loads the CCL libraries if they are not yet loaded, and returns a new
API object.

=cut

sub new_api_object {
  _load_ccl_parser()
    or _abort "Unable to find ANSYS CFX CCL Parser";

  # we export these to the environment so the parser can get them back
  # again ...
  export_parser_conf();

  my $api = API->new();

  # disable undo and change list handling -- we don't use them, and it
  # makes it more than 2x slower!
  $api->setUndo(0);
  $api->setChangeList(0);

  $api;
}

=head2 format_ccl_error

 _error("Errors in CCL processing:\n", CFX5::CCLI::format_ccl_error($rv));

Formats the errors from the CCL parser (a string containing a list of
newline-separated errors) into a form which is acceptable to the
word-wrapping code in the _error etc calls.  (It indents them by a
space, apart from the first line which is assumed to be indented by
the comma in the suggested usage above).

=cut

sub format_ccl_error {
  (my $str=shift) =~ s!^! !gm;
  substr($str,1);
}

=head1 THE CCL SETUP/CHECKING PROCESS

This is the highest-level entry point available.  It takes as input a
definition file, and runs through the process of checking the CCL in
the definition file and putting the MMS dataset into it.  It will
optionally delete all its working files.

=head2 prepare_def_file

  CFX5::CCLI::prepare_def_file('def','ndef','out',0) or exit(1);

Runs the following sequence of commands:

=over

=item 1.

ccl2flow I<def> I<base>ccl I<base>mms

=item 2.

cclsetup $CFX5_TT_FILE I<base>mms I<base>mms.setup

=item 3.

writemms I<ndef> I<base>mms.setup

=item 4.

(optional) delete all the working files from above.

=back

The first two arguments are the definition file(s) to read from and
write to, in that order.  They may both be the same, in which case the
original definition file will be modified to include the parsed CCL
datasets.

If the third argument is defined, it is a filename to which any output
on stdout will be appended.  Otherwise it will just go to the screen.

The files are deleted unless the fourth argument is 0.  I<base> will
be the basename of the def file including the ".", or empty if the
definition file is just called "def".

Access to the fifth argument requires specification of the fourth 
argument. The fifth argument controls whether an existing mms.setup
file should be inserted into the def file, rather than re-running
CCL2FLOW and CCLSetup.

The function will get as far through the process as possible, but if
it fails, the return value will be FALSE.  Otherwise it will be TRUE.

If the definition file is from CFX-5.3 or earlier and includes no CCL
dataset, the return value will be TRUE but negative; otherwise it will
be positive.

=cut

sub prepare_def_file {
  my $defin=shift;
  my $def=shift;
  my $out=shift;
  my $cleanup=shift;
  $cleanup=1 unless defined $cleanup;
  my $writeMMSOnly=shift; 
  $writeMMSOnly=0 unless defined $writeMMSOnly;

  # make $base, which is the basename of the files to use, from the
  # definition file name.  $base does not include the directory, and
  # if $def has the filename "def", $base will be empty; otherwise
  # $base is (eq) "cfx5.".
  my @comps=split_file($def);
  my $f=pop @comps;
  $def=$f;
  if ($f =~ s/\.def$//) {
    $f.='.';
  } elsif ($f eq 'def') {
    $f='';
  }
  my $base=$f;

  _debug("Getting CCL from $def and writing to \"$base\" [empty is normal]");

  # now get the name of the directory we are to work in (the rest of
  # the split up filename from $def) and of the current directory, so
  # we can get back
  my $dir=join_file(@comps);
  my $origdir=fullpath('.');

  _debug("Working in $dir, origin is $origdir");

  unless (change_cwd($dir)) {
    _error("Cannot change working directory to $dir: $!");
    return 0;
  }

  #
  # Write the MMS dataset only
  #
  if ($writeMMSOnly) {
    unless (-f $base.'mms.setup') {
      _error("File ${base}mms.setup not found, may or may not be a bug!");
      goto abrt; 
    }
    writemms($def, $base.'mms.setup') or goto abrt;    
  }
  #
  # Process the CCL and write the resulting MMS dataset
  #
  else {
    # the new solver script will create the ccl file for ccl2flow to
    # read.  For now, we give a debug message indicating whether or not
    # it exists; when ccl2flow has been changed to read this file, it
    # may be appropriate to change this to an error if the file does not
    # exist.
    unless (-f $base.'ccl') {
      _debug("File ${base}ccl not found, may or may not be a bug!");
    }

    my $rv;

    $rv=ccl2flow($defin, $base.'mms', $base.'ccl', $out) or goto abrt;
    if ($rv eq 'CFX-5.3') {
      #append_to_file($out, "\nDefinition file contains no CFX Command Language data\n\n");

      if (-f 'def.lck') {
        _warn("def.lck file exists after failed command language processing, removing.");
        unlink('def.lck');
      }

      change_cwd($origdir);
      return -1;
    }

    cclsetup($base.'mms', $base.'mms.setup', $out)  or goto abrt;
    writemms($def, $base.'mms.setup') or goto abrt;

    if ($cleanup) {
      for my $f ($base.'ccl', $base.'mms', $base.'mms.setup') {
        unlink $f or _warn "Error unlinking $f: $!";
      }
    }
  }

  change_cwd($origdir);

  return 1;

 abrt:
  change_cwd($origdir);
  return 0;
}

=head2 export_parser_conf

  export_parser_conf();

Exports the configuration variables needed by the CCL parser:

=over

=item CFX5_CCL_LIBS

=item CFX5_CCL_LIBSDIR

=item CFX5_CCL_RULESDIR

(deprecated)

=item CFX5_CCL_RULES

(deprecated)

=item CFX5_CCL_PARSER

=item CFX5LIBDIR

(Needed to find CFX5::Units)

=back

=cut

use CFX5::Config::def
  (
   # the rules file (these aliases will be removed in 5.4.1)
   CFX5_CCL_RULESDIR => sub { $Config{CFX_DATA_DIR}; },
   CFX5_CCL_RULES => sub { join_file($Config{CFX5_CCL_RULESDIR},'RULES'); },

   # the LIBS and LIBSDIR variables replace the RULES and RULESDIR ones
   # From 5.7 on, only RULES,execrules and VARIABLES are set 
   CFX5_CCL_LIBS => sub {
      get_ccl_libs(); 
   },
   #CFX5_CCL_LIBS => sub { list_to_path('RULES', 'execrules.ccl', 'VARIABLES', 'MATERIALS', 'REACTIONS'); },
   #CFX5_CCL_LIBS => sub { list_to_path('RULES', 'VARIABLES', 'MATERIALS', 'REACTIONS'); },
   CFX5_CCL_LIBSDIR => sub { $Config{CFX5_CCL_RULESDIR}; },

   # find the Perl code for the parser.
   CFX5_CCL_PARSER => sub { join_file($Config{CFX5_LIB_ROOT},'ccl'); },

   # the TT file location
   #CFX5_TT_FILE => sub { join_file($Config{CFX_DATA_DIR},'tt.txt'); },
   CFX5_TTPROC_FILE => sub { join_file($Config{CFX_DATA_DIR},'ttproc.txt'); },
   
   # the CCL editor executable
   CFX5_CCLEDITOR_EXEC => sub { join_file($Config{CFX5EXECDIR},'ccleditor.exe'); },
  );

sub export_parser_conf {
  # note that, because of the warning in the code above, the order of
  # this list is important (PARSER vs LIB)
  export_conf(qw(CFX5_CCL_LIBS CFX5_CCL_LIBSDIR CFX5_CCL_RULESDIR
                 CFX5_CCL_RULES CFX5_CCL_PARSER
		 CFX5LIBDIR CFX_DATA_DIR CFX_CUSTOM_DATA_DIR));
}

sub update_ccl_libs {
  $Config{CFX5_CCL_LIBS}=get_ccl_libs();
}

sub get_ccl_libs {
  my @libs;
  my $custom=defined $Config{CFX_CUSTOM_DATA_DIR};
  if ( $custom and -f join_file($Config{CFX_CUSTOM_DATA_DIR}, 'RULES')){ 
    push @libs, join_file($Config{CFX_CUSTOM_DATA_DIR}, 'RULES');
  } else {
    push @libs, join_file($Config{CFX_DATA_DIR}, 'RULES');
  }
  if ( $custom and -f join_file($Config{CFX_CUSTOM_DATA_DIR}, 'execrules.ccl')){
    push @libs, join_file($Config{CFX_CUSTOM_DATA_DIR}, 'execrules.ccl');
  } else {
    push @libs, join_file($Config{CFX_DATA_DIR}, 'execrules.ccl');
  }
  if ( $custom and -f join_file($Config{CFX_CUSTOM_DATA_DIR}, 'VARIABLES')){
    push @libs, join_file($Config{CFX_CUSTOM_DATA_DIR}, 'VARIABLES');
  } else {
    push @libs, join_file($Config{CFX_DATA_DIR}, 'VARIABLES');
  }     
  list_to_path(@libs);
}

=head1 INDIVIDUAL COMPONENTS

=head2 getcclds

  $ccl = getcclds('file.def');
  die "Error" unless defined $ccl;
  if (ref $ccl) {
    # process it
  }

Reads the CCL dataset from a definition file, and returns the
L<CFX5::DefFile::DataSet> object corresponding to it.  If an error
occurs, the return value is C<undef>; if the definition file can be
read OK but contains no CCL dataset, the return value is the string
"No CFX Command Language dataset" (ie, TRUE but not a reference).

The dataset read always has WHAT 'G/COMMANDS', but the WHEN may vary.
If a dataset exists with WHEN = 0, that one is used; otherwise, the
one with the largest WHEN value is returned.

Eventually, I hope to be able to re-bless this object into a subclass
of L<CFX5::DefFile::DataSet> which has more CCL-related features.
This will not happen in the short term, though.  For now, the lines of
CCL can be retrieved with a call such as

  my @ccl=$cclds->data();

=cut

sub getcclds {
  my $file=shift;
  my $dsName=shift; 
  
  # Use default if no CCL dataset name provided
  $dsName = 'G/COMMANDS' unless (defined $dsName) ; 

  unless (CFX5::FileTests::datafile($file)) {
    return undef;
  }

  cfx5_require('CFX5::Files::Def');
  my $def=CFX5::Files::Def->new($file);
  return undef unless $def;
  return $def->getcclds($dsName);
}

=head2 setcclds

I<Not yet implemented>

  $ds = getcclds('file.def'); # read the existing dataset
  $ds->data(@newccl);         # set the CCL data
  $ds->header(-WHEN => 0);    # we always write user CCL to WHEN=0
  setcclds('file.def',$dataset,"Written using cfx5ccl");
                              # write the new dataset back to the file

The opposite of C<getcclds>: writes the CCL dataset back to the
definition file.  The third argument is optional; if it is included,
it is written to the C<ORIGIN> attribute of the dataset before it is
saved to the file, with the string

  "by $Config{USER} at $current_date"

appended.  If this argument is not passed, the C<ORIGIN> attribute is
not changed, and is written if it exists in the dataset object; this
means that if you use C<getcclds> followed immediately by C<setcclds>,
I<it will be incorrect>.

The C<$dataset> object should be an object of class
C<CFX5::DefFile::DataSet> or a subclass thereof.  The C<WHEN> field is
not set to zero before writing, so you should ensure that you do this
if necessary.

=cut

sub setcclds {
  internal_error("setcclds NYI");
}

=head2 writeccl

  writeccl('file.def','file.ccl') or die;
  writeccl('file.def',\@ccl) or die;

This call inserts CCL into the definition file, either from a file or
an array reference.  Return value is TRUE on success or FALSE on
failure.

This no longer uses the writecmds executable, but does it internally.

=cut

sub writeccl {
  my $deffile=shift;
  my $cclfile=shift;
  my $wrap=shift;
  $wrap=0 unless defined $wrap;
  my $what=shift;
  $what= 'G/COMMANDS' unless defined $what;
 

  CFX5::FileTests::datafile_w($deffile, "ANSYS CFX Definition file $deffile")
    or return 0;

  my $def = new CFX5::DefFile($deffile);
  return '' unless $def;

  # is it a file or a list?
  my @ccl;
  if (ref $cclfile) {
    if (ref($cclfile) eq 'ARRAY') {
      # it's an arrayref, use the lines
      @ccl=@$cclfile;
    } else {
      internal_error("Unsupported attempt to write non-ARRAY reference $cclfile as CCL");
    }
  } else {
    # it's a file, read it
    CFX5::FileTests::datafile($cclfile, "CFX Command Language data file $cclfile")
        or return 0;

    local *FILE;
    if (open FILE, "<$cclfile") {
      while(<FILE>){
        chomp $_;
        $_ =~ s![\r\n]*$!!;
        push @ccl, $_;
      }
      close FILE;
    } else {
      _error("Unable to open CCL file $cclfile");
      return 0;
    }
  }
  
  if ($wrap) {
    # Wraps the ccl - be careful, this doensn't handle everything that cclapi does.
  
    # check for lines >= 80 chars and wrap them
    my @splitccl;
    for (my $i=0; $i < @ccl; $i++) {
      my $line=$ccl[$i];
      # continue wrapping while the length is greater than 79 
      while( length($line) > 79 ){
      # Start at 79 and work back until we find a suitable point to split
      # Do not split on whitespace
        for (my $j=78; $j>0; $j--) {
          if( not (substr($line,$j,1) =~ m!\s!) ){
            push @splitccl, substr($line,0, $j)."\\"; 
            $line=substr($line,$j);
            $j=0;
          }
        }
      }
      # The final line (or first line if we didn't wrap).
      push @splitccl, $line;
    }
    @ccl=@splitccl;
  }
  else {
    # check for lines >=80 chars
    my @errlines;
    for (my $i=0; $i < @ccl; $i++) {
      if (length($ccl[$i]) >= 80) {
        # $i+1 is the line number in the file
        push @errlines, $i+1;
      }
    }
    if (@errlines) {
      if (@errlines == 1) {
        _error("Line",@errlines,"contains 80 or more characters.");
      } else {
        my $last=pop @errlines;
        _error("Lines",join(', ',@errlines),'and',$last,
               "contain 80 or more characters.");
      }
      return '';
    }
  }

  my $ds=$def->new_dataset();
  $ds->header(what => $what);
  $ds->header(where => 'EVERY');
  $ds->header(when => 0);
  $ds->header(datatype => 'STRI');

  $ds->data(@ccl);

  return $def->write_dataset($ds);

  1;
}

=head2 validate_ccl

  my @errors=CFX5::CCLI->validate_ccl(\@ccl, $rules, $rulesdir);

Validates the supplied CCL and returns any errors.  If supplied, the
$rules and $rulesdir arguments are used to locate the rules files;
otherwise, the defaults are used.

=cut

sub validate_ccl {
  my $self=shift;

  my $rccl=shift;

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

  my $libs=shift || $Config{CFX5_CCL_LIBS};
  my $libsdir=shift; # defaulting handled by the API
  _trace("Loading libraries $libs");
  my $rv=$api->readNamedLibraries($libs, $libsdir);
  if ($rv) {
    _error("Unable to read library files:\n", 
           $self->format_ccl_error($rv));
    return $rv;
  }

  _trace("Loading CCL data");
  $rv=$api->parseCCLData($rccl, 0);
  if ($rv) {
    _error("Unable to read command language input data:\n", 
           $self->format_ccl_error($rv));
    return $rv;
  }

  _trace("Validating CCL data");
  $rv=$api->validateCCLData();

  _trace("Complete");

  if (defined $rv) {
    return split(/\r?\n\r?/, $rv);
  } else {
    # all OK
    return ();
  }
}

=head2 ccl2flow

  ccl2flow('file.def','file.mms','file.ccl','out') or die;

I<Note that the arguments are in a different order to those to the
ccl2flow executable!>

This calls the F<ccl2flow> executable to read the def file, and
extract the CCL data into an MMS file readable by C<cclsetup>.  The
ccl2flow executable also generates a CCL file (mostly as a debugging
aid), which will be written to C<'file.ccl'> if it is defined;
otherwise, this file is discarded.

If there is a fourth argument, it is taken as a file to which standard
output and error will be redirected.

The return value will be TRUE on success or FALSE on failure.  In
particular, the following values are returned:

=over

=item C<undef>

Something fatal happened, an error message has been printed.

=item C<'CFX-5.3'>

The def file passed is a CFX-5.3 definition file, and does not include
a CCL dataset.  This is not an error.  The file will have been handled
appropriately by C<ccl2flow>; it is not yet clear whether it should be
passed onto C<cclsetup> and friends.

=item C<'Parser Warning'>

Some warnings were encountered during parsing, but no errors.

=item C<'Parser Error'>

Some errors were encountered by the parser, but an MMS file was
produced.

=item C<'ok'>

Everything is, so far as we can tell, peachy.

=back

(Not all of these return values are implemented yet).

=cut

sub ccl2flow {
  my $deffile=shift;
  my $mmsfile=shift;
  my $cclfile=shift;
  my $outfile=shift;

  _debug("ccl2flow $deffile -> $mmsfile", 
	 (defined $cclfile ? " ccl to $cclfile" : ()));

  carp "Usage is mmsfile cclfile not ccl mms" 
    if ($mmsfile =~ m/\.ccl$/ or (defined $cclfile and $cclfile =~ m/\.mms$/));

  my $supresscclfile=0;
  unless ($cclfile) {
    $cclfile="cfx5.$$.tmp.ccl";
    $supresscclfile=1;
  }

  CFX5::FileTests::datafile($deffile, "ANSYS CFX Solver Input File $deffile")
    or return 0;
  CFX5::FileTests::datafile_w($mmsfile, "Temporary MMS Information File $mmsfile")
    or return 0;
  CFX5::FileTests::datafile_w($cclfile, "Temporary CFX Command Language data File $cclfile")
    or return 0;

  CFX5::FileTests::execfile($Config{CFX5_CCL2FLOW_EXEC},"ccl2flow executable $Config{CFX5_CCL2FLOW_EXEC}")
    or return 0;

  export_parser_conf();
  export_conf(qw(CFX_DATA_DIR));

  #
  # write the output to the outfile if requested
  #
  my $rv;
  if (defined $outfile) {
    $rv=do_output_cmd($Config{CFX5_CCL2FLOW_EXEC},$deffile,$cclfile,$mmsfile);
    my $otxt=$rv->cmd_output();
    if (ref($outfile)) {
      push @$outfile, split(m/\n\r*/, $otxt);
    } else {
      append_to_file($outfile,$otxt)
        or _print $otxt;
    }
  } else {
    $rv=parse_rv(do_cmd_quiet($Config{CFX5_CCL2FLOW_EXEC},$deffile,$cclfile,$mmsfile));
  }

  unless ($rv->is_success()) {
    _error($rv->errstr());
  }

  if ($supresscclfile) {
    unlink($cclfile)
      or _warn "Error unlinking temporary CFX Command Language data file: $!";
  }

  return 0 unless defined $rv->rv();

  if ($rv->rv() == 2) {
    return "CFX-5.3";
  }

  unless ($rv->is_success()) {
    if ($rv->rv() == 3) {
      # exit status 3 is "user error", so give a nicer message.
      if (defined $outfile) {
	_error "An error was encountered in problem setup.  See the output file\n\t$outfile\nfor more details.";
      } else {
	_error "An error was encountered in problem setup.  See the messages above\nfor more details.";
      }
    } else {
      _error "ccl2flow exited with non-zero return code $rv";
    }
    return 0;
  }

  "ok";
}

=head2 cclsetup

  cclsetup('file.mms','file.mms.setup','out') or die;

Reads an MMS file written by C<ccl2flow> and writes an MMS setup file,
after doing some checks on its contents.

The return value will be FALSE if the file could not be written,
or TRUE on success.

Note that, currently, the cclsetup program works on fixed filenames,
so you should probably work in a temporary directory; this function
will hide the fact that the filenames are fixed from the programmer as
much as possible, but it does not go to the lengths of making a new
temporary directory.  Yet. C<;-)> For debugging purposes, we also
leave the .tt file lying around.

=cut

sub cclsetup {
  my $mmsfile=shift;
  my $setupfile=shift;
  my $outfile=shift;

  CFX5::FileTests::datafile($mmsfile, "Temporary MMS file $mmsfile")
    or return 0;
  CFX5::FileTests::datafile_w($setupfile, "Temporary MMS setup file $setupfile")
    or return 0;

  my $ttfile=$Config{CFX5_TT_FILE};
  my $ttprocfile=$Config{CFX5_TTPROC_FILE};
  if (defined $ttfile) {
    # this is a user override now
    CFX5::FileTests::datafile($ttfile, "ANSYS CFX TT file $ttfile")
        or return 0;
    _note("Using user-supplied TT file $ttfile");
  } else {
    CFX5::FileTests::datafile($ttprocfile, "ANSYS CFX TT file $ttprocfile")
        or return 0;
  }

  CFX5::FileTests::execfile($Config{CFX5_CCLSETUP_EXEC},"cclsetup executable")
    or return 0;

  #
  # firstly, we need to make a preprocessed copy of the TT file
  #
  if (defined $ttfile) {
    ttprocess($ttfile, 'cfx5.tt');
  } else {
    # this is already pre-processed.  If we're using this version, we
    # assume that any version already present in the directory (think
    # cfx5control) is valid.
    unless (-f 'cfx5.tt') {
      # ideally, we'd use copy_file_for_read, but this can cause
      # problems with SAMBA, where it sometimes can't resolve the
      # symlink, and so refuses to admit that the file exists.  So, we
      # just copy it anyway, it's only 300k.
      unless (copy_file($ttprocfile, 'cfx5.tt')) {
        _error("Error copying TT file $ttprocfile for reading: $!");
        return 0;
      }
    }
  }

  #
  # ensure that the input file has the right name
  #
  # this file, we have to update
  unless ($mmsfile eq 'cfx5.mms') {
    if (-f 'cfx5.mms') {
      unless (unlink 'cfx5.mms') {
        _error("Unable to replace MMS data file cfx5.mms: $!");
        return 0;
      }
    }
    unless (copy_file_for_read($mmsfile,'cfx5.mms')) {
      _error("Error copying MMS input file $mmsfile for reading: $!");
      return 0;
    }
  }

  #
  # now run the command
  #
  my $rv;
  if (defined $outfile) {
    $rv=do_output_cmd($Config{CFX5_CCLSETUP_EXEC});
    my $output=$rv->cmd_output();

    #
    # save the output (condense double newlines into single newlines)
    #
    $output =~ s/[\n\r]{2}/\n/g;
    if (ref $outfile) {
      push @$outfile, split(m/\n\r*/, $output);
    } else {
      append_to_file($outfile,$output)
        or _print $output;
    }
  } else {
    $rv = parse_rv(do_cmd_quiet($Config{CFX5_CCLSETUP_EXEC}));
  }

  #
  # save the generated files
  #
  # TODO: stop doing this!  Can't we just go with these names?
  move_file('cfx5.mms.setup',$setupfile)
    if (-f 'cfx5.mms.setup' and $setupfile ne 'cfx5.mms.setup');
  move_file('cfx5.mms.setup.attrb',"$setupfile.attrb")
    if (-f 'cfx5.mms.setup.attrb' and "$setupfile.attrb" ne 'cfx5.mms.setup.attrb');

  return 0 unless defined $rv;

  unless ($rv->is_success()) {
    _error($rv->errstr());
    return 0;
  }

  1;
}

=head2 writemms

  writemms('file.mms.setup','file.def') or die;

This uses the F<writemms> executable to insert the C<'file.mms.setup'>
into the named definition file.  Return value is TRUE on success or
FALSE on failure.

=cut

use CFX5::Config::def CFX5_WRITEMMS_EXEC => sub { join_file($Config{CFX5EXECDIR},'writemms.exe'); };

sub writemms {
  my $deffile=shift;
  my $mmsfile=shift;

  CFX5::FileTests::datafile_w($deffile, "ANSYS CFX Definition file $deffile")
    or return 0;
  CFX5::FileTests::datafile($mmsfile, "Temporary MMS setup file $mmsfile")
    or return 0;

  CFX5::FileTests::execfile($Config{CFX5_WRITEMMS_EXEC},"writemms executable")
    or return 0;

  my @args=($deffile,$mmsfile);

  if (-f "$mmsfile.attrb") {
    _debug("Found dataset attributes file $mmsfile.attrb");
    push @args, "$mmsfile.attrb";
  }

  my $rv=handle_rv(do_cmd_quiet($Config{CFX5_WRITEMMS_EXEC},@args));

  return 0 unless defined $rv;

  if ($rv) {
    _error "writemms exited with non-zero return code $rv";
    return 0;
  }

  1;
}

=head1 MISCELLANEOUS ROUTINES

=head2 ttprocess

  ttprocess($Config{CFX5_TT_FILE},'cfx5.tt') or die;

C<ttprocess> is an emulation of part of the C preprocessor, but
slightly modified.  It is done like this for two reasons:

=over

=item 1. 

Not all users will have access to the C preprocessor, especially users
on NT.

=item 2.

The program reading this TT file is limited to 80 characters per line,
and there is a concern that some macro expansions might go above this
limit; by doing it ourselves we can add code to make sure this does
not happen.

=back

I am not quite sure what the performance implications of doing it this way are, but since it is a cut-down version of the full functionality, I don't think it should be too bad.

The routine takes as parameters two files, an input file and an output
file.  The input file is read, and processed as follows before being
written to the output file:

=over

=item 1.

Any lines matching C<^#\s*define\s+> are treated as C C<#define>
statements, and read and replaced with a blank line.

=item 2.

If the text of any define appears in the current line, it is replaced
by the definition.  The process is then restarted, so definitions may
be recursive (as for the C preprocessor).

=item 3.

If the line goes beyond char 80, a backslash/newline is inserted at
character 80.

=back

=cut 

# try to do each replacement in turn, return 1 if we manage one or 0
# otherwise.
sub do_replacements {
  my $rline=shift;
  my $rdefs=shift;

  for (keys %$rdefs) {
    return 1 if $$rline =~ s/\b$_\b/$rdefs->{$_}/g;
  }
  return 0;
}

sub ttprocess {
  my $infile=shift;
  my $outfile=shift;

  CFX5::FileTests::datafile($infile) or return 0;
  CFX5::FileTests::datafile_w($outfile) or return 0;

  local (*INFILE,*OUTFILE);
  unless (open INFILE, "<$infile") {
    _error("Unable to read $infile: $!");
    goto abrt;
  }
  unless (open OUTFILE, ">$outfile") {
    _error("Unable to write $outfile: $!");
    goto infile;
  }

  #
  # now go through the files
  #
  my %defs;
  my $line=0;
  while (<INFILE>) {
    $line++;
    if (m/^\#\s*define\s+([^\s]+)\s+(.*?)\s*$/) {
      _warn("$1 redefined at $infile line $line")
	if (exists $defs{$1});
      # just ignore anything that #defines something to itself.  This
      # will fix direct infinite loops.
      $defs{$1}=$2 unless $1 eq $2;
      $_="\n";
    } else {
      my $line=$_;
      1 while (do_replacements(\$line,\%defs));
      $_=$line;
    }

    # make sure the lines are wrapped correctly.  That means that they
    # should be wrapped and a continuation character inserted, but
    # only outside of comments.
    while (length($_) > 80) {
      # a possible
      my $cpos;
      if (m!//!g) {
	$cpos=pos($_) - 2;
      }
      
      # if there's a comment within 80 chars, we're done here
      last if (defined $cpos and $cpos < 80);

      # if there is a comment here, it's past 80 columns, so let's
      # split.  (If the comment goes over 80chars we don't care, but
      # more, we can't allow continuation lines inside comments)
      print OUTFILE substr($_,0,79), "\\\n";
      $_=substr($_,79);
    }
    print OUTFILE $_;
  }

  unless (close(OUTFILE)) {
    _error("Unable to close $outfile: $!");
    goto outfile;
  }
  unless (close(INFILE)) {
    _error("Unable to close $infile: $!");
    goto infile;
  }

  return 1;
 outfile:
  close OUTFILE;
 infile:
  close INFILE;
 abrt:
  return 0;
}

1;
