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

/*
   This module contains the following operators:

      Merge      merge           Merge datasets with different fields
*/

#include <cdi.h>

#include "cdo_options.h"
#include "cdo_rlimit.h"
#include "process_int.h"
#include "cdo_zaxis.h"
#include "util_files.h"

static void
checkDupEntry(int vlistID1, int vlistID2, const char *filename)
{
  char vname1[CDI_MAX_NAME], vname2[CDI_MAX_NAME];
  Varray<double> lev1, lev2;

  const auto nvars1 = vlistNvars(vlistID1);
  const auto nvars2 = vlistNvars(vlistID2);

  for (int varID1 = 0; varID1 < nvars1; ++varID1)
    {
      vlistInqVarName(vlistID1, varID1, vname1);
      const auto param1 = vlistInqVarParam(vlistID1, varID1);
      const auto gridID1 = vlistInqVarGrid(vlistID1, varID1);
      const auto zaxisID1 = vlistInqVarZaxis(vlistID1, varID1);
      const auto gtype1 = gridInqType(gridID1);
      const auto gsize1 = gridInqSize(gridID1);
      const auto ztype1 = zaxisInqType(zaxisID1);
      const size_t nlev1 = zaxisInqSize(zaxisID1);
      if (nlev1 > lev1.size()) lev1.resize(nlev1);
      cdoZaxisInqLevels(zaxisID1, lev1.data());

      for (int varID2 = 0; varID2 < nvars2; ++varID2)
        {
          vlistInqVarName(vlistID2, varID2, vname2);
          const auto param2 = vlistInqVarParam(vlistID2, varID2);
          const auto gridID2 = vlistInqVarGrid(vlistID2, varID2);
          const auto zaxisID2 = vlistInqVarZaxis(vlistID2, varID2);
          const auto gtype2 = gridInqType(gridID2);
          const auto gsize2 = gridInqSize(gridID2);
          const auto ztype2 = zaxisInqType(zaxisID2);
          const size_t nlev2 = zaxisInqSize(zaxisID2);
          if (gtype1 == gtype2 && gsize1 == gsize2 && ztype1 == ztype2 && nlev1 == nlev2)
            {
              if (nlev2 > lev2.size()) lev2.resize(nlev2);
              cdoZaxisInqLevels(zaxisID2, lev2.data());

              if (zaxisInqLevels(zaxisID1, nullptr) && zaxisInqLevels(zaxisID2, nullptr))
                {
                  for (size_t k = 0; k < nlev2; ++k)
                    if (!IS_EQUAL(lev1[k], lev2[k])) return;
                }

              if ((param1 < 0 || param2 < 0) && strcmp(vname1, vname2) == 0)
                {
                  cdoWarning("Duplicate entry of parameter name %s in %s!", vname2, filename);
                }
              else if (param1 >= 0 && param2 >= 0 && param1 == param2 && strcmp(vname1, vname2) == 0)
                {
                  char paramstr[32];
                  cdiParamToString(param2, paramstr, sizeof(paramstr));
                  cdoWarning("Duplicate entry of parameter %s in %s!", paramstr, filename);
                }
              else if (param1 != param2 && strcmp(vname1, vname2) == 0)
                {
                  cdoWarning("Duplicate entry of parameter name %s with different IDs in %s!", vname2, filename);
                }
            }
        }
    }
}

static int
getTaxisindex(const std::vector<int> &vlistIDs)
{
  if (vlistNtsteps(vlistIDs[0]) == 0)
    {
      const int nmerge = vlistIDs.size();
      for (int im = 1; im < nmerge; im++)
        {
          if (vlistNtsteps(vlistIDs[im]) != 0) return im;
        }
    }

  return 0;
}

int
cdoInqFiletypeX(CdoStreamID streamID)
{
  const auto filetype = cdoInqFiletype(streamID);
  switch (filetype)
    {
    case CDI_FILETYPE_NC5:
    case CDI_FILETYPE_NC4C:
    case CDI_FILETYPE_NC4:
    case CDI_FILETYPE_NC2:
    case CDI_FILETYPE_NC: return CDI_FILETYPE_NC;
    case CDI_FILETYPE_GRB2:
    case CDI_FILETYPE_GRB: return CDI_FILETYPE_GRB;
    }

  return filetype;
}

void *
Merge(void *process)
{
  int nrecs = 0;

  cdoInitialize(process);

  operatorCheckArgc(0);

  auto lcopy = unchangedRecord();

  const auto streamCnt = cdoStreamCnt();
  const auto nmerge = streamCnt - 1;

  cdo::set_numfiles(nmerge + 8);

  auto ofilename = cdoGetStreamName(streamCnt - 1);

  if (!Options::cdoOverwriteMode && fileExists(ofilename) && !userFileOverwrite(ofilename))
    cdoAbort("Outputfile %s already exists!", ofilename);

  std::vector<CdoStreamID> streamIDs(nmerge);
  std::vector<int> vlistIDs(nmerge), numrecs(nmerge), numsteps(nmerge);

  auto filetypex = -1;
  for (int im = 0; im < nmerge; im++)
    {
      streamIDs[im] = cdoOpenRead(im);
      vlistIDs[im] = cdoStreamInqVlist(streamIDs[im]);
      if (im == 0) filetypex = cdoInqFiletypeX(streamIDs[im]);
      if (filetypex != cdoInqFiletypeX(streamIDs[im])) lcopy = false;
    }

  const auto taxisindex = getTaxisindex(vlistIDs);

  const auto taxisID1 = vlistInqTaxis(vlistIDs[taxisindex]);
  const auto taxisID2 = taxisDuplicate(taxisID1);

  const auto vlistID2 = vlistCreate();
  vlistCopy(vlistID2, vlistIDs[0]);
  for (int im = 1; im < nmerge; im++)
    {
      checkDupEntry(vlistID2, vlistIDs[im], cdoGetCommandFromInStream(im));
      vlistMerge(vlistID2, vlistIDs[im]);
    }

  int numconst = 0;
  for (int im = 0; im < nmerge; im++)
    {
      numsteps[im] = vlistNtsteps(vlistIDs[im]);
      if (numsteps[im] == 0) numsteps[im] = 1;
      if (numsteps[im] == 1) numconst++;
    }

  if (numconst > 0 && numconst < nmerge)
    for (int im = 0; im < nmerge; im++)
      {
        if (numsteps[im] == 1)
          {
            const auto vlistID1 = vlistIDs[im];
            const auto nvars = vlistNvars(vlistID1);
            for (int varID = 0; varID < nvars; ++varID)
              vlistDefVarTimetype(vlistID2, vlistMergedVar(vlistID1, varID), TIME_CONSTANT);
          }
      }

  if (Options::cdoVerbose)
    {
      for (int im = 0; im < nmerge; im++) vlistPrint(vlistIDs[im]);
      vlistPrint(vlistID2);
    }

  const auto streamID2 = cdoOpenWrite(streamCnt - 1);

  vlistDefTaxis(vlistID2, taxisID2);
  cdoDefVlist(streamID2, vlistID2);

  VarList varList2;
  varListInit(varList2, vlistID2);

  Field field;

  int tsID = 0;
  while (tsID >= 0)
    {
      for (int im = 0; im < nmerge; im++)
        {
          if (vlistIDs[im] == -1) continue;
          numrecs[im] = cdoStreamInqTimestep(streamIDs[im], tsID);
        }

      {
        int im;
        for (im = 0; im < nmerge; im++)
          if (numrecs[im] != 0) break;
        if (im == nmerge) break;  // EOF on all input streams
      }

      if (tsID == 1)
        {
          for (int im = 0; im < nmerge; im++)
            if (numrecs[im] == 0 && numsteps[im] == 1) vlistIDs[im] = -1;
        }

      if (numrecs[taxisindex] == 0)
        {
          for (int im = 1; im < nmerge; im++)
            if (vlistIDs[im] != -1 && numrecs[im] != 0)
              cdoWarning("Input stream %d has %d timestep%s. Stream %d has more timesteps, skipped!", taxisindex + 1, tsID,
                         tsID == 1 ? "" : "s", im + 1);
          break;
        }
      else
        {
          bool lstop = false;
          for (int im = 1; im < nmerge; im++)
            if (vlistIDs[im] != -1 && numrecs[im] == 0)
              {
                cdoWarning("Input stream %d has %d timestep%s. Stream %d has more timesteps, skipped!", im + 1, tsID,
                           tsID == 1 ? "" : "s", taxisindex + 1);
                lstop = true;
                break;
              }
          if (lstop) break;
        }

      taxisCopyTimestep(taxisID2, taxisID1);
      cdoDefTimestep(streamID2, tsID);

      for (int im = 0; im < nmerge; im++)
        {
          const auto streamID1 = streamIDs[im];
          const auto vlistID1 = vlistIDs[im];
          if (vlistID1 == -1) continue;

          nrecs = numrecs[im];
          for (int recID = 0; recID < nrecs; recID++)
            {
              int varID, levelID;
              cdoInqRecord(streamID1, &varID, &levelID);

              const auto varID2 = vlistMergedVar(vlistID1, varID);
              const auto levelID2 = vlistMergedLevel(vlistID1, varID, levelID);

              if (Options::cdoVerbose) cdoPrint("fileID=%d  varID=%d levelID=%d   varID2=%d levelID2=%d", im, varID, levelID, varID2, levelID2);

              cdoDefRecord(streamID2, varID2, levelID2);
              if (lcopy)
                {
                  cdoCopyRecord(streamID2, streamID1);
                }
              else
                {
                  field.init(varList2[varID2]);
                  cdoReadRecord(streamID1, field);
                  cdoWriteRecord(streamID2, field);
                }
            }
        }

      tsID++;
    }

  for (auto &streamID : streamIDs) cdoStreamClose(streamID);

  cdoStreamClose(streamID2);

  vlistDestroy(vlistID2);

  cdoFinish();

  return nullptr;
}
