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

/*
   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"
#include "cdo_default_values.h"

static void
check_dup_entry(int vlistID1, int vlistID2, const char *filename)
{
  Varray<double> lev1, lev2;

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

  VarList varList1, varList2;
  varListInit(varList1, vlistID1);
  varListInit(varList2, vlistID2);

  for (int varID1 = 0; varID1 < nvars1; ++varID1)
    {
      const auto vname1 = varList1[varID1].name;
      const auto param1 = varList1[varID1].param;
      const auto gtype1 = gridInqType(varList1[varID1].gridID);
      const auto gsize1 = varList1[varID1].gridsize;
      const auto ztype1 = zaxisInqType(varList1[varID1].zaxisID);
      const size_t nlev1 = varList1[varID1].nlevels;
      if (nlev1 > lev1.size()) lev1.resize(nlev1);
      cdo_zaxis_inq_levels(varList1[varID1].zaxisID, lev1.data());

      for (int varID2 = 0; varID2 < nvars2; ++varID2)
        {
          const auto vname2 = varList2[varID2].name;
          const auto param2 = varList2[varID2].param;
          const auto gtype2 = gridInqType(varList2[varID2].gridID);
          const auto gsize2 = varList2[varID2].gridsize;
          const auto ztype2 = zaxisInqType(varList2[varID2].zaxisID);
          const size_t nlev2 = varList2[varID2].nlevels;
          if (gtype1 == gtype2 && gsize1 == gsize2 && ztype1 == ztype2 && nlev1 == nlev2)
            {
              if (nlev2 > lev2.size()) lev2.resize(nlev2);
              cdo_zaxis_inq_levels(varList2[varID2].zaxisID, lev2.data());

              if (zaxisInqLevels(varList1[varID1].zaxisID, nullptr) && zaxisInqLevels(varList2[varID2].zaxisID, nullptr))
                {
                  for (size_t k = 0; k < nlev2; ++k)
                    if (IS_NOT_EQUAL(lev1[k], lev2[k])) return;
                }

              if ((param1 < 0 || param2 < 0) && strcmp(vname1, vname2) == 0)
                {
                  cdo_warning("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));
                  cdo_warning("Duplicate entry of parameter %s in %s!", paramstr, filename);
                }
              else if (param1 != param2 && strcmp(vname1, vname2) == 0)
                {
                  cdo_warning("Duplicate entry of parameter name %s with different IDs in %s!", vname2, filename);
                }
            }
        }
    }
}

static int
get_taxis_index(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 = cdo_inq_filetype(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)
{
  cdo_initialize(process);

  operator_check_argc(0);

  auto dataIsUnchanged = data_is_unchanged();

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

  cdo::set_numfiles(nmerge + 8);

  auto ofilename = cdo_get_stream_name(streamCnt - 1);

  if (!Options::cdoOverwriteMode && FileUtils::file_exists(ofilename) && !FileUtils::user_file_overwrite(ofilename))
    cdo_abort("Outputfile %s already exists!", ofilename);

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

  const auto setFileType = (CdoDefault::FileType == CDI_UNDEFID);
  auto filetypex = -1;
  for (int im = 0; im < nmerge; im++)
    {
      streamIDs[im] = cdo_open_read(im);
      vlistIDs[im] = cdo_stream_inq_vlist(streamIDs[im]);
      if (im == 0) filetypex = cdoInqFiletypeX(streamIDs[im]);
      if (filetypex != cdoInqFiletypeX(streamIDs[im])) dataIsUnchanged = false;
    }

  if (setFileType) CdoDefault::FileType = cdo_inq_filetype(streamIDs[0]);
  // printf("setFileType=%d  filetype=%d\n", setFileType, CdoDefault::FileType);

  const auto taxisindex = get_taxis_index(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++)
    {
      check_dup_entry(vlistID2, vlistIDs[im], cdo_get_command_from_in_stream(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 = cdo_open_write(streamCnt - 1);

  vlistDefTaxis(vlistID2, taxisID2);
  cdo_def_vlist(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] = cdo_stream_inq_timestep(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)
              cdo_warning("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)
              {
                cdo_warning("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;
        }

      cdo_taxis_copy_timestep(taxisID2, taxisID1);
      cdo_def_timestep(streamID2, tsID);

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

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

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

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

              cdo_def_record(streamID2, varID2, levelID2);
              if (dataIsUnchanged)
                {
                  cdo_copy_record(streamID2, streamID1);
                }
              else
                {
                  field.init(varList2[varID2]);
                  cdo_read_record(streamID1, field);
                  cdo_write_record(streamID2, field);
                }
            }
        }

      tsID++;
    }

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

  cdo_stream_close(streamID2);

  vlistDestroy(vlistID2);

  cdo_finish();

  return nullptr;
}
