/*
 * 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 <cxmemory.h>
#include <cxmessages.h>
#include <cxstrutils.h>

#include <cpl_error.h>
#include <cpl_array.h>
#include <cpl_matrix.h>
#include <cpl_image.h>
#include <cpl_imagelist.h>
#include <cpl_propertylist.h>

#include "gialias.h"
#include "gierror.h"
#include "gicube.h"
#include "giutils.h"


/**
 * @defgroup gicube Data Cube
 *
 * TBD
 */

/**@{*/

enum GiCubeAxes {
    GICUBE_X = 0,
    GICUBE_Y = 1,
    GICUBE_Z = 2,
    GICUBE_AXES
};

typedef enum GiCubeAxes GiCubeAxes;


struct GiCubeAxis {
    cxdouble start;
    cxdouble step;
};

typedef struct GiCubeAxis GiCubeAxis;


struct GiCubeWCS {
    cxdouble origin[GICUBE_AXES];
    cxdouble point[GICUBE_AXES];

    cxchar* axis_type[GICUBE_AXES];
    cxchar* axis_unit[GICUBE_AXES];

    cpl_matrix* transform;
};

typedef struct GiCubeWCS GiCubeWCS;


struct GiCube {
    cxsize width;
    cxsize height;
    cxsize depth;
    cxsize size;

    GiCubeAxis* axes[GICUBE_AXES];

    GiCubeWCS* wcs;

    cxdouble* pixels;

    cpl_imagelist* planes;
};



inline static void
_giraffe_cube_clear_wcs(GiCube* self)
{

    register cxsize axis = 0;

    for (axis = 0; axis < GICUBE_AXES; ++axis) {

        if (self->wcs->axis_type[axis] != NULL) {
            cx_free(self->wcs->axis_type[axis]);
            self->wcs->axis_type[axis] = NULL;
        }

        if (self->wcs->axis_unit[axis] != NULL) {
            cx_free(self->wcs->axis_unit[axis]);
            self->wcs->axis_unit[axis] = NULL;
        }

    }

    cpl_matrix_delete(self->wcs->transform);
    self->wcs->transform = NULL;

    cx_free(self->wcs);
    self->wcs = NULL;

    return;

}


inline static void
_giraffe_cube_set_wcs(GiCube* self, const cpl_propertylist* axes,
                      const cpl_matrix* transform)
{

    GiCubeWCS* wcs = NULL;

    cx_assert(self != NULL);
    cx_assert(axes != NULL);
    cx_assert(transform != NULL);

    wcs = cx_malloc(sizeof *self->wcs);

    wcs->transform = cpl_matrix_duplicate(transform);

    wcs->origin[GICUBE_X] = cpl_propertylist_get_double(axes, "XORIGIN");
    wcs->origin[GICUBE_Y] = cpl_propertylist_get_double(axes, "YORIGIN");
    wcs->origin[GICUBE_Z] = cpl_propertylist_get_double(axes, "ZORIGIN");

    wcs->point[GICUBE_X] = cpl_propertylist_get_double(axes, "XPOINT");
    wcs->point[GICUBE_Y] = cpl_propertylist_get_double(axes, "YPOINT");
    wcs->point[GICUBE_Z] = cpl_propertylist_get_double(axes, "ZPOINT");

    wcs->axis_type[GICUBE_X] =
            cx_strdup(cpl_propertylist_get_string(axes, "XTYPE"));
    wcs->axis_type[GICUBE_Y] =
            cx_strdup(cpl_propertylist_get_string(axes, "YTYPE"));
    wcs->axis_type[GICUBE_Z] =
            cx_strdup(cpl_propertylist_get_string(axes, "ZTYPE"));

    wcs->axis_unit[GICUBE_X] =
            cx_strdup(cpl_propertylist_get_string(axes, "XUNIT"));
    wcs->axis_unit[GICUBE_Y] =
            cx_strdup(cpl_propertylist_get_string(axes, "YUNIT"));
    wcs->axis_unit[GICUBE_Z] =
            cx_strdup(cpl_propertylist_get_string(axes, "ZUNIT"));


    if (self->wcs != NULL) {
        _giraffe_cube_clear_wcs(self);
    }

    self->wcs = wcs;

    return;

}


inline static cxdouble
_giraffe_cube_get_wcs_origin(const GiCube* self, GiCubeAxes axis)
{
    return self->wcs->origin[axis];
}


inline static cxdouble
_giraffe_cube_get_wcs_point(const GiCube* self, GiCubeAxes axis)
{
    return self->wcs->point[axis];
}


inline static const cxchar*
_giraffe_cube_get_wcs_axistype(const GiCube* self, GiCubeAxes axis)
{
    return self->wcs->axis_type[axis];
}


inline static const cxchar*
_giraffe_cube_get_wcs_axisunit(const GiCube* self, GiCubeAxes axis)
{
    return self->wcs->axis_unit[axis];
}


inline static const cpl_matrix*
_giraffe_cube_get_wcs_transform(const GiCube* self)
{
    return self->wcs->transform;
}


inline static void
_giraffe_cube_set_size(GiCube* self, cxsize width, cxsize height,
                       cxsize depth)
{

    self->width = width;
    self->height = height;
    self->depth = depth;

    self->size = self->width * self->height * self->depth;

    return;

}


inline static GiCube*
_giraffe_cube_new(void)
{

    GiCube* self = cx_malloc(sizeof *self);


    if (self != NULL) {
        _giraffe_cube_set_size(self, 0, 0, 0);

        self->axes[GICUBE_X] = NULL;
        self->axes[GICUBE_Y] = NULL;
        self->axes[GICUBE_Z] = NULL;

        self->wcs = NULL;

        self->pixels = NULL;
        self->planes = NULL;
    }

    return self;

}


inline static cxint
_giraffe_cube_init_planes(GiCube* self)
{

    register cxsize i = 0;

    register cxdouble* base = NULL;


    self->planes = cpl_imagelist_new();
    cx_assert(self->planes != NULL);


    base = self->pixels;

    for (i = 0; i < self->depth; i++) {

        cpl_image* plane = cpl_image_wrap_double(self->width, self->height,
                                                 base);

        cx_assert(plane != NULL);
        cpl_imagelist_set(self->planes, plane, i);

        base += self->width * self->height;

    }

    return 0;

}


inline static void
_giraffe_cube_clear_planes(GiCube* self)
{

    register cxsize i = 0;


    for (i = 0; i < self->depth; i++) {

        cpl_image* plane = cpl_imagelist_unset(self->planes, 0);

        cpl_image_unwrap(plane);

    }

    cx_assert(cpl_imagelist_get_size(self->planes) == 0);

    cpl_imagelist_delete(self->planes);
    self->planes =NULL;

    return;

}


inline static void
_giraffe_cube_delete(GiCube* self)
{

    register cxint i = 0;

    for (i = 0; i < GICUBE_AXES; i++) {
        if (self->axes[i] != NULL) {
            cx_free(self->axes[i]);
            self->axes[i] = NULL;
        }
    }

    if (self->wcs != NULL) {
        _giraffe_cube_clear_wcs(self);
        self->wcs = NULL;
    }

    if (self->planes != NULL) {
        _giraffe_cube_clear_planes(self);
        self->planes = NULL;
    }

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

    cx_free(self);

    return;

}


inline static cxbool
_giraffe_cube_has_axis(const GiCube* self, GiCubeAxes axis)
{
    return (self->axes[axis] == NULL) ? FALSE : TRUE;
}


inline static cxint
_giraffe_cube_get_axis(const GiCube* self, GiCubeAxes axis, cxdouble* start,
                       cxdouble* step)
{

    if (self->axes[axis] == NULL) {
        return 1;
    }
    else {

        if (start != NULL) {
            *start = self->axes[axis]->start;
        }

        if (step != NULL) {
            *step = self->axes[axis]->step;
        }

    }

    return 0;

}


inline static cxint
_giraffe_cube_set_axis(GiCube* self, GiCubeAxes axis, cxdouble start,
                       cxdouble step)
{

    if (self->axes[axis] == NULL) {
        self->axes[axis] = cx_calloc(1, sizeof(GiCubeAxis));
    }

    cx_assert(self->axes[axis] != NULL);

    self->axes[axis]->start = start;
    self->axes[axis]->step = step;

    return 0;

}


/**
 * @brief
 *   Create an empty data cube.
 *
 * @return
 *   A pointer to the newly created data cube, or @c NULL in case of an error.
 *
 * The function allocates the memory for a data cube object and initializes it
 * to a valid, empty data cube, i.e. the width, height and depth of the
 * created cube is set to @c 0. No data buffers are allocated.
 */

GiCube*
giraffe_cube_new(void)
{

    return _giraffe_cube_new();

}


/**
 * @brief
 *   Create a data cube with the given width, height and depth.
 *
 * @param width   The width of the data cube (size along the x-axis).
 * @param height  The width of the data cube (size along the y-axis).
 * @param depth   The width of the data cube (size along the z-axis).
 * @param data    Optional data buffer containing the data values.
 *
 * @return
 *   The function returns a pointer to the newly created data cube, or
 *   @c NULL in case of an error.
 *
 * The function creates a new data cube object with the size given by
 * @em width (size along the x-axis), @em height (size along the y-axis), and
 * @em depth (size along the z-axis). A data buffer @em data may be passed to
 * the function which contains the data values of the data cube. The size of
 * @em data must be equal to the product of @em width, @em height and
 * @em depth, and its organization in memory must be as if it is a stack of
 * images, i.e. the rows of the first plane come first, followed by the rows
 * of the second plane, and so on.
 *
 * If a data buffer @em data is passed to the function, this data buffer is
 * owned by the data cube, if the function call succeeds.
 */

GiCube*
giraffe_cube_create(cxsize width, cxsize height, cxsize depth, cxdouble* data)
{

    GiCube* self = _giraffe_cube_new();


    _giraffe_cube_set_size(self, width, height, depth);

    if (self->size == 0) {
        _giraffe_cube_delete(self);
        return NULL;
    }

    if (data != NULL) {
        self->pixels = data;
    }
    else {
        self->pixels = cx_calloc(self->size, sizeof(cxdouble));
    }

    cx_assert(self->pixels != NULL);


    /*
     * Create the image list object for accessing the cube plane by plane.
     * It is also needed (for the time being) to save a cube to disk.
     */

    giraffe_error_push();

    _giraffe_cube_init_planes(self);

    if (cpl_error_get_code() != CPL_ERROR_NONE) {
        _giraffe_cube_delete(self);
        return NULL;
    }

    giraffe_error_pop();

    return self;

}


/**
 * @brief
 *   Destroys a cube object.
 *
 * @param self  The data cube object to delete.
 *
 * @return
 *   Nothing.
 *
 * The function destroys the data cube @em self, and all its contents.
 */

void
giraffe_cube_delete(GiCube* self)
{

    if (self != NULL) {
        _giraffe_cube_delete(self);
    }

    return;

}


/**
 * @brief
 *   Get the width of the given data cube.
 *
 * @param self  The data cube to query.
 *
 * @return
 *   The width of the data cube @em self.
 *
 * The function retrieves the current width, i.e. the size along the x-axis,
 * of the given data cube @em self.
 */

cxsize
giraffe_cube_get_width(const GiCube* self)
{

    cx_assert(self != NULL);
    return self->width;

}


/**
 * @brief
 *   Get the height of the given data cube.
 *
 * @param self  The data cube to query.
 *
 * @return
 *   The height of the data cube @em self.
 *
 * The function retrieves the current height, i.e. the size along the y-axis,
 * of the given data cube @em self.
 */

cxsize
giraffe_cube_get_height(const GiCube* self)
{

    cx_assert(self != NULL);
    return self->height;

}


/**
 * @brief
 *   Get the depth of the given data cube.
 *
 * @param self  The data cube to query.
 *
 * @return
 *   The depth of the data cube @em self.
 *
 * The function retrieves the current depth, i.e. the size along the z-axis,
 * of the given data cube @em self.
 */

cxsize
giraffe_cube_get_depth(const GiCube* self)
{

    cx_assert(self != NULL);
    return self->depth;

}


/**
 * @brief
 *   Get the size of the given data cube.
 *
 * @param self  The data cube to query.
 *
 * @return
 *   The size of the data cube @em self.
 *
 * The function retrieves the current size, i.e. the total number of pixels
 * (data values), of the given data cube @em self.
 */

cxsize
giraffe_cube_get_size(const GiCube* self)
{

    cx_assert(self != NULL);
    return self->size;

}


/**
 * @brief
 *   Set the size of a data cube.
 *
 * @param self    The data cube to query.
 * @param width   The width to set.
 * @param height  The height to set.
 * @param depth   The depth to set.
*
 * @return
 *   The function returns @c 0 on success, and a non-zero value otherwise.
 *
 * The function changes the width, height, and depth of the data cube
 * @em self to the new values @em width, @em height, and @em depth. Any
 * data the data cube had stored before this function is called are lost, i.e.
 * all data values are reset to @c 0.
 */

cxint
giraffe_cube_set_size(GiCube* self, cxsize width, cxsize height, cxsize depth)
{

    const cxchar* const _id = "giraffe_cube_set_size";


    cx_assert(self != NULL);

    if ((width == 0) || (height == 0) || (depth == 0)) {
        cpl_error_set(_id, CPL_ERROR_ILLEGAL_INPUT);
        return 1;
    }

    if ((self->width == width) && (self->height == height) &&
        (self->depth == depth)) {
        memset(self->pixels, 0, self->size * sizeof(cxdouble));
    }
    else {

        /*
         * Clear the list of planes and destroy the old pixel buffer
         */

        if (self->planes != NULL) {
            _giraffe_cube_clear_planes(self);
        }

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


        /*
         * Set the new sizes
         */

        _giraffe_cube_set_size(self, width, height, depth);


        /*
         * Re-create the pixel buffer and the list of planes from the updated
         * size specifications.
         */

        self->pixels = cx_calloc(self->size, sizeof(cxdouble));
        cx_assert(self->pixels);


        giraffe_error_push();

        _giraffe_cube_init_planes(self);

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

        giraffe_error_pop();

    }

    return 0;

}


/**
 * @brief
 *   Get a reference to the data cube's pixel buffer.
 *
 * @em self  The data cube to query.
 *
 * @return
 *   A pointer to the pixel buffer of the data cube @em self, or @c NULL if
 *   the cube does not contain any data, or an error occurred.
 *
 * The function provides a reference to the pixel buffer of the data cube
 * @em self, for a fast, direct access of the pixel values.
 */

cxdouble*
giraffe_cube_get_data(const GiCube* self)
{

    const cxchar* const _id = "giraffe_cube_get_data";


    if (self == NULL) {
        cpl_error_set(_id, CPL_ERROR_NULL_INPUT);
        return 0;
    }

    return self->pixels;

}


/**
 * @brief
 *   Check whether an x-axis is defined for the given cube.
 *
 * @param self  The data cube to be checked.
 *
 * @return
 *   The function returns @c TRUE if an x-axis is defined, or @c FALSE if
 *   no x-axis was previously defined.
 *
 * The function checks whether @b giraffe_cube_set_xaxis() was previously
 * called for the cube @em self.
 */

cxbool
giraffe_cube_has_xaxis(const GiCube* self)
{
    return _giraffe_cube_has_axis(self, GICUBE_X);
}


/**
 * @brief
 *   Check whether a y-axis is defined for the given cube.
 *
 * @param self  The data cube to be checked.
 *
 * @return
 *   The function returns @c TRUE if an y-axis is defined, or @c FALSE if
 *   no y-axis was previously defined.
 *
 * The function checks whether @b giraffe_cube_set_yaxis() was previously
 * called for the cube @em self.
 */

cxbool
giraffe_cube_has_yaxis(const GiCube* self)
{
    return _giraffe_cube_has_axis(self, GICUBE_Y);
}


/**
 * @brief
 *   Check whether a z-axis is defined for the given cube.
 *
 * @param self  The data cube to be checked.
 *
 * @return
 *   The function returns @c TRUE if an z-axis is defined, or @c FALSE if
 *   no z-axis was previously defined.
 *
 * The function checks whether @b giraffe_cube_set_zaxis() was previously
 * called for the cube @em self.
 */

cxbool
giraffe_cube_has_zaxis(const GiCube* self)
{
    return _giraffe_cube_has_axis(self, GICUBE_Z);
}


/**
 * @brief
 *   Check whether a world coordinate system is defined for the given cube.
 *
 * @param self  The data cube to be checked.
 *
 * @return
 *   The function returns @c TRUE if a world coordinate system is defined,
 *   or @c FALSE if no world coordinate system was previously defined.
 *
 * The function checks whether @b giraffe_cube_set_wcs() was previously
 * called for the cube @em self.
 */

cxbool
giraffe_cube_has_wcs(const GiCube* self)
{
    return self->wcs != NULL;
}


/**
 * @brief
 *   Get the data cube's x-axis start value and step size.
 *
 * @param self   The data cube to query.
 * @param start  The location where the x-axis start value is stored.
 * @param step   The location where the x-axis step size is stored.
 *
 * @return
 *   The function returns @c 0 on success, or a non-zero value in case the
 *   cube's axis definition was not found.
 *
 * The function queries the data cube @em self for the start value and the
 * step size of the cube's x-axis and stores the values in the variables
 * pointed to by @em start and @em step respectively.
 *
 * If either @em start or @em step is a @c NULL pointer, the corresponding
 * value is not returned.
 */

cxint
giraffe_cube_get_xaxis(const GiCube* self, cxdouble* start, cxdouble* step)
{

    cx_assert(self != NULL);

    return _giraffe_cube_get_axis(self, GICUBE_X, start, step);

}


/**
 * @brief
 *   Get the data cube's y-axis start value and step size.
 *
 * @param self   The data cube to query.
 * @param start  The location where the y-axis start value is stored.
 * @param step   The location where the y-axis step size is stored.
 *
 * @return
 *   The function returns @c 0 on success, or a non-zero value in case the
 *   cube's axis definition was not found.
 *
 * The function queries the data cube @em self for the start value and the
 * step size of the cube's y-axis and stores the values in the variables
 * pointed to by @em start and @em step respectively.
 *
 * If either @em start or @em step is a @c NULL pointer, the corresponding
 * value is not returned.
 */

cxint
giraffe_cube_get_yaxis(const GiCube* self, cxdouble* start, cxdouble* step)
{

    cx_assert(self != NULL);

    return _giraffe_cube_get_axis(self, GICUBE_Y, start, step);

}


/**
 * @brief
 *   Get the data cube's z-axis start value and step size.
 *
 * @param self   The data cube to query.
 * @param start  The location where the z-axis start value is stored.
 * @param step   The location where the z-axis step size is stored.
 *
 * @return
 *   The function returns @c 0 on success, or a non-zero value in case the
 *   cube's axis definition was not found.
 *
 * The function queries the data cube @em self for the start value and the
 * step size of the cube's z-axis and stores the values in the variables
 * pointed to by @em start and @em step respectively.
 *
 * If either @em start or @em step is a @c NULL pointer, the corresponding
 * value is not returned.
 */

cxint
giraffe_cube_get_zaxis(const GiCube* self, cxdouble* start, cxdouble* step)
{

    cx_assert(self != NULL);

    return _giraffe_cube_get_axis(self, GICUBE_Z, start, step);

}


/**
 * @brief
 *   Set the data cube's x-axis start value and step size.
 *
 * @param self   The data cube to query.
 * @param start  The x-axis start value to set.
 * @param step   The x-axis step size to set.
 *
 * @return
 *   The function returns 0 on success and a non-zero value otherwise.
 *
 * The function sets the start value and the step size of the x-axis of the
 * data cube @em self.
 */

cxint
giraffe_cube_set_xaxis(GiCube* self, cxdouble start, cxdouble step)
{

    cx_assert(self != NULL);

    return _giraffe_cube_set_axis(self, GICUBE_X, start, step);

}


/**
 * @brief
 *   Set the data cube's y-axis start value and step size.
 *
 * @param self   The data cube to query.
 * @param start  The y-axis start value to set.
 * @param step   The y-axis step size to set.
 *
 * @return
 *   The function returns 0 on success and a non-zero value otherwise.
 *
 * The function sets the start value and the step size of the y-axis of the
 * data cube @em self.
 */

cxint
giraffe_cube_set_yaxis(GiCube* self, cxdouble start, cxdouble step)
{

    cx_assert(self != NULL);

    return _giraffe_cube_set_axis(self, GICUBE_Y, start, step);

}


/**
 * @brief
 *   Set the data cube's z-axis start value and step size.
 *
 * @param self   The data cube to modify.
 * @param start  The z-axis start value to set.
 * @param step   The z-axis step size to set.
 *
 * @return
 *   The function returns 0 on success and a non-zero value otherwise.
 *
 * The function sets the start value and the step size of the z-axis of the
 * data cube @em self.
 */

cxint
giraffe_cube_set_zaxis(GiCube* self, cxdouble start, cxdouble step)
{

    cx_assert(self != NULL);

    return _giraffe_cube_set_axis(self, GICUBE_Z, start, step);

}


/**
 * @brief
 *   Remove the world coordinate system from the cube.
 *
 * @param self  The data cube to modify.
 *
 * @return
 *   Nothing.
 *
 * The function removes the world coordinate system information from the
 * data cube @em self. If a cube does not have any world coordinate system
 * set, calling this function has no effect.
 */

void
giraffe_cube_clear_wcs(GiCube* self)
{

    if (self->wcs != NULL) {
        _giraffe_cube_clear_wcs(self);
    }

    return;

}


/**
 * @brief
 *   Set the data cube's world coordinate system
 *
 * @param self            The data cube to modify.
 * @param axes            Axes properties of the WCS axes
 * @param transformation  Transformation matrix from cube coordinates to
 *                        WCS coordinates
 *
 * @return
 *   The function returns 0 on success and a non-zero value otherwise.
 *
 * The function sets a world coordinate system (WCS) for the cube @em self.
 * Only linear transformations are supported, i.e transformations which can
 * be expressed as a matrix. This transformation matrix is then given by the
 * three by three matrix @em transformation. The origin of the WCS, its
 * reference point in cube coordinates (pixel coordinates), the axis types
 * and axis units are given by the property list @em axes. To be valid this
 * property list must provide the axis description for each of the three axes
 * (X, Y, and Z) of the cube. The reference pixel in cube coordinates is given
 * by the three double properties @c [XYZ]ORIGIN, and the associated WCS
 * coordinates by the three double properties @c [XYZ]POINT. The axis
 * type and the axis unit are given by the string properties @c [XYZ]TYPE
 * and @c [XYZ]UNIT, respectively.
 */

cxint
giraffe_cube_set_wcs(GiCube* self, const cpl_propertylist* axes,
                     const cpl_matrix* transformation)
{

    cx_assert(self != NULL);

    if ((axes == NULL) || (transformation == NULL)) {
        return -1;
    }

    if ((cpl_matrix_get_nrow(transformation) != GICUBE_AXES) ||
            (cpl_matrix_get_ncol(transformation) != GICUBE_AXES)) {
        return 3;
    }

    if ((cpl_propertylist_has(axes, "XORIGIN") == FALSE) ||
            (cpl_propertylist_has(axes, "YORIGIN") == FALSE) ||
            (cpl_propertylist_has(axes, "ZORIGIN") == FALSE)) {
        return 2;
    }

    if ((cpl_propertylist_has(axes, "XPOINT") == FALSE) ||
            (cpl_propertylist_has(axes, "YPOINT") == FALSE) ||
            (cpl_propertylist_has(axes, "ZPOINT") == FALSE)) {
        return 2;
    }

    if ((cpl_propertylist_has(axes, "XTYPE") == FALSE) ||
            (cpl_propertylist_has(axes, "YTYPE") == FALSE) ||
            (cpl_propertylist_has(axes, "ZTYPE") == FALSE)) {
        return 2;
    }

    if ((cpl_propertylist_has(axes, "XUNIT") == FALSE) ||
            (cpl_propertylist_has(axes, "YUNIT") == FALSE) ||
            (cpl_propertylist_has(axes, "ZUNIT") == FALSE)) {
        return 2;
    }

    _giraffe_cube_set_wcs(self, axes, transformation);

    return 1;
}


/**
 * @brief
 *   Compute the square root of the elements of a cube.
 *
 * @param self  The cube
 *
 * @return
 *   The function returns @c 0 if no error occurs, otherwise a non-zero
 *   value is returned.
 *
 * The square root of each pixel of the cube is computed.
 */

cxint
giraffe_cube_sqrt(GiCube* self)
{

    cpl_error_code status = CPL_ERROR_NONE;


    if (self == NULL) {
        return -1;
    }

    cx_assert(self->planes != NULL);

    status = cpl_imagelist_power(self->planes, 0.5);

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

    return 0;

}


/**
 * @brief
 *   Integrate a cube along the z-axis.
 *
 * @param self   The data cube.
 * @param start  The start of the integration domain.
 * @param end    The end of the integration domain.
 *
 * @return
 *   On success, an image of the integrated data values of the cube is
 *   returned. If an error occurs @c NULL is returned.
 *
 * For each pixel of the cube in the xy-plane, the function integrates the
 * values stored in this pixel of the data cube along the z-axis. The
 * integration domain is given by @em start and @em end.
 *
 * The positions of the pixels in the returned image correspond to the
 * xy-plane of the cube. The integrated pixel values are the sum of the
 * cube elements along the z-axis which are greater than @em start and less
 * than @em end. If @em start or @em end are not integer numbers, the
 * integration takes the pixel fractions into account.
 *
 * The value @em end must be greater than @em start, @em start must be
 * greater or equal zero, and @em end must be less or equal the depth of
 * the cube as returned by giraffe_cube_get_depth().
 */

cpl_image*
giraffe_cube_integrate(const GiCube* self, cxdouble start, cxdouble end)
{

    cxsize depth = 0;
    cxsize first = (cxsize)ceil(start);
    cxsize last  = (cxsize)floor(end);

    cpl_image* image = NULL;


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

    depth = giraffe_cube_get_depth(self);

    if ((start >= end) || (start < 0.) || (end > depth)) {
        return NULL;
    }


    image = cpl_image_duplicate(cpl_imagelist_get(self->planes, first));

    if (image != NULL) {

        if (first == last) {
            cpl_image_multiply_scalar(image, (end - start));
        }
        else {

            cxsize i = 0;

            cxdouble fstart = first - start;
            cxdouble fend   = end - last;


            for (i = first + 1; i < last; ++i) {
                cpl_image_add(image,
                              cpl_imagelist_get_const(self->planes, i));
            }


            /*
             * Correct for the pixel fractions at both ends of the
             * integration domain.
             */

            if ((fstart > 0.) && (first > 0)) {
                cpl_image* tmp = cpl_imagelist_get(self->planes, first - 1);

                tmp = cpl_image_multiply_scalar_create(tmp, fstart);
                cpl_image_add(image, tmp);

                cpl_image_delete(tmp);
                tmp = NULL;
            }

            if ((fend > 0.) && (last < depth)) {
                cpl_image* tmp = cpl_imagelist_get(self->planes, last);

                tmp = cpl_image_multiply_scalar_create(tmp, fend);
                cpl_image_add(image, tmp);

                cpl_image_delete(tmp);
                tmp = NULL;
            }

        }

    }

    return image;

}


/**
 * @brief
 *   Save the given data cube to disk.
 *
 * @param self        The cube to write to disk.
 * @param properties  The property list to write.
 * @param filename    The name of the file to write.
 * @param data        Extra data (unused).
 *
 * @return
 *   The function returns @c 0 on success, or a non-zero value if an error
 *   occurred. In the latter case an appropriate error code is also set.
 *
 * The function writes the property list @em properties and the associated
 * data cube @em self to the file @em filename.
 */

cxint
giraffe_cube_save(const GiCube* self, cpl_propertylist* properties,
                  const cxchar* filename, cxcptr data)
{

    cxbool xaxis = FALSE;
    cxbool yaxis = FALSE;
    cxbool zaxis = FALSE;

    cxdouble xstart = 0.;
    cxdouble xstep  = 0.;
    cxdouble ystart = 0.;
    cxdouble ystep  = 0.;
    cxdouble zstart = 0.;
    cxdouble zstep  = 0.;

    cxuint iomode = CPL_IO_CREATE;


    if (properties == NULL || filename == NULL) {
        return -1;
    }


    /*
     * If present use the cube writer configuration
     */

    if (data != NULL) {
        iomode = *((cxuint*)data);
    }


    if (self == NULL) {

        /*
         * If no cube has been provided, but properties and a filename
         * were given an empty cube is written to the file. Currently
         * this is restricted to creating a new cube file, to avoid
         * turning the property list into a valid extension header.
         */

        if (iomode != CPL_IO_CREATE) {
            return -2;
        }

        giraffe_error_push();

        cpl_propertylist_erase_regexp(properties, "CRVAL[0-9]*", 0);
        cpl_propertylist_erase_regexp(properties, "CRPIX[0-9]*", 0);
        cpl_propertylist_erase_regexp(properties, "CDELT[0-9]*", 0);
        cpl_propertylist_erase_regexp(properties, "CTYPE[0-9]*", 0);
        cpl_propertylist_erase_regexp(properties, "CUNIT[0-9]*", 0);

        cpl_propertylist_erase(properties, GIALIAS_BUNIT);

        cpl_propertylist_erase(properties, "DATAMIN");
        cpl_propertylist_erase(properties, "DATAMAX");

        cpl_propertylist_erase(properties, GIALIAS_DATAMEAN);
        cpl_propertylist_erase(properties, GIALIAS_DATAMEDI);
        cpl_propertylist_erase(properties, GIALIAS_DATASIG);

        cpl_propertylist_save(properties, filename, iomode);

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

        giraffe_error_pop();

    }
    else {

        cxsize size = 0;

        const cpl_array* pixels = NULL;


        /*
         * Update the data cube's properties. The world coordinates are only
         * written if a world coordinate system has been previously defined.
         * If a full world coordinate system is not present, the world
         * coordinates as defined by the cube's axes are used provided that
         * the information is present for all three axes. If this information
         * is missing too, no world coordinates are written.
         */

        if (giraffe_cube_get_xaxis(self, &xstart, &xstep) == 0) {
            xaxis = TRUE;
        }

        if (giraffe_cube_get_yaxis(self, &ystart, &ystep) == 0) {
            yaxis = TRUE;
        }

        if (giraffe_cube_get_zaxis(self, &zstart, &zstep) == 0) {
            zaxis = TRUE;
        }


        if (giraffe_cube_has_wcs(self) == TRUE) {

            const cxchar* ctype[GICUBE_AXES];
            const cxchar* cunit[GICUBE_AXES];

            cxint status = 0;

            cxdouble crpix[GICUBE_AXES];
            cxdouble crval[GICUBE_AXES];

            const cpl_matrix* cd = _giraffe_cube_get_wcs_transform(self);


            crpix[GICUBE_X] = _giraffe_cube_get_wcs_origin(self, GICUBE_X);
            crpix[GICUBE_Y] = _giraffe_cube_get_wcs_origin(self, GICUBE_Y);
            crpix[GICUBE_Z] = _giraffe_cube_get_wcs_origin(self, GICUBE_Z);

            crval[GICUBE_X] = _giraffe_cube_get_wcs_point(self, GICUBE_X);
            crval[GICUBE_Y] = _giraffe_cube_get_wcs_point(self, GICUBE_Y);
            crval[GICUBE_Z] = _giraffe_cube_get_wcs_point(self, GICUBE_Z);

            ctype[GICUBE_X] = _giraffe_cube_get_wcs_axistype(self, GICUBE_X);
            ctype[GICUBE_Y] = _giraffe_cube_get_wcs_axistype(self, GICUBE_Y);
            ctype[GICUBE_Z] = _giraffe_cube_get_wcs_axistype(self, GICUBE_Z);

            cunit[GICUBE_X] = _giraffe_cube_get_wcs_axisunit(self, GICUBE_X);
            cunit[GICUBE_Y] = _giraffe_cube_get_wcs_axisunit(self, GICUBE_Y);
            cunit[GICUBE_Z] = _giraffe_cube_get_wcs_axisunit(self, GICUBE_Z);

            status = giraffe_propertylist_update_wcs(properties, GICUBE_AXES,
                                                     crpix, crval, ctype, cunit,
                                                     cd);

            if (status != 0) {
                giraffe_propertylist_update_wcs(properties, 0, NULL, NULL, NULL,
                                                NULL, NULL);
            }

        }
        else if ((xaxis == TRUE) && (yaxis == TRUE) && (zaxis == TRUE)) {

            const cxchar* ctype[] = {"PIXEL", "PIXEL", "AWAV"};
            const cxchar* cunit[] = {"bin", "bin", "nm"};

            cxint status = 0;

            cxdouble crpix[] = {1., 1., 1.};
            cxdouble crval[] = {xstart, ystart, zstart};

            cpl_matrix* cd = cpl_matrix_new(3, 3);

            cpl_matrix_set(cd, 0, 0, xstep);
            cpl_matrix_set(cd, 1, 1, ystep);
            cpl_matrix_set(cd, 2, 2, zstep);

            status = giraffe_propertylist_update_wcs(properties, GICUBE_AXES,
                                                     crpix, crval, ctype, cunit,
                                                     cd);

            if (status != 0) {
                giraffe_propertylist_update_wcs(properties, 0, NULL, NULL, NULL,
                                                NULL, NULL);
            }

            cpl_matrix_delete(cd);
            cd = NULL;

        }
        else {

            giraffe_propertylist_update_wcs(properties, 0, NULL, NULL, NULL,
                                            NULL, NULL);

        }


        /*
         * Update data properties
         */

        giraffe_error_push();

        size = giraffe_cube_get_size(self);
        pixels = cpl_array_wrap_double(giraffe_cube_get_data(self), size);


        cpl_propertylist_update_string(properties, GIALIAS_BUNIT,
                                       "adu");

        cpl_propertylist_update_double(properties, GIALIAS_DATAMIN,
                                       cpl_array_get_min(pixels));
        cpl_propertylist_set_comment(properties, GIALIAS_DATAMIN,
                                     "Minimal pixel value");

        cpl_propertylist_update_double(properties, GIALIAS_DATAMAX,
                                       cpl_array_get_max(pixels));
        cpl_propertylist_set_comment(properties, GIALIAS_DATAMAX,
                                     "Maximum pixel value");

        cpl_propertylist_update_double(properties, GIALIAS_DATAMEAN,
                                       cpl_array_get_mean(pixels));
        cpl_propertylist_set_comment(properties, GIALIAS_DATAMEAN,
                                     "Mean of pixel values");

        cpl_propertylist_update_double(properties, GIALIAS_DATASIG,
                                       cpl_array_get_stdev(pixels));
        cpl_propertylist_set_comment(properties, GIALIAS_DATASIG,
                                     "Standard deviation of pixel values");

        cpl_propertylist_update_double(properties, GIALIAS_DATAMEDI,
                                       cpl_array_get_median(pixels));
        cpl_propertylist_set_comment(properties, GIALIAS_DATAMEDI,
                                     "Median of pixel values");

        cpl_array_unwrap((cpl_array*)pixels);
        pixels = NULL;

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

        giraffe_error_pop();


        /*
         * Write the data cube to the output file
         */

        giraffe_error_push();

        cpl_imagelist_save(self->planes, filename, CPL_BPP_IEEE_FLOAT,
                           properties, iomode);

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

        giraffe_error_pop();

    }

    return 0;

}
/**@}*/
