/*
 *  $Id: chaining.c 30047 2026-06-11 08:30:45Z yeti-dn $
 *  Copyright (C) 2026 David Necas (Yeti).
 *  E-mail: yeti@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 <glib/gi18n-lib.h>
#include <string.h>
#include <stdlib.h>
#include <gtk/gtk.h>
#include <gwy.h>
#include "preview.h"
#include "libgwyddion/omp.h"

#define RUN_MODES (GWY_RUN_IMMEDIATE | GWY_RUN_INTERACTIVE)

typedef enum {
    RNG_POSITION,
    RNG_RADIUS,
    RNG_HEIGHT,
    RNG_DIRECTION,
    RNG_TERMINATION,
    RNG_NRNGS
} ObjSynthRng;

enum {
    PARAM_COVERAGE,
    PARAM_RADIUS,
    PARAM_RADIUS_NOISE,
    PARAM_DIR_NOISE,
    PARAM_TERMINATION_ENABLE,
    PARAM_TERMINATION_LEN,
    PARAM_SNAP_CENTER,
    PARAM_SNAP_WIDTH,
    PARAM_HEIGHT,
    PARAM_HEIGHT_NOISE,
    PARAM_CHAIN_HEIGHT_NOISE,
    PARAM_APPLY_CLOSING,
    PARAM_CLOSING_SIZE,
    PARAM_SEED,
    PARAM_RANDOMIZE,
    PARAM_UPDATE,
    PARAM_ACTIVE_PAGE,
    BUTTON_LIKE_CURRENT_IMAGE,

    PARAM_DIMS0
};

typedef enum {
    ANGLE_BLOCKED       = 0,
    ANGLE_TOUCHING      = 1,
    ANGLE_FREE          = 2,
    ANGLE_NO_NEIGHBOURS = 3,
} AngleRangeResult;

typedef struct {
    gdouble x;
    gdouble y;
    gdouble r;
} Disc;

typedef struct {
    gdouble from;
    gdouble to;
} Range;

typedef struct {
    GwyParams *params;
    GwyField *field;
    GwyField *result;
    /* Cached input image parameters. */
    gdouble zscale;  /* Negative value means there is no input image. */
} ModuleArgs;

typedef struct {
    ModuleArgs *args;
    GtkWidget *dialog;
    GwyParamTable *table_dimensions;
    GwyParamTable *table_generator;
    GwyField *template_;
} ModuleGUI;

static gboolean         module_register     (void);
static GwyParamDef*     define_module_params(void);
static void             module_main         (GwyFile *data,
                                             GwyRunModeFlags mode);
static void             execute             (ModuleArgs *args);
static GwyDialogOutcome run_gui             (ModuleArgs *args,
                                             GwyFile *data,
                                             gint id);
static GtkWidget*       dimensions_tab_new  (ModuleGUI *gui);
static GtkWidget*       generator_tab_new   (ModuleGUI *gui);
static void             param_changed       (ModuleGUI *gui,
                                             gint id);
static void             dialog_response     (ModuleGUI *gui,
                                             gint response);
static void             preview             (gpointer user_data);

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Generates chains of discs."),
    "Yeti <yeti@gwyddion.net>",
    "1.0",
    "David Nečas (Yeti)",
    "2026",
};

GWY_MODULE_QUERY2(module_info, chaining)

static gboolean
module_register(void)
{
    gwy_synth_func_register("chaining",
                            module_main,
                            N_("/_Deposition/_Chains..."),
                            NULL,
                            RUN_MODES,
                            N_("Generate surface of chained discs"));

    return TRUE;
}

static GwyParamDef*
define_module_params(void)
{
    static GwyParamDef *paramdef = NULL;

    if (paramdef)
        return paramdef;

    paramdef = gwy_param_def_new();
    gwy_param_def_set_function_name(paramdef, gwy_synth_func_current());
    // Close packing is ≈ 0.906, but with varying size maybe we can exceed it a little bit.
    gwy_param_def_add_double(paramdef, PARAM_COVERAGE, "coverage", _("Co_verage"), 1e-5, 0.92, 0.4);
    gwy_param_def_add_double(paramdef, PARAM_RADIUS, "radius", _("_Radius"), 5.0, 1000.0, 30.0);
    gwy_param_def_add_double(paramdef, PARAM_RADIUS_NOISE, "radius_noise", _("Spread"), 0.0, 1.0, 0.0);
    gwy_param_def_add_double(paramdef, PARAM_DIR_NOISE, "dir_noise", _("_Direction spread"), 0.0, 1.0, 0.1);
    gwy_param_def_add_boolean(paramdef, PARAM_TERMINATION_ENABLE, "termination_enable", NULL, FALSE);
    gwy_param_def_add_double(paramdef, PARAM_TERMINATION_LEN, "termination_len",
                             _("Spontaneous _termination"), 0.0, 9.0, 2.0);
    gwy_param_def_add_double(paramdef, PARAM_SNAP_CENTER, "snap_center", _("C_enter"), -1.0, 2.0, 0.0);
    gwy_param_def_add_double(paramdef, PARAM_SNAP_WIDTH, "snap_width", _("_Width"), 0.01, 100.0, 0.5);
    gwy_param_def_add_double(paramdef, PARAM_HEIGHT, "height", _("_Height"), 1e-4, 1000.0, 1.0);
    gwy_param_def_add_double(paramdef, PARAM_HEIGHT_NOISE, "height_noise", _("Spread"), 0.0, 1.0, 0.5);
    gwy_param_def_add_double(paramdef, PARAM_CHAIN_HEIGHT_NOISE, "chain_height_noise",
                             _("Chain height spread"), 0.0, 1.0, 0.5);
    gwy_param_def_add_boolean(paramdef, PARAM_APPLY_CLOSING, "apply_closing", _("E_xpand borders"), FALSE);
    gwy_param_def_add_double(paramdef, PARAM_CLOSING_SIZE, "closing_size", _("Si_ze"), 1.0, 100.0, 10.0);
    gwy_param_def_add_seed(paramdef, PARAM_SEED, "seed", NULL);
    gwy_param_def_add_randomize(paramdef, PARAM_RANDOMIZE, PARAM_SEED, "randomize", NULL, TRUE);
    gwy_param_def_add_instant_updates(paramdef, PARAM_UPDATE, "update", NULL, TRUE);
    gwy_param_def_add_active_page(paramdef, PARAM_ACTIVE_PAGE, "active_page", NULL);
    gwy_synth_define_dimensions_params(paramdef, PARAM_DIMS0);
    return paramdef;
}

static void
module_main(GwyFile *data, GwyRunModeFlags mode)
{
    GwyDialogOutcome outcome = GWY_DIALOG_PROCEED;
    ModuleArgs args;
    GwyField *field;
    gint id;

    g_return_if_fail(mode & RUN_MODES);
    gwy_clear1(args);
    gwy_data_browser_get_current(GWY_APP_FIELD, &field,
                                 GWY_APP_FIELD_ID, &id,
                                 0);
    args.field = field;
    args.zscale = field ? gwy_field_rms(field) : -1.0;

    args.params = gwy_params_new_from_settings(define_module_params());
    gwy_synth_sanitise_params(args.params, PARAM_DIMS0, field);
    if (mode == GWY_RUN_INTERACTIVE) {
        outcome = run_gui(&args, data, id);
        gwy_params_save_to_settings(args.params);
        if (outcome == GWY_DIALOG_CANCEL)
            goto end;
    }
    args.result = gwy_synth_make_result_field((args.field = field), args.params, FALSE);
    execute(&args);
    gwy_synth_add_image_to_file(args.result, data, id, args.params);

end:
    g_clear_object(&args.result);
    g_object_unref(args.params);
}

static GwyDialogOutcome
run_gui(ModuleArgs *args, GwyFile *data, gint id)
{
    GwyDialogOutcome outcome;
    GwyDialog *dialog;
    GtkWidget *hbox, *dataview;
    GtkNotebook *notebook;
    ModuleGUI gui;

    gwy_clear1(gui);
    gui.args = args;
    gui.template_ = args->field;

    if (gui.template_)
        args->field = gwy_synth_make_preview_field(gui.template_, PREVIEW_SIZE);
    else
        args->field = gwy_field_new(PREVIEW_SIZE, PREVIEW_SIZE, PREVIEW_SIZE, PREVIEW_SIZE, TRUE);
    args->result = gwy_synth_make_result_field(args->field, args->params, TRUE);

    gui.dialog = gwy_dialog_new(_("Random Discs"));
    dialog = GWY_DIALOG(gui.dialog);
    gwy_dialog_add_stock_buttons(dialog, GWY_STOCK_UPDATE, GWY_STOCK_RESET, GWY_STOCK_CANCEL, GWY_STOCK_OK, 0);

    dataview = gwy_create_preview(args->result, NULL, PREVIEW_SIZE);
    gwy_setup_data_view(GWY_DATA_VIEW(dataview), data, GWY_FILE_IMAGE, id, GWY_FILE_ITEM_PALETTE);
    if (gui.template_)
        gwy_setup_data_view(GWY_DATA_VIEW(dataview), data, GWY_FILE_IMAGE, id, GWY_FILE_ITEM_PALETTE);
    hbox = gwy_create_dialog_preview_hbox(GTK_DIALOG(dialog), GWY_DATA_VIEW(dataview), FALSE);

    notebook = GTK_NOTEBOOK(gtk_notebook_new());
    gtk_box_pack_start(GTK_BOX(hbox), GTK_WIDGET(notebook), TRUE, TRUE, 0);

    gtk_notebook_append_page(notebook, dimensions_tab_new(&gui), gtk_label_new(_("Dimensions")));
    gtk_notebook_append_page(notebook, generator_tab_new(&gui), gtk_label_new(_("Generator")));
    gwy_param_active_page_link_to_notebook(args->params, PARAM_ACTIVE_PAGE, notebook);

    g_signal_connect_swapped(gui.table_dimensions, "param-changed", G_CALLBACK(param_changed), &gui);
    g_signal_connect_swapped(gui.table_generator, "param-changed", G_CALLBACK(param_changed), &gui);
    g_signal_connect_swapped(dialog, "response", G_CALLBACK(dialog_response), &gui);
    gwy_dialog_set_preview_func(dialog, GWY_PREVIEW_IMMEDIATE, preview, &gui, NULL);

    outcome = gwy_dialog_run(dialog);

    g_clear_object(&args->field);
    g_clear_object(&args->result);

    return outcome;
}

static GtkWidget*
dimensions_tab_new(ModuleGUI *gui)
{
    gui->table_dimensions = gwy_param_table_new(gui->args->params);
    gwy_synth_append_dimensions_to_param_table(gui->table_dimensions, 0);

    return gwy_param_table_widget(gui->table_dimensions);
}

static GtkWidget*
generator_tab_new(ModuleGUI *gui)
{
    GwyParamTable *table;

    table = gui->table_generator = gwy_param_table_new(gui->args->params);

    gwy_param_table_append_header(table, -1, _("Discs"));
    gwy_param_table_append_slider(table, PARAM_COVERAGE);
    gwy_param_table_append_slider(table, PARAM_RADIUS);
    gwy_param_table_slider_set_mapping(table, PARAM_RADIUS, GWY_SCALE_MAPPING_LOG);
    gwy_param_table_slider_add_alt(table, PARAM_RADIUS);
    gwy_param_table_append_slider(table, PARAM_RADIUS_NOISE);
    gwy_param_table_append_separator(table);
    gwy_param_table_append_slider(table, PARAM_DIR_NOISE);
    gwy_param_table_append_slider(table, PARAM_TERMINATION_LEN);
    gwy_param_table_set_unitstr(table, PARAM_TERMINATION_LEN, "log<sub>10</sub>");
    gwy_param_table_slider_set_mapping(table, PARAM_TERMINATION_LEN, GWY_SCALE_MAPPING_LINEAR);
    gwy_param_table_add_enabler(table, PARAM_TERMINATION_ENABLE, PARAM_TERMINATION_LEN);

    gwy_param_table_append_header(table, -1, _("Snapping Distribution"));
    gwy_param_table_append_slider(table, PARAM_SNAP_CENTER);
    gwy_param_table_slider_set_mapping(table, PARAM_SNAP_CENTER, GWY_SCALE_MAPPING_LINEAR);
    gwy_param_table_append_slider(table, PARAM_SNAP_WIDTH);

    gwy_param_table_append_header(table, -1, _("Height"));
    gwy_param_table_append_slider(table, PARAM_HEIGHT);
    gwy_param_table_slider_set_mapping(table, PARAM_HEIGHT, GWY_SCALE_MAPPING_LOG);
    if (gui->template_) {
        gwy_param_table_append_button(table, BUTTON_LIKE_CURRENT_IMAGE, -1, GWY_RESPONSE_SYNTH_INIT_Z,
                                      _("_Like Current Image"));
    }
    gwy_param_table_append_slider(table, PARAM_HEIGHT_NOISE);
    gwy_param_table_append_slider(table, PARAM_CHAIN_HEIGHT_NOISE);

    gwy_param_table_append_header(table, -1, _("Postprocessing"));
    gwy_param_table_append_checkbox(table, PARAM_APPLY_CLOSING);
    gwy_param_table_append_slider(table, PARAM_CLOSING_SIZE);
    gwy_param_table_slider_add_alt(table, PARAM_CLOSING_SIZE);

    gwy_param_table_append_header(table, -1, _("Options"));
    gwy_param_table_append_seed(table, PARAM_SEED);
    gwy_param_table_append_checkbox(table, PARAM_RANDOMIZE);
    gwy_param_table_append_separator(table);
    gwy_param_table_append_checkbox(table, PARAM_UPDATE);


    return gwy_param_table_widget(table);
}

static void
param_changed(ModuleGUI *gui, gint id)
{
    GwyParamTable *table = gui->table_generator;

    if (gwy_synth_handle_param_changed(gui->table_dimensions, id))
        id = -1;

    if (id < 0 || id == PARAM_DIMS0 + GWY_DIMS_PARAM_ZUNIT) {
        static const gint zids[] = { PARAM_HEIGHT };

        gwy_synth_update_value_unitstrs(table, zids, G_N_ELEMENTS(zids));
        gwy_synth_update_like_current_button_sensitivity(table, BUTTON_LIKE_CURRENT_IMAGE);
    }
    if (id < 0
        || id == PARAM_DIMS0 + GWY_DIMS_PARAM_XYUNIT
        || id == PARAM_DIMS0 + GWY_DIMS_PARAM_XRES
        || id == PARAM_DIMS0 + GWY_DIMS_PARAM_XREAL) {
        static const gint xyids[] = { PARAM_RADIUS, PARAM_CLOSING_SIZE };

        gwy_synth_update_lateral_alts(table, xyids, G_N_ELEMENTS(xyids));
    }

    if ((id < PARAM_DIMS0 || id == PARAM_DIMS0 + GWY_DIMS_PARAM_INITIALIZE)
        && id != PARAM_UPDATE && id != PARAM_RANDOMIZE)
        gwy_dialog_invalidate(GWY_DIALOG(gui->dialog));
}

static void
dialog_response(ModuleGUI *gui, gint response)
{
    ModuleArgs *args = gui->args;

    if (response == GWY_RESPONSE_SYNTH_INIT_Z) {
        gdouble zscale = args->zscale;
        gint power10z;

        if (zscale > 0.0) {
            gwy_params_get_unit(args->params, PARAM_DIMS0 + GWY_DIMS_PARAM_ZUNIT, &power10z);
            gwy_param_table_set_double(gui->table_generator, PARAM_HEIGHT, zscale/gwy_exp10(power10z));
        }
    }
    else if (response == GWY_RESPONSE_SYNTH_TAKE_DIMS) {
        gwy_synth_use_dimensions_template(gui->table_dimensions);
    }
}

static void
preview(gpointer user_data)
{
    ModuleGUI *gui = (ModuleGUI*)user_data;

    execute(gui->args);
    gwy_field_data_changed(gui->args->result);
}

static void
circular_area_fill_periodic(GwyField *field, gint col, gint row,
                            gdouble radius, gdouble value)
{
    if (radius < 0.0)
        return;

    gint r2 = floor(radius*radius + 1e-12);
    gint r = floor(radius + 1e-12);
    gint xres = field->xres, yres = field->yres;
    gdouble *d = gwy_field_get_data(field);

    /* Ensure circle centre inside the base area. */
    col %= xres;
    if (col < 0)
        col += xres;

    row %= yres;
    if (row < 0)
        row += yres;

    for (gint i = -r; i <= r; i++) {
        gint ii = (i + row + yres) % yres;
        gdouble *drow = d + ii*xres;

        gdouble s = sqrt(r2 - i*i);
        gint jfrom = col + ceil(-s);
        gint jto = col + floor(s);
        /* If we are to fill the entire row just do it simply.  As a benefit, we can assume we fill less than one
         * entire row in the other cases. */
        if (jto+1 - jfrom >= xres) {
            for (gint j = xres; j; j--, drow++)
                *drow = value;
        }
        /* Sticking outside to the left. */
        else if (jfrom < 0) {
            for (gint j = 0; j <= jto; j++)
                drow[j] = value;
            gint jj = (jfrom + xres) % xres;
            for (gint j = jj; j < xres; j++)
                drow[j] = value;
        }
        /* Sticking outside to the right. */
        else if (jto >= xres) {
            gint jj = (jto + 1) % xres;
            for (gint j = 0; j < jj; j++)
                drow[j] = value;
            for (gint j = jfrom; j < xres; j++)
                drow[j] = value;
        }
        /* Entirely inside. */
        else {
            for (gint j = jfrom; j <= jto; j++)
                drow[j] = value;
        }
    }
}

static gdouble
random_disc_radius(GwyRandGenSet *rngset, gdouble radius, gdouble radius_noise)
{
    gdouble r = radius;
    if (radius_noise) {
        r *= exp(gwy_rand_gen_set_gaussian(rngset, RNG_RADIUS, radius_noise));
        r = CLAMP(r, 0.25*radius, 4.0*radius);
    }
    return r;
}

static gdouble
periodic_distance2(const Disc *d1, const Disc *d2, gdouble xsize, gdouble ysize)
{
    gdouble dx, dy;

    if (d1->x <= d2->x)
        dx = MIN(d2->x - d1->x, d1->x + xsize - d2->x);
    else
        dx = MIN(d1->x - d2->x, d2->x + xsize - d1->x);

    if (d1->y <= d2->y)
        dy = MIN(d2->y - d1->y, d1->y + ysize - d2->y);
    else
        dy = MIN(d1->y - d2->y, d2->y + ysize - d1->y);

    return dx*dx + dy*dy;
}

static gboolean
generate_new_starting_disc(GArray *discs,
                           gdouble radius, gdouble radius_noise, GwyRandGenSet *rngset,
                           gdouble xsize, gdouble ysize)
{
    GRand *rng = gwy_rand_gen_set_rng(rngset, RNG_POSITION);

    Disc d;
    d.r = random_disc_radius(rngset, radius, radius_noise);
    if (radius_noise) {
        d.r *= exp(gwy_rand_gen_set_gaussian(rngset, RNG_RADIUS, radius_noise));
        d.r = CLAMP(d.r, 0.25*radius, 4.0*radius);
    }

    gint failcount = 0;
    while (failcount < 50) {
        d.x = g_rand_double(rng)*xsize;
        d.y = g_rand_double(rng)*ysize;

        guint i;
        for (i = 0; i < discs->len; i++) {
            const Disc *p = &g_array_index(discs, Disc, i);
            gdouble d2 = periodic_distance2(&d, p, xsize, ysize);
            gdouble s = d.r + p->r;
            if (d2 <= s*s)
                break;
        }
        if (i == discs->len) {
            g_array_append_val(discs, d);
            return TRUE;
        }
        else
            failcount++;
    }
    return FALSE;
}

static void
find_blocked_angles(GArray *discs, gdouble newr, gdouble xsize, gdouble ysize,
                    GArray *blocked_angles)
{
    g_array_set_size(blocked_angles, 0);

    // The preceding disc is always touching and not blocking anything.
    guint n = discs->len;
    const Disc *prevd = &g_array_index(discs, Disc, n-1);
    gdouble R = prevd->r + newr;
    for (guint i = 0; i < n-1; i++) {
        const Disc *p = &g_array_index(discs, Disc, i);
        gdouble K = p->r;
        gdouble d2 = periodic_distance2(prevd, p, xsize, ysize);
        gdouble s = R + newr + K;
        // Too far to block any angle.
        if (d2 >= s*s)
            continue;

        gdouble ca = (d2 + R*R - (K + newr)*(K + newr))/(2*R*sqrt(d2));
        if (fabs(ca) >= 1.0)
            continue;

        gdouble dx = p->x - prevd->x, dy = p->y - prevd->y;
        if (dx < -0.5*xsize)
            dx += xsize;
        else if (dx > 0.5*xsize)
            dx -= xsize;

        if (dy < -0.5*ysize)
            dy += ysize;
        else if (dy > 0.5*ysize)
            dy -= ysize;

        // Since the two angles are created by offsets from the central angle, they are always in order, even though
        // they are not always both in [-π, π].
        gdouble alpha = acos(ca);
        gdouble phi = atan2(dy, dx);
        Range range = { .from = phi - alpha, .to = phi + alpha };
        g_array_append_val(blocked_angles, range);
    }

    // The angular intervals for other discs can occasionally intersect. Split intervals crossing the +π/−π bounary,
    // ensuring we can always do simple comparisons.
    guint len = blocked_angles->len;
    for (guint i = 0; i < len; i++) {
        Range *range = &g_array_index(blocked_angles, Range, i);
        if (range->from < -G_PI) {
            Range leading = { .from = range->from + 2*G_PI, .to = G_PI };
            range->from = -G_PI;
            g_array_append_val(blocked_angles, leading);
        }
        if (range->to > G_PI) {
            Range trailing = { .from = -G_PI, .to = range->to - 2*G_PI };
            range->to = G_PI;
            g_array_append_val(blocked_angles, trailing);
        }
    }

    len = blocked_angles->len;
    if (len < 2)
        return;

    g_array_sort(blocked_angles, gwy_compare_double);
    for (guint i = 1; i < len; ) {
        // The ranges are ordered by the starting angle (and do not cross the +π/−π boundary). So the only possible
        // merging types are (using simple angle comparison):
        // (a) (i+1)-th is contained in i-th
        // (b) (i+1)-th contains i-th
        // (c) (i+1)-th starts in i-th and extends beyond its end.
        // In all these cases, the condition is that the (i+1)-th from is before the i-th interval ends. The merging
        // is always [i-th from, max(tos)].
        Range *ra = &g_array_index(blocked_angles, Range, i-1);
        Range *rb = &g_array_index(blocked_angles, Range, i);
        if (rb->from <= ra->to) {
            ra->to = fmax(ra->to, rb->to);
            g_array_remove_index(blocked_angles, i);
            len--;
        }
        else
            i++;
    }
}

static AngleRangeResult
find_possible_angle_range(gdouble phi, GArray *blocked_angles, Range *phi_range)
{
    gdouble largest_before = -G_PI, smallest_after = G_PI;
    gint n = blocked_angles->len;
    if (!n) {
        // The difference from ANGLE_FREE is that there is nothing to snap to. This is true when placing the very
        // first disc of any chain (which does not happen here) and the total second disc.
        phi_range->from = largest_before;
        phi_range->to = smallest_after;
        return ANGLE_NO_NEIGHBOURS;
    }

    const Range *ranges = &g_array_index(blocked_angles, Range, 0);

    // All angles are blocked.
    if (n == 1 && ranges[0].from <= -G_PI && ranges[0].to >= G_PI)
        return ANGLE_BLOCKED;

    gint phi_blocked_by = -1;
    for (gint i = 0; i < n; i++) {
        if (ranges[i].to <= phi)
            largest_before = fmax(largest_before, ranges[i].to);
        if (ranges[i].from >= phi)
            smallest_after = fmin(smallest_after, ranges[i].from);
        if (ranges[i].from < phi && ranges[i].to > phi) {
            phi_blocked_by = i;
            break;
        }
    }
    // When nothing blocks phi, we already have the range – except when one of the endpoints is ±π because then the
    // free interval extends further. Look at the next interval across the +π/−π boundary.
    if (phi_blocked_by < 0) {
        phi_range->from = largest_before;
        phi_range->to = smallest_after;
        if (smallest_after >= G_PI)
            phi_range->to = ranges[0].from + 2*G_PI;
        if (largest_before <= -G_PI)
            phi_range->from = ranges[n-1].to - 2*G_PI;
        return ANGLE_FREE;
    }

    // There seem to be some free angles, but not containing phi. We know the blocking interval, so look to both
    // sides and find the closest free angle.
    gdouble phi_prev = ranges[phi_blocked_by].from, phi_next = ranges[phi_blocked_by].to;
    if (phi_next >= G_PI) {
        g_assert(phi_blocked_by == n-1);
        phi_next = ranges[0].to + 2*G_PI;
    }
    else if (phi_prev <= -G_PI) {
        g_assert(phi_blocked_by == 0);
        phi_prev = ranges[n-1].from - 2*G_PI;
    }
    if (phi_next - phi <= phi - phi_prev)
        phi_range->from = phi_range->to = phi_next;
    else
        phi_range->from = phi_range->to = phi_prev;
    return ANGLE_TOUCHING;
}

static void
add_disc(GwyField *field, GArray *discs, gdouble height, gdouble height_noise, gdouble chain_z, gdouble area,
         GwyRandGenSet *rngset, gdouble *coverage)
{
    gdouble z = gwy_rand_gen_set_gaussian(rngset, RNG_HEIGHT, height_noise);
    z = sqrt(z*z + 1.0) + z;   /* To (0, ∞) */

    const Disc *disc = &g_array_index(discs, Disc, discs->len-1);
    circular_area_fill_periodic(field, floor(disc->x), floor(disc->y), disc->r, 0.5*height*(chain_z + z));
    *coverage += G_PI*disc->r*disc->r/area;
}

static void
execute(ModuleArgs *args)
{
    GwyParams *params = args->params;
    gboolean do_initialise = gwy_params_get_boolean(params, PARAM_DIMS0 + GWY_DIMS_PARAM_INITIALIZE);
    gdouble height = gwy_params_get_double(params, PARAM_HEIGHT);
    gdouble height_noise = gwy_params_get_double(params, PARAM_HEIGHT_NOISE);
    gdouble chain_height_noise = gwy_params_get_double(params, PARAM_CHAIN_HEIGHT_NOISE);
    gdouble coverage = gwy_params_get_double(params, PARAM_COVERAGE);
    gdouble radius = gwy_params_get_double(params, PARAM_RADIUS);
    gdouble radius_noise = gwy_params_get_double(params, PARAM_RADIUS_NOISE);
    gdouble dir_noise = gwy_params_get_double(params, PARAM_DIR_NOISE);
    gboolean enable_termination = gwy_params_get_boolean(params, PARAM_TERMINATION_ENABLE);
    gdouble termination_len = gwy_exp10(gwy_params_get_double(params, PARAM_TERMINATION_LEN));
    gdouble snap_center = gwy_params_get_double(params, PARAM_SNAP_CENTER);
    gdouble snap_width = gwy_params_get_double(params, PARAM_SNAP_WIDTH);
    gboolean apply_closing = gwy_params_get_boolean(params, PARAM_APPLY_CLOSING);
    gdouble closing_size = gwy_params_get_double(params, PARAM_CLOSING_SIZE);
    GwyField *result = args->result;
    gint xres = gwy_field_get_xres(result), yres = gwy_field_get_yres(result);
    gdouble xsize = xres, ysize = yres;

    GwyRandGenSet *rngset = gwy_rand_gen_set_new(RNG_NRNGS);
    gwy_rand_gen_set_init(rngset, gwy_params_get_int(params, PARAM_SEED));

    gint power10z;
    gwy_params_get_unit(params, PARAM_DIMS0 + GWY_DIMS_PARAM_ZUNIT, &power10z);
    height *= gwy_exp10(power10z);

    GArray *discs = g_array_new(FALSE, FALSE, sizeof(Disc));
    GArray *blocked_angles = g_array_new(FALSE, FALSE, sizeof(Range));
    gwy_field_clear(result);

    gdouble curr_cov = 0.0;
    while (curr_cov < coverage && generate_new_starting_disc(discs, radius, radius_noise, rngset, xsize, ysize)) {
        gdouble chain_z = gwy_rand_gen_set_gaussian(rngset, RNG_HEIGHT, chain_height_noise);
        add_disc(result, discs, height, height_noise, chain_z, xsize*ysize, rngset, &curr_cov);
        gdouble phi = 2.0*G_PI*gwy_rand_gen_set_double(rngset, RNG_DIRECTION);
        guint chain_len = 1;
        gdouble chain_end = (enable_termination
                             ? termination_len*(-log(gwy_rand_gen_set_double(rngset, RNG_TERMINATION)))
                             : G_MAXDOUBLE);

        while (curr_cov < coverage && chain_len <= chain_end) {
            phi += G_PI*gwy_rand_gen_set_gaussian(rngset, RNG_DIRECTION, dir_noise);
            phi = gwy_canonicalize_angle(phi, FALSE, TRUE);
            const Disc *oldd = &g_array_index(discs, Disc, discs->len-1);
            Disc newd;
            newd.r = random_disc_radius(rngset, radius, radius_noise);
            find_blocked_angles(discs, newd.r, xsize, ysize, blocked_angles);
            Range phi_range;

            AngleRangeResult arr = find_possible_angle_range(phi, blocked_angles, &phi_range);
            if (arr == ANGLE_BLOCKED) {
                // If we cannot find any angle for the new disc, we are stuck. Start afresh somewhere else.
                break;
            }
            if (arr == ANGLE_TOUCHING || arr == ANGLE_NO_NEIGHBOURS) {
                // If the disc had to be moved from the natural direction elsewhere, it is already touching something,
                // so do not do any additional snapping. Likewise, when there is nothing to snap to, do not snap to
                // empty space.
                phi = 0.5*(phi_range.from + phi_range.to);
            }
            else {
                // The natural direction is free. Apply snapping as required. The returned range is positioned to
                // contain phi without any mode-2π complications.
                gdouble pplus = (phi_range.to - phi)/(phi_range.to - phi_range.from);
                if (gwy_rand_gen_set_double(rngset, RNG_DIRECTION) < pplus) {
                    // Snap to larger angles.
                    phi_range.from = phi;
                }
                else {
                    // Snap to smaller angles.
                    phi_range.to = phi_range.from;
                    phi_range.from = phi;
                }
                gdouble t = snap_center + gwy_rand_gen_set_gaussian(rngset, RNG_DIRECTION, snap_width);
                t = fmin(fmax(t, 0.0), 1.0);
                phi = phi_range.from + t*(phi_range.to - phi_range.from);
            }

            newd.x = oldd->x + (oldd->r + newd.r)*cos(phi);
            newd.y = oldd->y + (oldd->r + newd.r)*sin(phi);
            newd.x = fmod(newd.x + 2*xsize, xsize);
            newd.y = fmod(newd.y + 2*ysize, ysize);
            g_array_append_val(discs, newd);
            add_disc(result, discs, height, height_noise, chain_z, xsize*ysize, rngset, &curr_cov);
            chain_len++;
        }
    }
    g_array_free(blocked_angles, TRUE);
    g_array_free(discs, TRUE);

    if (apply_closing) {
        GwyNield *mask = gwy_field_new_nield_alike(result);
        gwy_nield_mark_by_threshold(mask, result, G_MINDOUBLE, TRUE);
        GwyNield *origmask = gwy_nield_copy(mask);
        gwy_nield_grow(mask, closing_size, GWY_DISTANCE_TRANSFORM_EUCLIDEAN, FALSE);
        gwy_nield_merge(mask, origmask, GWY_MERGE_SUBTRACT);
        g_object_unref(origmask);
        gwy_field_laplace_solve(result, mask, GWY_LAPLACE_MASKED, 1.0);
        g_object_unref(mask);
    }

    if (args->field && do_initialise)
        gwy_field_sum_fields(result, result, args->field);

    gwy_rand_gen_set_free(rngset);
}

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