/*
  This file is part of CDO. CDO is a collection of Operators to manipulate and analyse Climate model Data.

  Author: Oliver Heidmann

*/

#include "processManager.h"
#include "process.h"
#include "cdo_timer.h"
#include "cdo_output.h"
#include "cdo_options.h"
#include "fileStream.h"
#include "factory.h"
#include "util_string.h"

#include <stack>
#include <mutex>
#include <ranges>

static std::mutex processMutex;

static std::string parse_err_msg = "";

static const int IS_OBASE = -1;

extern "C" size_t getPeakRSS();
static void
get_max_memstring(char *p_memstring, size_t memstringLen)
{
  auto memmax = getPeakRSS();
  if (memmax)
    {
      size_t muindex = 0;
      const char *mu[] = { "B", "KB", "MB", "GB", "TB", "PB" };
      size_t nmu = sizeof(mu) / sizeof(char *);
      while (memmax > 9999 && muindex < nmu - 1)
        {
          memmax /= 1024;
          muindex++;
        }
      std::snprintf(p_memstring, memstringLen, " %zu%s", memmax, mu[muindex]);
    }
}

static void
print_benchmarks(double runTime, double readTime, double writeTime, const char *memString)
{
  auto numberOfUsedThreads = get_process_num();
  if (Options::test)
    {
      int in = lround(100 * readTime / runTime);
      int out = lround(100 * writeTime / runTime);
      fprintf(stdout, " [%.2fs%s IO:%d/%d%% %dthread%s]", runTime, memString, in, out, numberOfUsedThreads,
              ADD_PLURAL(numberOfUsedThreads));
    }
  else { fprintf(stdout, " [%.2fs%s]", runTime, memString); }
}

static void
print_processed_values(Process *p_process, double runTime, double readTime, double writeTime)
{
  set_text_color(stdout, GREEN);
  fprintf(stdout, "%s: ", p_process->prompt);
  reset_text_color(stdout);

  auto nvals = p_process->inq_nvals();

  auto nvars = p_process->m_nvars;
  if (nvals > 0)
    {
      fprintf(stdout, "Processed %zu value%s from %d variable%s", nvals, ADD_PLURAL(nvals), nvars, ADD_PLURAL(nvars));
    }
  else if (nvars > 0) { fprintf(stdout, "Processed %d variable%s", nvars, ADD_PLURAL(nvars)); }

  auto ntimesteps = p_process->ntimesteps;
  if ((nvals || nvars) && ntimesteps > 0) fprintf(stdout, " over %d timestep%s", ntimesteps, ADD_PLURAL(ntimesteps));

  if (p_process->m_ID == 0)
    {
      char memString[32] = { "" };
      get_max_memstring(memString, sizeof(memString));
      print_benchmarks(runTime, readTime, writeTime, memString);
    }

  // if (m_nvars > 0 || nvals > 0 || ntimesteps > 0 || m_ID == 0) fprintf(stdout, ".");
  fprintf(stdout, "\n");
}

void
ProcessManager::buildProcessTree(std::vector<std::shared_ptr<Node>> roots)
{
  Debug(PROCESS, "Building process Tree");
  std::shared_ptr<Node> node = roots[0]->isFile ? roots[0]->children[0] : roots[0];

  std::shared_ptr<Process> first_process;
  try
    {
      first_process = create_process(node->oper, split_args(node->arguments));
    }
  catch (std::runtime_error &e)
    {
      cdo_abort("%s: %s",node->oper,e.what());
    }

  if (node->numOut() == IS_OBASE)
    {
      Debug(PROCESS, "Setting obase for %s", node->oper);
      first_process->set_obase(roots[0]->oper);
    }
  else if (node->numOut() > 0)
    {
      for (const auto &n : roots)
        {
          Debug(PROCESS, "adding out files to %s", node->oper);
          first_process->add_file_out_stream(n->oper);
        }
    }

  for (const auto &c : node->children)
    {
      if (c->isFile)
        {
          Debug(PROCESS, "Adding file in stream: %s", c->oper);
          first_process->add_file_in_stream(c->oper);
        }
      else
        {
          auto c_ptr = build_node(c);
          first_process->add_child(c_ptr);
          c_ptr->add_parent(first_process);
        }
    }

  set_process_num(m_processes.size());
  FileStream::enableTimers(m_processes.size() == 1 && Threading::ompNumThreads == 1);
}

std::shared_ptr<Process>
ProcessManager::build_node(std::shared_ptr<Node> parent_node)
{
  Debug(PROCESS, "Building process for %s", parent_node->oper);
  auto parent_process = create_process(parent_node->oper, split_args(parent_node->arguments));
  for (auto child_node : parent_node->children)
    {
      if (child_node->isFile)
        {
          Debug(PROCESS, "Adding file in stream: %s", child_node->oper);
          parent_process->add_file_in_stream(child_node->oper);
        }
      else
        {
          Debug(PROCESS, "Building Process for %s", child_node->oper);
          auto child_process = build_node(child_node);
          parent_process->add_child(child_process);
          child_process->add_parent(parent_process);
        }
    }
  return parent_process;
}

void
ProcessManager::run_processes()
{
  for (auto &idProcessPair : m_processes)
    {
      if (idProcessPair.first)
        {
          /*TEMP*/
          if (!Options::silentMode && (cdo::stdoutIsTerminal || Options::cdoVerbose))
            {
              // MpMO::Print(Green("%s: ") + "Process started", idProcessPair.second->prompt);
              set_text_color(stdout, GREEN);
              fprintf(stdout, "%s: ", idProcessPair.second->prompt);
              reset_text_color(stdout);
              fprintf(stdout, "Process started\n");
            }
          m_threadIDs.push_back(idProcessPair.second->start_thread());
        }
    }
  m_threadIDs.push_back(pthread_self());
  // MpMO::PrintCerr(Green("%s: ") + "xProcess started", get_process_from_id(0).inq_prompt());
  Process *process_zero = get_process_from_id(0).get();

  cdo::timer runTime;
  execute(process_zero);
  if (!Options::silentMode && (cdo::stdoutIsTerminal || Options::cdoVerbose))
    print_processed_values(process_zero, runTime.elapsed(), cdo::readTimer.elapsed(), cdo::writeTimer.elapsed());
}

void
ProcessManager::kill_processes()
{
  for (auto threadID : m_threadIDs)
    {
      if (threadID != pthread_self())
        {
          pthread_cancel(threadID);
          Debug(PROCESS_MANAGER, "process killed: %ld", threadID);
        }
    }
}

void
ProcessManager::clear_processes()
{
  Debug(PROCESS_MANAGER, "Deleting Processes");
  m_processes.clear();
  m_numProcesses = 0;
  m_numProcessesActive = 0;
}

const std::shared_ptr<Process>
ProcessManager::create_process(const std::string &operatorName, const std::vector<std::string> &arguments)
{
  std::shared_ptr<Process> new_process;
  if ((m_numProcesses + 1) >= MAX_PROCESS) { cdo_abort("Limit of %d processes reached!", MAX_PROCESS); }
  auto processID = m_numProcesses++;

  auto it = Factory::find(operatorName, [&processID]() { cdo_abort("Process %d could not be created", processID); });

  auto constructor_function = Factory::get_constructor(it);
  auto success = m_processes.insert(std::make_pair(processID, constructor_function(processID, operatorName, arguments)));
  new_process = success.first->second;
  m_numProcessesActive++;

  return new_process;
}

int
ProcessManager::get_num_processes(void)
{
  std::scoped_lock lock(processMutex);
  int pnums = m_processes.size();
  return pnums;
}

int
ProcessManager::get_num_active_processes(void)
{
  std::scoped_lock lock(processMutex);
  int pnums = m_numProcessesActive;
  return pnums;
}

const std::shared_ptr<Process> &
ProcessManager::get_process_from_id(int p_processID)
{
  std::scoped_lock lock(processMutex);

  auto process = m_processes.find(p_processID);
  if (process == m_processes.end()) cdo_abort("Process with ID: %d not found", p_processID);

  return process->second;
}
