#!/usr/bin/env perl
#-----------------------------------------------------------------------------------------------
#
# build-namelist
#
# The simplest use of build-namelist is to execute it from the build directory where configure
# was run.  By default it will use the config_cache.xml file that was written by 
# mosart.cpl7.template to determine the build time properties of the executable, and will write 
# the files that contain  the output namelists in that same directory. 
#
# Date        Contributor      Modification
# -------------------------------------------------------------------------------------------
# 2012-03-10  Vertenstein      Updated for cesm1.1
#--------------------------------------------------------------------------------------------

use strict;
#use warnings;
#use diagnostics;

use Cwd qw(getcwd abs_path);
use English;
use Getopt::Long;
use IO::File;
use File::Basename;
use Data::Dumper;

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

sub usage {
    die <<EOF;
SYNOPSIS
     build-namelist [options]

     Build the mosart namelist 
OPTIONS
     -help [or -h]            	   Print usage to STDOUT.					    
     -infile "filepath"       	   Specify a file or list of files (comma delimited) 	    
                              	   containing namelists to read values from.			    
     -namelist "namelist"     	   Specify namelist settings directly on the commandline by 	    
                              	   supplying a string containing FORTRAN namelist syntax
                              	      -namelist "&mosart_exp a=b/"		    
     -print "level"           	   Print level for debugging:				    
                              	      0 = silent						    
                              	      1 = regular						    
                              	      2 = verbose						    
     -caseroot                     CASEROOT directory variable
     -scriptsroot                  SCRIPTSROOT directory variable
     -simyr <year>                 Starting year to simulate (default 2000)
     -inst_string                  INST_STRING variable

Note: The precedence for setting the values of namelist variables is (highest to lowest):
      0. namelist values set by specific command-line options
      1. values set on the command-line using the -namelist option,
      2. values read from the file specified by -infile,
      3. values from the namelist defaults file.

EOF
}

#-----------------------------------------------------------------------------------------------
# Set the directory that contains the MOSART configuration scripts.  If the 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 $cmdline = "@ARGV";                 # Command line arguments to script
my $cwd = getcwd();                    # current working directory
my $cfgdir;                            # absolute pathname of directory that contains this script
my $nm = "ProgName::";                 # name to use if script dies
if ($ProgDir) { 
    $cfgdir = abs_path($ProgDir);
} else {
    $cfgdir = $cwd;
}

my $cfg_cache = "config_cache.xml";       # Default name of configuration cache file
my $outdirname = "$cwd";                  # Default name of output directory name

#-----------------------------------------------------------------------------------------------
# Process command-line options.
#
my %opts = ( help        => 0,
	     silent      => 0,
	     caseroot    => 0,
	     scriptsroot => 0,
             simyr       => 2000,
	     inst_string => 0,
	    );

GetOptions(
    "h|help"        => \$opts{'help'},
    "infile=s"      => \$opts{'infile'},
    "namelist=s"    => \$opts{'namelist'},
    "print=i"       => \$opts{'print'},
    "simyr=i"       => \$opts{'simyr'},
    "caseroot=s"    => \$opts{'caseroot'},
    "scriptsroot=s" => \$opts{'scriptsroot'},
    "inst_string=s" => \$opts{'inst_string'},	   
)  or usage();

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

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

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

my $CASEROOT    = $opts{'caseroot'}; 
my $SCRIPTSROOT = $opts{'scriptsroot'};       
my $INST_STRING = $opts{'inst_string'}; 

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

#-----------------------------------------------------------------------------------------------
# Create config_cache.xml file (needed below)

my $config_cache = "$CASEROOT/Buildconf/mosartconf/config_cache.xml";
my  $fh = new IO::File;
$fh->open(">$config_cache") or die "** can't open file: $config_cache\n";
print $fh  <<"EOF";
<?xml version="1.0"?>
<config_definition>
</config_definition>
EOF
$fh->close;
if ($print>=1) { print "Wrote file $config_cache $eol"; }
(-f "config_cache.xml")  or  die <<"EOF";
** $ProgName - Cannot find configuration cache file: config_cache.xml\" **
EOF

#-----------------------------------------------------------------------------------------------
# Make sure we can find required perl modules, definition, and defaults files.
# Look for them under the directory that contains the configure script.

my $cesmroot = abs_path( "$cfgdir/../../../");
my $perl5dir = "$cesmroot/cime/utils/perl5lib";

# The root directory to cesm utils Tools

my $SetupTools_dir = "$cesmroot/cime/scripts/Tools";
(-f "$SetupTools_dir/SetupTools.pm")  or
    fatal_error("Cannot find perl module \"SetupTools.pm\" in directory\n" .
                "\"$SetupTools_dir\" \n");

# The Build::Config module provides utilities to access the configuration information
# in the config_cache.xml file
my $file = "$perl5dir/Build/Config.pm";
(-f "$file")  or  die <<"EOF";
** $ProgName - Cannot find perl module $file **
EOF

# The Build::NamelistDefinition module provides utilities to validate that the output
# namelists are consistent with the namelist definition file
my $file = "$perl5dir/Build/NamelistDefinition.pm";
(-f "$file")  or  die <<"EOF";
** $ProgName - Cannot find perl module $file **
EOF

# The Build::NamelistDefaults module provides a utility to obtain default values of namelist
# variables based on finding a best fit with the attributes specified in the defaults file.
my $file = "$perl5dir/Build/NamelistDefaults.pm";
(-f "$file")  or  die <<"EOF";
** $ProgName - Cannot find perl module $file **
EOF

# The Build::Namelist module provides utilities to parse input namelists, to query and modify
# namelists, and to write output namelists.
(-f "$file")  or  die <<"EOF";
** $ProgName - Cannot find perl module $file **
EOF

# The namelist definition file contains entries for all namelist variables that
# can be output by build-namelist.
my $nl_definition_file = "$cfgdir/namelist_files/namelist_definition_mosart.xml";
(-f "$nl_definition_file")  or  die <<"EOF";
** $ProgName - Cannot find namelist definition file \"$nl_definition_file\" **
EOF
if ($print>=2) { print "Using namelist definition file $nl_definition_file$eol"; }

# The namelist defaults file contains default values for all required namelist variables.
my $nl_defaults_file = "$cfgdir/namelist_files/namelist_defaults_mosart.xml"; 
(-f "$nl_defaults_file")  or  die <<"EOF";
** $ProgName - Cannot find namelist defaults file \"$nl_defaults_file\" **
EOF
if ($print>=2) { print "Using namelist defaults file $nl_defaults_file$eol"; }

#-----------------------------------------------------------------------------------------------
my @dirs = ( "$cfgdir/..", "$perl5dir", "$SetupTools_dir" );
unshift @INC, "$CASEROOT/Tools", @dirs;

require Build::Config;
require Build::NamelistDefinition;
require Build::NamelistDefaults;
require Build::Namelist;
require SetupTools;

#-----------------------------------------------------------------------------------------------
# Create a configuration object from the MOSART config_cache.xml file. 
my $cfg = Build::Config->new('config_cache.xml');

# Create a namelist definition object.  This object provides a method for verifying that the
# output namelist variables are in the definition file, and are output in the correct
# namelist groups.
my $definition = Build::NamelistDefinition->new($nl_definition_file);

# Create a namelist defaults object.  This object provides default values for variables
# contained in the input defaults file.  The configuration object provides attribute
# values that are relevent for the MOSART for which the namelist is being produced.
my $defaults = Build::NamelistDefaults->new( $nl_defaults_file, $cfg);
$defaults->add( "$nl_defaults_file" );

# Create an empty namelist object.  Add values to it in order of precedence.
my $nl = Build::Namelist->new();

#-----------------------------------------------------------------------------------------------
# Process the user input in order of precedence.  At each point we'll only add new
# values to the namelist and not overwrite previously specified specified values which
# have higher precedence.

# Process the -namelist arg.
if (defined $opts{'namelist'}) {
    # Parse commandline namelist
    my $nl_arg = Build::Namelist->new($opts{'namelist'});

    # Validate input namelist -- trap exceptions
    my $nl_arg_valid;
    eval { $nl_arg_valid = $definition->validate($nl_arg); };
    if ($@) {
	die "$ProgName - ERROR: Invalid namelist variable in commandline arg '-namelist'.\n $@";
    }

    # Merge input values into namelist.  Previously specified values have higher precedence
    # and are not overwritten.
    $nl->merge_nl($nl_arg_valid);
}

# Process the -infile arg.
if (defined $opts{'infile'}) {
   foreach my $infile ( split( /,/, $opts{'infile'} ) ) {
      # Parse namelist input from a file
      my $nl_infile = Build::Namelist->new($infile);

      # Validate input namelist -- trap exceptions
      my $nl_infile_valid;
      eval { $nl_infile_valid = $definition->validate($nl_infile); };
      if ($@) {
         die "$ProgName - ERROR: Invalid namelist variable in '-infile' $infile.\n $@";
      }

      # Merge input values into namelist.  Previously specified values have higher precedence
      # and are not overwritten.
      $nl->merge_nl($nl_infile_valid);
   }
}

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

####################################
# Required xml variables           #
####################################

my %xmlvars = ();
SetupTools::getxmlvars($CASEROOT, \%xmlvars);
foreach my $attr (keys %xmlvars) {
  $xmlvars{$attr} = SetupTools::expand_xml_var($xmlvars{$attr}, \%xmlvars);
}

my $DIN_LOC_ROOT    = $xmlvars{'DIN_LOC_ROOT'};
my $MOSART_MODE        = $xmlvars{'MOSART_MODE'};
my $MOSART_FLOOD_MODE  = $xmlvars{'MOSART_FLOOD_MODE'};
my $ROF_GRID        = $xmlvars{'ROF_GRID'};
my $LND_GRID        = $xmlvars{'LND_GRID'};
my $ROF_NCPL        = $xmlvars{'ROF_NCPL'};

(-d $DIN_LOC_ROOT)  or  mkdir $DIN_LOC_ROOT;
if ($print>=2) { 
    print "CESM inputdata root directory: $DIN_LOC_ROOT \n"; 
}

####################################
# namelist group: mosart_inparm       #
####################################

if ($ROF_GRID eq "null") {
    if ($MOSART_MODE ne "NULL") {
	die "$ProgName ERROR:: ROF_GRID null MOSART_MODE not NULL.\n";
    }
}

if ($MOSART_MODE eq "NULL") {
    add_default($nl, 'rtm_mode', 'val'=>"$MOSART_MODE");
}
else {
    # Set Initial conditions if there is a file for this grid and sim-yr
    add_default($nl, 'do_rtm', 'val'=>".true.");
    add_default($nl, 'decomp_option');
    add_default($nl, 'smat_option');
    add_default($nl, 'frivinp_rtm' , 'rof_grid'=>$ROF_GRID );
    add_default($nl, 'ice_runoff'  , 'lnd_grid'=>$LND_GRID );
    add_default($nl, 'rtmhist_mfilt');
    add_default($nl, 'rtmhist_nhtfrq');
    add_default($nl, 'rtmhist_ndens');
    add_default($nl, 'delt_mosart');
#    add_default($nl, 'finidat_rtm' , 'rof_grid'=>$ROF_GRID, 'simyr'=>$opts{'simyr'}, 'nofail'=>1 );
    my $val = ( 3600 * 24 ) / $ROF_NCPL;
    add_default($nl, 'coupling_period', 'val'=>$val);
    if ($print>=2) { 
        print "River Transport Model (MOSART) mode is $MOSART_MODE \n"; 
        print "  MOSART lnd grid is $LND_GRID \n";
        print "  MOSART rof grid is $ROF_GRID \n";
    }
}

#-----------------------------------------------------------------------------------------------
# Validate that the entire resultant namelist is valid
#
$definition->validate($nl);

#-----------------------------------------------------------------------------------------------
# Write output files
#
my $note = "";

# mosart_in
my @groups = qw(mosart_inparm);
my $outfile = "./mosart_in";
$nl->write($outfile, 'groups'=>\@groups, 'note'=>"$note" );
if ($print>=2) { print "Writing mosart namelist to $outfile $eol"; }

# Write out required local input files
check_input_files($nl, $DIN_LOC_ROOT, "$CASEROOT/Buildconf/mosart.input_data_list");

#===============================================================================================
#===============================================================================================
# END OF MAIN SCRIPT
#===============================================================================================
#===============================================================================================

sub add_default {

# Add a value for the specified variable to the specified namelist object.  The variables
# already in the object have the higher precedence, so if the specified variable is already
# defined in the object then don't overwrite it, just return.
#
# This method checks the definition file and adds the variable to the correct
# namelist group.
#
# The value can be provided by using the optional argument key 'val' in the
# calling list.  Otherwise a default value is obtained from the namelist
# defaults object.  If no default value is found this method throws an exception
# unless the 'nofail' option is set true.
#
# Example 1: Specify the default value $val for the namelist variable $var in namelist
#            object $nl:
#
#  add_default($nl, $var, 'val'=>$val)
#
# Example 2: Add a default for variable $var if an appropriate value is found.  Otherwise
#            don't add the variable
#
#  add_default($nl, $var, 'nofail'=>1)
#
#
# ***** N.B. ***** This routine assumes the following variables are in package main::
#  $definition        -- the namelist definition object
#  $defaults          -- the namelist defaults object
#  $DIN_LOC_ROOT      -- CESM inputdata root directory

    my $nl = shift;     # namelist object
    my $var = shift;    # name of namelist variable
    my %opts = @_;      # options

    # If variable has quotes around it
    if ( $var =~ /'(.+)'/ ) {
       $var = $1;
    }
    # Query the definition to find which group the variable belongs to.  Exit if not found.
    my $group = $definition->get_group_name($var);
    unless ($group) {
	my $fname = $definition->get_file_name();
	die "$ProgName - ERROR: variable \"$var\" not found in namelist definition file $fname.\n";
    }

    # check whether the variable has a value in the namelist object -- if so then skip to end
    my $val = $nl->get_variable_value($group, $var);
    if (! defined $val) {

       # Look for a specified value in the options hash

       if (defined $opts{'val'}) {
	   $val = $opts{'val'};
       }
       # or else get a value from namelist defaults object.
       # Note that if the 'val' key isn't in the hash, then just pass anything else
       # in %opts to the get_value method to be used as attributes that are matched
       # when looking for default values.
       else {
	   $val = $defaults->get_value($var, \%opts);

           # Truncate model_version appropriately
   
           if ( $var eq "model_version" ) {
               $val =~ /(URL: https:\/\/[a-zA-Z0-9._-]+\/)([a-zA-Z0-9\/._-]+)(\/bld\/.+)/;
               $val = $2;
           }
       }

       unless ( defined($val) ) {
	   unless ($opts{'nofail'}) {
	       die "$ProgName - No default value found for $var.\n" . 
                   "            Are defaults provided? \n";
	   }
	   else {
	       return;
	   }
       }


       # query the definition to find out if the variable is an input pathname
       my $is_input_pathname = $definition->is_input_pathname($var);

       # The default values for input pathnames are relative.  If the namelist
       # variable is defined to be an absolute pathname, then prepend
       # the CESM inputdata root directory.
       if (not defined $opts{'no_abspath'}) {
	   if (defined $opts{'set_abspath'}) {
	       $val = set_abs_filepath($val, $opts{'set_abspath'});
	   } else {
	       if ($is_input_pathname eq 'abs') {
		   $val = set_abs_filepath($val, $DIN_LOC_ROOT);
	       }
	   }
       }

       # query the definition to find out if the variable takes a string value.
       # The returned string length will be >0 if $var is a string, and 0 if not.
       my $str_len = $definition->get_str_len($var);

       # If the variable is a string, then add quotes if they're missing
       if ($str_len > 0) {
	   $val = quote_string($val);
       }

       # set the value in the namelist
       $nl->set_variable_value($group, $var, $val);
    }

}

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

sub check_input_files {

# For each variable in the namelist which is an input dataset, check to see if it
# exists locally.
#
# ***** N.B. ***** This routine assumes the following variables are in package main::
#  $definition        -- the namelist definition object

    my $nl = shift;     # namelist object
    my $inputdata_rootdir = shift;    # if false prints test, else creates inputdata file
    my $outfile = shift;
    open(OUTFILE, ">>$outfile") if defined $inputdata_rootdir;

    # Look through all namelist groups
    my @groups = $nl->get_group_names();
    foreach my $group (@groups) {

	# Look through all variables in each group
	my @vars = $nl->get_variable_names($group);
	foreach my $var (@vars) {

	    # Is the variable an input dataset?
	    my $input_pathname_type = $definition->is_input_pathname($var);

	    # If it is, check whether it exists locally and print status
	    if ($input_pathname_type) {

		# Get pathname of input dataset
		my $pathname = $nl->get_variable_value($group, $var);
		# Need to strip the quotes
		$pathname =~ s/[\'\"]//g;

		if ($input_pathname_type eq 'abs') {
                    if ($inputdata_rootdir) {
                        print OUTFILE "$var = $pathname\n";
                    }
                    else {
		        if (-e $pathname) {  # use -e rather than -f since the absolute pathname
			                     # might be a directory
			    print "OK -- found $var = $pathname\n";
		        }
		        else {
			    print "NOT FOUND:  $var = $pathname\n";
		        }
                    }
		}
		elsif ($input_pathname_type =~ m/rel:(.+)/o) {
		    # The match provides the namelist variable that contains the
		    # root directory for a relative filename
		    my $rootdir_var = $1;
		    my $rootdir = $nl->get_variable_value($group, $rootdir_var);
		    $rootdir =~ s/[\'\"]//g;
                    if ($inputdata_rootdir) {
                        $pathname = "$rootdir/$pathname";
                        print OUTFILE "$var = $pathname\n";
                    }
                    else {
		        if (-f "$rootdir/$pathname") {
			    print "OK -- found $var = $rootdir/$pathname\n";
		        }
		        else {
			    print "NOT FOUND:  $var = $rootdir/$pathname\n";
		        }
                    }
		}
	    }
	}
    }
    close OUTFILE if defined $inputdata_rootdir;
    return 0 if defined $inputdata_rootdir;
}

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

sub set_abs_filepath {

# check whether the input filepath is an absolute path, and if it isn't then
# prepend a root directory

    my ($filepath, $rootdir) = @_;

    # strip any leading/trailing whitespace
    $filepath =~ s/^\s+//;
    $filepath =~ s/\s+$//;
    $rootdir  =~ s/^\s+//;
    $rootdir  =~ s/\s+$//;

    # strip any leading/trailing quotes
    $filepath =~ s/^['"]+//;
    $filepath =~ s/["']+$//;
    $rootdir =~ s/^['"]+//;
    $rootdir =~ s/["']+$//;

    my $out = $filepath;
    unless ( $filepath =~ /^\// ) {  # unless $filepath starts with a /
	$out = "$rootdir/$filepath"; # prepend the root directory
    }
    return $out;
}

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

sub valid_option {

    my ($val, @expect) = @_;
    my ($expect);

    $val =~ s/^\s+//;
    $val =~ s/\s+$//;
    foreach $expect (@expect) {
	if ($val =~ /^$expect$/i) { return $expect; }
    }
    return undef;
}

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

sub quote_string {
    my $str = shift;
    $str =~ s/^\s+//;
    $str =~ s/\s+$//;
    unless ($str =~ /^['"]/) {        #"'
        $str = "\'$str\'";
    }
    return $str;
}




