#!/usr/bin/env perl
#-----------------------------------------------------------------------------------------------
#
# configure
#
#
# This utility allows the CISM user to specify compile-time configuration
# options via a commandline interface.  The output from configure is a
# file with a list of CPP options and a cache file that contains all 
# configuration parameters required to produce the CPP file.  A subsequent 
# invocation of configure can use the cache file as input (via the -defaults 
# argument) to reproduce the CISM configuration contained in it.  Note that 
# when a cache file is used to set default values only the model parameters 
# are used.  The parameters that are platform dependent (e.g., compiler options, 
# library locations, etc) are ignored.
#
# As the build time configurable options of CISM are changed, this script
# must also be changed.  Thus configure is maintained under revision
# control in the CISM source tree and it is assumed that only the version of
# configure in the source tree will be used to build CISM.  Thus we assume
# that the root of the source tree can be derived from the location of this
# script.
#
#-----------------------------------------------------------------------------------------------

use strict;
#use warnings;
#use diagnostics;

use Cwd;
use English;
use Getopt::Long;
use IO::File;
use IO::Handle;
#-----------------------------------------------------------------------------------------------

sub usage {
    die <<EOF;
SYNOPSIS
     configure [options]
OPTIONS
     User supplied values are denoted in angle brackets (<>).  Any value that contains
     white-space must be quoted.  Long option names may be supplied with either single
     or double leading dashes.  A consequence of this is that single letter options may
     NOT be bundled.

     -cache <file>       Name of output cache file (default: config_cache.xml).
     -cachedir <file>    Name of directory where output cache file is written (default: CISM build directory).
     -cism_bld <dir>      Directory where CISM will be built.  This is where configure will write the
                         output files it generates (Makefile, Filepath, etc...)
     -cism_exedir <dir>   Directory where CISM executable will be created (default: CISM build directory).
     -cism_root <dir>     Root directory of cism source code (default: directory above location of this script)
     -cppdefs <string>   A string of user specified CPP defines.  Appended to
                         Makefile defaults.  E.g. -cppdefs '-DVAR1 -DVAR2'
     -comp_intf <name>   Component interface to use (ESMF or MCT) (default MCT)
     -defaults <file>    Specify full path to a configuration file which will be used to supply defaults instead of
                         the defaults in bld/.  This file is used to specify model
                         configuration parameters only.  Parameters relating to the build which
                         are system dependent will be ignored.
     -help [or -h]       Print usage to STDOUT.
     -mach <name>        Machine name to use for CCSM build.
     -silent [or -s]     Turns on silent mode - only fatal messages issued.
     -target_os          Override the os setting for cross platform compilation [aix | darwin | dec_osf | irix | 
                         linux | solaris | super-ux | unicosmp | bgl ].  Default: OS on which configure is
			 executing as defined by the perl \$OSNAME variable.
     -usr_src <dir1>[,<dir2>[,<dir3>[...]]]
                         Directories containing user source code.
     -verbose [or -v]    Turn on verbose echoing of settings made by configure.
     -version            Echo the CVS tag name used to check out this CISM distribution.
EOF
}

#-----------------------------------------------------------------------------------------------
# Setting autoflush (an IO::Handle method) on STDOUT helps in debugging.  It forces the test
# descriptions to be printed to STDOUT before the error messages start.

*STDOUT->autoflush();                  

#-----------------------------------------------------------------------------------------------
# Set the directory that contains the CISM configuration scripts.  If the configure command was
# issued using a relative or absolute path, that path is in $ProgDir.  Otherwise assume the
# command was issued from the current working directory.

(my $ProgName = $0) =~ s!(.*)/!!;      # name of this script
my $ProgDir = $1;                      # name of directory containing this script -- may be a
                                       # relative or absolute path, or null if the script is in
                                       # the user's PATH
my $cwd = getcwd();                    # current working directory
my $cfgdir;                            # absolute pathname of directory that contains this script
if ($ProgDir) { 
    $cfgdir = absolute_path($ProgDir);
} else {
    $cfgdir = $cwd;
}

#-----------------------------------------------------------------------------------------------
# Save commandline
my $commandline = "$cfgdir/configure @ARGV";

#-----------------------------------------------------------------------------------------------
# Parse command-line options.
my %opts = (
	    cache       => "config_cache.xml",
            cism_root    => undef,
	    );
GetOptions(
    "cache=s"                   => \$opts{'cache'},
    "cachedir=s"                => \$opts{'cachedir'},
    "cism_bld=s"                 => \$opts{'cism_bld'},
    "cism_exedir=s"              => \$opts{'cism_exedir'},
    "cism_root=s"                => \$opts{'cism_root'},
    "cppdefs=s"                 => \$opts{'cppdefs'},
    "comp_intf=s"               => \$opts{'comp_interface'},
    "debug"                     => \$opts{'debug'},
    "defaults=s"                => \$opts{'defaults'},
    "mach=s"                    => \$opts{'mach'},
    "h|help"                    => \$opts{'help'},
    "s|silent"                  => \$opts{'silent'},
    "target_os=s"               => \$opts{'target_os'},
    "usr_src=s"                 => \$opts{'usr_src'},
    "v|verbose"                 => \$opts{'verbose'},
    "version"                   => \$opts{'version'},
)  or usage();

# Give usage message.
usage() if $opts{'help'};

# Echo version info.
version($cfgdir) if $opts{'version'};    

# Check for unparsed argumentss
if (@ARGV) {
    print "ERROR: unrecognized arguments: @ARGV\n";
    usage();
}

# Define 3 print levels:
# 0 - only issue fatal error messages
# 1 - only informs what files are created (default)
# 2 - verbose
my $print = 1;
if ($opts{'silent'})  { $print = 0; }
if ($opts{'verbose'}) { $print = 2; }
my $eol = "\n";

my %cfg = ();           # build configuration

#-----------------------------------------------------------------------------------------------
# Make sure we can find required perl modules and configuration files.
# Look for them in the directory that contains the configure script.

# Check for the configuration definition file.
my $config_def_file = "config_definition.xml";
(-f "$cfgdir/$config_def_file")  or  die <<"EOF";
** Cannot find configuration definition file \"$config_def_file\" in directory 
    \"$cfgdir/\" **
EOF

# The XML::Lite module is required to parse the XML configuration files.
(-f "$cfgdir/../../../../scripts/ccsm_utils/Tools/perl5lib/XML/Lite.pm")  or  die <<"EOF";
** Cannot find perl module \"XML/Lite.pm\" in directory 
    \"$cfgdir/../../../../scripts/ccsm_utils/Tools/perl5lib\" **
EOF

# The Build::Config module provides utilities to store and manipulate the configuration.
(-f "$cfgdir/../../../../scripts/ccsm_utils/Tools/perl5lib/Build/Config.pm")  or  die <<"EOF";
** Cannot find perl module \"Build/Config.pm\" in directory 
    \"$cfgdir/../../../../scripts/ccsm_utils/Tools/perl5lib\" **
EOF

if ($print>=2) { print "Setting CISM configuration script directory to $cfgdir$eol"; }

#-----------------------------------------------------------------------------------------------
# Add $cfgdir/perl5lib to the list of paths that Perl searches for modules
my @dirs = (  $cfgdir, "$cfgdir/../../../../scripts/ccsm_utils/Tools/perl5lib");
unshift @INC, @dirs;
require XML::Lite;
require Build::Config;

# Initialize the configuration.  The $config_def_file provides the definition of a CISM
# configuration as well as the defaults.
# $cfg_ref is a reference to the new configuration object.
my $cfg_ref = Build::Config->new("$cfgdir/$config_def_file", 
				 "$config_def_file");

#-----------------------------------------------------------------------------------------------
# CISM root directory.  
my $cism_root;

if ( ! defined($opts{'cism_root'} ) ) {
  $cism_root = absolute_path("$cfgdir/..");
} else {
  $cism_root = $opts{'cism_root'};
}

if ( &is_valid_directory( "$cism_root/source_glimmer-cism", allowEnv=>1 ) ) {
    $cfg_ref->set('cism_root', $cism_root);
} else {
    die <<"EOF";
** Invalid CISM root directory: $cism_root
** 
** The CISM root directory must contain the subdirectory /source_glimmer-cism/.
** cism_root can be entered on the command line or it will be derived
** from the location of this script.
EOF
}

if ($print>=2) { print "Setting CISM root directory to $cism_root$eol"; }

#-----------------------------------------------------------------------------------------------
# CISM build directory.
my $cism_bld;
if (defined $opts{'cism_bld'}) {
    $cism_bld = absolute_path($opts{'cism_bld'});
}
else { # use default value
    $cism_bld = absolute_path($cfg_ref->get('cism_bld'));
}

if (&is_valid_directory( $cism_bld ) or mkdirp($cism_bld)) {
    # If the build directory exists or can be made then set the value...
    $cfg_ref->set('cism_bld', $cism_bld);
}
else {
    die <<"EOF";
** Could not create the specified CISM build directory: $cism_bld
EOF
}

if ($print>=2) { print "Setting CISM build directory to $cism_bld$eol"; }

#-----------------------------------------------------------------------------------------------
# CISM install directory.
my $cism_exedir;
if (defined $opts{'cism_exedir'}) {
    $cism_exedir = absolute_path($opts{'cism_exedir'});
}
else { # use default value
    $cism_exedir = $cism_bld;
}

#-----------------------------------------------------------------------------------------------
# User source directories.
my $usr_src = '';
if (defined $opts{'usr_src'}) {
    my @dirs = split ',', $opts{'usr_src'};
    my @adirs;
    while ( my $dir = shift @dirs ) {
	if (&is_valid_directory( "$dir", allowEnv=>1 ) ) {
	    push @adirs, $dir;
	} else {
	    die "** User source directory does not exist: $dir\n";
	}
    }
    $usr_src = join ',', @adirs;
    $cfg_ref->set('usr_src', $usr_src);
}

if ($print>=2) { print "Setting user source directories to $usr_src$eol"; }

#-----------------------------------------------------------------------------------------------
# configuration cache directory and file.
my $config_cache_dir;
my $config_cache_file;
if (defined $opts{'cachedir'}) {
    $config_cache_dir = absolute_path($opts{'cachedir'});
}
else {
    $config_cache_dir = $cfg_ref->get('cism_bld');
}

if (&is_valid_directory( $config_cache_dir, allowEnv=>1 ) or mkdirp($config_cache_dir)) {
    $config_cache_file = "$config_cache_dir/$opts{'cache'}";
} else {
    die <<"EOF";
** Could not create the specified directory for configuration cache file: $config_cache_dir
EOF
}

if ($print>=2) { print "The configuration cache file will be created in $config_cache_file$eol"; }

#-----------------------------------------------------------------------------------------------
# comp_interface option
if (defined $opts{'comp_interface'}) {
    $cfg_ref->set('comp_interface', $opts{'comp_interface'});
}
my $comp_interface = $cfg_ref->get('comp_interface');
if ($print>=2) { print "Using $comp_interface for comp_interface.$eol"; }

#-----------------------------------------------------------------------------------------------
# Makefile configuration #######################################################################
#-----------------------------------------------------------------------------------------------

#-----------------------------------------------------------------------------------------------
# Determine target OS -- allow cross compilation only if target_os is specified on commandline.
my $target_os = $OSNAME;
if (defined $opts{'target_os'}) {
    $target_os = $opts{'target_os'};
}
$cfg_ref->set('target_os', $target_os);

if ($print>=2) { print "Target OS: $target_os.$eol"; }

#-----------------------------------------------------------------------------------------------
# machine
my $mach_val = $opts{'mach'};

$cfg_ref->set('mach', $mach_val);
my $mach = $mach_val;

if ($print>=2) { print "CCSM machine/compiler: $mach$eol";}

#-----------------------------------------------------------------------------------------------
# For the CPP tokens, start with the defaults (from defaults file) and append the specifications
# from the commandline.  That way the user can override defaults since the commandline versions
# occur last.
my $usr_cppdefs = $cfg_ref->get('cppdefs');
if (defined $opts{'cppdefs'}) {
    $usr_cppdefs .= " $opts{'cppdefs'}";
}
$cfg_ref->set('cppdefs', $usr_cppdefs);

if ($usr_cppdefs and $print>=2) { print "Default and user CPP definitions: \'$usr_cppdefs\'$eol";}

# CPP defines to put on Makefile
my $make_cppdefs = "$usr_cppdefs";

if ($print>=2) { print "CPP definitions set by configure: \'$make_cppdefs\'$eol"; }

#-----------------------------------------------------------------------------------------------
# Write configuration files ####################################################################
#-----------------------------------------------------------------------------------------------

my $fp_filename      = 'Filepath';             # name of output filepath file
my $cpp_filename     = 'CCSM_cppdefs';         # name of output file for cism's cppdefs in ccsm
my $misc_filename    = 'misc.h';               # name of output auxiliary cpp tokens file
my $preproc_filename = 'preproc.h';            # name of output land model cpp tokens file

# Write the filepath file for ccsm.
write_filepath_ccsmbld("$cism_bld/$fp_filename", "cism", $cfg_ref, allowEnv=>1 );
if ($print>=2) { print "creating $cism_bld/$fp_filename\n"; }

# Write the file for cism's cppdefs needed in ccsm.
write_cppdefs("$cism_bld/$cpp_filename", $make_cppdefs);
if ($print>=2) { print "creating $cism_bld/$cpp_filename\n"; }

# Write the configuration file.
$cfg_ref->write_file($config_cache_file, $commandline);
if ($print>=2) { print "creating $config_cache_file\n"; }

#-----------------------------------------------------------------------------------------------
# Finished #####################################################################################
#-----------------------------------------------------------------------------------------------
if ($print>=2) { print "configure done.\n"; }
exit;

#-------------------------------------------------------------------------------

sub write_filepath_ccsmbld
{
    my ($file, $model, $cfg_ref, %opts) = @_;
    my  $fh = new IO::File;

    $fh->open(">$file") or die "** can't open filepath file: $file\n";

    # configuration parameters used to determine paths
    my $usr_src     = $cfg_ref->get('usr_src');
    my $cism_root   = $cfg_ref->get('cism_root');
    my $spmd        = $cfg_ref->get('spmd');
    my $comp_intf   = $cfg_ref->get('comp_interface');
    my $cism_bld    = $cfg_ref->get('cism_bld');

    # Source root
    my $srcdir = "$cism_root";
    if ( ! &is_valid_directory( "$srcdir", %opts )  ) { die "** source directory does not exist: $srcdir\n"; }

    # User specified source directories.
    if ($usr_src  =~ /\S+/) {
       my @dirs = split ',', $usr_src;
       while ( my $dir = shift @dirs ) {
	  print $fh "$dir\n";
       }
    } else {
       print $fh "../SourceMods/src.glc\n";
    }

    my @dirs = ( "source_glc", "source_glimmer-cism", "mpi" );
    #if ( $comp_intf eq "MCT" ) { 
    #  push( @dirs, "cpl_mct" );
    #} elsif ( $comp_intf eq "ESMF" ) { 
    #  push( @dirs, "cpl_esmf" );
    #} else {
    #  push( @dirs, "$comp_intf" );
    #}
    foreach my $dir ( @dirs ) {
       if ( &is_valid_directory( "$srcdir/$dir", %opts )  ) {
	       print $fh "$srcdir/$dir\n";
       } else {
          die "** source directory does not exist: $srcdir/$dir\n";
       }
    }
    if ( ! $spmd ) {
       print $fh "$srcdir/../../../utils/mct/mpi-serial\n";
    }

    $fh->close;
}
#-------------------------------------------------------------------------------

sub write_cppdefs
{
    my ($file, $make_cppdefs) = @_;
    my  $fh = new IO::File;

    $fh->open(">$file") or die "** can't open cpp defs file: $file\n";

    print $fh "$make_cppdefs\n";
    $fh->close;
}

#-------------------------------------------------------------------------------

sub absolute_path {
#
# Convert a pathname into an absolute pathname, expanding any . or .. characters.
# Assumes pathnames refer to a local filesystem.
# Assumes the directory separator is "/".
#
  my $path = shift;
  my $cwd = getcwd();  # current working directory
  my $abspath;         # resulting absolute pathname

# Strip off any leading or trailing whitespace.  (This pattern won't match if
# there's embedded whitespace.
  $path =~ s!^\s*(\S*)\s*$!$1!;

# Convert relative to absolute path.

  if ($path =~ m!^\.$!) {          # path is "."
      return $cwd;
  } elsif ($path =~ m!^\./!) {     # path starts with "./"
      $path =~ s!^\.!$cwd!;
  } elsif ($path =~ m!^\.\.$!) {   # path is ".."
      $path = "$cwd/..";
  } elsif ($path =~ m!^\.\./!) {   # path starts with "../"
      $path = "$cwd/$path";
  } elsif ($path =~ m!^[^/]!) {    # path starts with non-slash character
      $path = "$cwd/$path";
  }

  my ($dir, @dirs2);
  my @dirs = split "/", $path, -1;   # The -1 prevents split from stripping trailing nulls
                                     # This enables correct processing of the input "/".

  # Remove any "" that are not leading.
  for (my $i=0; $i<=$#dirs; ++$i) {
      if ($i == 0 or $dirs[$i] ne "") {
	  push @dirs2, $dirs[$i];
      }
  }
  @dirs = ();

  # Remove any "."
  foreach $dir (@dirs2) {
      unless ($dir eq ".") {
	  push @dirs, $dir;
      }
  }
  @dirs2 = ();

  # Remove the "subdir/.." parts.
  foreach $dir (@dirs) {
    if ( $dir !~ /^\.\.$/ ) {
        push @dirs2, $dir;
    } else {
        pop @dirs2;   # remove previous dir when current dir is ..
    }
  }
  if ($#dirs2 == 0 and $dirs2[0] eq "") { return "/"; }
  $abspath = join '/', @dirs2;
  return( $abspath );
}

#-------------------------------------------------------------------------------

sub subst_env_path {
#
# Substitute for any environment variables contained in a pathname.
# Assumes the directory separator is "/".
#
  my $path = shift;
  my $newpath;         # resulting pathname
  my $nm = "subst_env_path";

# Strip off any leading or trailing whitespace.  (This pattern won't match if
# there's embedded whitespace.
  $path =~ s!^\s*(\S*)\s*$!$1!;

  my ($dir, @dirs2);
  my @dirs = split "/", $path, -1;   # The -1 prevents split from stripping trailing nulls
                                     # This enables correct processing of the input "/".

  foreach $dir (@dirs) {
    if ( $dir =~ m/(^[^\$]*)\$(.*$)/ ) {
        my $startvar = $1;
        my $envvarnm = $2;
        if ( ! defined($ENV{$envvarnm}) ) {
           die "${nm}:: ENV variable $envvarnm is in pathname ($path) -- but NOT defined\n";
        }
        push @dirs2, "$startvar$ENV{$envvarnm}";
    } elsif ( $dir =~ m/\$/) {
        die "${nm}:: malformed ENV variable is in pathname ($path)\n";
    } else {
        push @dirs2, $dir;
    }
  }
  $newpath = join '/', @dirs2;
  return( $newpath );
}

#-------------------------------------------------------------------------------

sub mkdirp {
    my ($dir) = @_;
    my (@dirs) = split /\//, $dir;
    my (@subdirs, $path);

    # if $dir is absolute pathname then @dirs will start with ""
    if ($dirs[0] eq "") { push @subdirs, shift @dirs; }  

    while ( @dirs ) { # check that each subdir exists and mkdir if it doesn't
	push @subdirs, shift @dirs;
	$path = join '/', @subdirs;
	unless (-d $path or mkdir($path, 0777)) { return 0; }
    }
    return 1;
}

#-------------------------------------------------------------------------------

sub version {
# The version is found in CISM's ChangeLog file.
# $cfgdir is set by the configure script to the name of its directory.

    my ($cfgdir) = @_;

    my $logfile = "$cfgdir/../doc/ChangeLog";

    my $fh = IO::File->new($logfile, '<') or die "** can't open ChangeLog file: $logfile\n";

    while (my $line = <$fh>) {

	if ($line =~ /^Tag name:\s*(\w+)/ ) {
	    print "$1\n";
	    exit;
	}
    }

}

#-------------------------------------------------------------------------------

sub is_valid_directory {
#
# Validate that the input is a valid existing directory.
# If allowEnv=>1 expand environment variables.
#
  my ($dir, %opts) = @_;
  my $nm = "is_valid_directory";

  my $valid = 0;
  # Expand environment variables
  if ( $opts{'allowEnv'} ) {
     $dir = subst_env_path( $dir );
  }
  if ( -d $dir ) { $valid = 1; }
  return( $valid );
  
}

#-------------------------------------------------------------------------------

sub print_hash {
    my %h = @_;
    my ($k, $v);
    while ( ($k,$v) = each %h ) { print "$k => $v\n"; }
}

