/*
  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 <cassert>

// Debug and message includes
#include <cdi.h>

#include "cdo_options.h"
#include "cdo_default_values.h"
#include "process.h"

thread_local Process *localProcess;
static int NumProcessActive = 0;

int
cdoFiletype(void)
{
  if (CdoDefault::FileType == CDI_UNDEFID)
    {
      CdoDefault::FileType = CDI_FILETYPE_GRB;
      if (Options::cdoVerbose) cdoPrint("Set default filetype to GRIB1");
    }

  return CdoDefault::FileType;
}

Process &
processSelf(void)
{
  return *localProcess;
}

void
processDefVarNum(int nvars)
{
  localProcess->nvars += nvars;
}

int
processInqVarNum(void)
{
  return localProcess->nvars;
}

/* cdoStreamInqTimeStep(...)
 * sets the given p_pstreamptr to given tsID and returns the number of records this timestep contains
 */
int
cdoStreamInqTimestep(CdoStreamID p_pstreamptr, int tsID)
{
  Debug(PROCESS, "%s pstreamID %d", p_pstreamptr->m_name, p_pstreamptr->getID());
  int nrecs = p_pstreamptr->inqTimestep(tsID);
  if (nrecs && tsID == p_pstreamptr->getTsID())
    {
      localProcess->ntimesteps++;
      Debug(PROCESS, "Timestep cnt for localProcess: %s: %d", localProcess->prompt, localProcess->ntimesteps);
    }
  return nrecs;
}

int
operatorArgc(void)
{
  return localProcess->m_oargv.size();
}

int
cdoOperatorArgc(void)
{
  return localProcess->getOperArgc();
}

const std::string &
cdoOperatorArgv(size_t idx)
{
  const auto argc = localProcess->getOperArgc();
  assert(((void) "Internal error: argument index out of bounds!", (idx < argc)));

  return localProcess->m_oargv[idx];
}

const std::vector<std::string> &
cdoGetOperArgv()
{
  return localProcess->m_oargv;
}

void
operatorCheckArgc(int numargs)
{
  const auto argc = localProcess->m_oargc;

  if (argc < numargs)
    cdoAbort("Too few arguments! Need %d found %d.", numargs, argc);
  else if (argc > numargs)
    cdoAbort("Too many arguments! Need %d found %d.", numargs, argc);
}

void
printEnter(const char *prompt, const char *enter)
{
  set_text_color(stderr, BRIGHT, MAGENTA);
  fprintf(stderr, "%-16s : ", prompt);
  reset_text_color(stderr);
  fprintf(stderr, "Enter %s > ", enter);
}

static std::string
getInput()
{
  char pline[1024];
  std::string fline = "";

  do
    {
      std::cin.getline(pline, 1024);
      //std::cout << pline << std::endl;
      fline += pline;
    }
  while (std::string(pline).find("\\") != std::string::npos);

  return fline;
}

void
operatorInputArg(const char *enter)
{
  while (true)
    {
      if (localProcess->m_oargc != 0) return;

      if (enter) printEnter(localProcess->prompt, enter);
      std::stringstream stringStream(getInput());

      std::string line;
      while (std::getline(stringStream, line))
        {
          std::size_t prev = 0, pos;
          while ((pos = line.find_first_of(", \\", prev)) != std::string::npos)
            {
              if (pos > prev) localProcess->m_oargv.push_back(line.substr(prev, pos - prev));
              prev = pos + 1;
            }
          if (prev < line.length()) localProcess->m_oargv.push_back(line.substr(prev, std::string::npos));
        }
      localProcess->m_oargc = localProcess->m_oargv.size();
    }
}

int
cdoOperatorAdd(const char *name, int f1, int f2, const char *enter)
{
  return localProcess->operatorAdd(name, f1, f2, enter);
}

int
cdoOperatorID(void)
{
  return localProcess->getOperatorID();
}

int
cdoOperatorF1(int operID)
{
  return localProcess->oper[operID].f1;
}

int
cdoOperatorF2(int operID)
{
  return localProcess->oper[operID].f2;
}

const char *
cdoOperatorName(int operID)
{
  return localProcess->oper[operID].name;
}

const char *
cdoOperatorEnter(int operID)
{
  return localProcess->oper[operID].enter;
}

int
cdoStreamNumber()
{
  return operatorStreamNumber(localProcess->operatorName);
}

int
cdoStreamCnt(void)
{
  return localProcess->m_streamCnt;
}

CdoStreamID
cdoOpenRead(int inStreamIDX)
{
  Debug(PROCESS, "Getting in stream %d of process %d", inStreamIDX, localProcess->m_ID);

  if (localProcess->getInStreamCnt() < inStreamIDX || inStreamIDX < 0)
    cdoAbort("instream %d of process %d not found", inStreamIDX, localProcess->m_ID);

  const auto inStream = localProcess->inputStreams[inStreamIDX];
  inStream->openRead();

  return inStream;
}

int
cdoStreamOpenRead(int inStreamIDX)
{
  return cdoOpenRead(inStreamIDX)->getID();  // return ID
}

/*parameters:
 *  p_outStreamIDX: In operator defined out stream ID
 *  filetype      : Currently not used in operators! Default value is CDI_UNDEFID.
 */
CdoStreamID
cdoOpenWrite(int p_outStreamIDX, int filetype)
{
  if (filetype == CDI_UNDEFID) filetype = cdoFiletype();
  Debug(PROCESS, "Getting out stream %d of process %d", p_outStreamIDX, localProcess->m_ID);

  const int outStreamIDX = p_outStreamIDX - localProcess->inputStreams.size();
  if (outStreamIDX > localProcess->getOutStreamCnt() || outStreamIDX < 0)
    {
      cdoAbort("outstream %d of %d not found. Called with streamIdx = %d", outStreamIDX, localProcess->m_ID, p_outStreamIDX);
    }

  const auto outStream = localProcess->outputStreams[outStreamIDX];
  outStream->openWrite(filetype);

  return outStream;
}

CdoStreamID
cdoOpenWrite(const std::string &p_filename, int filetype)
{
  if (filetype == CDI_UNDEFID) filetype = cdoFiletype();

  localProcess->addFileOutStream(p_filename);

  const auto pstreamID = localProcess->outputStreams.back()->openWrite(filetype);
  if (pstreamID == -1) cdoAbort("Could not create pstream for file: %s", p_filename);

  return localProcess->outputStreams.back();
}

CdoStreamID
cdoOpenAppend(int p_outFileIndex)
{
  const auto streamIndex = p_outFileIndex - localProcess->inputStreams.size();
  const auto outStream = localProcess->outputStreams[streamIndex];

  const auto fileID = outStream->openAppend();
  if (fileID < 0) cdiOpenError(fileID, "Open failed on >%s<", outStream->m_name.c_str());

  return outStream;
}

int
cdoStreamOpenAppend(int p_outFileIndex)
{
  return cdoOpenAppend(p_outFileIndex)->getID();
}

/**
 * Returns the output stream name as std::string for \p outStreamID.
 */
static const char *
cdoGetOutStreamName()
{
  return localProcess->getOutStreamName();
}

/**
 * Returns the input stream name as std::string for inStreamID.
 */

Process *
cdoGetInputChild(int p_inID)
{
  for (Process *processPtr : localProcess->childProcesses)
    {
      if (processPtr->hasOutStream(localProcess->inputStreams[p_inID])) return processPtr;
    }
  return nullptr;
}

static const char *
cdoGetInStreamName(int p_inStream)
{
  return localProcess->inputStreams[p_inStream]->m_name.c_str();
}

const char *
cdoGetStreamName(int p_streamIndex)
{
  const char *streamName = nullptr;
  Debug(PROCESS, "stridx %d", p_streamIndex);
  if (p_streamIndex >= static_cast<int>(localProcess->inputStreams.size()))
    {
      Debug(PROCESS, "Getting output stream name %d", p_streamIndex);
      streamName = cdoGetOutStreamName();
    }
  else
    {
      Debug(PROCESS, "Getting input stream name %d", p_streamIndex);
      streamName = cdoGetInStreamName(p_streamIndex);
    }

  Debug(PROCESS, "StreamName is: %s", streamName);

  return streamName;
}

const char *
cdoGetCommandFromInStream(int p_streamIndex)
{
  Process *process = cdoGetInputChild(p_streamIndex);
  if (process == nullptr)  // then this should be a file and cdoGetStreamName can handle this
    {
      return cdoGetStreamName(p_streamIndex);
    }
  return process->getCommand();
}

bool
cdoAssertFilesOnly()
{
  return localProcess->hasNoPipes();
}

const char *
cdoGetObase()
{
  return localProcess->m_obase;
}

void
cdoInitialize(void *p_process)
{
#if defined(_OPENMP)
  omp_set_num_threads(Threading::ompNumThreads);  // Has to be called for every module (pthread)!
#endif
  localProcess = (Process *) p_process;
#ifdef HAVE_LIBPTHREAD
  localProcess->threadID = pthread_self();
#endif
  Debug(PROCESS, "Initializing process: %s", localProcess->m_operatorCommand);

#ifdef HAVE_LIBPTHREAD
  Debug(PROCESS, "process %d thread %ld", localProcess->m_ID, pthread_self());
#endif
}

const char *
processInqPrompt(void)
{
  if (localProcess)
    {
      return localProcess->inqPrompt();
    }
  return cdo::progname;
}

extern "C" size_t getPeakRSS();

void
cdoFinish(void)
{
  Debug(PROCESS, "Finishing process: %d", localProcess->m_ID);

#ifdef HAVE_LIBPTHREAD
  Debug(PROCESS, "process %d thread %ld", localProcess->m_ID, pthread_self());
#endif
  if (!Options::silentMode) localProcess->printProcessedValues();

  localProcess->setInactive();
  NumProcessActive--;
}

/* TODO move cdoClose internals into Pstream::close/ CdoStream::close */
void
cdoStreamClose(CdoStreamID pstreamPtr)
{
  if (pstreamPtr == nullptr) cdoAbort("Internal problem, stream %d not open!", pstreamPtr->getID());
  Debug(PROCESS, "Adding %d to pstream %d %s", localProcess->inqNvals(), pstreamPtr->getID(), pstreamPtr->m_name);
  pstreamPtr->close();
}

/******************************************************************/
/* Functions that only work on streamID's and do not need process */
/******************************************************************/

int
cdoStreamInqVlist(CdoStreamID p_pstreamPtr)
{
  if (p_pstreamPtr == nullptr) return -1;

  Debug(PROCESS, "Inquiring Vlist from pstream %d", p_pstreamPtr->getID());
  const auto vlistID = p_pstreamPtr->inqVlist();
  if (vlistNumber(vlistID) == CDI_COMP && cdoStreamNumber() == CDI_REAL)
    cdoAbort("Fields with complex numbers are not supported by this operator!");

  if (vlistNumber(vlistID) == CDI_REAL && cdoStreamNumber() == CDI_COMP)
    cdoAbort("This operator needs fields with complex numbers!");

  processDefVarNum(vlistNvars(vlistID));

  return vlistID;
}

// - - - - - - -
void
cdoWriteRecordF(CdoStreamID p_pstreamPtr, float *data, size_t nmiss)
{
  p_pstreamPtr->writeRecord(data, nmiss);
}

void
cdoWriteRecord(CdoStreamID p_pstreamPtr, double *data, size_t nmiss)
{
  p_pstreamPtr->writeRecord(data, nmiss);
}

void
cdoWriteRecord(CdoStreamID p_pstreamPtr, Field &field)
{
  if (field.memType == MemType::Float)
    cdoWriteRecordF(p_pstreamPtr, field.vec_f.data(), field.nmiss);
  else
    cdoWriteRecord(p_pstreamPtr, field.vec_d.data(), field.nmiss);
}

void
cdoWriteRecord(CdoStreamID p_pstreamPtr, Field3D &field, int levelID, size_t nmiss)
{
  const auto offset = levelID * field.gridsize * field.nwpv;
  if (field.memType == MemType::Float)
    cdoWriteRecordF(p_pstreamPtr, field.vec_f.data() + offset, nmiss);
  else
    cdoWriteRecord(p_pstreamPtr, field.vec_d.data() + offset, nmiss);
}
// - - - - - - -
void
cdoDefVlist(CdoStreamID p_pstreamPtr, int vlistID)
{
  p_pstreamPtr->defVlist(vlistID);
}
// - - - - - - -
void
cdoDefTimestep(CdoStreamID p_pstreamPtr, int tsID)
{
  p_pstreamPtr->defTimestep(tsID);
}
// - - - - - - -
//
int
cdoInqFiletype(CdoStreamID p_pstreamPtr)
{
  return p_pstreamPtr->inqFileType();
}
// - - - - - - -
void
cdoInqGRIBinfo(CdoStreamID p_streamPtr, int *intnum, float *fltnum, off_t *bignum)
{
  streamInqGRIBinfo(p_streamPtr->m_fileID, intnum, fltnum, bignum);
}

// - - - - - - -
/* marked for process */
int
cdoFileID(CdoStreamID p_pstreamPtr)
{
  return p_pstreamPtr->m_fileID;
}

// - - - - - - -
int
cdoInqByteorder(CdoStreamID p_pstreamPtr)
{
  return p_pstreamPtr->inqByteorder();
}

// - - - - - - -
void
cdoReadRecordF(CdoStreamID p_pstreamPtr, float *data, size_t *nmiss)
{
  if (data == nullptr) cdoAbort("Data pointer not allocated (cdoReadRecord)!");
  p_pstreamPtr->readRecord(data, nmiss);
}

void
cdoReadRecord(CdoStreamID p_pstreamPtr, double *data, size_t *nmiss)
{
  if (data == nullptr) cdoAbort("Data pointer not allocated (cdoReadRecord)!");
  p_pstreamPtr->readRecord(data, nmiss);
}

void
cdoReadRecord(CdoStreamID streamID, Field &field)
{
  if (field.memType == MemType::Float)
    cdoReadRecordF(streamID, field.vec_f.data(), &field.nmiss);
  else
    cdoReadRecord(streamID, field.vec_d.data(), &field.nmiss);
}

void
cdoReadRecord(CdoStreamID streamID, Field3D &field, int levelID, size_t *nmiss)
{
  const auto offset = levelID * field.gridsize * field.nwpv;
  if (field.memType == MemType::Float)
    cdoReadRecordF(streamID, field.vec_f.data() + offset, nmiss);
  else
    cdoReadRecord(streamID, field.vec_d.data() + offset, nmiss);
}
// - - - - - - -

void
cdoCopyRecord(CdoStreamID pstreamPtrDest, CdoStreamID pstreamPtrSrc)
{
  Debug(PROCESS, "pstreamIDdest = %d pstreamIDsrc = %d", pstreamPtrDest->getID(), pstreamPtrSrc->getID());
  pstreamPtrDest->copyRecord(pstreamPtrSrc);
}

// - - - - - - -

void
cdoInqRecord(CdoStreamID pstreamptr, int *varID, int *levelID)
{
  pstreamptr->inqRecord(varID, levelID);
}

void
cdoDefRecord(CdoStreamID pstreamptr, int varID, int levelID)
{
  pstreamptr->defRecord(varID, levelID);
}
// - - - - - - -

void
cdoDefCompType(CdoStreamID p_streamID, int p_cdi_compression_type)
{
  streamDefCompType(p_streamID->m_fileID, p_cdi_compression_type);
}

bool
unchangedRecord()
{
  bool unchanged = (localProcess->m_ID == 0 && localProcess->hasNoPipes() && Options::cdoRegulargrid == false
                    && CdoDefault::FileType == -1 && CdoDefault::DataType == -1 && CdoDefault::Byteorder == -1);
  return unchanged;
}

void
nospec(int vlistID)
{
  const auto nvars = vlistNvars(vlistID);
  for (int varID = 0; varID < nvars; varID++)
    {
      const auto gridID = vlistInqVarGrid(vlistID, varID);
      const auto gridtype = gridInqType(gridID);
      if (gridtype == GRID_SPECTRAL) cdoAbort("Operator not defined for spectral fields");
    }
}

char *
cdoVlistInqVarName(int vlistID, int varID, char *name)
{
  vlistInqVarName(vlistID, varID, name);
  return name;
}

void
cdoSetNAN(double missval, size_t gridsize, double *array)
{
  if (std::isnan(missval))
    {
      const double newmissval = -9e33;
      for (size_t i = 0; i < gridsize; ++i)
        if (DBL_IS_EQUAL(array[i], missval)) array[i] = newmissval;
    }
}
