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

#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, const Varray<double> &pph)
{
  long k;

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

  const auto MaxLev = k - 1;

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

  const auto MinLev = k;

  *kmax = MaxLev;
  *kmin = MinLev;
}

static void
pl_index(int *kmax, int *kmin, double pmax, double pmin, long nlevs, const 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;
  bool zrev = false;
  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;

  cdo_initialize(process);

  if (cdo_operator_argc() > 0)
    {
      operator_check_argc(2);
      nvars2 = 1;
      pmin = parameter_to_double(cdo_operator_argv(0));
      pmax = parameter_to_double(cdo_operator_argv(1));
    }
  else
    {
      nvars2 = NVARS;
    }

  const auto streamID1 = cdo_open_read(0);
  const auto vlistID1 = cdo_stream_inq_vlist(streamID1);

  VarList varList1;
  varListInit(varList1, vlistID1);

  const auto gridsize = vlist_check_gridsize(vlistID1);

  const auto aclcac_code = 223;

  const auto nvars = vlistNvars(vlistID1);
  for (int varID = 0; varID < nvars; ++varID)
    {
      zaxisID = varList1[varID].zaxisID;
      auto code = varList1[varID].code;

      if (code <= 0)
        {
          strcpy(varname, varList1[varID].name);
          cstr_to_lower_case(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)
        cdo_abort("Cloud cover (parameter 223) not found on pressure or hybrid levels!");
      else
        cdo_abort("Cloud cover (parameter 223) not found!");
    }

  const auto missval = varList1[aclcacID].missval;
  gridID = varList1[aclcacID].gridID;
  zaxisID = varList1[aclcacID].zaxisID;

  nlevel = varList1[aclcacID].nlevels;
  const auto nhlev = nlevel + 1;

  Varray<double> aclcac(gridsize * nlevel);
  Varray<double> cloud[NVARS];
  for (int 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 (int 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
        cdo_abort("Unsupported vertical coordinate table format!");
    }
  else
    cdo_abort("Unsupported Z-Axis type!");

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

  const auto vlistID2 = vlistCreate();

  if (nvars2 == 1)
    {
      const auto 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
    {
      auto 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 = cdo_open_write(1);
  cdo_def_vlist(streamID2, vlistID2);

  int tsID = 0;
  while (true)
    {
      const auto nrecs = cdo_stream_inq_timestep(streamID1, tsID);
      if (nrecs == 0) break;

      cdo_taxis_copy_timestep(taxisID2, taxisID1);

      cdo_def_timestep(streamID2, tsID);

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

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

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

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

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

      for (int varID = 0; varID < nvars2; ++varID)
        {
          const auto nmiss = varray_num_mv(gridsize, cloud[varID], missval);

          cdo_def_record(streamID2, varID, 0);
          cdo_write_record(streamID2, cloud[varID].data(), nmiss);
        }

      tsID++;
    }

  cdo_stream_close(streamID2);
  cdo_stream_close(streamID1);

  vlistDestroy(vlistID2);

  cdo_finish();

  return nullptr;
}
