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

#include "process_int.h"
#include <mpim_grid.h>
#include "griddes.h"
#include "util_string.h"

size_t genIcosphereCoords(int subdivisions, bool lbounds, std::vector<double> &xvals, std::vector<double> &yvals,
                          std::vector<double> &xbounds, std::vector<double> &ybounds);

static void
gen_grid_icosphere(GridDesciption &grid, const char *pline)
{
  int gridtype = GRID_UNSTRUCTURED;
  bool lbounds = true;
  long b = 0;

  if (*pline != 0)
    {
      if (*pline == 'r')
        pline++;
      else
        return;

      if (*pline == 0) return;
      if (!isdigit((int) *pline)) return;

      char *endptr = (char *) pline;
      const long r = strtol(pline, &endptr, 10);
      if (*endptr == 0 || r != 2) return;
      pline = endptr;

      if (*pline == 'b')
        pline++;
      else
        return;

      if (*pline == 0) return;
      if (!isdigit((int) *pline)) return;

      endptr = (char *) pline;
      b = strtol(pline, &endptr, 10);

      if (*endptr != 0)
        {
          pline = endptr;
          if (*pline != '_') return;
          pline++;
          if (*pline == 0) return;
          if (*pline == '0')
            {
              lbounds = false;
              pline++;
            }
          if (*pline != 0) return;
        }
    }

  grid.type = gridtype;
  if (lbounds) grid.nvertex = 3;

  const auto ncells = genIcosphereCoords(b + 1, lbounds, grid.xvals, grid.yvals, grid.xbounds, grid.ybounds);
  grid.xsize = ncells;
  grid.ysize = ncells;
  strcpy(grid.xname, "clon");
  strcpy(grid.yname, "clat");
  strcpy(grid.xunits, "radian");
  strcpy(grid.yunits, "radian");
}

static void
gen_grid_lonlat(GridDesciption &grid, const char *pline, double inc, double lon1, double lon2, double lat1, double lat2)
{
  int gridtype = GRID_LONLAT;
  bool lbounds = true;

  if (*pline != 0 && (*pline == '+' || *pline == '-') && (isdigit((int) *(pline + 1)) || ispunct((int) *(pline + 1))))
    {
      char *endptr = (char *) pline;
      const auto off = strtod(pline, &endptr);
      pline = endptr;

      lon1 -= off;
      lon2 += off;
      lat1 -= off;
      lat2 += off;
      if (lat1 < -90) lat1 = -90;
      if (lat2 > 90) lat2 = 90;
    }

  if (*pline != 0)
    {
      if (*pline == '_')
        pline++;
      else
        return;

      if (*pline == 0) return;

      if (!isdigit((int) *pline) && !ispunct((int) *pline)) return;

      char *endptr = (char *) pline;
      inc = strtod(pline, &endptr);
      if (*endptr != 0)
        {
          pline = endptr;
          if (*pline == '_')
            pline++;
          else
            return;

          if (*pline == 0) return;
          if (*pline == 'c')
            {
              gridtype = GRID_CURVILINEAR;
              pline++;
              if (*pline == '0')
                {
                  lbounds = false;
                  pline++;
                }
            }
          else if (*pline == 'u')
            {
              gridtype = GRID_UNSTRUCTURED;
              pline++;
              if (*pline == '0')
                {
                  lbounds = false;
                  pline++;
                }
            }
          if (*pline != 0) return;
        }

      if (inc < 1e-9) inc = 1;
    }

  grid.type = gridtype;

  if (lon1 >= lon2 || lat1 >= lat2) cdoAbort("Invalid grid box: lon1=%g lon2=%g lat1=%g lat2=%g", lon1, lon2, lat1, lat2);

  const auto nlon = (size_t)((lon2 - lon1) / inc + 0.5);
  const auto nlat = (size_t)((lat2 - lat1) / inc + 0.5);

  grid.xvals.resize(nlon);
  grid.yvals.resize(nlat);

  for (size_t i = 0; i < nlon; ++i) grid.xvals[i] = lon1 + inc / 2 + i * inc;
  for (size_t i = 0; i < nlat; ++i) grid.yvals[i] = lat1 + inc / 2 + i * inc;

  if (gridtype == GRID_LONLAT)
    {
      grid.xsize = nlon;
      grid.ysize = nlat;
    }
  else
    {
      std::vector<double> yvals(nlat);
      for (size_t j = 0; j < nlat; ++j) yvals[j] = grid.yvals[j];
      const auto gridsize = nlon * nlat;
      grid.xvals.resize(gridsize);
      grid.yvals.resize(gridsize);
      for (size_t j = 0; j < nlat; ++j)
        for (size_t i = 0; i < nlon; ++i)
          {
            grid.xvals[j * nlon + i] = grid.xvals[i];
            grid.yvals[j * nlon + i] = yvals[j];
          }

      if (gridtype == GRID_CURVILINEAR)
        {
          grid.xsize = nlon;
          grid.ysize = nlat;
        }
      else
        {
          grid.xsize = gridsize;
          grid.ysize = gridsize;
          if (lbounds) grid.nvertex = 4;
        }

      if (lbounds && nlon > 1 && nlat > 1)
        {
          std::vector<double> xbounds(2 * nlon), ybounds(2 * nlat);

          grid_gen_bounds(nlon, grid.xvals, xbounds);
          grid_gen_bounds(nlat, yvals, ybounds);
          grid_check_lat_borders(2 * nlat, ybounds.data());

          grid.xbounds.resize(4 * gridsize);
          grid.ybounds.resize(4 * gridsize);
          grid_gen_xbounds2D(nlon, nlat, xbounds, grid.xbounds);
          grid_gen_ybounds2D(nlon, nlat, ybounds, grid.ybounds);
        }
    }
}

static void
gen_grid_gme(GridDesciption &grid, const char *pline)
{
  if (isdigit((int) *pline))
    {
      char *endptr = (char *) pline;
      const long ni = strtol(pline, &endptr, 10);
      if (*endptr == 0)
        {
          grid.type = GRID_GME;
          grid.ni = ni;
          grid.nd = 10;
          gme_factorni(grid.ni, &grid.ni2, &grid.ni3);
          grid.size = (grid.ni + 1) * (grid.ni + 1) * 10;
        }
    }
}

static void
gaussianLatitudesInDegrees(std::vector<double> &lats, std::vector<double> &lat_bounds, size_t nlat)
{
  // lats(nlat)
  // lat_bounds(nlat+1)
  std::vector<double> latw(nlat), latw_cumsum(nlat);

  gaussianLatitudes(&lats[0], &latw[0], nlat);

  for (size_t j = 0; j < nlat; ++j) lats[j] = RAD2DEG * std::asin(lats[j]);

  latw_cumsum[0] = latw[0];
  for (size_t j = 1; j < nlat; ++j) latw_cumsum[j] = latw_cumsum[j - 1] + latw[j];

  lat_bounds[0] = 1.0;
  for (size_t j = 1; j < nlat; ++j) lat_bounds[j] = 1.0 - latw_cumsum[j - 1];
  lat_bounds[nlat] = -1.0;

  for (size_t j = 0; j < nlat + 1; ++j) lat_bounds[j] = RAD2DEG * std::asin(lat_bounds[j]);
}

static void
gen_grid_gea(GridDesciption &grid, const char *pline)
{
  if (isdigit((int) *pline))
    {
      auto endptr = (char *) pline;
      const auto dx = strtod(pline, &endptr);
      if (*endptr != 0) return;

      const auto dy = dx;
      constexpr auto re = 6378.137;
      constexpr auto f = 1.0 / 298.257223563;
      constexpr auto rp = re * (1.0 - f);
      constexpr auto polar_circumference = 2.0 * M_PI * rp;
      constexpr auto equator_circumference = 2.0 * M_PI * re;

      size_t nlat = 0.5 * polar_circumference / dy;
      if (nlat % 2) nlat++;

      std::vector<double> lats(nlat), lat_bounds(nlat + 1);
      gaussianLatitudesInDegrees(lats, lat_bounds, nlat);

      std::vector<double> cell_height(nlat);
      for (size_t j = 0; j < nlat; ++j) cell_height[j] = 0.25 * polar_circumference * (lat_bounds[j] - lat_bounds[j + 1]) / 90.0;

      size_t nlone = equator_circumference / dx;
      if (nlone % 2) nlone++;

      std::vector<int> reducedPoints(nlat);
      size_t ncells = 0;
      for (size_t j = 0; j < nlat; ++j)
        {
          const auto rlat = re * std::cos(DEG2RAD * lats[j]);
          const auto circumference = 2.0 * M_PI * rlat;
          const auto dx_to_use = dx * dy / cell_height[j];

          size_t nlon = std::max((int) std::lround(circumference / dx_to_use), 1);
          if (nlon % 2) nlon++;

          reducedPoints[j] = nlon;
          ncells += nlon;
        }

      // printf("%zu %zu %zu %zu %g\n", ncells, nlone, nlat, nlone*nlat, 100.0*ncells/(nlone*nlat));

      std::vector<double> lons(ncells);
      size_t ij = 0;
      for (size_t j = 0; j < nlat; ++j)
        {
          const size_t nlon = reducedPoints[j];
          for (size_t i = 0; i < nlon; ++i) lons[ij++] = i * 360. / nlon;
        }

      grid.type = GRID_GAUSSIAN_REDUCED;
      grid.size = ncells;
      grid.xsize = ncells;
      grid.ysize = nlat;
      grid.numLPE = nlat / 2;
      grid.xvals.resize(ncells);
      grid.yvals.resize(nlat);
      grid.ybounds.resize(nlat * 2);
      grid.reducedPoints.resize(nlat);
      for (size_t i = 0; i < ncells; ++i) grid.xvals[i] = lons[i];
      for (size_t j = 0; j < nlat; ++j) grid.yvals[j] = lats[j];
      for (size_t j = 0; j < nlat; ++j) grid.ybounds[j * 2 + 1] = lat_bounds[j];
      for (size_t j = 0; j < nlat; ++j) grid.ybounds[j * 2] = lat_bounds[j + 1];
      for (size_t j = 0; j < nlat; ++j) grid.reducedPoints[j] = reducedPoints[j];
    }
}

static void
gen_grid_reg2d(GridDesciption &grid, const char *pline)
{
  if (isdigit((int) *pline))
    {
      grid.type = GRID_LONLAT;
      grid.xsize = atol(pline);
      while (isdigit((int) *pline)) pline++;
      if (*pline == 'x' || *pline == '/' || *pline == '_')
        pline++;
      else
        {
          grid.type = CDI_UNDEFID;
          return;
        }
      grid.ysize = atol(pline);
      while (isdigit((int) *pline)) pline++;

      grid.def_xfirst = true;
      grid.def_yfirst = true;
    }
}

static void
gen_grid_point(GridDesciption &grid, const char *pline)
{
  if (isdigit((int) *pline) || ispunct((int) *pline) || *pline == '-')
    {
      grid.type = GRID_LONLAT;
      grid.xsize = 1;
      grid.ysize = 1;
      grid.xvals.resize(1);
      grid.yvals.resize(1);
      grid.xvals[0] = atof(pline);
      while (isdigit((int) *pline) || ispunct((int) *pline) || *pline == '-') pline++;
      if (*pline == '_') pline++;
      if (strncmp(pline, "lat=", 4) != 0)
        {
          grid.type = CDI_UNDEFID;
          return;
        }
      pline += 4;
      if (isdigit((int) *pline) || ispunct((int) *pline) || *pline == '-')
        grid.yvals[0] = atof(pline);
      else
        grid.type = CDI_UNDEFID;
    }
}

int
gridFromName(const char *gridnameptr)
{
  const char *pline;
  int gridID = CDI_UNDEFID;
  GridDesciption grid;
  size_t len;
  char *endptr;

  char *gridname = strdup(gridnameptr);
  cstrToLowerCase(gridname);

  if (gridname[0] == 't')  // t<RES>grid or t<RES>spec
    {
      int off = 0;
      int type = 'q';
      if (gridname[1] == 'l')
        {
          type = 'l';
          off = 1;
        }
      else if (gridname[1] == 'c')
        {
          type = 'c';
          off = 1;
        }

      pline = &gridname[off + 1];
      if (isdigit((int) *pline))
        {
          grid.ntr = atol(pline);
          while (isdigit((int) *pline)) pline++;
          // clang-format off
          if      (cstr2IsEqualLen(pline, "grid", len)) grid.type = GRID_GAUSSIAN;
          else if (cstr2IsEqualLen(pline, "zon", len))  grid.type = GRID_GAUSSIAN;
          else if (cstr2IsEqualLen(pline, "spec", len)) grid.type = GRID_SPECTRAL;
          else if (cstr2IsEqualLen(pline, "", len))     grid.type = GRID_SPECTRAL;
          // clang-format on

          if (pline[len] != 0) return gridID;

          if (grid.type == GRID_GAUSSIAN)
            {
              if (type == 'l')
                grid.ysize = ntr_to_nlat_linear(grid.ntr);
              else if (type == 'c')
                grid.ysize = ntr_to_nlat_cubic(grid.ntr);
              else
                grid.ysize = ntr_to_nlat(grid.ntr);

              grid.numLPE = grid.ysize / 2;
              grid.xsize = (cstr2IsEqual(pline, "zon"))
                               ? 1
                               : ((type == 'c') ? nlat_to_nlon_cubic(grid.ysize) : nlat_to_nlon(grid.ysize));

              grid.def_xfirst = true;
              grid.def_yfirst = true;
              grid.yvals.resize(grid.ysize);
              grid.ybounds.resize(grid.ysize * 2);

              const auto nlat = grid.ysize;
              std::vector<double> lats(nlat), lat_bounds(nlat + 1);
              gaussianLatitudesInDegrees(lats, lat_bounds, nlat);

              for (size_t j = 0; j < nlat; ++j) grid.yvals[j] = lats[j];
              for (size_t j = 0; j < nlat; ++j) grid.ybounds[j * 2 + 1] = lat_bounds[j];
              for (size_t j = 0; j < nlat; ++j) grid.ybounds[j * 2] = lat_bounds[j + 1];
            }
        }
    }
  else if (gridname[0] == 'r')  // r<LON>x<LAT>; regular 2D grid
    {
      pline = &gridname[1];
      gen_grid_reg2d(grid, pline);
    }
  else if (cstr2IsEqualLen(gridname, "lon=", len))  // lon=<LON>_lat=<LAT>; one gridpoint
    {
      pline = &gridname[len];
      gen_grid_point(grid, pline);
    }
  else if (gridname[0] == 'g' && gridname[1] == 'm' && gridname[2] == 'e')  // gme<NI>
    {
      pline = &gridname[3];
      gen_grid_gme(grid, pline);
    }
  else if (gridname[0] == 'n' && gridname[1] == 'i')  // ni<NI>
    {
      pline = &gridname[2];
      gen_grid_gme(grid, pline);
    }
  else if (gridname[0] == 'g' && gridname[1] == 'e' && gridname[2] == 'a')
    {
      // gea<DX>: gaussian reduced equal area; DX in km
      pline = &gridname[3];
      gen_grid_gea(grid, pline);
    }
  else if ((gridname[0] == 'f' || gridname[0] == 'n') && isdigit((int) gridname[1]))
    {
      // FXXX - full (regular) Gaussian grid with XXX latitude lines between the pole and equator
      pline = &gridname[1];
      const auto numLPE = strtol(pline, &endptr, 10);
      pline = endptr;

      if (cstr2IsEqualLen(pline, "zon", len))
        {
          grid.xsize = 1;
          pline += 3;
        }
      else if (*pline == 'b')
        {
          grid.genBounds = true;
          pline++;
        }

      if (*pline == 0)
        {
          grid.type = GRID_GAUSSIAN;
          grid.numLPE = numLPE;
          grid.ysize = numLPE * 2;
          if (!grid.xsize) grid.xsize = nlat_to_nlon(grid.ysize);

          grid.def_xfirst = true;
          grid.def_yfirst = true;
          /* this will change result of remapcon
          grid.yvals.resize(grid.ysize);
          grid.ybounds.resize(grid.ysize * 2);

          size_t nlat = grid.ysize;
          std::vector<double> lats(nlat), lat_bounds(nlat + 1);
          gaussianLatitudesInDegrees(lats, lat_bounds, nlat);

          for (size_t j = 0; j < nlat; ++j) grid.yvals[j] = lats[j];
          for (size_t j = 0; j < nlat; ++j) grid.ybounds[j * 2 + 1] = lat_bounds[j];
          for (size_t j = 0; j < nlat; ++j) grid.ybounds[j * 2] = lat_bounds[j + 1];
          */
        }
    }
  else if (gridname[0] == 'o' && isdigit((int) gridname[1]))  // O<xxx>
    {
      pline = &gridname[1];
      const auto numLPE = strtol(pline, &endptr, 10);
      pline = endptr;

      if (cstr2IsEqualLen(pline, "zon", len))
        {
          grid.xsize = 1;
          pline += 3;
        }
      else if (*pline == 'b')
        {
          grid.genBounds = true;
          pline++;
        }

      if (*pline == 0)
        {
          grid.type = GRID_GAUSSIAN;
          grid.numLPE = numLPE;
          grid.ysize = numLPE * 2;
          if (!grid.xsize) grid.xsize = nlat_to_nlon(grid.ysize) + 16;

          grid.def_xfirst = true;
          grid.def_yfirst = true;
        }
    }
  else if (gridname[0] == 'g' && isdigit(gridname[1]))  // g<LON>x<LAT> or g<SIZE>
    {
      pline = &gridname[1];
      if (isdigit((int) *pline))
        {
          grid.type = GRID_GENERIC;
          grid.xsize = atol(pline);
          while (isdigit((int) *pline)) pline++;
          if (*pline)
            {
              pline++;
              grid.ysize = atol(pline);
              while (isdigit((int) *pline)) pline++;
            }
          else if (grid.xsize == 1)
            {
              grid.size = 1;
              grid.xsize = 0;
            }
        }
    }
  else if (cstr2IsEqualLen(gridname, "germany", len))  // germany_Xdeg
    {
      pline = &gridname[len];
      gen_grid_lonlat(grid, pline, 0.1, 5.6, 15.2, 47.1, 55.1);
    }
  else if (cstr2IsEqualLen(gridname, "europe", len))  // europe_Xdeg
    {
      pline = &gridname[len];
      gen_grid_lonlat(grid, pline, 1, -30, 60, 30, 80);
    }
  else if (cstr2IsEqualLen(gridname, "africa", len))  // africa_Xdeg
    {
      pline = &gridname[len];
      gen_grid_lonlat(grid, pline, 1, -20, 60, -40, 40);
    }
  else if (cstr2IsEqualLen(gridname, "global", len))  // global_Xdeg
    {
      pline = &gridname[len];
      gen_grid_lonlat(grid, pline, 1, -180, 180, -90, 90);
    }
  else if (cstr2IsEqualLen(gridname, "ico", len))  // icoR02BXX
    {
      pline = &gridname[len];
      gen_grid_icosphere(grid, pline);
    }

  if (grid.type != -1) gridID = gridDefine(grid);

  free(gridname);

  return gridID;
}
