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

  Author: Uwe Schulzweida

*/

/*
   This module contains the following operators:

      Info       info            Dataset information
      Info       map             Dataset information and simple map
*/

#include <cfloat>

#include <cdi.h>

#include "cdo_options.h"
#include "cdo_vlist.h"
#include "process_int.h"
#include "mpmo_color.h"
#include "varray.h"
#include "datetime.h"
#include "printinfo.h"
#include "cdo_zaxis.h"

void
field_min_max_sum(const Field &field, double &min, double &max, double &sum)
{
  auto mms = MinMaxSum(min, max, sum);
  if (field.memType == MemType::Float)
    mms = varray_min_max_sum(field.size, field.vec_f, mms);
  else
    mms = varray_min_max_sum(field.size, field.vec_d, mms);

  min = mms.min;
  max = mms.max;
  sum = mms.sum;
}

size_t
field_min_max_sum_mv(const Field &field, double &min, double &max, double &sum)
{
  auto mms = MinMaxSum(min, max, sum);
  if (field.memType == MemType::Float)
    mms = varray_min_max_sum_mv(field.size, field.vec_f, static_cast<float>(field.missval), mms);
  else
    mms = varray_min_max_sum_mv(field.size, field.vec_d, field.missval, mms);

  min = mms.min;
  max = mms.max;
  sum = mms.sum;
  return mms.n;
}

static void
printGridIndex(int nlon, int nlat, int i)
{
  const int index = nlat < 10 ? 2 : nlat < 100 ? 3 : nlat < i ? 4 : 5;

  std::stringstream s;
  s << std::string(index, ' ');
  for (int ilon = 0; ilon < nlon; ilon++) s << ((ilon + 1) / i) % 10;

  printf("%s\n", s.str().c_str());
}

template <typename T>
static void
printMap(const int nlon, const int nlat, const Varray<T> &varray, const double missval, const double min, const double max)
{
  // source code from PINGO
  double level[10] = {};
  int bmin = 1, bmax = 1;
  unsigned char c;

  auto step = (max - min) / 10;

  if (IS_NOT_EQUAL(step, 0))
    {
      const auto a = std::pow(10, std::floor(std::log(step) / M_LN10));
      auto b = step / a;

      if (b > 5)
        b = 0.5 * std::ceil(b / 0.5);
      else if (b > 2)
        b = 0.2 * std::ceil(b / 0.2);
      else if (b > 1)
        b = 0.1 * std::ceil(b / 0.1);
      else
        b = 1;

      step = b * a;

      if (min < 0 && max > 0)
        {
          const int min_n = (int) std::floor(10 * (-min) / (max - min) - 0.5);
          const int max_n = (int) std::ceil(10 * (-min) / (max - min) - 0.5);
          level[min_n] = 0;
          for (int i = min_n - 1; i >= 0; i--) level[i] = level[i + 1] - step;
          for (int i = max_n; i < 9; i++) level[i] = level[i - 1] + step;
        }
      else
        {
          level[0] = step * std::ceil(min / step + 0.5);
          for (int i = 1; i < 9; i++) level[i] = level[i - 1] + step;
        }
    }
  else
    for (int i = 0; i < 9; i++) level[i] = min;

  printf("\n");

  if (nlon >= 1000)
    {
      printf("%.*s", nlat < 10 ? 2 : nlat < 100 ? 3 : nlat < 1000 ? 4 : 5, "     ");
      for (int ilon = 0; ilon < nlon; ilon++) printf("%d", ((ilon + 1) / 1000) % 10);
      printf("\n");
    }

  if (nlon >= 100)
    {
      printf("%.*s", nlat < 10 ? 2 : nlat < 100 ? 3 : nlat < 1000 ? 4 : 5, "     ");
      for (int ilon = 0; ilon < nlon; ilon++) printf("%d", ((ilon + 1) / 100) % 10);
      printf("\n");
    }

  if (nlon >= 10)
    {
      printf("%.*s", nlat < 10 ? 2 : nlat < 100 ? 3 : nlat < 1000 ? 4 : 5, "     ");
      for (int ilon = 0; ilon < nlon; ilon++) printf("%d", ((ilon + 1) / 10) % 10);
      printf("\n");
    }

  printf("%.*s", nlat < 10 ? 2 : nlat < 100 ? 3 : nlat < 1000 ? 4 : 5, "     ");

  for (int ilon = 0; ilon < nlon; ilon++) printf("%d", (ilon + 1) % 10);
  printf("\n\n");

  for (int ilat = 0; ilat < nlat; ilat++)
    {
      printf("%0*d ", nlat < 10 ? 1 : nlat < 100 ? 2 : nlat < 1000 ? 3 : 4, ilat + 1);
      for (int ilon = 0; ilon < nlon; ilon++)
        {
          auto x = varray[ilat * nlon + ilon];
          if (DBL_IS_EQUAL(x, missval))
            c = '.';
          else if (DBL_IS_EQUAL(x, min) && !DBL_IS_EQUAL(min, max))
            c = 'm';
          else if (DBL_IS_EQUAL(x, max) && !DBL_IS_EQUAL(min, max))
            c = 'M';
          else if (DBL_IS_EQUAL(x, 0.))
            c = '*';
          else if (x < 0)
            {
              c = '9';
              for (int i = 0; i < 9; i++)
                if (level[i] > x)
                  {
                    c = i + '0';
                    break;
                  }
            }
          else
            {
              c = '0';
              for (int i = 8; i >= 0; i--)
                if (level[i] < x)
                  {
                    c = i + 1 + '0';
                    break;
                  }
            }

          TextMode mode(MODELESS);
          TextColor color(BLACK);
          switch (c)
            {
            /* clang-format off */
            case '0': mode = BRIGHT   ; color = BLUE    ; break ;
            case '1': mode = MODELESS ; color = BLUE    ; break ;
            case '2': mode = BRIGHT   ; color = CYAN    ; break ;
            case '3': mode = MODELESS ; color = CYAN    ; break ;
            case '4': mode = MODELESS ; color = GREEN   ; break ;
            case '5': mode = MODELESS ; color = YELLOW  ; break ;
            case '6': mode = MODELESS ; color = RED     ; break ;
            case '7': mode = BRIGHT   ; color = RED     ; break ;
            case '8': mode = MODELESS ; color = MAGENTA ; break ;
            case '9': mode = BRIGHT   ; color = MAGENTA ; break ;
            /* clang-format on */
            case 'm':
              (bmax == 1) ? mode = BLINK : mode = MODELESS, color = BLACK;
              if (bmax) bmax = 0;
              break;
            case 'M':
              (bmin == 1) ? mode = BLINK : mode = MODELESS, color = BLACK;
              if (bmin) bmin = 0;
              break;
            }

          set_text_color(stdout, mode, color);
          putchar(c);
          reset_text_color(stdout);
        }
      printf(" %0*d\n", nlat < 10 ? 1 : nlat < 100 ? 2 : nlat < 1000 ? 3 : 4, ilat + 1);
    }
  printf("\n");

  for (int i = 1; i <= 4; i++)
    {
      const int current = 10000 / std::pow(10, i);
      if (nlon >= current) printGridIndex(nlon, nlat, current);
    }
  printf("\n");

  for (int i = 0; i < 10; i++)
    {
      printf("%d=%c%+9.3e,%+9.3e%c%s", i, '[', i == 0 ? min : level[i - 1], i == 9 ? max : level[i],
             i == 9 || level[i] <= 0 ? ']' : ']', i != 2 && i != 5 && i != 8 ? "  " : "");

      if (i == 2 || i == 5 || i == 8) printf("\n");
    }

  printf("*=0  .=miss  m=min=%+9.3e  M=max=%+9.3e\n", min, max);
  printf("\n");
}

template <typename T>
static size_t
complexSum(size_t gridsize, const Varray<T> &varray, double missval, double &sum, double &sumi)
{
  size_t n = 0;
  for (size_t i = 0; i < gridsize; i++)
    {
      if (!DBL_IS_EQUAL(varray[i * 2], missval) && !DBL_IS_EQUAL(varray[i * 2 + 1], missval))
        {
          sum += varray[i * 2];
          sumi += varray[i * 2 + 1];
          n++;
        }
    }

  return n;
}

static size_t
fieldComplexSum(const Field &field, double &sum, double &sumi)
{
  if (field.memType == MemType::Float)
    return complexSum(field.gridsize, field.vec_f, static_cast<float>(field.missval), sum, sumi);
  else
    return complexSum(field.gridsize, field.vec_d, field.missval, sum, sumi);
}

struct Infostat
{
  double min, max, sum, sumi;
  size_t nvals, nmiss;
  int nlevels;
};

static void
infostatInit(Infostat &infostat)
{
  infostat.nvals = 0;
  infostat.nmiss = 0;
  infostat.nlevels = 0;
  infostat.min = DBL_MAX;
  infostat.max = -DBL_MAX;
  infostat.sum = 0.0;
  infostat.sumi = 0.0;
}

static void
print_header(int fileIndex, bool lvinfo, const std::string &e, const std::string &v)
{
  set_text_color(stdout, BRIGHT);
  if (fileIndex)
    fprintf(stdout, "%6d :       Date     Time   %s Gridsize    Miss :     Minimum        Mean     Maximum : %s%s\n",
            fileIndex, lvinfo ? "Nlevs" : "Level", e.c_str(), v.c_str());
  else
    fprintf(stdout, "       :       Date     Time   %s Gridsize    Miss :     Minimum        Mean     Maximum : %s%s\n",
            lvinfo ? "Nlevs" : "Level", e.c_str(), v.c_str());
  reset_text_color(stdout);
}

void *
Info(void *process)
{
  enum
  {
    E_NAME,
    E_CODE,
    E_PARAM
  };
  int fpeRaised = 0;
  size_t imiss = 0;
  char paramstr[32];

  cdo_initialize(process);

  // clang-format off
  const auto INFO   = cdo_operator_add("info",   E_PARAM,  0, nullptr);
  const auto INFOP  = cdo_operator_add("infop",  E_PARAM,  0, nullptr);
  const auto INFON  = cdo_operator_add("infon",  E_NAME,   0, nullptr);
  const auto INFOC  = cdo_operator_add("infoc",  E_CODE,   0, nullptr);
  const auto VINFON = cdo_operator_add("vinfon", E_NAME,   0, nullptr);
  const auto XINFON = cdo_operator_add("xinfon", E_NAME,   0, nullptr);
  const auto MAP    = cdo_operator_add("map",    E_PARAM,  0, nullptr);
  // clang-format on

  (void) (INFO);   // CDO_UNUSED
  (void) (INFOP);  // CDO_UNUSED
  (void) (INFON);  // CDO_UNUSED
  (void) (INFOC);  // CDO_UNUSED

  const auto operatorID = cdo_operator_id();
  const auto operfunc = cdo_operator_f1(operatorID);

  operator_check_argc(0);

  const auto lvinfo = (operatorID == VINFON || operatorID == XINFON);

  DateTimeList dtlist;

  std::string e = (operfunc == E_NAME) ? "Parameter name" : ((operfunc == E_CODE) ? "Code number" : "Parameter ID");

  std::string v = (Options::cdoVerbose) ? " : Extra" : "";
  int indg = 0;

  for (int indf = 0; indf < cdo_stream_cnt(); indf++)
    {
      const auto streamID = cdo_open_read(indf);
      const auto vlistID = cdo_stream_inq_vlist(streamID);
      const auto taxisID = vlistInqTaxis(vlistID);

      const auto nvars = vlistNvars(vlistID);
      if (nvars == 0) continue;

      std::vector<Infostat> infostat(nvars);

      VarList varList;
      varListInit(varList, vlistID);

      Field field;

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

          dtlist.taxis_inq_timestep(taxisID, 0);
          const auto vdateString = date_to_string(dtlist.get_vdate(0));
          const auto vtimeString = time_to_string(dtlist.get_vtime(0));

          for (int varID = 0; varID < nvars; ++varID) infostatInit(infostat[varID]);

          for (int recID = 0; recID < nrecs; ++recID)
            {
              if ((tsID == 0 && recID == 0) || operatorID == MAP) print_header(-(indf + 1), lvinfo, e, v);

              int varID, levelID;
              cdo_inq_record(streamID, &varID, &levelID);
              field.init(varList[varID]);
              cdo_read_record(streamID, field);
              const auto nmiss = field.nmiss;

              indg = lvinfo ? varID + 1 : indg + 1;

              const auto gridsize = varList[varID].gridsize;

              auto loutput = !lvinfo;

              if (loutput) infostatInit(infostat[varID]);

              auto &infostatr = infostat[varID];
              infostatr.nlevels += 1;
              infostatr.nmiss += nmiss;

              if (varList[varID].nlevels == infostatr.nlevels) loutput = true;

              if (loutput)
                {
                  cdiParamToString(varList[varID].param, paramstr, sizeof(paramstr));

                  fprintf(stdout, "%6d ", indg);
                  fprintf(stdout, ":");

                  set_text_color(stdout, MAGENTA);
                  fprintf(stdout, "%s %s ", vdateString.c_str(), vtimeString.c_str());
                  reset_text_color(stdout);

                  set_text_color(stdout, GREEN);
                  if (lvinfo)
                    fprintf(stdout, "%7d ", varList[varID].nlevels);
                  else
                    fprintf(stdout, "%7g ", cdo_zaxis_inq_level(varList[varID].zaxisID, levelID));

                  fprintf(stdout, "%8zu %7zu ", gridsize, infostatr.nmiss);
                  reset_text_color(stdout);

                  fprintf(stdout, ":");

                  set_text_color(stdout, BLUE);
                }

              if (varList[varID].nwpv == CDI_REAL)
                {
                  fpeRaised = 0;

                  if (infostatr.nmiss)
                    {
                      const auto nvals = field_min_max_sum_mv(field, infostatr.min, infostatr.max, infostatr.sum);
                      imiss = gridsize - nvals;
                      infostatr.nvals += nvals;
                    }
                  else if (gridsize == 1)
                    {
                      const double val = (field.memType == MemType::Float) ? field.vec_f[0] : field.vec_d[0];
                      infostatr.sum = (infostatr.nvals == 0) ? val : infostatr.sum + val;
                      infostatr.nvals += 1;
                    }
                  else
                    {
                      field_min_max_sum(field, infostatr.min, infostatr.max, infostatr.sum);
                      infostatr.nvals += gridsize;
                    }

                  if (loutput)
                    {
                      if (infostatr.nvals == 0)
                        fprintf(stdout, "                     nan            ");
                      else if (infostatr.nvals == 1)
                        fprintf(stdout, "            %#12.5g            ", infostatr.sum);
                      else
                        fprintf(stdout, "%#12.5g%#12.5g%#12.5g", infostatr.min, infostatr.sum / infostatr.nvals, infostatr.max);
                    }
                }
              else
                {
                  const auto nvals = fieldComplexSum(field, infostatr.sum, infostatr.sumi);

                  fpeRaised = 0;

                  imiss = gridsize - nvals;
                  infostatr.nvals += nvals;

                  if (loutput)
                    {
                      const auto arrmean_r = (infostatr.nvals > 0) ? infostatr.sum / infostatr.nvals : 0.0;
                      const auto arrmean_i = (infostatr.nvals > 0) ? infostatr.sumi / infostatr.nvals : 0.0;
                      fprintf(stdout, "   -  (%#12.5g,%#12.5g)  -", arrmean_r, arrmean_i);
                    }
                }

              if (loutput)
                {
                  reset_text_color(stdout);

                  fprintf(stdout, " : ");

                  // set_text_color(stdout, GREEN);
                  if (operfunc == E_NAME)
                    fprintf(stdout, "%-14s", varList[varID].name);
                  else if (operfunc == E_CODE)
                    fprintf(stdout, "%4d   ", varList[varID].code);
                  else
                    fprintf(stdout, "%-14s", paramstr);
                  // reset_text_color(stdout);

                  if (Options::cdoVerbose)
                    {
                      char varextra[CDI_MAX_NAME];
                      vlistInqVarExtra(vlistID, varID, varextra);
                      fprintf(stdout, " : %s", varextra);
                    }

                  fprintf(stdout, "\n");
                }

              if (imiss != nmiss && nmiss) cdo_warning("Found %zu of %zu missing values!", imiss, nmiss);

              if (fpeRaised > 0) cdo_warning("floating-point exception reported: %s!", fpe_errstr(fpeRaised));

              if (operatorID == MAP)
                {
                  const auto gridID = varList[varID].gridID;
                  const auto gridtype = gridInqType(gridID);
                  const auto nlon = gridInqXsize(gridID);
                  const auto nlat = gridInqYsize(gridID);

                  if (gridtype == GRID_GAUSSIAN || gridtype == GRID_LONLAT || gridtype == GRID_CURVILINEAR
                      || (gridtype == GRID_GENERIC && nlon * nlat == gridInqSize(gridID) && nlon < 1024))
                    {
                      if (field.memType == MemType::Float)
                        printMap(nlon, nlat, field.vec_f, static_cast<float>(field.missval), infostatr.min, infostatr.max);
                      else
                        printMap(nlon, nlat, field.vec_d, field.missval, infostatr.min, infostatr.max);
                    }
                }
            }

          tsID++;
        }

      cdo_stream_close(streamID);
    }

  if (indg > 36 && operatorID != MAP) print_header(0, lvinfo, e, v);

  cdo_finish();

  return nullptr;
}
