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

/*
   This module contains the following operators:

      Output     output          ASCII output
      Output     outputf         Formatted output
      Output     outputint       Integer output
      Output     outputsrv       SERVICE output
      Output     outputext       EXTRA output
      Output     outputtab       Table output
*/

#include <cdi.h>

#include "cdo_options.h"
#include "process_int.h"
#include "param_conversion.h"
#include <mpim_grid.h>
#include "printinfo.h"
#include "cdo_zaxis.h"

static void
outputarr(int dig, size_t gridsize, const Varray<double> &array)
{
  for (size_t i = 0; i < gridsize; i++)
    {
      fprintf(stdout, "  arr[%zu] = %.*g;\n", i, dig, array[i]);
    }
}

static void
outputsp(size_t gridsize, const Varray<double> &array, long ntr)
{
  const auto mm = varray_min_max(gridsize, array);
  if (/* T11 */ mm.min >= -1 && mm.max <= 12)
    {
      auto spc = array.data();
      for (long m = 0; m <= ntr; m++)
        {
          for (long n = m; n <= ntr; n++)
            {
              fprintf(stdout, "%3d", (int) *spc++);
              fprintf(stdout, "%3d", (int) *spc++);
            }
          fprintf(stdout, "\n");
        }
    }
}

static void
output(size_t gridsize, const Varray<double> &array)
{
  int nout = 0;
  for (size_t i = 0; i < gridsize; i++)
    {
      if (nout == 6)
        {
          nout = 0;
          fprintf(stdout, "\n");
        }
      fprintf(stdout, " %12.6g", array[i]);
      nout++;
    }
  fprintf(stdout, "\n");
}

static void
outputxyz(size_t gridsize, Varray<double> &array, double missval, size_t nlon, size_t nlat, const Varray<double> &lon,
          const Varray<double> &lat)
{
  double fmin = 0.0;
  double x, y, z;
  for (size_t i = 0; i < gridsize; i++)
    if (!DBL_IS_EQUAL(array[i], missval))
      {
        if (array[i] < fmin) fmin = array[i];
        fprintf(stdout, "%g\t%g\t%g\t%g\n", lon[i], lat[i], array[i], array[i]);
      }
  const char *fname = "frontplane.xyz";
  auto fp = fopen(fname, "w");
  if (fp == nullptr) cdo_abort("Open failed on %s", fname);
  // first front plane
  double dx = (lon[1] - lon[0]);
  double x0 = lon[0] - dx / 2;
  double y0 = lat[0] - dx / 2;
  double z0 = fmin;
  fprintf(fp, ">\n");
  for (size_t i = 0; i < nlon; ++i)
    {
      x = x0;
      y = y0;
      z = z0;
      fprintf(fp, "%g %g %g\n", x, y, z);
      x = x0;
      y = y0;
      z = array[i];
      fprintf(fp, "%g %g %g\n", x, y, z);
      x = x0 + dx;
      y = y0;
      fprintf(fp, "%g %g %g\n", x, y, z);
      x0 = x; /*y0 = y0;*/
      z0 = z;
    }
  x = x0;
  y = y0;
  z = fmin;
  fprintf(fp, "%g %g %g\n", x, y, z);
  x = lon[0] - dx / 2;
  fprintf(fp, "%g %g %g\n", x, y, z);

  // second front plane
  x0 = lon[0] - dx / 2;
  y0 = lat[0] - dx / 2;
  z0 = fmin;
  fprintf(fp, ">\n");
  for (size_t i = 0; i < nlat; ++i)
    {
      x = x0;
      y = y0;
      z = z0;
      fprintf(fp, "%g %g %g\n", x, y, z);
      x = x0;
      y = y0;
      z = array[i * nlon];
      fprintf(fp, "%g %g %g\n", x, y, z);
      x = x0;
      y = y0 + dx;
      fprintf(fp, "%g %g %g\n", x, y, z);
      /*x0 = x0;*/ y0 = y;
      z0 = z;
    }
  x = x0;
  y = y0;
  z = fmin;
  fprintf(fp, "%g %g %g\n", x, y, z);
  y = lat[0] - dx / 2;
  fprintf(fp, "%g %g %g\n", x, y, z);

  fclose(fp);
}

static void
read_xy_coordinates(bool hasRegxyCoordinates, int gridID0, Varray<double> &grid_xvals, Varray<double> &grid_yvals)
{
  const auto gridsize = gridInqSize(gridID0);
  const auto xsize = gridInqXsize(gridID0);
  const auto ysize = gridInqYsize(gridID0);

  if (hasRegxyCoordinates)
    {
      grid_xvals.resize(xsize);
      grid_yvals.resize(ysize);
      gridInqXvals(gridID0, grid_xvals.data());
      gridInqYvals(gridID0, grid_yvals.data());
    }
  else
    {
      const auto gridIDx = generate_full_point_grid(gridID0);

      if (!gridHasCoordinates(gridIDx)) cdo_abort("Cell center coordinates missing!");

      grid_xvals.resize(gridsize);
      grid_yvals.resize(gridsize);
      gridInqXvals(gridIDx, grid_xvals.data());
      gridInqYvals(gridIDx, grid_yvals.data());

      if (gridIDx != gridID0) gridDestroy(gridIDx);
    }
}

static void
read_lonlat_coordinates(int gridID0, Varray<double> &grid_center_lon, Varray<double> &grid_center_lat)
{
  const auto gridsize = gridInqSize(gridID0);

  const auto gridIDx = generate_full_point_grid(gridID0);

  if (!gridHasCoordinates(gridIDx)) cdo_abort("Cell center coordinates missing!");

  grid_center_lon.resize(gridsize);
  grid_center_lat.resize(gridsize);
  gridInqXvals(gridIDx, grid_center_lon.data());
  gridInqYvals(gridIDx, grid_center_lat.data());

  // Convert lat/lon units if required
  cdo_grid_to_degree(gridIDx, CDI_XAXIS, gridsize, grid_center_lon.data(), "grid center lon");
  cdo_grid_to_degree(gridIDx, CDI_YAXIS, gridsize, grid_center_lat.data(), "grid center lat");

  if (gridIDx != gridID0) gridDestroy(gridIDx);
}

static void
outputint(size_t gridsize, const Varray<double> &array)
{
  int nout = 0;
  for (size_t i = 0; i < gridsize; i++)
    {
      if (nout == 8)
        {
          nout = 0;
          fprintf(stdout, "\n");
        }
      fprintf(stdout, " %8d", (int) array[i]);
      nout++;
    }
  fprintf(stdout, "\n");
}

static void
outputf(int nelem, const char *format, size_t gridsize, const Varray<double> &array)
{
  int nout = 0;
  for (size_t i = 0; i < gridsize; i++)
    {
      if (nout == nelem)
        {
          nout = 0;
          fprintf(stdout, "\n");
        }
      fprintf(stdout, format, array[i]);
      nout++;
    }
  fprintf(stdout, "\n");
}

void *
Output(void *process)
{
  size_t nmiss;
  int nelem = 1;
  int index;
  const char *format = nullptr;
  char paramstr[32];
  int year, month, day;
  std::vector<int> keys;
  int nkeys = 0, k;
  int nKeys = 0;

  // clang-format off
  int Keylen[]           = {      0,        8,      11,      4,      8,   6,   6,     6,     6,     6,     6,      4,      4,          6,     10,      8,      5,       2,     2 };
  enum                     {knohead,   kvalue,  kparam,  kcode,  kname,  kx,  ky,  klon,  klat,  klev,  kbin,  kxind,  kyind,  ktimestep,  kdate,  ktime,  kyear,  kmonth,  kday };
  const char *Keynames[] = {"nohead",  "value", "param", "code", "name", "x", "y", "lon", "lat", "lev", "bin", "xind", "yind", "timestep", "date", "time", "year", "month", "day"};


  cdo_initialize(process);

  const auto OUTPUT    = cdo_operator_add("output",    0, 1, nullptr);
  const auto OUTPUTINT = cdo_operator_add("outputint", 0, 0, nullptr);
  const auto OUTPUTSRV = cdo_operator_add("outputsrv", 0, 0, nullptr);
  const auto OUTPUTEXT = cdo_operator_add("outputext", 0, 0, nullptr);
  const auto OUTPUTF   = cdo_operator_add("outputf",   0, 0, nullptr);
  const auto OUTPUTTS  = cdo_operator_add("outputts",  0, 0, nullptr);
  const auto OUTPUTFLD = cdo_operator_add("outputfld", 0, 0, nullptr);
  const auto OUTPUTARR = cdo_operator_add("outputarr", 0, 0, nullptr);
  const auto OUTPUTXYZ = cdo_operator_add("outputxyz", 0, 0, nullptr);
  const auto OUTPUTTAB = cdo_operator_add("outputtab", 0, 0, nullptr);
  // clang-format on

  (void) (OUTPUT);  // CDO_UNUSED

  const auto operatorID = cdo_operator_id();
  const auto opercplx = (cdo_operator_f2(operatorID) == 1);

  if (operatorID == OUTPUTF)
    {
      operator_input_arg("format and number of elements [optional]");

      if (cdo_operator_argc() < 1) cdo_abort("Too few arguments!");

      format = cdo_operator_argv(0).c_str();
      if (cdo_operator_argc() == 2) nelem = parameter_to_int(cdo_operator_argv(1));
    }
  else if (operatorID == OUTPUTTAB)
    {
      auto lhead = true;

      operator_input_arg("keys to print");

      const auto npar = cdo_operator_argc();
      auto parnames = cdo_get_oper_argv();

      if (Options::cdoVerbose)
        for (int i = 0; i < npar; i++) cdo_print("key %d = %s", i + 1, parnames[i]);

      keys.resize(npar);
      nkeys = 0;
      nKeys = sizeof(Keynames) / sizeof(char *);
      for (int i = 0; i < npar; i++)
        {
          const char *currentName = parnames[i].c_str();
          for (k = 0; k < nKeys; ++k)
            {
              // int len = strlen(parnames[i]);
              int len = strlen(Keynames[k]);
              if (len < 3) len = 3;
              if (strncmp(currentName, Keynames[k], len) == 0)
                {
                  int len2 = strlen(currentName);
                  if (len2 > len && currentName[len] != ':')
                    cdo_abort("Key parameter >%s< contains invalid character at position %d!", currentName, len + 1);

                  if (k == knohead)
                    lhead = false;
                  else
                    {
                      keys[nkeys++] = k;
                      if (len2 > len && currentName[len] == ':' && isdigit(currentName[len + 1]))
                        Keylen[k] = atoi(&currentName[len + 1]);
                    }
                  break;
                }
            }

          if (k == nKeys) cdo_abort("Key %s unsupported!", currentName);
        }

      if (Options::cdoVerbose)
        for (k = 0; k < nkeys; ++k)
          cdo_print("keynr = %d  keyid = %d  keylen = %d  keyname = %s", k, keys[k], Keylen[keys[k]], Keynames[keys[k]]);

      if (lhead)
        {
          fprintf(stdout, "#");
          for (k = 0; k < nkeys; ++k)
            {
              int len = Keylen[keys[k]];
              //   if ( k == 0 ) len -= 1;
              fprintf(stdout, "%*s ", len, Keynames[keys[k]]);
            }
          fprintf(stdout, "\n");
        }
    }
  else
    {
      operator_check_argc(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);

      VarList varList;
      varListInit(varList, vlistID);

      const auto ngrids = vlistNgrids(vlistID);
      int ndiffgrids = 0;
      for (index = 1; index < ngrids; index++)
        if (vlistGrid(vlistID, 0) != vlistGrid(vlistID, index)) ndiffgrids++;

      if (ndiffgrids > 0) cdo_abort("Too many different grids!");

      auto gridID0 = vlistGrid(vlistID, 0);
      const auto gridtype = gridInqType(gridID0);
      const auto hasRegxyCoordinates = (gridtype == GRID_LONLAT || gridtype == GRID_GAUSSIAN || gridtype == GRID_PROJECTION);
      auto gridsize = gridInqSize(gridID0);
      const auto xsize = gridInqXsize(gridID0);
      size_t nwpv = (vlistNumber(vlistID) == CDI_COMP) ? 2 : 1;
      if (nwpv == 2 && !opercplx) cdo_abort("Fields with complex numbers are not supported by this operator!");
      auto gridsizemax = nwpv * gridInqSize(gridID0);
      Varray<double> array(gridsizemax);
      Varray<double> grid_center_lon, grid_center_lat;
      Varray<double> grid_xvals, grid_yvals;

      if (operatorID == OUTPUTTAB) read_xy_coordinates(hasRegxyCoordinates, gridID0, grid_xvals, grid_yvals);

      if (operatorID == OUTPUTFLD || operatorID == OUTPUTXYZ || operatorID == OUTPUTTAB)
        read_lonlat_coordinates(gridID0, grid_center_lon, grid_center_lat);

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

          const auto vdate = taxisInqVdate(taxisID);
          const auto vtime = taxisInqVtime(taxisID);
          const auto vdateString = date_to_string(vdate);
          const auto vtimeString = time_to_string(vtime);

          cdiDecodeDate(vdate, &year, &month, &day);

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

              const auto code = varList[varID].code;
              const auto gridID = varList[varID].gridID;
              const auto zaxisID = varList[varID].zaxisID;
              const auto datatype = varList[varID].datatype;
              const auto dig = (datatype == CDI_DATATYPE_FLT64) ? Options::CDO_dbl_digits : Options::CDO_flt_digits;
              gridsize = varList[varID].nwpv * varList[varID].gridsize;
              auto nlon = gridInqXsize(gridID);
              auto nlat = gridInqYsize(gridID);
              const auto level = cdo_zaxis_inq_level(zaxisID, levelID);
              const auto missval = varList[varID].missval;

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

              if (nlon * nlat != gridsize)
                {
                  nlon = gridsize;
                  nlat = 1;
                }

              cdo_read_record(streamID, array.data(), &nmiss);

              if (operatorID == OUTPUTSRV)
                fprintf(stdout, "%4d %8g %8ld %4d %8zu %8zu %d %d\n", code, level, (long) vdate, vtime, nlon, nlat, 0, 0);

              if (operatorID == OUTPUTEXT) fprintf(stdout, "%8ld %4d %8g %8zu\n", (long) vdate, code, level, gridsize);

              if (operatorID == OUTPUTINT)
                {
                  outputint(gridsize, array);
                }
              else if (operatorID == OUTPUTF)
                {
                  outputf(nelem, format, gridsize, array);
                }
              else if (operatorID == OUTPUTTS)
                {
                  if (gridsize > 1) cdo_abort("operator works only with one gridpoint!");

                  fprintf(stdout, "%s %s %.*g\n", vdateString.c_str(), vtimeString.c_str(), dig, array[0]);
                }
              else if (operatorID == OUTPUTFLD)
                {
                  int hour, minute, second;
                  cdiDecodeTime(vtime, &hour, &minute, &second);
                  double xdate = vdate - (vdate / 100) * 100 + (hour * 3600 + minute * 60 + second) / 86400.;
                  for (size_t i = 0; i < gridsize; i++)
                    if (!DBL_IS_EQUAL(array[i], missval))
                      fprintf(stdout, "%g\t%g\t%g\t%.*g\n", xdate, grid_center_lat[i], grid_center_lon[i], dig, array[i]);
                }
              else if (operatorID == OUTPUTTAB)
                {
                  const auto is2dGrid = (gridtype == GRID_CURVILINEAR || gridtype == GRID_LONLAT || gridtype == GRID_PROJECTION);
                  for (size_t i = 0; i < gridsize; i++)
                    {
                      auto yind = i;
                      auto xind = i;
                      if (is2dGrid)
                        {
                          yind /= xsize;
                          xind -= yind * xsize;
                        }
                      const auto lon = grid_center_lon[i];
                      const auto lat = grid_center_lat[i];

                      const auto xval = hasRegxyCoordinates ? grid_xvals[xind] : grid_xvals[i];
                      const auto yval = hasRegxyCoordinates ? grid_yvals[yind] : grid_yvals[i];

                      for (k = 0; k < nkeys; ++k)
                        {
                          int len = Keylen[keys[k]];
                          // clang-format off
                          switch (keys[k])
                            {
                            case kvalue:    fprintf(stdout, "%*.*g ", len, dig, array[i]); break;
                            case kparam:    fprintf(stdout, "%*s ", len, paramstr); break;
                            case kcode:     fprintf(stdout, "%*d ", len, code); break;
                            case kname:     fprintf(stdout, "%*s ", len, varList[varID].name); break;
                            case kx:        fprintf(stdout, "%*g ", len, xval); break;
                            case ky:        fprintf(stdout, "%*g ", len, yval); break;
                            case klon:      fprintf(stdout, "%*g ", len, lon); break;
                            case klat:      fprintf(stdout, "%*g ", len, lat); break;
                            case klev:      fprintf(stdout, "%*g ", len, level); break;
                            case kbin:      fprintf(stdout, "%*g ", len, level); break;
                            case kxind:     fprintf(stdout, "%*zu ", len, xind + 1); break;
                            case kyind:     fprintf(stdout, "%*zu ", len, yind + 1); break;
                            case ktimestep: fprintf(stdout, "%*d ", len, tsID + 1); break;
                            case kdate:     fprintf(stdout, "%*s ", len, vdateString.c_str()); break;
                            case ktime:     fprintf(stdout, "%*s ", len, vtimeString.c_str()); break;
                            case kyear:     fprintf(stdout, "%*d ", len, year); break;
                            case kmonth:    fprintf(stdout, "%*d ", len, month); break;
                            case kday:      fprintf(stdout, "%*d ", len, day); break;
                            }
                          // clang-format on
                        }
                      fprintf(stdout, "\n");
                    }
                }
              else if (operatorID == OUTPUTXYZ)
                {
                  if (tsID == 0 && recID == 0) outputxyz(gridsize, array, missval, nlon, nlat, grid_center_lon, grid_center_lat);
                }
              else if (operatorID == OUTPUTARR)
                {
                  outputarr(dig, gridsize, array);
                }
              else
                {
                  if (gridInqType(gridID) == GRID_SPECTRAL && gridsize <= 156)
                    outputsp(gridsize, array, gridInqTrunc(gridID));
                  else
                    output(gridsize, array);
                }
            }

          tsID++;
        }

      cdo_stream_close(streamID);
    }

  cdo_finish();

  return nullptr;
}
