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

  Copyright (C) 2003-2018 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 "cdo_int.h"
#include "util_string.h"
#include "cdo_vlist.h"

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

double
cdoZaxisInqLevel(int zaxisID, int levelID)
{
  int zaxistype = zaxisInqType(zaxisID);
  double level = zaxisInqLevels(zaxisID, NULL) ? zaxisInqLevel(zaxisID, levelID) : (zaxistype == ZAXIS_SURFACE) ? 0 : levelID + 1;
  return level;
}

int
cdoZaxisInqLevels(int zaxisID, double *levels)
{
  int size = zaxisInqLevels(zaxisID, NULL);

  if (levels)
    {
      if (size)
        zaxisInqLevels(zaxisID, levels);
      else
        {
          size = zaxisInqSize(zaxisID);
          if (size == 1 && zaxisInqType(zaxisID) == ZAXIS_SURFACE)
            levels[0] = 0;
          else
            for (int i = 0; i < size; ++i) levels[i] = i + 1;
        }
    }

  return size;
}

static void
compare_lat_reg2d(size_t ysize, int gridID1, int gridID2)
{
  if (ysize > 1)
    {
      std::vector<double> yvals1(ysize);
      std::vector<double> yvals2(ysize);

      gridInqYvals(gridID1, &yvals1[0]);
      gridInqYvals(gridID2, &yvals2[0]);

      if (IS_EQUAL(yvals1[0], yvals2[ysize - 1]) && IS_EQUAL(yvals1[ysize - 1], yvals2[0]))
        {
          if (yvals1[0] > yvals2[0])
            cdoWarning("Latitude orientation differ! First grid: N->S; second grid: S->N");
          else
            cdoWarning("Latitude orientation differ! First grid: S->N; second grid: N->S");
        }
      else
        {
          for (size_t i = 0; i < ysize; ++i)
            if (fabs(yvals1[i] - yvals2[i]) > 3.e-5)
              {
                cdoWarning("Grid latitudes differ!");
                break;
              }
        }
    }
}

static void
compare_lon_reg2d(size_t xsize, int gridID1, int gridID2)
{
  if (xsize > 1)
    {
      std::vector<double> xvals1(xsize);
      std::vector<double> xvals2(xsize);

      gridInqXvals(gridID1, &xvals1[0]);
      gridInqXvals(gridID2, &xvals2[0]);

      for (size_t i = 0; i < xsize; ++i)
        if (fabs(xvals1[i] - xvals2[i]) > 3.e-5)
          {
            cdoWarning("Grid longitudes differ!");
            break;
          }
    }
}

static void
compare_grid_unstructured(int gridID1, int gridID2)
{
  if (gridInqXvals(gridID1, NULL) && gridInqXvals(gridID1, NULL) == gridInqXvals(gridID2, NULL) && gridInqYvals(gridID1, NULL)
      && gridInqYvals(gridID1, NULL) == gridInqYvals(gridID2, NULL))
    {
      size_t gridsize = gridInqSize(gridID1);
      std::vector<double> xvals1(gridsize);
      std::vector<double> xvals2(gridsize);
      std::vector<double> yvals1(gridsize);
      std::vector<double> yvals2(gridsize);

      gridInqXvals(gridID1, &xvals1[0]);
      gridInqXvals(gridID2, &xvals2[0]);
      gridInqYvals(gridID1, &yvals1[0]);
      gridInqYvals(gridID2, &yvals2[0]);

      size_t inc = gridsize > 10000 ? gridsize / 1000 : 1;
      for (size_t i = 0; i < gridsize; i += inc)
        if (fabs(xvals1[i] - xvals2[i]) > 2.e-5 || fabs(yvals1[i] - yvals2[i]) > 2.e-5)
          {
            // printf("%d %g %g %g %g %g %g\n", i, xvals1[i], xvals2[i],
            // yvals1[i], yvals2[i], xvals1[i] - xvals2[i], yvals1[i] -
            // yvals2[i]);
            cdoWarning("Geographic location of some grid points differ!");
            break;
          }
    }
}

void
cdoCompareGrids(int gridID1, int gridID2)
{
  /* compare grids of first variable */

  if (gridInqType(gridID1) == gridInqType(gridID2))
    {
      if (gridInqType(gridID1) == GRID_GAUSSIAN || gridInqType(gridID1) == GRID_LONLAT)
        {
          size_t xsize = gridInqXsize(gridID1);
          size_t ysize = gridInqYsize(gridID1);

          if (ysize == gridInqYsize(gridID2))
            compare_lat_reg2d(ysize, gridID1, gridID2);
          else
            cdoWarning("ysize of input grids differ!");

          if (xsize == gridInqXsize(gridID2))
            compare_lon_reg2d(xsize, gridID1, gridID2);
          else
            cdoWarning("xsize of input grids differ!");
        }
      else if (gridInqType(gridID1) == GRID_CURVILINEAR || gridInqType(gridID1) == GRID_UNSTRUCTURED)
        {
          compare_grid_unstructured(gridID1, gridID2);
        }
    }
  else if (gridInqSize(gridID1) > 1)
    {
      cdoWarning("Grids have different types! First grid: %s; second grid: %s", gridNamePtr(gridInqType(gridID1)),
                 gridNamePtr(gridInqType(gridID2)));
    }
}

static int
cmpnames(const void *s1, const void *s2)
{
  const char *name1 = (const char *) s1;
  const char *name2 = (const char *) s2;

  return strcmp(name1, name2);
}

void
vlistCompare(int vlistID1, int vlistID2, int flag)
{
  int varID;
  bool lchecknames = false;

  int nvars = vlistNvars(vlistID1);

  if (nvars != vlistNvars(vlistID2)) cdoAbort("Input streams have different number of variables per timestep!");

  if (vlistNrecs(vlistID1) != vlistNrecs(vlistID2))
    cdoAbort("Input streams have different number of %s per timestep!", nvars == 1 ? "layers" : "records");

  for (varID = 0; varID < nvars; varID++)
    {
      if (nvars > 1)
        {
          if (flag & CMP_NAME)
            {
              char name1[CDI_MAX_NAME], name2[CDI_MAX_NAME];
              vlistInqVarName(vlistID1, varID, name1);
              vlistInqVarName(vlistID2, varID, name2);
              strtolower(name1);
              strtolower(name2);
              if (strcmp(name1, name2) != 0)
                {
                  cdoWarning("Input streams have different parameter names!");
                  lchecknames = true;
                  flag -= CMP_NAME;
                  //    break;
                }
            }
        }

      if (flag & CMP_GRIDSIZE)
        {
          if (gridInqSize(vlistInqVarGrid(vlistID1, varID)) != gridInqSize(vlistInqVarGrid(vlistID2, varID)))
            cdoAbort("Grid size of the input parameters do not match!");
        }

      if (flag & CMP_NLEVEL)
        {
          int zaxisID1 = vlistInqVarZaxis(vlistID1, varID);
          int zaxisID2 = vlistInqVarZaxis(vlistID2, varID);
          if (zaxisID1 != zaxisID2)
            {
              int nlev1 = zaxisInqSize(zaxisID1);
              int nlev2 = zaxisInqSize(zaxisID2);
              if (nlev1 != nlev2) cdoAbort("Number of levels of the input parameters do not match!");

              std::vector<double> lev1(nlev1);
              std::vector<double> lev2(nlev1);
              cdoZaxisInqLevels(zaxisID1, &lev1[0]);
              cdoZaxisInqLevels(zaxisID2, &lev2[0]);

              bool ldiffer = false;
              for (int i = 0; i < nlev1; ++i)
                if (IS_NOT_EQUAL(lev1[i], lev2[i]))
                  {
                    ldiffer = true;
                    break;
                  }
              if (ldiffer)
                {
                  ldiffer = false;
                  for (int i = 0; i < nlev1; ++i)
                    if (IS_NOT_EQUAL(lev1[i], lev2[nlev1 - 1 - i]))
                      {
                        ldiffer = true;
                        break;
                      }

                  if (ldiffer)
                    cdoWarning("Input parameters have different levels!");
                  else
                    cdoWarning("Z-axis orientation differ!");
                  break;
                }
            }
        }
    }

  if (flag & CMP_GRID)
    {
      int gridID1 = vlistInqVarGrid(vlistID1, 0);
      int gridID2 = vlistInqVarGrid(vlistID2, 0);
      cdoCompareGrids(gridID1, gridID2);
    }

  if (lchecknames)
    {
      NEW_2D(char, names1, nvars, CDI_MAX_NAME);
      NEW_2D(char, names2, nvars, CDI_MAX_NAME);

      for (varID = 0; varID < nvars; varID++) vlistInqVarName(vlistID1, varID, &names1[varID][0]);
      for (varID = 0; varID < nvars; varID++) vlistInqVarName(vlistID2, varID, &names2[varID][0]);

      qsort(names1[0], nvars, CDI_MAX_NAME, cmpnames);
      qsort(names2[0], nvars, CDI_MAX_NAME, cmpnames);

      for (varID = 0; varID < nvars; varID++)
        if (strcmp(names1[varID], names2[varID]) != 0) break;

      if (varID == nvars) cdoPrint("Use CDO option --sortname to sort the parameter by name (NetCDF only)!");

      DELETE_2D(names1);
      DELETE_2D(names2);
    }
}

int
vlistCompareX(int vlistID1, int vlistID2, int flag)
{
  int nvars = vlistNvars(vlistID1);
  int nvars2 = vlistNvars(vlistID2);
  int nlevels2 = zaxisInqSize(vlistInqVarZaxis(vlistID2, 0));

  if (nvars2 != 1) cdoAbort("Internal problem, vlistCompareX() called with unexpected vlistID2 argument!");

  for (int varID = 0; varID < nvars; varID++)
    {
      if (flag & CMP_GRIDSIZE)
        {
          if (gridInqSize(vlistInqVarGrid(vlistID1, varID)) != gridInqSize(vlistInqVarGrid(vlistID2, 0)))
            cdoAbort("Grid size of the input parameters do not match!");
        }

      if (flag & CMP_NLEVEL)
        {
          if ((zaxisInqSize(vlistInqVarZaxis(vlistID1, varID)) != nlevels2) && nlevels2 > 1)
            cdoAbort("Number of levels of the input parameters do not match!");
        }
    }

  if (flag & CMP_GRID)
    {
      int gridID1 = vlistInqVarGrid(vlistID1, 0);
      int gridID2 = vlistInqVarGrid(vlistID2, 0);
      cdoCompareGrids(gridID1, gridID2);
    }

  return nlevels2;
}

int
vlistIsSzipped(int vlistID)
{
  int lszip = FALSE;
  int nvars = vlistNvars(vlistID);

  for (int varID = 0; varID < nvars; varID++)
    {
      int comptype = vlistInqVarCompType(vlistID, varID);
      if (comptype == CDI_COMPRESS_SZIP)
        {
          lszip = TRUE;
          break;
        }
    }

  return lszip;
}

int
vlistInqNWPV(int vlistID, int varID)
{
  int nwpv;  // number of words per value; real:1  complex:2

  if (vlistInqVarDatatype(vlistID, varID) == CDI_DATATYPE_CPX32 || vlistInqVarDatatype(vlistID, varID) == CDI_DATATYPE_CPX64)
    nwpv = 2;
  else
    nwpv = 1;

  return nwpv;
}

size_t
vlist_check_gridsize(int vlistID)
{
  bool lerror = false;
  int ngrids = vlistNgrids(vlistID);
  int gridID = vlistGrid(vlistID, 0);
  size_t ngp = gridInqSize(gridID);

  /* check gridsize */
  for (int index = 0; index < ngrids; ++index)
    {
      gridID = vlistGrid(vlistID, index);
      if (ngp != gridInqSize(gridID))
        {
          lerror = true;
          break;
        }
    }

  if (lerror)
    {
      cdoPrint("This operator requires all variables on the same horizontal grid.");
      cdoPrint("Horizontal grids found:");
      for (int index = 0; index < ngrids; ++index)
        {
          gridID = vlistGrid(vlistID, index);
          cdoPrint("  grid=%d  type=%s  points=%zu", index + 1, gridNamePtr(gridInqType(gridID)), gridInqSize(gridID));
        }
      cdoAbort("The input stream contains variables on different horizontal grids!");
    }

  return ngp;
}

double *
vlist_read_vct(int vlistID, int *rzaxisIDh, int *rnvct, int *rnhlev, int *rnhlevf, int *rnhlevh)
{
  double *vct = NULL;
  int zaxisIDh = -1;
  int nhlev = 0, nhlevf = 0, nhlevh = 0;
  int nvct = 0;

  bool lhavevct = false;
  int nzaxis = vlistNzaxis(vlistID);
  for (int iz = 0; iz < nzaxis; ++iz)
    {
      // bool mono_level = false;
      bool mono_level = true;
      int zaxisID = vlistZaxis(vlistID, iz);
      int nlevel = zaxisInqSize(zaxisID);
      int zaxistype = zaxisInqType(zaxisID);

      if (cdoVerbose)
        cdoPrint("ZAXIS_HYBRID = %d ZAXIS_HYBRID_HALF=%d nlevel=%d mono_level=%d", zaxisInqType(zaxisID) == ZAXIS_HYBRID,
                 zaxisInqType(zaxisID) == ZAXIS_HYBRID_HALF, nlevel, mono_level);

      if ((zaxistype == ZAXIS_HYBRID || zaxistype == ZAXIS_HYBRID_HALF) && nlevel > 1 && !mono_level)
        {
          int l;
          std::vector<double> level(nlevel);
          cdoZaxisInqLevels(zaxisID, &level[0]);
          for (l = 0; l < nlevel; l++)
            {
              if ((l + 1) != (int) (level[l] + 0.5)) break;
            }
          if (l == nlevel) mono_level = true;
        }

      if ((zaxistype == ZAXIS_HYBRID || zaxistype == ZAXIS_HYBRID_HALF) && nlevel > 1 && mono_level)
        {
          nvct = zaxisInqVctSize(zaxisID);
          if (nlevel == (nvct / 2 - 1))
            {
              if (lhavevct == false)
                {
                  lhavevct = true;
                  zaxisIDh = zaxisID;
                  nhlev = nlevel;
                  nhlevf = nhlev;
                  nhlevh = nhlevf + 1;

                  vct = (double *) Malloc(nvct * sizeof(double));
                  zaxisInqVct(zaxisID, vct);
                  if (cdoVerbose)
                    cdoPrint("Detected half-level model definition : nlevel == (nvct/2 - 1) (nlevel: %d, nvct: %d, nhlevf: %d, "
                             "nhlevh: %d) ",
                             nlevel, nvct, nhlevf, nhlevh);
                }
            }
          else if (nlevel == (nvct / 2))
            {
              if (lhavevct == false)
                {
                  lhavevct = true;
                  zaxisIDh = zaxisID;
                  nhlev = nlevel;
                  nhlevf = nhlev - 1;
                  nhlevh = nhlev;

                  vct = (double *) Malloc(nvct * sizeof(double));
                  zaxisInqVct(zaxisID, vct);
                  if (cdoVerbose)
                    cdoPrint(
                        "Detected full-level model definition : nlevel == (nvct/2) (nlevel: %d, nvct: %d, nhlevf: %d, nhlevh: %d) ",
                        nlevel, nvct, nhlevf, nhlevh);
                }
            }
          else if (nlevel == (nvct - 4 - 1))
            {
              if (lhavevct == false)
                {
                  int vctsize;
                  int voff = 4;

                  std::vector<double> rvct(nvct);
                  zaxisInqVct(zaxisID, &rvct[0]);

                  if ((int) (rvct[0] + 0.5) == 100000 && rvct[voff] < rvct[voff + 1])
                    {
                      lhavevct = true;
                      zaxisIDh = zaxisID;
                      nhlev = nlevel;
                      nhlevf = nhlev;
                      nhlevh = nhlev + 1;

                      vctsize = 2 * nhlevh;
                      vct = (double *) Malloc(vctsize * sizeof(double));

                      /* calculate VCT for LM */

                      for (int i = 0; i < vctsize / 2; i++)
                        {
                          if (rvct[voff + i] >= rvct[voff] && rvct[voff + i] <= rvct[3])
                            {
                              vct[i] = rvct[0] * rvct[voff + i];
                              vct[vctsize / 2 + i] = 0;
                            }
                          else
                            {
                              vct[i] = (rvct[0] * rvct[3] * (1 - rvct[voff + i])) / (1 - rvct[3]);
                              vct[vctsize / 2 + i] = (rvct[voff + i] - rvct[3]) / (1 - rvct[3]);
                            }
                        }

                      if (cdoVerbose)
                        {
                          for (int i = 0; i < vctsize / 2; i++)
                            fprintf(stdout, "%5d %25.17f %25.17f\n", i, vct[i], vct[vctsize / 2 + i]);
                        }
                    }
                }
            }
        }
    }

  *rzaxisIDh = zaxisIDh;
  *rnvct = nvct;
  *rnhlev = nhlev;
  *rnhlevf = nhlevf;
  *rnhlevh = nhlevh;

  return vct;
}

void
vlist_change_hybrid_zaxis(int vlistID1, int vlistID2, int zaxisID1, int zaxisID2)
{
  int nvct0 = 0;
  std::vector<double> vct;

  int nzaxis = vlistNzaxis(vlistID1);
  for (int i = 0; i < nzaxis; ++i)
    {
      int zaxisID = vlistZaxis(vlistID1, i);
      int nlevel = zaxisInqSize(zaxisID);

      if (zaxisID == zaxisID1 && nlevel > 1)
        {
          int nvct = zaxisInqVctSize(zaxisID);
          if (!vct.size())
            {
              nvct0 = nvct;
              vct.resize(nvct);
              zaxisInqVct(zaxisID, vct.data());

              vlistChangeZaxisIndex(vlistID2, i, zaxisID2);
            }
          else
            {
              if (nvct0 == nvct && memcmp(vct.data(), zaxisInqVctPtr(zaxisID), nvct * sizeof(double)) == 0)
                vlistChangeZaxisIndex(vlistID2, i, zaxisID2);
            }
        }
    }
}

int
vlist_get_psvarid(int vlistID, int zaxisID)
{
  int psvarid = -1;
  char name[CDI_MAX_NAME];
  char psname[CDI_MAX_NAME];
  psname[0] = 0;
  cdiZaxisInqKeyStr(zaxisID, CDI_KEY_PSNAME, CDI_MAX_NAME, psname);

  if (psname[0])
    {
      int nvars = vlistNvars(vlistID);
      for (int varID = 0; varID < nvars; ++varID)
        {
          vlistInqVarName(vlistID, varID, name);
          if (strcmp(name, psname) == 0)
            {
              psvarid = varID;
              break;
            }
        }
      if (cdoVerbose && psvarid == -1) cdoWarning("Surface pressure variable not found - %s", psname);
    }

  return psvarid;
}
