=head1 NAME

CFX5::Parallel::HostFile - hostinfo.ccl file manipulation

=head1 SYNOPSIS

This package contains routines for reading and writing hostinfo.ccl
files, largely in terms of L<CFX5::Parallel::Host> objects.

=cut

package CFX5::Parallel::HostFile;

use strict;

use vars qw($CVSVersion);
$CVSVersion='$Id$ ';

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

use CFX5::Parallel::Host;

use CFX5::CCLI;

use CFX5::PkgLoader qw(cfx5_require);

=head2 CONSTRUCTORS

=head3 from_file

  my $hosts=CFX5::Parallel::HostFile->from_file('hostinfo.ccl');

=cut

sub from_file {
  my $this=shift;
  my $class=ref($this) || $this;
  my $self={};
  bless $self, $class;
  $self->init_standalone();
  $self->read(@_);
  $self;
}

sub init_standalone {
  my $self=shift;
  # create a new CCL API object we can use.
  my $cclapi = CFX5::CCLI::new_api_object();
  $self->{cclapi} = $cclapi;
  return 1;
}

sub ccl_api_obj {
  my $self=shift;
  return $self->{cclapi};
}

=head3 new_default

Create a default host file object, and return that.  Currently, such
an object simply includes the local host.

=cut

sub new_default {
  my $this=shift;
  my $class=ref($this) || $this;
  my $self={};
  bless $self, $class;
  $self->init_standalone();
  $self->add_default_contents(@_);
  $self;
}

sub add_default_contents {
  my $self=shift;

  # ensure that the local host exists in the database
  my $h=CFX5::Parallel::Host->new($self, $Config{CFX_HOST});
  # make sure the root directory is recorded
  $h->root();
  $h->mark_non_library();
}

=head2 INTERROGATION

=head3 all_known_hosts"
=cut

sub all_known_hosts {
  my $self=shift;

  my $haref;
  my $rv = $self->ccl_api_obj()->getChildInfoList('/SIMULATION CONTROL/EXECUTION CONTROL/PARALLEL HOST LIBRARY', \$haref);
  if ($rv) {
    _error("Error retrieving full host information list:", CFX5::CCLI::format_ccl_error($rv));
    return ();
  }

  my @rv;
  for my $hn (@$haref) {
    next unless $hn->{type} eq 'HOST DEFINITION';
    push @rv, $hn->{name};
  }

  @rv;
}

=head3 host

  my $host=$hosts->host('xenon');

Returns a CFX5::Parallel::Host object for the named host.

=cut

sub host {
  my $self=shift;
  my $hn=shift;

  return CFX5::Parallel::Host->new($self, $hn);
}

=head2 READING AND WRITING

=head3 read

  $hosts->read('hostinfo.ccl');

=cut

sub read {
  my $self=shift;
  my $file=shift;

  CFX5::FileTests::datafile($file)
      or return '';

  local *FILE;
  unless (open FILE, "<$file") {
    _error("Error reading $file: $!");
    return '';
  }
  my @lines=<FILE>;
  close FILE;

  my $api=$self->ccl_api_obj();
  my $rv = $api->parseCCLData(\@lines, 1);
  if ($rv) {
    _error("Error reading host database from $file:", CFX5::CCLI::format_ccl_error($rv));
    return '';
  }

  1;
}

=head3 fill_in_and_write

  $hosts->fill_in_and_write('newhostinfo.ccl');
  $hosts->fill_in_and_write(\*STDOUT);

We do these operations together as we want to be able to attach
comments to various hosts.

=cut

sub fill_in_and_write {
  my $self=shift;
  my $file_or_fh=shift;
  my $strict=shift;

  my ($fh,$file);
  if (ref $file_or_fh) {
    # assume a filehandle
    $fh=$file_or_fh;
  } else {
    # assume a file.  Write to a temporary name, and then rename at
    # the end.
    $file=$file_or_fh;

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

    require Symbol;
    $fh=Symbol::gensym();
    unless (open $fh, ">$file.tmp") {
      _error("Unable to open $file.tmp for writing: $!");
      return 0;
    }
  }

  my $date=localtime;
  print $fh <<"END_INTRO";
# ANSYS CFX Host Solver Input File
#
# Automatically created
#   by $0
#   at $date.

SIMULATION CONTROL:
  EXECUTION CONTROL:
    PARALLEL HOST LIBRARY:
END_INTRO

  cfx5_require('IO::Handle');

  for my $h ($self->all_known_hosts()) {
    _info("Processing host", $h);
    $self->fill_in_host($h, $fh, $strict);
    IO::Handle::flush($fh);
  }

  print $fh "   END # PARALLEL HOST LIBRARY\n";
  print $fh "  END # EXECUTION CONTROL\n";
  print $fh "END # SIMULATION CONTROL\n";

  if (defined $file) {
    # we had a filename, so we have to close and rename it now
    unless (close $fh) {
      _error("Unable to finish write to $file.tmp: $!");
      return 0;
    }
    unless (rename "$file.tmp" => $file) {
      _error("Unable to rename $file.tmp to $file: $!");
      return 0;
    }
  }

  return 1;
}

sub fill_in_host {
  my $self=shift;
  my $hn=shift;
  my $fh=shift;
  my $strict=shift;

  my $h=CFX5::Parallel::Host->new($self, $hn);

  # first, we check for the host in DNS
  unless (defined $h->hostaddr()) {
    print $fh "  # host name lookup failed for ", $h->hostname(), "\n";
    # if it doesn't exist, we don't add a record for it at all.
    return '';
  }

  unless ($h->detect_host_arch()) {
    unless ($h->check_link()) {
      print $fh "  # no remote shell access to ", $h->hostname(), "\n";
      # but we still include what information we have
      if ($strict) {
        return '';
      }
      goto DONE;
    }
  }

 DONE:
  print $fh map { "$_\n" } $h->ccl_def();

  1;
}

=head3 write

  $hosts->write('newhostinfo.ccl');
  $hosts->write(\*STDOUT);

Like fill_in_and_write except it doesn't fill anything in!

=cut

sub write {
  my $self=shift;
  my $file_or_fh=shift;
  my $strict=shift;

  my ($fh,$file);
  if (ref $file_or_fh) {
    # assume a filehandle
    $fh=$file_or_fh;
  } else {
    # assume a file.  Write to a temporary name, and then rename at
    # the end.
    $file=$file_or_fh;

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

    require Symbol;
    $fh=Symbol::gensym();
    unless (open $fh, ">$file.tmp") {
      _error("Unable to open $file.tmp for writing: $!");
      return 0;
    }
  }

  my $date=localtime;
  print $fh <<"END_INTRO";
# ANSYS CFX Host Solver Input File
#
# Automatically created
#   by $0
#   at $date.

SIMULATION CONTROL:
  EXECUTION CONTROL:
    PARALLEL HOST LIBRARY:
END_INTRO

  for my $hn ($self->all_known_hosts()) {
    my $h=CFX5::Parallel::Host->new($self, $hn);
    print $fh map { "$_\n" } $h->ccl_def();
  }

  print $fh "    END # PARALLEL HOST LIBRARY\n";
  print $fh "  END # EXECUTION CONTROL\n";
  print $fh "END # SIMULATION CONTROL\n";

  if (defined $file) {
    # we had a filename, so we have to close and rename it now
    unless (close $fh) {
      _error("Unable to finish write to $file.tmp: $!");
      return 0;
    }
    unless (rename "$file.tmp" => $file) {
      _error("Unable to rename $file.tmp to $file: $!");
      return 0;
    }
  }

  return 1;
}

=head2 ADDING INFORMATION

=head3 add_host

Adds a host by name

=cut

sub add_host {
  my $self=shift;
  my $host=shift;

  # ensure that the local host exists in the database
  my $h=CFX5::Parallel::Host->new($self, $host);
  # this will ensure it appears
  $h->mark_non_library();

  1;
}

=head2 CCL DATABASE JOB-LIKE THINGS

=head3 read_ccl_parm

=cut

sub read_ccl_parm {
  my $self=shift;
  my $cp=shift;
  my $c;

  my $api=$self->ccl_api_obj();
  if ($api->haveParameter($cp)) {
    my $rv=$api->getStringValue("$cp",\$c);
    if (defined $rv) {
      _error("Error getting $cp from Command Language database:\n",
             CFX5::CCLI::format_ccl_error($rv));
      return undef;
    }

    $c =~ s!\\\#!\#!g;
  }

  $c;
}

=head3 set_ccl_parm

=cut

sub set_ccl_parm {
  my $self=shift;
  my $api=$self->ccl_api_obj();
  my $opts=ref($_[0]) ? shift : {};
  my $cp=shift;
  my $c=shift;

  unless (defined $c) {
    internal_error("Cannot set CCL parameter \"$cp\" to undefined value");
  }

  $c =~ s!\#!\\\#!g;

  if ($api->haveParameter($cp)) {
    # if we already have it, we can just set it like this, as it is
    # more efficient (?)
    my $rv=$api->setStringValue("$cp",$c);
    if (defined $rv) {
      _error("Error setting $cp in Command Language database:\n",
             CFX5::CCLI::format_ccl_error($rv));
    }
  } else {
    # we construct a snippet of CCL which we can send to the API to
    # parse, as setStringValue does not allow creation of objects.
    my @path=split(m!/!,$cp);
    shift @path; # remove leading blank
    my $pname=pop @path;
    my @ccl;
    for my $n (@path) {
      if ($n =~ m/:/) {
        push @ccl , "$n";
      } else {
        push @ccl, "$n:";
      }
    }
    push @ccl , "$pname = $c";
    for my $n (@path) {
      push @ccl, "END";
    }
    my $rv=$api->parseCCLData(\@ccl,1);
    if (defined $rv) {
      _error("Error setting $cp in Command Language database:\n",
             CFX5::CCLI::format_ccl_error($rv));
    }
  }

  $c;
}

sub format_ccl_error {
  my $self=shift;
  return CFX5::CCLI::format_ccl_error(@_);
}

1;
