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

#include <algorithm> // sort

#include <cdi.h>

#include "functs.h"
#include "process_int.h"
#include "cdo_vlist.h"
#include "param_conversion.h"
#include <mpim_grid.h>
#include "util_files.h"
#include "cdo_options.h"
#include "cdi_lockedIO.h"

static int globalGridType = CDI_UNDEFID;

struct EnsfileType
{
  CdoStreamID streamID;
  int vlistID;
  size_t gridsize;
  std::vector<long> gridindex;
  VarList varList;
  Field field;
};

struct xyinfoType
{
  double x = 0, y = 0;
  int id = -1;
};

static bool
cmpx(const xyinfoType &a, const xyinfoType &b)
{
  return a.x < b.x;
}

static bool
cmpxy_lt(const xyinfoType &a, const xyinfoType &b)
{
  return (a.y < b.y || (std::fabs(a.y - b.y) <= 0 && a.x < b.x));
}

static bool
cmpxy_gt(const xyinfoType &a, const xyinfoType &b)
{
  return (a.y > b.y || (std::fabs(a.y - b.y) <= 0 && a.x < b.x));
}

static int
genGrid(int ngrids, int nfiles, std::vector<EnsfileType> &ef, bool ginit, int igrid, long nxblocks)
{
  bool lsouthnorth = true;
  bool lregular = false;
  bool lcurvilinear = false;
  bool lunstructured = false;
  int gridID2 = -1;

  long nx = (nxblocks != -1) ? nxblocks : -1;

  auto gridID = vlistGrid(ef[0].vlistID, igrid);
  auto gridtype0 = (globalGridType != CDI_UNDEFID) ? globalGridType : gridInqType(gridID);
  if (ngrids > 1 && gridtype0 == GRID_GENERIC && gridInqXsize(gridID) == 0 && gridInqYsize(gridID) == 0) return gridID2;
  if (gridtype0 == GRID_UNSTRUCTURED) lunstructured = true;
  const int nv = lunstructured ? gridInqNvertex(gridID) : 0;
  const bool lcenter = globalGridType == CDI_UNDEFID && gridHasCoordinates(gridID);
  const bool lbounds = lunstructured && globalGridType == CDI_UNDEFID && gridHasBounds(gridID);

  std::vector<xyinfoType> xyinfo(nfiles);
  std::vector<long> xsize(nfiles), ysize(nfiles);
  Varray2D<double> xvals(nfiles), yvals(nfiles);
  Varray2D<double> xbounds(nfiles), ybounds(nfiles);

  for (int fileID = 0; fileID < nfiles; fileID++)
    {
      gridID = vlistGrid(ef[fileID].vlistID, igrid);
      const auto gridtype = (globalGridType != CDI_UNDEFID) ? globalGridType : gridInqType(gridID);
      if (gridtype == GRID_LONLAT || gridtype == GRID_GAUSSIAN)
        lregular = true;
      else if (gridtype == GRID_CURVILINEAR)
        lcurvilinear = true;
      else if (gridtype == GRID_UNSTRUCTURED)
        lunstructured = true;
      else if (gridtype == GRID_GENERIC /*&& gridInqXsize(gridID) > 0 && gridInqYsize(gridID) > 0*/)
        ;
      else
        cdoAbort("Unsupported grid type: %s!", gridNamePtr(gridtype));

      xsize[fileID] = lunstructured ? gridInqSize(gridID) : gridInqXsize(gridID);
      ysize[fileID] = lunstructured ? 1 : gridInqYsize(gridID);
      if (xsize[fileID] == 0) xsize[fileID] = 1;
      if (ysize[fileID] == 0) ysize[fileID] = 1;

      if (lregular)
        {
          xvals[fileID].resize(xsize[fileID]);
          yvals[fileID].resize(ysize[fileID]);
        }
      else if (lcurvilinear || lunstructured)
        {
          if (lcenter)
            {
              xvals[fileID].resize(xsize[fileID] * ysize[fileID]);
              yvals[fileID].resize(xsize[fileID] * ysize[fileID]);
            }
          if (lbounds)
            {
              xbounds[fileID].resize(nv * xsize[fileID] * ysize[fileID]);
              ybounds[fileID].resize(nv * xsize[fileID] * ysize[fileID]);
            }
        }

      if (lregular || lcurvilinear || lunstructured)
        {
          if (lcenter)
            {
              gridInqXvals(gridID, xvals[fileID].data());
              gridInqYvals(gridID, yvals[fileID].data());
            }
          if (lbounds)
            {
              gridInqXbounds(gridID, xbounds[fileID].data());
              gridInqYbounds(gridID, ybounds[fileID].data());
            }
        }
      // printf("fileID %d, gridID %d\n", fileID, gridID);

      xyinfo[fileID].id = fileID;
      if (lregular)
        {
          xyinfo[fileID].x = xvals[fileID][0];
          xyinfo[fileID].y = yvals[fileID][0];

          if (ysize[fileID] > 1)
            {
              if (yvals[fileID][0] > yvals[fileID][ysize[fileID] - 1]) lsouthnorth = false;
            }
        }
    }

  if (Options::cdoVerbose && lregular)
    for (int fileID = 0; fileID < nfiles; fileID++) printf("1 %d %g %g \n", xyinfo[fileID].id, xyinfo[fileID].x, xyinfo[fileID].y);

  if (lregular)
    {
      std::sort(xyinfo.begin(), xyinfo.end(), cmpx);

      if (Options::cdoVerbose)
        for (int fileID = 0; fileID < nfiles; fileID++)
          printf("2 %d %g %g \n", xyinfo[fileID].id, xyinfo[fileID].x, xyinfo[fileID].y);

      if (lsouthnorth)
        std::sort(xyinfo.begin(), xyinfo.end(), cmpxy_lt);
      else
        std::sort(xyinfo.begin(), xyinfo.end(), cmpxy_gt);

      if (Options::cdoVerbose)
        for (int fileID = 0; fileID < nfiles; fileID++)
          printf("3 %d %g %g \n", xyinfo[fileID].id, xyinfo[fileID].x, xyinfo[fileID].y);

      if (nx <= 0)
        {
          nx = 1;
          for (int fileID = 1; fileID < nfiles; fileID++)
            {
              if (DBL_IS_EQUAL(xyinfo[0].y, xyinfo[fileID].y))
                nx++;
              else
                break;
            }
        }
    }
  else
    {
      if (nx <= 0) nx = nfiles;
    }

  const long ny = nfiles / nx;
  if (nx * ny != nfiles) cdoAbort("Number of input files (%ld) and number of blocks (%ldx%ld) differ!", nfiles, nx, ny);

  long xsize2 = 0;
  for (long i = 0; i < nx; ++i) xsize2 += xsize[xyinfo[i].id];
  long ysize2 = 0;
  for (long j = 0; j < ny; ++j) ysize2 += ysize[xyinfo[j * nx].id];
  if (Options::cdoVerbose) cdoPrint("xsize2 %ld  ysize2 %ld", xsize2, ysize2);

  {  // verify size of data
    const long xs = xsize[xyinfo[0].id];
    for (long j = 1; j < ny; ++j)
      if (xsize[xyinfo[j * nx].id] != xs) cdoAbort("xsize=%ld differ from first file (xsize=%ld)!", xsize[xyinfo[j * nx].id], xs);
    const long ys = ysize[xyinfo[0].id];
    for (long i = 1; i < nx; ++i)
      if (ysize[xyinfo[i].id] != ys) cdoAbort("ysize=%ld differ from first file (ysize=%ld)!", ysize[xyinfo[i].id], ys);
  }

  Varray<double> xvals2, yvals2;
  Varray<double> xbounds2, ybounds2;
  if (lregular)
    {
      xvals2.resize(xsize2);
      yvals2.resize(ysize2);
    }
  else if (lcurvilinear || lunstructured)
    {
      if (lcenter)
        {
          xvals2.resize(xsize2 * ysize2);
          yvals2.resize(xsize2 * ysize2);
        }
      if (lbounds)
        {
          xbounds2.resize(nv * xsize2 * ysize2);
          ybounds2.resize(nv * xsize2 * ysize2);
        }
    }

  std::vector<long> xoff(nx + 1), yoff(ny + 1);

  xoff[0] = 0;
  for (long i = 0; i < nx; ++i)
    {
      const long idx = xyinfo[i].id;
      if (lregular) arrayCopy(xsize[idx], xvals[idx].data(), &xvals2[xoff[i]]);
      xoff[i + 1] = xoff[i] + xsize[idx];
    }

  yoff[0] = 0;
  for (long j = 0; j < ny; ++j)
    {
      const long idx = xyinfo[j * nx].id;
      if (lregular) arrayCopy(ysize[idx], yvals[idx].data(), &yvals2[yoff[j]]);
      yoff[j + 1] = yoff[j] + ysize[idx];
    }

  if (!ginit)
    {
      for (int fileID = 0; fileID < nfiles; fileID++)
        {
          const long idx = xyinfo[fileID].id;
          const long iy = fileID / nx;
          const long ix = fileID - iy * nx;

          const long offset = yoff[iy] * xsize2 + xoff[ix];
          // printf("fileID %d %d, iy %d, ix %d, offset %d\n", fileID, xyinfo[fileID].id, iy, ix, offset);

          long ij = 0;
          for (long j = 0; j < ysize[idx]; ++j)
            for (long i = 0; i < xsize[idx]; ++i)
              {
                if (lcurvilinear || lunstructured)
                  {
                    if (lcenter)
                      {
                        xvals2[offset + j * xsize2 + i] = xvals[idx][ij];
                        yvals2[offset + j * xsize2 + i] = yvals[idx][ij];
                      }
                    if (lbounds)
                      {
                        for (long k = 0; k < nv; ++k)
                          {
                            xbounds2[(offset + j * xsize2 + i) * nv + k] = xbounds[idx][ij * nv + k];
                            ybounds2[(offset + j * xsize2 + i) * nv + k] = ybounds[idx][ij * nv + k];
                          }
                      }
                  }
                ef[idx].gridindex[ij++] = offset + j * xsize2 + i;
              }
        }
    }

  gridID2 = gridCreate(gridtype0, xsize2 * ysize2);
  if (!lunstructured)
    {
      gridDefXsize(gridID2, xsize2);
      gridDefYsize(gridID2, ysize2);
    }
  else if (nv > 0)
    {
      gridDefNvertex(gridID2, nv);
    }

  if (lregular || lcurvilinear || lunstructured)
    {
      if (lcenter)
        {
          gridDefXvals(gridID2, xvals2.data());
          gridDefYvals(gridID2, yvals2.data());
        }
      if (lbounds)
        {
          gridDefXbounds(gridID2, xbounds2.data());
          gridDefYbounds(gridID2, ybounds2.data());
        }
    }

  gridID = vlistGrid(ef[0].vlistID, igrid);

  grid_copy_keys(gridID, gridID2);

  if (gridtype0 == GRID_PROJECTION) grid_copy_mapping(gridID, gridID2);

  return gridID2;
}

void *
Collgrid(void *process)
{
  int nxblocks = -1;
  int varID, levelID;
  int nrecs0;

  cdoInitialize(process);

  const auto nfiles = cdoStreamCnt() - 1;
  const auto ofilename = cdoGetStreamName(nfiles);

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

  std::vector<EnsfileType> ef(nfiles);

  for (int fileID = 0; fileID < nfiles; fileID++)
    {
      ef[fileID].streamID = cdoOpenRead(fileID);
      ef[fileID].vlistID = cdoStreamInqVlist(ef[fileID].streamID);
      varListInit(ef[fileID].varList, ef[fileID].vlistID);
    }

  const auto &varList1 = ef[0].varList;
  const auto vlistID1 = ef[0].vlistID;
  vlistClearFlag(vlistID1);

  // check that the contents is always the same
  for (int fileID = 1; fileID < nfiles; fileID++) vlistCompare(vlistID1, ef[fileID].vlistID, CMP_NAME | CMP_NLEVEL);

  const auto nvars = vlistNvars(vlistID1);
  std::vector<bool> vars(nvars, false), vars1(nvars, false);

  auto nsel = operatorArgc();
  int noff = 0;

  if (nsel > 0)
    {
      auto argument = cdoOperatorArgv(0).c_str();
      if (strcmp(argument, "gridtype=unstructured") == 0)
        {
          nsel--;
          noff++;
          globalGridType = GRID_UNSTRUCTURED;
        }
      else
        {
          int len = (int) strlen(argument);
          while (--len >= 0 && isdigit(argument[len]))
            ;

          if (len == -1)
            {
              nsel--;
              noff++;
              nxblocks = parameter2int(argument);
            }
        }
    }

  if (nsel == 0)
    {
      for (varID = 0; varID < nvars; varID++) vars1[varID] = true;
    }
  else
    {
      if (Options::cdoVerbose)
        for (int i = 0; i < nsel; i++) cdoPrint("name %d = %s\n", i + 1, cdoOperatorArgv(noff + i).c_str());

      std::vector<bool> selfound(nsel);
      for (int i = 0; i < nsel; i++) selfound[i] = false;

      for (varID = 0; varID < nvars; varID++)
        {
          for (int isel = 0; isel < nsel; isel++)
            {
              if (cdoOperatorArgv(noff + isel) == varList1[varID].name)
                {
                  selfound[isel] = true;
                  vars1[varID] = true;
                }
            }
        }

      int err = 0;
      for (int isel = 0; isel < nsel; isel++)
        {
          if (selfound[isel] == false)
            {
              err++;
              cdoWarning("Variable name %s not found!", cdoOperatorArgv(noff + isel).c_str());
            }
        }
      if (err) cdoAbort("Could not find all requested variables: (%d/%d)", nsel - err, nsel);
    }

  for (varID = 0; varID < nvars; varID++)
    {
      if (vars1[varID])
        {
          const auto nlevels = varList1[varID].nlevels;
          for (int levID = 0; levID < nlevels; levID++) vlistDefFlag(vlistID1, varID, levID, true);
        }
    }

  for (int fileID = 0; fileID < nfiles; fileID++)
    {
      const auto gridsize = vlistGridsizeMax(ef[fileID].vlistID);
      ef[fileID].gridsize = gridsize;
      ef[fileID].gridindex.resize(gridsize);
    }

  const auto vlistID2 = vlistCreate();
  cdoVlistCopyFlag(vlistID2, vlistID1);
  // if ( Options::cdoVerbose ) vlistPrint(vlistID1);
  // if ( Options::cdoVerbose ) vlistPrint(vlistID2);
  // auto vlistID2 = vlistDuplicate(vlistID1);
  const auto nvars2 = vlistNvars(vlistID2);

  const auto ngrids1 = vlistNgrids(vlistID1);
  const auto ngrids2 = vlistNgrids(vlistID2);

  std::vector<int> gridIDs(ngrids2);

  bool ginit = false;
  for (int i2 = 0; i2 < ngrids2; ++i2)
    {
      int i1;
      for (i1 = 0; i1 < ngrids1; ++i1)
        if (vlistGrid(vlistID1, i1) == vlistGrid(vlistID2, i2)) break;

      if (!ginit)
        {
          gridIDs[i2] = genGrid(ngrids2, nfiles, ef, ginit, i1, nxblocks);
          if (gridIDs[i2] != -1) ginit = true;
        }
      else
        gridIDs[i2] = genGrid(ngrids2, nfiles, ef, ginit, i1, nxblocks);
    }

  const auto taxisID1 = vlistInqTaxis(vlistID1);
  const auto taxisID2 = taxisDuplicate(taxisID1);
  vlistDefTaxis(vlistID2, taxisID2);

  size_t gridsize2 = 0;
  for (int i = 0; i < ngrids2; ++i)
    {
      if (gridIDs[i] != -1)
        {
          if (gridsize2 == 0) gridsize2 = gridInqSize(gridIDs[i]);
          if (gridsize2 != gridInqSize(gridIDs[i]))
            cdoAbort("Target gridsize differ (gridID=%d  gridsize=%zu)!", i + 1, gridInqSize(gridIDs[i]));
          vlistChangeGridIndex(vlistID2, i, gridIDs[i]);
        }
    }
  
  VarList varList2;
  varListInit(varList2, vlistID2);

  for (varID = 0; varID < nvars2; varID++)
    {
      const auto gridID = varList2[varID].gridID;
      for (int i = 0; i < ngrids2; ++i)
        {
          if (gridIDs[i] != -1)
            {
              if (gridID == vlistGrid(vlistID2, i) || gridsize2 == gridInqSize(gridID)) vars[varID] = true;
              break;
            }
        }
    }

  const auto streamID2 = cdoOpenWrite(nfiles);
  cdoDefVlist(streamID2, vlistID2);

  Field field2;

  int tsID = 0;
  do
    {
      nrecs0 = cdoStreamInqTimestep(ef[0].streamID, tsID);
      for (int fileID = 1; fileID < nfiles; fileID++)
        {
          const auto nrecs = cdoStreamInqTimestep(ef[fileID].streamID, tsID);
          if (nrecs != nrecs0)
            cdoAbort("Number of records at time step %d of %s and %s differ!", tsID + 1, cdoGetStreamName(0),
                     cdoGetStreamName(fileID));
        }

      taxisCopyTimestep(taxisID2, taxisID1);

      if (nrecs0 > 0) cdoDefTimestep(streamID2, tsID);

      for (int recID = 0; recID < nrecs0; recID++)
        {
          cdoInqRecord(ef[0].streamID, &varID, &levelID);
          if (Options::cdoVerbose && tsID == 0) printf(" tsID, recID, varID, levelID %d %d %d %d\n", tsID, recID, varID, levelID);

          for (int fileID = 1; fileID < nfiles; fileID++)
            {
              int varIDx, levelIDx;
              cdoInqRecord(ef[fileID].streamID, &varIDx, &levelIDx);
            }

          if (vlistInqFlag(vlistID1, varID, levelID) == true)
            {
              const auto varID2 = vlistFindVar(vlistID2, varID);
              const auto levelID2 = vlistFindLevel(vlistID2, varID, levelID);
              if (Options::cdoVerbose && tsID == 0) printf("varID %d %d levelID %d %d\n", varID, varID2, levelID, levelID2);

              field2.init(varList2[varID2]);

              const auto missval = varList2[varID2].missval;
              if (field2.memType == MemType::Float)
                for (size_t i = 0; i < gridsize2; i++) field2.vec_f[i] = missval;
              else
                for (size_t i = 0; i < gridsize2; i++) field2.vec_d[i] = missval;

#ifdef _OPENMP
#pragma omp parallel for default(shared)
#endif
              for (int fileID = 0; fileID < nfiles; fileID++)
                {
                  auto &field1 = ef[fileID].field;
                  field1.init(ef[fileID].varList[varID]);
                  cdoReadRecord(ef[fileID].streamID, field1);

                  if (vars[varID2])
                    {
                      const auto gridsize = ef[fileID].gridsize;
                      const auto &gindex = ef[fileID].gridindex;
                      if (field1.memType == MemType::Float)
                        for (size_t i = 0; i < gridsize; ++i) field2.vec_f[gindex[i]] = field1.vec_f[i];
                      else
                        for (size_t i = 0; i < gridsize; ++i) field2.vec_d[gindex[i]] = field1.vec_d[i];
                    }
                }

              cdoDefRecord(streamID2, varID2, levelID2);

              if (vars[varID2])
                {
                  fieldNumMV(field2);
                  cdoWriteRecord(streamID2, field2);
                }
              else
                cdoWriteRecord(streamID2, ef[0].field);
            }
        }

      tsID++;
    }
  while (nrecs0 > 0);

  for (int fileID = 0; fileID < nfiles; fileID++) cdoStreamClose(ef[fileID].streamID);

  cdoStreamClose(streamID2);

  cdoFinish();

  return nullptr;
}
