/*
 *  $Id: layer-point.c 30035 2026-06-10 14:50:37Z yeti-dn $
 *  Copyright (C) 2024-2026 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-point.h"

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

enum {
    OBJECT_SIZE = 2
};

enum {
    PROP_0,
    PROP_DRAW_MARKER,
    PROP_MARKER_RADIUS,
    PROP_DRAW_AS_VECTOR,
    PROP_NUMBERED,
    NUM_PROPERTIES,
};

struct _GwyLayerPointPrivate {
    /* Properties */
    gboolean draw_marker;
    gboolean draw_as_vector;
    gboolean numbered;
    gdouble marker_radius;
};

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 draw_or_invalidate(GwyVectorLayer *layer,
                               cairo_t *cr,
                               GwyDataView *dataview,
                               cairo_region_t *region,
                               const gdouble *xy,
                               guint i);
static void invalidate_object (GwyVectorLayer *layer,
                               cairo_region_t *region,
                               const gdouble *xy);
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(GwyLayerPoint, gwy_layer_point, GWY_TYPE_VECTOR_LAYER,
                        G_ADD_PRIVATE(GwyLayerPoint))

static void
gwy_layer_point_class_init(GwyLayerPointClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
    GwyVectorLayerClass *vector_class = GWY_VECTOR_LAYER_CLASS(klass);

    parent_class = gwy_layer_point_parent_class;

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

    vector_class->selection_type = GWY_TYPE_SELECTION_POINT;
    vector_class->draw = draw;
    vector_class->invalidate_object = invalidate_object;
    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_DRAW_MARKER] = g_param_spec_boolean("draw-marker", NULL,
                                                        "Whether to draw point markers",
                                                        TRUE,
                                                        GWY_GPARAM_RWE);
    properties[PROP_MARKER_RADIUS] = g_param_spec_double("marker-radius", NULL,
                                                         "Radius of marker if it is drawn",
                                                         0.0, 1024.0, 0.0,
                                                         GWY_GPARAM_RWE);
    properties[PROP_DRAW_AS_VECTOR] = g_param_spec_boolean("draw-as-vector", NULL,
                                                           "Whether to draw makers as lines from the origin",
                                                           FALSE,
                                                           GWY_GPARAM_RWE);
    properties[PROP_NUMBERED] = g_param_spec_boolean("numbered", NULL,
                                                     "Whether to show point numbers.",
                                                     FALSE,
                                                     GWY_GPARAM_RWE);

    g_object_class_install_properties(gobject_class, NUM_PROPERTIES, properties);
}

static void
gwy_layer_point_init(GwyLayerPoint *layer)
{
    GwyLayerPointPrivate *priv;

    layer->priv = priv = gwy_layer_point_get_instance_private(layer);
    priv->draw_marker = TRUE;
}

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

    switch (prop_id) {
        case PROP_DRAW_MARKER:
        gwy_layer_point_set_draw_marker(layer, g_value_get_boolean(value));
        break;

        case PROP_MARKER_RADIUS:
        gwy_layer_point_set_marker_radius(layer, g_value_get_double(value));
        break;

        case PROP_DRAW_AS_VECTOR:
        gwy_layer_point_set_draw_as_vector(layer, g_value_get_boolean(value));
        break;

        case PROP_NUMBERED:
        gwy_layer_point_set_numbered(layer, g_value_get_boolean(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)
{
    GwyLayerPointPrivate *priv = GWY_LAYER_POINT(object)->priv;

    switch (prop_id) {
        case PROP_DRAW_MARKER:
        g_value_set_boolean(value, priv->draw_marker);
        break;

        case PROP_MARKER_RADIUS:
        g_value_set_double(value, priv->marker_radius);
        break;

        case PROP_DRAW_AS_VECTOR:
        g_value_set_boolean(value, priv->draw_as_vector);
        break;

        case PROP_NUMBERED:
        g_value_set_boolean(value, priv->numbered);
        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);

    if (!GWY_LAYER_POINT(layer)->priv->draw_marker)
        return;

    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);

    gint n = gwy_selection_get_n_objects(selection);
    for (gint i = 0; i < n; i++) {
        gdouble xy[OBJECT_SIZE];
        gwy_selection_get_object(selection, i, xy);
        draw_or_invalidate(layer, cr, dataview, NULL, xy, i);
    }

    cairo_stroke(cr);
}

static void
draw_or_invalidate(GwyVectorLayer *layer,
                   cairo_t *cr, GwyDataView *dataview,
                   cairo_region_t *region,
                   const gdouble *xy, guint i)
{
    GwyLayerPointPrivate *priv = GWY_LAYER_POINT(layer)->priv;
    g_assert(cr || region);

    gdouble x, y;
    if (priv->draw_as_vector) {
        gdouble xc, yc;
        gwy_data_view_get_real_data_sizes(dataview, &xc, &yc);
        xc *= 0.5;
        yc *= 0.5;
        gdouble xl0, yl0;
        _gwy_transform_line_to_target(dataview, xc, yc, xy[0], xy[1], &xl0, &yl0, &x, &y);

        gwy_cairo_draw_or_add_thin_line(cr, region, xl0, yl0, x, y);
    }
    else {
        gwy_data_view_coords_real_to_widget(dataview, xy[0], xy[1], &x, &y);
        if (cr)
            gwy_cairo_cross(cr, x, y, 0.5*CROSS_SIZE);
        if (region)
            gwy_cairo_region_add_crectangle(region, x, y, 0.5*CROSS_SIZE, 0.5*CROSS_SIZE, 1.0);
    }

    if (priv->draw_marker && priv->marker_radius > 0.0) {
        /* The radius is in image pixels, not screen pixels. Transform through real sizes. */
        gdouble xscale, yscale, r = priv->marker_radius;
        gwy_data_view_get_real_pixel_sizes(dataview, &xscale, &yscale);
        xscale /= gwy_data_view_get_xmeasure(dataview);
        yscale /= gwy_data_view_get_ymeasure(dataview);

        if (cr)
            gwy_cairo_ellipse(cr, x, y, r*xscale, r*yscale);
        if (region)
            gwy_cairo_region_add_ellipse(region, x, y, r*xscale, r*yscale, 1.0);
    }

    if (priv->numbered) {
        PangoLayout *layout = gwy_vector_layer_ensure_layout(layer);
        g_return_if_fail(layout);

        gchar buf[16];
        g_snprintf(buf, sizeof(buf), "%u", i+1);
        pango_layout_set_text(layout, buf, -1);

        PangoRectangle rect;
        pango_layout_get_pixel_extents(layout, &rect, NULL);
        // FIXME: This is simplistic. When also drawing a marker ellipse, the label should go outside the ellipse when
        // it does not fit inside.
        gwy_cairo_draw_or_add_layout(cr, region, layout, x + rect.x, y + rect.y);
    }
}

static void
invalidate_object(GwyVectorLayer *layer,
                  cairo_region_t *region,
                  const gdouble *xy)
{
    GtkWidget *parent = gwy_data_view_layer_get_parent(GWY_DATA_VIEW_LAYER(layer));
    g_return_if_fail(GWY_IS_DATA_VIEW(parent));
    draw_or_invalidate(layer, NULL, GWY_DATA_VIEW(parent), region, xy, 9999);
}

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);

    GwyLayerPointPrivate *priv = GWY_LAYER_POINT(layer)->priv;
    gwy_data_view_set_named_cursor(dataview, priv->draw_marker ? "move" : "crosshair");
}

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_point_new:
 *
 * Creates a new point vector layer.
 *
 * Returns: A newly created point vector layer.
 **/
GwyVectorLayer*
gwy_layer_point_new(void)
{
    return (GwyVectorLayer*)g_object_new(GWY_TYPE_LAYER_POINT, NULL);
}

/**
 * gwy_layer_point_set_draw_marker:
 * @layer: A point vector layer.
 * @draw_marker: %TRUE to draw visible markes for the points; %FALSE to not draw any markers.
 *
 * Sets whether a point vector layer is drawn with visible markers.
 *
 * The marker size can be set using gwy_layer_point_set_marker_radius().
 **/
void
gwy_layer_point_set_draw_marker(GwyLayerPoint *layer,
                                gboolean draw_marker)
{
    g_return_if_fail(GWY_IS_LAYER_POINT(layer));

    GwyLayerPointPrivate *priv = layer->priv;
    if (!draw_marker == !priv->draw_marker)
        return;

    priv->draw_marker = !!draw_marker;
    g_object_notify_by_pspec(G_OBJECT(layer), properties[PROP_DRAW_MARKER]);
    gwy_data_view_layer_updated(GWY_DATA_VIEW_LAYER(layer));
}

/**
 * gwy_layer_point_get_draw_marker:
 * @layer: A point vector layer.
 *
 * Reports whether a point vector layer is drawn with visible markers.
 *
 * Returns: %TRUE if points are drawn with visible markers; %FALSE for invisible points.
 **/
gboolean
gwy_layer_point_get_draw_marker(GwyLayerPoint *layer)
{
    g_return_val_if_fail(GWY_IS_LAYER_POINT(layer), FALSE);
    return layer->priv->draw_marker;
}

/**
 * gwy_layer_point_set_draw_as_vector:
 * @layer: A point vector layer.
 * @draw_as_vector: %TRUE to draw points as lines from image centre; %FALSE for normal points.
 *
 * Sets whether a point vector layer is drawn as vectors from image centre.
 **/
void
gwy_layer_point_set_draw_as_vector(GwyLayerPoint *layer,
                                   gboolean draw_as_vector)
{
    g_return_if_fail(GWY_IS_LAYER_POINT(layer));

    GwyLayerPointPrivate *priv = layer->priv;
    if (!draw_as_vector == !priv->draw_as_vector)
        return;

    priv->draw_as_vector = !!draw_as_vector;
    g_object_notify_by_pspec(G_OBJECT(layer), properties[PROP_DRAW_AS_VECTOR]);
    gwy_data_view_layer_updated(GWY_DATA_VIEW_LAYER(layer));
}

/**
 * gwy_layer_point_get_draw_as_vector:
 * @layer: A point vector layer.
 *
 * Reports whether a point vector layer is drawn as vectors from image centre.
 *
 * Returns: %TRUE if points are drawn as vectors from the image centre; %FALSE for normal points.
 **/
gboolean
gwy_layer_point_get_draw_as_vector(GwyLayerPoint *layer)
{
    g_return_val_if_fail(GWY_IS_LAYER_POINT(layer), FALSE);
    return layer->priv->draw_as_vector;
}

/**
 * gwy_layer_point_set_numbered:
 * @layer: A point vector layer.
 * @numbered: %TRUE to show point numbers; %FALSE to not show point numbers.
 *
 * Sets whether a point vector layer has visibly numbered points.
 **/
void
gwy_layer_point_set_numbered(GwyLayerPoint *layer, gboolean numbered)
{
    g_return_if_fail(GWY_IS_LAYER_POINT(layer));

    GwyLayerPointPrivate *priv = layer->priv;
    if (!numbered == !priv->numbered)
        return;

    priv->numbered = !!numbered;
    g_object_notify_by_pspec(G_OBJECT(layer), properties[PROP_NUMBERED]);
    gwy_data_view_layer_updated(GWY_DATA_VIEW_LAYER(layer));
}

/**
 * gwy_layer_point_get_numbered:
 * @layer: A point vector layer.
 *
 * Reports whether a point vector layer has visibly numbered points.
 *
 * Returns: %TRUE if point numbers are shown; %FALSE if point numbers are not shown.
 **/
gboolean
gwy_layer_point_get_numbered(GwyLayerPoint *layer)
{
    g_return_val_if_fail(GWY_IS_LAYER_POINT(layer), FALSE);
    return layer->priv->numbered;
}

/**
 * gwy_layer_point_get_marker_radius:
 * @layer: A point vector layer.
 *
 * Gets the marker radius of a point vector layer.
 *
 * Returns: The radius of the circular marker.
 **/
gdouble
gwy_layer_point_get_marker_radius(GwyLayerPoint *layer)
{
    g_return_val_if_fail(GWY_IS_LAYER_POINT(layer), 0.0);
    return layer->priv->marker_radius;
}

/**
 * gwy_layer_point_set_marker_radius:
 * @layer: A point vector layer.
 * @marker_radius: Radius of cicular marker around the points.
 *
 * Sets the radius of the marker around points of a point vector layer.
 *
 * The marker also needs to be enabled using gwy_layer_point_set_draw_marker() to be visible.
 **/
void
gwy_layer_point_set_marker_radius(GwyLayerPoint *layer,
                                  gdouble marker_radius)
{
    g_return_if_fail(GWY_IS_LAYER_POINT(layer));
    g_return_if_fail(marker_radius >= 0.0 && marker_radius <= 1024.0);

    GwyLayerPointPrivate *priv = layer->priv;
    if (marker_radius == priv->marker_radius)
        return;

    priv->marker_radius = marker_radius;
    g_object_notify_by_pspec(G_OBJECT(layer), properties[PROP_MARKER_RADIUS]);
    if (priv->draw_marker)
        gwy_data_view_layer_updated(GWY_DATA_VIEW_LAYER(layer));
}

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-point
 * @title: GwyLayerPoint
 * @short_description: Data view layer for point-wise selections
 *
 * #GwyLayerPoint allows selection of individual points. It uses #GwySelectionPoint 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 : */
