#include "processManager.h"
#include "process.h"
#include "cdo_output.h"
#include "cdo_options.h"
#include "mpmo_color.h"
#include "fileStream.h"

#include <stack>
#include <mutex>

static std::mutex processMutex;

static std::string parse_err_msg = "";

void
ProcessManager::runProcesses()
{
  for (auto &idProcessPair : m_processes)
    {
      if (idProcessPair.first != 0)
        {
          /*TEMP*/
          if (Options::silentMode == 0)
            {
              MpMO::PrintCerr(Green("%s: ") + "Process started", idProcessPair.second.prompt);
            }
          m_threadIDs.push_back(idProcessPair.second.run());
        }
    }
  m_threadIDs.push_back(pthread_self());
  // MpMO::PrintCerr(Green("%s: ") + "xProcess started", getProcess(0).inqPrompt());
  getProcess(0).m_module.func(&(getProcess(0)));
}

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

int
ProcessManager::getNumVariableInputOperators()
{
  int num = 0;
  for (auto &process : m_processes)
    {
      if (process.second.inputIsVariable()) num++;
    }
  return num;
}

void
ProcessManager::validateProcesses()
{
  for (auto &process : m_processes)
    {
      process.second.checkStreamCnt();
      auto stat = process.second.m_status;
      if (stat != ProcessStatus::Ok)
        {
          if (stat == ProcessStatus::TooFewStreams || stat == ProcessStatus::TooManyStreams)
            if (getNumVariableInputOperators() > 1)
              {
                cdoWarning("Did you forget to use '[' and/or ']' for multiple variable input operators?");
                cdoWarning("use option --variableInput, for description");
              }
          process.second.handleProcessErr();
        }
    }
}

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

Process *
ProcessManager::createProcess(const char *command)
{
  Debug(PROCESS_MANAGER, "Creating new process for command: %s with ID: %d ", command, m_numProcesses);
  int processID = m_numProcesses++;

  const char *operatorName = get_original(command);
  auto success = m_processes.insert(std::make_pair(processID, Process(processID, operatorName, command)));
  if (!success.second)
    {
      cdoAbort("Process %d could not be created", processID);
    }

  m_numProcessesActive++;
  Debug(PROCESS_MANAGER, "m_numProcessesActive: %d", m_numProcessesActive);

  if (processID >= MAX_PROCESS) cdoAbort("Limit of %d processes reached!", MAX_PROCESS);

  return &success.first->second;
}

int
ProcessManager::processNums(void)
{
  std::unique_lock<std::mutex> locked_mutex(processMutex);
  int pnums = m_processes.size();
  return pnums;
}

int
ProcessManager::processNumsActive(void)
{
  std::unique_lock<std::mutex> locked_mutex(processMutex);
  int pnums = m_numProcessesActive;
  return pnums;
}

/**
 * Handles process Creation for cdo command p_argvEntry
 * Adds \p p_parentProcess as parent to the newly created process.
 * Adds newly created process to \p p_parentProcess.
 * Also checks if \p p_parentProcess accepts another processes streams
 * as input and exits with error message if not.
 */
Process *
ProcessManager::addProcess(Process *p_parentProcess, const char *argvEntry)
{
  if (p_parentProcess->m_module.restrictions == FilesOnly)
    {
      cdoAbort("operator -%s can not be used with pipes.", p_parentProcess->operatorName);
    }
  Process *newProcess = createProcess(argvEntry);
  if (newProcess->m_module.streamOutCnt == 0)
    {
      cdoAbort("operator -%s can not take -%s  with 0 outputs as input", p_parentProcess->operatorName, newProcess->operatorName);
    }

  Debug(PROCESS_MANAGER, "Adding new child %s to %s", newProcess->getCommand(), p_parentProcess->getCommand());
  p_parentProcess->addChild(newProcess);
  newProcess->addParent(p_parentProcess);
  return newProcess;
}

std::set<std::string>
ProcessManager::handleFirstOperator(int p_argcStart, int argc, const char **argv, Process *p_rootProcess)
{
  std::set<std::string> files;
  for (int i = p_argcStart; i < argc; i++)
    {
      Debug(PROCESS_MANAGER, "Creating new pstream for output file: %s", argv[i]);
      if (strcmp(argv[i], "]") == 0)
        {
          cdoAbort("missing output file");
        }
      if (argv[i][0] == '-')
        {
          cdoAbort("missing Output file, did you add a '-' to a file?");
        }
      p_rootProcess->addFileOutStream(argv[i]);
      files.insert(argv[i]);
    }
  return files;
}

void
ProcessManager::checkSingleBracketOnly(const char *p_argvEntry, char p_bracketType)
{
  if (strlen(p_argvEntry) != 1)
    {
      cdoAbort("Only single %c allowed", p_bracketType);
    }
}

void
ProcessManager::createProcesses(int argc, const char **argv)
{
  ParseStatus parseStatus = createProcessesFromInput(argc, argv);
  if (parseStatus != ParseStatus::Ok)
    {
      // Program Exits here
      handleParseError(parseStatus);
    }
  validateProcesses();
}

#include <algorithm>
std::string
getStackAsString(std::stack<Process *> p_stack)
{
  std::string stackString = "";
  std::vector<std::string> strVec;
  while (!p_stack.empty())
    {
      strVec.push_back(p_stack.top()->operatorName);
      p_stack.pop();
    }
  std::reverse(strVec.begin(), strVec.end());
  for (auto strEle : strVec)
    {
      stackString += " " + strEle;
    }
  return stackString;
}

/* comment FOR DEVELOPERS ONLY (do not include in docu)
 *
 * This is the so to speak parser for cdo console inputs.
 *
 *  This parser runs over every argv that comes after the cdo options.  The
 *  fist thing that is done is processing the first operator, since this
 *  operator is the only one that has output files we can remove the output
 *  file from out argv by limiting the argc. Obase operators are treated as if
 *  they have a single output since the operator itself takes care of writing
 *  and creating the files it needs for its output. We also create the first
 *  process for the operator and push it on out stack.  Our stack will contain
 *  all operators that do not have all in- or output they need.  After the
 *  first operator is handled the parser will go over each other element in
 *  argv.  Here 4 cases can happen. Only one of the 4 will happen in one
 *  iteration.
 *
 *  If an operator is found we create a new process and add this process as
 *  child to the process on top of the stack. Likewise we add the top process
 *  as parent to the new process. Then the new Process is added to the stack.
 *
 *  Does the argv element represent a file (indicated by the missing '-' in
 *  argv[i][0]) we create a file stream and add it to the process at the top of
 *  the stack.
 *
 *  In case of a '[' or ']' we check if there is only one bracket since we
 *  decided to not allow multiple brackets in the same argv entry.Then we add
 *  ('[') or remove (']') the top of the process stack to a set (named
 *  bracketOperators) which will keep track of which operators used a bracket.
 *  This stack allows to 'mark' an operator so that it is only removed in case
 *  of a ']'.  The ']' indicates that the top process should be removed.and
 *  that it SHOULD have the correct number of inputs.
 *
 *  At the end of each iteration we remove all operators that have all their
 *  inputs AND are not contained in out bracket operators set. So a not closed
 *  bracket will cause a wanted miss function of the parser as the operator
 *  will not be removed and more inputs will be added. This will be found later
 *  by our routine (Process::validate) that checks if every process has the
 *  correct number of inputs and outputs and will throw an error.
 */

ParseStatus
ProcessManager::createProcessesFromInput(int argc, const char **argv)
{
  Debug(PROCESS_MANAGER, "== Process Creation Start ==");
  Debug(PROCESS_MANAGER, "operators: %s", argvToString(argc, argv));

  Process *root_process = createProcess(argv[0]);
  int cntOutFiles = (int) root_process->m_module.streamOutCnt;

  std::set<std::string> files;

  unsigned int lastNonOutputIdx = argc - cntOutFiles;
  if (cntOutFiles == -1)
    {
      root_process->m_obase = argv[argc - 1];
      cntOutFiles = 1;
      lastNonOutputIdx = argc - 1;
    }
  else
    {
      if (lastNonOutputIdx <= 0)
        {
          return ParseStatus::MissingOutFile;
        }
      files = handleFirstOperator(lastNonOutputIdx, argc, argv, root_process);
    }

  Process *currentProcess;
  std::stack<Process *> processStack;
  std::set<Process *> bracketOperators;
  const char *argvEntry;
  int unclosedBrackets = 0;
  unsigned int idx = 1;

  processStack.push(root_process);
  while (!processStack.empty() && idx < lastNonOutputIdx)
    {
      Debug(PROCESS_MANAGER, "%s", getStackAsString(processStack));
      currentProcess = processStack.top();
      Debug(PROCESS_MANAGER, "iteration %d, current argv:%s, currentProcess: ", idx, argv[idx], currentProcess->m_operatorCommand);

      argvEntry = argv[idx];
      //------------------------------------------------------
      // case: operator
      if (argvEntry[0] == '-')
        {
          Debug(PROCESS_MANAGER, "Found new Operator: %s", argvEntry);
          currentProcess = addProcess(currentProcess, argvEntry);
          if (currentProcess->m_module.restrictions == OnlyFirst)
            {
              parse_err_msg += "Operator " + std::string(argvEntry) + " can only be used if it is first in the operator chain";
              return ParseStatus::OperatorNotFirst;
            }
          processStack.push(currentProcess);
        }
      // - - - - - - - - - - - - - - - - - - - - - - - - - - -
      // case: bracket start
      else if (argvEntry[0] == '[')
        {
          checkSingleBracketOnly(argvEntry, '[');
          bracketOperators.insert(currentProcess);
          unclosedBrackets++;
        }
      // - - - - - - - - - - - - - - - - - - - - - - - - - - -
      // case: bracket end
      else if (argvEntry[0] == ']')
        {
          checkSingleBracketOnly(argvEntry, ']');
          unclosedBrackets--;
          bracketOperators.erase(processStack.top());
          processStack.pop();
        }
      // - - - - - - - - - - - - - - - - - - - - - - - - - - -
      // case: file
      else if (currentProcess->m_module.streamInCnt != 0)
        {
          Debug(PROCESS_MANAGER, "adding in file to %s", currentProcess->operatorName);
          if (files.find(argvEntry) == files.end())
            {
              currentProcess->addFileInStream(argvEntry);
            }
          else
            {
              parse_err_msg += std::string(argvEntry) + " is used as an output, files can not be used as in- and output at the same time";
              return ParseStatus::FileIsInAndOutput;
            }
        }
      // -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
      // remove finished
      while (!processStack.empty() && processStack.top()->hasAllInputs()
             && bracketOperators.find(processStack.top()) == bracketOperators.end())
        {
          Debug(PROCESS_MANAGER, "Removing %s from stack", processStack.top()->operatorName);
          processStack.pop();
        }
      //------------------------------------------------------
      idx++;
    }
  //---------------------------------------------------------------
  if (unclosedBrackets > 0)
    {
      return ParseStatus::ClosingBracketMissing;
    }
  if (unclosedBrackets < 0)
    {
      return ParseStatus::OpenBracketMissing;
    }
  if (idx < lastNonOutputIdx)
    {
      if (argv[idx][0] == ']')
        {
          return ParseStatus::OpenBracketMissing;
        }
      return ParseStatus::UnprocessedInput;
    }

  Debug(PROCESS_MANAGER, "== Process Creation End ==");

  setProcessNum(m_processes.size());
  FileStream::enableTimers(m_processes.size() == 1 && Threading::ompNumThreads == 1);
  return ParseStatus::Ok;
}

void
ProcessManager::handleParseError(ParseStatus p_errCode)
{
  switch (p_errCode)
    {
    case ParseStatus::ClosingBracketMissing:
      {
        cdoAbort("Missing ']'.");
        break;
      }
    case ParseStatus::OpenBracketMissing:
      {
        cdoAbort("Missing '['.");
        break;
      }
    case ParseStatus::UnprocessedInput:
      {
        cdoAbort("Unprocessed Input, could not process all Operators/Files");
        break;
      }
    case ParseStatus::MissingOutFile:
      {
        cdoAbort("Missing out file for first operator");
        break;
      }
    case ParseStatus::OperatorNotFirst:
      {
        cdoAbort("%s", parse_err_msg);
        break;
      }
    case ParseStatus::MissingObase:
      {
        break;
      }
    case ParseStatus::FileIsInAndOutput:
      {
        cdoAbort("%s", parse_err_msg);
      }
    case ParseStatus::Ok:
      {
        return;
      }
    }
}

Process &
ProcessManager::getProcess(int p_processID)
{
  std::unique_lock<std::mutex> locked_mutex(processMutex);

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

  return process->second;
}
