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

#include "process_int.h"
#include "cdo_vlist.h"
#include "param_conversion.h"
#include "util_string.h"


/* ================================================= */
/* LayerCloud calculates random overlap cloud cover */
/* ================================================= */

static void
layer_cloud(const Varray<double> &cc, Varray<double> &ll, long MaxLev, long MinLev, long dimgp)
{
  constexpr double ZEPSEC = 1. - 1.0e-12;

  for (long i = 0; i < dimgp; i++) ll[i] = 1. - cc[i + MaxLev * dimgp];

  //  printf("maxlev %d minlev %d\n", MaxLev, MinLev);

  for (long k = MaxLev + 1; k <= MinLev; k++)
    {
      for (long i = 0; i < dimgp; i++)
        {
          const double maxval = std::max(cc[i + (k - 1) * dimgp], cc[i + k * dimgp]);
          const double minval = std::min(cc[i + (k - 1) * dimgp], ZEPSEC);
          ll[i] *= (1. - maxval) / (1. - minval);
        }
    }

  for (long i = 0; i < dimgp; i++) ll[i] = 1. - ll[i];
}

static void
vct2plev(const Varray<double> &vct, Varray<double> &plevs, long nlevs)
{
  constexpr double SCALESLP = 101325.0;
  for (long k = 0; k < nlevs; k++) plevs[k] = vct[k] + vct[k + nlevs] * SCALESLP;
}

static void
hl_index(int *kmax, int *kmin, double pmax, double pmin, long nhlevs, Varray<double> &pph)
{
  long k;

  for (k = 0; k < nhlevs; k++)
    if (pph[k] > pmax) break;

  const long MaxLev = k - 1;

  for (k = nhlevs - 1; k >= 0; k--)
    if (pph[k] < pmin) break;

  const long MinLev = k;

  *kmax = MaxLev;
  *kmin = MinLev;
}

static void
pl_index(int *kmax, int *kmin, double pmax, double pmin, long nlevs, Varray<double> &plevs)
{
  long k;
  long MaxLev = -1, MinLev = -1;

  for (k = 0; k < nlevs; k++)
    if (plevs[k] >= pmax)
      {
        MaxLev = k;
        break;
      }

  for (k = nlevs - 1; k >= 0; k--)
    if (plevs[k] < pmin)
      {
        MinLev = k;
        break;
      }

  *kmax = MaxLev;
  *kmin = MinLev;
}

#define NVARS 3

void *
Cloudlayer(void *process)
{
  int gridID, zaxisID;
  int nlevel, nlevs, nrecs, code;
  int varID, levelID;
  bool zrev = false;
  size_t nmiss;
  int aclcacID = -1;
  int nvars2 = 0;
  int aclcac_code_found = 0;
  int kmin[NVARS] = { -1, -1, -1 }, kmax[NVARS] = { -1, -1, -1 };
  char varname[CDI_MAX_NAME];
  double sfclevel = 0;
  double pmin = 0, pmax = 0;

  cdoInitialize(process);

  if (operatorArgc() > 0)
    {
      operatorCheckArgc(2);
      nvars2 = 1;
      pmin = parameter2double(cdoOperatorArgv(0));
      pmax = parameter2double(cdoOperatorArgv(1));
    }
  else
    {
      nvars2 = NVARS;
    }

  const auto streamID1 = cdoOpenRead(0);
  const auto vlistID1 = cdoStreamInqVlist(streamID1);

  const auto gridsize = vlist_check_gridsize(vlistID1);

  const auto aclcac_code = 223;

  const auto nvars = vlistNvars(vlistID1);
  for (varID = 0; varID < nvars; ++varID)
    {
      zaxisID = vlistInqVarZaxis(vlistID1, varID);
      code = vlistInqVarCode(vlistID1, varID);

      if (code <= 0)
        {
          vlistInqVarName(vlistID1, varID, varname);
          cstrToLowerCase(varname);
          if (strcmp(varname, "aclcac") == 0) code = 223;
        }

      if (code == aclcac_code)
        {
          aclcac_code_found = 1;
          if (zaxisInqType(zaxisID) == ZAXIS_PRESSURE || zaxisInqType(zaxisID) == ZAXIS_HYBRID)
            {
              aclcacID = varID;
              break;
            }
        }
    }

  if (aclcacID == -1)
    {
      if (aclcac_code_found)
        cdoAbort("Cloud cover (parameter 223) not found on pressure or hybrid levels!");
      else
        cdoAbort("Cloud cover (parameter 223) not found!");
    }

  const auto missval = vlistInqVarMissval(vlistID1, aclcacID);
  gridID = vlistInqVarGrid(vlistID1, aclcacID);
  zaxisID = vlistInqVarZaxis(vlistID1, aclcacID);

  nlevel = zaxisInqSize(zaxisID);
  const auto nhlev = nlevel + 1;

  Varray<double> aclcac(gridsize * nlevel);
  Varray<double> cloud[NVARS];
  for (varID = 0; varID < nvars2; ++varID) cloud[varID].resize(gridsize);

  if (zaxisInqType(zaxisID) == ZAXIS_PRESSURE)
    {
      Varray<double> plevs(nlevel);
      zaxisInqLevels(zaxisID, plevs.data());
      if (plevs[0] > plevs[nlevel - 1])
        {
          zrev = true;
          for (levelID = 0; levelID < nlevel / 2; ++levelID)
            {
              const double ptmp = plevs[levelID];
              plevs[levelID] = plevs[nlevel - 1 - levelID];
              plevs[nlevel - 1 - levelID] = ptmp;
            }
        }
      /*
      for ( levelID = 0; levelID < nlevel; ++levelID )
        {
          printf("level %d %g\n", levelID, plevs[levelID]);
        }
      */
      if (nvars2 == 1)
        {
          pl_index(&kmax[0], &kmin[0], pmin, pmax, nlevel, plevs);
        }
      else
        {
          pl_index(&kmax[2], &kmin[2], 5000., 44000., nlevel, plevs);
          pl_index(&kmax[1], &kmin[1], 46000., 73000., nlevel, plevs);
          pl_index(&kmax[0], &kmin[0], 75000., 101300., nlevel, plevs);
        }
    }
  else if (zaxisInqType(zaxisID) == ZAXIS_HYBRID)
    {
      const int nvct = zaxisInqVctSize(zaxisID);
      if (nlevel == (nvct / 2 - 1))
        {
          Varray<double> vct(nvct);
          zaxisInqVct(zaxisID, vct.data());

          nlevs = nlevel + 1;
          Varray<double> plevs(nlevs);
          vct2plev(vct, plevs, nlevs);

          if (nvars2 == 1)
            {
              hl_index(&kmax[0], &kmin[0], pmin, pmax, nhlev, plevs);
            }
          else
            {
              hl_index(&kmax[2], &kmin[2], 5000., 44000., nhlev, plevs);
              hl_index(&kmax[1], &kmin[1], 46000., 73000., nhlev, plevs);
              hl_index(&kmax[0], &kmin[0], 75000., 101300., nhlev, plevs);
            }
        }
      else
        cdoAbort("Unsupported vertical coordinate table format!");
    }
  else
    cdoAbort("Unsupported Z-Axis type!");

  const auto surfaceID = zaxisCreate(ZAXIS_SURFACE, 1);
  zaxisDefLevels(surfaceID, &sfclevel);

  const auto vlistID2 = vlistCreate();

  if (nvars2 == 1)
    {
      varID = vlistDefVar(vlistID2, gridID, surfaceID, TIME_VARYING);
      vlistDefVarParam(vlistID2, varID, cdiEncodeParam(33, 128, 255));
      cdiDefKeyString(vlistID2, varID, CDI_KEY_NAME, "cld_lay");
      cdiDefKeyString(vlistID2, varID, CDI_KEY_LONGNAME, "cloud layer");
      vlistDefVarMissval(vlistID2, varID, missval);
    }
  else
    {
      varID = vlistDefVar(vlistID2, gridID, surfaceID, TIME_VARYING);
      vlistDefVarParam(vlistID2, varID, cdiEncodeParam(34, 128, 255));
      cdiDefKeyString(vlistID2, varID, CDI_KEY_NAME, "low_cld");
      cdiDefKeyString(vlistID2, varID, CDI_KEY_LONGNAME, "low cloud");
      vlistDefVarMissval(vlistID2, varID, missval);

      varID = vlistDefVar(vlistID2, gridID, surfaceID, TIME_VARYING);
      vlistDefVarParam(vlistID2, varID, cdiEncodeParam(35, 128, 255));
      cdiDefKeyString(vlistID2, varID, CDI_KEY_NAME, "mid_cld");
      cdiDefKeyString(vlistID2, varID, CDI_KEY_LONGNAME, "mid cloud");
      vlistDefVarMissval(vlistID2, varID, missval);

      varID = vlistDefVar(vlistID2, gridID, surfaceID, TIME_VARYING);
      vlistDefVarParam(vlistID2, varID, cdiEncodeParam(36, 128, 255));
      cdiDefKeyString(vlistID2, varID, CDI_KEY_NAME, "hih_cld");
      cdiDefKeyString(vlistID2, varID, CDI_KEY_LONGNAME, "high cloud");
      vlistDefVarMissval(vlistID2, varID, missval);
    }

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

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

  int tsID = 0;
  while ((nrecs = cdoStreamInqTimestep(streamID1, tsID)))
    {
      taxisCopyTimestep(taxisID2, taxisID1);

      cdoDefTimestep(streamID2, tsID);

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

          const size_t offset = zrev ? (nlevel - 1 - levelID) * gridsize : levelID * gridsize;

          if (varID == aclcacID)
            {
              cdoReadRecord(streamID1, aclcac.data() + offset, &nmiss);
              if (nmiss != 0) cdoAbort("Missing values unsupported!");
            }
        }

      for (varID = 0; varID < nvars2; ++varID)
        {
          for (size_t i = 0; i < gridsize; i++) cloud[varID][i] = missval;
        }

      for (varID = 0; varID < nvars2; ++varID)
        {
          if (kmax[varID] != -1 && kmin[varID] != -1) layer_cloud(aclcac, cloud[varID], kmax[varID], kmin[varID], gridsize);
        }

      for (varID = 0; varID < nvars2; ++varID)
        {
          nmiss = varrayNumMV(gridsize, cloud[varID], missval);

          cdoDefRecord(streamID2, varID, 0);
          cdoWriteRecord(streamID2, cloud[varID].data(), nmiss);
        }

      tsID++;
    }

  cdoStreamClose(streamID2);
  cdoStreamClose(streamID1);

  vlistDestroy(vlistID2);

  cdoFinish();

  return nullptr;
}
