/*
 * Copyright (C) 2024 Red Hat Inc.
 *
 * 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, see <http://www.gnu.org/licenses/>.
 *
 */

#include "config.h"

#include "backends/meta-cursor-sprite-xcursor.h"
#include "backends/meta-logical-monitor.h"
#include "backends/meta-screen-cast.h"
#include "clutter/clutter.h"
#include "compositor/meta-window-actor-private.h"
#include "core/meta-fraction.h"
#include "core/util-private.h"
#include "meta/meta-wayland-compositor.h"
#include "tests/meta-test/meta-context-test.h"
#include "tests/meta-monitor-test-utils.h"
#include "tests/meta-ref-test.h"
#include "tests/meta-wayland-test-driver.h"
#include "tests/meta-wayland-test-utils.h"

#define CURSOR_SCALE_METHOD_BUFFER_SCALE "buffer-scale"
#define CURSOR_SCALE_METHOD_VIEWPORT "viewport"
#define CURSOR_SCALE_METHOD_VIEWPORT_CROPPED "viewport-cropped"

struct _MetaCrossOverlay
{
  GObject parent;
};

static MetaContext *test_context;
static MetaWaylandTestDriver *test_driver;

static void clutter_content_iface_init (ClutterContentInterface *iface);

#define META_TYPE_CROSS_OVERLAY (meta_cross_overlay_get_type ())
G_DECLARE_FINAL_TYPE (MetaCrossOverlay, meta_cross_overlay,
                      META, CROSS_OVERLAY, GObject)
G_DEFINE_TYPE_WITH_CODE (MetaCrossOverlay, meta_cross_overlay,
                         G_TYPE_OBJECT,
                         G_IMPLEMENT_INTERFACE (CLUTTER_TYPE_CONTENT,
                                                clutter_content_iface_init))

static void
meta_cross_overlay_paint_content (ClutterContent      *content,
                                  ClutterActor        *actor,
                                  ClutterPaintNode    *node,
                                  ClutterPaintContext *paint_context)
{
  g_autoptr (ClutterPaintNode) cross_node = NULL;
  ClutterActorBox allocation;
  ClutterActorBox horizontal;
  ClutterActorBox vertical;
  CoglColor color;

  clutter_actor_get_allocation_box (actor, &allocation);

  cogl_color_init_from_4f (&color, 0.0f, 0.0f, 0.0f, 1.0f);
  cross_node = clutter_color_node_new (&color);
  clutter_paint_node_add_child (node, cross_node);

  horizontal = (ClutterActorBox) {
    .x1 = allocation.x1,
    .y1 = (allocation.y2 - allocation.y1) / 2 - 0.5f,
    .x2 = allocation.x2,
    .y2 = (allocation.y2 - allocation.y1) / 2 + 0.5f,
  };
  vertical = (ClutterActorBox) {
    .x1 = (allocation.x2 - allocation.x1) / 2 - 0.5f,
    .y1 = allocation.y1,
    .x2 = (allocation.x2 - allocation.x1) / 2 + 0.5f,
    .y2 = allocation.y2,
  };

  clutter_paint_node_add_rectangle (cross_node, &horizontal);
  clutter_paint_node_add_rectangle (cross_node, &vertical);
}

static void
clutter_content_iface_init (ClutterContentInterface *iface)
{
  iface->paint_content = meta_cross_overlay_paint_content;
}

static void
meta_cross_overlay_class_init (MetaCrossOverlayClass *klass)
{
}

static void
meta_cross_overlay_init (MetaCrossOverlay *overlay)
{
}

static void
on_stage_size_changed (ClutterActor *stage,
                       GParamSpec   *pspec,
                       ClutterActor *overlay_actor)
{
  float width, height;

  clutter_actor_get_size (stage, &width, &height);
  clutter_actor_set_size (overlay_actor, width, height);
}

static ClutterActor *
create_overlay_actor (void)
{
  MetaBackend *backend = meta_context_get_backend (test_context);
  ClutterActor *stage = meta_backend_get_stage (backend);
  ClutterActor *actor;
  g_autoptr (ClutterContent) content = NULL;

  content = g_object_new (META_TYPE_CROSS_OVERLAY, NULL);
  actor = clutter_actor_new ();
  clutter_actor_set_content (actor, content);
  clutter_actor_set_name (actor, "cross-overlay");
  clutter_actor_show (actor);

  clutter_actor_add_child (stage, actor);
  g_signal_connect_object (stage, "notify::size",
                           G_CALLBACK (on_stage_size_changed),
                           actor,
                           G_CONNECT_DEFAULT);

  return actor;
}

static ClutterStageView *
setup_test_case (int                           width,
                 int                           height,
                 float                         scale,
                 MetaLogicalMonitorLayoutMode  layout_mode,
                 ClutterVirtualInputDevice    *virtual_pointer)
{
  MetaBackend *backend = meta_context_get_backend (test_context);
  MetaMonitorManager *monitor_manager =
    meta_backend_get_monitor_manager (backend);
  MetaMonitorManagerTest *monitor_manager_test =
    META_MONITOR_MANAGER_TEST (monitor_manager);
  MetaRenderer *renderer = meta_backend_get_renderer (backend);
  GList *logical_monitors;
  MetaLogicalMonitor *logical_monitor;
  MetaFraction scale_fraction;
  GList *views;
  g_autofree char *output_serial = NULL;
  MetaMonitorTestSetup *test_setup;
  MonitorTestCaseSetup test_case_setup = {
    .modes = {
      {
        .width = width,
        .height = height,
        .refresh_rate = 60.0
      },
    },
    .n_modes = 1,
    .outputs = {
      {
        .crtc = 0,
        .modes = { 0 },
        .n_modes = 1,
        .preferred_mode = 0,
        .possible_crtcs = { 0 },
        .n_possible_crtcs = 1,
        .width_mm = 150,
        .height_mm = 85,
        .scale = scale,
      },
    },
    .n_outputs = 1,
    .crtcs = {
      {
        .current_mode = -1
      },
    },
    .n_crtcs = 1
  };
  static int output_serial_counter = 0x12300000;

  /* Always generate unique serials to never trigger policy trying to inherit
   * the scale from previous configurations.
   */
  output_serial = g_strdup_printf ("0x%x", output_serial_counter++);
  test_case_setup.outputs[0].serial = output_serial;

  meta_monitor_manager_test_set_layout_mode (monitor_manager_test,
                                             layout_mode);
  test_setup = meta_create_monitor_test_setup (backend,
                                               &test_case_setup,
                                               MONITOR_TEST_FLAG_NO_STORED);
  meta_monitor_manager_test_emulate_hotplug (monitor_manager_test, test_setup);

  logical_monitors = meta_monitor_manager_get_logical_monitors (monitor_manager);
  g_assert_cmpuint (g_list_length (logical_monitors), ==, 1);
  logical_monitor = META_LOGICAL_MONITOR (logical_monitors->data);
  g_assert_cmpfloat_with_epsilon (meta_logical_monitor_get_scale (logical_monitor),
                                  scale,
                                  FLT_EPSILON);

  scale_fraction = meta_fraction_from_double (scale);

  meta_wayland_test_driver_set_property_int (test_driver,
                                             "scale-num",
                                             scale_fraction.num);
  meta_wayland_test_driver_set_property_int (test_driver,
                                             "scale-denom",
                                             scale_fraction.denom);

  switch (layout_mode)
    {
    case META_LOGICAL_MONITOR_LAYOUT_MODE_LOGICAL:
      clutter_virtual_input_device_notify_absolute_motion (virtual_pointer,
                                                           g_get_monotonic_time (),
                                                           width / scale / 2.0f,
                                                           height / scale / 2.0f);
      break;
    case META_LOGICAL_MONITOR_LAYOUT_MODE_PHYSICAL:
      clutter_virtual_input_device_notify_absolute_motion (virtual_pointer,
                                                           g_get_monotonic_time (),
                                                           width / 2.0f,
                                                           height / 2.0f);
      break;
    }

  meta_flush_input (test_context);

  views = meta_renderer_get_views (renderer);
  g_assert_cmpuint (g_list_length (views), ==, 1);
  return CLUTTER_STAGE_VIEW (views->data);
}

static const char *
layout_mode_to_string (MetaLogicalMonitorLayoutMode layout_mode)
{
  switch (layout_mode)
    {
    case META_LOGICAL_MONITOR_LAYOUT_MODE_LOGICAL:
      return "logical";
    case META_LOGICAL_MONITOR_LAYOUT_MODE_PHYSICAL:
      return "physical";
    }
  g_assert_not_reached ();
}

static const char *
cursor_mode_to_string (MetaScreenCastCursorMode cursor_mode)
{
  switch (cursor_mode)
    {
    case META_SCREEN_CAST_CURSOR_MODE_HIDDEN:
      return "hidden";
    case META_SCREEN_CAST_CURSOR_MODE_EMBEDDED:
      return "embedded";
    case META_SCREEN_CAST_CURSOR_MODE_METADATA:
      return "metadata";
    }

  g_assert_not_reached ();
}

static const char *
reftest_flags_to_string (MetaReftestFlag flags)
{
  if (flags & META_REFTEST_FLAG_UPDATE_REF)
    return "update-ref";
  else
    return "";
}

static void
verify_screen_cast_content (const char               *ref_test_name,
                            int                       test_seq_no,
                            MetaScreenCastCursorMode  cursor_mode)
{
  g_autoptr (GSubprocess) subprocess = NULL;
  g_autofree char *test_seq_no_string = NULL;
  MetaReftestFlag reftest_flags;

  test_seq_no_string = g_strdup_printf ("%d", test_seq_no);
  reftest_flags = META_REFTEST_FLAG_NONE;
  subprocess =
    meta_launch_test_executable ("mutter-cursor-tests-screen-cast-client",
                                 ref_test_name,
                                 test_seq_no_string,
                                 cursor_mode_to_string (cursor_mode),
                                 reftest_flags_to_string (reftest_flags),
                                 NULL);
  meta_wait_test_process (subprocess);
}

static void
wait_for_no_windows (void)
{
  MetaDisplay *display = meta_context_get_display (test_context);

  while (TRUE)
    {
      g_autoptr (GList) windows = NULL;

      windows = meta_display_list_all_windows (display);
      if (!windows)
        return;

      g_main_context_iteration (NULL, TRUE);
    }
}

static void
test_client_cursor (ClutterStageView    *view,
                    const char          *scale_method,
                    MetaCursor           cursor,
                    MtkMonitorTransform  transform,
                    const char          *ref_test_name,
                    int                  ref_test_seq,
                    MetaReftestFlag      ref_test_flags)
{
  const char *cursor_name;
  const char *transform_name;
  MetaWaylandTestClient *test_client;
  MetaWindow *window;
  MetaWindowActor *window_actor;

  g_debug ("Testing cursor with client using %s", scale_method);

  cursor_name = meta_cursor_get_name (cursor);
  transform_name = mtk_monitor_transform_to_string (transform);
  test_client =
    meta_wayland_test_client_new_with_args (test_context,
                                            "cursor-tests-client",
                                            scale_method,
                                            cursor_name,
                                            transform_name,
                                            NULL);
  meta_wayland_test_driver_wait_for_sync_point (test_driver, 0);

  window = meta_find_window_from_title (test_context,
                                        "cursor-tests-surface");
  g_assert_nonnull (window);
  meta_wait_for_window_shown (window);
  window_actor = meta_window_actor_from_window (window);
  g_assert_nonnull (window_actor);
  meta_wait_for_window_cursor (test_context);

  meta_ref_test_verify_view (view,
                             ref_test_name,
                             ref_test_seq,
                             ref_test_flags);

  verify_screen_cast_content (ref_test_name,
                              ref_test_seq,
                              META_SCREEN_CAST_CURSOR_MODE_EMBEDDED);
  verify_screen_cast_content (ref_test_name,
                              ref_test_seq,
                              META_SCREEN_CAST_CURSOR_MODE_METADATA);

  meta_wayland_test_driver_emit_sync_event (test_driver, 0);

  g_object_add_weak_pointer (G_OBJECT (window_actor),
                             (gpointer *) &window_actor);
  meta_wayland_test_client_finish (test_client);
  while (window_actor)
    g_main_context_iteration (NULL, TRUE);
}

static void
meta_test_native_cursor_scaling (void)
{
  MetaBackend *backend = meta_context_get_backend (test_context);
  MetaDisplay *display = meta_context_get_display (test_context);
  ClutterSeat *seat = meta_backend_get_default_seat (backend);
  g_autoptr (ClutterVirtualInputDevice) virtual_pointer = NULL;
  ClutterActor *overlay_actor;
  ClutterStageView *view;
  MetaCursor cursor;
  struct {
    int width;
    int height;
    float scale;
    MetaLogicalMonitorLayoutMode layout_mode;
  } test_cases[] = {
    {
      .width = 1920, .height = 1080, .scale = 1.0,
      .layout_mode = META_LOGICAL_MONITOR_LAYOUT_MODE_LOGICAL,
    },
    {
      .width = 1920, .height = 1080, .scale = 1.0,
      .layout_mode = META_LOGICAL_MONITOR_LAYOUT_MODE_PHYSICAL,
    },
    {
      .width = 1920, .height = 1080, .scale = 2.0,
      .layout_mode = META_LOGICAL_MONITOR_LAYOUT_MODE_LOGICAL,
    },
    {
      .width = 1920, .height = 1080, .scale = 2.0,
      .layout_mode = META_LOGICAL_MONITOR_LAYOUT_MODE_PHYSICAL,
    },
    {
      .width = 1440, .height = 900, .scale = 1.5,
      .layout_mode = META_LOGICAL_MONITOR_LAYOUT_MODE_LOGICAL,
    },
    {
      .width = 1440, .height = 900, .scale = 2.25,
      .layout_mode = META_LOGICAL_MONITOR_LAYOUT_MODE_LOGICAL,
    },
  };
  int i;

  cursor = META_CURSOR_MOVE_OR_RESIZE_WINDOW;
  meta_display_set_cursor (display, cursor);
  virtual_pointer = clutter_seat_create_virtual_device (seat,
                                                        CLUTTER_POINTER_DEVICE);
  overlay_actor = create_overlay_actor ();

  for (i = 0; i < G_N_ELEMENTS (test_cases); i++)
    {
      g_autofree char *ref_test_name = NULL;

      g_debug ("Testing monitor resolution %dx%d with scale %f and "
               "%s layout mode",
               test_cases[i].width, test_cases[i].height, test_cases[i].scale,
               layout_mode_to_string (test_cases[i].layout_mode));

      wait_for_no_windows ();

      ref_test_name = g_strdup_printf ("%s/%d", g_test_get_path (), i);

      view = setup_test_case (test_cases[i].width, test_cases[i].height,
                              test_cases[i].scale,
                              test_cases[i].layout_mode,
                              virtual_pointer);
      meta_ref_test_verify_view (view,
                                 ref_test_name,
                                 0,
                                 meta_ref_test_determine_ref_test_flag ());
      verify_screen_cast_content (ref_test_name, 0,
                                  META_SCREEN_CAST_CURSOR_MODE_EMBEDDED);
      verify_screen_cast_content (ref_test_name, 0,
                                  META_SCREEN_CAST_CURSOR_MODE_METADATA);

      test_client_cursor (view,
                          CURSOR_SCALE_METHOD_BUFFER_SCALE,
                          cursor,
                          MTK_MONITOR_TRANSFORM_NORMAL,
                          ref_test_name, 1,
                          meta_ref_test_determine_ref_test_flag ());
      test_client_cursor (view,
                          CURSOR_SCALE_METHOD_VIEWPORT,
                          cursor,
                          MTK_MONITOR_TRANSFORM_NORMAL,
                          ref_test_name, 0,
                          META_REFTEST_FLAG_NONE);
    }

  clutter_actor_destroy (overlay_actor);
}

static void
meta_test_native_cursor_cropping (void)
{
  MetaBackend *backend = meta_context_get_backend (test_context);
  MetaDisplay *display = meta_context_get_display (test_context);
  ClutterSeat *seat = meta_backend_get_default_seat (backend);
  g_autoptr (ClutterVirtualInputDevice) virtual_pointer = NULL;
  ClutterActor *overlay_actor;
  ClutterStageView *view;
  struct {
    int width;
    int height;
    float scale;
    MetaLogicalMonitorLayoutMode layout_mode;
  } test_cases[] = {
    {
      .width = 1920, .height = 1080, .scale = 1.0,
      .layout_mode = META_LOGICAL_MONITOR_LAYOUT_MODE_LOGICAL,
    },
    {
      .width = 1920, .height = 1080, .scale = 1.0,
      .layout_mode = META_LOGICAL_MONITOR_LAYOUT_MODE_PHYSICAL,
    },
    {
      .width = 1920, .height = 1080, .scale = 2.0,
      .layout_mode = META_LOGICAL_MONITOR_LAYOUT_MODE_LOGICAL,
    },
    {
      .width = 1920, .height = 1080, .scale = 2.0,
      .layout_mode = META_LOGICAL_MONITOR_LAYOUT_MODE_PHYSICAL,
    },
    {
      .width = 1440, .height = 900, .scale = 1.5,
      .layout_mode = META_LOGICAL_MONITOR_LAYOUT_MODE_LOGICAL,
    },
    {
      .width = 1440, .height = 900, .scale = 2.25,
      .layout_mode = META_LOGICAL_MONITOR_LAYOUT_MODE_LOGICAL,
    },
  };
  int i;

  meta_display_set_cursor (display, META_CURSOR_DEFAULT);
  virtual_pointer = clutter_seat_create_virtual_device (seat,
                                                        CLUTTER_POINTER_DEVICE);
  overlay_actor = create_overlay_actor ();

  for (i = 0; i < G_N_ELEMENTS (test_cases); i++)
    {
      g_autofree char *ref_test_name = NULL;

      g_debug ("Testing monitor resolution %dx%d with scale %f and "
               "%s layout mode",
               test_cases[i].width, test_cases[i].height, test_cases[i].scale,
               layout_mode_to_string (test_cases[i].layout_mode));

      wait_for_no_windows ();

      ref_test_name = g_strdup_printf ("%s/%d", g_test_get_path (), i);

      view = setup_test_case (test_cases[i].width, test_cases[i].height,
                              test_cases[i].scale,
                              test_cases[i].layout_mode,
                              virtual_pointer);

      test_client_cursor (view,
                          CURSOR_SCALE_METHOD_VIEWPORT_CROPPED,
                          META_CURSOR_MOVE_OR_RESIZE_WINDOW,
                          MTK_MONITOR_TRANSFORM_NORMAL,
                          ref_test_name, 0,
                          meta_ref_test_determine_ref_test_flag ());
    }

  clutter_actor_destroy (overlay_actor);
}

static void
meta_test_native_cursor_transform (void)
{
  MetaBackend *backend = meta_context_get_backend (test_context);
  MetaDisplay *display = meta_context_get_display (test_context);
  ClutterSeat *seat = meta_backend_get_default_seat (backend);
  g_autoptr (ClutterVirtualInputDevice) virtual_pointer = NULL;
  ClutterActor *overlay_actor;
  ClutterStageView *view;
  struct {
    int width;
    int height;
    float scale;
    MetaLogicalMonitorLayoutMode layout_mode;
    MtkMonitorTransform transform;
  } test_cases[] = {
    {
      .width = 1920, .height = 1080, .scale = 1.0,
      .layout_mode = META_LOGICAL_MONITOR_LAYOUT_MODE_LOGICAL,
      .transform = MTK_MONITOR_TRANSFORM_90,
    },
    {
      .width = 1920, .height = 1080, .scale = 1.0,
      .layout_mode = META_LOGICAL_MONITOR_LAYOUT_MODE_PHYSICAL,
      .transform = MTK_MONITOR_TRANSFORM_90,
    },
    {
      .width = 1920, .height = 1080, .scale = 2.0,
      .layout_mode = META_LOGICAL_MONITOR_LAYOUT_MODE_LOGICAL,
      .transform = MTK_MONITOR_TRANSFORM_90,
    },
    {
      .width = 1920, .height = 1080, .scale = 2.0,
      .layout_mode = META_LOGICAL_MONITOR_LAYOUT_MODE_PHYSICAL,
      .transform = MTK_MONITOR_TRANSFORM_90,
    },
    {
      .width = 1440, .height = 900, .scale = 1.5,
      .layout_mode = META_LOGICAL_MONITOR_LAYOUT_MODE_LOGICAL,
    },
    {
      .width = 1440, .height = 900, .scale = 2.25,
      .layout_mode = META_LOGICAL_MONITOR_LAYOUT_MODE_LOGICAL,
      .transform = MTK_MONITOR_TRANSFORM_270,
    },
  };
  int i;

  meta_display_set_cursor (display, META_CURSOR_DEFAULT);
  virtual_pointer = clutter_seat_create_virtual_device (seat,
                                                        CLUTTER_POINTER_DEVICE);
  overlay_actor = create_overlay_actor ();

  for (i = 0; i < G_N_ELEMENTS (test_cases); i++)
    {
      g_autofree char *ref_test_name = NULL;

      g_debug ("Testing monitor resolution %dx%d with scale %f and "
               "%s layout mode",
               test_cases[i].width, test_cases[i].height, test_cases[i].scale,
               layout_mode_to_string (test_cases[i].layout_mode));

      wait_for_no_windows ();

      ref_test_name = g_strdup_printf ("%s/%d", g_test_get_path (), i);

      view = setup_test_case (test_cases[i].width, test_cases[i].height,
                              test_cases[i].scale,
                              test_cases[i].layout_mode,
                              virtual_pointer);

      test_client_cursor (view,
                          CURSOR_SCALE_METHOD_BUFFER_SCALE,
                          META_CURSOR_DEFAULT,
                          test_cases[i].transform,
                          ref_test_name, 0,
                          meta_ref_test_determine_ref_test_flag ());
      test_client_cursor (view,
                          CURSOR_SCALE_METHOD_VIEWPORT,
                          META_CURSOR_DEFAULT,
                          test_cases[i].transform,
                          ref_test_name, 1,
                          meta_ref_test_determine_ref_test_flag ());
      test_client_cursor (view,
                          CURSOR_SCALE_METHOD_VIEWPORT_CROPPED,
                          META_CURSOR_MOVE_OR_RESIZE_WINDOW,
                          test_cases[i].transform,
                          ref_test_name, 2,
                          meta_ref_test_determine_ref_test_flag ());
    }

  clutter_actor_destroy (overlay_actor);
}

static void
init_tests (void)
{
  g_test_add_func ("/backends/native/cursor/scaling",
                   meta_test_native_cursor_scaling);
  g_test_add_func ("/backends/native/cursor/cropping",
                   meta_test_native_cursor_cropping);
  g_test_add_func ("/backends/native/cursor/transform",
                   meta_test_native_cursor_transform);
}

static void
on_before_tests (void)
{
  MetaWaylandCompositor *compositor =
    meta_context_get_wayland_compositor (test_context);

  test_driver = meta_wayland_test_driver_new (compositor);
  meta_wayland_test_driver_set_property_int (test_driver,
                                             "cursor-theme-size",
                                             meta_prefs_get_cursor_size ());
}

static void
on_after_tests (void)
{
  g_clear_object (&test_driver);
}

int
main (int    argc,
      char **argv)
{
  g_autoptr (MetaContext) context = NULL;

  context = meta_create_test_context (META_CONTEXT_TEST_TYPE_TEST,
                                      (META_CONTEXT_TEST_FLAG_NO_X11 |
                                       META_CONTEXT_TEST_FLAG_TEST_CLIENT |
                                       META_CONTEXT_TEST_FLAG_NO_ANIMATIONS));
  g_assert_true (meta_context_configure (context, &argc, &argv, NULL));
  meta_context_test_set_background_color (META_CONTEXT_TEST (context),
                                          COGL_COLOR_INIT (255, 255, 255, 255));

  test_context = context;

  init_tests ();

  g_signal_connect (context, "before-tests",
                    G_CALLBACK (on_before_tests), NULL);
  g_signal_connect (context, "after-tests",
                    G_CALLBACK (on_after_tests), NULL);

  return meta_context_test_run_tests (META_CONTEXT_TEST (context),
                                      META_TEST_RUN_FLAG_NONE);
}
