/*
 *  $Id: layer-axis.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 <math.h>
#include <gdk/gdkkeysyms.h>

#include "libgwyddion/macros.h"
#include "libgwyddion/math.h"
#include "libgwyddion/selection-axis.h"

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

enum {
    OBJECT_SIZE = 1
};

enum {
    PROP_0,
    PROP_THICKNESS,
    NUM_PROPERTIES,
};

struct _GwyLayerAxisPrivate {
    /* Properties */
    gdouble thickness;
};

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_horizontal(GwyVectorLayer *layer,
                                          cairo_t *cr,
                                          GwyDataView *dataview,
                                          cairo_region_t *region,
                                          gdouble x);
static void draw_or_invalidate_vertical  (GwyVectorLayer *layer,
                                          cairo_t *cr,
                                          GwyDataView *dataview,
                                          cairo_region_t *region,
                                          gdouble y);
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_vline                   (GwyVectorLayer *layer,
                                          gdouble xreal);
static gint near_hline                   (GwyVectorLayer *layer,
                                          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(GwyLayerAxis, gwy_layer_axis, GWY_TYPE_VECTOR_LAYER,
                        G_ADD_PRIVATE(GwyLayerAxis))

static void
gwy_layer_axis_class_init(GwyLayerAxisClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
    GwyVectorLayerClass *vector_class = GWY_VECTOR_LAYER_CLASS(klass);

    parent_class = gwy_layer_axis_parent_class;

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

    vector_class->selection_type = GWY_TYPE_SELECTION_AXIS;
    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_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_axis_init(GwyLayerAxis *layer)
{
    layer->priv = gwy_layer_axis_get_instance_private(layer);
}

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

    switch (prop_id) {
        case PROP_THICKNESS:
        gwy_layer_axis_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)
{
    GwyLayerAxisPrivate *priv = GWY_LAYER_AXIS(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));

    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);
    GwyOrientation orientation = gwy_selection_axis_get_orientation(GWY_SELECTION_AXIS(selection));
    for (gint i = 0; i < n; i++) {
        gdouble xy;
        gwy_selection_get_object(selection, i, &xy);
        if (orientation == GWY_ORIENTATION_VERTICAL)
            draw_or_invalidate_vertical(layer, cr, GWY_DATA_VIEW(parent), NULL, xy);
        else
            draw_or_invalidate_horizontal(layer, cr, GWY_DATA_VIEW(parent), NULL, xy);
    }

    cairo_stroke(cr);
}

static void
draw_or_invalidate_horizontal(GwyVectorLayer *layer,
                              cairo_t *cr, GwyDataView *dataview,
                              cairo_region_t *region,
                              gdouble x)
{
    GwyLayerAxisPrivate *priv = GWY_LAYER_AXIS(layer)->priv;
    gdouble xreal, yreal, ymin, ymax, xscr;
    gdouble thickness = priv->thickness;

    gwy_data_view_get_real_data_sizes(dataview, &xreal, &yreal);
    gwy_data_view_coords_real_to_widget(dataview, x, 0.0, &xscr, NULL);
    gwy_data_view_coords_real_to_widget(dataview, 0.0, 0.0, NULL, &ymin);
    gwy_data_view_coords_real_to_widget(dataview, xreal, yreal, NULL, &ymax);

    /* FIXME */
    xscr = floor(xscr) + 0.5;
    gwy_cairo_draw_or_add_thin_line(cr, region, xscr, ymin, xscr, ymax);

    if (!(thickness > 1.0))
        return;

    /* FIXME how to draw the end markers nicely when the image can kind of end anywhere for weird zooms? */
    ymin = GWY_ROUND(ymin) + 0.5;
    ymax = GWY_ROUND(ymax) - 0.5;

    gdouble tx = 0.5*thickness * gwy_data_view_get_xzoom(dataview);
    gwy_cairo_draw_or_add_thin_line(cr, region, xscr - tx, ymin, xscr + tx, ymin);
    gwy_cairo_draw_or_add_thin_line(cr, region, xscr - tx, ymax, xscr + tx, ymax);
}

static void
draw_or_invalidate_vertical(GwyVectorLayer *layer,
                            cairo_t *cr, GwyDataView *dataview,
                            cairo_region_t *region,
                            gdouble y)
{
    GwyLayerAxisPrivate *priv = GWY_LAYER_AXIS(layer)->priv;
    gdouble xreal, yreal, xmin, xmax, yscr;
    gdouble thickness = priv->thickness;

    gwy_data_view_get_real_data_sizes(dataview, &xreal, &yreal);
    gwy_data_view_coords_real_to_widget(dataview, 0.0, y, NULL, &yscr);
    gwy_data_view_coords_real_to_widget(dataview, 0.0, 0.0, &xmin, NULL);
    gwy_data_view_coords_real_to_widget(dataview, xreal, yreal, &xmax, NULL);

    /* FIXME */
    yscr = floor(yscr) + 0.5;
    gwy_cairo_draw_or_add_thin_line(cr, region, xmin, yscr, xmax, yscr);

    if (!(thickness > 1.0))
        return;

    /* FIXME how to draw the end markers nicely when the image can kind of end anywhere for weird zooms? */
    xmin = GWY_ROUND(xmin) + 0.5;
    xmax = GWY_ROUND(xmax) - 0.5;

    gdouble ty = 0.5*thickness * gwy_data_view_get_yzoom(dataview);
    gwy_cairo_draw_or_add_thin_line(cr, region, xmin, yscr - ty, xmin, yscr + ty);
    gwy_cairo_draw_or_add_thin_line(cr, region, xmax, yscr - ty, xmax, yscr + ty);
}

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));
    GwySelection *selection = gwy_vector_layer_get_selection(layer);
    GwyOrientation orientation = gwy_selection_axis_get_orientation(GWY_SELECTION_AXIS(selection));
    if (orientation == GWY_ORIENTATION_VERTICAL)
        draw_or_invalidate_vertical(layer, NULL, GWY_DATA_VIEW(parent), region, xy[0]);
    else
        draw_or_invalidate_horizontal(layer, NULL, GWY_DATA_VIEW(parent), region, xy[0]);
}

static void
set_cursor_according_to_proximity(GwyVectorLayer *layer, gdouble xreal, gdouble yreal)
{
    GwySelection *selection = gwy_vector_layer_get_selection(layer);
    GwyOrientation orientation = gwy_selection_axis_get_orientation(GWY_SELECTION_AXIS(selection));
    const gchar *name = NULL;

    if (orientation == GWY_ORIENTATION_VERTICAL) {
        if (near_hline(layer, yreal) >= 0)
            name = "row-resize";
    }
    else {
        if (near_vline(layer, xreal) >= 0)
            name = "col-resize";
    }
    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);
    GwyOrientation orientation = gwy_selection_axis_get_orientation(GWY_SELECTION_AXIS(selection));

    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 (orientation == GWY_ORIENTATION_VERTICAL) {
        if (xreal == xy[0])
            return;
    }
    else {
        if (yreal == xy[0])
            return;
    }

    xy[0] = (orientation == GWY_ORIENTATION_VERTICAL ? yreal : xreal);
    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;

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

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

    GwyOrientation orientation = gwy_selection_axis_get_orientation(GWY_SELECTION_AXIS(selection));
    gint iobj = (orientation == GWY_ORIENTATION_VERTICAL ? near_hline(layer, yreal) : near_vline(layer, xreal));
    if (just_choose_object(layer, iobj))
        return;

    gdouble xy[OBJECT_SIZE] = { (orientation == GWY_ORIENTATION_VERTICAL ? yreal : xreal) };
    iobj = gwy_vector_layer_update_object(layer, iobj, xy);
    gwy_vector_layer_set_current_object(layer, iobj);

    /* This sets the correct cursor also for movement. */
    set_cursor_according_to_proximity(layer, xreal, yreal);
}

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

    GwyOrientation orientation = gwy_selection_axis_get_orientation(GWY_SELECTION_AXIS(selection));
    gdouble xreal, yreal;
    gwy_data_view_coords_widget_to_real(dataview, x, y, &xreal, &yreal);
    gdouble xy[OBJECT_SIZE] = { (orientation == GWY_ORIENTATION_VERTICAL ? yreal : xreal) };
    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_axis_new:
 *
 * Creates a new axis vector layer.
 *
 * Returns: A newly created axis vector layer.
 **/
GwyVectorLayer*
gwy_layer_axis_new(void)
{
    return (GwyVectorLayer*)g_object_new(GWY_TYPE_LAYER_AXIS, NULL);
}

/**
 * gwy_layer_axis_set_thickness:
 * @layer: A axis vector layer.
 * @thickness: Marker size, denoting line thickness.
 *
 * Sets the size of markers denoting line thickness.
 **/
void
gwy_layer_axis_set_thickness(GwyLayerAxis *layer, gdouble thickness)
{
    g_return_if_fail(GWY_IS_LAYER_AXIS(layer));
    g_return_if_fail(thickness >= 0.0);

    GwyLayerAxisPrivate *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_axis_get_thickness:
 * @layer: A axis vector layer.
 *
 * Gets the size of markers denoting line thickness.
 *
 * Returns: Line thickness.
 **/
gdouble
gwy_layer_axis_get_thickness(GwyLayerAxis *layer)
{
    g_return_val_if_fail(GWY_IS_LAYER_AXIS(layer), 0.0);
    return layer->priv->thickness;
}

static int
near_vline(GwyVectorLayer *layer, gdouble xreal)
{
    gdouble d2min;
    gint i = gwy_vector_layer_find_nearest(layer, find_nearest_horizontally, 1, xreal, 0.0, &d2min);
    return (d2min > PROXIMITY_DISTANCE*PROXIMITY_DISTANCE) ? -1 : i;
}

static int
near_hline(GwyVectorLayer *layer, gdouble yreal)
{
    gdouble d2min;
    gint i = gwy_vector_layer_find_nearest(layer, find_nearest_vertically, 1, 0.0, yreal, &d2min);
    return (d2min > PROXIMITY_DISTANCE*PROXIMITY_DISTANCE) ? -1 : i;
}

/**
 * SECTION:layer-axis
 * @title: GwyLayerAxis
 * @short_description: Data view layer for selections along either axis
 * @see_also: #GwyLayerCross
 *
 * #GwyLayerAxis allows selection of lines oriented along either of the cardinal axes or both. It uses
 * #GwySelectionAxis 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 : */
