/*
 *  $Id: brick.c 30051 2026-06-12 13:19:37Z vojtakl4 $
 *  Copyright (C) 2012-2025 David Necas (Yeti), Petr Klapetek.
 *  E-mail: yeti@gwyddion.net, klapetek@gwyddion.net.
 *
 *  This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public
 *  License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any
 *  later version.
 *
 *  This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
 *  warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 *  details.
 *
 *  You should have received a copy of the GNU General Public License along with this program; if not, write to the
 *  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

#include "config.h"
#include <string.h>
#include <stdlib.h>
#include <glib/gi18n-lib.h>

#include "libgwyddion/macros.h"
#include "libgwyddion/serializable-utils.h"
#include "libgwyddion/serialize.h"
#include "libgwyddion/brick.h"
#include "libgwyddion/interpolation.h"

#include "libgwyddion/omp.h"
#include "libgwyddion/internal.h"

#define TYPE_NAME "GwyBrick"

enum {
    SGNL_DATA_CHANGED,
    NUM_SIGNALS
};

enum {
    ITEM_XRES, ITEM_YRES, ITEM_ZRES,
    ITEM_XREAL, ITEM_YREAL, ITEM_ZREAL,
    ITEM_XOFF, ITEM_YOFF, ITEM_ZOFF,
    ITEM_UNIT_X, ITEM_UNIT_Y, ITEM_UNIT_Z, ITEM_UNIT_W,
    ITEM_DATA,
    ITEM_CALIBRATION,
    NUM_ITEMS
};

static void             finalize              (GObject *object);
static void             alloc_data            (GwyBrick *brick,
                                               gint xres,
                                               gint yres,
                                               gint zres,
                                               gboolean nullme);
static void             serializable_init     (GwySerializableInterface *iface);
static void             serializable_itemize  (GwySerializable *serializable,
                                               GwySerializableGroup *group);
static gboolean         serializable_construct(GwySerializable *serializable,
                                               GwySerializableGroup *group,
                                               GwyErrorList **error_list);
static GwySerializable* serializable_copy     (GwySerializable *serializable);
static void             serializable_assign   (GwySerializable *destination,
                                               GwySerializable *source);

static guint signals[NUM_SIGNALS];
static GObjectClass *parent_class = NULL;

static GwySerializableItem serializable_items[NUM_ITEMS] = {
    { .name = "xres",        .ctype = GWY_SERIALIZABLE_INT32,        },
    { .name = "yres",        .ctype = GWY_SERIALIZABLE_INT32,        },
    { .name = "zres",        .ctype = GWY_SERIALIZABLE_INT32,        },
    { .name = "xreal",       .ctype = GWY_SERIALIZABLE_DOUBLE,       },
    { .name = "yreal",       .ctype = GWY_SERIALIZABLE_DOUBLE,       },
    { .name = "zreal",       .ctype = GWY_SERIALIZABLE_DOUBLE,       },
    { .name = "xoff",        .ctype = GWY_SERIALIZABLE_DOUBLE,       },
    { .name = "yoff",        .ctype = GWY_SERIALIZABLE_DOUBLE,       },
    { .name = "zoff",        .ctype = GWY_SERIALIZABLE_DOUBLE,       },
    { .name = "unit_x",      .ctype = GWY_SERIALIZABLE_OBJECT,       },
    { .name = "unit_y",      .ctype = GWY_SERIALIZABLE_OBJECT,       },
    { .name = "unit_z",      .ctype = GWY_SERIALIZABLE_OBJECT,       },
    { .name = "unit_w",      .ctype = GWY_SERIALIZABLE_OBJECT,       },
    { .name = "data",        .ctype = GWY_SERIALIZABLE_DOUBLE_ARRAY,
                             .flags = GWY_SERIALIZABLE_BIG_DATA      },
    { .name = "calibration", .ctype = GWY_SERIALIZABLE_OBJECT_ARRAY, },
};

G_DEFINE_TYPE_WITH_CODE(GwyBrick, gwy_brick, G_TYPE_OBJECT,
                        G_ADD_PRIVATE(GwyBrick)
                        G_IMPLEMENT_INTERFACE(GWY_TYPE_SERIALIZABLE, serializable_init))

static void
serializable_init(GwySerializableInterface *iface)
{
    iface->itemize   = serializable_itemize;
    iface->construct = serializable_construct;
    iface->copy      = serializable_copy;
    iface->assign    = serializable_assign;

    serializable_items[ITEM_XRES].aux.pspec = g_param_spec_int(serializable_items[ITEM_XRES].name, NULL, NULL,
                                                               1, G_MAXINT, 1,
                                                               G_PARAM_STATIC_STRINGS);
    serializable_items[ITEM_YRES].aux.pspec = g_param_spec_int(serializable_items[ITEM_YRES].name, NULL, NULL,
                                                               1, G_MAXINT, 1,
                                                               G_PARAM_STATIC_STRINGS);
    serializable_items[ITEM_ZRES].aux.pspec = g_param_spec_int(serializable_items[ITEM_ZRES].name, NULL, NULL,
                                                               1, G_MAXINT, 1,
                                                               G_PARAM_STATIC_STRINGS);
    serializable_items[ITEM_XREAL].aux.pspec = g_param_spec_double(serializable_items[ITEM_XREAL].name, NULL, NULL,
                                                                   G_MINDOUBLE, G_MAXDOUBLE, 1.0,
                                                                   G_PARAM_STATIC_STRINGS);
    serializable_items[ITEM_YREAL].aux.pspec = g_param_spec_double(serializable_items[ITEM_YREAL].name, NULL, NULL,
                                                                   G_MINDOUBLE, G_MAXDOUBLE, 1.0,
                                                                   G_PARAM_STATIC_STRINGS);
    serializable_items[ITEM_ZREAL].aux.pspec = g_param_spec_double(serializable_items[ITEM_ZREAL].name, NULL, NULL,
                                                                   G_MINDOUBLE, G_MAXDOUBLE, 1.0,
                                                                   G_PARAM_STATIC_STRINGS);
    /* No need to add pspecs for offsets because they have unlimited values and default zero. */
    gwy_fill_serializable_defaults_pspec(serializable_items, NUM_ITEMS, FALSE);
    serializable_items[ITEM_UNIT_X].aux.object_type = GWY_TYPE_UNIT;
    serializable_items[ITEM_UNIT_Y].aux.object_type = GWY_TYPE_UNIT;
    serializable_items[ITEM_UNIT_Z].aux.object_type = GWY_TYPE_UNIT;
    serializable_items[ITEM_UNIT_W].aux.object_type = GWY_TYPE_UNIT;
    serializable_items[ITEM_CALIBRATION].aux.object_type = GWY_TYPE_LINE;
}

static void
gwy_brick_class_init(GwyBrickClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
    GType type = G_TYPE_FROM_CLASS(klass);

    parent_class = gwy_brick_parent_class;

    gobject_class->finalize = finalize;

    /**
     * GwyBrick::data-changed:
     * @gwyline: The #GwyBrick which received the signal.
     *
     * The ::data-changed signal is never emitted by the object itself.  It is intended as a means to notify object
     * users they should update themselves, usually when a computation modifying the data is finished.
     */
    signals[SGNL_DATA_CHANGED] = g_signal_new("data-changed", type,
                                              G_SIGNAL_RUN_FIRST,
                                              G_STRUCT_OFFSET(GwyBrickClass, data_changed),
                                              NULL, NULL,
                                              g_cclosure_marshal_VOID__VOID,
                                              G_TYPE_NONE, 0);
    g_signal_set_va_marshaller(signals[SGNL_DATA_CHANGED], type, g_cclosure_marshal_VOID__VOIDv);
}

static void
gwy_brick_init(GwyBrick *brick)
{
    brick->priv = gwy_brick_get_instance_private(brick);

    alloc_data(brick, 1, 1, 1, TRUE);
    brick->xreal = brick->yreal = brick->zreal = 1.0;
}

static void
finalize(GObject *object)
{
    GwyBrick *brick = (GwyBrick*)object;
    GwyBrickPrivate *priv = brick->priv;

    g_clear_object(&priv->zcalibration);
    g_clear_object(&priv->unit_x);
    g_clear_object(&priv->unit_y);
    g_clear_object(&priv->unit_z);
    g_clear_object(&priv->unit_w);
    g_free(priv->data);

    G_OBJECT_CLASS(parent_class)->finalize(object);
}

static void
alloc_data(GwyBrick *brick,
           gint xres,
           gint yres,
           gint zres,
           gboolean nullme)
{
    gsize n = (gsize)xres * (gsize)yres * (gsize)zres;
    GwyBrickPrivate *priv = brick->priv;
    if (brick->xres == xres && brick->yres == yres && brick->zres == zres) {
        if (nullme)
            gwy_clear(priv->data, n);
        return;
    }

    gsize brickn = (gsize)brick->xres * (gsize)brick->yres * (gsize)brick->zres;
    if (brickn != n) {
        GWY_FREE(priv->data);
        priv->data = (nullme ? g_new0(gdouble, n) : g_new(gdouble, n));
    }
    else if (nullme)
        gwy_clear(priv->data, n);

    brick->xres = xres;
    brick->yres = yres;
    brick->zres = zres;
}

static void
copy_info(GwyBrick *src, GwyBrick *dest)
{
    dest->xreal = src->xreal;
    dest->yreal = src->yreal;
    dest->zreal = src->zreal;
    dest->xoff = src->xoff;
    dest->yoff = src->yoff;
    dest->zoff = src->zoff;
    gwy_brick_copy_units(src, dest);
}

/**
 * gwy_brick_new: (constructor)
 * @xres: X resolution, i.e., the number of samples in x direction
 * @yres: Y resolution, i.e., the number of samples in y direction
 * @zres: Z resolution, i.e., the number of samples in z direction
 * @xreal: Real physical dimension in x direction.
 * @yreal: Real physical dimension in y direction.
 * @zreal: Real physical dimension in z direction.
 * @nullme: Whether the data brick should be initialized to zeroes. If %FALSE, the data will not be initialized.
 *
 * Creates a new data brick.
 *
 * Returns: (transfer full):
 *          A newly created data brick.
 **/
GwyBrick*
gwy_brick_new(gint xres, gint yres, gint zres, gdouble xreal, gdouble yreal, gdouble zreal, gboolean nullme)
{
    GwyBrick *brick = g_object_new(GWY_TYPE_BRICK, NULL);

    alloc_data(brick, xres, yres, zres, nullme);
    brick->xreal = xreal;
    brick->yreal = yreal;
    brick->zreal = zreal;

    return brick;
}

/**
 * gwy_brick_new_alike:
 * @model: A data brick to take resolutions and units from.
 * @nullme: Whether the data brick should be initialized to zeroes. If %FALSE, the data will not be initialized.
 *
 * Creates a new data brick similar to an existing one.
 *
 * Use gwy_brick_duplicate() if you want to copy a data brick including
 * data.
 *
 * Returns: (transfer full):
 *          A newly created data brick.
 **/
GwyBrick*
gwy_brick_new_alike(GwyBrick *model,
                    gboolean nullme)
{
    GwyBrick *brick = g_object_new(GWY_TYPE_BRICK, NULL);
    g_return_val_if_fail(GWY_IS_BRICK(model), brick);

    GwyBrickPrivate *priv, *new_priv;

    alloc_data(brick, model->xres, model->yres, model->zres, nullme);
    copy_info(model, brick);

    priv = model->priv;
    new_priv = brick->priv;
    if (priv->zcalibration)
        new_priv->zcalibration = gwy_line_copy(priv->zcalibration);

    return brick;
}

/**
 * gwy_brick_crop:
 * @brick: A data brick.
 * @xpos: X position where to start from.
 * @ypos: Y position where to start from.
 * @zpos: Z position where to start from.
 * @width: X resolution (width) to be extracted.
 * @height: Y resolution (height) to be extracted.
 * @depth: Z resolution (depth) to be extracted.
 *
 * Crops a new data brick to a smaller size.
 *
 * The real dimensions are recalculated and calibration, if present, is cropped to match the new @z. Offsets are reset
 * to zero.
 **/
void
gwy_brick_crop(GwyBrick *brick,
               gint xpos, gint ypos, gint zpos,
               gint width, gint height, gint depth)
{
    g_return_if_fail(GWY_IS_BRICK(brick));

    gint xres = brick->xres, yres = brick->yres, zres = brick->zres;
    g_return_if_fail(xpos >= 0 && ypos >= 0 && zpos >= 0 && width >= 0 && height >= 0 && depth >= 0);
    g_return_if_fail(xpos + width <= xres && ypos + height <= yres && zpos + depth <= zres);

    brick->xoff = brick->yoff = brick->zoff = 0.0;
    if (width == xres && height == yres && depth == zres)
        return;

    GwyBrickPrivate *priv = brick->priv;
    gdouble dx = gwy_brick_get_dx(brick), dy = gwy_brick_get_dy(brick), dz = gwy_brick_get_dz(brick);
    gdouble *rdata = g_new(gdouble, width*height*depth);
    const gdouble *data = priv->data;

    for (gint lev = 0; lev < depth; lev++) {
        for (gint row = 0; row < height; row++) {
            gwy_assign(rdata + width*(row + height*lev),
                       data + xpos + xres*(row + ypos + yres*(lev + zpos)),
                       width);
        }
    }
    GWY_SWAP(gdouble*, rdata, priv->data);
    g_free(rdata);

    brick->xreal = width*dx;
    brick->yreal = width*dy;
    brick->zreal = width*dz;
    if (priv->zcalibration)
        gwy_line_crop(priv->zcalibration, zpos, depth);

    gwy_brick_invalidate(brick);
}

/**
 * gwy_brick_new_part:
 * @brick: A data brick.
 * @xpos: X position where to start from.
 * @ypos: Y position where to start from.
 * @zpos: Z position where to start from.
 * @width: X resolution (width) to be extracted.
 * @height: Y resolution (height) to be extracted.
 * @depth: Z resolution (depth) to be extracted.
 * @keep_offsets: %TRUE to keep offsets of data during extraction.
 *
 * Creates a new data brick as a part of existing one.
 *
 * Use gwy_brick_duplicate() if you want to copy a whole data brick.
 *
 * Returns: (transfer full):
 *          A newly created data brick.
 **/
GwyBrick*
gwy_brick_new_part(GwyBrick *brick,
                   gint xpos, gint ypos, gint zpos,
                   gint width, gint height, gint depth,
                   gboolean keep_offsets)
{
    g_return_val_if_fail(GWY_IS_BRICK(brick), NULL);

    gint xres = brick->xres, yres = brick->yres, zres = brick->zres;
    g_return_val_if_fail(xpos >= 0 && ypos >= 0 && zpos >= 0 && width >= 0 && height >= 0 && depth >= 0, NULL);
    g_return_val_if_fail(xpos + width <= xres && ypos + height <= yres && zpos + depth <= zres, NULL);

    if (width == xres && height == yres && depth == zres) {
        GwyBrick *part = gwy_brick_copy(brick);
        if (!keep_offsets)
            part->xoff = part->yoff = part->zoff = 0.0;
        return part;
    }

    gdouble dx = gwy_brick_get_dx(brick), dy = gwy_brick_get_dy(brick), dz = gwy_brick_get_dz(brick);
    GwyBrick *part = gwy_brick_new(width, height, depth, width*dx, height*dy, depth*dz, FALSE);

    GwyBrickPrivate *priv = brick->priv, *rpriv = part->priv;
    const gdouble *data = priv->data;
    gdouble *rdata = rpriv->data;
    for (gint lev = 0; lev < depth; lev++) {
        for (gint row = 0; row < height; row++) {
            gwy_assign(rdata + width*(row + height*lev),
                       data + xpos + xres*(row + ypos + yres*(lev + zpos)),
                       width);
        }
    }
    gwy_brick_copy_units(brick, part);

    if (priv->zcalibration)
        rpriv->zcalibration = gwy_line_part_extract(priv->zcalibration, zpos, depth);

    if (keep_offsets) {
        part->xoff = xpos*dx + brick->xoff;
        part->yoff = ypos*dy + brick->yoff;
        part->zoff = zpos*dz + brick->zoff;
    }
    else
        part->xoff = part->yoff = part->zoff = 0.0;

    return part;
}

/**
 * gwy_brick_resize:
 * @brick: A data brick to be resized.
 * @xres: Desired X resolution.
 * @yres: Desired Y resolution.
 * @zres: Desired Z resolution.
 *
 * Resizes a data brick to a different size, discarding data.
 *
 * The real dimensions and offsets are unchanged. Z-calibration, if present, is discarded. The brick contents becomes
 * undefined. The function just makes sure @brick has the correct pixel dimensions.
 **/
void
gwy_brick_resize(GwyBrick *brick,
                 gint xres, gint yres, gint zres)
{
    g_return_if_fail(GWY_IS_BRICK(brick));
    g_return_if_fail(xres > 0);
    g_return_if_fail(yres > 0);
    g_return_if_fail(zres > 0);
    alloc_data(brick, xres, yres, zres, FALSE);
    g_clear_object(&brick->priv->zcalibration);
    gwy_brick_invalidate(brick);
}

/**
 * gwy_brick_data_changed:
 * @brick: A data brick.
 *
 * Emits signal "data_changed" on a data brick.
 **/
void
gwy_brick_data_changed(GwyBrick *brick)
{
    g_signal_emit(brick, signals[SGNL_DATA_CHANGED], 0);
}

/**
 * gwy_brick_resample:
 * @brick: A data brick.
 * @xres: Desired x resolution.
 * @yres: Desired y resolution.
 * @zres: Desired z resolution.
 * @interpolation: Interpolation method to use.
 *
 * Resamples a data brick.
 *
 * In other words changes the size of three dimensional field related with data brick. The original values are used
 * for resampling using a requested interpolation alorithm.
 **/
void
gwy_brick_resample(GwyBrick *brick,
                   gint xres,
                   gint yres,
                   gint zres,
                   GwyInterpolationType interpolation)
{
    g_return_if_fail(GWY_IS_BRICK(brick));
    g_return_if_fail(xres > 0 && yres > 0 && zres > 0);

    gdouble *bdata, *srcdata;
    gint row, col, lev;
    gint srcxres, srcyres, srczres;
    gdouble xratio, yratio, zratio;

    /* FIXME: What to do with calibration? */
    srcxres = brick->xres;
    srcyres = brick->yres;
    srczres = brick->zres;
    if (xres == srcxres && yres == srcyres && zres == srczres)
        return;

    bdata = g_new(gdouble, xres*yres*zres);

    xratio = (gdouble)srcxres/xres;
    yratio = (gdouble)srcyres/yres;
    zratio = (gdouble)srczres/zres;

    if (interpolation != GWY_INTERPOLATION_ROUND) {
        g_warning("Only the ROUND interpolation method is implemented.");
        interpolation = GWY_INTERPOLATION_ROUND;
    }

    if (interpolation == GWY_INTERPOLATION_ROUND) {
        srcdata = brick->priv->data;
        for (lev = 0; lev < zres; lev++) {
            gint srclev = MIN((gint)(zratio*lev + 0.5), brick->zres-1);
            for (row = 0; row < yres; row++) {
                gint srcrow = MIN((gint)(yratio*row + 0.5), brick->yres-1);
                for (col = 0; col < xres; col++) {
                    gint srccol = MIN((gint)(xratio*col + 0.5), brick->xres-1);
                    bdata[col + xres*(row + yres*lev)] = srcdata[srccol + srcxres*(srcrow + srcyres*srclev)];
                }
            }
        }
    }

    g_free(brick->priv->data);
    brick->priv->data = bdata;
    brick->xres = xres;
    brick->yres = yres;
    brick->zres = zres;
}

/**
 * gwy_brick_copy_data:
 * @src: Source brick.
 * @dest: Destination brick.
 *
 * Copies the contents of an already allocated brick to a brick of the same size.
 *
 * Only the data are copied.  To make a data brick completely identical to another, including units and change of
 * dimensions, you can use gwy_brick_assign().
 **/
void
gwy_brick_copy_data(GwyBrick *src, GwyBrick *dest)
{
    g_return_if_fail(GWY_IS_BRICK(src));
    g_return_if_fail(GWY_IS_BRICK(dest));
    g_return_if_fail(src->xres == dest->xres && src->yres == dest->yres && src->zres == dest->zres);
    if (src == dest)
        return;

    gwy_assign(dest->priv->data, src->priv->data, src->xres*src->yres*src->zres);
}

/**
 * gwy_brick_get_dval:
 * @brick: A data brick.
 * @x: Position in data brick in range [0, x-resolution]. If the value is outside this range, the nearest border value
 *     is returned.
 * @y: Position in data brick in range [0, y-resolution]. If the value is outside this range, the nearest border value
 *     is returned.
 * @z: Position in data brick in range [0, z-resolution]. If the value is outside this range, the nearest border value
 *     is returned.
 * @interpolation: Interpolation method to use.
 *
 * Gets interpolated value at arbitrary data brick point indexed by pixel coordinates.
 *
 * Note that pixel values are centered in intervals [@i, @i+1]. See also gwy_brick_get_dval_real() that does the same,
 * but takes real coordinates.
 *
 * Returns: Value interpolated in the data brick.
 **/
gdouble
gwy_brick_get_dval(GwyBrick *brick,
                   gdouble x, gdouble y, gdouble z,
                   GwyInterpolationType interpolation)
{
    g_return_val_if_fail(GWY_IS_BRICK(brick), 0.0);

    if (interpolation != GWY_INTERPOLATION_ROUND) {
        g_warning("Only the ROUND interpolation method is implemented.");
        interpolation = GWY_INTERPOLATION_ROUND;
    }

    gint floorx = floor(x), floory = floor(y), floorz = floor(z);
    gint xres = brick->xres, yres = brick->yres, zres = brick->zres;
    gint ix = CLAMP(floorx, 0, xres-1);
    gint iy = CLAMP(floory, 0, yres-1);
    gint iz = CLAMP(floorz, 0, zres-1);

    if (interpolation == GWY_INTERPOLATION_ROUND)
        return brick->priv->data[(iz*yres + iy)*xres + ix];

    return 0.0;
}

/**
 * gwy_brick_get_dval_real:
 * @brick: A data brick.
 * @x: X postion in real coordinates.
 * @y: Y postition in real coordinates.
 * @z: Z postition in real coordinates. Z-calibration is not taken into account.
 * @interpolation: Interpolation method to use.
 *
 * Gets interpolated value at arbitrary data brick point indexed by pixel coordinates.
 *
 * Note pixel values are centered in intervals [@j, @j+1]. See also gwy_brick_get_dval() that does the same, but takes
 * pixel coordinates.
 *
 * Returns: Value interpolated in the data brick.
 **/
gdouble
gwy_brick_get_dval_real(GwyBrick *brick,
                        gdouble x, gdouble y, gdouble z,
                        GwyInterpolationType interpolation)
{
    g_return_val_if_fail(GWY_IS_BRICK(brick), 0.0);
    return gwy_brick_get_dval(brick,
                              x * brick->xres/brick->xreal, y * brick->xres/brick->xreal, z * brick->zres/brick->zreal,
                              interpolation);
}

/**
 * gwy_brick_get_data:
 * @brick: A data brick.
 *
 * Gets the raw data buffer of a data brick.
 *
 * The returned buffer is not guaranteed to be valid through whole data
 * brick life time.  Some function may change it, most notably
 * gwy_brick_resample().
 *
 * This function invalidates any cached information, use gwy_brick_get_data_const() if you are not going to change the
 * data.
 *
 * Returns: The data as an array of doubles of length @xres*@yres*@zres.
 **/
gdouble*
gwy_brick_get_data(GwyBrick *brick)
{
    g_return_val_if_fail(GWY_IS_BRICK(brick), NULL);
    gwy_brick_invalidate(brick);
    return brick->priv->data;
}

/**
 * gwy_brick_get_data_const:
 * @brick: A data brick.
 *
 * Gets the raw data buffer of a data brick, read-only.
 *
 * The returned buffer is not guaranteed to be valid through whole data brick life time.  Some function may change it,
 * most notably gwy_brick_resample().
 *
 * Use gwy_brick_get_data() if you want to change the data.
 *
 * Returns: The data as an array of doubles of length @xres*@yres*@zres.
 **/
const gdouble*
gwy_brick_get_data_const(GwyBrick *brick)
{
    g_return_val_if_fail(GWY_IS_BRICK(brick), NULL);
    return (const gdouble*)brick->priv->data;
}

/**
 * gwy_brick_get_data_with_size:
 * @brick: A data brick.
 * @out_count: (out): Number of items in the returned buffer.
 *
 * Gets the raw data buffer of a data brick. Used for gobject-introspection bindings.
 *
 * The returned buffer is not guaranteed to be valid through whole data
 * brick life time.  Some function may change it, most notably
 * gwy_brick_resample().
 *
 * This function invalidates any cached information.
 *
 * Returns: (transfer none) (array length=out_count):
 *          The data as an array of doubles of length @xres*@yres*@zres.
 **/
gdouble*
gwy_brick_get_data_with_size(GwyBrick *brick, gsize *out_count)
{
    g_return_val_if_fail(GWY_IS_BRICK(brick), NULL);
    gwy_brick_invalidate(brick);
    *out_count = (gsize)brick->xres * (gsize)brick->yres * (gsize)brick->zres;
    return brick->priv->data;
}

/**
 * gwy_brick_set_data_with_size:
 * @brick: A data brick.
 * @data: (array length=data_count): The data to be set as a pointer to an array of @data_len #gdoubles.
 * @data_count: Number of items in @data. It has to be equal to gwy_brick_get_xres()*gwy_brick_get_yres()*gwy_brick_get_zres(), else the function does nothing.
 *
 * Sets new values to the raw data buffer of a data brick. Used for gobject-introspection bindings.
 *
 * This function invalidates any cached information.
 **/
void
gwy_brick_set_data_with_size(GwyBrick *brick, gdouble *data, gsize data_count)
{
    g_return_if_fail(GWY_IS_BRICK(brick));
    if ((gsize)brick->xres * (gsize)brick->yres * (gsize)brick->zres == data_count) {
        gwy_brick_invalidate(brick);
	memcpy(brick->priv->data, data, data_count*sizeof(gdouble));
    }
}

/**
 * gwy_brick_invalidate:
 * @brick: A data brick to invalidate.
 *
 * Invalidates cached data brick stats.
 *
 * User code should rarely need this macro, as all #GwyBrick methods do proper invalidation when they change data,
 * as well as gwy_brick_get_data() does.
 *
 * However, if you get raw data with gwy_brick_get_data() and then mix direct changes to it with calls to methods like
 * gwy_brick_max(), you may need to explicitely invalidate cached values to let gwy_brick_max() know it has to
 * recompute the maximum.
 **/
void
gwy_brick_invalidate(G_GNUC_UNUSED GwyBrick *brick)
{
    /* Currently we do not cache anything but we should have an invalidate method in the API anyway. */
}

/**
 * gwy_brick_get_xres:
 * @brick: A data brick.
 *
 * Gets the x resolution of a data brick.
 *
 * Returns: Resolution (number of data points).
 **/
gint
gwy_brick_get_xres(GwyBrick *brick)
{
    g_return_val_if_fail(GWY_IS_BRICK(brick), 0);
    return brick->xres;
}

/**
 * gwy_brick_get_yres:
 * @brick: A data brick.
 *
 * Gets the y resolution of a data brick.
 *
 * Returns: Resolution (number of data points).
 **/
gint
gwy_brick_get_yres(GwyBrick *brick)
{
    g_return_val_if_fail(GWY_IS_BRICK(brick), 0);
    return brick->yres;
}
/**
 * gwy_brick_get_zres:
 * @brick: A data line.
 *
 * Gets the z resolution of a data brick.
 *
 * Returns: Resolution (number of data points).
 **/
gint
gwy_brick_get_zres(GwyBrick *brick)
{
    g_return_val_if_fail(GWY_IS_BRICK(brick), 0);
    return brick->zres;
}

/**
 * gwy_brick_get_xreal:
 * @brick: A data brick.
 *
 * Gets the physical size of a data brick in the x direction.
 *
 * Returns: Real size of a data brick the x direction.
 **/
gdouble
gwy_brick_get_xreal(GwyBrick *brick)
{
    g_return_val_if_fail(GWY_IS_BRICK(brick), 0.0);
    return brick->xreal;
}

/**
 * gwy_brick_get_yreal:
 * @brick: A data brick.
 *
 * Gets the physical size of a data brick in the y direction.
 *
 * Returns: Real size of a data brick the y direction.
 **/
gdouble
gwy_brick_get_yreal(GwyBrick *brick)
{
    g_return_val_if_fail(GWY_IS_BRICK(brick), 0.0);
    return brick->yreal;
}
/**
 * gwy_brick_get_zreal:
 * @brick: A data brick.
 *
 * Gets the physical size of a data brick in the z direction.
 *
 * Returns: Real size of a data brick the z direction.
 **/
gdouble
gwy_brick_get_zreal(GwyBrick *brick)
{
    g_return_val_if_fail(GWY_IS_BRICK(brick), 0.0);
    return brick->zreal;
}

/**
 * gwy_brick_get_xoffset:
 * @brick: A data brick.
 *
 * Gets the offset of data brick origin in x direction.
 *
 * Returns: Offset value.
 **/
gdouble
gwy_brick_get_xoffset(GwyBrick *brick)
{
    g_return_val_if_fail(GWY_IS_BRICK(brick), 0.0);
    return brick->xoff;
}

/**
 * gwy_brick_get_yoffset:
 * @brick: A data brick.
 *
 * Gets the offset of data brick origin in y direction.
 *
 * Returns: Offset value.
 **/
gdouble
gwy_brick_get_yoffset(GwyBrick *brick)
{
    g_return_val_if_fail(GWY_IS_BRICK(brick), 0.0);
    return brick->yoff;
}

/**
 * gwy_brick_get_zoffset:
 * @brick: A data brick.
 *
 * Gets the offset of data brick origin in z direction.
 *
 * Returns: Offset value.
 **/
gdouble
gwy_brick_get_zoffset(GwyBrick *brick)
{
    g_return_val_if_fail(GWY_IS_BRICK(brick), 0.0);
    return brick->zoff;
}

/**
 * gwy_brick_set_xoffset:
 * @brick: A data brick.
 * @xoffset: New offset value.
 *
 * Sets the offset of a data brick origin in the x direction.
 *
 * Note offsets don't affect any calculation, nor functions like gwy_brick_rtoi().
 **/
void
gwy_brick_set_xoffset(GwyBrick *brick,
                      gdouble xoffset)
{
    g_return_if_fail(GWY_IS_BRICK(brick));
    brick->xoff = xoffset;
}

/**
 * gwy_brick_set_yoffset:
 * @brick: A data brick.
 * @yoffset: New offset value.
 *
 * Sets the offset of a data brick origin in the y direction.
 *
 * Note offsets don't affect any calculation, nor functions like gwy_brick_rtoi().
 **/
void
gwy_brick_set_yoffset(GwyBrick *brick,
                      gdouble yoffset)
{
    g_return_if_fail(GWY_IS_BRICK(brick));
    brick->yoff = yoffset;
}
/**
 * gwy_brick_set_zoffset:
 * @brick: A data brick.
 * @zoffset: New offset value.
 *
 * Sets the offset of a data brick origin in the z direction.
 *
 * Note offsets don't affect any calculation, nor functions like gwy_brick_rtoi().
 **/
void
gwy_brick_set_zoffset(GwyBrick *brick,
                      gdouble zoffset)
{
    g_return_if_fail(GWY_IS_BRICK(brick));
    brick->zoff = zoffset;
}

/**
 * gwy_brick_set_xreal:
 * @brick: A data brick.
 * @xreal: New real x dimensions value
 *
 * Sets the real x dimension of a brick.
 **/
void
gwy_brick_set_xreal(GwyBrick *brick, gdouble xreal)
{
    g_return_if_fail(GWY_IS_BRICK(brick));
    g_return_if_fail(xreal > 0.0);
    brick->xreal = xreal;
}

/**
 * gwy_brick_set_yreal:
 * @brick: A data brick.
 * @yreal: New real y dimensions value
 *
 * Sets the real y dimension of a brick.
 **/
void
gwy_brick_set_yreal(GwyBrick *brick, gdouble yreal)
{
    g_return_if_fail(GWY_IS_BRICK(brick));
    g_return_if_fail(yreal > 0.0);
    brick->yreal = yreal;
}

/**
 * gwy_brick_set_zreal:
 * @brick: A data brick.
 * @zreal: New real z dimensions value
 *
 * Sets the real z dimension of a brick.
 **/
void
gwy_brick_set_zreal(GwyBrick *brick, gdouble zreal)
{
    g_return_if_fail(GWY_IS_BRICK(brick));
    g_return_if_fail(zreal > 0.0);
    brick->zreal = zreal;
}

/**
 * gwy_brick_get_dx:
 * @brick: A data brick.
 *
 * Gets the horizontal (X) voxel size of a brick in real units.
 *
 * The result is the same as gwy_brick_get_xreal(brick)/gwy_brick_get_xres(brick).
 *
 * Returns: Horizontal voxel size.
 **/
gdouble
gwy_brick_get_dx(GwyBrick *brick)
{
    g_return_val_if_fail(GWY_IS_BRICK(brick), 0.0);
    return brick->xreal/brick->xres;
}

/**
 * gwy_brick_get_dy:
 * @brick: A data brick.
 *
 * Gets the vertical (Y) voxel size of a brick in real units.
 *
 * The result is the same as gwy_brick_get_yreal(brick)/gwy_brick_get_yres(brick).
 *
 * Returns: Vertical voxel size.
 **/
gdouble
gwy_brick_get_dy(GwyBrick *brick)
{
    g_return_val_if_fail(GWY_IS_BRICK(brick), 0.0);
    return brick->yreal/brick->yres;
}

/**
 * gwy_brick_get_dz:
 * @brick: A data brick.
 *
 * Gets the level-wise (Z) voxel size of a brick in real units.
 *
 * The result is the same as gwy_brick_get_zreal(brick)/gwy_brick_get_zres(brick).
 *
 * Note that it cannot -- and hence does not -- take into account any attached Z calibration.
 *
 * Returns: Level-wise voxel size.
 **/
gdouble
gwy_brick_get_dz(GwyBrick *brick)
{
    g_return_val_if_fail(GWY_IS_BRICK(brick), 0.0);
    return brick->zreal/brick->zres;
}

/**
 * gwy_brick_get_unit_x:
 * @brick: A data brick.
 *
 * Returns x direction SI unit of a data brick.
 *
 * The returned object can be modified to change the brick x units.
 *
 * Returns: (transfer none):
 *          SI unit corresponding to the lateral (X) dimension of the data brick.  Its reference count is not
 *          incremented.
 **/
GwyUnit*
gwy_brick_get_unit_x(GwyBrick *brick)
{
    g_return_val_if_fail(GWY_IS_BRICK(brick), NULL);

    GwyBrickPrivate *priv = brick->priv;
    if (!priv->unit_x)
        priv->unit_x = gwy_unit_new(NULL);

    return priv->unit_x;
}

/**
 * gwy_brick_get_unit_y:
 * @brick: A data brick.
 *
 * Returns y direction SI unit of a data brick.
 *
 * The returned object can be modified to change the brick y units.
 *
 * Returns: (transfer none):
 *          SI unit corresponding to the lateral (Y) dimension of the data brick.  Its reference count is not
 *          incremented.
 **/
GwyUnit*
gwy_brick_get_unit_y(GwyBrick *brick)
{
    g_return_val_if_fail(GWY_IS_BRICK(brick), NULL);

    GwyBrickPrivate *priv = brick->priv;
    if (!priv->unit_y)
        priv->unit_y = gwy_unit_new(NULL);

    return priv->unit_y;
}

/**
 * gwy_brick_get_unit_z:
 * @brick: A data brick.
 *
 * Returns z direction SI unit of a data brick.
 *
 * The returned object can be modified to change the brick z units (not taking into account z-calibration).
 *
 * Returns: (transfer none):
 *          SI unit corresponding to the "height" (Z) dimension of the data brick.  Its reference count is not
 *          incremented.
 **/
GwyUnit*
gwy_brick_get_unit_z(GwyBrick *brick)
{
    g_return_val_if_fail(GWY_IS_BRICK(brick), NULL);

    GwyBrickPrivate *priv = brick->priv;
    if (!priv->unit_z)
        priv->unit_z = gwy_unit_new(NULL);

    return priv->unit_z;
}

/**
 * gwy_brick_get_unit_w:
 * @brick: A data brick.
 *
 * Returns value SI unit of a data brick.
 *
 * The returned object can be modified to change the brick value units.
 *
 * Returns: (transfer none):
 *          SI unit corresponding to the "value" of the data brick.  Its reference count is not incremented.
 **/
GwyUnit*
gwy_brick_get_unit_w(GwyBrick *brick)
{
    g_return_val_if_fail(GWY_IS_BRICK(brick), NULL);

    GwyBrickPrivate *priv = brick->priv;
    if (!priv->unit_w)
        priv->unit_w = gwy_unit_new(NULL);

    return priv->unit_w;
}

/**
 * gwy_brick_copy_units:
 * @brick: A data brick.
 * @target: Target data brick.
 *
 * Sets lateral and value units of a data brick to match another data brick.
 **/
void
gwy_brick_copy_units(GwyBrick *brick, GwyBrick *target)
{
    g_return_if_fail(GWY_IS_BRICK(brick));
    g_return_if_fail(GWY_IS_BRICK(target));

    GwyBrickPrivate *priv = brick->priv, *tpriv = target->priv;
    _gwy_copy_unit(priv->unit_x, &tpriv->unit_x);
    _gwy_copy_unit(priv->unit_y, &tpriv->unit_y);
    _gwy_copy_unit(priv->unit_z, &tpriv->unit_z);
    _gwy_copy_unit(priv->unit_w, &tpriv->unit_w);
}

/**
 * gwy_brick_get_value_format_x:
 * @brick: A data brick.
 * @style: Unit format style.
 * @format: (nullable): A SI value format to modify, or %NULL to allocate a new one.
 *
 * Finds value format good for displaying coordinates of a data brick.
 *
 * Returns: The value format.  If @format is %NULL, a newly allocated format is returned, otherwise (modified) @format
 *          itself is returned.
 **/
GwyValueFormat*
gwy_brick_get_value_format_x(GwyBrick *brick,
                             GwyUnitFormatStyle style,
                             GwyValueFormat *format)
{
    g_return_val_if_fail(GWY_IS_BRICK(brick), format);

    gdouble max = brick->xreal;
    gdouble unit = brick->xreal/brick->xres;
    return gwy_unit_get_format_with_resolution(gwy_brick_get_unit_x(brick), style, max, unit, format);
}

/**
 * gwy_brick_get_value_format_y:
 * @brick: A data brick.
 * @style: Unit format style.
 * @format: (nullable): A SI value format to modify, or %NULL to allocate a new one.
 *
 * Finds value format good for displaying values of a data brick.
 *
 * Returns: The value format.  If @format is %NULL, a newly allocated format is returned, otherwise (modified) @format
 *          itself is returned.
 **/
GwyValueFormat*
gwy_brick_get_value_format_y(GwyBrick *brick,
                             GwyUnitFormatStyle style,
                             GwyValueFormat *format)
{
    g_return_val_if_fail(GWY_IS_BRICK(brick), format);

    gdouble max = brick->yreal;
    gdouble unit = brick->yreal/brick->yres;
    return gwy_unit_get_format_with_resolution(gwy_brick_get_unit_y(brick), style, max, unit, format);
}

/**
 * gwy_brick_get_value_format_z:
 * @brick: A data brick.
 * @style: Unit format style.
 * @format: (nullable): A SI value format to modify, or %NULL to allocate a new one.
 *
 * Finds value format good for displaying values of a data brick.
 *
 * Returns: The value format.  If @format is %NULL, a newly allocated format is returned, otherwise (modified) @format
 *          itself is returned.
 **/
GwyValueFormat*
gwy_brick_get_value_format_z(GwyBrick *brick,
                             GwyUnitFormatStyle style,
                             GwyValueFormat *format)
{
    g_return_val_if_fail(GWY_IS_BRICK(brick), format);

    gdouble max = brick->zreal;
    gdouble unit = brick->zreal/brick->zres;
    return gwy_unit_get_format_with_resolution(gwy_brick_get_unit_z(brick), style, max, unit, format);
}

/**
 * gwy_brick_min:
 * @brick: A data brick.
 *
 * Find the minimum value in a data brick.
 *
 * Returns: The minimum value within the brick.
 **/
gdouble
gwy_brick_min(GwyBrick *brick)
{
    g_return_val_if_fail(GWY_IS_BRICK(brick), G_MAXDOUBLE);

    gsize n = (gsize)brick->xres * (gsize)brick->yres * (gsize)brick->zres;
    const gdouble *d = brick->priv->data;
    gdouble min = d[0];
    for (gsize i = 1; i < n; i++)
        min = fmin(min, d[i]);

    return min;
}

/**
 * gwy_brick_max:
 * @brick: A data brick.
 *
 * Find the maximum value in a data brick.
 *
 * Returns: The maximum value within the brick.
 **/
gdouble
gwy_brick_max(GwyBrick *brick)
{
    g_return_val_if_fail(GWY_IS_BRICK(brick), -G_MAXDOUBLE);

    gsize n = (gsize)brick->xres * (gsize)brick->yres * (gsize)brick->zres;
    const gdouble *d = brick->priv->data;
    gdouble max = d[0];
    for (gsize i = 1; i < n; i++)
        max = fmax(max, d[i]);

    return max;
}

/**
 * gwy_brick_get_value_format_w:
 * @brick: A data brick.
 * @style: Unit format style.
 * @format: (nullable): A SI value format to modify, or %NULL to allocate a new one.
 *
 * Finds value format good for displaying values of a data brick.
 *
 * Note this functions searches for minimum and maximum value in @brick, therefore it's relatively slow.
 *
 * Returns: The value format.  If @format is %NULL, a newly allocated format is returned, otherwise (modified) @format
 *          itself is returned.
 **/
GwyValueFormat*
gwy_brick_get_value_format_w(GwyBrick *brick,
                             GwyUnitFormatStyle style,
                             GwyValueFormat *format)
{
    g_return_val_if_fail(GWY_IS_BRICK(brick), format);

    gdouble max = gwy_brick_max(brick);
    gdouble min = gwy_brick_min(brick);
    if (max == min) {
        max = ABS(max);
        min = 0.0;
    }

    return gwy_unit_get_format(gwy_brick_get_unit_w(brick), style, max - min, format);
}

/**
 * gwy_brick_itor:
 * @brick: A data brick.
 * @pixpos: Pixel coordinate.
 *
 * Transforms pixel coordinate to real (physical) coordinate in x direction.
 *
 * That is it maps range [0..x resolution] to range [0..x real-size].  It is not suitable for conversion of matrix
 * indices to physical coordinates, you have to use gwy_brick_itor(@brick, @pixpos + 0.5) for that.
 *
 * Returns: @pixpos in real coordinates.
 **/
gdouble
gwy_brick_itor(GwyBrick *brick, gdouble pixpos)
{
    g_return_val_if_fail(GWY_IS_BRICK(brick), 0.0);
    return pixpos * brick->xreal/brick->xres;
}

/**
 * gwy_brick_jtor:
 * @brick: A data brick.
 * @pixpos: Pixel coordinate.
 *
 * Transforms pixel coordinate to real (physical) coordinate in y direction.
 *
 * That is it maps range [0..y resolution] to range [0..y real-size].  It is not suitable for conversion of matrix
 * indices to physical coordinates, you have to use gwy_brick_itor(@brick, @pixpos + 0.5) for that.
 *
 * Returns: @pixpos in real coordinates.
 **/
gdouble
gwy_brick_jtor(GwyBrick *brick, gdouble pixpos)
{
    g_return_val_if_fail(GWY_IS_BRICK(brick), 0.0);
    return pixpos * brick->yreal/brick->yres;
}

/**
 * gwy_brick_ktor:
 * @brick: A data brick.
 * @pixpos: Pixel coordinate.
 *
 * Transforms pixel coordinate to real (physical) coordinate in z direction.
 *
 * That is it maps range [0..z resolution] to range [0..z real-size].  It is not suitable for conversion of matrix
 * indices to physical coordinates, you have to use gwy_brick_itor(@brick, @pixpos + 0.5) for that.
 *
 * Returns: @pixpos in real coordinates.
 **/
gdouble
gwy_brick_ktor(GwyBrick *brick, gdouble pixpos)
{
    g_return_val_if_fail(GWY_IS_BRICK(brick), 0.0);
    return pixpos * brick->zreal/brick->zres;
}

/**
 * gwy_brick_rtoi:
 * @brick: A data brick.
 * @realpos: Real coordinate.
 *
 * Transforms real (physical) coordinate to pixel coordinate in x axis.
 *
 * That is it maps range [0..x real-size] to range [0..x resolution].
 *
 * Returns: @realpos in pixel coordinates.
 **/
gdouble
gwy_brick_rtoi(GwyBrick *brick, gdouble realpos)
{
    g_return_val_if_fail(GWY_IS_BRICK(brick), 0.0);
    return realpos * brick->xres/brick->xreal;
}

/**
 * gwy_brick_rtoj:
 * @brick: A data brick.
 * @realpos: Real coordinate.
 *
 * Transforms real (physical) coordinate to pixel coordinate in y axis.
 *
 * That is it maps range [0..y real-size] to range [0..y resolution].
 *
 * Returns: @realpos in pixel coordinates.
 **/
gdouble
gwy_brick_rtoj(GwyBrick *brick, gdouble realpos)
{
    g_return_val_if_fail(GWY_IS_BRICK(brick), 0.0);
    return realpos * brick->yres/brick->yreal;
}

/**
 * gwy_brick_rtok:
 * @brick: A data brick.
 * @realpos: Real coordinate.
 *
 * Transforms real (physical) coordinate to pixel coordinate in z axis.
 *
 * That is it maps range [0..z real-size] to range [0..z resolution].
 *
 * Returns: @realpos in pixel coordinates.
 **/
gdouble
gwy_brick_rtok(GwyBrick *brick, gdouble realpos)
{
    g_return_val_if_fail(GWY_IS_BRICK(brick), 0.0);
    return realpos * brick->zres/brick->zreal;
}

/**
 * gwy_brick_ktor_cal:
 * @brick: A data brick.
 * @pixpos: Pixel coordinate.
 *
 * Transforms pixel coordinate to real (physical) coordinate in z direction, taking into account calibration.
 *
 * Unlike gwy_brick_ktor(), this function takes into account the @z calibration and, if calibration is not present,
 * the @z axis offset. Since the calibration is available only for discrete pixel coordinates, the values are
 * interpolated between and clamped if outside the range.
 *
 * The values in the calibration are assumed to correspond to pixel centres. This convention is also kept when no
 * calibration is present.
 *
 * Returns: @pixpos in real coordinates.
 **/
gdouble
gwy_brick_ktor_cal(GwyBrick *brick,
                   gdouble pixpos)
{
    g_return_val_if_fail(GWY_IS_BRICK(brick), 0.0);

    GwyBrickPrivate *priv = brick->priv;
    GwyLine *calibration = priv->zcalibration;
    const gdouble *cdata;
    gint i;

    if (!calibration)
        return (pixpos + 0.5)*brick->zreal/brick->zres + brick->zoff;

    i = (gint)floor(pixpos);
    cdata = calibration->priv->data;
    if (i < 0)
        return cdata[0];
    if (i >= calibration->res-1)
        return cdata[calibration->res-1];

    pixpos -= i;
    return cdata[i]*(1.0 - pixpos) + cdata[i+1]*pixpos;
}

/**
 * gwy_brick_rtok_cal:
 * @brick: A data brick.
 * @realpos: Real coordinate.
 *
 * Transforms real (physical) coordinate to pixel coordinate in z axis, taking into account calibration.
 *
 * Unlike gwy_brick_rtok(), this function takes into account the @z calibration and, if calibration is not present,
 * the @z axis offset. Since the calibration is available only for discrete pixel coordinates, the values are
 * interpolated between and clamped if outside the range.
 *
 * The values in the calibration are assumed to correspond to pixel centres. This convention is also kept when no
 * calibration is present.
 *
 * Returns: @realpos in pixel coordinates.
 **/
gdouble
gwy_brick_rtok_cal(GwyBrick *brick,
                   gdouble realpos)
{
    g_return_val_if_fail(GWY_IS_BRICK(brick), 0.0);

    GwyBrickPrivate *priv = brick->priv;
    GwyLine *calibration = priv->zcalibration;
    const gdouble *cdata;
    gdouble t;
    gint i, j, k;

    if (!calibration)
        return (realpos - brick->zoff)/brick->zreal*brick->zres - 0.5;

    cdata = calibration->priv->data;
    i = 0;
    j = calibration->res-1;
    if (cdata[i] <= cdata[j]) {
        /* The normal increasing case. */
        if (realpos <= cdata[i])
            return i;
        if (realpos >= cdata[j])
            return j;

        /* Now cdata[i] < realpos < cdata[j]. Use bisection. */
        do {
            k = (i + j)/2;
            if (realpos < cdata[k])
                j = k;
            else
                i = k;
        } while (j > i+1);

        g_assert(j == i+1);

        if (cdata[j] <= cdata[i])
            return i;

        t = (realpos - cdata[i])/(cdata[j] - cdata[i]);
        return i + GWY_CLAMP(t, 0.0, 1.0);
    }
    else {
        /* The decreasing case. XXX: We probably do not actually support this and other things can break. */
        if (realpos <= cdata[j])
            return j;
        if (realpos >= cdata[i])
            return i;

        /* Now cdata[i] < realpos < cdata[j]. Use bisection. */
        do {
            k = (i + j)/2;
            if (realpos < cdata[k])
                i = k;
            else
                j = k;
        } while (j > i+1);

        g_assert(j == i+1);

        if (cdata[i] <= cdata[j])
            return i;

        t = (realpos - cdata[j])/(cdata[i] - cdata[j]);
        return j - GWY_CLAMP(t, 0.0, 1.0);
    }
}

static inline gboolean
_gwy_brick_check_inside(GwyBrick *brick, gint col, gint row, gint lev)
{
    return col >= 0 && col < brick->xres && row >= 0 && row < brick->yres && lev >= 0 && lev < brick->zres;
}

/**
 * gwy_brick_get_val:
 * @brick: A data brick.
 * @col: Position in the brick (column index).
 * @row: Position in the brick (row index).
 * @lev: Position in the brick (level index).
 *
 * Gets value at given position in a data brick.
 *
 * Do not access data with this function inside inner loops, it's slow. Get raw data buffer with
 * gwy_brick_get_data_const() and access it directly instead.
 *
 * Returns: Value at given index.
 **/
gdouble
gwy_brick_get_val(GwyBrick *brick,
                  gint col,
                  gint row,
                  gint lev)
{
    g_return_val_if_fail(_gwy_brick_check_inside(brick, col, row, lev), 0.0);
    return brick->priv->data[col + brick->xres*row + brick->xres*brick->yres*lev];
}

/**
 * gwy_brick_set_val:
 * @brick: A data brick.
 * @col: Position in the brick (column index).
 * @row: Position in the brick (row index).
 * @lev: Position in the brick (level index).
 * @value: Value to be set.
 *
 * Sets value at given position in a data brick.
 *
 * Do not access data with this function inside inner loops, it's slow. Get raw data buffer with
 * gwy_brick_get_data_const() and access it directly instead.
 **/
void
gwy_brick_set_val(GwyBrick *brick,
                  gint col,
                  gint row,
                  gint lev,
                  gdouble value)
{
    g_return_if_fail(_gwy_brick_check_inside(brick, col, row, lev));
    brick->priv->data[col + brick->xres*row + brick->xres*brick->yres*lev] = value;
}

/**
 * gwy_brick_get_val_real:
 * @brick: A data brick.
 * @x: Position in the brick (x direction).
 * @y: Position in the brick (y direction).
 * @z: Position in the brick (z direction).
 *
 * Gets value at given position in a data brick, in real coordinates.
 *
 * Do not access data with this function inside inner loops, it's slow.
 * Get raw data buffer with gwy_brick_get_data_const() and access it
 * directly instead.
 *
 * Returns: Value at given index.
 **/
gdouble
gwy_brick_get_val_real(GwyBrick *brick,
                       gdouble x,
                       gdouble y,
                       gdouble z)
{
    gint col = gwy_brick_rtoi(brick, x);
    gint row = gwy_brick_rtoj(brick, y);
    gint lev = gwy_brick_rtok(brick, z);

    g_return_val_if_fail(_gwy_brick_check_inside(brick, col, row, lev), 0.0);

    return brick->priv->data[col + brick->xres*row + brick->xres*brick->yres*lev];
}

/**
 * gwy_brick_set_val_real:
 * @brick: A data brick.
 * @x: Position in the brick (x direction).
 * @y: Position in the brick (y direction).
 * @z: Position in the brick (z direction).
 * @value: Value to be set.
 *
 * Sets value at given position in a data brick.
 *
 * Do not access data with this function inside inner loops, it's slow.
 * Get raw data buffer with gwy_brick_get_data_const() and access it
 * directly instead.
 **/
void
gwy_brick_set_val_real(GwyBrick *brick,
                       gdouble x,
                       gdouble y,
                       gdouble z,
                       gdouble value)
{
    gint col = gwy_brick_rtoi(brick, x);
    gint row = gwy_brick_rtoj(brick, y);
    gint lev = gwy_brick_rtok(brick, z);

    g_return_if_fail(_gwy_brick_check_inside(brick, col, row, lev));

    brick->priv->data[col + brick->xres*row + brick->xres*brick->yres*lev] = value;
}

/**
 * gwy_brick_fill:
 * @brick: A data brick.
 * @value: Value to fill data brick with.
 *
 * Fills a data brick with specified value.
 **/
void
gwy_brick_fill(GwyBrick *brick,
               gdouble value)
{
    g_return_if_fail(GWY_IS_BRICK(brick));
    gsize n = (gsize)brick->xres * (gsize)brick->yres * (gsize)brick->zres;
    gdouble *d = brick->priv->data;
    for (gsize i = 0; i < n; i++)
        d[i] = value;
}

/**
 * gwy_brick_clear:
 * @brick: A data brick.
 *
 * Fills a data brick with zeroes.
 **/
void
gwy_brick_clear(GwyBrick *brick)
{
    g_return_if_fail(GWY_IS_BRICK(brick));
    gsize n = (gsize)brick->xres * (gsize)brick->yres * (gsize)brick->zres;
    gwy_clear(brick->priv->data, n);
}

/**
 * gwy_brick_add:
 * @brick: A data brick.
 * @value: Value to be added.
 *
 * Adds a specified value to all values in a data brick.
 **/
void
gwy_brick_add(GwyBrick *brick,
              gdouble value)
{
    g_return_if_fail(GWY_IS_BRICK(brick));
    gsize n = (gsize)brick->xres * (gsize)brick->yres * (gsize)brick->zres;
    gdouble *d = brick->priv->data;
    for (gsize i = 0; i < n; i++)
        d[i] += value;
}

/**
 * gwy_brick_multiply:
 * @brick: A data brick.
 * @value: Value to multiply data brick with.
 *
 * Multiplies all values in a data brick with a specified value.
 **/
void
gwy_brick_multiply(GwyBrick *brick,
                   gdouble value)
{
    g_return_if_fail(GWY_IS_BRICK(brick));
    gsize n = (gsize)brick->xres * (gsize)brick->yres * (gsize)brick->zres;
    gdouble *d = brick->priv->data;
    for (gsize i = 0; i < n; i++)
        d[i] *= value;
}

static gboolean
gwy_brick_extract_field_common(GwyBrick *brick,
                               GwyField *target,
                               gint istart, gint jstart, gint kstart,
                               gint width, gint height, gint depth,
                               gboolean keep_offsets)
{
    gint xres, yres, zres;
    gdouble dx, dy, dz;

    g_return_val_if_fail(GWY_IS_BRICK(brick), FALSE);
    g_return_val_if_fail(GWY_IS_FIELD(target), FALSE);
    g_return_val_if_fail((width == -1 && height > 0 && depth > 0)
                         || (width > 0 && height == -1 && depth > 0)
                         || (width > 0 && height > 0 && depth == -1), FALSE);
    g_return_val_if_fail(_gwy_brick_check_inside(brick, istart, jstart, kstart), FALSE);

    xres = brick->xres;
    yres = brick->yres;
    zres = brick->zres;
    dx = brick->xreal/xres;
    dy = brick->yreal/yres;
    dz = brick->zreal/zres;

    if (width == -1 && height > 0 && depth > 0) {
        g_return_val_if_fail(jstart + height <= yres, FALSE);
        g_return_val_if_fail(kstart + depth <= zres, FALSE);
        gwy_field_resize(target, height, depth);
        target->xreal = dy*height;
        target->yreal = dz*depth;
        if (keep_offsets) {
            target->xoff = dy*jstart + brick->yoff;
            target->yoff = dz*kstart + brick->zoff;
        }
        _gwy_copy_unit(brick->priv->unit_y, &target->priv->unit_xy);
    }
    else if (width > 0 && height == -1 && depth > 0) {
        g_return_val_if_fail(istart + width <= xres, FALSE);
        g_return_val_if_fail(kstart + depth <= zres, FALSE);
        gwy_field_resize(target, width, depth);
        target->xreal = dx*width;
        target->yreal = dz*depth;
        if (keep_offsets) {
            target->xoff = dx*istart + brick->xoff;
            target->yoff = dz*kstart + brick->zoff;
        }
        _gwy_copy_unit(brick->priv->unit_x, &target->priv->unit_xy);
    }
    else if (width > 0 && height > 0 && depth == -1) {
        g_return_val_if_fail(istart + width <= xres, FALSE);
        g_return_val_if_fail(jstart + height <= yres, FALSE);
        gwy_field_resize(target, width, height);
        target->xreal = dx*width;
        target->yreal = dy*height;
        if (keep_offsets) {
            target->xoff = dx*istart + brick->xoff;
            target->yoff = dy*jstart + brick->yoff;
        }
        _gwy_copy_unit(brick->priv->unit_x, &target->priv->unit_xy);
    }
    else {
        g_assert_not_reached();
    }

    if (!keep_offsets)
        target->xoff = target->yoff = 0.0;

    return TRUE;
}

/**
 * gwy_brick_extract_plane:
 * @brick: A data brick.
 * @target: Datafield to be filled by extracted plane. It will be resized if necessary.
 * @istart: Column where to start (pixel coordinates).
 * @jstart: Row where to start (pixel coordinates).
 * @kstart: Level where to start (pixel coordinates).
 * @width: Pixel width of extracted plane. If @width is -1, the yz plane will be extracted.
 * @height: Pixel height of extracted plane.  If @height is -1, the xz plane will be extracted
 * @depth: Pixel depth of extracted plane. If @depth is -1, the xy plane will be extracted
 * @keep_offsets: Keep the physical offsets in extracted field.
 *
 * Extracts a plane from a data brick.
 *
 * One value of set (@width, @height, @depth) needs to be -1, determining the plane orientation.
 *
 * Use gwy_brick_extract_xy_plane() to simply extract one entire XY plane.
 **/
void
gwy_brick_extract_plane(GwyBrick *brick,
                        GwyField *target,
                        gint istart, gint jstart, gint kstart,
                        gint width, gint height, gint depth,
                        gboolean keep_offsets)
{
    gint col, row, lev, xres, yres;
    const gdouble *bdata, *b;
    gdouble *ddata, *d;

    if (!gwy_brick_extract_field_common(brick, target, istart, jstart, kstart, width, height, depth, keep_offsets))
        return;

    xres = brick->xres;
    yres = brick->yres;
    bdata = brick->priv->data;
    ddata = target->priv->data;

    if (width == -1 && height > 0 && depth > 0) {
        col = istart;
        d = ddata;
        for (lev = 0; lev < depth; lev++) {
            b = bdata + col + xres*jstart + xres*yres*(lev + kstart);
            for (row = 0; row < height; row++, d++, b += xres)
                *d = *b;
        }
    }
    else if (width > 0 && height == -1 && depth > 0) {
        row = jstart;
        for (lev = 0; lev < depth; lev++) {
            gwy_assign(ddata + lev*width,
                       bdata + istart + xres*row + xres*yres*(lev + kstart),
                       width);
        }
    }
    else if (width > 0 && height > 0 && depth == -1) {
        lev = kstart;
        for (row = 0; row < height; row++) {
            gwy_assign(ddata + row*width,
                       bdata + istart + xres*(row + jstart) + xres*yres*(lev),
                       width);
        }
    }

    _gwy_copy_unit(brick->priv->unit_w, &target->priv->unit_z);
    gwy_field_invalidate(target);
}

/**
 * gwy_brick_new_field_like_xy_plane:
 * @brick: A data brick.
 * @nullme: Whether the data field should be initialized to zeros. If %FALSE, the data will not be initialized.
 *
 * Creates a new data field matching one XY plane of a data brick.
 *
 * The pixel dimensions, physical dimensions, offsets and units will match the brick plane. The brick should have
 * identical @x and @y units.
 *
 * There are a number of functions which create similar fields already filled with useful data, for example
 * gwy_brick_extract_xy_plane() or gwy_brick_sum_xy_plane().
 *
 * Returns: (transfer full):
 *          A new data field matching one @brick plane.
 **/
GwyField*
gwy_brick_new_field_like_xy_plane(GwyBrick *brick,
                                  gboolean nullme)
{
    GwyField *field = gwy_field_new(1, 1, 1.0, 1.0, nullme);
    g_return_val_if_fail(GWY_IS_BRICK(brick), field);
    gwy_brick_extract_field_common(brick, field, 0, 0, 0, brick->xres, brick->yres, -1, TRUE);
    _gwy_copy_unit(brick->priv->unit_w, &field->priv->unit_z);
    return field;
}

/**
 * gwy_brick_extract_xy_plane:
 * @brick: A data brick.
 * @target: Datafield to be filled by extracted plane. It will be resized if necessary.
 * @lev: Position in the brick (level index).
 *
 * Extracts one full XY plane of a data brick to a data field.
 **/
void
gwy_brick_extract_xy_plane(GwyBrick *brick,
                           GwyField *target,
                           gint lev)
{
    g_return_if_fail(GWY_IS_BRICK(brick));
    gwy_brick_extract_plane(brick, target, 0, 0, lev, brick->xres, brick->yres, -1, TRUE);
}

/**
 * gwy_brick_sum_plane:
 * @brick: A data brick.
 * @target: Datafield to be filled by summed plane. It will be resized if necessary.
 * @istart: Column where to start (pixel coordinates).
 * @jstart: Row where to start (pixel coordinates).
 * @kstart: Level where to start (pixel coordinates).
 * @width: Pixel width of summed plane. If @width is -1, the yz planes will be summed.
 * @height: Pixel height of summed plane.  If @height is -1, the xz planes will be summed
 * @depth: Pixel depth of summed plane. If @depth is -1, the xy planes will be summed
 * @keep_offsets: Keep the physical offsets in extracted field.
 *
 * Sums planes in certain direction
 *
 * One value of set (@width, @height, @depth) needs to be -1, determining the plane orientation. In contrast to
 * gwy_brick_extract_plane, the appropriate start coordinate (e.g. @istart if @width = -1) is not used for single
 * plane extraction, but the planes are accumulated in whole range (0..xres for given example).
 **/
void
gwy_brick_sum_plane(GwyBrick *brick,
                    GwyField *target,
                    gint istart, gint jstart, gint kstart,
                    gint width, gint height, gint depth,
                    gboolean keep_offsets)
{
    gint col, row, lev, xres, yres, zres;
    const gdouble *bdata, *brow;
    gdouble *ddata, *drow;

    if (!gwy_brick_extract_field_common(brick, target, istart, jstart, kstart, width, height, depth, keep_offsets))
        return;

    xres = brick->xres;
    yres = brick->yres;
    zres = brick->zres;
    bdata = brick->priv->data;
    gwy_field_clear(target);
    ddata = target->priv->data;

    if (width == -1 && height > 0 && depth > 0) {
        /* Sum in planes along x scan lines.  Here the good memory access strategy is trivial; just accumulate each
         * scan line. Target locations never collide, so parallelisation is safe. */
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            private(brow,lev,row,col) \
            shared(bdata,ddata,xres,yres,jstart,kstart,height,depth)
#endif
        for (lev = 0; lev < depth; lev++) {
            for (row = 0; row < height; row++) {
                gdouble s = 0.0;

                brow = bdata + xres*(row + jstart) + xres*yres*(lev + kstart);
                for (col = 0; col < xres; col++)
                    s += brow[col];
                ddata[row + lev*height] = s;
            }
        }
    }
    else if (width > 0 && height == -1 && depth > 0) {
        /* Sum in planes, but in y direction.  This means for each brick plane we have one ‘active’ row of the output
         * field where we accumulate all the x scan lines in the brick plane. Target locations with different lev do
         * not collide, so outer for-cycle parallelisation is safe. */
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            private(brow,drow,lev,row,col) \
            shared(bdata,ddata,xres,yres,istart,kstart,width,depth)
#endif
        for (lev = 0; lev < depth; lev++) {
            for (row = 0; row < yres; row++) {
                brow = bdata + istart + xres*row + xres*yres*(lev + kstart);
                drow = ddata + lev*width;
                for (col = 0; col < width; col++)
                    drow[col] += brow[col];
            }
        }
    }
    else if (width > 0 && height > 0 && depth == -1) {
        /* Sum along z profiles.  This means for each brick plane iterate synchronously through this plane and the
         * output field, and accumulate. Target locations always collide. */
        /* We have many parallelisation options:
         * 1. Switch loop order and make iteration by row the outermost loop. This removes collisions, results in
         *    still mostly linear memory access and can be handled by simple parallel for.
         *    Verdict: Somewhat faster than serial, maybe 10 %.
         *
         * 2. Use generic parallel, query the number of threads and thread id, form blocks explicitly and execute them
         *    (hiding the ugly parts in helper functions in gwyprocessinternal).
         *    Verdict: The fastest, but just barely, about 1/3 faster, requires code modification (explicit split to
         *    blocks).
         *
         * 3. Write it using tasks, except tasks should be larger than a single addition, so we would need explicit
         *    blocks.  We can make the row cycle to produce tasks and declare dependencies between the arrays – this
         *    seems complicated.
         *    Verdict: ??? too complicated to try.
         *
         * 4. Add parallel for pragma to the middle loop instead of the outermost one.  This removes collisions, but
         *    probably splits the work to quite small pieces.
         *    Verdict: The second fastest, about 1/3 faster and trivial to implement.
         *
         * 5. Add taskloop pragma to the middle loop.  Here we can control granularity with grainsize.
         *    Verdict: ??? too complicated to try.
         *
         * 6. Use atomic construct –  atomic update.  Isn't it too expensive for plain summation?
         *    Verdict: Yes, it is very slow.
         *
         * 7. In OpenMP 4.5+ we can use reduction(+:array[:size]) on ddata. However, we must ensure the compiler sees
         *    us accessing ddata, not drow!
         *    Verdict: Slower than serial.
         *
         * 8. Probably more...
         *
         * The winner is variant 4, but the speedup is nothing to write home about.  Memory bandwidth limited? */
        for (lev = 0; lev < zres; lev++) {
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            private(brow,drow,row,col) \
            shared(bdata,ddata,xres,yres,istart,jstart,height,width,lev)
#endif
            for (row = 0; row < height; row++) {
                brow = bdata + istart + xres*(row + jstart) + xres*yres*lev;
                drow = ddata + row*width;
                for (col = 0; col < width; col++)
                    drow[col] += brow[col];
            }
        }
    }

    _gwy_copy_unit(brick->priv->unit_w, &target->priv->unit_z);
    gwy_field_invalidate(target);
}

/**
 * gwy_brick_sum_xy_plane:
 * @brick: A data brick.
 * @target: Datafield to be filled by the summary data. It will be resized if necessary.
 *
 * Sums all z-profiles of a data brick to a data field.
 *
 * The result is an xy plane and can be alternatively imagined as the sum of all xy planes.
 **/
void
gwy_brick_sum_xy_plane(GwyBrick *brick, GwyField *target)
{
    g_return_if_fail(GWY_IS_BRICK(brick));
    gwy_brick_sum_plane(brick, target, 0, 0, 0, brick->xres, brick->yres, -1, TRUE);
}

/**
 * gwy_brick_min_plane:
 * @brick: A data brick.
 * @target: Datafield to be filled by the minima plane. It will be resized if necessary.
 * @istart: Column where to start (pixel coordinates).
 * @jstart: Row where to start (pixel coordinates).
 * @kstart: Level where to start (pixel coordinates).
 * @width: Pixel width of summarized plane. If @width is -1, the yz planes will be summarized.
 * @height: Pixel height of summarized plane.  If @height is -1, the xz planes will be summarized.
 * @depth: Pixel depth of summarized plane. If @depth is -1, the xy planes will be summarized
 * @keep_offsets: Keep the physical offsets in extracted field.
 *
 * Finds minima of profiles in certain direction.
 *
 * One value of set (@width, @height, @depth) needs to be -1, determining the plane orientation. In contrast to
 * gwy_brick_extract_plane, the appropriate start coordinate (e.g. @istart if @width = -1) is not used for single
 * plane extraction, but the planes are accumulated in whole range (0..xres for given example).
 **/
void
gwy_brick_min_plane(GwyBrick *brick,
                    GwyField *target,
                    gint istart, gint jstart, gint kstart,
                    gint width, gint height, gint depth,
                    gboolean keep_offsets)
{
    gint col, row, lev, xres, yres, zres;
    const gdouble *bdata, *brow;
    gdouble *ddata, *drow;

    if (!gwy_brick_extract_field_common(brick, target, istart, jstart, kstart, width, height, depth, keep_offsets))
        return;

    xres = brick->xres;
    yres = brick->yres;
    zres = brick->zres;
    bdata = brick->priv->data;
    ddata = target->priv->data;

    /* See gwy_brick_sum_plane() for explanation. */
    if (width == -1 && height > 0 && depth > 0) {
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            private(brow,lev,row,col) \
            shared(bdata,ddata,xres,yres,jstart,kstart,height,depth)
#endif
        for (lev = 0; lev < depth; lev++) {
            for (row = 0; row < height; row++) {
                gdouble m = G_MAXDOUBLE;

                brow = bdata + xres*(row + jstart) + xres*yres*(lev + kstart);
                for (col = 0; col < xres; col++) {
                    if (brow[col] < m)
                        m = brow[col];
                }
                ddata[row + lev*height] = m;
            }
        }
    }
    else if (width > 0 && height == -1 && depth > 0) {
        gwy_field_fill(target, G_MAXDOUBLE);
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            private(brow,drow,lev,row,col) \
            shared(bdata,ddata,xres,yres,istart,kstart,width,depth)
#endif
        for (lev = 0; lev < depth; lev++) {
            for (row = 0; row < yres; row++) {
                brow = bdata + istart + xres*row + xres*yres*(lev + kstart);
                drow = ddata + lev*width;
                for (col = 0; col < width; col++) {
                    if (brow[col] < drow[col])
                        drow[col] = brow[col];
                }
            }
        }
    }
    else if (width > 0 && height > 0 && depth == -1) {
        gwy_field_fill(target, G_MAXDOUBLE);
        for (lev = 0; lev < zres; lev++) {
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            private(brow,drow,row,col) \
            shared(bdata,ddata,xres,yres,istart,jstart,height,width,lev)
#endif
            for (row = 0; row < height; row++) {
                brow = bdata + istart + xres*(row + jstart) + xres*yres*lev;
                drow = ddata + row*width;
                for (col = 0; col < width; col++) {
                    if (brow[col] < drow[col])
                        drow[col] = brow[col];
                }
            }
        }
    }

    _gwy_copy_unit(brick->priv->unit_w, &target->priv->unit_z);
    gwy_field_invalidate(target);
}

/**
 * gwy_brick_min_xy_plane:
 * @brick: A data brick.
 * @target: Datafield to be filled by the summary data. It will be resized if necessary.
 *
 * Computes the minima along z-axis of a data brick to a data field.
 *
 * The result is an xy plane.
 **/
void
gwy_brick_min_xy_plane(GwyBrick *brick, GwyField *target)
{
    g_return_if_fail(GWY_IS_BRICK(brick));
    gwy_brick_min_plane(brick, target,
                        0, 0, 0, brick->xres, brick->yres, -1,
                        TRUE);
}

/**
 * gwy_brick_max_plane:
 * @brick: A data brick.
 * @target: Datafield to be filled by the maxima plane. It will be resized if necessary.
 * @istart: Column where to start (pixel coordinates).
 * @jstart: Row where to start (pixel coordinates).
 * @kstart: Level where to start (pixel coordinates).
 * @width: Pixel width of summarized plane. If @width is -1, the yz planes will be summarized.
 * @height: Pixel height of summarized plane.  If @height is -1, the xz planes will be summarized.
 * @depth: Pixel depth of extracted plane. If @depth is -1, the xy planes will be summarized
 * @keep_offsets: Keep the physical offsets in extracted field.
 *
 * Finds minima of profiles in certain direction.
 *
 * One value of set (@width, @height, @depth) needs to be -1, determining the plane orientation. In contrast to
 * gwy_brick_extract_plane, the appropriate start coordinate (e.g. @istart if @width = -1) is not used for single
 * plane extraction, but the planes are accumulated in whole range (0..xres for given example).
 **/
void
gwy_brick_max_plane(GwyBrick *brick,
                    GwyField *target,
                    gint istart, gint jstart, gint kstart,
                    gint width, gint height, gint depth,
                    gboolean keep_offsets)
{
    gint col, row, lev, xres, yres, zres;
    const gdouble *bdata, *brow;
    gdouble *ddata, *drow;

    if (!gwy_brick_extract_field_common(brick, target, istart, jstart, kstart, width, height, depth, keep_offsets))
        return;

    xres = brick->xres;
    yres = brick->yres;
    zres = brick->zres;
    bdata = brick->priv->data;
    ddata = target->priv->data;

    /* See gwy_brick_sum_plane() for explanation. */
    if (width == -1 && height > 0 && depth > 0) {
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            private(brow,lev,row,col) \
            shared(bdata,ddata,xres,yres,jstart,kstart,height,depth)
#endif
        for (lev = 0; lev < depth; lev++) {
            for (row = 0; row < height; row++) {
                gdouble m = G_MINDOUBLE;

                brow = bdata + xres*(row + jstart) + xres*yres*(lev + kstart);
                for (col = 0; col < xres; col++) {
                    if (brow[col] > m)
                        m = brow[col];
                }
                ddata[row + lev*height] = m;
            }
        }
    }
    else if (width > 0 && height == -1 && depth > 0) {
        gwy_field_fill(target, G_MINDOUBLE);
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            private(brow,drow,lev,row,col) \
            shared(bdata,ddata,xres,yres,istart,kstart,width,depth)
#endif
        for (lev = 0; lev < depth; lev++) {
            for (row = 0; row < yres; row++) {
                brow = bdata + istart + xres*row + xres*yres*(lev + kstart);
                drow = ddata + lev*width;
                for (col = 0; col < width; col++) {
                    if (brow[col] > drow[col])
                        drow[col] = brow[col];
                }
            }
        }
    }
    else if (width > 0 && height > 0 && depth == -1) {
        gwy_field_fill(target, G_MINDOUBLE);
        for (lev = 0; lev < zres; lev++) {
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            private(brow,drow,row,col) \
            shared(bdata,ddata,xres,yres,istart,jstart,height,width,lev)
#endif
            for (row = 0; row < height; row++) {
                brow = bdata + istart + xres*(row + jstart) + xres*yres*lev;
                drow = ddata + row*width;
                for (col = 0; col < width; col++) {
                    if (brow[col] > drow[col])
                        drow[col] = brow[col];
                }
            }
        }
    }

    _gwy_copy_unit(brick->priv->unit_w, &target->priv->unit_z);
    gwy_field_invalidate(target);
}

/**
 * gwy_brick_max_xy_plane:
 * @brick: A data brick.
 * @target: Datafield to be filled by the summary data. It will be resized if necessary.
 *
 * Computes the maxima along z-axis of a data brick to a data field.
 *
 * The result is an xy plane.
 **/
void
gwy_brick_max_xy_plane(GwyBrick *brick, GwyField *target)
{
    g_return_if_fail(GWY_IS_BRICK(brick));
    gwy_brick_max_plane(brick, target, 0, 0, 0, brick->xres, brick->yres, -1, TRUE);
}

static void
convert_positions_to_coordinates(GwyBrick *brick,
                                 GwyField *target,
                                 const gint *pos,
                                 gint width, gint height, gint depth)
{
    GwyLine *calibration;
    const gdouble *caldata;
    gdouble *data;
    gdouble q, off;
    gint i, n;

    n = target->xres*target->yres;
    data = target->priv->data;

    if (width == -1 && height > 0 && depth > 0) {
        q = brick->xreal/brick->xres;
        off = brick->xoff + 0.5*q;
        _gwy_copy_unit(brick->priv->unit_x, &target->priv->unit_z);
    }
    else if (width > 0 && height == -1 && depth > 0) {
        q = brick->yreal/brick->yres;
        off = brick->yoff + 0.5*q;
        _gwy_copy_unit(brick->priv->unit_y, &target->priv->unit_z);
    }
    else if (width > 0 && height > 0 && depth == -1) {
        calibration = gwy_brick_get_zcalibration(brick);
        if (calibration) {
            caldata = calibration->priv->data;
            _gwy_copy_unit(calibration->priv->unit_y, &target->priv->unit_z);
            for (i = 0; i < n; i++)
                data[i] = caldata[i];
            return;
        }
        q = brick->zreal/brick->zres;
        off = brick->zoff + 0.5*q;
        _gwy_copy_unit(brick->priv->unit_z, &target->priv->unit_z);
    }
    else {
        g_return_if_reached();
    }

#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            private(i) \
            shared(data,pos,n,q,off)
#endif
    for (i = 0; i < n; i++)
        data[i] = pos[i]*q + off;
}

/**
 * gwy_brick_minpos_plane:
 * @brick: A data brick.
 * @target: Datafield to be filled by the minima positions plane. It will be resized if necessary.
 * @istart: Column where to start (pixel coordinates).
 * @jstart: Row where to start (pixel coordinates).
 * @kstart: Level where to start (pixel coordinates).
 * @width: Pixel width of summarized plane. If @width is -1, the yz planes will be summarized.
 * @height: Pixel height of summarized plane.  If @height is -1, the xz planes will be summarized.
 * @depth: Pixel depth of summarized plane. If @depth is -1, the xy planes will be summarized
 * @keep_offsets: Keep the physical offsets in summarized field.
 *
 * Finds minima coordinates in profiles in certain direction.
 *
 * One value of set (@width, @height, @depth) needs to be -1, determining the plane orientation. In contrast to
 * gwy_brick_extract_plane, the appropriate start coordinate (e.g. @istart if @width = -1) is not used for single
 * plane extraction, but the planes are accumulated in whole range (0..xres for given example)
 **/
void
gwy_brick_minpos_plane(GwyBrick *brick,
                       GwyField *target,
                       gint istart, gint jstart, gint kstart,
                       gint width, gint height, gint depth,
                       gboolean keep_offsets)
{
    gint col, row, lev, xres, yres, zres;
    const gdouble *bdata, *brow;
    gdouble *ddata, *drow;
    gint *pos;

    if (!gwy_brick_extract_field_common(brick, target, istart, jstart, kstart, width, height, depth, keep_offsets))
        return;

    xres = brick->xres;
    yres = brick->yres;
    zres = brick->zres;
    bdata = brick->priv->data;
    ddata = target->priv->data;
    pos = g_new0(gint, target->xres*target->yres);

    /* See gwy_brick_sum_plane() for explanation. */
    if (width == -1 && height > 0 && depth > 0) {
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            private(brow,lev,row,col) \
            shared(bdata,ddata,pos,xres,yres,jstart,kstart,height,depth)
#endif
        for (lev = 0; lev < depth; lev++) {
            for (row = 0; row < height; row++) {
                gdouble m = G_MAXDOUBLE;

                brow = bdata + xres*(row + jstart) + xres*yres*(lev + kstart);
                for (col = 0; col < xres; col++) {
                    if (brow[col] < m) {
                        m = brow[col];
                        pos[lev*height + row] = col;
                    }
                }
            }
        }
    }
    else if (width > 0 && height == -1 && depth > 0) {
        gwy_field_fill(target, G_MAXDOUBLE);
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            private(brow,drow,lev,row,col) \
            shared(bdata,ddata,pos,xres,yres,istart,kstart,width,depth)
#endif
        for (lev = 0; lev < depth; lev++) {
            for (row = 0; row < yres; row++) {
                brow = bdata + istart + xres*row + xres*yres*(lev + kstart);
                drow = ddata + lev*width;
                for (col = 0; col < width; col++) {
                    if (brow[col] < drow[col]) {
                        drow[col] = brow[col];
                        pos[lev*width + col] = row;
                    }
                }
            }
        }
    }
    else if (width > 0 && height > 0 && depth == -1) {
        gwy_field_fill(target, G_MAXDOUBLE);
        for (lev = 0; lev < zres; lev++) {
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            private(brow,drow,row,col) \
            shared(bdata,ddata,pos,xres,yres,istart,jstart,height,width,lev)
#endif
            for (row = 0; row < height; row++) {
                brow = bdata + istart + xres*(row + jstart) + xres*yres*lev;
                drow = ddata + row*width;
                for (col = 0; col < width; col++) {
                    if (brow[col] < drow[col]) {
                        drow[col] = brow[col];
                        pos[row*width + col] = lev;
                    }
                }
            }
        }
    }

    convert_positions_to_coordinates(brick, target, pos, width, height, depth);
    g_free(pos);
    gwy_field_invalidate(target);
}

/**
 * gwy_brick_minpos_xy_plane:
 * @brick: A data brick.
 * @target: Datafield to be filled by the summary data. It will be resized if necessary.
 *
 * Computes the location of minima along z-axis of a data brick to a data field.
 *
 * The result is an xy plane.
 **/
void
gwy_brick_minpos_xy_plane(GwyBrick *brick,
                          GwyField *target)
{
    g_return_if_fail(GWY_IS_BRICK(brick));
    gwy_brick_minpos_plane(brick, target, 0, 0, 0, brick->xres, brick->yres, -1, TRUE);
}

/**
 * gwy_brick_maxpos_plane:
 * @brick: A data brick.
 * @target: Datafield to be filled by the maxima positions plane. It will be resized if necessary.
 * @istart: Column where to start (pixel coordinates).
 * @jstart: Row where to start (pixel coordinates).
 * @kstart: Level where to start (pixel coordinates).
 * @width: Pixel width of summarized plane. If @width is -1, the yz planes will be summarized.
 * @height: Pixel height of summarized plane.  If @height is -1, the xz planes will be summarized
 * @depth: Pixel depth of summarized plane. If @depth is -1, the xy planes will be summarized
 * @keep_offsets: Keep the physical offsets in summarized field.  Not implemented.
 *
 * Finds maxima coordinates of profiles in certain direction.
 *
 * One value of set (@width, @height, @depth) needs to be -1, determining the plane orientation. In contrast to
 * gwy_brick_extract_plane, the appropriate start coordinate (e.g. @istart if @width = -1) is not used for single
 * plane extraction, but the planes are accumulated in whole range (0..xres for given example).
 **/
void
gwy_brick_maxpos_plane(GwyBrick *brick,
                       GwyField *target,
                       gint istart,
                       gint jstart,
                       gint kstart,
                       gint width,
                       gint height,
                       gint depth,
                       gboolean keep_offsets)
{
    gint col, row, lev, xres, yres, zres;
    const gdouble *bdata, *brow;
    gdouble *ddata, *drow;
    gint *pos;

    if (!gwy_brick_extract_field_common(brick, target, istart, jstart, kstart, width, height, depth, keep_offsets))
        return;

    xres = brick->xres;
    yres = brick->yres;
    zres = brick->zres;
    bdata = brick->priv->data;
    ddata = target->priv->data;
    pos = g_new0(gint, target->xres*target->yres);

    /* See gwy_brick_sum_plane() for explanation. */
    if (width == -1 && height > 0 && depth > 0) {
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            private(brow,lev,row,col) \
            shared(bdata,ddata,pos,xres,yres,jstart,kstart,height,depth)
#endif
        for (lev = 0; lev < depth; lev++) {
            for (row = 0; row < height; row++) {
                gdouble m = G_MINDOUBLE;

                brow = bdata + xres*(row + jstart) + xres*yres*(lev + kstart);
                for (col = 0; col < xres; col++) {
                    if (brow[col] > m) {
                        m = brow[col];
                        pos[lev*height + row] = col;
                    }
                }
            }
        }
    }
    else if (width > 0 && height == -1 && depth > 0) {
        gwy_field_fill(target, G_MINDOUBLE);
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            private(brow,drow,lev,row,col) \
            shared(bdata,ddata,pos,xres,yres,istart,kstart,width,depth)
#endif
        for (lev = 0; lev < depth; lev++) {
            for (row = 0; row < yres; row++) {
                brow = bdata + istart + xres*row + xres*yres*(lev + kstart);
                drow = ddata + lev*width;
                for (col = 0; col < width; col++) {
                    if (brow[col] > drow[col]) {
                        drow[col] = brow[col];
                        pos[lev*width + col] = row;
                    }
                }
            }
        }
    }
    else if (width > 0 && height > 0 && depth == -1) {
        gwy_field_fill(target, G_MINDOUBLE);
        for (lev = 0; lev < zres; lev++) {
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            private(brow,drow,row,col) \
            shared(bdata,ddata,pos,xres,yres,istart,jstart,height,width,lev)
#endif
            for (row = 0; row < height; row++) {
                brow = bdata + istart + xres*(row + jstart) + xres*yres*lev;
                drow = ddata + row*width;
                for (col = 0; col < width; col++) {
                    if (brow[col] > drow[col]) {
                        drow[col] = brow[col];
                        pos[row*width + col] = lev;
                    }
                }
            }
        }
    }

    convert_positions_to_coordinates(brick, target, pos, width, height, depth);
    g_free(pos);
    gwy_field_invalidate(target);
}

/**
 * gwy_brick_maxpos_xy_plane:
 * @brick: A data brick.
 * @target: Datafield to be filled by the summary data. It will be resized if necessary.
 *
 * Computes the location of maxima along z-axis of a data brick to a data field.
 *
 * The result is an xy plane.
 **/
void
gwy_brick_maxpos_xy_plane(GwyBrick *brick,
                          GwyField *target)
{
    g_return_if_fail(GWY_IS_BRICK(brick));
    gwy_brick_maxpos_plane(brick, target, 0, 0, 0, brick->xres, brick->yres, -1, TRUE);
}

/**
 * gwy_brick_mean_plane:
 * @brick: A data brick.
 * @target: Datafield to be filled by the mean plane. It will be resized if necessary.
 * @istart: Column where to start (pixel coordinates).
 * @jstart: Row where to start (pixel coordinates).
 * @kstart: Level where to start (pixel coordinates).
 * @width: Pixel width of summarized plane. If @width is -1, the yz planes will be summarized.
 * @height: Pixel height of summarized plane.  If @height is -1, the xz planes will be summarized.
 * @depth: Pixel depth of summarized plane. If @depth is -1, the xy planes will be summarized.
 * @keep_offsets: Keep the physical offsets in summarized field.
 *
 * Finds mean of planes in certain direction.
 *
 * One value of set (@width, @height, @depth) needs to be -1, determining the plane orientation. In contrast to
 * gwy_brick_extract_plane, the appropriate start coordinate (e.g. @istart if @width = -1) is not used for single
 * plane extraction, but the planes are accumulated in whole range (0..xres for given example).
 **/
void
gwy_brick_mean_plane(GwyBrick *brick,
                     GwyField *target,
                     gint istart, gint jstart, gint kstart,
                     gint width, gint height, gint depth,
                     gboolean keep_offsets)
{
    g_return_if_fail(GWY_IS_BRICK(brick));
    g_return_if_fail(GWY_IS_FIELD(target));
    gwy_brick_sum_plane(brick, target, istart, jstart, kstart, width, height, depth, keep_offsets);

    if (width == -1 && height > 0 && depth > 0)
        gwy_field_multiply(target, 1.0/brick->xres);
    else if (width > 0 && height == -1 && depth > 0)
        gwy_field_multiply(target, 1.0/brick->yres);
    else if (width > 0 && height > 0 && depth == -1)
        gwy_field_multiply(target, 1.0/brick->zres);
}

/**
 * gwy_brick_mean_xy_plane:
 * @brick: A data brick.
 * @target: Datafield to be filled by the summary data. It will be resized if necessary.
 *
 * Calculates mean values of all z-profiles of a data brick to a data field.
 *
 * The result is an xy plane and can be alternatively imagined as the average
 * of all xy planes.
 **/
void
gwy_brick_mean_xy_plane(GwyBrick *brick,
                        GwyField *target)
{
    g_return_if_fail(GWY_IS_BRICK(brick));
    gwy_brick_mean_plane(brick, target, 0, 0, 0, brick->xres, brick->yres, -1, TRUE);
}

/**
 * gwy_brick_rms_plane:
 * @brick: A data brick.
 * @target: Datafield to be filled by the rms plane. It will be resized if necessary.
 * @istart: Column where to start (pixel coordinates).
 * @jstart: Row where to start (pixel coordinates).
 * @kstart: Level where to start (pixel coordinates).
 * @width: Pixel width of summarized plane. If @width is -1, the yz planes will be summarized.
 * @height: Pixel height of summarized plane.  If @height is -1, the xz planes will be summarized.
 * @depth: Pixel depth of summarized plane. If @depth is -1, the xy planes will be summarized.
 * @keep_offsets: Keep the physical offsets in extracted field.
 *
 * Finds rms of planes in certain direction and extract the result (GwyField). One value of set (@width, @height,
 * @depth) needs to be -1, determining the plane orientation. In contrast to gwy_brick_extract_plane, the appropriate
 * start coordinate (e.g. @istart if @width = -1) is not used for single plane extraction, but the planes are
 * accumulated in whole range (0..xres for given example)
 **/
void
gwy_brick_rms_plane(GwyBrick *brick,
                    GwyField *target,
                    gint istart,
                    gint jstart,
                    gint kstart,
                    gint width,
                    gint height,
                    gint depth,
                    gboolean keep_offsets)
{
    gint col, row, lev, xres, yres, zres, i, n;
    const gdouble *bdata, *brow, *mdata, *mrow;
    gdouble *ddata, *drow;
    GwyField *meanfield;
    gdouble q = 1.0;

    if (!gwy_brick_extract_field_common(brick, target, istart, jstart, kstart, width, height, depth, keep_offsets))
        return;

    meanfield = gwy_field_new(1, 1, 1.0, 1.0, FALSE);
    gwy_brick_mean_plane(brick, meanfield, istart, jstart, kstart, width, height, depth, keep_offsets);
    gwy_field_clear(target);
    xres = brick->xres;
    yres = brick->yres;
    zres = brick->zres;
    bdata = brick->priv->data;
    ddata = target->priv->data;
    mdata = meanfield->priv->data;
    n = target->xres*target->yres;

    /* See gwy_brick_sum_plane() for explanation. */
    if (width == -1 && height > 0 && depth > 0) {
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
        private(brow,mrow,lev,row,col) \
        shared(bdata,ddata,mdata,xres,yres,jstart,kstart,height,depth)
#endif
        for (lev = 0; lev < depth; lev++) {
            for (row = 0; row < height; row++) {
                gdouble s = 0.0;

                brow = bdata + xres*(row + jstart) + xres*yres*(lev + kstart);
                mrow = mdata + row + lev*height;
                for (col = 0; col < xres; col++) {
                    gdouble v = brow[col] - mrow[col];
                    s += v*v;
                }
                ddata[row + lev*height] = s;
            }
        }
        q = 1.0/xres;
    }
    else if (width > 0 && height == -1 && depth > 0) {
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
        private(brow,drow,mrow,lev,row,col) \
        shared(bdata,ddata,mdata,xres,yres,istart,kstart,width,depth)
#endif
        for (lev = 0; lev < depth; lev++) {
            for (row = 0; row < yres; row++) {
                brow = bdata + istart + xres*row + xres*yres*(lev + kstart);
                drow = ddata + lev*width;
                mrow = mdata + lev*width;
                for (col = 0; col < width; col++) {
                    gdouble v = brow[col] - mrow[col];
                    drow[col] += v*v;
                }
            }
        }
        q = 1.0/yres;
    }
    else if (width > 0 && height > 0 && depth == -1) {
        for (lev = 0; lev < zres; lev++) {
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
        private(brow,drow,mrow,row,col) \
        shared(bdata,ddata,mdata,xres,yres,istart,jstart,height,width,lev)
#endif
            for (row = 0; row < height; row++) {
                brow = bdata + istart + xres*(row + jstart) + xres*yres*lev;
                drow = ddata + row*width;
                mrow = mdata + row*width;
                for (col = 0; col < width; col++) {
                    gdouble v = brow[col] - mrow[col];
                    drow[col] += v*v;
                }
            }
        }
        q = 1.0/zres;
    }
    g_object_unref(meanfield);

#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            private(i) \
            shared(ddata,n,q)
#endif
    for (i = 0; i < n; i++)
        ddata[i] = sqrt(q*ddata[i]);

    _gwy_copy_unit(brick->priv->unit_w, &target->priv->unit_z);
    gwy_field_invalidate(target);
}

/**
 * gwy_brick_rms_xy_plane:
 * @brick: A data brick.
 * @target: Datafield to be filled by the summary data. It will be resized if necessary.
 *
 * Calculates rms values of all z-profiles of a data brick to a data field.
 *
 * The result is an xy plane and can be alternatively imagined as the variation
 * among xy planes.
 **/
void
gwy_brick_rms_xy_plane(GwyBrick *brick,
                       GwyField *target)
{
    g_return_if_fail(GWY_IS_BRICK(brick));
    gwy_brick_rms_plane(brick, target, 0, 0, 0, brick->xres, brick->yres, -1, TRUE);
}

/**
 * gwy_brick_extract_line:
 * @brick: A data brick.
 * @target: Dataline to be filled by extracted line. It will be resized if necessary.
 * @istart: Column where to start (pixel coordinates).
 * @jstart: Row where to start (pixel coordinates).
 * @kstart: Level where to start (pixel coordinates).
 * @iend: Column where to end, exclusive (pixel coordinates).
 * @jend: Row where to end, exclusive (pixel coordinates).
 * @kend: Level where to end, exclusive (pixel coordinates).
 * @keep_offsets: Keep physical offsets in extracted line.
 *
 * Extracts a line (GwyLine) from the brick.
 *
 * Only line orientations parallel to coordinate axes are supported now, i.e. two of the start coordinates need to be
 * same as end ones.
 **/
void
gwy_brick_extract_line(GwyBrick *brick, GwyLine *target,
                       gint istart, gint jstart, gint kstart,
                       gint iend, gint jend, gint kend,
                       gboolean keep_offsets)
{
    gint col, row, lev, xres, yres, zres;
    gdouble *bdata, *ddata;
    GwyUnit *unit = NULL;

    g_return_if_fail(GWY_IS_BRICK(brick));
    g_return_if_fail(GWY_IS_LINE(target));
    g_return_if_fail(_gwy_brick_check_inside(brick, istart, jstart, kstart));

    xres = brick->xres;
    yres = brick->yres;
    zres = brick->zres;
    g_return_if_fail(iend >= 0 && iend <= xres
                     && jend >= 0 && jend <= yres
                     && kend >= 0 && kend <= zres);

    bdata = brick->priv->data;

    if ((jstart == jend) && (kstart == kend)) {
        gwy_line_resize(target, ABS(iend - istart));
        ddata = gwy_line_get_data(target);
        unit = brick->priv->unit_x;

        row = jstart;
        lev = kstart;
        if (iend >= istart) {
            for (col = 0; col < (iend - istart); col++)
                ddata[col] = bdata[col + istart + xres*row + xres*yres*lev];
        }
        else {
            for (col = 0; col < (istart - iend); col++)
                ddata[col] = bdata[iend - col - 1 + xres*row + xres*yres*lev];
            GWY_SWAP(gint, istart, iend);
        }
        target->off = keep_offsets ? istart*brick->xreal/xres : 0.0;
        target->real = (iend - istart)*brick->xreal/xres;
    }
    else if ((istart == iend) && (kstart == kend)) {
        gwy_line_resize(target, ABS(jend - jstart));
        ddata = gwy_line_get_data(target);
        unit = brick->priv->unit_y;

        col = istart;
        lev = kstart;
        if (jend >= jstart) {
            for (row = 0; row < (jend - jstart); row++)
                ddata[row] = bdata[col + xres*(row + jstart) + xres*yres*lev];
        }
        else {
            for (row = 0; row < (jstart - jend); row++)
                ddata[row] = bdata[col + xres*(jstart - row - 1) + xres*yres*lev];
            GWY_SWAP(gint, jstart, jend);
        }
        target->off = keep_offsets ? jstart*brick->yreal/yres : 0.0;
        target->real = (jend - jstart)*brick->yreal/yres;
    }
    else if ((istart == iend) && (jstart == jend)) {
        gwy_line_resize(target, ABS(kend - kstart));
        ddata = gwy_line_get_data(target);
        unit = brick->priv->unit_x;

        col = istart;
        row = jstart;
        if (kend >= kstart) {
            for (lev = 0; lev < (kend - kstart); lev++)
                ddata[lev] = bdata[col + xres*row + xres*yres*(lev + kstart)];
        }
        else {
            for (lev = 0; lev < (kstart - kend); lev++)
                ddata[lev] = bdata[col + xres*row + xres*yres*(kend - lev - 1)];
            GWY_SWAP(gint, kstart, kend);
        }
        target->off = keep_offsets ? kstart*brick->zreal/zres : 0.0;
        target->real = (kend - kstart)*brick->zreal/zres;
    }
    else {
        g_return_if_reached();
    }

    _gwy_copy_unit(unit, &target->priv->unit_x);
    _gwy_copy_unit(brick->priv->unit_w, &target->priv->unit_y);
}

/**
 * gwy_brick_extract_z_line:
 * @brick: A data brick.
 * @target: Dataline to be filled by extracted line. It will be resized if necessary.
 * @i: Column index.
 * @j: Row index.
 *
 * Extracts one full Z profile of a data brick to a data line.
 **/
void
gwy_brick_extract_z_line(GwyBrick *brick,
                         GwyLine *target,
                         gint i, gint j)
{
    g_return_if_fail(GWY_IS_BRICK(brick));
    gwy_brick_extract_line(brick, target, i, j, 0, i, j, brick->zres, TRUE);
}

/**
 * gwy_brick_get_zcalibration:
 * @brick: A data brick.
 *
 * Gets the z-axis non-linear calibration of a data brick.
 *
 * Returns: (transfer none): Z Calibration (non-linear Z-axis values as ordinates).
 **/
GwyLine*
gwy_brick_get_zcalibration(GwyBrick *brick)
{
    GwyBrickPrivate *priv;

    g_return_val_if_fail(GWY_IS_BRICK(brick), NULL);

    priv = brick->priv;

    return priv->zcalibration;
}

/**
 * gwy_brick_set_zcalibration:
 * @brick: A data brick.
 * @calibration: (nullable) (transfer none):
 *               GwyLine pointer with z-axis non-linear calibration of a data brick (values are stored as
 *               ordinates).  It can also be %NULL to unset the calibration.
 *
 * Sets the z-axis non-linear calibration of a data brick.
 **/
void
gwy_brick_set_zcalibration(GwyBrick *brick, GwyLine *calibration)
{
    GwyLine *oldcal;
    GwyBrickPrivate *priv;

    g_return_if_fail(GWY_IS_BRICK(brick));
    g_return_if_fail(!calibration || GWY_IS_LINE(calibration));

    priv = brick->priv;
    oldcal = priv->zcalibration;
    if (oldcal == calibration)
        return;

    if (calibration)
        g_object_ref(calibration);

    priv->zcalibration = calibration;
    g_clear_object(&oldcal);
}

/**
 * gwy_brick_copy_zcalibration:
 * @brick: A data brick.
 * @target: Target data brick.
 *
 * Copies non-linear z-axis calibration between two data bricks.
 *
 * Both bricks must have the same Z resolution.  For a meaningful usage, they should also have the same Z real sizes
 * and units (uncalibrated).
 *
 * If @brick has no z-axis calibration, existing @targets' calibration is
 * deleted.
 **/
void
gwy_brick_copy_zcalibration(GwyBrick *brick, GwyBrick *target)
{
    GwyBrickPrivate *priv, *tpriv;

    g_return_if_fail(GWY_IS_BRICK(brick));
    g_return_if_fail(GWY_IS_BRICK(target));
    if (target == brick)
        return;

    g_return_if_fail(target->zres == brick->zres);
    priv = brick->priv;
    tpriv = (GwyBrickPrivate*)target->priv;
    if (priv->zcalibration && tpriv->zcalibration)
        gwy_line_assign(tpriv->zcalibration, priv->zcalibration);
    else if (priv->zcalibration && !tpriv->zcalibration)
        tpriv->zcalibration = gwy_line_copy(priv->zcalibration);
    else if (!priv->zcalibration && tpriv->zcalibration)
        g_clear_object(&tpriv->zcalibration);
}

/* All dimensions are in the destination; source sizes are always with x and
 * y swapped. */
static inline void
copy_2d_block_swap_x_y(const gdouble *src, gdouble *dest,
                       guint xres, G_GNUC_UNUSED guint yres,
                       guint width, guint height)
{
    const gdouble *s;
    gdouble *d;
    guint i, j;

    for (i = 0; i < width; i++) { /* src y */
        s = src + i*yres;
        d = dest + i;
        for (j = height; j; j--, s++, d += xres) /* src x */
            *d = *s;
    }
}

/* All dimensions are in the destination; source sizes are always with x and z swapped. */
static inline void
copy_3d_block_swap_x_z(const gdouble *src, gdouble *dest,
                       guint xres, guint yres, guint zres,
                       guint width, guint height, guint depth)
{
    const gdouble *s;
    gdouble *d;
    guint i, j, k, n;

    n = xres*yres;
    for (i = 0; i < width; i++) { /* src z */
        for (j = 0; j < height; j++) { /* src y */
            s = src + (i*yres + j)*zres;
            d = dest + j*xres + i;
            for (k = depth; k; k--, s++, d += n) /* src x */
                *d = *s;
        }
    }
}

/* Rotates x → y → z → x.  All dimensions are in the destination; source sizes are always with z → y → x → z rotated. */
static inline void
copy_3d_block_swap_yzx(const gdouble *src, gdouble *dest,
                       guint xres, guint yres, guint zres,
                       guint width, guint height, guint depth)
{
    const gdouble *s;
    gdouble *d;
    guint i, j, k;

    for (i = 0; i < width; i++) { /* src z */
        for (j = 0; j < depth; j++) { /* src y */
            s = src + (i*zres + j)*yres;
            d = dest + j*xres*yres + i;
            for (k = height; k; k--, s++, d += xres) /* src x */
                *d = *s;
        }
    }
}

/* Rotates x → z → y → x.  All dimensions are in the destination; source sizes are always with z → x → y → z rotated. */
static inline void
copy_3d_block_swap_zxy(const gdouble *src, gdouble *dest,
                       guint xres, guint yres, guint zres,
                       guint width, guint height, guint depth)
{
    const gdouble *s;
    gdouble *d;
    guint i, j, k, n;

    n = xres*yres;
    for (i = 0; i < height; i++) { /* src z */
        for (j = 0; j < width; j++) { /* src y */
            s = src + (i*xres + j)*zres;
            d = dest + i*xres + j;
            for (k = depth; k; k--, s++, d += n) /* src x */
                *d = *s;
        }
    }
}

/**
 * gwy_brick_transpose:
 * @brick: A data brick.
 * @target: Destination data brick.  It cannot be @brick itself and will be resized as needed.
 * @type: Basic transposition type (which axes are swapped).
 * @xflipped: %TRUE to reflect X, i.e. rows within XY planes.
 * @yflipped: %TRUE to reflect Y, i.e. columns within XY planes.
 * @zflipped: %TRUE to reflect Z, i.e. the XY plane order.
 *
 * Transposes a data brick, exchanging and/or flipping axes.
 *
 * Real dimensions and units are updated.  Since real sizes cannot go backward, flipping an axis results in the
 * corresponding offset being reset (the real dimension stays positive).  If the Z axis is preserved its calibration
 * is copied to the target; otherwise the target will have no Z axis calibration.
 **/
void
gwy_brick_transpose(GwyBrick *brick,
                    GwyBrick *target,
                    GwyBrickTransposeType type,
                    gboolean xflipped,
                    gboolean yflipped,
                    gboolean zflipped)
{
    enum {
        BLOCK_SWAP_N2D = 64,
        BLOCK_SWAP_N3D = 12,
    };

    guint oldxres, oldyres, oldzres, xres, yres, zres;
    gdouble xreal, yreal, zreal, newxreal, newyreal, newzreal;
    gdouble xoff, yoff, zoff, newxoff, newyoff, newzoff;
    GwyLine *zcal;
    guint i, j, k, sizex, sizey, sizez;
    guint tids[3];
    const gdouble *bdata;
    GwyUnit *oldunit[4], *unit[4];
    gdouble *rdata;

    g_return_if_fail(GWY_IS_BRICK(brick));
    g_return_if_fail(GWY_IS_BRICK(target));
    g_return_if_fail(target != brick);

    xres = oldxres = brick->xres;
    yres = oldyres = brick->yres;
    zres = oldzres = brick->zres;

    newxreal = xreal = brick->xreal;
    newyreal = yreal = brick->yreal;
    newzreal = zreal = brick->zreal;

    newxoff = xoff = brick->xoff;
    newyoff = yoff = brick->yoff;
    newzoff = zoff = brick->zoff;

    tids[0] = 0;
    tids[1] = 1;
    tids[2] = 2;

    /* First fix the dimensions. */
    if (type == GWY_BRICK_TRANSPOSE_XYZ) {
        /* Identity. */
    }
    else if (type == GWY_BRICK_TRANSPOSE_XZY) {
        /* Y <-> Z */
        GWY_SWAP(guint, yres, zres);
        GWY_SWAP(gdouble, newyreal, newzreal);
        GWY_SWAP(gdouble, newyoff, newzoff);
        tids[1] = 2;
        tids[2] = 1;
    }
    else if (type == GWY_BRICK_TRANSPOSE_YXZ) {
        /* X <-> Y */
        GWY_SWAP(guint, xres, yres);
        GWY_SWAP(gdouble, newxreal, newyreal);
        GWY_SWAP(gdouble, newxoff, newyoff);
        tids[0] = 1;
        tids[1] = 0;
    }
    else if (type == GWY_BRICK_TRANSPOSE_ZYX) {
        /* Z <-> X */
        GWY_SWAP(guint, zres, xres);
        GWY_SWAP(gdouble, newzreal, newxreal);
        GWY_SWAP(gdouble, newzoff, newxoff);
        tids[0] = 2;
        tids[2] = 0;
    }
    else if (type == GWY_BRICK_TRANSPOSE_YZX) {
        /* X -> Y -> Z -> X */
        xres = oldzres;
        yres = oldxres;
        zres = oldyres;
        newxreal = zreal;
        newyreal = xreal;
        newzreal = yreal;
        newxoff = zoff;
        newyoff = xoff;
        newzoff = yoff;
        tids[0] = 1;
        tids[1] = 2;
        tids[2] = 0;
    }
    else if (type == GWY_BRICK_TRANSPOSE_ZXY) {
        /* X -> Z -> Y -> X */
        xres = oldyres;
        yres = oldzres;
        zres = oldxres;
        newxreal = yreal;
        newyreal = zreal;
        newzreal = xreal;
        newxoff = yoff;
        newyoff = zoff;
        newzoff = xoff;
        tids[0] = 2;
        tids[1] = 0;
        tids[2] = 1;
    }
    else {
        g_assert_not_reached();
    }

    bdata = brick->priv->data;
    gwy_brick_resize(target, xres, yres, zres);
    target->xreal = newxreal;
    target->yreal = newyreal;
    target->zreal = newzreal;
    rdata = target->priv->data;

    /* There are 48 different combinations, which is way too much.  Implement the transformation in two steps:
     * 1. Reshaping, without regard to axis inversion (6 types, one trivial). */
    if (type == GWY_BRICK_TRANSPOSE_XYZ) {
        /* Identity. */
        gwy_assign(rdata, bdata, xres*yres*zres);
    }
    else if (type == GWY_BRICK_TRANSPOSE_XZY) {
        /* Y <-> Z */
        /* Copy entire rows. */
        for (i = 0; i < zres; i++) {
            for (j = 0; j < yres; j++)
                gwy_assign(rdata, bdata + (j*zres + i)*xres, xres);
        }
    }
    else if (type == GWY_BRICK_TRANSPOSE_YXZ) {
        /* X <-> Y */
        /* Transpose entire XY planes. */
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
        private(i,j,k,sizex,sizey) \
        shared(bdata,rdata,xres,yres,zres)
#endif
        for (i = 0; i < zres; i++) {
            for (j = 0; j < yres; j += BLOCK_SWAP_N2D) {
                sizey = MIN(BLOCK_SWAP_N2D, yres - j);
                for (k = 0; k < xres; k += BLOCK_SWAP_N2D) {
                    sizex = MIN(BLOCK_SWAP_N2D, xres - k);
                    copy_2d_block_swap_x_y(bdata + (i*xres + k)*yres + j,
                                           rdata + (i*yres + j)*xres + k,
                                           xres, yres, sizex, sizey);
                }
            }
        }
    }
    else if (type == GWY_BRICK_TRANSPOSE_ZYX) {
        /* Z <-> X */
        /* Work by small blocks. This seems to be a good cycle nesting. */
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
        private(i,j,k,sizex,sizey,sizez) \
        shared(bdata,rdata,xres,yres,zres)
#endif
        for (i = 0; i < zres; i += BLOCK_SWAP_N3D) {
            sizez = MIN(BLOCK_SWAP_N3D, zres - i);
            for (j = 0; j < yres; j += BLOCK_SWAP_N3D) {
                sizey = MIN(BLOCK_SWAP_N3D, yres - j);
                for (k = 0; k < xres; k += BLOCK_SWAP_N3D) {
                    sizex = MIN(BLOCK_SWAP_N3D, xres - k);
                    copy_3d_block_swap_x_z(bdata + (k*yres + j)*zres + i,
                                           rdata + (i*yres + j)*xres + k,
                                           xres, yres, zres,
                                           sizex, sizey, sizez);
                }
            }
        }
    }
    else if (type == GWY_BRICK_TRANSPOSE_YZX) {
        /* X -> Y -> Z -> X */
        /* Work by small blocks. */
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
        private(i,j,k,sizex,sizey,sizez) \
        shared(bdata,rdata,xres,yres,zres)
#endif
        for (k = 0; k < xres; k += BLOCK_SWAP_N3D) {
            sizex = MIN(BLOCK_SWAP_N3D, xres - k);
            for (i = 0; i < zres; i += BLOCK_SWAP_N3D) {
                sizez = MIN(BLOCK_SWAP_N3D, zres - i);
                for (j = 0; j < yres; j += BLOCK_SWAP_N3D) {
                    sizey = MIN(BLOCK_SWAP_N3D, yres - j);
                    copy_3d_block_swap_yzx(bdata + (k*zres + i)*yres + j,
                                           rdata + (i*yres + j)*xres + k,
                                           xres, yres, zres,
                                           sizex, sizey, sizez);
                }
            }
        }
    }
    else if (type == GWY_BRICK_TRANSPOSE_ZXY) {
        /* X -> Z -> Y -> X */
        /* Work by small blocks. */
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
        private(i,j,k,sizex,sizey,sizez) \
        shared(bdata,rdata,xres,yres,zres)
#endif
        for (j = 0; j < yres; j += BLOCK_SWAP_N3D) {
            sizey = MIN(BLOCK_SWAP_N3D, yres - j);
            for (k = 0; k < xres; k += BLOCK_SWAP_N3D) {
                sizex = MIN(BLOCK_SWAP_N3D, xres - k);
                for (i = 0; i < zres; i += BLOCK_SWAP_N3D) {
                    sizez = MIN(BLOCK_SWAP_N3D, zres - i);
                    copy_3d_block_swap_zxy(bdata + (j*xres + k)*zres + i,
                                           rdata + (i*yres + j)*xres + k,
                                           xres, yres, zres,
                                           sizex, sizey, sizez);
                }
            }
        }
    }
    else {
        g_assert_not_reached();
    }

    /* 2. Reverse along axes (8 types, one trivial). */
    target->xoff = newxoff;
    target->yoff = newyoff;
    target->zoff = newzoff;
    /* The target can only have a calibration if Z axis was preserved. */
    if (tids[2] == 2 && (zcal = gwy_brick_get_zcalibration(brick))) {
        zcal = gwy_line_copy(zcal);
        gwy_brick_set_zcalibration(target, zcal);
        g_object_unref(zcal);
    }
    /* This resets offsets and flips the calibration as needed. */
    gwy_brick_flip(target, xflipped, yflipped, zflipped);

    /* Set units. */
    oldunit[0] = gwy_brick_get_unit_x(brick);
    oldunit[1] = gwy_brick_get_unit_y(brick);
    oldunit[2] = gwy_brick_get_unit_z(brick);
    oldunit[3] = gwy_brick_get_unit_w(brick);
    unit[0] = gwy_brick_get_unit_x(target);
    unit[1] = gwy_brick_get_unit_y(target);
    unit[2] = gwy_brick_get_unit_z(target);
    unit[3] = gwy_brick_get_unit_w(target);
    gwy_unit_assign(unit[tids[0]], oldunit[0]);
    gwy_unit_assign(unit[tids[1]], oldunit[1]);
    gwy_unit_assign(unit[tids[2]], oldunit[2]);
    gwy_unit_assign(unit[3], oldunit[3]);
}

/**
 * gwy_brick_flip:
 * @brick: A data brick.
 * @xflipped: %TRUE to reflect X, i.e. rows within XY planes. The image in each Z plane will be left–right mirrored.
 * @yflipped: %TRUE to reflect Y, i.e. columns within XY planes. The image in each Z plane will be flipped upside
 *            down.
 * @zflipped: %TRUE to reflect Z, i.e. the XY plane order. The order in the image Z-stack will be reversed.
 *
 * Reflects a data brick in place.
 *
 * Since real sizes cannot go backward, flipping an axis results in the corresponding offset being reset (the real
 * dimension stays positive).
 **/
void
gwy_brick_flip(GwyBrick *brick,
               gboolean xflipped, gboolean yflipped, gboolean zflipped)
{
    g_return_if_fail(GWY_IS_BRICK(brick));

    GwyBrickPrivate *priv = brick->priv;
    gint xres = brick->xres;
    gint yres = brick->yres;
    gint zres = brick->zres;
    gdouble *data = brick->priv->data;

    if (!xflipped && !yflipped && !zflipped) {
        /* Do nothing. */
    }
    else if (xflipped && yflipped && zflipped)
        invert_array_in_place(data, xres*yres*zres);
    else if (!xflipped && !yflipped && zflipped) {
        gint n = xres*yres;
        for (gint i = 0; i < zres/2; i++) {
            gdouble *r1 = data + i*n;
            gdouble *r2 = data + (zres-1 - i)*n;
            for (gint j = n; j; j--, r1++, r2++)
                GWY_SWAP(gdouble, *r1, *r2);
        }
    }
    else if (xflipped && !yflipped && !zflipped) {
        gint n = yres*zres;
        for (gint i = 0; i < n; i++)
            invert_array_in_place(data + i*xres, xres);
    }
    else if (!xflipped && yflipped && !zflipped) {
        for (gint i = 0; i < zres; i++) {
            for (gint j = 0; j < yres/2; j++) {
                gdouble *r1 = data + (i*yres + j)*xres;
                gdouble *r2 = data + (i*yres + yres-1 - j)*xres;
                for (gint k = xres; k; k--, r1++, r2++)
                    GWY_SWAP(gdouble, *r1, *r2);
            }
        }
    }
    else if (xflipped && yflipped && !zflipped) {
        gint n = xres*yres;
        for (gint i = 0; i < zres; i++)
            invert_array_in_place(data + i*n, n);
    }
    else if (xflipped && !yflipped && zflipped) {
        for (gint i = 0; i < zres/2; i++) {
            for (gint j = 0; j < yres; j++) {
                gdouble *r1 = data + (i*yres + j)*xres;
                gdouble *r2 = data + ((zres-1 - i)*yres + j)*xres + xres-1;
                for (gint k = xres; k; k--, r1++, r2--)
                    GWY_SWAP(gdouble, *r1, *r2);
            }
        }
    }
    else if (!xflipped && yflipped && zflipped) {
        for (gint i = 0; i < zres/2; i++) {
            for (gint j = 0; j < yres; j++) {
                gdouble *r1 = data + (i*yres + j)*xres;
                gdouble *r2 = data + ((zres-1 - i)*yres + (yres-1 - j))*xres;
                for (gint k = xres; k; k--, r1++, r2++)
                    GWY_SWAP(gdouble, *r1, *r2);
            }
        }
    }
    else {
        g_assert_not_reached();
    }

    brick->xoff = xflipped ? 0.0 : brick->xoff;
    brick->yoff = yflipped ? 0.0 : brick->yoff;
    brick->zoff = zflipped ? 0.0 : brick->zoff;

    if (zflipped && priv->zcalibration)
        gwy_line_flip(priv->zcalibration);
}

/**
 * gwy_brick_invert_value:
 * @brick: A data brick.
 *
 * Inverts a data brick data in place.
 *
 * The values are inverted about the mean value. Use gwy_brick_multiply() to change the sings of all values.
 **/
void
gwy_brick_invert_value(GwyBrick *brick)
{
    g_return_if_fail(GWY_IS_BRICK(brick));

    GwyBrickPrivate *priv = brick->priv;
    gint xres = brick->xres, yres = brick->yres, zres = brick->zres;
    gdouble *data = priv->data;

    gdouble avg = data[0];
    gint n = xres*yres*zres;
    for (gint i = 1; i < n; i++)
        avg += data[i];
    avg /= n;
    for (gint i = 0; i < n; i++)
        data[i] = 2.0*avg - data[i];
}

/**
 * gwy_brick_set_plane:
 * @brick: A data brick.
 * @plane: Datafield to be inserted into brick. It must have the appropriate size.
 * @istart: Column where to start (pixel coordinates).
 * @jstart: Row where to start (pixel coordinates).
 * @kstart: Level where to start (pixel coordinates).
 * @width: Pixel width of the inserted plane. If @width is -1, the yz plane will be filled.
 * @height: Pixel height of insered plane. If @height is -1, the xz plane will be filled.
 * @depth: Pixel depth of inserted plane. If @depth is -1, the xy plane will be filled.
 *
 * Fill a single plane in the brick by a two-dimensional data (GwyField).
 *
 * One value of set (@width, @height, @depth) needs to be -1, determining the plane orientation.
 **/
void
gwy_brick_set_plane(GwyBrick *brick,
                    GwyField *plane,
                    gint istart, gint jstart, gint kstart,
                    gint width, gint height, gint depth)
{
    gint col, row, lev, xres, yres, zres;
    const gdouble *ddata, *d;
    gdouble *bdata, *b;

    g_return_if_fail(GWY_IS_BRICK(brick));
    g_return_if_fail(GWY_IS_FIELD(plane));
    g_return_if_fail((width == -1 && height > 0 && depth > 0)
                     || (width > 0 && height == -1 && depth > 0)
                     || (width > 0 && height > 0 && depth == -1));
    g_return_if_fail(_gwy_brick_check_inside(brick, istart, jstart, kstart));
    xres = brick->xres;
    yres = brick->yres;
    zres = brick->zres;
    bdata = brick->priv->data;
    ddata = plane->priv->data;

    if (width == -1 && height > 0 && depth > 0) {
        g_return_if_fail(plane->xres == height);
        g_return_if_fail(plane->yres == depth);
        g_return_if_fail(jstart + height <= yres);
        g_return_if_fail(kstart + depth <= zres);
        col = istart;
        d = ddata;
        for (lev = 0; lev < depth; lev++) {
            b = bdata + col + xres*jstart + xres*yres*(lev + kstart);
            for (row = 0; row < height; row++, d++, b += xres)
                *b = *d;
        }
    }
    else if (width > 0 && height == -1 && depth > 0) {
        g_return_if_fail(plane->xres == width);
        g_return_if_fail(plane->yres == depth);
        g_return_if_fail(istart + width <= xres);
        g_return_if_fail(kstart + depth <= zres);
        row = jstart;
        for (lev = 0; lev < depth; lev++)
            gwy_assign(bdata + istart + xres*row + xres*yres*(lev + kstart), ddata + lev*width, width);
    }
    else if (width > 0 && height > 0 && depth == -1) {
        g_return_if_fail(plane->xres == width);
        g_return_if_fail(plane->yres == height);
        g_return_if_fail(istart + width <= xres);
        g_return_if_fail(jstart + height <= yres);
        lev = kstart;
        for (row = 0; row < height; row++)
            gwy_assign(bdata + istart + xres*(row + jstart) + xres*yres*(lev), ddata + row*width, width);
    }
    else {
        g_return_if_reached();
    }
}

/**
 * gwy_brick_set_xy_plane:
 * @brick: A data brick.
 * @plane: Datafield to be inserted into brick. It must have dimensions @xres by @yres.
 * @lev: Position in the brick (level index).
 *
 * Sets one full single XY plane in a data brick from data field values.
 **/
void
gwy_brick_set_xy_plane(GwyBrick *brick,
                       GwyField *plane,
                       gint lev)
{
    g_return_if_fail(GWY_IS_BRICK(brick));
    gwy_brick_set_plane(brick, plane, 0, 0, lev, brick->xres, brick->yres, -1);
}

/**
 * gwy_brick_add_to_xy_planes:
 * @brick: A data brick.
 * @plane: Datafield to be added to all brick XY planes.  It must have dimensions @xres by @yres.
 *
 * Adds a data field to all brick XY planes.
 **/
void
gwy_brick_add_to_xy_planes(GwyBrick *brick,
                           GwyField *plane)
{
    gint n, lev;

    g_return_if_fail(GWY_IS_BRICK(brick));
    g_return_if_fail(GWY_IS_FIELD(plane));
    g_return_if_fail(plane->xres == brick->xres);
    g_return_if_fail(plane->yres == brick->yres);

    n = brick->xres * brick->yres;
    for (lev = 0; lev < brick->zres; lev++) {
        gdouble *d = brick->priv->data + n*lev;
        const gdouble *p = plane->priv->data;
        gint k;

        for (k = 0; k < n; k++)
            d[k] += p[k];
    }
}

/**
 * gwy_brick_multiply_xy_planes:
 * @brick: A data brick.
 * @plane: Datafield to multiply all brick XY planes with. It must have dimensions @xres by @yres.
 *
 * Multiplies all brick XY planes by a datafield.
 **/
void
gwy_brick_multiply_xy_planes(GwyBrick *brick,
                             GwyField *plane)
{
    gint n, lev;

    g_return_if_fail(GWY_IS_BRICK(brick));
    g_return_if_fail(GWY_IS_FIELD(plane));
    g_return_if_fail(plane->xres == brick->xres);
    g_return_if_fail(plane->yres == brick->yres);

    n = brick->xres * brick->yres;
    for (lev = 0; lev < brick->zres; lev++) {
        gdouble *d = brick->priv->data + n*lev;
        const gdouble *p = plane->priv->data;
        gint k;

        for (k = 0; k < n; k++)
            d[k] *= p[k];
    }
}

/**
 * gwy_brick_add_to_z_lines:
 * @brick: A data brick.
 * @line: Data line to add to each Z lines.  It must have dimension @zres.
 *
 * Adds a data line to all brick Z lines.
 **/
void
gwy_brick_add_to_z_lines(GwyBrick *brick,
                         GwyLine *line)
{
    gint n, lev;

    g_return_if_fail(GWY_IS_BRICK(brick));
    g_return_if_fail(GWY_IS_LINE(line));
    g_return_if_fail(line->res == brick->zres);

    n = brick->xres * brick->yres;
    for (lev = 0; lev < brick->zres; lev++) {
        gdouble *d = brick->priv->data + n*lev;
        gdouble v = line->priv->data[lev];
        gint k;

        for (k = 0; k < n; k++)
            d[k] += v;
    }
}

static void
serializable_itemize(GwySerializable *serializable, GwySerializableGroup *group)
{
    GwyBrick *brick = GWY_BRICK(serializable);
    GwyBrickPrivate *priv = brick->priv;
    gsize ndata = (gsize)brick->xres * (gsize)brick->yres * (gsize)brick->zres;

    gwy_serializable_group_alloc_size(group, NUM_ITEMS);
    gwy_serializable_group_append_int32(group, serializable_items + ITEM_XRES, brick->xres);
    gwy_serializable_group_append_int32(group, serializable_items + ITEM_YRES, brick->yres);
    gwy_serializable_group_append_int32(group, serializable_items + ITEM_ZRES, brick->zres);
    gwy_serializable_group_append_double(group, serializable_items + ITEM_XREAL, brick->xreal);
    gwy_serializable_group_append_double(group, serializable_items + ITEM_YREAL, brick->yreal);
    gwy_serializable_group_append_double(group, serializable_items + ITEM_ZREAL, brick->zreal);
    gwy_serializable_group_append_double(group, serializable_items + ITEM_XOFF, brick->xoff);
    gwy_serializable_group_append_double(group, serializable_items + ITEM_YOFF, brick->yoff);
    gwy_serializable_group_append_double(group, serializable_items + ITEM_ZOFF, brick->zoff);
    gwy_serializable_group_append_unit(group, serializable_items + ITEM_UNIT_X, priv->unit_x);
    gwy_serializable_group_append_unit(group, serializable_items + ITEM_UNIT_Y, priv->unit_y);
    gwy_serializable_group_append_unit(group, serializable_items + ITEM_UNIT_Z, priv->unit_z);
    gwy_serializable_group_append_unit(group, serializable_items + ITEM_UNIT_W, priv->unit_w);
    gwy_serializable_group_append_double_array(group, serializable_items + ITEM_DATA, priv->data, ndata);
    if (priv->zcalibration) {
        gwy_serializable_group_append_object_array(group, serializable_items + ITEM_CALIBRATION,
                                                   (GObject**)&priv->zcalibration, 1);
    }
    gwy_serializable_group_itemize(group);
}

static gboolean
serializable_construct(GwySerializable *serializable, GwySerializableGroup *group, GwyErrorList **error_list)
{
    GwySerializableItem its[NUM_ITEMS], *it;
    gboolean ok = FALSE;
    gwy_assign(its, serializable_items, NUM_ITEMS);
    gwy_deserialize_filter_items(its, NUM_ITEMS, group, TYPE_NAME, error_list);

    GwyBrick *brick = GWY_BRICK(serializable);
    GwyBrickPrivate *priv = brick->priv;

    /* Botched up data dimensions is a hard fail. */
    guint xres = its[ITEM_XRES].value.v_int32;
    guint yres = its[ITEM_YRES].value.v_int32;
    guint zres = its[ITEM_ZRES].value.v_int32;

    it = its + ITEM_DATA;
    gsize logical_dims[3] = { xres, yres, zres };
    if (!gwy_check_data_dimension(error_list, TYPE_NAME, 3, it->array_size, logical_dims))
        goto fail;

    brick->xres = xres;
    brick->yres = yres;
    brick->zres = zres;
    g_free(priv->data);
    priv->data = it->value.v_double_array;
    it->value.v_double_array = NULL;

    /* The rest is already validated by pspec. */
    brick->xreal = its[ITEM_XREAL].value.v_double;
    brick->yreal = its[ITEM_YREAL].value.v_double;
    brick->zreal = its[ITEM_ZREAL].value.v_double;
    brick->xoff = its[ITEM_XOFF].value.v_double;
    brick->yoff = its[ITEM_YOFF].value.v_double;
    brick->zoff = its[ITEM_ZOFF].value.v_double;

    priv->unit_x = (GwyUnit*)its[ITEM_UNIT_X].value.v_object;
    its[ITEM_UNIT_X].value.v_object = NULL;
    priv->unit_y = (GwyUnit*)its[ITEM_UNIT_Y].value.v_object;
    its[ITEM_UNIT_Y].value.v_object = NULL;
    priv->unit_z = (GwyUnit*)its[ITEM_UNIT_Z].value.v_object;
    its[ITEM_UNIT_Z].value.v_object = NULL;
    priv->unit_w = (GwyUnit*)its[ITEM_UNIT_W].value.v_object;
    its[ITEM_UNIT_W].value.v_object = NULL;

    it = its + ITEM_CALIBRATION;
    if (it->array_size >= 1) {
        GwyLine *line = GWY_LINE(it->value.v_object_array[0]);
        if (gwy_line_get_res(line) == brick->zres) {
            priv->zcalibration = line;
            it->value.v_object_array[0] = NULL;
        }
        else {
            gwy_error_list_add(error_list, GWY_DESERIALIZE_ERROR, GWY_DESERIALIZE_ERROR_REPLACED,
                               // TRANSLATORS: Error message.
                               _("Calibration data #%u of GwyBrick do not match the Z-resolution %d."),
                               0, brick->zres);
        }
    }
    /* Silently accept multiple calibrations even though we do not know what to do with them at this moment. */

    ok = TRUE;

fail:
    g_free(its[ITEM_DATA].value.v_double_array);
    g_clear_object(&its[ITEM_UNIT_X].value.v_object);
    g_clear_object(&its[ITEM_UNIT_Y].value.v_object);
    g_clear_object(&its[ITEM_UNIT_Z].value.v_object);
    g_clear_object(&its[ITEM_UNIT_W].value.v_object);
    it = its + ITEM_CALIBRATION;
    for (guint i = 0; i < it->array_size; i++)
        g_clear_object(it->value.v_object_array + i);
    GWY_FREE(it->value.v_object_array);

    return ok;
}

static GwySerializable*
serializable_copy(GwySerializable *serializable)
{
    GwyBrick *brick = GWY_BRICK(serializable);
    GwyBrick *copy = gwy_brick_new_alike(brick, FALSE);
    gsize n = (gsize)brick->xres * (gsize)brick->yres * (gsize)brick->zres;
    gwy_assign(copy->priv->data, brick->priv->data, n);
    return GWY_SERIALIZABLE(copy);
}

static void
serializable_assign(GwySerializable *destination, GwySerializable *source)
{
    GwyBrick *destbrick = GWY_BRICK(destination), *srcbrick = GWY_BRICK(source);

    alloc_data(destbrick, srcbrick->xres, srcbrick->yres, srcbrick->zres, FALSE);
    gwy_brick_copy_data(srcbrick, destbrick);
    copy_info(srcbrick, destbrick);
    gwy_brick_copy_zcalibration(srcbrick, destbrick);
}

/**
 * gwy_brick_copy:
 * @brick: A data brick to duplicate.
 *
 * Create a new data brick as a copy of an existing one.
 *
 * This function is a convenience gwy_serializable_copy() wrapper.
 *
 * Returns: (transfer full):
 *          A copy of the data brick.
 **/
GwyBrick*
gwy_brick_copy(GwyBrick *brick)
{
    /* Try to return a valid object even on utter failure. Returning NULL probably would crash something soon. */
    if (!GWY_IS_BRICK(brick)) {
        g_assert(GWY_IS_BRICK(brick));
        return g_object_new(GWY_TYPE_BRICK, NULL);
    }
    return GWY_BRICK(gwy_serializable_copy(GWY_SERIALIZABLE(brick)));
}

/**
 * gwy_brick_assign:
 * @destination: Target data brick.
 * @source: Source data brick.
 *
 * Makes one data brick equal to another.
 *
 * This function is a convenience gwy_serializable_assign() wrapper.
 **/
void
gwy_brick_assign(GwyBrick *destination, GwyBrick *source)
{
    g_return_if_fail(GWY_IS_BRICK(destination));
    g_return_if_fail(GWY_IS_BRICK(source));
    if (destination != source)
        gwy_serializable_assign(GWY_SERIALIZABLE(destination), GWY_SERIALIZABLE(source));
}


/**
 * gwy_brick_threshold:
 * @brick: A data brick.
 * @threshold: Threshold value.
 * @below: Lower replacement value.
 * @above: Upper replacement value.
 *
 * Tresholds values of a brick.
 *
 * Values smaller than @threshold are set to value @bottom, values higher than @threshold or equal to it are set to
 * value @top
 *
 * Returns: The total number of values above threshold.
 **/
gint
gwy_brick_threshold(GwyBrick *brick,
                    gdouble threshold,
                    gdouble below,
                    gdouble above)
{
    gint i, n, tot = 0;
    gdouble *d;

    g_return_val_if_fail(GWY_IS_BRICK(brick), 0);
    n = brick->xres * brick->yres * brick->zres;
    d = brick->priv->data;
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            reduction(+:tot) \
            private(i) \
            shared(d,n,threshold,below,above)
#endif
    for (i = 0; i < n; i++) {
        if (d[i] < threshold)
            d[i] = below;
        else {
            d[i] = above;
            tot++;
        }
    }

    return tot;
}


/**
 * SECTION: brick
 * @title: GwyBrick
 * @short_description: Three-dimensional floating point data
 *
 * #GwyBrick is a regular three-dimensional floating point data array. It is typically useful for volume data obtained
 * from SPMs, like in force volume measurements.
 **/

/**
 * GwyBrick:
 *
 * The #GwyBrick struct contains private data only and should be accessed using the functions below.
 **/

/**
 * GwyBrickTransposeType:
 * @GWY_BRICK_TRANSPOSE_XYZ: No change (useful with axis flipping).
 * @GWY_BRICK_TRANSPOSE_XZY: Axes Z and Y are swapped.
 * @GWY_BRICK_TRANSPOSE_YXZ: Axes Y and Z are swapped.
 * @GWY_BRICK_TRANSPOSE_YZX: Axis X becomes Y, Y becomes Z and Z becomes X.
 * @GWY_BRICK_TRANSPOSE_ZXY: Axis X becomes Z, Y becomes X and Z becomes Y.
 * @GWY_BRICK_TRANSPOSE_ZYX: Axes X and Z are swapped.
 *
 * Type of volume data transposition.
 *
 * The enum values names spell which old axis becomes which new axes.
 **/

/* vim: set cin columns=120 tw=118 et ts=4 sw=4 cino=>1s,e0,n0,f0,{0,}0,^0,\:1s,=0,g1s,h0,t0,+1s,c3,(0,u0 : */
