/*
 * This file is part of the GIRAFFE Pipeline
 * Copyright (C) 2002-2019 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 St, Fifth Floor, Boston, MA  02110-1301  USA
 */

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


#include <string.h>
#include <math.h>

#include <cxmacros.h>
#include <cxmessages.h>
#include <cxmemory.h>
#include <cxstrutils.h>

#include <cpl_error.h>
#include <cpl_image.h>
#include <cpl_table.h>
#include <cpl_parameterlist.h>
#include <cpl_msg.h>

#include "gialias.h"
#include "gierror.h"
#include "gimessages.h"
#include "gimatrix.h"
#include "gichebyshev.h"
#include "gimodel.h"
#include "gipsfdata.h"
#include "gilocalization.h"
#include "gimask.h"
#include "gimath.h"
#include "giclip.h"
#include "gifiberutils.h"
#include "gipsf.h"


/**
 * @defgroup gipsf PSF Profile Fitting
 *
 * TBD
 */

/**@{*/

enum GiProfileId {
    PROFILE_PSFEXP   = 1 << 1,
    PROFILE_PSFEXP2  = 1 << 2,
    PROFILE_GAUSSIAN = 1 << 3
};

typedef enum GiProfileId GiProfileId;


struct GiPsfParams {
    cxint bsize;        /* Size of X bins for the fit */
    cxint mwidth;       /* Maximum spectrum half-width for the fit */
    cxdouble limit;     /* Profile computation limit. Fraction of amplitude */
    cxbool normalize;   /* Use normalized pixel values */
};

typedef struct GiPsfParams GiPsfParams;


struct GiPsfBin {
    cxdouble zmin;
    cxdouble zmax;
    cxdouble xcenter;
    cxdouble ycenter;
    cxdouble ywidth;
};

typedef struct GiPsfBin GiPsfBin;

struct GiPsfParameterFit {
    cpl_image* fit;
    cpl_matrix* coeffs;
};

typedef struct GiPsfParameterFit GiPsfParameterFit;


/*
 * @brief
 *   Perform a 1d fit of a PSF profile parameter.
 *
 * @param result   Container for the fit results.
 * @param psfdata  PSF profile data.
 * @param name     PSF profile parameter name to fit.
 * @param fibers   Table of fibers used.
 * @param order    Order of the Chebyshev polynomial to fit.
 * @param setup    Kappa-sigma clipping algorithm setup information.
 *
 * @return
 *   The function returns 0 on success, or a non-zero value otherwise.
 *
 * The function fits a 1d Chebyshev polynomial of the order @em order to the
 * PSF profile parameter data given by @em psfdata and @em name. The object
 * @em psfdata contains the data for each parameter (peak position, width,
 * etc.) of the used profile model. The parameter for which the fit is
 * performed is selected by the name @em name. The list of fibers for which
 * profile data are available in @em psfdata is given by @em fibers.
 *
 * When fitting the parameter data, data points deviating too much from
 * the fit are rejected using a kappa-sigma rejection algorithm. The
 * kappa-sigma rejection is configured by @em setup.
 *
 * The results, i.e. the fit coefficients and the fit of the parameter data
 * are written to the results container @em result. The matrix and image
 * to which the results are written must have been created by the caller
 * with the appropriate size.
 */

inline static cxint
_giraffe_psf_fit_profile1d(GiPsfParameterFit* result,
                           const GiPsfData* psfdata, const cxchar* name,
                           const cpl_table* fibers, cxint order,
                           const GiClipParams* setup)
{

    cxint i = 0;
    cxint ns = 0;
    cxint nx = 0;
    cxint nb = 0;

    cpl_matrix* x = NULL;
    cpl_matrix* base = NULL;

    const cpl_image* parameter = NULL;


    cx_assert(result != NULL);
    cx_assert(result->coeffs != NULL);
    cx_assert(result->fit != NULL);
    cx_assert(psfdata != NULL && name != NULL);
    cx_assert(fibers != NULL);
    cx_assert(setup != NULL);

    nb = giraffe_psfdata_bins(psfdata);
    ns = giraffe_psfdata_fibers(psfdata);
    nx = giraffe_psfdata_ysize(psfdata);

    if (ns != cpl_table_get_nrow(fibers)) {
        return -1;
    }

    if ((cpl_image_get_size_x(result->fit) != ns) ||
        (cpl_image_get_size_y(result->fit) != nx)) {
            return -1;
    }

    if ((cpl_matrix_get_nrow(result->coeffs) != order + 1) ||
        (cpl_matrix_get_ncol(result->coeffs) != ns)) {
            return -1;
    }

    for (i = 0; i < ns; i++) {

        register cxint j = 0;
        register cxint valid_bins = 0;

        for (j = 0; j < nb; j++) {
            if (giraffe_psfdata_get_bin(psfdata, i, j) >= 0.) {
                ++valid_bins;
            }
        }

        if (valid_bins < order + 1) {
            return 1;
        }

    }


    /*
     * Compute the Chebyshev base for all points
     */

    x = cpl_matrix_new(nx, 1);

    for (i = 0; i < nx; i++) {
        cpl_matrix_set(x, i, 0, i);
    }

    base = giraffe_chebyshev_base1d(0., (cxdouble)nx, order + 1, x);

    if (base == NULL) {
        cpl_matrix_delete(x);
        x = NULL;

        return 2;
    }

    cpl_matrix_delete(x);
    x = NULL;


    /*
     * Fit PSF profile parameter data
     */

    parameter = giraffe_psfdata_get_data(psfdata, name);

    if (parameter == NULL) {
        return 3;
    }

    for (i = 0; i < ns; i++) {

        cxint j = 0;
        cxint k = 0;
        cxint naccepted = 0;
        cxint ntotal = 0;
        cxint iteration = 0;

        const cxdouble* _parameter =
            cpl_image_get_data_double_const(parameter);

        cxdouble ratio = 1.;
        cxdouble* _fit = cpl_image_get_data_double(result->fit);

        cpl_matrix* y = NULL;
        cpl_matrix* ydiff = NULL;
        cpl_matrix* coeffs = NULL;
        cpl_matrix* fit = NULL;


        x = cpl_matrix_new(nb, 1);
        y = cpl_matrix_new(1, nb);
        ydiff = cpl_matrix_new(1, nb);

        for (j = 0; j < nb; j++) {

            cxdouble bin = giraffe_psfdata_get_bin(psfdata, i, j);


            if (bin >= 0.) {
                cpl_matrix_set(x, k, 0, bin);
                cpl_matrix_set(y, 0, k, _parameter[j * ns + i]);
                ++k;
            }

        }


        /*
         * Shrink matrices to their actual size.
         */

        cpl_matrix_set_size(x, k, 1);
        cpl_matrix_set_size(y, 1, k);
        cpl_matrix_set_size(ydiff, 1, k);

        ntotal = cpl_matrix_get_nrow(x);
        naccepted = ntotal;

        while ((naccepted > 0) && (iteration < setup->iterations) &&
               (ratio > setup->fraction)) {

            cxdouble sigma = 0.;

            cpl_matrix* _base = NULL;


            if (coeffs != NULL) {
                cpl_matrix_delete(coeffs);
                coeffs = NULL;
            }

            if (fit != NULL) {
                cpl_matrix_delete(fit);
                fit = NULL;
            }

            _base = giraffe_chebyshev_base1d(0., (cxdouble)nx, order + 1, x);
            coeffs =  giraffe_matrix_leastsq(_base, y);

            if (coeffs == NULL) {
                cpl_matrix_delete(_base);
                _base = NULL;
            }

            cpl_matrix_delete(_base);
            _base = NULL;

            fit = cpl_matrix_product_create(coeffs, base);

            for (j = 0; j < cpl_matrix_get_nrow(x); j++) {

                cxint xlower = (cxint) ceil(cpl_matrix_get(x, j, 0));
                cxint xupper = (cxint) floor(cpl_matrix_get(x, j, 0));

                cxdouble ylower = cpl_matrix_get(fit, 0, xlower);
                cxdouble yupper = cpl_matrix_get(fit, 0, xupper);
                cxdouble yfit = (yupper + ylower) / 2.;

                cpl_matrix_set(ydiff, 0, j, cpl_matrix_get(y, 0, j) - yfit);

            }

            sigma = setup->level * giraffe_matrix_sigma_mean(ydiff, 0.);


            /*
             * Reject data points deviating too much.
             */

             k = 0;
             for (j = 0; j < cpl_matrix_get_ncol(ydiff); j++) {
                 if (fabs(cpl_matrix_get(ydiff, 0, j)) <= sigma) {
                     cpl_matrix_set(x, k, 0, cpl_matrix_get(x, j, 0));
                     cpl_matrix_set(y, 0, k, cpl_matrix_get(y, 0, j));
                     ++k;
                 }
             }

             cpl_matrix_set_size(x, k, 1);
             cpl_matrix_set_size(y, 1, k);
             cpl_matrix_set_size(ydiff, 1, k);


             /*
              * Stop if no additional data points have been rejected
              * in the last iteration. Otherwise update the number of
              * accepted points, the ratio, and reset the fit coefficients
              * and the parameter fit.
              */

            if (k == naccepted) {
                break;
            }
            else {
                naccepted = k;
                ratio = (cxdouble)naccepted / (cxdouble) ntotal;
                ++iteration;
            }

        }


        /*
         * Save the fit coefficients and the parameter fit to the
         * results object.
         */

        cx_assert(cpl_matrix_get_ncol(coeffs) == order + 1);

        for (j = 0; j < cpl_matrix_get_ncol(coeffs); j++) {
            cpl_matrix_set(result->coeffs, j, i,
                           cpl_matrix_get(coeffs, 0, j));
        }

        for (j = 0; j < nx; j++) {
            _fit[j * ns + i] = cpl_matrix_get(fit, 0, j);
        }


        /*
         * Cleanup temporary buffers
         */

        cpl_matrix_delete(x);
        x = NULL;

        cpl_matrix_delete(y);
        y = NULL;

        cpl_matrix_delete(ydiff);
        ydiff = NULL;

        cpl_matrix_delete(coeffs);
        coeffs = NULL;

        cpl_matrix_delete(fit);
        fit = NULL;

    }

    cpl_matrix_delete(base);
    base = NULL;

    return 0;

}


/*
 * @brief
 *   Perform a 2d fit of a PSF profile parameter.
 *
 * @param result   Container for the fit results.
 * @param fibers   Table of fibers used.
 * @param psfdata  Binned parameter data to fit.
 * @param xbin     Abscissa values of the bins.
 * @param ybin     Ordinate values of the bins.
 * @param xorder   X order of the Chebyshev polynomial to fit.
 * @param yorder   Y order of the Chebyshev polynomial to fit.
 * @param yfit     Ordinate values for which the fit is computed.
 * @param ystart   Minimum value of the ordinate values range.
 * @param yend     Maximum value of the ordinate values range.
 * @param setup    Kappa-sigma clipping algorithm setup information.
 *
 * @return
 *   The function returns 0 on success, or a non-zero value otherwise.
 *
 * The function fits a 2d Chebyshev polynomial of the order @em xorder and
 * @em yorder, along x and y respectively, to the PSF profile parameter
 * data given by @em psfdata. The object @em psfdata contains the data for
 * a single PSF profile parameter (peak position, width, etc.) of the used
 * profile model. The list of fibers for which profile data are available
 * in @em psfdata is given by @em fibers. The abscissa and ordinate values
 * for which the PSF parameter data have been measured.
 *
 * When fitting the parameter data, data points deviating too much from
 * the fit are rejected using a kappa-sigma rejection algorithm. The
 * kappa-sigma rejection is configured by @em setup.
 *
 * The results, i.e. the fit coefficients and the fit of the parameter data
 * are written to the results container @em result. The matrix and image
 * to which the results are written must have been created by the caller
 * with the appropriate sizes.
 *
 * The fit is computed for the positions given by @em yfit, @em ystart and
 * @em yend., where the latter define the ordinate domain for which @em yfit
 * is given. The object @em yfit itself gives the ordinate values, and, by its
 * bin structure (row index), the abscissa values for whichthe fit will
 * be computed.
 */

inline static cxint
_giraffe_psf_fit_profile2d(GiPsfParameterFit* result, const cpl_table* fibers,
                           const cpl_image* psfdata, const cpl_image* xbin,
                           const cpl_image* ybin, cxint xorder, cxint yorder,
                           const cpl_image* yfit, cxint ystart, cxint yend,
                           const GiClipParams* setup)
{


    cxint i = 0;
    cxint k = 0;
    cxint ns = 0;
    cxint nb = 0;
    cxint nx = 0;
    cxint iteration = 0;
    cxint ntotal = 0;
    cxint naccepted = 0;
    cxint nspectra = 0;
    cxint status = 0;
    cxint ncx = xorder + 1;
    cxint ncy = yorder + 1;

    cxdouble ratio = 1.;

    cpl_matrix* x = NULL;
    cpl_matrix* y = NULL;
    cpl_matrix* z = NULL;
    cpl_matrix* zdiff = NULL;
    cpl_matrix* nbins = NULL;

    GiChebyshev2D* fit = NULL;


    cx_assert(result != NULL);
    cx_assert(result->coeffs != NULL);
    cx_assert(result->fit != NULL);
    cx_assert(fibers != NULL);
    cx_assert(psfdata != NULL);
    cx_assert(xbin != NULL && ybin != NULL);
    cx_assert(yfit != NULL);
    cx_assert(setup != NULL);

    nb = cpl_image_get_size_y(xbin);
    ns = cpl_image_get_size_x(xbin);
    nx = cpl_image_get_size_y(result->fit);

    if (ns != cpl_table_get_nrow(fibers)) {
        return -1;
    }

    if ((cpl_image_get_size_x(result->fit) != ns) ||
        (cpl_image_get_size_y(result->fit) != nx)) {
            return -1;
    }

    if ((cpl_matrix_get_nrow(result->coeffs) != ncx) ||
        (cpl_matrix_get_ncol(result->coeffs) != ncy)) {
            return -1;
    }

    for (i = 0; i < ns; i++) {

        register cxint j = 0;
        register cxint valid_bins = 0;

        const cxdouble* _xbin = cpl_image_get_data_double_const(xbin);

        for (j = 0; j < nb; j++) {
            if (_xbin[j * ns + i] >= 0.) {
                ++valid_bins;
            }
        }

        if (valid_bins < ncx * ncy) {
            return 1;
        }

    }


    /*
     * Fill working buffers
     */

    x = cpl_matrix_new(nb * ns, 1);
    y = cpl_matrix_new(nb * ns, 1);
    z = cpl_matrix_new(1, nb * ns);
    zdiff = cpl_matrix_new(1, nb * ns);
    nbins = cpl_matrix_new(nb * ns, 1);

    for (i = 0; i < ns; i++) {

        register cxint j = 0;

        const cxdouble* _xbin = cpl_image_get_data_double_const(xbin);
        const cxdouble* _ybin = cpl_image_get_data_double_const(ybin);
        const cxdouble* _zbin = cpl_image_get_data_double_const(psfdata);


        for ( j = 0; j < nb; j++) {

            register cxint l = j * ns + i;


            if (_xbin[l] >= 0.) {
                cpl_matrix_set(nbins, k, 0, nspectra);
                cpl_matrix_set(x, k, 0, _xbin[l]);
                cpl_matrix_set(y, k, 0, _ybin[l]);
                cpl_matrix_set(z, 0, k, _zbin[l]);
                ++k;
            }

        }

        ++nspectra;

    }


    /*
     * Shrink working buffers to their actual size
     */

    cpl_matrix_set_size(x, k, 1);
    cpl_matrix_set_size(y, k, 1);
    cpl_matrix_set_size(z, 1, k);
    cpl_matrix_set_size(zdiff, 1, k);
    cpl_matrix_set_size(nbins, k, 1);

    ntotal = cpl_matrix_get_nrow(x);
    naccepted = ntotal;

    while ((naccepted > 0) && (iteration < setup->iterations) &&
           (ratio > setup->fraction))
    {

        cxdouble sigma = 0.;

        cpl_matrix* base = NULL;
        cpl_matrix* coeffs = NULL;
        cpl_matrix* _coeffs = NULL;

        register cxdouble* _pfit = cpl_image_get_data_double(result->fit);


        base = giraffe_chebyshev_base2d(0., ystart, nx, yend, ncx, ncy, x, y);

        if (base == NULL) {
            cpl_matrix_delete(nbins);
            nbins = NULL;

            cpl_matrix_delete(zdiff);
            zdiff = NULL;

            cpl_matrix_delete(z);
            z = NULL;

            cpl_matrix_delete(y);
            y = NULL;

            cpl_matrix_delete(x);
            x = NULL;

            return 1;
        }

        _coeffs = giraffe_matrix_leastsq(base, z);

        if (_coeffs == NULL) {
            cpl_matrix_delete(base);
            base = NULL;

            cpl_matrix_delete(nbins);
            nbins = NULL;

            cpl_matrix_delete(zdiff);
            zdiff = NULL;

            cpl_matrix_delete(z);
            z = NULL;

            cpl_matrix_delete(y);
            y = NULL;

            cpl_matrix_delete(x);
            x = NULL;

            return 1;
        }

        cpl_matrix_delete(base);
        base = NULL;


        /*
         * Compute parameter fit and reject data points deviating too
         * much from the fit
         */

        coeffs = cpl_matrix_wrap(xorder + 1, yorder + 1,
                                 cpl_matrix_get_data(_coeffs));

        if (fit != NULL) {
            giraffe_chebyshev2d_delete(fit);
            fit = NULL;
        }

        fit = giraffe_chebyshev2d_new(xorder, yorder);
        status = giraffe_chebyshev2d_set(fit, 0., nx, ystart, yend, coeffs);

        if (status != 0) {
            giraffe_chebyshev2d_delete(fit);
            fit = NULL;

            cpl_matrix_unwrap(coeffs);
            coeffs = NULL;

            cpl_matrix_delete(_coeffs);
            _coeffs = NULL;

            cpl_matrix_delete(nbins);
            nbins = NULL;

            cpl_matrix_delete(zdiff);
            zdiff = NULL;

            cpl_matrix_delete(z);
            z = NULL;

            cpl_matrix_delete(y);
            y = NULL;

            cpl_matrix_delete(x);
            x = NULL;

            return 1;
        }

        cpl_matrix_unwrap(coeffs);
        coeffs = NULL;

        cpl_matrix_delete(_coeffs);
        _coeffs = NULL;


        /* FIXME: Check whether performance can be improved if the fit
         *        is only computed for the bins instead of the full
         *        image y-axis pixels. Note that this needs an additional
         *        buffer and a computation of the last fit on the full
         *        grid.
         */

        for (i = 0; i < ns; i++) {

            register cxint j = 0;

            register const cxdouble* _yfit =
                cpl_image_get_data_double_const(yfit);

            for (j = 0; j < nx; j++) {

                register cxint l = j * ns + i;

                _pfit[l] = giraffe_chebyshev2d_eval(fit, j, _yfit[l]);

            }

        }

        for (i = 0; i < cpl_matrix_get_nrow(x); i++) {

            cxint n = cpl_matrix_get(nbins, i, 0);
            cxint lower = (cxint) ceil(cpl_matrix_get(x, i, 0)) * ns + n;
            cxint upper = (cxint) floor(cpl_matrix_get(x, i, 0)) * ns + n;

            cxdouble zfit = (_pfit[lower] + _pfit[upper]) / 2.;

            cpl_matrix_set(zdiff, 0, i, cpl_matrix_get(z, 0, i) - zfit);

        }

        sigma = setup->level * giraffe_matrix_sigma_mean(zdiff, 0.);

        k = 0;
        for (i = 0; i < cpl_matrix_get_ncol(zdiff); i++) {
            if (fabs(cpl_matrix_get(zdiff, 0, i)) <= sigma) {
                cpl_matrix_set(x, k, 0, cpl_matrix_get(x, i, 0));
                cpl_matrix_set(y, k, 0, cpl_matrix_get(y, i, 0));
                cpl_matrix_set(z, 0, k, cpl_matrix_get(z, 0, i));
                cpl_matrix_set(nbins, k, 0, cpl_matrix_get(nbins, i, 0));
                ++k;
            }
        }

        cpl_matrix_set_size(x, k, 1);
        cpl_matrix_set_size(y, k, 1);
        cpl_matrix_set_size(z, 1, k);
        cpl_matrix_set_size(zdiff, 1, k);
        cpl_matrix_set_size(nbins, k, 1);


        /*
         * Stop if no additional data points have been rejected
         * in the last iteration. Otherwise update the number of
         * accepted points, the ratio, and reset the fit coefficients
         * and the parameter fit.
         */

        if (k == naccepted) {
            break;
        }
        else {
            naccepted = k;
            ratio = (cxdouble)naccepted / (cxdouble) ntotal;
            ++iteration;
        }

    }


    /*
     * Copy the fit coefficients to the results container.
     */

    for (i = 0; i < cpl_matrix_get_nrow(result->coeffs); i++) {

        register cxint j = 0;

        const cpl_matrix* c = giraffe_chebyshev2d_coeffs(fit);


        for (j = 0; j < cpl_matrix_get_ncol(result->coeffs); j++) {
            cpl_matrix_set(result->coeffs, i, j, cpl_matrix_get(c, i, j));
        }

    }


    /*
     * Cleanup temporary buffers
     */

    giraffe_chebyshev2d_delete(fit);
    fit = NULL;

    cpl_matrix_delete(nbins);
    nbins = NULL;

    cpl_matrix_delete(zdiff);
    zdiff = NULL;

    cpl_matrix_delete(z);
    z = NULL;

    cpl_matrix_delete(y);
    y = NULL;

    cpl_matrix_delete(x);
    x = NULL;

    return 0;
}


/*
 * @brief
 *  Fit a PSF profile model to each fiber spectrum.
 *
 * @param result    Fitted PSF profile model parameters.
 * @param zraw      Raw image of the fiber spectra.
 * @param zvar      Raw image of flux errors.
 * @param locy      Fiber centroid position.
 * @param locw      Fiber width.
 * @param fibers    List of fibers to process.
 * @param bpm       Optional bad pixel map.
 * @param config    PSF profile fit setup parameters.
 *
 * @return
 *  The function returns @c 0 on success, or a non-zero value otherwise.
 *
 * The function fits a profile model given by @em psfmodel to each bin along
 * the dispersion direction (x-axis) for all fibers listed in @em fibers.
 * The bin size is given by the setup parameters @em config. It also
 * specifies the maximum allowed half width of a fiber spectrum.
 *
 * @note
 *  Currently only the line models "psfexp", "psfexp2" and "gaussian"
 *  are supported.
 */

inline static cxint
_giraffe_psf_compute_profile(GiPsfData* result, cpl_image* zraw,
                             cpl_image* zvar, cpl_image* locy,
                             cpl_image* locw, cpl_table* fibers,
                             cpl_image* bpm, GiModel* profile,
                             GiPsfParams* config)
{

    const cxchar* model = NULL;
    const cxchar* ridx = NULL;

    const cxdouble cutoff = log(config->limit);

    cxint nx = 0;
    cxint ny = 0;
    cxint ns = 0;
    cxint fiber = 0;
    cxint nbins = 0;
    cxint nspectra = 0;

    cxsize n = 0;

    cxdouble exponent;    /* PSF profile exponent initial guess */

    cpl_matrix* mx = NULL;
    cpl_matrix* my = NULL;
    cpl_matrix* ms = NULL;

    cpl_image* zx = NULL;
    cpl_image* zv = NULL;

    GiProfileId psfmodel = 0;


    cx_assert(result != NULL);
    cx_assert((zraw != NULL) && (zvar != NULL));
    cx_assert((locy != NULL) && (locw != NULL));
    cx_assert(fibers != NULL);
    cx_assert(profile != NULL);
    cx_assert(config != NULL);

    cx_assert(cpl_image_get_size_x(zraw) == cpl_image_get_size_x(zvar));
    cx_assert(cpl_image_get_size_y(zraw) == cpl_image_get_size_y(zvar));

    cx_assert(cpl_image_get_size_x(locy) == cpl_image_get_size_x(locw));
    cx_assert(cpl_image_get_size_y(locy) == cpl_image_get_size_y(locw));


    nx = cpl_image_get_size_y(zraw);
    ny = cpl_image_get_size_x(zraw);
    ns = cpl_table_get_nrow(fibers);

    nbins = (cxint) giraffe_psfdata_bins(result);

    if (ns != cpl_image_get_size_x(locy)) {
        return -1;
    }

    if ((bpm != NULL) && (cpl_image_get_type(bpm) != CPL_TYPE_INT)) {
        return -2;
    }

    if (giraffe_psfdata_fibers(result) != (cxsize) ns) {
        return -3;
    }

    if ((giraffe_psfdata_xsize(result) != (cxsize) ny) ||
        (giraffe_psfdata_ysize(result) != (cxsize) nx)) {
        return -3;
    }


    /*
     * Check the model type. Only the line models "psfexp", "psfexp2" and
     * "gaussian" are supported.
     */

    model = giraffe_model_get_name(profile);

    if (strcmp(model, "psfexp") == 0) {
        psfmodel = PROFILE_PSFEXP;
    }
    else if (strcmp(model, "psfexp2") == 0) {
        psfmodel = PROFILE_PSFEXP2;
    }
    else if (strcmp(model, "gaussian") == 0) {
        psfmodel = PROFILE_GAUSSIAN;
    }
    else {
        return -4;
    }


    if (config->normalize != FALSE) {

        cxint x = 0;

        cxdouble zmax = 0.;
        cxdouble* zsum = cx_calloc(nx, sizeof(cxdouble));


        /*
         * Find maximum pixel value, taking bad pixels into account if the
         * bad pixel map is present.
         */

        if (bpm == NULL) {

            for (x = 0; x < nx; x++) {

                register cxint y = 0;

                register const cxdouble* _zraw =
                    cpl_image_get_data_double(zraw);


                for (y = 0; y < ny; y++) {
                    zsum[x] += _zraw[x * ny + y];
                }

                if (zsum[x] > zmax) {
                    zmax = zsum[x];
                }
            }

        }
        else {

            for (x = 0; x < nx; x++) {

                register cxint y = 0;
                register const cxint* _bpm = cpl_image_get_data_int(bpm);

                register const cxdouble* _zraw =
                    cpl_image_get_data_double(zraw);


                for (y = 0; y < ny; y++) {
                    register cxint i = x * ny + y;

                    if (_bpm[i] == 0) {
                        zsum[x] += _zraw[i];
                    }
                }

                if (zsum[x] > zmax) {
                    zmax = zsum[x];
                }
            }

        }


        /*
         * Allocate the buffers for the normalized images and scale
         * the fiber spectrum fluxes and errors.
         */

        zx = cpl_image_new(ny, nx, CPL_TYPE_DOUBLE);
        zv = cpl_image_new(ny, nx, CPL_TYPE_DOUBLE);


        for (x = 0; x < nx; x++) {

            register cxint y = 0;

            register cxdouble scale = zmax / zsum[x];
            register const cxdouble* _zraw = cpl_image_get_data_double(zraw);
            register const cxdouble* _zvar = cpl_image_get_data_double(zvar);
            register cxdouble* _zx = cpl_image_get_data_double(zx);
            register cxdouble* _zv = cpl_image_get_data_double(zv);

            for(y = 0; y < nx; y++) {
                register cxint i = x * ny + y;

                _zx[i] = _zraw[i] * scale;
                _zv[i] = _zvar[i] * scale;
            }

        }

        cx_free(zsum);
        zsum = NULL;

    }
    else {
        zx = zraw;
        zv = zvar;
    }


    /*
     * Save the initial values of the profile models exponent parameter,
     * since it must be reset after each bin has been fitted.
     */

    giraffe_error_push();

    exponent = giraffe_model_get_parameter(profile, "Width2");

    if (cpl_error_get_code() != CPL_ERROR_NONE) {
        exponent = 0.;
    }

    giraffe_error_pop();


    /*
     * Get the calibration spectrum reference index column from the
     * fiber list.
     */

    ridx = giraffe_fiberlist_query_index(fibers);


    /*
     * Allocate buffers for the profile data points and their errors. The
     * buffer size is choosen to be large enough for the number of bins of
     * requested psf data object and the given maximum fiber width.
     */

    mx = cpl_matrix_new(nbins * config->mwidth, 1);
    my = cpl_matrix_new(nbins * config->mwidth, 1);
    ms = cpl_matrix_new(nbins * config->mwidth, 1);

    if ((mx == NULL) || (my == NULL) || (ms == NULL)) {
        if (config->normalize == TRUE) {
            cpl_image_delete(zx);
            zx = NULL;

            cpl_image_delete(zv);
            zv = NULL;
        }

        if (mx != NULL) {
            cpl_matrix_delete(mx);
            mx = NULL;
        }

        if (my != NULL) {
            cpl_matrix_delete(my);
            my = NULL;
        }

        if (ms != NULL) {
            cpl_matrix_delete(ms);
            ms = NULL;
        }

        return 1;
    }


    /*
     * Allocate the buffers of the results structure here, to avoid
     * complicated error handling in the nested loops.
     */

    giraffe_psfdata_set_model(result, giraffe_model_get_name(profile));

    for (n = 0; n < giraffe_model_count_parameters(profile); n++) {

        const cxchar* name = giraffe_model_parameter_name(profile, n);

        cpl_image* values = cpl_image_new(ns, nbins, CPL_TYPE_DOUBLE);

        if ((name == NULL) || (values == NULL)) {

            giraffe_psfdata_clear(result);

            cpl_matrix_delete(mx);
            mx = NULL;

            cpl_matrix_delete(my);
            my = NULL;

            cpl_matrix_delete(ms);
            ms = NULL;

            if (config->normalize == TRUE) {
                cpl_image_delete(zx);
                zx = NULL;

                cpl_image_delete(zv);
                zv = NULL;
            }

            return 1;
        }

        giraffe_psfdata_set_data(result, name, values);

    }


    /*
     * Loop over all available fibers
     */

    for (fiber = 0; fiber < ns; fiber++) {

        cxint x = 0;
        cxint bin = 0;
        cxint cs = cpl_table_get_int(fibers, ridx, fiber, NULL) - 1;
        const cxint* _bpm = NULL;

        const cxdouble* _locy = cpl_image_get_data_double(locy);
        const cxdouble* _locw = cpl_image_get_data_double(locw);
        const cxdouble* _zx = cpl_image_get_data_double(zx);
        const cxdouble* _zv = cpl_image_get_data_double(zv);


        if (bpm != NULL) {
            _bpm = cpl_image_get_data_int(bpm);
        }


        /*
         * Fit a profile for each bin
         */

        for (x = 0, bin = 0; x < nx; x += config->bsize, bin++) {

            register cxint k = 0;
            register cxint xx = 0;

            cxint status = 0;
            cxint ndata = 0;
            cxint iterations = giraffe_model_get_iterations(profile);

            cxdouble amplitude = 0.;
            cxdouble bckground = 0.;
            cxdouble center = 0.;
            cxdouble width1 = 0.;
            cxdouble width2 = 0.;

            GiPsfBin xbin = {0., 0., 0., 0., 0.};


            /*
             * Loop over each element of this bin
             */

            for (k = 0, xx = x; (k < config->bsize) && (xx < nx); k++, xx++) {

                register cxint y = 0;
                register cxint l = xx * ns + cs;
                register cxint m = xx * ny;

                cxdouble zxmin = CX_MAXDOUBLE;
                cxdouble zxmax = 0.;
                cxdouble swidth = CX_MIN(_locw[l], config->mwidth);
                cxdouble ylo = (cxint) floor(_locy[l] - swidth);
                cxdouble yup = (cxint) ceil(_locy[l] + swidth);
                cxdouble ycenter = _locy[l];


                ylo = CX_MAX(0., ylo);
                yup = CX_MIN(ny, yup);

                if (_bpm == NULL) {

                    for (y = ylo; y < yup; y++) {

                        register cxint i = m + y;

                        cpl_matrix_set(mx, ndata, 0, (cxdouble)y - ycenter);
                        cpl_matrix_set(my, ndata, 0, _zx[i]);
                        cpl_matrix_set(ms, ndata, 0, sqrt(_zv[i]));

                        if (_zx[i] > zxmax) {
                            zxmax = _zx[i];
                        }

                        if (_zx[i] < zxmin) {
                            zxmin = _zx[i];
                        }

                        ++ndata;

                    }

                }
                else {

                    for (y = ylo; y < yup; y++) {

                        register cxint i = m + y;

                        if (_bpm[i] == 0) {
                            cpl_matrix_set(mx, ndata, 0,
                                           (cxdouble)y - ycenter);
                            cpl_matrix_set(my, ndata, 0, _zx[i]);
                            cpl_matrix_set(ms, ndata, 0, sqrt(_zv[i]));

                            if (_zx[i] > zxmax) {
                                zxmax = _zx[i];
                            }

                            if (_zx[i] < zxmin) {
                                zxmin = _zx[i];
                            }

                            ++ndata;
                        }

                    }

                }

                xbin.zmin += zxmin;
                xbin.zmax += zxmax;
                xbin.xcenter += xx;
                xbin.ycenter += ycenter;
                xbin.ywidth += swidth;

            }


            /*
             * Compute per bin average values
             */

            xbin.zmin /= k;
            xbin.zmax /= k;
            xbin.xcenter /= k;
            xbin.ycenter /= k;
            xbin.ywidth /= k;


            /*
             * Avoid negative background values
             */

            xbin.zmin = CX_MAX(0., xbin.zmin);


            /*
             * Setup model for this bin
             */

            giraffe_model_set_parameter(profile, "Amplitude",
                                        xbin.zmax - xbin.zmin);
            giraffe_model_set_parameter(profile, "Center", 0.);
            giraffe_model_set_parameter(profile, "Background", xbin.zmin);

            switch (psfmodel) {
            case PROFILE_PSFEXP:
                width1 = pow(xbin.ywidth, exponent) / cutoff;
                giraffe_model_set_parameter(profile, "Width2", exponent);
                break;

            case PROFILE_PSFEXP2:
                width1 = xbin.ywidth / pow(cutoff, 1. / exponent);
                giraffe_model_set_parameter(profile, "Width2", exponent);
                break;

            case PROFILE_GAUSSIAN:
                width1 = xbin.ywidth / pow(cutoff, 0.5);
                break;

            default:
                break;
            }

            giraffe_model_set_parameter(profile, "Width1", width1);


            /*
             * Fit the profile
             */

            status = giraffe_model_fit_sequence(profile, mx, my, ms,
                                                ndata, 0, 1);

            amplitude = giraffe_model_get_parameter(profile, "Amplitude");
            bckground = giraffe_model_get_parameter(profile, "Background");
            center = giraffe_model_get_parameter(profile, "Center");
            width1 = giraffe_model_get_parameter(profile, "Width1");

            if ((psfmodel == PROFILE_PSFEXP) ||
                (psfmodel == PROFILE_PSFEXP2)) {
                width2 = giraffe_model_get_parameter(profile, "Width2");
            }

            /*
             * Check fit results. The fit failed, if the maximum
             * number of iterations has been reached, fitted amplitude
             * is negative or the fitted width is negative.
             */

            if ((status != 0) ||
                (giraffe_model_get_position(profile) >= iterations) ||
                (amplitude <= 0.) ||
                (width1 <= 0.)) {

                xbin.xcenter = -1.;
                amplitude = 0.;
                bckground = 0.;
                center = 0.;
                width1 = 0.;
                width2 = 0.;

            }

            giraffe_psfdata_set_bin(result, fiber, bin, xbin.xcenter);

            giraffe_psfdata_set(result, "Amplitude", fiber, bin, amplitude);
            giraffe_psfdata_set(result, "Center", fiber, bin,
                                xbin.ycenter + center);
            giraffe_psfdata_set(result, "Background", fiber, bin, bckground);
            giraffe_psfdata_set(result, "Width1", fiber, bin, width1);

            if ((psfmodel == PROFILE_PSFEXP) ||
                (psfmodel == PROFILE_PSFEXP2)) {
                giraffe_psfdata_set(result, "Width2", fiber, bin, width2);
            }

        }

        ++nspectra;

    }


    /*
     * Cleanup
     */

    cpl_matrix_delete(mx);
    mx = NULL;

    cpl_matrix_delete(my);
    my = NULL;

    cpl_matrix_delete(ms);
    ms = NULL;

    if (config->normalize == TRUE) {
        cpl_image_delete(zx);
        zx = NULL;

        cpl_image_delete(zv);
        zv = NULL;
    }

    return 0;

}


/*
 * @brief
 *  Fit a PSF profile model to each fiber spectrum.
 *
 * @param result    Fitted PSF profile model parameters.
 * @param zraw      Raw image of the fiber spectra.
 * @param zvar      Raw image of flux errors.
 * @param locy      Fiber centroid position.
 * @param locw      Fiber width.
 * @param fibers    List of fibers to process.
 * @param bpm       Optional bad pixel map.
 * @param config    PSF profile fit setup parameters.
 *
 * @return
 *  The function returns @c 0 on success, or a non-zero value otherwise.
 *
 * The function fits a profile model given by @em psfmodel to each bin along
 * the dispersion direction (x-axis) for all fibers listed in @em fibers.
 * The bin size is given by the setup parameters @em config. It also
 * specifies the maximum allowed half width of a fiber spectrum.
 *
 * @note
 *  Currently only the line models "psfexp", "psfexp2" and "gaussian"
 *  are supported.
 */

inline static cxint
_giraffe_psf_refine_profile(GiPsfData* result, const GiPsfData* psfdata,
                            cpl_image* zraw, cpl_image* zvar,
                            cpl_table* fibers, cpl_image* bpm,
                            GiModel* profile, GiPsfParams* config)
{

    const cxchar* model = NULL;

    const cxdouble cutoff = log(config->limit);

    cxint nx = 0;
    cxint ny = 0;
    cxint ns = 0;
    cxint fiber = 0;
    cxint nbins = 0;
    cxint nspectra = 0;
    cxint binsize = 0;

    cxsize n = 0;

    cpl_matrix* mx = NULL;
    cpl_matrix* my = NULL;
    cpl_matrix* ms = NULL;

    GiProfileId psfmodel = 0;


    cx_assert(result != NULL);
    cx_assert(psfdata != NULL);
    cx_assert((zraw != NULL) && (zvar != NULL));
    cx_assert(fibers != NULL);
    cx_assert(profile != NULL);
    cx_assert(config != NULL);

    cx_assert(cpl_image_get_size_x(zraw) == cpl_image_get_size_x(zvar));
    cx_assert(cpl_image_get_size_y(zraw) == cpl_image_get_size_y(zvar));


    nx = cpl_image_get_size_y(zraw);
    ny = cpl_image_get_size_x(zraw);
    ns = cpl_table_get_nrow(fibers);

    if ((bpm != NULL) && (cpl_image_get_type(bpm) != CPL_TYPE_INT)) {
        return -1;
    }

    if ((giraffe_psfdata_fibers(result) != (cxsize) ns) ||
        (giraffe_psfdata_bins(result) != (cxsize) nx)) {
        return -2;
    }

    if ((giraffe_psfdata_xsize(result) != (cxsize) ny) ||
        (giraffe_psfdata_ysize(result) != (cxsize) nx)) {
        return -2;
    }

    nbins = giraffe_psfdata_bins(result);

    if ((giraffe_psfdata_fibers(psfdata) != (cxsize) ns)) {
        return -3;
    }

    if ((giraffe_psfdata_xsize(psfdata) != (cxsize) ny) ||
        (giraffe_psfdata_ysize(psfdata) != (cxsize) nx)) {
        return -3;
    }

    binsize = nx / giraffe_psfdata_bins(psfdata);


    /*
     * Check the model type. Only the line models "psfexp", "psfexp2" and
     * "gaussian" are supported.
     */

     model = giraffe_model_get_name(profile);

     if (strcmp(model, "psfexp") == 0) {
         psfmodel = PROFILE_PSFEXP;
     }
     else if (strcmp(model, "psfexp2") == 0) {
         psfmodel = PROFILE_PSFEXP2;
     }
     else if (strcmp(model, "gaussian") == 0) {
         psfmodel = PROFILE_GAUSSIAN;
     }
     else {
         return -4;
     }


    /*
     * Allocate buffers for the profile data points and their errors. The
     * buffer size is choosen to be large enough for the number of bins of
     * requested psf data object and the given maximum fiber width.
     */

     mx = cpl_matrix_new(nbins * config->mwidth, 1);
     my = cpl_matrix_new(nbins * config->mwidth, 1);
     ms = cpl_matrix_new(nbins * config->mwidth, 1);

     if ((mx == NULL) || (my == NULL) || (ms == NULL)) {

         if (mx != NULL) {
             cpl_matrix_delete(mx);
             mx = NULL;
         }

         if (my != NULL) {
             cpl_matrix_delete(my);
             my = NULL;
         }

         if (ms != NULL) {
             cpl_matrix_delete(ms);
             ms = NULL;
         }

         return 1;

     }


    /*
     * Allocate the buffers of the results structure here, to avoid
     * complicated error handling in the nested loops.
     */

    giraffe_psfdata_set_model(result, giraffe_model_get_name(profile));

    for (n = 0; n < giraffe_model_count_parameters(profile); n++) {

        const cxchar* name = giraffe_model_parameter_name(profile, n);

        cpl_image* values = cpl_image_new(ns, nbins, CPL_TYPE_DOUBLE);


        if ((name == NULL) || (values == NULL)) {

            giraffe_psfdata_clear(result);

            cpl_matrix_delete(mx);
            mx = NULL;

            cpl_matrix_delete(my);
            my = NULL;

            cpl_matrix_delete(ms);
            ms = NULL;

            return 1;

        }

        giraffe_psfdata_set_data(result, name, values);

    }


    /*
     * Loop over all available fibers
     */

    for (fiber = 0; fiber < ns; fiber++) {

        cxint x = 0;
        const cxint* _bpm = NULL;

        const cxdouble* _zx = cpl_image_get_data_double(zraw);
        const cxdouble* _zv = cpl_image_get_data_double(zvar);


        if (bpm != NULL) {
            _bpm = cpl_image_get_data_int(bpm);
        }


        /*
         * Fit a profile for each bin
         */

        for (x = 0; x < nx; x++) {

            register cxint y = 0;
            register cxint m = x * ny;
            register cxint bin = CX_MAX(0, CX_MIN((cxint) floor(x / binsize),
                                        nbins));

            cxint status = 0;
            cxint ndata = 0;
            cxint iterations = giraffe_model_get_iterations(profile);

            cxdouble xcenter = 0.;
            cxdouble ycenter = 0.;
            cxdouble swidth = 0.;
            cxdouble ylo = 0.;
            cxdouble yup = ny;
            cxdouble amplitude = giraffe_psfdata_get(psfdata, "Amplitude",
                                                     fiber, bin);
            cxdouble bckground = giraffe_psfdata_get(psfdata, "Background",
                                                     fiber, bin);
            cxdouble center = giraffe_psfdata_get(psfdata, "Center",
                                                  fiber, bin);
            cxdouble width1 = giraffe_psfdata_get(psfdata, "Width1",
                                                  fiber, bin);
            cxdouble width2 = 0.;


            switch (psfmodel) {
                case PROFILE_PSFEXP:
                    width2 = giraffe_psfdata_get(psfdata, "Width2",
                                                 fiber, bin);
                    swidth = pow(width1 * cutoff, 1./ width2);
                    break;

                case PROFILE_PSFEXP2:
                    width2 = giraffe_psfdata_get(psfdata, "Width2",
                                                 fiber, bin);
                    swidth = width1 * pow(cutoff, 1./ width2);
                    break;

                case PROFILE_GAUSSIAN:
                    swidth = width1 * pow(cutoff, 0.5);
                    break;

                default:
                    break;
            }

            swidth = CX_MIN(swidth, config->mwidth);

            ylo = (cxint) floor(center - swidth);
            ylo = CX_MAX(0., ylo);

            yup = (cxint) ceil(center + swidth);
            yup = CX_MIN(ny, yup);

            if (_bpm == NULL) {

                for (y = ylo; y < yup; y++) {

                    register cxint i = m + y;

                    cpl_matrix_set(mx, ndata, 0, (cxdouble)y - center);
                    cpl_matrix_set(my, ndata, 0, _zx[i]);
                    cpl_matrix_set(ms, ndata, 0, sqrt(_zv[i]));

                    ++ndata;

                }

            }
            else {

                for (y = ylo; y < yup; y++) {

                    register cxint i = m + y;

                    if (_bpm[i] == 0) {
                        cpl_matrix_set(mx, ndata, 0, (cxdouble)y - center);
                        cpl_matrix_set(my, ndata, 0, _zx[i]);
                        cpl_matrix_set(ms, ndata, 0, sqrt(_zv[i]));

                        ++ndata;
                    }

                }

            }


            /*
             * Avoid negative background values
             */

            bckground = CX_MAX(0., bckground);


            /*
             * Setup model for this bin
             */

            giraffe_model_set_parameter(profile, "Amplitude", amplitude);
            giraffe_model_set_parameter(profile, "Center", 0.);
            giraffe_model_set_parameter(profile, "Background", bckground);
            giraffe_model_set_parameter(profile, "Width1", width1);

            switch (psfmodel) {
                case PROFILE_PSFEXP:
                    giraffe_model_set_parameter(profile, "Width2", width2);
                    break;

                case PROFILE_PSFEXP2:
                    giraffe_model_set_parameter(profile, "Width2", width2);
                    break;

                case PROFILE_GAUSSIAN:
                    break;

                default:
                    break;
            }


            /*
             * Fit the profile
             */

            status = giraffe_model_fit_sequence(profile, mx, my, ms,
                                                ndata, 0, 1);

            amplitude = giraffe_model_get_parameter(profile, "Amplitude");
            bckground = giraffe_model_get_parameter(profile, "Background");
            ycenter = giraffe_model_get_parameter(profile, "Center");
            width1 = giraffe_model_get_parameter(profile, "Width1");

            if ((psfmodel == PROFILE_PSFEXP) ||
                (psfmodel == PROFILE_PSFEXP2)) {
                width2 = giraffe_model_get_parameter(profile, "Width2");
            }


            /*
             * Check fit results. The fit failed, if the maximum
             * number of iterations has been reached, fitted amplitude
             * is negative or the fitted width is negative.
             */

            if ((status != 0) ||
                (giraffe_model_get_position(profile) >= iterations) ||
                (amplitude <= 0.) ||
                (width1 <= 0.)) {

                xcenter = -1.;
                ycenter = 0.;
                amplitude = 0.;
                bckground = 0.;
                width1 = 0.;
                width2 = 0.;

            }
            else {
                xcenter = x;
            }

            giraffe_psfdata_set_bin(result, fiber, x, xcenter);

            giraffe_psfdata_set(result, "Amplitude", fiber, x, amplitude);
            giraffe_psfdata_set(result, "Center", fiber, x,
                                ycenter + center);
            giraffe_psfdata_set(result, "Background", fiber, x, bckground);
            giraffe_psfdata_set(result, "Width1", fiber, x, width1);

            if ((psfmodel == PROFILE_PSFEXP) ||
                (psfmodel == PROFILE_PSFEXP2)) {
                giraffe_psfdata_set(result, "Width2", fiber, x, width2);
            }

        }

        ++nspectra;

    }


    /*
     * Cleanup
     */

    cpl_matrix_delete(mx);
    mx = NULL;

    cpl_matrix_delete(my);
    my = NULL;

    cpl_matrix_delete(ms);
    ms = NULL;

    return 0;

}


/*
 * @brief
 *  Compute a one dimensional fit of all PSF profile model parameters for
 *  the given grid.
 *
 * @param psfdata  PSF profile parameters to be fitted.
 * @param fibers   The list of fibers for which a PSF profile is available.
 * @param order    Order of the Chebyshev polynomial to fit.
 * @param setup    Sigma clipping algorithm configuration object.
 *
 * @return
 *  The function returns a newly created PSF data object containing the
 *  fitted parameters on success, or @c NULL if an error occurrs.
 *
 * TBD
 */

inline static GiPsfData*
_giraffe_psf_fit_parameters1d(const GiPsfData* psfdata,
                              const cpl_table* fibers,
                              const cxchar** names,
                              cxint order,
                              const GiClipParams* setup)
{

    cxint i = 0;
    cxint ns = 0;
    cxint nx = 0;
    cxint ny = 0;
    cxint status = 0;

    GiPsfData* psffit = NULL;


    cx_assert(psfdata != NULL);
    cx_assert(fibers != NULL);
    cx_assert(setup != NULL);

    ns = giraffe_psfdata_fibers(psfdata);
    nx = giraffe_psfdata_ysize(psfdata);
    ny = giraffe_psfdata_xsize(psfdata);

    psffit = giraffe_psfdata_create(ns, nx, ny, nx);

    giraffe_psfdata_set_model(psffit, giraffe_psfdata_get_model(psfdata));


    /*
     * Create the bin data for the fitted PSF parameter data. This
     * is actually not needed, but makes psffit a complete, valid
     * PSF data object.
     */

    for (i = 0; i < ns; i++) {

        register cxint j = 0;

        for (j = 0; j < nx; j++) {
            giraffe_psfdata_set_bin(psffit, i, j, j);
        }

    }


    if (names == NULL) {

        cxsize j = 0;
        cxsize count = giraffe_psfdata_parameters(psfdata);

        for (j = 0; j < count; j++) {

            const cxchar* name = giraffe_psfdata_get_name(psfdata, j);

            GiPsfParameterFit pfit = {NULL, NULL};


            pfit.fit = cpl_image_new(ns, nx, CPL_TYPE_DOUBLE);
            pfit.coeffs = cpl_matrix_new(order + 1, ns);

            status = _giraffe_psf_fit_profile1d(&pfit, psfdata, name,
                                                fibers, order, setup);

            if (status != 0) {
                cpl_matrix_delete(pfit.coeffs);
                pfit.coeffs = NULL;

                cpl_image_delete(pfit.fit);
                pfit.fit = NULL;

                giraffe_psfdata_delete(psffit);
                psffit = NULL;

                return NULL;
            }
            else {
                giraffe_psfdata_set_data(psffit, name, pfit.fit);
                pfit.fit = NULL;

                cpl_matrix_delete(pfit.coeffs);
                pfit.coeffs = NULL;

            }

        }

    }
    else {

        /*
         * For each PSF parameter, whose name is listed in name and present in
         * the PSF data object, a one dimensional polynomial model is created.
         */

        i = 0;
        while (names[i] != NULL) {

            if (giraffe_psfdata_contains(psfdata, names[i]) == TRUE) {

                GiPsfParameterFit pfit = {NULL, NULL};


                pfit.fit = cpl_image_new(ns, nx, CPL_TYPE_DOUBLE);
                pfit.coeffs = cpl_matrix_new(order + 1, ns);

                status = _giraffe_psf_fit_profile1d(&pfit, psfdata, names[i],
                                                    fibers, order, setup);

                if (status != 0) {
                    cpl_matrix_delete(pfit.coeffs);
                    pfit.coeffs = NULL;

                    cpl_image_delete(pfit.fit);
                    pfit.fit = NULL;

                    giraffe_psfdata_delete(psffit);
                    psffit = NULL;

                    return NULL;
                }
                else {
                    giraffe_psfdata_set_data(psffit, names[i], pfit.fit);
                    pfit.fit = NULL;

                    cpl_matrix_delete(pfit.coeffs);
                    pfit.coeffs = NULL;

                }

            }

            ++i;

        }

    }

    return psffit;

}


/*
 * @brief
 *  Compute the fit of all PSF profile model parameters for the given grid.
 *
 * @param result  Container for the fitted PSF parameters.
 *
 * @return
 *  The function returns 0 on success, or a non-zero value otherwise.
 *
 * TBD
 */

inline static GiPsfData*
_giraffe_psf_fit_parameters(const GiPsfData* psfdata,
                            const cpl_table* fibers,
                            const cxchar** names,
                            cxint yorder, cxint worder,
                            const GiClipParams* setup)
{

    const cxchar* center = NULL;

    cxint i = 0;
    cxint ns = 0;
    cxint nx = 0;
    cxint ny = 0;
    cxint status = 0;

    GiPsfData* psffit = NULL;


    cx_assert(psfdata != NULL);
    cx_assert(fibers != NULL);
    cx_assert(names != NULL);
    cx_assert(setup != NULL);

    ns = giraffe_psfdata_fibers(psfdata);
    nx = giraffe_psfdata_ysize(psfdata);
    ny = giraffe_psfdata_xsize(psfdata);

    psffit = giraffe_psfdata_create(ns, nx, ny, nx);

    giraffe_psfdata_set_model(psffit, giraffe_psfdata_get_model(psfdata));


    /*
     * Create the bin data for the fitted PSF parameter data. This
     * is actually not needed, but makes psffit a complete, valid
     * PSF data object.
     */

    for (i = 0; i < ns; i++) {

        register cxint j = 0;

        for (j = 0; j < nx; j++) {
            giraffe_psfdata_set_bin(psffit, i, j, j);
        }

    }

    center = names[0];
    if (giraffe_psfdata_contains(psfdata, center) == FALSE) {

        giraffe_psfdata_delete(psffit);
        psffit = NULL;

        return NULL;

    }
    else {

        GiPsfParameterFit pfit = {NULL, NULL};


        pfit.fit = cpl_image_new(ns, nx, CPL_TYPE_DOUBLE);
        pfit.coeffs = cpl_matrix_new(yorder + 1, ns);

        status = _giraffe_psf_fit_profile1d(&pfit, psfdata, center, fibers,
                                            yorder, setup);

        if (status != 0) {
            cpl_matrix_delete(pfit.coeffs);
            pfit.coeffs = NULL;

            cpl_image_delete(pfit.fit);
            pfit.fit = NULL;

            giraffe_psfdata_delete(psffit);
            psffit = NULL;

            return NULL;
        }
        else {
            giraffe_psfdata_set_data(psffit, center, pfit.fit);
            pfit.fit = NULL;

            cpl_matrix_delete(pfit.coeffs);
            pfit.coeffs = NULL;

        }

    }


    i = 1;
    while (names[i] != NULL) {

        if (giraffe_psfdata_contains(psfdata, names[i]) == TRUE) {

            const cpl_image* xbin = giraffe_psfdata_get_bins(psfdata);
            const cpl_image* ybin = giraffe_psfdata_get_data(psfdata, center);
            const cpl_image* yfit = giraffe_psfdata_get_data(psffit, center);
            const cpl_image* pdata = giraffe_psfdata_get_data(psfdata,
                                                              names[i]);

            GiPsfParameterFit pfit = {NULL, NULL};


            pfit.fit = cpl_image_new(ns, nx, CPL_TYPE_DOUBLE);
            pfit.coeffs = cpl_matrix_new(yorder + 1, worder + 1);

            status = _giraffe_psf_fit_profile2d(&pfit, fibers, pdata, xbin,
                                                ybin, yorder, worder, yfit,
                                                0, ny, setup);

            if (status != 0) {
                cpl_matrix_delete(pfit.coeffs);
                pfit.coeffs = NULL;

                cpl_image_delete(pfit.fit);
                pfit.fit = NULL;

                giraffe_psfdata_delete(psffit);
                psffit = NULL;

                return NULL;
            }
            else {
                giraffe_psfdata_set_data(psffit, names[i], pfit.fit);
                pfit.fit = NULL;

                cpl_matrix_delete(pfit.coeffs);
                pfit.coeffs = NULL;

            }
        }

        ++i;

    }

    return psffit;

}


/*
 * @brief
 *  Compute a localization mask and coefficients from the PSF profile
 *  parameters.
 *
 * @return
 *  The function returns 0 on success, or a non-zero value otherwise.
 *
 * TBD
 */

inline static int
_giraffe_psf_compute_mask(GiMaskPosition* positions, GiMaskPosition* coeffs,
                          const GiPsfData* psfdata, const cpl_table* fibers,
                          cxint yorder, cxint worder,
                          const GiClipParams* setup)
{

    const cxchar* const lcenter = "Center";
    const cxchar* const lwidth = "Width1";
    const cxchar* const lexponent = "Width2";
    const cxchar* model = NULL;

    cxint i = 0;
    cxint ns = 0;
    cxint nx = 0;
    cxint ny = 0;
    cxint status = 0;

    const cpl_image* xbin = NULL;
    const cpl_image* ybin = NULL;
    cpl_image* width = NULL;

    GiPsfParameterFit center = {NULL, NULL};
    GiPsfParameterFit halfwidth = {NULL, NULL};

    GiProfileId psfmodel = 0;


    cx_assert((positions != NULL) &&
              (positions->type == GIMASK_FITTED_DATA) &&
              (positions->my != NULL) &&
              (positions->mw != NULL));
    cx_assert((coeffs != NULL) &&
              (coeffs->type == GIMASK_FIT_COEFFS) &&
              (coeffs->my != NULL) &&
              (coeffs->mw != NULL));
    cx_assert(psfdata != NULL);
    cx_assert(fibers != NULL);
    cx_assert(setup != NULL);

    model = giraffe_psfdata_get_model(psfdata);

    if (strcmp(model, "psfexp") == 0) {
        psfmodel = PROFILE_PSFEXP;
    }
    else if (strcmp(model, "psfexp2") == 0) {
        psfmodel = PROFILE_PSFEXP2;
    }
    else if (strcmp(model, "gaussian") == 0) {
        psfmodel = PROFILE_GAUSSIAN;
    }
    else {
        return -1;
    }

    ns = giraffe_psfdata_fibers(psfdata);
    nx = giraffe_psfdata_ysize(psfdata);
    ny = giraffe_psfdata_xsize(psfdata);

    if ((cpl_matrix_get_nrow(positions->my) != nx) ||
        (cpl_matrix_get_ncol(positions->my) != ns) ||
        (cpl_matrix_get_nrow(positions->mw) != nx) ||
        (cpl_matrix_get_ncol(positions->mw) != ns)) {
            return -1;
    }

    if ((cpl_matrix_get_nrow(coeffs->my) != yorder + 1) ||
        (cpl_matrix_get_ncol(coeffs->my) != ns)) {
        return -1;
    }

    if ((cpl_matrix_get_nrow(coeffs->mw) != worder + 1) ||
        (cpl_matrix_get_ncol(coeffs->mw) != worder + 1)) {
            return -1;
    }

    if (giraffe_psfdata_contains(psfdata, lcenter) == FALSE ||
        giraffe_psfdata_contains(psfdata, lwidth) == FALSE) {
        return 1;
    }

    center.fit = cpl_image_wrap_double(ns, nx,
                                       cpl_matrix_get_data(positions->my));
    center.coeffs = coeffs->my;

    status = _giraffe_psf_fit_profile1d(&center, psfdata, lcenter, fibers,
                                        yorder, setup);

    if (status != 0) {
        cpl_image_unwrap(center.fit);

        center.fit = NULL;
        center.coeffs = NULL;

        return 1;
    }

    width = cpl_image_new(ns, nx, CPL_TYPE_DOUBLE);

    switch (psfmodel) {
    case PROFILE_PSFEXP:
        {

            const cxdouble LOG2 = log(2.);

            if (giraffe_psfdata_contains(psfdata, lexponent) == FALSE) {
                cpl_image_delete(width);
                width = NULL;

                cpl_image_unwrap(center.fit);
                center.fit = NULL;
                center.coeffs = NULL;

                return 1;
            }

            for (i = 0; i < ns; i++) {

                register cxint j = 0;

                register cxdouble* _width = cpl_image_get_data_double(width);


                for (j = 0; j < nx; j++) {

                    register cxint k = j * ns + i;

                    cxdouble width1 =
                        giraffe_psfdata_get(psfdata, lwidth, i, j);
                    cxdouble width2 =
                        giraffe_psfdata_get(psfdata, lexponent, i, j);


                    _width[k] = 2. * pow(LOG2 * width1, 1. / width2);

                }

            }

        }
        break;

    case PROFILE_PSFEXP2:
        {

            const cxdouble LOG2 = log(2.);

            if (giraffe_psfdata_contains(psfdata, lexponent) == FALSE) {
                cpl_image_delete(width);
                width = NULL;

                cpl_image_unwrap(center.fit);
                center.fit = NULL;
                center.coeffs = NULL;

                return 1;
            }

            for (i = 0; i < ns; i++) {

                register cxint j = 0;

                register cxdouble* _width = cpl_image_get_data_double(width);


                for (j = 0; j < nx; j++) {

                    register cxint k = j * ns + i;

                    cxdouble width1 =
                        giraffe_psfdata_get(psfdata, lwidth, i, j);
                    cxdouble width2 =
                        giraffe_psfdata_get(psfdata, lexponent, i, j);


                    _width[k] = 2. * pow(LOG2, 1. / width2) * width1;

                }

            }

        }
        break;

    case PROFILE_GAUSSIAN:
        {

            const cxdouble fwhmscale = 4. * sqrt(2. * log(2.));

            for (i = 0; i < ns; i++) {

                register cxint j = 0;

                register cxdouble* _width = cpl_image_get_data_double(width);


                for (j = 0; j < nx; j++) {

                    register cxint k = j * ns + i;

                    _width[k] = fwhmscale *
                        giraffe_psfdata_get(psfdata, lwidth, i, j);
                }

            }

        }
        break;

    default:
        /* This point should never be reached! */

        cpl_image_delete(width);
        width = NULL;

        cpl_image_unwrap(center.fit);
        center.fit = NULL;
        center.coeffs = NULL;

        gi_error("Unsupported PSF profile model encountered!");
        break;
    }


    xbin = giraffe_psfdata_get_bins(psfdata);
    ybin = giraffe_psfdata_get_data(psfdata, lcenter);


    halfwidth.fit = cpl_image_wrap_double(ns, nx,
                                          cpl_matrix_get_data(positions->mw));
    halfwidth.coeffs = coeffs->mw;

    status = _giraffe_psf_fit_profile2d(&halfwidth, fibers, width, xbin,
                                        ybin, worder, worder, center.fit,
                                        0, ny, setup);

    if (status != 0) {
        cpl_image_unwrap(halfwidth.fit);
        halfwidth.fit = NULL;
        halfwidth.coeffs = NULL;

        cpl_image_delete(width);
        width = NULL;

        cpl_image_unwrap(center.fit);
        center.fit = NULL;
        center.coeffs = NULL;

        return 1;
    }

    cpl_image_unwrap(halfwidth.fit);
    halfwidth.fit = NULL;
    halfwidth.coeffs = NULL;

    cpl_image_delete(width);
    width = NULL;

    cpl_image_unwrap(center.fit);
    center.fit = NULL;
    center.coeffs = NULL;

    return 0;

}


inline static cpl_image*
_giraffe_psf_simulate_mask(const GiPsfData* psfdata,
                           const cpl_image* amplitude,
                           const cpl_image* background,
                           cxdouble cutoff)
{

    const cxchar* model = NULL;

    cxint i = 0;
    cxint nfibers = 0;
    cxint nbins = 0;
    cxint _nbins = 0;
    cxint nx = 0;
    cxint ny = 0;
    cxint fiber = 0;

    cxdouble bsize = 1.;
    cxdouble _bsize = 1.;
    cxdouble* _mask = NULL;

    const cpl_image* center = NULL;
    const cpl_image* width = NULL;
    const cpl_image* exponent = NULL;

    cpl_image* mask = NULL;

    GiModel* profile = NULL;

    GiProfileId psfmodel = 0;


    cx_assert(psfdata != NULL);

    model = giraffe_psfdata_get_model(psfdata);

    if (strcmp(model, "psfexp") == 0) {
        psfmodel = PROFILE_PSFEXP;
    }
    else if (strcmp(model, "psfexp2") == 0) {
        psfmodel = PROFILE_PSFEXP2;
    }
    else if (strcmp(model, "gaussian") == 0) {
        psfmodel = PROFILE_GAUSSIAN;
    }
    else {
        return NULL;
    }

    nfibers = giraffe_psfdata_fibers(psfdata);
    nbins = giraffe_psfdata_bins(psfdata);
    nx = giraffe_psfdata_ysize(psfdata);
    ny = giraffe_psfdata_xsize(psfdata);

    center = giraffe_psfdata_get_data(psfdata, "Center");
    width = giraffe_psfdata_get_data(psfdata, "Width1");
    exponent = giraffe_psfdata_get_data(psfdata, "Width2");

    if (amplitude == NULL) {
        amplitude = giraffe_psfdata_get_data(psfdata, "Amplitude");
    }
    else {
        if ((cpl_image_get_size_x(amplitude) != nfibers) ||
            (cpl_image_get_size_y(amplitude) > nbins)) {
                return NULL;
        }
    }

    if (background == NULL) {
        background = giraffe_psfdata_get_data(psfdata, "Background");
    }
    else {
        if ((cpl_image_get_size_x(background) != nfibers) ||
            (cpl_image_get_size_y(background) > nbins)) {
                return NULL;
        }
    }

    bsize = (cxdouble)nx / (cxdouble)nbins;

    _nbins = cpl_image_get_size_y(amplitude);
    _bsize = (cxdouble)nx / (cxdouble)_nbins;

    mask = cpl_image_new(ny, nx, CPL_TYPE_DOUBLE);
    _mask = cpl_image_get_data_double(mask);

    profile = giraffe_model_new(model);

    for (fiber = 0; fiber < nfibers; fiber++) {

        cxint ylower = 0;
        cxint yupper = ny;

        const cxdouble* _amplitude =
            cpl_image_get_data_double_const(amplitude);
        const cxdouble* _center =
            cpl_image_get_data_double_const(center);
        const cxdouble* _width =
            cpl_image_get_data_double_const(width);
        const cxdouble* _exponent =
            cpl_image_get_data_double_const(exponent);

        for (i = 0; i < nx; i++) {

            register cxint j = 0;
            register cxint k = 0;
            register cxint l = 0;
            register cxint bin = 0;

            register cxdouble a = 1.;
            register cxdouble b = 0.;
            register cxdouble c = 0.;
            register cxdouble s = 0.;
            register cxdouble e = 0.;


            bin = CX_MAX(0, CX_MIN((cxint)floor(i / bsize), nbins - 1));
            k = bin * nfibers + fiber;

            bin = CX_MAX(0, CX_MIN((cxint)floor(i / _bsize), _nbins - 1));
            l = bin * nfibers + fiber;

            a = _amplitude[l];
            c = _center[k];
            s = _width[k];
            e = _exponent[k];

            giraffe_model_set_parameter(profile, "Amplitude", a);
            giraffe_model_set_parameter(profile, "Background", b);
            giraffe_model_set_parameter(profile, "Center", c);
            giraffe_model_set_parameter(profile, "Width1", s);
            giraffe_model_set_parameter(profile, "Width2", e);

            switch (psfmodel) {
            case PROFILE_PSFEXP:
                {
                    cxdouble w = pow(s * log(1. / cutoff), 1. / e);

                    ylower = (cxint) floor(c - w);
                    yupper = (cxint) ceil(c + w);
                }
                break;

            case PROFILE_PSFEXP2:
                {
                    cxdouble w = s * pow(log(1. / cutoff), 1. / e);

                    ylower = (cxint) floor(c - w);
                    yupper = (cxint) ceil(c + w);
                }
                break;

            case PROFILE_GAUSSIAN:
                {
                    cxdouble w = s * sqrt(log(1. / cutoff));
                    ylower = (cxint) floor(c - w);
                    yupper = (cxint) ceil(c + w);
                }
                break;

            default:
                gi_error("Unsupported PSF profile model encountered!");
                break;
            }

            ylower = CX_MAX(0, ylower);
            yupper = CX_MIN(ny, yupper);

            for (j = ylower; j < yupper; j++) {

                cxint status = 0;

                cxdouble value = 0.;

                // FIXME: Performance problem? Check this!
                //register cxdouble value =
                //    a * exp(-pow(fabs(j - c) / s, e)) + b;

                giraffe_model_set_argument(profile, "x", j);
                giraffe_model_evaluate(profile, &value, &status);

                _mask[i * ny + j] += value;

            }

        }

    }

    giraffe_model_delete(profile);
    profile = NULL;

    return mask;

}


/**
 * @brief
 *  Compute the position and width of the spectra from the fiber profile.
 *
 * @return
 *  The function returns 0 on success, or a non-zero value otherwise.
 *
 * TBD
 */

cxint
giraffe_compute_fiber_profiles(GiLocalization* result, GiImage* image,
                               GiTable* fibers, GiLocalization* master,
                               GiImage* bpixel, GiPsfConfig* config)
{

    const cxchar* const _func = "giraffe_compute_fiber_profiles";

    cxint i = 0;
    cxint status = 0;
    cxint nfibers = 0;
    cxint nframes = 1;
    cxint nbins = 0;
    cxint nx = 0;
    cxint ny = 0;

    cxdouble conad      = 1.;
    cxdouble bias_ron   = 0.;
    cxdouble bias_sigma = 0.;
    cxdouble dark_value = 0.;

    cx_string* s = NULL;

    cpl_table* _fibers = NULL;
    cpl_table* locc = NULL;

    cpl_matrix* my = NULL;

    cpl_image* _image = NULL;
    cpl_image* _variance = NULL;
    cpl_image* _locy = NULL;
    cpl_image* _locw = NULL;
    cpl_image* _bpixel = NULL;

    cpl_propertylist* properties = NULL;

    GiModel* psfmodel = NULL;

    GiPsfData* psfdata = NULL;
    GiPsfData* psffit = NULL;

    GiMaskPosition positions = {GIMASK_FITTED_DATA, NULL, NULL};
    GiMaskPosition coeffs = {GIMASK_FIT_COEFFS, NULL, NULL};

    GiPsfParams psf_setup = {0, 0, 1000., FALSE};


    if ((result == NULL) || (image == NULL) || (fibers == NULL) ||
        (master == NULL) || (config == NULL)) {
        cpl_error_set(_func, CPL_ERROR_NULL_INPUT);
        return 1;
    }

    if ((master->locy == NULL) || (master->locw == NULL)) {
        cpl_error_set(_func, CPL_ERROR_NULL_INPUT);
        return 1;
    }

    if ((result->locy != NULL) || (result->locw != NULL) ||
        (result->locc != NULL) || (result->psf != NULL)) {
        cpl_error_set(_func, CPL_ERROR_ILLEGAL_INPUT);
        return 1;
    }

    _image = giraffe_image_get(image);
    _locy = giraffe_image_get(master->locy);
    _locw = giraffe_image_get(master->locw);

    if (bpixel != NULL) {
        _bpixel = giraffe_image_get(bpixel);
    }

    _fibers = giraffe_table_get(fibers);

    if (_fibers == NULL) {
        cpl_error_set(_func, CPL_ERROR_DATA_NOT_FOUND);
        return 1;
    }

    nfibers = cpl_table_get_nrow(_fibers);

    nx = cpl_image_get_size_y(_image);
    ny = cpl_image_get_size_x(_image);

    nbins = (cxint) ceil(nx / config->binsize);


    /*
     * Get raw image properties.
     */

    properties = giraffe_image_get_properties(image);

    if (cpl_propertylist_has(properties, GIALIAS_NFIBERS) == FALSE) {
        cpl_propertylist_append_int(properties, GIALIAS_NFIBERS, nfibers);
        cpl_propertylist_set_comment(properties, GIALIAS_NFIBERS,
                                     "Number of fibres");
    }
    else {

        cxint _nfibers = cpl_propertylist_get_int(properties,
                                                  GIALIAS_NFIBERS);

        if (nfibers != _nfibers) {
            cpl_error_set(_func, CPL_ERROR_ILLEGAL_INPUT);
            return 1;
        }

    }


    giraffe_error_push();

    conad = giraffe_propertylist_get_conad(properties);

    if (cpl_error_get_code() != CPL_ERROR_NONE) {
        return 1;
    }

    giraffe_error_pop();


    if (!cpl_propertylist_has(properties, GIALIAS_BIASERROR)) {
        cpl_msg_warning(_func, "Missing bias error property (%s)! Setting "
                        "bias error to 0.", GIALIAS_BIASERROR);
        bias_sigma = 0.;
    }
    else {
        bias_sigma = cpl_propertylist_get_double(properties, GIALIAS_BIASERROR);
    }


    giraffe_error_push();

    bias_ron = giraffe_propertylist_get_ron(properties);

    if (cpl_error_get_code() != CPL_ERROR_NONE) {
        return 1;
    }

    giraffe_error_pop();


    if (cpl_propertylist_has(properties, GIALIAS_DARKVALUE) == FALSE) {
        cpl_msg_warning(_func, "Missing dark value property (%s) will be "
                        "set to %.2f!", GIALIAS_DARKVALUE, dark_value);
        cpl_propertylist_append_double(properties, GIALIAS_DARKVALUE,
                                       dark_value);
    }
    else {
        dark_value = cpl_propertylist_get_double(properties,
                                                 GIALIAS_DARKVALUE);
    }


    if (cpl_propertylist_has(properties, GIALIAS_DATANCOM)) {
        nframes = cpl_propertylist_get_int(properties, GIALIAS_DATANCOM);
    }


    /*
     * Convert the bias and dark errors from ADU to electrons.
     */

    bias_sigma *= conad;
    dark_value *= conad;


    /*
     * Prepare the input image and the variance image for the profile fitting.
     */

    giraffe_error_push();

    _image = cpl_image_multiply_scalar_create(_image, nframes * conad);
    _variance = cpl_image_abs_create(_image);

    cpl_image_add_scalar(_variance,
                         nframes * (bias_ron * bias_ron + nframes *
                         (bias_sigma * bias_sigma + dark_value * dark_value)));

    if (cpl_error_get_code() != CPL_ERROR_NONE) {
        if (_variance != NULL) {
            cpl_image_delete(_variance);
            _variance = NULL;
        }

        cpl_image_delete(_image);
        _image = NULL;

        return 1;
    }

    giraffe_error_pop();


    /*
     * Initialize PSF profile model.
     */

    psfmodel = giraffe_model_new(config->profile);

    giraffe_model_thaw(psfmodel);

    giraffe_model_set_parameter(psfmodel, "Amplitude", 1.);
    giraffe_model_set_parameter(psfmodel, "Background", 0.);
    giraffe_model_set_parameter(psfmodel, "Center", 0.);
    giraffe_model_set_parameter(psfmodel, "Width1", config->width);

    if (cx_strncasecmp(config->profile, "psfexp", 6) == 0) {

        cxdouble _exponent = fabs(config->exponent);

        giraffe_model_set_parameter(psfmodel, "Width2", _exponent);

        if (config->exponent > 0) {
            giraffe_model_freeze_parameter(psfmodel, "Width2");
        }

    }

    giraffe_model_set_iterations(psfmodel, config->fit.iterations);
    giraffe_model_set_delta(psfmodel, config->fit.delta);
    giraffe_model_set_tests(psfmodel, config->fit.tests);


    /*
     * Fit a PSF profile model to each fiber and compute the profile
     * parameters.
     */

    cpl_msg_info(_func, "Fitting fiber profiles ...");

    psf_setup.bsize = config->binsize;
    psf_setup.mwidth = config->maxwidth;
    psf_setup.normalize = config->normalize;

    psfdata = giraffe_psfdata_create(nfibers, nbins, ny, nx);

    status = _giraffe_psf_compute_profile(psfdata, _image, _variance, _locy,
                                          _locw, _fibers, _bpixel, psfmodel,
                                          &psf_setup);

    cpl_image_delete(_image);
    _image = NULL;

    if (status != 0) {
        giraffe_psfdata_delete(psfdata);
        psfdata= NULL;

        giraffe_model_delete(psfmodel);
        psfmodel = NULL;

        cpl_image_delete(_variance);
        _variance = NULL;

        cpl_msg_error(_func, "Fiber profile fit failed!");

        return 2;
    }


    /*
     * Scale the computed profiles to the level of the actual, average
     * input frame.
     */

    _image = (cpl_image*) giraffe_psfdata_get_data(psfdata, "Amplitude");
    cpl_image_divide_scalar(_image, nframes * conad);

    _image = (cpl_image*) giraffe_psfdata_get_data(psfdata, "Background");
    cpl_image_divide_scalar(_image, nframes * conad);

    _image = NULL;


    /*
     * Fit a polynomial model to each PSF profile parameter if
     * it was requested.
     */

    cpl_msg_info(_func, "Fitting PSF profile parameters ...");

    if (config->parameter_fit == TRUE) {

        const cxchar* parameters[] = {"Center", "Amplitude", "Background",
                "Width1", "Width2", NULL};

        psffit = _giraffe_psf_fit_parameters(psfdata, _fibers, parameters,
                                             config->yorder, config->worder,
                                             &config->clip);

    }
    else {

        psffit = _giraffe_psf_fit_parameters1d(psfdata, _fibers,
                                               NULL, config->yorder,
                                               &config->clip);

    }

    if (psffit == NULL) {
        giraffe_psfdata_delete(psfdata);
        psfdata= NULL;

        giraffe_model_delete(psfmodel);
        psfmodel = NULL;

        cpl_image_delete(_variance);
        _variance = NULL;

        cpl_msg_error(_func, "PSF parameter fit failed!");
        return 3;
    }

    giraffe_model_delete(psfmodel);
    psfmodel = NULL;

    cpl_image_delete(_variance);
    _variance = NULL;


    /*
     * Compute a fiber localization mask from the fitted fiber profiles.
     */

    positions.my = cpl_matrix_new(nx, nfibers);
    positions.mw = cpl_matrix_new(nx, nfibers);

    coeffs.my = cpl_matrix_new(config->yorder + 1, nfibers);
    coeffs.mw = cpl_matrix_new(config->worder + 1, config->worder + 1);

    status = _giraffe_psf_compute_mask(&positions, &coeffs, psfdata, _fibers,
                                       config->yorder, config->worder,
                                       &config->clip);

    if (status != 0) {

        giraffe_psfdata_delete(psffit);
        psffit = NULL;

        giraffe_psfdata_delete(psfdata);
        psfdata= NULL;

        cpl_msg_error(_func, "Computation of localization mask from "
                      "the fiber profile failed!");

        return 4;
    }

    giraffe_psfdata_delete(psfdata);
    psfdata= NULL;


    /*
     * Fill the results object. Convert the matrices to images and tables
     * and add the necessary properties.
     */

    properties = giraffe_image_get_properties(image);

    cpl_propertylist_update_string(properties, GIALIAS_PSFMODEL,
                                   config->profile);
    cpl_propertylist_set_comment(properties, GIALIAS_PSFMODEL,
                                 "PSF profile model identifier");

    cpl_propertylist_update_int(properties, GIALIAS_PSFXBINS,
                                config->binsize);
    cpl_propertylist_set_comment(properties, GIALIAS_PSFXBINS,
                                 "Size of bins along the dispersion "
                                 "direction.");

    cpl_propertylist_update_int(properties, GIALIAS_PSFYDEG,
                                config->yorder);
    cpl_propertylist_set_comment(properties, GIALIAS_PSFYDEG,
                                 "Order of the fiber center polynomial "
                                 "model.");

    cpl_propertylist_update_int(properties, GIALIAS_PSFWDEG,
                                config->worder);
    cpl_propertylist_set_comment(properties, GIALIAS_PSFWDEG,
                                 "Order of the fiber width 2d polynomial "
                                 "model.");

    cpl_propertylist_update_int(properties, GIALIAS_PSFWDEG,
                                config->worder);
    cpl_propertylist_set_comment(properties, GIALIAS_PSFWDEG,
                                 "Order of the fiber width 2d polynomial "
                                 "model.");

    cpl_propertylist_update_bool(properties, GIALIAS_PSFNORM,
                                 config->normalize);
    cpl_propertylist_set_comment(properties, GIALIAS_PSFNORM,
                                 "Pixel value normalization.");

    cpl_propertylist_update_int(properties, GIALIAS_PSFNX,
                                cpl_matrix_get_nrow(positions.my));
    cpl_propertylist_set_comment(properties, GIALIAS_PSFNX,
                                 "Number of pixels per spectrum.");

    cpl_propertylist_update_int(properties, GIALIAS_PSFNS,
                                cpl_matrix_get_ncol(positions.my));
    cpl_propertylist_set_comment(properties, GIALIAS_PSFNS,
                                 "Number of detected fibers.");

    cpl_propertylist_update_double(properties, GIALIAS_PSFSIGMA,
                                   config->clip.level);
    cpl_propertylist_set_comment(properties, GIALIAS_PSFSIGMA,
                                 "Sigma multiplier used for the PSF "
                                 "parmeter fit.");

    cpl_propertylist_update_double(properties, GIALIAS_PSFSIGMA,
                                   config->clip.level);
    cpl_propertylist_set_comment(properties, GIALIAS_PSFSIGMA,
                                 "Sigma multiplier used for the fit of PSF "
                                 "parameters.");

    cpl_propertylist_update_int(properties, GIALIAS_PSFNITER,
                                   config->clip.iterations);
    cpl_propertylist_set_comment(properties, GIALIAS_PSFNITER,
                                 "Number of iterations used for the fit "
                                 "of PSF parameters.");

    cpl_propertylist_update_double(properties, GIALIAS_PSFMFRAC,
                                   config->clip.fraction);
    cpl_propertylist_set_comment(properties, GIALIAS_PSFMFRAC,
                                 "Minimum allowed fraction of accepted "
                                 "over total data points used for the "
                                 "fit of PSF parameters.");


    /* Fiber profile center position */

    result->locy = giraffe_image_create(CPL_TYPE_DOUBLE,
                                        cpl_matrix_get_ncol(positions.my),
                                        cpl_matrix_get_nrow(positions.my));
    giraffe_image_copy_matrix(result->locy, positions.my);

    cpl_matrix_delete(positions.my);
    positions.my = NULL;

    giraffe_image_set_properties(result->locy, properties);
    properties = giraffe_image_get_properties(result->locy);

    cpl_propertylist_update_string(properties, GIALIAS_GIRFTYPE, "LOCY");
    cpl_propertylist_set_comment(properties, GIALIAS_GIRFTYPE,
                                 "GIRAFFE localization centroid");


    /* Fiber profile widths */

    result->locw = giraffe_image_create(CPL_TYPE_DOUBLE,
                                        cpl_matrix_get_ncol(positions.mw),
                                        cpl_matrix_get_nrow(positions.mw));
    giraffe_image_copy_matrix(result->locw, positions.mw);

    cpl_matrix_delete(positions.mw);
    positions.mw = NULL;

    properties = giraffe_image_get_properties(result->locy);

    giraffe_image_set_properties(result->locw, properties);
    properties = giraffe_image_get_properties(result->locw);

    cpl_propertylist_update_string(properties, GIALIAS_GIRFTYPE, "LOCWY");
    cpl_propertylist_set_comment(properties, GIALIAS_GIRFTYPE,
                                 "GIRAFFE localization half-width");


    /* Fiber polynomial model coefficients table */

    locc = cpl_table_new(cpl_matrix_get_ncol(coeffs.my));

    cpl_table_new_column(locc, "BUTTON", CPL_TYPE_INT);
    for (i = 0; i < cpl_table_get_nrow(locc); i++) {
        cpl_table_set_int(locc, "BUTTON", i, i);
    }

    for (i = 0; i < cpl_matrix_get_nrow(coeffs.my); i++) {

        cxchar* label = NULL;

        cx_asprintf(&label, "YC%d", i);
        cpl_table_new_column(locc, label, CPL_TYPE_DOUBLE);
        cx_free(label);

    }

    properties = giraffe_image_get_properties(result->locy);

    result->locc = giraffe_table_create(locc, properties);
    properties = giraffe_table_get_properties(result->locc);

    cpl_table_delete(locc);
    locc = NULL;

    my = cpl_matrix_transpose_create(coeffs.my);
    giraffe_table_copy_matrix(result->locc, "YC0", my);

    cpl_matrix_delete(my);
    my = NULL;

    cpl_matrix_delete(coeffs.my);
    coeffs.my = NULL;


    /* Add coefficients of the 2D fit of the fiber widths as properties */

    s = cx_string_new();

    for (i = 0; i < cpl_matrix_get_ncol(coeffs.mw); i++) {
        cx_string_sprintf(s, "%s%d", GIALIAS_LOCWIDCOEF, i);
        cpl_propertylist_update_double(properties, cx_string_get(s),
                                       cpl_matrix_get(coeffs.mw, 0, i));
    }

    cx_string_delete(s);
    s = NULL;

    cpl_matrix_delete(coeffs.mw);
    coeffs.mw = NULL;

    cpl_propertylist_update_string(properties, GIALIAS_GIRFTYPE,
                                   "LOCYWCHEB");
    cpl_propertylist_set_comment(properties, GIALIAS_GIRFTYPE,
                                 "GIRAFFE localization fit coefficients");


    /* Fiber profile PSF parameters */

    if (psffit != NULL) {
        result->psf = psffit;
    }

    return 0;

}


/**
 * @brief
 *  Creates a setup object for the PSF profile fit.
 *
 * @param list  Parameter list from which the setup informations is read.
 *
 * @return A newly allocated and initialized setup object if no errors
 *   occurred, or @c NULL otherwise.
 */

GiPsfConfig*
giraffe_psf_config_create(cpl_parameterlist* list)
{

    cpl_parameter *p;

    GiPsfConfig *self = NULL;


    if (list == NULL) {
        return NULL;
    }

    self = cx_calloc(1, sizeof *self);

    p = cpl_parameterlist_find(list, "giraffe.psf.model");
    self->profile = cx_strdup(cpl_parameter_get_string(p));

    if (cx_strncasecmp(self->profile, "psfexp", 6) == 0) {
        self->width = 16.;
    }
    else {
        self->width = 4.;
    }

    p = cpl_parameterlist_find(list, "giraffe.psf.binsize");
    self->binsize = cpl_parameter_get_int(p);

    if (self->binsize < 1) {
        self->binsize = 1;
    }

    p = cpl_parameterlist_find(list, "giraffe.psf.maxwidth");
    self->maxwidth = cpl_parameter_get_double(p);

    if (self->width > 0.) {
        p = cpl_parameterlist_find(list, "giraffe.psf.width");
        self->width = cpl_parameter_get_double(p);
    }

    if (self->width > self->maxwidth) {
        self->width = self->maxwidth;
    }

    p = cpl_parameterlist_find(list, "giraffe.psf.exponent");
    self->exponent = cpl_parameter_get_double(p);

    p = cpl_parameterlist_find(list, "giraffe.psf.normalize");
    self->normalize = cpl_parameter_get_bool(p);

    p = cpl_parameterlist_find(list, "giraffe.psf.profile.iterations");
    self->fit.iterations = cpl_parameter_get_int(p);

    p = cpl_parameterlist_find(list, "giraffe.psf.profile.tests");
    self->fit.tests = cpl_parameter_get_int(p);

    p = cpl_parameterlist_find(list, "giraffe.psf.profile.dchisquare");
    self->fit.delta = cpl_parameter_get_double(p);

    p = cpl_parameterlist_find(list, "giraffe.psf.parameters.fit");
    self->parameter_fit = cpl_parameter_get_bool(p);

    p = cpl_parameterlist_find(list, "giraffe.psf.parameters.yorder");
    self->yorder = cpl_parameter_get_int(p);

    if (self->yorder < 0) {
        giraffe_psf_config_destroy(self);
        return NULL;
    }

    p = cpl_parameterlist_find(list, "giraffe.psf.parameters.worder");
    self->worder = cpl_parameter_get_int(p);

    if (self->worder < 0) {
        giraffe_psf_config_destroy(self);
        return NULL;
    }

    p = cpl_parameterlist_find(list, "giraffe.psf.parameters.sigma");
    self->clip.level = cpl_parameter_get_double(p);

    p = cpl_parameterlist_find(list, "giraffe.psf.parameters.iterations");
    self->clip.iterations = cpl_parameter_get_int(p);

    p = cpl_parameterlist_find(list, "giraffe.psf.parameters.fraction");
    self->clip.fraction = cpl_parameter_get_double(p);

    return self;

}


/**
 * @brief
 *   Destroys a PSF profile fit setup object.
 *
 * @param self  The setup object to destroy.
 *
 * @return Nothing.
 *
 * The function deallocates the memory used by the setup object
 * @em self.
 */

void
giraffe_psf_config_destroy(GiPsfConfig* self)
{

    if (self != NULL) {
        if (self->profile != NULL) {
            cx_free((cxptr) self->profile);
            self->profile = NULL;
        }

        cx_free(self);
    }

    return;

}


/**
 * @brief
 *   Adds parameters for the PSF profile computation of the fibers.
 *
 * @param list  Parameter list to which parameters are added.
 *
 * @return Nothing.
 *
 * TBD
 */

void
giraffe_psf_config_add(cpl_parameterlist* list)
{

    cpl_parameter* p = NULL;


    if (list == NULL) {
        return;
    }

    p = cpl_parameter_new_enum("giraffe.psf.model",
                               CPL_TYPE_STRING,
                               "PSF profile model: `psfexp', `psfexp2'",
                               "giraffe.psf",
                               "psfexp2", 3, "psfexp", "psfexp2", "gaussian");
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "psf-model");
    cpl_parameterlist_append(list, p);

    p = cpl_parameter_new_value("giraffe.psf.normalize",
                                CPL_TYPE_BOOL,
                                "Use normalized pixel values.",
                                "giraffe.psf",
                                FALSE);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "psf-norm");
    cpl_parameterlist_append(list, p);


    p = cpl_parameter_new_value("giraffe.psf.binsize",
                                CPL_TYPE_INT,
                                "Size of bin along dispersion axis",
                                "giraffe.psf",
                                64);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "psf-binsize");
    cpl_parameterlist_append(list, p);


    p = cpl_parameter_new_value("giraffe.psf.maxwidth",
                                CPL_TYPE_DOUBLE,
                                "Maximum width of the PSF profile.",
                                "giraffe.psf",
                                16.);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "psf-maxwidth");
    cpl_parameterlist_append(list, p);


    p = cpl_parameter_new_value("giraffe.psf.width",
                                CPL_TYPE_DOUBLE,
                                "Initial width of the PSF profile.",
                                "giraffe.psf",
                                0.);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "psf-width");
    cpl_parameterlist_append(list, p);


    p = cpl_parameter_new_value("giraffe.psf.exponent",
                                CPL_TYPE_DOUBLE,
                                "Exponent of the exponential PSF profile "
                                "(will not be fitted if > 0).",
                                "giraffe.psf",
                                -3.);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "psf-exponent");
    cpl_parameterlist_append(list, p);


    p = cpl_parameter_new_value("giraffe.psf.profile.iterations",
                                CPL_TYPE_INT,
                                "Maximum number of iterations used for "
                                "the fit of the fiber PSF profile.",
                                "giraffe.psf",
                                120);

    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "psf-pfniter");
    cpl_parameterlist_append(list, p);


    p = cpl_parameter_new_value("giraffe.psf.profile.tests",
                                CPL_TYPE_INT,
                                "Maximum number of tests used for the fit "
                                "of the fiber PSF profile",
                                "giraffe.psf",
                                7);

    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "psf-pfntest");
    cpl_parameterlist_append(list, p);


    p = cpl_parameter_new_value("giraffe.psf.profile.dchisquare",
                                CPL_TYPE_DOUBLE,
                                "Minimum chi-square difference used for the "
                                "fit of the fiber PSF profile.",
                                "giraffe.psf",
                                0.001);

    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "psf-pfdchisq");
    cpl_parameterlist_append(list, p);


    p = cpl_parameter_new_value("giraffe.psf.parameters.fit",
                                CPL_TYPE_BOOL,
                                "2D fit of the PSF profile parameters "
                                "using a Chebyshev polynomial model.",
                                "giraffe.psf",
                                FALSE);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "psf-prmfit");
    cpl_parameterlist_append(list, p);


    p = cpl_parameter_new_value("giraffe.psf.parameters.yorder",
                                CPL_TYPE_INT,
                                "Order of Chebyshev polynomial fit.",
                                "giraffe.psf",
                                4);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "psf-yorder");
    cpl_parameterlist_append(list, p);


    p = cpl_parameter_new_value("giraffe.psf.parameters.worder",
                                CPL_TYPE_INT,
                                "Order of Chebyshev 2D polynomial fit.",
                                "giraffe.psf",
                                4);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "psf-worder");
    cpl_parameterlist_append(list, p);


    p = cpl_parameter_new_value("giraffe.psf.parameters.sigma",
                                CPL_TYPE_DOUBLE,
                                "PSF parameter fitting: sigma threshold "
                                "factor",
                                "giraffe.psf",
                                3.5);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "psf-sigma");
    cpl_parameterlist_append(list, p);


    p = cpl_parameter_new_value("giraffe.psf.parameters.iterations",
                                CPL_TYPE_INT,
                                "PSF parameter fitting: number of "
                                "iterations",
                                "giraffe.psf",
                                10);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "psf-niter");
    cpl_parameterlist_append(list, p);


    p = cpl_parameter_new_range("giraffe.psf.parameters.fraction",
                                CPL_TYPE_DOUBLE,
                                "PSF parameter fitting: minimum fraction "
                                "of points accepted/total.",
                                "giraffe.psf",
                                0.8, 0.0, 1.0);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "psf-mfrac");
    cpl_parameterlist_append(list, p);

    return;

}
/**@}*/
