/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set sw=2 sts=2 et cin: */
/*
 * This file is part of the MUSE Instrument Pipeline
 * Copyright (C) 2005-2015 European Southern Observatory
 *
 * 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; either version 2 of the License, or
 * (at your option) any later version.
 *
 * 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.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

/*----------------------------------------------------------------------------*
 *                              Includes                                      *
 *----------------------------------------------------------------------------*/
#include <cpl.h>
#include <math.h>
#include <string.h>

#include "muse_pixgrid.h"

#include "muse_pfits.h"
#include "muse_quality.h"
#include "muse_utils.h"
#include "muse_wcs.h"

/*----------------------------------------------------------------------------*/
/**
 * @defgroup muse_pixgrid      Pixel grid
 *
 * The pixel grid is an index structure that is used when resampling a pixel
 * table into a datacube but can generally be used to find neighbors in the 3D
 * data of the pixel table.
 */
/*----------------------------------------------------------------------------*/

/**@{*/

/*---------------------------------------------------------------------------*/
/**
  @private
  @brief   Create a new pixgrid.
  @param   aSizeX   X size of the grid.
  @param   aSizeY   Y size of the grid.
  @param   aSizeZ   Z size of the grid.
  @return  Pointer to the newly created pixgrid.
*/
/*---------------------------------------------------------------------------*/
static muse_pixgrid *
muse_pixgrid_new(cpl_size aSizeX, cpl_size aSizeY, cpl_size aSizeZ)
{
  muse_pixgrid *pixels = cpl_calloc(1, sizeof(muse_pixgrid));
  pixels->size_x = aSizeX;
  pixels->size_y = aSizeY;
  pixels->size_z = aSizeZ;
  cpl_size size = aSizeX * aSizeY * aSizeZ;
  pixels->pix = cpl_calloc(size, sizeof(cpl_size));
  return pixels;
} /* muse_pixgrid_new() */

/*---------------------------------------------------------------------------*/
/**
  @private
  @brief   Add a table row to the pixgrid.
  @param   aPixels   Pointer to pixgrid.
  @param   aIndex    Pixel index, as computed by muse_pixgrid_get_index().
  @param   aRow      Row number to be added.

  This function adds a new entry into the grid, either directly in the grid or
  in the extension map (aPixels->ext). To do the latter, it allocates space for
  storage in the extension map, doubling the amount of allocated memory every
  time an enlargement is needed. The number of real (filled) extension map
  entries is stored in aPixels->n_ext, which the current number of allocated
  entries is tracked with aPixels->n_alloc.
*/
/*---------------------------------------------------------------------------*/
static void
muse_pixgrid_add(muse_pixgrid *aPixels, cpl_size aIndex, cpl_size aRow)
{
  if (aIndex < 0) {
    return;
  }
  if (aPixels->pix[aIndex] == 0 && aRow > 0) {
    /* First pixel is stored directly. */
    aPixels->pix[aIndex] = aRow;
  } else if (aPixels->pix[aIndex] == 0 && aRow == 0) {
    /* Special case: we cannot put "0" into the main map. */
    cpl_size i_ext = aPixels->n_ext++;
    if (aPixels->n_ext > aPixels->n_alloc) { /* double the number of allocated entries */
      aPixels->n_alloc = 2 * aPixels->n_ext;
      aPixels->ext = cpl_realloc(aPixels->ext,
                                 aPixels->n_alloc * sizeof(muse_pixels_ext));
    }
    aPixels->ext[i_ext].npix = 1;
    aPixels->ext[i_ext].pix = cpl_malloc(sizeof(cpl_size));
    aPixels->ext[i_ext].pix[0] = aRow ;
    aPixels->pix[aIndex] = - (i_ext + 1);
  } else if (aPixels->pix[aIndex] > 0) {
    /* When a second pixel is added, put both to the extension map. */
    cpl_size i_ext = aPixels->n_ext++;
    if (aPixels->n_ext > aPixels->n_alloc) { /* double the number of allocated entries */
      aPixels->n_alloc = 2 * aPixels->n_ext;
      aPixels->ext = cpl_realloc(aPixels->ext,
                                 aPixels->n_alloc * sizeof(muse_pixels_ext));
    }
    aPixels->ext[i_ext].npix = 2;
    aPixels->ext[i_ext].pix = cpl_malloc(2 * sizeof(cpl_size));
    aPixels->ext[i_ext].pix[0] = aPixels->pix[aIndex];
    aPixels->ext[i_ext].pix[1] = aRow;
    aPixels->pix[aIndex] = - (i_ext + 1);
  } else {
    /* Append additional pixels to the extension map. */
    cpl_size i_ext = - aPixels->pix[aIndex] - 1;
    int i_pix = aPixels->ext[i_ext].npix;
    aPixels->ext[i_ext].npix++;
    aPixels->ext[i_ext].pix
      = cpl_realloc(aPixels->ext[i_ext].pix,
                    aPixels->ext[i_ext].npix * sizeof(cpl_size));
    aPixels->ext[i_ext].pix[i_pix] = aRow;
  }
} /* muse_pixgrid_add() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Convert selected rows of a pixel table into pixel grid, linking the
           grid points to entries (=rows) in the pixel table.
  @param   aPixtable   the input pixel table
  @param   aHeader     the FITS header of the MUSE datacube to be created
  @param   aXSize      x size of the output grid
  @param   aYSize      y size of the output grid
  @param   aZSize      z size of the output grid (wavelength direction)
  @return  A muse_pixels * buffer for the output pixel grid or NULL on error.
  @remark  The returned pixel grid has to be deallocated after use with
           muse_pixgrid_delete().

  Construct a standard C array, where the array indices representing the 3D grid
  in the sense the the x-coordinate is varying fastest, the lambda-coordinate
  varying slowest (like in FITS buffers), i.e.
    index = [i + nx * j + nx*ny * l]
    (i: x-axis index, j: y-axis index, l: lambda-axis index,
    nx: x-axis length, ny: y-axis length).
  For each pixel table row search for the closest grid point. Store the pixel
  table row number in that grid point.

  @error{set CPL_ERROR_NULL_INPUT\, return NULL,
         aPixtable is NULL or it contains zero rows}
  @error{set CPL_ERROR_ILLEGAL_INPUT\, return NULL,
         one of the sizes is not positive}
  @error{set CPL_ERROR_UNSUPPORTED_MODE\, return NULL,
         the WCS in the pixel table is neither in pixels nor degrees}
  @error{set CPL_ERROR_DATA_NOT_FOUND\, return NULL,
         aPixtable is missing one of the coordinate columns}
 */
/*---------------------------------------------------------------------------*/
muse_pixgrid *
muse_pixgrid_create(muse_pixtable *aPixtable, cpl_propertylist *aHeader,
                    cpl_size aXSize, cpl_size aYSize, cpl_size aZSize)
{
  cpl_ensure(aPixtable, CPL_ERROR_NULL_INPUT, NULL);
  cpl_size nrow = muse_pixtable_get_nrow(aPixtable);
  if (nrow == 0) {
    cpl_msg_error(__func__, "Invalid pixel table (no entries?)");
    cpl_error_set(__func__, CPL_ERROR_NULL_INPUT);
    return NULL;
  }
  cpl_ensure(aXSize > 0 && aYSize > 0 && aZSize > 0, CPL_ERROR_ILLEGAL_INPUT,
             NULL);
  muse_pixtable_wcs wcstype = muse_pixtable_wcs_check(aPixtable);
  cpl_ensure(wcstype == MUSE_PIXTABLE_WCS_CELSPH ||
             wcstype == MUSE_PIXTABLE_WCS_PIXEL, CPL_ERROR_UNSUPPORTED_MODE,
             NULL);

  double crval3 = muse_pfits_get_crval(aHeader, 3),
         crpix3 = muse_pfits_get_crpix(aHeader, 3),
         cd33 = muse_pfits_get_cd(aHeader, 3, 3);
  muse_wcs *wcs = muse_wcs_new(aHeader);
  wcs->iscelsph = wcstype == MUSE_PIXTABLE_WCS_CELSPH;
  cpl_boolean loglambda = !strncmp(muse_pfits_get_ctype(aHeader, 3),
                                   "AWAV-LOG", 9);
  /* get all (relevant) table columns for easy pointer access */
  double ptxoff = 0., ptyoff = 0.;
  if (wcs->iscelsph) {
    ptxoff = muse_pfits_get_crval(aPixtable->header, 1);
    ptyoff = muse_pfits_get_crval(aPixtable->header, 2);
  }
  float *xpos = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_XPOS),
        *ypos = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_YPOS),
        *lbda = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_LAMBDA);
  if (!xpos || !ypos || !lbda) {
    cpl_msg_error(__func__, "Missing pixel table column (%p %p %p): %s",
                  (void *)xpos, (void *)ypos, (void *)lbda,
                  cpl_error_get_message());
    cpl_error_set(__func__, CPL_ERROR_DATA_NOT_FOUND);
    return NULL;
  }
#ifdef ESO_ENABLE_DEBUG
  int debug = 0;
  if (getenv("MUSE_DEBUG_GRID_CONVERSION")) {
    debug = atoi(getenv("MUSE_DEBUG_GRID_CONVERSION"));
  }
  if (debug) {
    printf("crpix=%f %f %f, crval=%f %f %f, cd=%e %e %f\n",
           wcs->crpix1, wcs->crpix2, crpix3, wcs->crval1, wcs->crval2, crval3,
           wcs->cd11, wcs->cd22, cd33);
  }
#endif
  if (wcs->iscelsph) {
    wcs->crval1 /= CPL_MATH_DEG_RAD; /* convert to radians before calling...    */
    wcs->crval2 /= CPL_MATH_DEG_RAD; /* ...muse_wcs_pixel_from_celestial_fast() */
  }
  double timeinit = cpl_test_get_walltime(),
         timeprogress = timeinit,
         cpuinit = cpl_test_get_cputime();
  cpl_boolean showprogress = cpl_msg_get_level() == CPL_MSG_DEBUG
                           || cpl_msg_get_log_level() == CPL_MSG_DEBUG;
  muse_pixgrid *pixgrid = muse_pixgrid_new(aXSize, aYSize, aZSize);

  /* Loop through the pixel table and write values into the pixel grid. *
   * This cannot easily be parallelized, as it has to do allocations on *
   * memory that would be common to all (OpenMP) threads.               */
  cpl_array *asel = cpl_table_where_selected(aPixtable->table);
  const cpl_size *sel = cpl_array_get_data_cplsize_const(asel);
  cpl_size isel, nsel = cpl_array_get_size(asel);
  for (isel = 0 ; isel < nsel; isel++) {
    if (showprogress && !((isel+1) % 1000000ll)) { /* output before every millionth entry */
      double timenow = cpl_test_get_walltime();
      if (timenow - timeprogress > 30.) { /* and more than half a minute passed */
        timeprogress = timenow;
        double percent = 100. * (isel + 1.) / nsel,
               elapsed = timeprogress - timeinit,
               remaining = (100. - percent) * elapsed / percent;
        /* overwritable only exists for INFO mode, but we check  *
         * above that we want this only for DEBUG mode output... */
        cpl_msg_info_overwritable(__func__, "pixel grid creation is %.1f%% "
                                  "complete, %gs elapsed, ~%gs remaining",
                                  percent, elapsed, remaining);
      } /* if: 1/2 min passed */
    } /* if: want debug output */
    cpl_size n = sel[isel];
    /* determine the pixel coordinates in the grid (indices, starting at 0) */
    double xpx, ypx;
    if (wcs->iscelsph) {
      muse_wcs_pixel_from_celestial_fast(wcs, (xpos[n] + ptxoff) / CPL_MATH_DEG_RAD,
                                         (ypos[n] + ptyoff) / CPL_MATH_DEG_RAD, &xpx, &ypx);
    } else {
      muse_wcs_pixel_from_projplane_fast(wcs, xpos[n], ypos[n],
                                         &xpx, &ypx);
    }
    int x = lround(xpx) - 1,
        y = lround(ypx) - 1,
        z = lround((lbda[n] - crval3) / cd33 + crpix3) - 1;
    if (loglambda) {
      z = lround(crval3 / cd33 * log(lbda[n] / crval3));
    }
    cpl_size idx = muse_pixgrid_get_index(pixgrid, x, y, z, CPL_TRUE);
#ifdef ESO_ENABLE_DEBUG
    if (debug) {
      printf("%"CPL_SIZE_FORMAT": %f %f %f -> %d %d %d (%"CPL_SIZE_FORMAT")\n",
             n, xpos[n] + ptxoff, ypos[n] + ptyoff, lbda[n], x, y, z, idx);
    }
#endif

    /* write the pixel values to the correct place in the grid */
    muse_pixgrid_add(pixgrid, idx, n);
  } /* for isel (all selected pixel table rows) */
  cpl_array_delete(asel);
  cpl_free(wcs);
  /* Clean up the possibly too many allocations; this is not strictly *
   * needed but nice to only consume as much memory as we need.       */
  pixgrid->ext = cpl_realloc(pixgrid->ext,
                             pixgrid->n_ext * sizeof(muse_pixels_ext));
  pixgrid->n_alloc = pixgrid->n_ext;
#ifdef ESO_ENABLE_DEBUG
  if (debug) {
    fflush(stdout);
  }
#endif

  cpl_size idx, npix_sum = 0;
  for (idx = 0; idx < aXSize * aYSize * aZSize; idx++) {
    npix_sum += muse_pixgrid_get_count(pixgrid, idx);
  }
  double timefini = cpl_test_get_walltime(),
         cpufini = cpl_test_get_cputime();
  cpl_msg_debug(__func__, "pixel grid: %dx%dx%d, %"CPL_SIZE_FORMAT" pixels "
                "total, %"CPL_SIZE_FORMAT" (%.1f%%) in extension map; took %gs "
                "(wall-clock) and %gs (CPU) to create", (int)pixgrid->size_x,
                (int)pixgrid->size_y, (int)pixgrid->size_z, npix_sum, pixgrid->n_ext,
                (double)pixgrid->n_ext / npix_sum * 100., timefini - timeinit,
                cpufini - cpuinit);

  return pixgrid;
} /* muse_pixgrid_create() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Convert selected rows of a pixel table into 2D pixgrid, linking the
           grid points to entries (=rows) in the pixel table.
  @param   aTable   the table component of a MUSE pixel table
  @param   aDX      X (spaxel) bin size
  @param   aZMin    Lower z (wavelength) limit
  @param   aZMax    Upper z (wavelength) limit
  @param   aDZ      Z (wavelength) bin size
  @param   aXMin    Lower x (spaxel) limit computed here (can be NULL)
  @return  The output pixgrid or NULL on error.

  The Y coordinate is ignored in this function.

  @error{set CPL_ERROR_NULL_INPUT\, return NULL,
         aTable is NULL or it contains zero rows}
  @error{set CPL_ERROR_ILLEGAL_INPUT\, return NULL,
         one of the sizes is not positive}
  @error{set CPL_ERROR_DATA_NOT_FOUND\, return NULL,
         aTable is missing one of the coordinate columns}
 */
/*---------------------------------------------------------------------------*/
muse_pixgrid *
muse_pixgrid_2d_create(cpl_table *aTable, double aDX,
                       double aZMin, double aZMax, double aDZ, float *aXMin)
{
  cpl_ensure(aTable, CPL_ERROR_NULL_INPUT, NULL);
  cpl_size nrow = cpl_table_get_nrow(aTable);
  if (nrow == 0) {
    cpl_msg_error(__func__, "Invalid pixel table (no entries?)");
    cpl_error_set(__func__, CPL_ERROR_NULL_INPUT);
    return NULL;
  }

  /* get the (relevant) table columns for easy pointer access */
  float *xpos = cpl_table_get_data_float(aTable, MUSE_PIXTABLE_XPOS),
        *lbda = cpl_table_get_data_float(aTable, MUSE_PIXTABLE_LAMBDA);
  if (!xpos || !lbda) {
    cpl_msg_error(__func__, "Missing pixel table column (%p %p): %s",
                  (void *)xpos, (void *)lbda, cpl_error_get_message());
    cpl_error_set(__func__, CPL_ERROR_DATA_NOT_FOUND);
    return NULL;
  }

  /* get the selection for fast access to the relevant rows */
  cpl_array *selection = cpl_table_where_selected(aTable);
  cpl_size nsel = cpl_array_get_size(selection);
  const cpl_size *sel = cpl_array_get_data_cplsize_const(selection);

  /* search for lowest x value in selected rows */
  float xlo = FLT_MAX, xhi = -FLT_MAX;
  cpl_size i;
  for (i = 0; i < nsel; i++) {
    if (xpos[sel[i]] > xhi) xhi = xpos[sel[i]];
    if (xpos[sel[i]] < xlo) xlo = xpos[sel[i]];
  } /* for i (all selected pixel table rows) */
  if (aXMin) {
    *aXMin = xlo;
  }

  /* create the empty 2D grid depending on size of input data */
  cpl_size xsize = ceil((xhi - xlo) / aDX) + 1,
           zsize = ceil((aZMax - aZMin) / aDZ) + 1;
  muse_pixgrid *pixgrid = muse_pixgrid_new(xsize, 1, zsize);

  /* loop through the pixel table and write values into the pixel grid */
  for (i = 0; i < nsel; i++) {
    /* determine the pixel coordinates in the grid; offset is min-1 to *
     * have all these indices start at 0 for easy buffer access        */
    int x = lround((xpos[sel[i]] - xlo) / aDX),
        z = lround((lbda[sel[i]] - aZMin) / aDZ);
    cpl_size idx = muse_pixgrid_get_index(pixgrid, x, 0, z, CPL_TRUE);
    /* write the pixel values to the correct place in the grid */
    muse_pixgrid_add(pixgrid, idx, sel[i]);
  } /* for i (all selected pixel table rows) */
  cpl_array_delete(selection);
  /* clean up the possibly too many allocations */
  pixgrid->ext = cpl_realloc(pixgrid->ext,
                             pixgrid->n_ext * sizeof(muse_pixels_ext));
  pixgrid->n_alloc = pixgrid->n_ext;

  return pixgrid;
} /* muse_pixgrid_2d_create() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Delete a pixgrid and remove its memory.
  @param   aPixels   Pointer to pixgrid.
*/
/*---------------------------------------------------------------------------*/
void
muse_pixgrid_delete(muse_pixgrid *aPixels)
{
  if (!aPixels) {
    return;
  }
  cpl_free(aPixels->pix);
  cpl_size i_ext;
  for (i_ext = 0; i_ext < aPixels->n_ext; i_ext++) {
    cpl_free(aPixels->ext[i_ext].pix);
  }
  aPixels->n_ext = 0;
  cpl_free(aPixels->ext);
  aPixels->n_alloc = 0;
  cpl_free(aPixels);
} /* muse_pixgrid_delete() */


/**@}*/
