/*
 *  $Id: layer-path.c 29656 2026-03-26 10:21:10Z yeti-dn $
 *  Copyright (C) 2024 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 <gdk/gdkkeysyms.h>

#include "libgwyddion/macros.h"
#include "libgwyddion/math.h"
#include "libgwyddion/selection-path.h"
#include "libgwyddion/spline.h"

#include "libgwyui/data-view.h"
#include "libgwyui/layer-path.h"
#include "libgwyui/cairo-utils.h"
#include "libgwyui/layer-utils.h"

enum {
    OBJECT_SIZE = 2
};

enum {
    PROP_0,
    PROP_THICKNESS,
    NUM_PROPERTIES,
};

struct _GwyLayerPathPrivate {
    /* Properties */
    gdouble thickness;

    /* Dynamic state */
    GwySpline *spline;
};

static void finalize       (GObject *object);
static void set_property   (GObject *object,
                            guint prop_id,
                            const GValue *value,
                            GParamSpec *pspec);
static void get_property   (GObject *object,
                            guint prop_id,
                            GValue *value,
                            GParamSpec *pspec);
static void draw           (GwyVectorLayer *layer,
                            cairo_t *cr);
static void pointer_moved  (GwyVectorLayer *layer,
                            gdouble x,
                            gdouble y);
static void button_pressed (GwyVectorLayer *layer,
                            gdouble x,
                            gdouble y);
static void button_released(GwyVectorLayer *layer,
                            gdouble x,
                            gdouble y);
static gint near_point     (GwyVectorLayer *layer,
                            gdouble xreal,
                            gdouble yreal);
//static gboolean key_pressed        (GwyVectorLayer *layer,
//                                    GdkEventKey *event);

static GParamSpec *properties[NUM_PROPERTIES] = { NULL, };
static GwyVectorLayerClass *parent_class = NULL;

G_DEFINE_TYPE_WITH_CODE(GwyLayerPath, gwy_layer_path, GWY_TYPE_VECTOR_LAYER,
                        G_ADD_PRIVATE(GwyLayerPath))

static void
gwy_layer_path_class_init(GwyLayerPathClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
    GwyVectorLayerClass *vector_class = GWY_VECTOR_LAYER_CLASS(klass);

    parent_class = gwy_layer_path_parent_class;

    gobject_class->finalize = finalize;
    gobject_class->set_property = set_property;
    gobject_class->get_property = get_property;

    vector_class->selection_type = GWY_TYPE_SELECTION_PATH;
    vector_class->draw = draw;
    /* We do NOT implement invalidate_object() because we need to always invalidate the entire path region. */
    vector_class->motion = pointer_moved;
    vector_class->button_press = button_pressed;
    vector_class->button_release = button_released;
    //vector_class->key_press = key_pressed;

    properties[PROP_THICKNESS] = g_param_spec_double("thickness", NULL,
                                                     "Size of markers denoting line thickness",
                                                     0.0, 1024.0, 0.0,
                                                     GWY_GPARAM_RWE);

    g_object_class_install_properties(gobject_class, NUM_PROPERTIES, properties);
}

static void
gwy_layer_path_init(GwyLayerPath *layer)
{
    GwyLayerPathPrivate *priv;

    layer->priv = priv = gwy_layer_path_get_instance_private(layer);
    priv->spline = gwy_spline_new();
}

static void
finalize(GObject *object)
{
    GwyLayerPathPrivate *priv = GWY_LAYER_PATH(object)->priv;

    gwy_spline_free(priv->spline);
    G_OBJECT_CLASS(parent_class)->finalize(object);
}

static void
set_property(GObject *object,
             guint prop_id,
             const GValue *value,
             GParamSpec *pspec)
{
    GwyLayerPath *layer = GWY_LAYER_PATH(object);

    switch (prop_id) {
        case PROP_THICKNESS:
        gwy_layer_path_set_thickness(layer, g_value_get_double(value));
        break;

        default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

static void
get_property(GObject *object,
             guint prop_id,
             GValue *value,
             GParamSpec *pspec)
{
    GwyLayerPathPrivate *priv = GWY_LAYER_PATH(object)->priv;

    switch (prop_id) {
        case PROP_THICKNESS:
        g_value_set_double(value, priv->thickness);
        break;

        default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

static void
draw(GwyVectorLayer *layer, cairo_t *cr)
{
    GwySelection *selection = gwy_vector_layer_get_selection(layer);
    GtkWidget *parent = gwy_data_view_layer_get_parent(GWY_DATA_VIEW_LAYER(layer));
    if (!parent) {
        g_warning("Standalone drawing is not implemented yet.");
        return;
    }
    g_return_if_fail(GWY_IS_DATA_VIEW(parent));
    GwyDataView *dataview = GWY_DATA_VIEW(parent);

    cairo_set_source_rgb(cr, 1.0, 1.0, 1.0);
    cairo_set_line_width(cr, 1.0);
    cairo_set_operator(cr, CAIRO_OPERATOR_DIFFERENCE);

    guint n = gwy_selection_get_n_objects(selection);
    if (!n)
        return;

    GwyXY *screenxy = g_new(GwyXY, n);
    for (guint i = 0; i < n; i++) {
        GwyXY *pt = screenxy + i;
        gdouble xy[OBJECT_SIZE];
        gwy_selection_get_object(selection, i, xy);
        gwy_data_view_coords_real_to_widget(dataview, xy[0], xy[1], &pt->x, &pt->y);
    }

    /* If there is just a single point draw it as a cross. */
    if (n == 1) {
        gwy_cairo_cross(cr, screenxy[0].x, screenxy[1].y, 0.5*CROSS_SIZE);
        g_free(screenxy);
        return;
    }

    /* Construct a segmented approximation of the spline. */
    GwyLayerPathPrivate *priv = GWY_LAYER_PATH(layer)->priv;
    GwySpline *spline = priv->spline;
    gwy_spline_set_points(spline, screenxy, n);

    gboolean closed = gwy_selection_path_get_closed(GWY_SELECTION_PATH(selection));
    /* Do not draw the back line for closed two-point spline which kind of silly. */
    gwy_spline_set_closed(spline, closed && n > 2);
    gwy_spline_set_slackness(spline, gwy_selection_path_get_slackness(GWY_SELECTION_PATH(selection)));

    guint nseg;
    const GwyXY *segmentxy = gwy_spline_sample_naturally(spline, &nseg);
    cairo_move_to(cr, segmentxy[0].x, segmentxy[0].y);
    for (guint i = 1; i < nseg; i++)
        cairo_line_to(cr, segmentxy[i].x, segmentxy[i].y);

    gdouble thickness = priv->thickness;
    if (thickness > 1.0) {
        gdouble qx = gwy_data_view_get_xzoom(dataview), qy = gwy_data_view_get_yzoom(dataview);
        const GwyXY *tangents = gwy_spline_get_tangents(spline);

        for (guint i = 0; i < n; i++) {
            const GwyXY *t = tangents + i;

            if (t->x == 0.0 && t->y == 0.0)
                continue;

            gdouble tx = 0.5*thickness*t->y * qx;
            gdouble ty = -0.5*thickness*t->x * qy;
            gwy_cairo_line(cr, screenxy[i].x - tx, screenxy[i].y - ty, screenxy[i].x + tx, screenxy[i].y + ty);
        }
    }

    cairo_stroke(cr);

    g_free(screenxy);
}

static void
set_cursor_according_to_proximity(GwyVectorLayer *layer, gdouble xreal, gdouble yreal)
{
    gint ipt = near_point(layer, xreal, yreal);
    const gchar *name = (ipt >= 0 ? "move" : NULL);
    gwy_data_view_set_named_cursor(GWY_DATA_VIEW(gwy_data_view_layer_get_parent(GWY_DATA_VIEW_LAYER(layer))), name);
}

static void
pointer_moved(GwyVectorLayer *layer, gdouble x, gdouble y)
{
    if (!gwy_vector_layer_get_editable(layer))
        return;

    GwySelection *selection = gwy_vector_layer_get_selection(layer);
    if (!selection)
        return;

    GtkWidget *parent = gwy_data_view_layer_get_parent(GWY_DATA_VIEW_LAYER(layer));
    g_return_if_fail(GWY_IS_DATA_VIEW(parent));
    GwyDataView *dataview = GWY_DATA_VIEW(parent);

    gwy_data_view_coords_widget_clamp(dataview, &x, &y);
    gdouble xreal, yreal, xy[OBJECT_SIZE];
    gwy_data_view_coords_widget_to_real(dataview, x, y, &xreal, &yreal);

    if (!gwy_vector_layer_get_current_button(layer)) {
        set_cursor_according_to_proximity(layer, xreal, yreal);
        return;
    }

    gint iobj = gwy_vector_layer_get_current_object(layer);
    if (iobj < 0)
        return;

    gwy_selection_get_object(selection, iobj, xy);
    if (xreal == xy[0] && yreal == xy[1])
        return;

    xy[0] = xreal;
    xy[1] = yreal;
    gwy_vector_layer_update_object(layer, iobj, xy);
}

static void
button_pressed(GwyVectorLayer *layer, gdouble x, gdouble y)
{
    gint button = gwy_vector_layer_get_current_button(layer);
    if (button != 1)
        return;

    GtkWidget *parent = gwy_data_view_layer_get_parent(GWY_DATA_VIEW_LAYER(layer));
    g_return_if_fail(GWY_IS_DATA_VIEW(parent));
    GwyDataView *dataview = GWY_DATA_VIEW(parent);

    /* Do nothing when we are outside. */
    if (!gwy_data_view_coords_widget_clamp(dataview, &x, &y))
        return;

    gdouble xreal, yreal;
    gwy_data_view_coords_widget_to_real(dataview, x, y, &xreal, &yreal);

    gint iobj = near_point(layer, xreal, yreal);
    if (just_choose_object(layer, iobj))
        return;

    gdouble xy[OBJECT_SIZE] = { xreal, yreal };
    iobj = gwy_vector_layer_update_object(layer, iobj, xy);
    gwy_vector_layer_set_current_object(layer, iobj);
    gwy_data_view_set_named_cursor(dataview, "move");
}

static void
button_released(GwyVectorLayer *layer, gdouble x, gdouble y)
{
    gint button = gwy_vector_layer_get_current_button(layer);
    gint iobj = gwy_vector_layer_get_current_object(layer);
    if (button != 1 || iobj < 0)
        return;

    GwySelection *selection = gwy_vector_layer_get_selection(layer);
    GtkWidget *parent = gwy_data_view_layer_get_parent(GWY_DATA_VIEW_LAYER(layer));
    g_return_if_fail(GWY_IS_DATA_VIEW(parent));
    GwyDataView *dataview = GWY_DATA_VIEW(parent);

    gdouble xreal, yreal;
    gwy_data_view_coords_widget_to_real(dataview, x, y, &xreal, &yreal);
    gdouble xy[OBJECT_SIZE] = { xreal, yreal };
    gwy_vector_layer_update_object(layer, iobj, xy);
    gwy_vector_layer_set_current_object(layer, -1);
    set_cursor_according_to_proximity(layer, xreal, yreal);
    gwy_selection_finished(selection);
}

/* TODO: Later */
#if 0
static gboolean
key_pressed(GwyVectorLayer *layer, GdkEventKey *event)
{
    gboolean large_step = (event->state & (GDK_CONTROL_MASK | GDK_MOD1_MASK));
    gint which = ((event->state & GDK_SHIFT_MASK) ? 2 : 0);
    GwyDataView *dataview;
    guint keyval = event->keyval;
    gint chosen = priv->chosen, xcurr, ycurr, xnew, ynew, move_distance;
    gdouble xy[4];

    if (chosen < 0
        || chosen >= gwy_selection_get_n_objects(priv->selection))
        return FALSE;

    if (keyval != GDK_KEY_Left && keyval != GDK_KEY_Right
        && keyval != GDK_KEY_Up && keyval != GDK_KEY_Down)
        return FALSE;

    dataview = GWY_DATA_VIEW(GWY_DATA_VIEW_LAYER(layer)->parent);
    g_return_val_if_fail(dataview, FALSE);

    gwy_selection_get_object(priv->selection, chosen, xy);
    gwy_data_view_coords_real_to_widget(dataview, xy[which], xy[which+1], &xcurr, &ycurr);
    xnew = xcurr;
    ynew = ycurr;
    move_distance = (large_step ? 16 : 1);
    if (keyval == GDK_KEY_Left)
        xnew -= move_distance;
    else if (keyval == GDK_KEY_Right)
        xnew += move_distance;
    else if (keyval == GDK_KEY_Up)
        ynew -= move_distance;
    else if (keyval == GDK_KEY_Down)
        ynew += move_distance;
    gwy_data_view_coords_widget_clamp(dataview, &xnew, &ynew);

    if (xnew != xcurr || ynew != ycurr) {
        gwy_data_view_coords_widget_to_real(dataview, xnew, ynew, xy+which, xy+which+1);
        gwy_selection_set_object(priv->selection, chosen, xy);
    }

    return TRUE;
}
#endif

/**
 * gwy_layer_path_new:
 *
 * Creates a new path vector layer.
 *
 * Returns: A newly created path vector layer.
 **/
GwyVectorLayer*
gwy_layer_path_new(void)
{
    return (GwyVectorLayer*)g_object_new(GWY_TYPE_LAYER_PATH, NULL);
}

/**
 * gwy_layer_path_set_thickness:
 * @layer: A path vector layer.
 * @thickness: Marker size, denoting path thickness.
 *
 * Sets the size of markers denoting path thickness.
 **/
void
gwy_layer_path_set_thickness(GwyLayerPath *layer, gdouble thickness)
{
    g_return_if_fail(GWY_IS_LAYER_PATH(layer));
    g_return_if_fail(thickness >= 0.0);

    GwyLayerPathPrivate *priv = layer->priv;
    if (thickness == priv->thickness)
        return;

    priv->thickness = thickness;
    g_object_notify_by_pspec(G_OBJECT(layer), properties[PROP_THICKNESS]);
    gwy_data_view_layer_updated(GWY_DATA_VIEW_LAYER(layer));
}

/**
 * gwy_layer_path_get_thickness:
 * @layer: A path vector layer.
 *
 * Gets the size of markers denoting path thickness.
 *
 * Returns: Path thickness.
 **/
gdouble
gwy_layer_path_get_thickness(GwyLayerPath *layer)
{
    g_return_val_if_fail(GWY_IS_LAYER_PATH(layer), 0.0);
    return layer->priv->thickness;
}

static gint
near_point(GwyVectorLayer *layer, gdouble xreal, gdouble yreal)
{
    gdouble d2min;
    gint i = gwy_vector_layer_find_nearest(layer, gwy_math_find_nearest_point, 1, xreal, yreal, &d2min);
    return (d2min > PROXIMITY_DISTANCE*PROXIMITY_DISTANCE) ? -1 : i;
}

/**
 * SECTION:layer-path
 * @title: GwyLayerPath
 * @short_description: Data view layer for path selections
 *
 * #GwyLayerPath allows selection a path using a sequence of points. It uses #GwySelectionPath selection type.
 **/

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