=head1 NAME

CFX5::MonFile - Handling of monitor point information

=head1 SYNOPSIS

  use CFX5::MonFile;

=head1 DESCRIPTION

This library implements some very basic handling of monitor point data
for the solver script.  It is not a fully-specified parser for monitor
point files.

=cut

package CFX5::MonFile;

use strict;

use CFX5::Config qw(%Config);
use CFX5::Output qw(:all);
use CFX5::FileTests;

use CFX5::DefFile;

=head2 CONSTRUCTORS

=head3 new

  my $mon=CFX5::MonFile->new('mon');

Returns an object representing the named file, which must exist.

=cut

sub new {
  my $this=shift;
  my $class=ref($this) || $this;
  my $self={
            file => undef,
           };
  bless $self, $class;
  $self->init(@_) or return undef;
  return $self;
}

sub init {
  my $self=shift;
  my $file=shift;
  unless (defined $file) {
    internal_error("Missing filename in constructor");
  }
  CFX5::FileTests::datafile($file)
      or return 0;
  $self->{file}=$file;
  1;
}

=head2 MEMBER DATA ACCESS

=head3 file

  unlink($mon->file());

Returns the filename.

=cut

sub file {
  my $self=shift;
  if (@_) {
    internal_error("Cannot (yet) set the filename");
  }
  return $self->{file};
}

=head2 TIMESTEP QUERIES

=head3 final_timestep

  my $step=$mon->final_timestep();

Returns the "ACCUMULATED TIMESTEP" value for the latest timestep in
the file.

=cut

sub final_timestep {
  my $self=shift;

  local *MFILE;

  unless (open MFILE, $self->file()) {
    _error("Error reading monitor point file",$self->file().": error opening file: $!");
    return undef;
  }

  my @CurrentHeader;
  my $AccumPos;
  my $LineNumber=0;
  my $rv;
  while (defined (my $l=<MFILE>)) {
    chomp $l;
    $LineNumber++;

    if ($l =~ m!^\@START@!) {
      # start of header, read it in
      @CurrentHeader=();
      undef $AccumPos;
      while (1) {
        my $l=<MFILE>;
        $LineNumber++;
        unless (defined $l) {
          _error("Error reading monitor point file",$self->file().": unexpected EOF in header.");
          close MFILE;
          return undef;
        }
        chomp $l;
        last if $l =~ m!^\@END@!;
        push @CurrentHeader, $l;
        if ($l =~ m!^ACCUMULATED TIMESTEP!) {
          $AccumPos=@CurrentHeader - 1;
        }
      }
      next;
    } else {
      # if we can, get the timestep information from this line - this
      # is necessary in case the solver fails before the first
      # timestep on an adaption run, so we don't lose the information
      # altogether.
      if (@CurrentHeader and defined $AccumPos) {
        my @data=split(/,\s*/,$l);
        if (defined $data[$AccumPos]) {
          $rv=$data[$AccumPos];
        }
      }
    }
  }

  close MFILE;

  unless (defined $rv) {
    # we failed dismally, can we give a more useful error?
    unless (@CurrentHeader) {
      _error("Error reading monitor point file",$self->file().": missing header.");
      return undef;
    }
    unless (defined $AccumPos) {
      _error("Error reading monitor point file",$self->file().": ACCUMULATED TIMESTEP missing from header.");
      return undef;
    }
  }

  # huzzah!  We have the power!
  return $rv;
}

=head2 DEFINITION FILE ACCESS

A set of calls to read monitor data from, and write it to, definition
and results files.

=head3 delete_from_res

  my @data=CFX5::MonFile->delete_from_res('file.res');

Deletes all C<G/MON> datasets from the results file.
Purges the file of deleted datasets.

=cut

sub delete_all_from_res
{
  my $this = shift;
  my $dest = shift;

  CFX5::FileTests::datafile($dest,"ANSYS CFX Results File $dest") or return ();

  my $def = CFX5::DefFile->new($dest);
  return () unless $def;

  CFX5::DefFile::init_xs();

  $def->delete_dataset(what => 'G/MON',
                       where => 'EVERY');
  $def->purge_deleted_datasets();
}


=head3 insert_into_res

  CFX5::MonFile->insert_into_res('file.res' => 'mon.old', 'mon');

Concatenates any files from the argument list, and inserts the result
into the res file as G/MON EVERY I<timestep>, where I<timestep> is the
latest timestep from the last file in the list.

=cut

sub insert_into_res {
  my $this=shift;
  my $dest=shift;

  my @monfiles=@_;

  # return if there's nothing to do
  return 1 unless @monfiles;

  # check that all the files exist
  for my $f (@monfiles) {
    CFX5::FileTests::datafile($f,"Monitor point data file $f")
        or return 0;
  }
  CFX5::FileTests::datafile_w($dest,"ANSYS CFX Results File $dest")
      or return 0;

  # get the timestep number
  my $lmon=$this->new($monfiles[$#monfiles]);
  return 0 unless $lmon;
  my $when=$lmon->final_timestep();
  return 0 unless defined $when;

  # now we're getting with it!
  my $def=CFX5::DefFile->new($dest);
  return 0 unless $def;

  # if there's already some monitor data in a res file, whichever one
  # has the highest timestep number will be used.  I'm not going to
  # make it an error, but we should at least give a warning if the
  # data is not going to be accessible.
  my @dsl=sort {
    $b->[2] <=> $a->[2]
  } $def->have_dataset(what => 'G/MON',where => 'EVERY');
  if (@dsl and $dsl[0]->[2] > $when) {
    _warn("The ANSYS CFX Results File $dest already contains monitor point data from timestep $dsl[0]->[2], which will be accessed in preference to the newly-added monitor point data, at timestep $when.");
  }

  if ($Config{CFX5_SOLVE_OLD_MON_HANDLING}) {
    my $ds=$def->new_dataset();
    $ds->header(what => 'G/MON');
    $ds->header(where => 'EVERY');
    $ds->header(when => $when);

    my @data;
    for my $f (@monfiles) {
      local *MFILE;
      unless (open MFILE, "<$f") {
        _error("Error reading monitor point file $f: error opening file: $!");
        return 0;
      }
      push @data, <MFILE>;
      close MFILE;
    }

    # data_text treats the data as more free-form text than the default
    # IO module code; in particular, it will allow lines >80 chars,
    # which is kinda handy ...
    chomp @data;
    $ds->data_text(@data);

    return $def->write_dataset($ds);
  } else {
    _trace("Writing monitor data to G/MON:EVERY:$when");
    return $def->store_text_dataset(\@monfiles, what => 'G/MON', 
                                    where => 'EVERY', when => $when);
  }
}

=head3 read_from_res

  my @data=CFX5::MonFile->read_from_res('file.res');

Reads the latest available C<G/MON> dataset from the results file, and
returns the lines as a list.

=cut

sub read_from_res {
  my $this=shift;
  my $dest=shift;

  CFX5::FileTests::datafile($dest,"ANSYS CFX Results File $dest")
      or return ();

  my $def=CFX5::DefFile->new($dest);
  return () unless $def;

  my @dsl=sort {
    $b->[2] <=> $a->[2]
  } $def->have_dataset(what => 'G/MON',where => 'EVERY');

  unless (@dsl) {
    # no datasets
    return ();
  }

  _debug("Have monitor data at WHEN =", join(", ", map { $_->[2] } @dsl));
  my $when=$dsl[0]->[2];

  _trace("Reading monitor data from G/MON:EVERY:$when");

  # otherwise, we know which one, as we sorted already
  my $ds=$def->read_dataset(what => 'G/MON',
                            where => 'EVERY',
                            when => $when);
  return () unless $ds;

  # the data_text call will read the data, and fix the line wrapping
  # etc we did above
  return $ds->data_text();
}

=head3 extract_from_res

  my @data=CFX5::MonFile->extract_from_res('file.res', 'file.mon');

Reads the latest available C<G/MON> dataset from the results file, and
writes it to the given file.

=cut

sub extract_from_res {
  my $this=shift;
  my $file=shift;
  my $dest=shift;

  CFX5::FileTests::datafile($file,"ANSYS CFX Results File $file")
      or return ();

  unless ($dest eq '-') {
    CFX5::FileTests::datafile_w($dest,"Monitor point data file $dest")
        or return ();
  }

  my $def=CFX5::DefFile->new($file);
  return () unless $def;

  my @dsl=sort {
    $b->[2] <=> $a->[2]
  } $def->have_dataset(what => 'G/MON',where => 'EVERY');

  unless (@dsl) {
    # no datasets -- success
    return 1;
  }

  _debug("Have monitor data at WHEN=", join(", ", map { $_->[2] } @dsl));
  my $when=$dsl[0]->[2];

  _trace("Reading monitor data from G/MON:EVERY:$when");

  # otherwise, we know which one, as we sorted already
  return $def->extract_text_dataset($dest,
                                    what => 'G/MON',
                                    where => 'EVERY',
                                    when => $when);
}

1;
