/*******************************TRICK HEADER******************************
PURPOSE: (Provides the front-end interface to the monte-carlo model)

PROGRAMMERS:
  (((Gary Turner) (OSR) (October 2019) (Antares) (Initial)))
  (((Isaac Reaves) (NASA) (November 2022) (Integration into Trick Core)))
**********************************************************************/
#include "trick/mc_master.hh"

#include <iterator> // std::prev
#include <fstream>  // std::ofstream
#include <cstdlib>  // system
#include "trick/message_type.h"
#include "trick/message_proto.h"
#include "trick/exec_proto.h"


/*****************************************************************************
Constructor
*****************************************************************************/
MonteCarloMaster::MonteCarloMaster(
    std::string location_)
  :
  active(false),
  generate_dispersions(true),
  run_name(),
  monte_dir(),
  input_file_name("input.py"),
  generate_meta_data(false),
  generate_summary(true),
  minimum_padding(0),
  monte_run_number(0),
  input_files_prepared(false),
  location(location_),
  variables(),
  num_runs(0)
{}

/*****************************************************************************
activate
Purpose:()
*****************************************************************************/
void
MonteCarloMaster::activate(
    std::string run_name_)
{
  run_name = run_name_;
  monte_dir = "MONTE_"+run_name;
  active = true;
}

/*****************************************************************************
prepare_input_files
Purpose:(Creates the top-level MONTE_<run_name> directory, clearing out any
           existing content.
         Creates the RUN_<run_num> subdirectories
         Creates the monte_<input.py> in each RUN_<run_num> subdirectory.)
*****************************************************************************/
bool
MonteCarloMaster::prepare_input_files()
{
  if (input_files_prepared) {
    std::string message = 
      std::string("File: ") + __FILE__ + ", Line: " +
      std::to_string(__LINE__) + ", Invalid sequence\n" + "Attempted to "
      "generate a set of input files, but this action has\nalready been "
      "completed. Keeping the original set of input files.\nIgnoring "
      "the later instruction.\n";
    message_publish(MSG_ERROR, message.c_str());
    return true;
  }

  // Create the new MONTE_ dir name where runs will go
  std::string command = "mkdir -p "+monte_dir;
  system( command.c_str());
  command = "rm -rf "+monte_dir+"/RUN_*";
  // TODO should we check return code for failure here? -Jordan 2/2020
  system( command.c_str());

  // Based on the number of runs, generate an appropriately-sized string to
  // contain the run number.
  // E.g. For 1-10 runs, need only 1 numeric character to supply:
  //         RUN_0, RUN_1, RUN_2, ..., RUN_9
  //      For between 10001-100000 runs, need 5 numeric characters to supply:
  //         RUN_00000, RUN_00001, ..., RUN_99999
  // This string length can be set to a MINIMUM value with the variable
  // minimum_padding.
  int max_length = std::to_string(num_runs-1).size();
  if (max_length < minimum_padding) {
    max_length = minimum_padding;
  }
  std::string run_num_base(max_length, '0');


  // create the master list of varaibles being recorded in the monte_values
  // files.  note that this is not all the variables, only those whose values
  // are being recorded ina s eparate file for every run.  For a full list of
  // variables and their type, see the meta-data file.
  if (generate_summary) {
    // Create the summary file and variable list
    std::string filename = monte_dir + "/monte_variables";
    std::ofstream variable_list(filename);
    // Check for success of file creation
    if (!variable_list.is_open()) {
      std::string message = std::string("File: ") + __FILE__ + ", Line: " +
        std::to_string(__LINE__) + ", I/O error\nUnable to open the " +
        "variable summary files for writing.\nDispersion summary will not " +
        "be generated.\n";
      message_publish(MSG_ERROR, message.c_str());
      generate_summary = false;
    }
    else {
      // Write the variable list
      variable_list << "run_number\n";
      for (auto var_it : variables) {
        if (var_it->include_in_summary) {
          variable_list << var_it->get_variable_name()
                        << ", " << var_it->units << "\n";
        }
      }
      variable_list.close();
    }
  }

  // Process each input file one at a time, and write all variables into each
  // file before moving on to the next file. This is better than trying to
  // keep a large number of files open so each variable can be written into
  // all files before moving on to the next variable.
  for (unsigned int run_num = 0; run_num < num_runs; ++run_num) {
    std::string run_num_str(run_num_base);
    std::string run_num_str_partial = std::to_string(run_num);
    int length = run_num_str_partial.size();

    // I'm going to be replacing contents of the string of zeros with the
    // run-number; make sure the run-number doesn't contain more characters
    // than in the string of zeros.  Because the zeros-string is as long as
    // the largest number, this should always pass.
    // Unreachable code in current implementation.  run_num_str is sized to
    // accommodate run_num_base, which ahs been given as many zeroes as the
    // number of characters in the largest run number.
    if (run_num_str_partial.size() > run_num_str.size()) {
      std::string message = 
        std::string("File: ") + __FILE__ + ", Line: " + 
        std::to_string(__LINE__) + ", Sizing Error\nAttempted to create a " +
        "filename with a run-number that exceeds the\npre-generated size " +
        "(e.g. trying to fit the number 10000 into 4 characters.\nThis " +
        "should never happen.\n";
      message_publish(MSG_ERROR, message.c_str());
      exec_terminate_with_return(1, __FILE__, __LINE__, message.c_str());
    }
    // else
    run_num_str.replace( max_length - length, length, run_num_str_partial);

    // Create the directories.
    command = "mkdir -p " +monte_dir+"/RUN_"+run_num_str;
    system( command.c_str());

    // Next write the input file into the created directory
    std::string filename_root = monte_dir + "/RUN_" + run_num_str + "/monte_";
    std::string filename = filename_root + input_file_name;
    std::ofstream input_file(filename);
    // Check for success of file-open using ofstream's failbit.
    if (input_file.fail()) {
      std::string message = 
        std::string("File: ") + __FILE__ + ", Line: " +
        std::to_string(__LINE__) + ", I/O error\nUnable to open file " + 
        filename.c_str() + " for writing.";
      message_publish(MSG_ERROR, message.c_str());
      exec_terminate_with_return(1, __FILE__, __LINE__, message.c_str());
    }

    // print the default (common) content to the top of the file and add the
    // run-number identification for any variables that may depend on this.
    input_file <<
    location << ".active = True"
    "\n" << location << ".generate_dispersions = False\n"
    "\nexec(open('"<<run_name<<"/"<<input_file_name<<"').read())"
    "\n" << location << ".monte_run_number = "<< run_num <<"\n";

    // Process all monte-carlo variables for this run.  Note -- each variable
    // has its own random generator, so the insertion of other variables
    // cannot interfere with the random number generation sequence.
    for (auto var_it : variables) {
      var_it->generate_assignment();
      input_file << var_it->get_command();
    }
    input_file << "\n";
    input_file.close();

    // Generate the run-level dispersions list
    if (generate_summary) {
      std::ofstream disp_list(filename_root + "values");
      if (disp_list.is_open()) {
        disp_list << run_num_str;
        // Print the run number at the beginning of the line
        for (auto var_it : variables) {
          if (var_it->include_in_summary) {
            disp_list    << ", " << var_it->get_assignment();
          }
        }
        disp_list << "\n";
        disp_list.close();
      }
      // Unreachable code in current implementation.
      else {
        std::string message = 
          std::string("File: ") + __FILE__ + ", Line: " + 
          std::to_string(__LINE__) + ", Output failure\nFailed to record " +
          "summary data for run " + run_num_str.c_str() + ".\n";
        message_publish(MSG_ERROR, message.c_str());
        generate_summary = false;
      }
    }
  }

  // Consolidate all individual run monte_values files.
  if (generate_summary) {
    std::string all_runs_filename = monte_dir + "/monte_values_all_runs";
    command = "rm -f " + all_runs_filename;
    system( command.c_str());
    command = "cat " + monte_dir + "/RUN_*/monte_values >> " +
                       all_runs_filename;
    system( command.c_str());
    command = "for r in " + monte_dir + "/RUN_*;" +
              "do ln -s ../monte_variables $r/monte_variables; done";
    system( command.c_str());
  }


  input_files_prepared = true;
  return true;
}

/*****************************************************************************
add_variable
Purpose:(Adds a pointer to an instantiated MonteCarloVariable to the
         master-list.  This master-list will be processed in generating the
         random assignments that are recorded in the monte_input.py files.)
*****************************************************************************/
void
MonteCarloMaster::add_variable(
    MonteCarloVariable & variable)
{
  if (input_files_prepared) {
    std::string message = 
      std::string("File: ") + __FILE__ + ", Line: " + 
      std::to_string(__LINE__) + ", Invalid sequence\nAttempted to add a " +
      "new variable " + variable.get_variable_name().c_str() + " to run " + 
      run_name.c_str() + ", but the input files have already been " +
      "generated.\nCannot modify input files to accommodate this new " +
      "variable.\nAddition of variable rejected.\n";
    message_publish(MSG_ERROR, message.c_str());
    return;
  }

  // check for uniqueness
  for (auto var_it = variables.begin(); var_it != variables.end(); ++var_it) {
    if ( (**var_it).get_variable_name() == variable.get_variable_name()) {
      std::string message = 
        std::string("File: ") + __FILE__ + ", Line:  "
        + std::to_string(__LINE__) + ",  Duplicated variable.\nAttempted " +
        "to add two settings for variable " + 
        variable.get_variable_name().c_str() + 
        ".\nTerminating to allow resolution of which setting to use.\n";
      message_publish(MSG_ERROR, message.c_str());
      exec_terminate_with_return(1, __FILE__, __LINE__, message.c_str());
    }
  }

  // if the variable is of type MonteCarloVariableFile, check for
  // other MonteCarloVariableFiles that use the same file.
  MonteCarloVariableFile * file_variable =
       dynamic_cast< MonteCarloVariableFile *> (&variable);
  if (file_variable != NULL) {
    std::string filename = file_variable->get_filename();
    bool found_file = false;
    for (auto it = file_list.begin(); it != file_list.end(); ++it) {
      if (filename == (*it).first) {
        (*it).second->register_dependent( file_variable);
        found_file = true;
        break;
      }
    }
    if (!found_file) {
      std::pair< std::string, MonteCarloVariableFile *>
                                   new_pair(filename, file_variable);
      file_list.push_back( new_pair);
    }
  }

  // Finally, add this variable to the list
  variables.push_back(&variable);
}

/*****************************************************************************
find_variable
Purpose:
   Get a pointer to a MonteCarloVariable instance based on its "name" -- i.e.
   the variable for which it is generating a value.
Limitations:
    - Only returns the base-class pointer, so this has limited
      versatility.  If the desire is to modify the distribution parameters
      or other characteristic of a MCVariable after it has been
      generated, the returned pointer may require an additional
      dynamic-cast to make the characteristics of the distribution type
      available.
    - Return value must be checked for NULL
*****************************************************************************/
MonteCarloVariable *
MonteCarloMaster::find_variable( std::string var_name)
{
  for (auto it : variables) {
    if (var_name == it->get_variable_name()) {
      return it;
    }
  }
  std::string message = 
    std::string("File: ") + __FILE__ + ", Line: " + 
    std::to_string(__LINE__) + ", Invalid name\nCould not find MonteCarlo " +
    "variable with name " + var_name.c_str() + 
    ".\nReturning a NULL pointer.\n";
  message_publish(MSG_ERROR, message.c_str());
  return NULL;
}


/*****************************************************************************
remove_variable
Purpose:(remove a variable from distribution after it has been added)
Limitation:(Must be run before "execute"; once the files have been generated, it is too late to remove a variable)
*****************************************************************************/
void
MonteCarloMaster::remove_variable( std::string var_name)
{
  // NOTE - cannot use find_variable(...); that returns a pointer to the
  // variable and this method needs the list iterator addressing the variable.
  for (auto it = variables.begin(); it != variables.end(); ++it) {
    if (var_name == (*it)->get_variable_name()) {
      variables.erase(it);
      return;
    }
  }
  std::string message = 
    std::string("File: ") + __FILE__ + ", Line: " + 
    std::to_string(__LINE__) + ", Invalid name\nAttempt to remove " +
    "MonteCarlo variable with name " + var_name.c_str() + " FAILED.\nDid " +
    "not find a variable with that name.\n";
  message_publish(MSG_WARNING, message.c_str());
}


/*****************************************************************************
set_num_runs
Purpose:(Sets the intended number of runs for this scenario)
*****************************************************************************/
void
MonteCarloMaster::set_num_runs(
  unsigned int num_runs_)
{
  if (input_files_prepared) {
    std::string message = 
      std::string("File: ") + __FILE__ + ", Line: " + 
      std::to_string(__LINE__) + ", Invalid sequence\nAttempted to set the " +
      "number of runs to " + std::to_string(num_runs_) + ", but the " +
      "input files have\nalready been generated.";
    message_publish(MSG_ERROR, message.c_str());
  }
  else {
    num_runs = num_runs_;
  }
}

/*****************************************************************************
execute
Purpose:(The main executive.  This should be run as an initialization-class
         job very early in the initialization cycle, after all the input
         processor operations have completed.)
*****************************************************************************/
void
MonteCarloMaster::execute()
{
  if (!active || !generate_dispersions) {
    return;
  }

  for (auto it = file_list.begin(); it != file_list.end(); ++it) {
    (*it).second->initialize_file();
  }

  prepare_input_files();

  if (generate_meta_data) {
    collate_meta_data();
  }

  // clean up any lingering aspects of the MonteCarloVariable instances.
  std::list<MonteCarloVariable *>::iterator var_it = variables.begin();
  for (; var_it != variables.end(); ++var_it) {
    (**var_it).shutdown();
  }

  std::string message =
    "\nMonte-Input files generated in "+ monte_dir +
    "\nRuns can now be launched using the generated monte-input "+
    "files and\nmanaged with an external load-management system, "+
    "such as SLURM.\n"+
    "This simulation is complete.  Exiting.\n";
  exec_terminate_with_return(0, __FILE__, __LINE__,message.c_str());
}

/*****************************************************************************
collate_meta_data
Purpose:(Generates an output of metadata describing the variables, their
         types, and other identifying characteristics)
*****************************************************************************/
void
MonteCarloMaster::collate_meta_data()
{
  std::string filename = "MonteCarlo_Meta_data_output";
  if (input_files_prepared) { // so directory exists
    filename = monte_dir + "/" + filename;
  }

  std::ofstream meta_data( filename);
  // Check for success of file-open.
  if (meta_data.fail()) {
    std::string message = 
      std::string("File: ") + __FILE__ + ", Line: " +
      std::to_string(__LINE__) + ", I/O error\nUnable to open file " + 
      filename.c_str() + " for writing.\nAborting generation of meta-data.\n";
    message_publish(MSG_WARNING, message.c_str());
    return;
  }

  if (!input_files_prepared) {
    meta_data << "Generating meta-data on the current configuration.\n" <<
      "The input files have not yet been generated which means this\n" <<
      "configuration has not been finalized and is subject to change.\n" <<
      "Sending meta-data to top-level directory.\n";
  }

  // Capture and alphabetize all variable names with their respective variable
  // type; count the number of each type.
  std::list< std::string > variable_details;
  std::list< std::string > exec_file_names;
  std::list< std::pair < unsigned int, std::string> > random_variables;
  unsigned int count_calc = 0;
  unsigned int count_const = 0;
  unsigned int count_exec = 0;
  unsigned int count_presc = 0;
  unsigned int count_rand = 0;
  unsigned int count_undef = 0;

  for (auto const * var_it : variables) {
    switch (var_it->get_type()) {
    // Unreachable case in current implementation.
    // All current variable classes have been given a "type"
    default:
      variable_details.push_back(var_it->summarize_variable());
      count_undef++;
      break;
    case MonteCarloVariable::Calculated:
      variable_details.push_back (var_it->summarize_variable());
      count_calc++;
      break;
    case MonteCarloVariable::Constant:
      variable_details.push_back (var_it->summarize_variable());
      count_const++;
      break;
    case MonteCarloVariable::Execute:
      exec_file_names.push_back (var_it->summarize_variable());
      count_exec++;
      break;
    case MonteCarloVariable::Prescribed:
      variable_details.push_back (var_it->summarize_variable());
      count_presc++;
      break;
    case MonteCarloVariable::Random:
      variable_details.push_back (var_it->summarize_variable());
      count_rand++;
      std::pair< unsigned int, std::string> var(var_it->get_seed(),
                                                var_it->get_variable_name());
      random_variables.push_back(var);
      break;
    }
  }
  variable_details.sort();

  meta_data <<
    "\n\n*************************** SUMMARY **************************\n" <<
    variables.size() << " total assignments\n  -  " <<
    count_const << " constant values\n  -  " <<
    count_calc << " calculated variables\n  -  " <<
    count_presc << " prescribed (file-based) variables\n  -  " <<
    count_rand << " random variables\n  -  " <<
    count_exec << " files for execution\n  -  " <<
    count_undef << " variables of undefined type" <<
    "\n\n********************* LIST OF VARIABLES, TYPES****************\n";

  std::list< std::string >::iterator var_name_it = variable_details.begin();
  for (; var_name_it != variable_details.end(); ++var_name_it) {
    meta_data << (*var_name_it) << "\n";
  }
  meta_data <<
  "**************************************************************\n";

  if (!exec_file_names.empty()) {
    meta_data <<
    "\n\n*********** LIST OF EXECUTABLE FILES AND FUNCTIONS ***********\n";
    for (auto const & var_name_it_temp : exec_file_names) {
      meta_data << var_name_it_temp << "\n***\n";
    }
    meta_data <<
    "**************************************************************\n";
  }

  if (!file_list.empty()) {
    meta_data <<
    "\n\n***** LIST OF DATA FILES AND THE VARIABLES THEY POPULATE *****";
    for (auto const & file_it : file_list) {
      meta_data << "\n******\n" << file_it.first << "\n";
      const std::list<MonteCarloVariableFile*> & dependents =
                                             file_it.second->get_dependents();
      for (auto const & itv : dependents) {
        meta_data << itv->get_column_number() << "  " <<
                     itv->get_variable_name() << "\n";
      }
    }
    meta_data <<
    "**************************************************************\n";
  }

  // Need to check the seeds on the random variables for inadvertent
  // correlation between variables.  If there are no random variables, there is
  // nothing to do.
  if (!random_variables.empty()) {
    random_variables.sort(seed_sort);

    meta_data <<
       "\n\n*****Duplicate seeds; check for intentional correlations*****\n";
    std::list< std::pair< unsigned int, std::string> >::iterator rand_it =
                                                      random_variables.begin();
    unsigned int prev_seed = (*rand_it).first;
    // start checking at the second element ... obviously the first element
    // doesn't match its previous value, and starting with the second element
    // guarantees that there will be a previous element.
    ++rand_it;
    bool in_duplicate = false;
    for (; rand_it != random_variables.end(); ++rand_it) {
      // if this seed matches the value of the previous seed, need to
      // record it and the variable associated with it.
      // Also record the variable associated with the previous list entry (i.e.
      // the one with the same seed), but need to be careful here in the case
      // of having 3 (or more) variables with matching seeds to avoid
      // multiple records being made of the middle duplicates.
      if ((*rand_it).first == prev_seed) {
        if (!in_duplicate) {
          // if this is the first match for a given seed, record the previous
          // entry as well.
          meta_data << (*std::prev(rand_it)).first << "     " <<
                       (*std::prev(rand_it)).second << "\n";
          in_duplicate = true;
        }
        // Then record this entry for all matches.
        meta_data << (*rand_it).first << "     " << (*rand_it).second << "\n";
      }
      else {
        in_duplicate = false;
        prev_seed = (*rand_it).first;
      }
    }
    meta_data <<
    "**************************************************************\n";
    meta_data <<
      "\n\n************************  ALL SEEDS *************************\n";
    for (rand_it = random_variables.begin();
         rand_it != random_variables.end();
         ++rand_it) {
      meta_data << (*rand_it).first << "     " << (*rand_it).second << "\n";
    }
  }
  meta_data.close();
}
