/* PIKA - Photo and Image Kooker Application
 * a rebranding of The GNU Image Manipulation Program (created with heckimp)
 * A derived work which may be trivial. However, any changes may be (C)2023 by Aldercone Studio
 *
 * Original copyright, applying to most contents (license remains unchanged): 
 * Copyright (C) 2009 Martin Nordholts <martinn@src.gnome.org>
 *
 * 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 3 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, see <https://www.gnu.org/licenses/>.
 */

#include <stdlib.h>
#include <string.h>

#include <gegl.h>
#include <gtk/gtk.h>
#include <gdk/gdkkeysyms.h>

#include "libpikabase/pikabase.h"
#include "libpikamath/pikamath.h"
#include "libpikawidgets/pikawidgets.h"

#include "tools/tools-types.h"

#include "tools/pikarectangleoptions.h"
#include "tools/tool_manager.h"

#include "display/pikadisplay.h"
#include "display/pikadisplayshell.h"
#include "display/pikadisplayshell-callbacks.h"
#include "display/pikadisplayshell-scale.h"
#include "display/pikadisplayshell-tool-events.h"
#include "display/pikadisplayshell-transform.h"
#include "display/pikaimagewindow.h"

#include "widgets/pikadialogfactory.h"
#include "widgets/pikadock.h"
#include "widgets/pikadockable.h"
#include "widgets/pikadockbook.h"
#include "widgets/pikadocked.h"
#include "widgets/pikadockwindow.h"
#include "widgets/pikahelp-ids.h"
#include "widgets/pikasessioninfo.h"
#include "widgets/pikatoolbox.h"
#include "widgets/pikatooloptionseditor.h"
#include "widgets/pikauimanager.h"
#include "widgets/pikawidgets-utils.h"

#include "core/pika.h"
#include "core/pikachannel.h"
#include "core/pikacontext.h"
#include "core/pikaimage.h"
#include "core/pikalayer.h"
#include "core/pikatoolinfo.h"
#include "core/pikatooloptions.h"

#include "pikacoreapp.h"

#include "pika-app-test-utils.h"
#include "tests.h"


#define PIKA_TEST_IMAGE_WIDTH            150
#define PIKA_TEST_IMAGE_HEIGHT           267

/* Put this in the code below when you want the test to pause so you
 * can do measurements of widgets on the screen for example
 */
#define PIKA_PAUSE (g_usleep (2 * 1000 * 1000))

#define ADD_TEST(function) \
  g_test_add ("/pika-tools/" #function, \
              PikaTestFixture, \
              pika, \
              pika_tools_setup_image, \
              function, \
              pika_tools_teardown_image);


typedef struct
{
  int avoid_sizeof_zero;
} PikaTestFixture;


static void               pika_tools_setup_image                         (PikaTestFixture  *fixture,
                                                                          gconstpointer     data);
static void               pika_tools_teardown_image                      (PikaTestFixture  *fixture,
                                                                          gconstpointer     data);
static void               pika_tools_synthesize_image_click_drag_release (PikaDisplayShell *shell,
                                                                          gdouble           start_image_x,
                                                                          gdouble           start_image_y,
                                                                          gdouble           end_image_x,
                                                                          gdouble           end_image_y,
                                                                          gint              button,
                                                                          GdkModifierType   modifiers);
static PikaDisplay      * pika_test_get_only_display                     (Pika             *pika);
static PikaImage        * pika_test_get_only_image                       (Pika             *pika);
static PikaDisplayShell * pika_test_get_only_display_shell               (Pika             *pika);


static void
pika_tools_setup_image (PikaTestFixture *fixture,
                        gconstpointer    data)
{
  Pika *pika = PIKA (data);

  pika_test_utils_create_image (pika,
                                PIKA_TEST_IMAGE_WIDTH,
                                PIKA_TEST_IMAGE_HEIGHT);
  pika_test_run_mainloop_until_idle ();
}

static void
pika_tools_teardown_image (PikaTestFixture *fixture,
                           gconstpointer    data)
{
  Pika *pika = PIKA (data);

  g_object_unref (pika_test_get_only_image (pika));
  pika_display_close (pika_test_get_only_display (pika));
  pika_test_run_mainloop_until_idle ();
}

/**
 * pika_tools_set_tool:
 * @pika:
 * @tool_id:
 * @display:
 *
 * Makes sure the given tool is the active tool and that the passed
 * display is the focused tool display.
 **/
static void
pika_tools_set_tool (Pika        *pika,
                     const gchar *tool_id,
                     PikaDisplay *display)
{
  /* Activate tool and setup active display for the new tool */
  pika_context_set_tool (pika_get_user_context (pika),
                         pika_get_tool_info (pika, tool_id));
  tool_manager_focus_display_active (pika, display);
}

/**
 * pika_test_get_only_display:
 * @pika:
 *
 * Asserts that there only is one image and display and then
 * returns the display.
 *
 * Returns: The #PikaDisplay.
 **/
static PikaDisplay *
pika_test_get_only_display (Pika *pika)
{
  g_assert (g_list_length (pika_get_image_iter (pika)) == 1);
  g_assert (g_list_length (pika_get_display_iter (pika)) == 1);

  return PIKA_DISPLAY (pika_get_display_iter (pika)->data);
}

/**
 * pika_test_get_only_display_shell:
 * @pika:
 *
 * Asserts that there only is one image and display shell and then
 * returns the display shell.
 *
 * Returns: The #PikaDisplayShell.
 **/
static PikaDisplayShell *
pika_test_get_only_display_shell (Pika *pika)
{
  return pika_display_get_shell (pika_test_get_only_display (pika));
}

/**
 * pika_test_get_only_image:
 * @pika:
 *
 * Asserts that there is only one image and returns that.
 *
 * Returns: The #PikaImage.
 **/
static PikaImage *
pika_test_get_only_image (Pika *pika)
{
  g_assert (g_list_length (pika_get_image_iter (pika)) == 1);
  g_assert (g_list_length (pika_get_display_iter (pika)) == 1);

  return PIKA_IMAGE (pika_get_image_iter (pika)->data);
}

static void
pika_test_synthesize_tool_button_event (PikaDisplayShell *shell,
                                       gint              x,
                                       gint              y,
                                       gint              button,
                                       gint              modifiers,
                                       GdkEventType      button_event_type)
{
  GdkEvent   *event   = gdk_event_new (button_event_type);
  GdkWindow  *window  = gtk_widget_get_window (GTK_WIDGET (shell->canvas));
  GdkDisplay *display = gdk_window_get_display (window);
  GdkSeat    *seat    = gdk_display_get_default_seat (display);

  g_assert (button_event_type == GDK_BUTTON_PRESS ||
            button_event_type == GDK_BUTTON_RELEASE);

  gdk_event_set_device (event, gdk_seat_get_pointer (seat));

  event->button.window     = g_object_ref (window);
  event->button.send_event = TRUE;
  event->button.time       = gtk_get_current_event_time ();
  event->button.x          = x;
  event->button.y          = y;
  event->button.axes       = NULL;
  event->button.state      = 0;
  event->button.button     = button;
  event->button.x_root     = -1;
  event->button.y_root     = -1;

  pika_display_shell_canvas_tool_events (shell->canvas,
                                         event,
                                         shell);
  gdk_event_free (event);
}

static void
pika_test_synthesize_tool_motion_event (PikaDisplayShell *shell,
                                        gint              x,
                                        gint              y,
                                        gint              modifiers)
{
  GdkEvent   *event   = gdk_event_new (GDK_MOTION_NOTIFY);
  GdkWindow  *window  = gtk_widget_get_window (GTK_WIDGET (shell->canvas));
  GdkDisplay *display = gdk_window_get_display (window);
  GdkSeat    *seat    = gdk_display_get_default_seat (display);

  gdk_event_set_device (event, gdk_seat_get_pointer (seat));

  event->motion.window     = g_object_ref (window);
  event->motion.send_event = TRUE;
  event->motion.time       = gtk_get_current_event_time ();
  event->motion.x          = x;
  event->motion.y          = y;
  event->motion.axes       = NULL;
  event->motion.state      = GDK_BUTTON1_MASK | modifiers;
  event->motion.is_hint    = FALSE;
  event->motion.x_root     = -1;
  event->motion.y_root     = -1;

  pika_display_shell_canvas_tool_events (shell->canvas,
                                         event,
                                         shell);
  gdk_event_free (event);
}

static void
pika_test_synthesize_tool_crossing_event (PikaDisplayShell *shell,
                                          gint              x,
                                          gint              y,
                                          gint              modifiers,
                                          GdkEventType      crossing_event_type)
{
  GdkEvent   *event   = gdk_event_new (crossing_event_type);
  GdkWindow  *window  = gtk_widget_get_window (GTK_WIDGET (shell->canvas));
  GdkDisplay *display = gdk_window_get_display (window);
  GdkSeat    *seat    = gdk_display_get_default_seat (display);

  g_assert (crossing_event_type == GDK_ENTER_NOTIFY ||
            crossing_event_type == GDK_LEAVE_NOTIFY);

  gdk_event_set_device (event, gdk_seat_get_pointer (seat));

  event->crossing.window     = g_object_ref (window);
  event->crossing.send_event = TRUE;
  event->crossing.subwindow  = NULL;
  event->crossing.time       = gtk_get_current_event_time ();
  event->crossing.x          = x;
  event->crossing.y          = y;
  event->crossing.x_root     = -1;
  event->crossing.y_root     = -1;
  event->crossing.mode       = GDK_CROSSING_NORMAL;
  event->crossing.detail     = GDK_NOTIFY_UNKNOWN;
  event->crossing.focus      = TRUE;
  event->crossing.state      = modifiers;

  pika_display_shell_canvas_tool_events (shell->canvas,
                                         event,
                                         shell);
  gdk_event_free (event);
}

static void
pika_tools_synthesize_image_click_drag_release (PikaDisplayShell *shell,
                                                gdouble           start_image_x,
                                                gdouble           start_image_y,
                                                gdouble           end_image_x,
                                                gdouble           end_image_y,
                                                gint              button /*1..3*/,
                                                GdkModifierType   modifiers)
{
  gdouble start_canvas_x  = -1.0;
  gdouble start_canvas_y  = -1.0;
  gdouble middle_canvas_x = -1.0;
  gdouble middle_canvas_y = -1.0;
  gdouble end_canvas_x    = -1.0;
  gdouble end_canvas_y    = -1.0;

  /* Transform coordinates */
  pika_display_shell_transform_xy_f (shell,
                                     start_image_x,
                                     start_image_y,
                                     &start_canvas_x,
                                     &start_canvas_y);
  pika_display_shell_transform_xy_f (shell,
                                     end_image_x,
                                     end_image_y,
                                     &end_canvas_x,
                                     &end_canvas_y);
  middle_canvas_x = (start_canvas_x + end_canvas_x) / 2;
  middle_canvas_y = (start_canvas_y + end_canvas_y) / 2;

  /* Enter notify */
  pika_test_synthesize_tool_crossing_event (shell,
                                            (int)start_canvas_x,
                                            (int)start_canvas_y,
                                            modifiers,
                                            GDK_ENTER_NOTIFY);

  /* Button press */
  pika_test_synthesize_tool_button_event (shell,
                                          (int)start_canvas_x,
                                          (int)start_canvas_y,
                                          button,
                                          modifiers,
                                          GDK_BUTTON_PRESS);

  /* Move events */
  pika_test_synthesize_tool_motion_event (shell,
                                          (int)start_canvas_x,
                                          (int)start_canvas_y,
                                          modifiers);
  pika_test_synthesize_tool_motion_event (shell,
                                          (int)middle_canvas_x,
                                          (int)middle_canvas_y,
                                          modifiers);
  pika_test_synthesize_tool_motion_event (shell,
                                          (int)end_canvas_x,
                                          (int)end_canvas_y,
                                          modifiers);

  /* Button release */
  pika_test_synthesize_tool_button_event (shell,
                                          (int)end_canvas_x,
                                          (int)end_canvas_y,
                                          button,
                                          modifiers,
                                          GDK_BUTTON_RELEASE);

  /* Leave notify */
  pika_test_synthesize_tool_crossing_event (shell,
                                            (int)start_canvas_x,
                                            (int)start_canvas_y,
                                            modifiers,
                                            GDK_LEAVE_NOTIFY);

  /* Process them */
  pika_test_run_mainloop_until_idle ();
}

/**
 * crop_tool_can_crop:
 * @fixture:
 * @data:
 *
 * Make sure it's possible to crop at all. Regression test for
 * "Bug 315255 - SIGSEGV, while doing a crop".
 **/
static void
crop_tool_can_crop (PikaTestFixture *fixture,
                    gconstpointer    data)
{
  Pika             *pika  = PIKA (data);
  PikaImage        *image = pika_test_get_only_image (pika);
  PikaDisplayShell *shell = pika_test_get_only_display_shell (pika);

  gint cropped_x = 10;
  gint cropped_y = 10;
  gint cropped_w = 20;
  gint cropped_h = 30;

  /* Fit display and pause and let it stabalize (two idlings seems to
   * always be enough)
   */
  pika_ui_manager_activate_action (pika_test_utils_get_ui_manager (pika),
                                   "view",
                                   "view-shrink-wrap");
  pika_test_run_mainloop_until_idle ();
  pika_test_run_mainloop_until_idle ();

  /* Activate crop tool */
  pika_tools_set_tool (pika, "pika-crop-tool", shell->display);

  /* Do the crop rect */
  pika_tools_synthesize_image_click_drag_release (shell,
                                                  cropped_x,
                                                  cropped_y,
                                                  cropped_x + cropped_w,
                                                  cropped_y + cropped_h,
                                                  1 /*button*/,
                                                  0 /*modifiers*/);

  /* Crop */
  pika_test_utils_synthesize_key_event (GTK_WIDGET (shell), GDK_KEY_Return);
  pika_test_run_mainloop_until_idle ();

  /* Make sure the new image has the expected size */
  g_assert_cmpint (cropped_w, ==, pika_image_get_width (image));
  g_assert_cmpint (cropped_h, ==, pika_image_get_height (image));
}

/**
 * crop_tool_can_crop:
 * @fixture:
 * @data:
 *
 * Make sure it's possible to change width of crop rect in tool
 * options without there being a pending rectangle. Regression test
 * for "Bug 322396 - Crop dimension entering causes crash".
 **/
static void
crop_set_width_without_pending_rect (PikaTestFixture *fixture,
                                     gconstpointer    data)
{
  Pika                 *pika    = PIKA (data);
  PikaDisplay          *display = pika_test_get_only_display (pika);
  PikaToolInfo         *tool_info;
  PikaRectangleOptions *rectangle_options;
  GtkWidget            *size_entry;

  /* Activate crop tool */
  pika_tools_set_tool (pika, "pika-crop-tool", display);

  /* Get tool options */
  tool_info         = pika_get_tool_info (pika, "pika-crop-tool");
  rectangle_options = PIKA_RECTANGLE_OPTIONS (tool_info->tool_options);

  /* Find 'Width' or 'Height' GtkTextEntry in tool options */
  size_entry = pika_rectangle_options_get_size_entry (rectangle_options);

  /* Set arbitrary non-0 value */
  pika_size_entry_set_value (PIKA_SIZE_ENTRY (size_entry),
                             0 /*field*/,
                             42.0 /*lower*/);

  /* If we don't crash, everything s fine */
}

int main(int argc, char **argv)
{
  Pika *pika   = NULL;
  gint  result = -1;

  pika_test_bail_if_no_display ();
  gtk_test_init (&argc, &argv, NULL);

  pika_test_utils_setup_menus_path ();

  /* Start up PIKA */
  pika = pika_init_for_gui_testing (TRUE /*show_gui*/);
  pika_test_run_mainloop_until_idle ();

  /* Add tests */
  ADD_TEST (crop_tool_can_crop);
  ADD_TEST (crop_set_width_without_pending_rect);

  /* Run the tests and return status */
  g_application_run (pika->app, 0, NULL);
  result = pika_core_app_get_exit_status (PIKA_CORE_APP (pika->app));

  g_application_quit (G_APPLICATION (pika->app));
  g_clear_object (&pika->app);

  return result;
}
