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

  Copyright (C) 2003-2021 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.
*/

#include <cdi.h>

#include "pipe.h"
#include "cdo_output.h"
#include "cthread_debug.h"

pipe_t::pipe_t() { pipe_init(); }

void
pipe_t::close()
{
  cthread_mutex_lock(m_mutex);
  EOP = true;
  Debug(PIPE, "%s write closed", name);
  cthread_mutex_unlock(m_mutex);
  cthread_cond_signal(tsDef_cond);
  cthread_cond_signal(tsInq_cond);
  cthread_cond_signal(recInq_cond);
  cthread_cond_signal(isClosed_cond);
}

void
pipe_t::pipe_init()
{
  EOP = false;

  recIDr = -1;
  recIDw = -1;
  tsIDr = -1;
  tsIDw = -1;

  varID = -1;
  levelID = -1;

  nrecs = 0;
  nmiss = 0;
  data_d = nullptr;
  data_f = nullptr;
  hasdata = false;
  usedata = true;
  data_is_float = false;
}

int
pipe_t::pipe_inq_timestep(const int p_tsID)
{
  // LOCK
  std::unique_lock<std::mutex> locked_mutex(m_mutex);
  usedata = false;
  recIDr = -1;
  if (p_tsID != tsIDr + 1)
    {
      if (!(p_tsID == tsIDr && tsIDr == tsIDw && recIDr == -1))
        cdo_abort("%s unexpected tsID %d %d %d", name.c_str(), p_tsID, tsIDr + 1, tsIDw);
    }

  tsIDr = p_tsID;
  while (tsIDw != p_tsID)
    {
      if (EOP)
        {
          Debug(PIPE, name.c_str(), " EOP");
          break;
        }
      if (hasdata)
        {
          Debug(PIPE, name.c_str(), " has data");
          hasdata = false;
          data_d = nullptr;
          data_f = nullptr;
          data_is_float = false;
          read_cond.notify_all();
        }
      else
        {
          Debug(PIPE, "%s has no data", name);
        }

      recInq_cond.notify_all(); /* o.k. ??? */

      Debug(PIPE, name.c_str(), " wait of tsDef_cond");
      tsDef_cond.wait(locked_mutex);
    }

  int numrecs = EOP ? 0 : nrecs;

  locked_mutex.unlock();
  // UNLOCK

  tsInq_cond.notify_all();

  return numrecs;
}

void
pipe_t::pipe_def_vlist(int &target_vlistID, const int new_vlistID)
{
  // LOCK
  m_mutex.lock();
  target_vlistID = new_vlistID;
  m_mutex.unlock();
  // UNLOCK

  // lets the program know that the vlist is now defined
  vlistDef_cond.notify_all();
}

//#define TIMEOUT 1  // wait 1 seconds
constexpr std::chrono::milliseconds TIMEOUT = std::chrono::milliseconds(1000);
#define MAX_WAIT_CYCLES 3600

int
pipe_t::pipe_inq_vlist(int &p_vlistID)
{
  Debug(PIPE, "Inquiring vlist for vlistID: %d", p_vlistID);
  std::chrono::milliseconds time_to_wait(0);
  int nwaitcycles = 0;

  // LOCK
  std::unique_lock<std::mutex> locked_mutex(m_mutex);
  while (p_vlistID == -1 && nwaitcycles < MAX_WAIT_CYCLES && !EOP)
    {
      time_to_wait += TIMEOUT;
      Debug(PIPE, name.c_str(), " wait of vlistDef_cond");
      vlistDef_cond.wait_for(locked_mutex, time_to_wait);
      nwaitcycles++;
    }
  // UNLOCK

  return p_vlistID;
}

void
pipe_t::pipe_def_timestep(const int p_vlistID, const int p_tsID)
{
  int numrecs;

  // LOCK
  m_mutex.lock();
  recIDw = -1;
  tsIDw++;
  if (p_tsID != tsIDw) cdo_abort("unexpected p_tsID %d(%d) for %s", p_tsID, tsIDw, name.c_str());

  if (p_tsID == 0)
    numrecs = vlistNrecs(p_vlistID);
  else
    {
      const auto vlistID = p_vlistID;
      numrecs = 0;
      for (int i = 0; i < vlistNvars(vlistID); i++)
        {
          if (vlistInqVarTimetype(vlistID, i) != TIME_CONSTANT) numrecs += zaxisInqSize(vlistInqVarZaxis(vlistID, i));
        }
      Debug(PIPE, " %s numrecs= %d nvars= %d ", name.c_str(), numrecs, vlistNvars(vlistID));
    }

  nrecs = numrecs;
  Debug(PIPE, "%s numrecs %d p_tsID %d %d %d", name.c_str(), numrecs, p_tsID, tsIDw, tsIDr);
  if (numrecs == 0) EOP = true;
  m_mutex.unlock();
  // UNLOCK

  tsDef_cond.notify_all();
  // sleep(1);

  // LOCK
  std::unique_lock<std::mutex> locked_mutex(m_mutex);
  while (tsIDr < p_tsID)
    {
      if (EOP)
        {
          Debug(PIPE, "EOP");
          break;
        }
      Debug(PIPE, name.c_str(), " wait of tsInq_cond (p_tsID ", p_tsID, " ", tsIDr, ")");
      tsInq_cond.wait(locked_mutex);
    }
  // UNLOCK
}

int
pipe_t::pipe_inq_record(int *p_varID, int *p_levelID)
{
  bool condSignal = false;

  // LOCK
  m_mutex.lock();
  Debug(PIPE, name.c_str(), " has no data ", recIDr, " ", recIDw);
  if (hasdata || usedata)
    {
      hasdata = false;
      data_d = nullptr;
      data_f = nullptr;
      usedata = false;
      condSignal = true;
    }
  m_mutex.unlock();
  // UNLOCK

  if (condSignal) read_cond.notify_all();

  // LOCK
  std::unique_lock<std::mutex> locked_mutex(m_mutex);
  usedata = true;
  recIDr++;

  Debug(PIPE, name.c_str(), "recID", recIDr, " ", recIDw);

  while (recIDw != recIDr)
    {
      if (EOP)
        {
          Debug(PIPE, "EOP");
          break;
        }
      Debug(PIPE, "%s wait for recDef_cond", name);
      recDef_cond.wait(locked_mutex);
    }

  if (EOP)
    {
      *p_varID = -1;
      *p_levelID = -1;
    }
  else
    {
      *p_varID = varID;
      *p_levelID = levelID;
    }

  locked_mutex.unlock();
  // UNLOCK

  recInq_cond.notify_all();

  return 0;
}

void
pipe_t::pipe_def_record(const int p_varID, const int p_levelID)
{
  bool condSignal = false;

  // LOCK
  m_mutex.lock();
  Debug(PIPE, name.c_str(), " has data ", recIDr, " ", recIDw);  //<- TODO: rethink positioning
  if (hasdata)
    {
      hasdata = false;
      data_d = nullptr;
      data_f = nullptr;
      condSignal = true;
    }
  m_mutex.unlock();
  // UNLOCK

  if (condSignal) read_cond.notify_all();

  // LOCK
  m_mutex.lock();
  usedata = true;
  recIDw++;
  varID = p_varID;
  levelID = p_levelID;
  Debug(PIPE, name.c_str(), "recID", recIDr, " ", recIDw);
  m_mutex.unlock();
  // UNLOCK

  recDef_cond.notify_all();

  // LOCK
  std::unique_lock<std::mutex> locked_mutex(m_mutex);
  while (recIDr < recIDw)
    {
      if (tsIDw != tsIDr) break;
      if (EOP) break;
      Debug(PIPE, name.c_str(), "wait of recInq_cond ", recIDr);
      recInq_cond.wait(locked_mutex);
    }
  // UNLOCK
}

/***
 * copys data from a pipe to data
 *
 * @param data destination for the record data
 * @param pipe pipe that has the wanted data
 */
size_t
pipe_t::pipe_read_pipe_record(double *const p_data, const int vlistID, size_t *const p_nmiss)
{
  if (!p_data) cdo_abort("No data pointer for %s", name.c_str());

  auto datasize = gridInqSize(vlistInqVarGrid(vlistID, varID));
  if (vlistNumber(vlistID) != CDI_REAL) datasize *= 2;

  if (data_is_float)
    {
      for (size_t i = 0; i < datasize; ++i) p_data[i] = (double) data_f[i];
    }
  else
    {
      memcpy(p_data, data_d, datasize * sizeof(double));
    }

  *p_nmiss = nmiss;
  return datasize;
}

size_t
pipe_t::pipe_read_pipe_record(float *const p_data, const int vlistID, size_t *const p_nmiss)
{
  if (!p_data) cdo_abort("No data pointer for %s", name.c_str());

  auto datasize = gridInqSize(vlistInqVarGrid(vlistID, varID));
  if (vlistNumber(vlistID) != CDI_REAL) datasize *= 2;

  if (data_is_float)
    {
      memcpy(p_data, data_f, datasize * sizeof(float));
    }
  else
    {
      for (size_t i = 0; i < datasize; ++i) p_data[i] = (float) data_d[i];
    }

  *p_nmiss = nmiss;
  return datasize;
}

size_t
pipe_t::pipe_read_record(const int p_vlistID, double *const p_data, size_t *const p_nmiss)
{
  *p_nmiss = 0;
  size_t nvals = 0;

  // LOCK
  std::unique_lock<std::mutex> locked_mutex(m_mutex);
  while (!hasdata)
    {
      Debug(PIPE, name.c_str(), " wait of write_cond");
      write_cond.wait(locked_mutex);
    }

  if (hasdata)
    {
      nvals = pipe_read_pipe_record(p_data, p_vlistID, p_nmiss);
    }
  else
    {
      cdo_abort("data type %d not implemented", hasdata);
    }

  Debug(PIPE, name.c_str(), " read record ", recIDr);

  hasdata = false;
  data_d = nullptr;
  locked_mutex.unlock();
  // UNLOCK

  read_cond.notify_all();
  return nvals;
}

size_t
pipe_t::pipe_read_record(const int p_vlistID, float *const p_data, size_t *const p_nmiss)
{
  *p_nmiss = 0;
  size_t nvals = 0;

  // LOCK
  std::unique_lock<std::mutex> locked_mutex(m_mutex);
  while (!hasdata)
    {
      Debug(PIPE, name.c_str(), " wait of write_cond");
      write_cond.wait(locked_mutex);
    }

  if (hasdata)
    {
      nvals = pipe_read_pipe_record(p_data, p_vlistID, p_nmiss);
    }
  else
    {
      cdo_abort("data type %d not implemented", hasdata);
    }

  Debug(PIPE, name.c_str(), " read record ", recIDr);

  hasdata = false;
  data_f = nullptr;
  locked_mutex.unlock();
  // UNLOCK

  read_cond.notify_all();
  return nvals;
}

size_t
pipe_t::pipe_read_record(const int p_vlistID, Field *const p_field, size_t *const p_nmiss)
{
  return pipe_read_record(p_vlistID, p_field->vec_d.data(), p_nmiss);
}

void
pipe_t::wait_for_read()
{
  write_cond.notify_all();

  Debug(PIPE, "%s write record $d", name, recIDw);

  // LOCK
  std::unique_lock<std::mutex> locked_mutex(m_mutex);
  while (hasdata)
    {
      if (!usedata) break;
      if (recIDw != recIDr) break;

      if (EOP)
        {
          Debug(PIPE, "EOP");
          break;
        }
      Debug(PIPE, " wait of read_cond %s", name);
      read_cond.wait(locked_mutex);
    }
  // UNLOCK
}

void
pipe_t::pipe_write_record(double *const p_data, const size_t p_nmiss)
{
  // LOCK
  m_mutex.lock();
  hasdata = true;  // data pointer
  data_is_float = false;
  data_d = p_data;
  nmiss = p_nmiss;
  m_mutex.unlock();
  // UNLOCK

  wait_for_read();
}

void
pipe_t::pipe_write_record(float *const p_data, const size_t p_nmiss)
{
  // LOCK
  m_mutex.lock();
  hasdata = true;  // data pointer
  data_is_float = true;
  data_f = p_data;
  nmiss = p_nmiss;
  m_mutex.unlock();
  // UNLOCK

  wait_for_read();
}

void
pipe_t::pipe_write_record(Field *const p_field, const size_t p_nmiss)
{
  pipe_write_record(p_field->vec_d.data(), p_nmiss);
}

void
pipe_t::pipe_set_name(const int processID, const int inputIDX)
{
  name = "(pipe" + std::to_string(processID + 1) + "." + std::to_string(inputIDX) + ")";
}
