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

  Copyright (C) 2003-2020 Uwe Schulzweida, <uwe.schulzweida AT mpimet.mpg.de>
  See COPYING file for copying and redistribution conditions.

  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; version 2 of the License.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.
*/

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#ifdef HAVE_PTHREAD_H
#include <pthread.h>
#endif

#ifdef _OPENMP
#include <omp.h>
#endif

#include <stdio.h>
#include <iostream>
#include <string>
#include <sstream>

#include "process.h"
#include "cdo_wtime.h"
#include "util_string.h"
#include "cdo_options.h"
#include "fileStream.h"
#include "pipeStream.h"

static int processNum = 0;

void
setProcessNum(int p_num)
{
  processNum = p_num;
}

Process::Process(int p_ID, const char *p_operatorName, const char *p_operatorCommand) : m_ID(p_ID), operatorName(p_operatorName)
{
  m_isActive = true;

  startTime = cdo_get_wtime();

  initProcess(p_operatorName, p_operatorCommand);
}

void
Process::setOperatorArgv(const char *operatorArguments)
{
  if (operatorArguments)
    {
      m_oargv.clear();
      Debug(PROCESS && strchr(operatorArguments, ',') != nullptr, "Setting operator arguments: %s", operatorArguments);

      std::istringstream f(operatorArguments);
      std::string s;
      std::string argument = "";
      getline(f, s, ',');
      while (getline(f, s, ','))
        {
          argument = s;
          while (argument.back() == '\\')
            {
              argument.back() = ',';
              getline(f, s, ',');
              argument.append(s);
            }

          Debug(PROCESS, "Adding argument %s", argument);
          m_oargv.push_back(argument);
        }
    }

  m_oargc = m_oargv.size();
}

bool
Process::inputIsVariable()
{
  return m_module.streamInCnt == -1;
}

void
Process::initProcess(const char *p_operatorName, const char *operatorCommand)
{
#ifdef HAVE_LIBPTHREAD
  threadID = pthread_self();
#endif

  setOperatorArgv(operatorCommand);
  m_operatorCommand = operatorCommand;

  defPrompt();  // has to be called after get operatorName

  m_module = getModule(p_operatorName);
  // if (m_module.func == nullptr) cdoAbort("Module for operator <%s> not implemented!\n", p_operatorName);
  operatorName = p_operatorName;
}

int
Process::getInStreamCnt()
{
  return inputStreams.size();
}

int
Process::getOutStreamCnt()
{
  return outputStreams.size();
}

void
Process::defPrompt()
{
  if (m_ID == 0)
    snprintf(prompt, sizeof(prompt), "%s    %s", cdo::progname, operatorName);
  else
    snprintf(prompt, sizeof(prompt), "%s(%d) %s", cdo::progname, m_ID, operatorName);
}

const char *
Process::inqPrompt() const
{
  return prompt;
}

void
Process::handleProcessErr()
{
  switch (m_status)
    {
    case ProcessStatus::UnlimitedIOCounts:
      {
        cdoAbort("I/O stream counts unlimited no allowed!");
        break;
      }
    case ProcessStatus::MissInput:
      {
        cdoAbort("Input streams missing!");
        break;
      }
    case ProcessStatus::MissOutput:
      {
        cdoAbort("Output streams missing!");
        break;
      }
    case ProcessStatus::TooManyStreams:
    case ProcessStatus::TooFewStreams:
      {
        const auto inCnt = m_module.streamInCnt;
        auto outCnt = m_module.streamOutCnt;
        const bool lobase = outCnt == -1;
        if (lobase) outCnt = 1;

        std::string caseCount = (m_status == ProcessStatus::TooManyStreams) ? "many" : "few";
        std::string pluralIn = (inCnt > 1) ? "s" : "";
        std::string pluralOut = (outCnt > 1) ? "s" : "";

        std::stringstream errMsg;
        errMsg << "Too " << caseCount << " streams specified! Operator " << operatorName << " needs " << inCnt << " input stream"
               << pluralIn << " and " << outCnt << " output " << (lobase ? "basename" : "stream") << pluralOut << "!";
        cdoAbort(errMsg.str());
        break;
      }
    case ProcessStatus::Ok: break;
    }
}

void
Process::validate()
{
  checkStreamCnt();
  if (m_status != ProcessStatus::Ok) handleProcessErr();
}

void
Process::checkStreamCnt()
{
  int streamCnt = 0;

  int wantedStreamInCnt = m_module.streamInCnt;
  int wantedStreamOutCnt = m_module.streamOutCnt;

  int streamInCnt0 = wantedStreamInCnt;

  bool obase = false;
  if (wantedStreamOutCnt == -1)
    {
      wantedStreamOutCnt = 1;
      obase = true;
    }

  if (wantedStreamInCnt == -1 && wantedStreamOutCnt == -1)
    {
      m_status = ProcessStatus::UnlimitedIOCounts;
    }

  // printf(" wantedStreamInCnt,wantedStreamOutCnt %d %d\n",
  // wantedStreamInCnt,wantedStreamOutCnt);
  else if (wantedStreamInCnt == -1)
    {
      wantedStreamInCnt = m_streamCnt - wantedStreamOutCnt;
      if (wantedStreamInCnt < 1) m_status = ProcessStatus::MissInput;
    }

  else if (wantedStreamOutCnt == -1)
    {
      wantedStreamOutCnt = m_streamCnt - wantedStreamInCnt;
      if (wantedStreamOutCnt < 1) m_status = ProcessStatus::MissOutput;
    }
  else 
    {
      // printf(" wantedStreamInCnt,wantedStreamOutCnt %d %d\n",
      // wantedStreamInCnt,wantedStreamOutCnt);

      streamCnt = wantedStreamInCnt + wantedStreamOutCnt;
      // printf(" streamCnt %d %d\n", m_streamCnt, streamCnt);

      if (m_streamCnt > streamCnt)
        m_status = ProcessStatus::TooManyStreams;
      else if (m_streamCnt < streamCnt && !obase)
        m_status = ProcessStatus::TooFewStreams;
      else if (wantedStreamInCnt > (int) inputStreams.size())
        m_status = ProcessStatus::TooFewStreams;

      else if (wantedStreamInCnt == 1 && streamInCnt0 == -1)
        m_status = ProcessStatus::Ok;
    }
}

bool
Process::hasAllInputs()
{
  if (m_module.streamInCnt == -1) return false;

  return m_module.streamInCnt == static_cast<short>(inputStreams.size());
}

void
Process::setInactive()
{
  m_isActive = false;
}

int
Process::operatorAdd(const char *name, int f1, int f2, const char *enter)
{
  const int operID = m_noper;

  if (operID >= MAX_OPERATOR) cdoAbort("Maximum number of %d operators reached!", MAX_OPERATOR);

  oper[m_noper] = { f1, f2, name, enter };

  m_noper++;

  return operID;
}

int
Process::getOperatorID()
{
  int operID = -1;

  if (m_noper > 0)
    {
      for (operID = 0; operID < m_noper; operID++)
        {
          if (oper[operID].name)
            {
              if (strcmp(operatorName, oper[operID].name) == 0) break;
            }
        }
      if (operID == m_noper)
        {
          cdoAbort("Operator not callable by this name! Name is: %s", operatorName);
        }
    }
  else
    {
      cdoAbort("Operator not initialized!");
    }

  return operID;
}

void
Process::addFileInStream(std::string file)
{
  inputStreams.push_back(std::make_shared<FileStream>(file));
  m_streamCnt++;
}

void
Process::addFileOutStream(std::string file)
{
  if (file[0] == '-')
    {
      cdoAbort("Missing output file. Found an operator instead of filename: %s", file);
    }
  outputStreams.push_back(std::make_shared<FileStream>(file));
  m_streamCnt++;
}

void
Process::addChild(Process *childProcess)
{
  childProcesses.push_back(childProcess);
  nchild = childProcesses.size();
  addPipeInStream();
}

void
Process::addPipeInStream()
{
#ifdef HAVE_LIBPTHREAD
  inputStreams.push_back(std::make_shared<PipeStream>(m_ID));
  m_streamCnt++;
#else
  cdoAbort("Cannot use pipes, pthread support not compiled in!");
#endif
}

void
Process::addParent(Process *parentProcess)
{
  parentProcesses.push_back(parentProcess);
  m_posInParent = parentProcess->inputStreams.size() - 1;
  addPipeOutStream();
}

void
Process::addPipeOutStream()
{
  outputStreams.push_back(parentProcesses[0]->inputStreams[m_posInParent]);
  m_streamCnt++;
}

pthread_t
Process::run()
{
  Debug(PROCESS, "starting new thread for process %d", m_ID);
  pthread_attr_t attr;
  auto status = pthread_attr_init(&attr);
  if (status) cdoSysError("pthread_attr_init failed for '%s'", operatorName);
  status = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
  if (status) cdoSysError("pthread_attr_setdetachstate failed for '%s'", operatorName);
  /*
    param.sched_priority = 0;
    status = pthread_attr_setschedparam(&attr, &param);
    if ( status ) cdoSysError("pthread_attr_setschedparam failed for '%s'", newarg+1);
  */
  /* status = pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED); */
  /* if ( status ) cdoSysError("pthread_attr_setinheritsched failed for '%s'", newarg+1); */

  int pthreadScope;
  pthread_attr_getscope(&attr, &pthreadScope);

  /* status = pthread_attr_setscope(&attr, PTHREAD_SCOPE_PROCESS); */
  /* if ( status ) cdoSysError("pthread_attr_setscope failed for '%s'", newarg+1); */
  /* If system scheduling scope is specified, then the thread is scheduled against all threads in the system */
  /* pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM); */

  size_t stacksize = 0;
  status = pthread_attr_getstacksize(&attr, &stacksize);
  if (stacksize < 2097152)
    {
      stacksize = 2097152;
      pthread_attr_setstacksize(&attr, stacksize);
    }

  pthread_t thrID;
  const auto rval = pthread_create(&thrID, &attr, m_module.func, this);
  if (rval != 0)
    {
      errno = rval;
      cdoSysError("pthread_create failed for '%s'", operatorName);
    }

  m_isActive = true;

  return thrID;
}

void
Process::printBenchmarks(double p_walltime, const char *p_memstring)
{
#ifdef HAVE_SYS_TIMES_H
  if (m_ID == 0)
    {
      fprintf(stderr, " [%.2fs%s]", p_walltime, p_memstring);
    }
#endif
}

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

void
Process::printProcessedValues()
{
  set_text_color(stderr, GREEN);
  fprintf(stderr, "%s: ", prompt);
  reset_text_color(stderr);

  const int64_t nvals = inqNvals();

  if (nvals > 0)
    {
      if (sizeof(int64_t) > sizeof(size_t))
#ifdef _WIN32
        fprintf(stderr, "Processed %I64d value%s from %d variable%s",
#else
        fprintf(stderr, "Processed %jd value%s from %d variable%s",
#endif
                (intmax_t) nvals, ADD_PLURAL(nvals), nvars, ADD_PLURAL(nvars));
      else
        fprintf(stderr, "Processed %zu value%s from %d variable%s", (size_t) nvals, ADD_PLURAL(nvals), nvars, ADD_PLURAL(nvars));
    }
  else if (nvars > 0)
    {
      fprintf(stderr, "Processed %d variable%s", nvars, ADD_PLURAL(nvars));
    }

  if (ntimesteps > 0) fprintf(stderr, " over %d timestep%s", ntimesteps, ADD_PLURAL(ntimesteps));

  char memstring[32] = { "" };
  if (m_ID == 0) getMaxMemString(memstring, sizeof(memstring));
  if (!Options::silentMode) printBenchmarks(cdo_get_wtime() - startTime, memstring);

  if (nvars > 0 || nvals > 0 || ntimesteps > 0 || m_ID == 0) fprintf(stderr, ".");
  fprintf(stderr, "\n");
}

bool
Process::hasOutStream(const CdoStreamID p_streamID)
{
  for (const CdoStreamID streamID : outputStreams)
    {
      if (streamID == p_streamID) return true;
    }
  return false;
}

bool
Process::hasInStream(const CdoStreamID p_streamID)
{
  for (const CdoStreamID streamID : inputStreams)
    {
      if (streamID == p_streamID) return true;
    }
  return false;
}

const char *
Process::getCommand()
{
  return m_operatorCommand + 1;
}

size_t
Process::inqNvals()
{
  size_t nvals = 0;
  for (size_t i = 0; i < inputStreams.size(); i++)
    {
      Debug(PROCESS, "Inquiring nvals from instream %s", inputStreams[i]->m_name);
      nvals += inputStreams[i]->getNvals();
    }
  return nvals;
}

bool
Process::hasNoPipes()
{
  if (childProcesses.size() == 0) return true;
  return false;
}

const char *
Process::getOutStreamName()
{
  return outputStreams[0]->m_name.c_str();
}

size_t
Process::getOperArgc()
{
  return m_oargc;
}

std::string
Process::getArgv(int p_idx)
{
  if (!(p_idx > m_oargc && p_idx > 0)) cdoAbort("Process Argv not found. Idx: %d, Process argc: %d", p_idx, m_oargv.size());
  return m_oargv[p_idx];
}

std::string
Process::getObase()
{
  return m_obase;
}
