/*
 * xrdesktop
 * Copyright 2018 Collabora Ltd.
 * Author: Lubosz Sarnecki <lubosz.sarnecki@collabora.com>
 * SPDX-License-Identifier: MIT
 */

#include <glib.h>
#include <glib-unix.h>

#include "xrd.h"

#define GRID_WIDTH 4
#define GRID_HEIGHT 4

typedef struct Example
{
  GMainLoop *loop;
  XrdClient *client;

  XrdWindow *switch_button;
  XrdWindow *restart_button;
  XrdWindow *quit_button;
  gboolean restart;

  GulkanTexture *cursor_texture;
  guint64 click_source;
  guint64 move_source;
  guint64 keyboard_source;
  guint64 quit_source;
  gboolean shutdown;

  gboolean always_overlay_mode;

  GdkPixbuf *window_pixbuf;
  GdkPixbuf *bw_window_pixbuf;
  GdkPixbuf *child_window_pixbuf;

  /* A real window manager will have a function like
   * _desktop_window_process_frame when updating a window's contents.
   * In this example, this is simulated by calling the window
   * update function for each desktop window in a imer. */
  GSList *desktop_window_list;
  gint64 desktop_window_manager_update_loop;

  GThread *upload_thread;
  volatile gint shutdown_threads;

  GRand *rand;
  gboolean saturating;
} Example;

/* Placeholder struct for a real desktop window manager's window struct.
 * Examples are KWin::EffectWindow (kwin plugin) and MetaWindow (gnome).
 *
 * When a per window state should be saved (for example an OpenGL texture for
 * external memory, create a Wrapper struct for a DesktopWindow and save
 * that in a XrdWindow's `native` instead of the DesktopWindow struct. Ex.
 * struct WindowWrapper {
 *   KWin::EffectWindow *desktop_window;
 *   GLuint *opengl_texture;
 * }
 */
typedef struct DesktopWindow
{
  GdkPixbuf *pixbuf;
  gchar *title;
  gboolean is_desaturated;
} DesktopWindow;

/* In a real desktop window manager, the wm creates window structs, not us. */
static DesktopWindow *
_create_desktop_window (Example *self,
                        gchar *title,
                        GdkPixbuf *pixbuf)
{
  DesktopWindow *desktop_window = g_malloc (sizeof (DesktopWindow));
  desktop_window->pixbuf = pixbuf;
  desktop_window->title = g_strdup (title);
  self->desktop_window_list =
    g_slist_append (self->desktop_window_list, desktop_window);
  return desktop_window;
}

static gboolean
_desktop_window_process_frame (Example *self, DesktopWindow *desktop_window);

static gboolean
_desktop_window_manager_update_loop_cb (gpointer _self)
{
  Example *self = _self;
  for (GSList *l = self->desktop_window_list; l; l = l->next)
    {
      DesktopWindow *desktop_window = l->data;
      _desktop_window_process_frame (self, desktop_window);
    }
  return TRUE;
}

static gboolean
_sigint_cb (gpointer _self)
{
  Example *self = (Example*) _self;
  g_main_loop_quit (self->loop);
  return TRUE;
}

static GdkPixbuf *
load_gdk_pixbuf (const gchar* name)
{
  GError * error = NULL;
  GdkPixbuf *pixbuf_rgb = gdk_pixbuf_new_from_resource (name, &error);

  if (error != NULL)
    {
      g_printerr ("Unable to read file: %s\n", error->message);
      g_error_free (error);
      return NULL;
    }

  GdkPixbuf *pixbuf = gdk_pixbuf_add_alpha (pixbuf_rgb, FALSE, 0, 0, 0);
  g_object_unref (pixbuf_rgb);

  return pixbuf;
}


static GulkanTexture *
_desktop_window_to_texture (Example *self,
                            DesktopWindow *desktop_window)
{
  GulkanClient *gc = xrd_client_get_gulkan (self->client);
  VkImageLayout layout = xrd_client_get_upload_layout (self->client);

  GulkanTexture *tex =
    gulkan_texture_new_from_pixbuf (gc, desktop_window->pixbuf,
                                    VK_FORMAT_R8G8B8A8_SRGB, layout, FALSE);
  return tex;
}

static gboolean
_desktop_window_process_frame (Example *self, DesktopWindow *desktop_window)
{
  XrdWindow *xrd_window = xrd_client_lookup_window (self->client,
                                                    desktop_window);
  if (!xrd_window)
    {
      g_print ("Error processing frame, window is NULL\n");
      return TRUE;
    }

  guint window_texture_width =
    (guint)gdk_pixbuf_get_width (desktop_window->pixbuf);
  guint window_texture_height =
    (guint)gdk_pixbuf_get_height (desktop_window->pixbuf);

  GulkanTexture *cached_texture = xrd_window_get_texture (xrd_window);

  /* we need to create a new texture if we have not submitted a texture yet
   * or if the window size changed. */
  VkExtent2D extent;
  if (cached_texture)
    extent = gulkan_texture_get_extent (cached_texture);

  if (cached_texture == NULL ||
      window_texture_width != extent.width ||
      window_texture_height != extent.height)
    {
      g_debug ("Allocating new texture for %s\n", desktop_window->title);
      cached_texture = _desktop_window_to_texture (self, desktop_window);

      /* Failing to creating a texture for a window should never happen. */
      if (!GULKAN_IS_TEXTURE (cached_texture))
        {
          g_print ("Error creating texture for window!\n");
          return FALSE;
        }

      xrd_window_set_and_submit_texture (xrd_window, cached_texture);
    }
  else
    {
      /* update window_wrapper->gulkan_texture with desktop window content */
      xrd_window_submit_texture (xrd_window);
    }

  return TRUE;
}

static XrdWindow *
_add_window (Example *self,
             DesktopWindow *desktop_window,
             float width_meter,
             gboolean draggable)

{
  /* a real window manager needs to find these from real desktop windows. */
  gchar *title = desktop_window->title;
  guint texture_width = (guint)gdk_pixbuf_get_width (desktop_window->pixbuf);
  guint texture_height = (guint)gdk_pixbuf_get_height (desktop_window->pixbuf);

  float ppm = (float) texture_width / width_meter;

  XrdWindow *window = xrd_window_new_from_pixels (self->client, title,
                                                  texture_width, texture_height,
                                                  ppm);

  g_object_set (window, "native", desktop_window, NULL);

  xrd_client_add_window (self->client, window, draggable, desktop_window);

  return window;
}

static gboolean
_init_client (Example *self, XrdClient *client);
static void
_cleanup_client (Example *self);
static gboolean
_init_threads(Example *self);

static gboolean
perform_switch (gpointer data)
{
  Example *self = data;
  /* disconnect all event callbacks */
  _cleanup_client (self);

  self->client = xrd_client_switch_mode (self->client);

  /* set up the example on the new client */
  _init_client (self, self->client);

  _desktop_window_manager_update_loop_cb (self);

  if (XRD_IS_SCENE_CLIENT (self->client))
    _init_threads(self);

  return FALSE;
}

static void
_button_switch_press_cb (XrdWindow     *window,
                         GxrController *controller,
                         gpointer       _self)
{
  (void) controller;
  (void) window;
  Example *self = _self;

  /* Don't clean up bere because the callback will return.
   * Instead do the cleanup and switch on the next mainloop iteration. */
  g_timeout_add (0, perform_switch, self);
}

static void
_button_quit_press_cb (XrdWindow     *window,
                       GxrController *controller,
                       gpointer       _self)
{
  (void) controller;
  (void) window;
  Example *self = _self;

  self->restart = FALSE;
  g_main_loop_quit (self->loop);
}

static void
_button_restart_press_cb (XrdWindow     *window,
                          GxrController *controller,
                          gpointer       _self)
{
  (void) controller;
  (void) window;
  Example *self = _self;

  self->restart = TRUE;
  g_main_loop_quit (self->loop);
}

static void
_init_child_window (Example      *self,
                    XrdWindow    *window)
{
  DesktopWindow *desktop_window =
     _create_desktop_window (self, "A child", self->child_window_pixbuf);

  XrdWindow *child = _add_window (self, desktop_window, 0.25f, FALSE);

  graphene_point_t offset = { .x = 25, .y = 25 };
  xrd_window_add_child (window, child, &offset);
}

static gboolean
_init_cursor (Example *self, GulkanClient *gc)
{
  GdkPixbuf *cursor_pixbuf = load_gdk_pixbuf ("/res/cursor.png");
  if (cursor_pixbuf == NULL)
    {
      g_printerr ("Could not load image.\n");
      return FALSE;
    }

  VkImageLayout layout = xrd_client_get_upload_layout (self->client);

  self->cursor_texture =
    gulkan_texture_new_from_pixbuf (gc, cursor_pixbuf,
                                    VK_FORMAT_R8G8B8A8_SRGB,
                                    layout, TRUE);

  XrdDesktopCursor *cursor = xrd_client_get_desktop_cursor (self->client);
  xrd_desktop_cursor_set_hotspot (cursor, 3, 3);
  xrd_desktop_cursor_set_and_submit_texture (cursor, self->cursor_texture);

  g_object_unref (cursor_pixbuf);

  return TRUE;
}

static void
_init_buttons (Example *self)
{
  graphene_point3d_t button_pos = {
    .x =  -1.5f,
    .y =  0.0f,
    .z = -2.5f
  };

  gchar *quit_str[] = { "Quit" };
  self->quit_button =
    xrd_client_button_new_from_text (self->client, .6f, .6f, 450.0f,
                                     1, quit_str);
  if (!self->quit_button)
    return;
  xrd_client_add_button (self->client, self->quit_button, &button_pos,
                         (GCallback) _button_quit_press_cb, self);

  button_pos.x += xrd_window_get_current_width_meters (self->quit_button);

  gchar *restart_str[] = { "Restart" };
  self->restart_button =
    xrd_client_button_new_from_text (self->client, .6f, .6f, 450.0f,
                                     1, restart_str);
  if (!self->restart_button)
    return;
  xrd_client_add_button (self->client, self->restart_button, &button_pos,
                         (GCallback) _button_restart_press_cb, self);

  button_pos.x += xrd_window_get_current_width_meters (self->restart_button);

  gchar *switch_str[] =  { "Switch", "Mode"};
  self->switch_button =
    xrd_client_button_new_from_text (self->client, .6f, .6f, 450.0f,
                                     2, switch_str);
  if (!self->switch_button)
    return;
  xrd_client_add_button (self->client, self->switch_button, &button_pos,
                         (GCallback) _button_switch_press_cb, self);
}

static gboolean
_init_windows (Example *self)
{
  float window_x = - 1.f;
  float window_y = .5f;

  /* A window manager iterates over current windows and decides which of those
   * should be mirrored. This example creates a grid with placeholders. */
  for (int col = 0; col < GRID_WIDTH; col++)
    {
      float max_window_height = 0;
      for (int row = 0; row < GRID_HEIGHT; row++)
        {
          gchar window_title[16];
          sprintf (window_title, "Bird [%d,%d]", col, row);
          DesktopWindow *desktop_window =
             _create_desktop_window (self, window_title, self->window_pixbuf);

          XrdWindow *window = _add_window (self, desktop_window, 0.5f, TRUE);

          window_x += xrd_window_get_current_width_meters (window);

          float window_height = xrd_window_get_current_height_meters (window);
          if (window_height > max_window_height)
            max_window_height = window_height;

          graphene_point3d_t point = {
            .x = window_x,
            .y = window_y,
            .z = -3
          };
          graphene_matrix_t transform;
          graphene_matrix_init_translate (&transform, &point);
          xrd_window_set_transformation (window, &transform);

          xrd_window_save_reset_transformation (window);

          if ((col + row) % 2 == 0)
            xrd_window_set_flip_y (window, TRUE);

          if (col == 0 && row == 0)
            _init_child_window (self, window);
        }
      window_x = -1.f;
      window_y += max_window_height;
    }

  return TRUE;
}

static void
_cleanup_client (Example *self)
{
  g_signal_handler_disconnect (self->client, self->click_source);
  g_signal_handler_disconnect (self->client, self->move_source);
  g_signal_handler_disconnect (self->client, self->keyboard_source);
  g_signal_handler_disconnect (self->client, self->quit_source);
  self->click_source = 0;
  self->move_source = 0;
  self->keyboard_source = 0;
  self->quit_source = 0;

  g_atomic_int_set (&self->shutdown_threads, 1);
  if (self->upload_thread)
    {
      g_thread_join (self->upload_thread);
      self->upload_thread = NULL;
    }
  g_atomic_int_set (&self->shutdown_threads, 0);
}

static void
_cleanup (Example *self)
{
  self->shutdown = TRUE;

  _cleanup_client (self);

  GSList *windows = xrd_client_get_windows (self->client);
  for (GSList *l = windows; l; l = l->next)
    {
      XrdWindow *window = l->data;

      DesktopWindow *desktop_window = NULL;
      g_object_get (window, "native", &desktop_window, NULL);
      g_free (desktop_window->title);
      /* desktop_window->pixbuf is not owned by desktop_window */

      xrd_window_close (window);
    }
  g_slist_free_full (self->desktop_window_list, g_free);

  g_object_unref (self->bw_window_pixbuf);
  g_object_unref (self->window_pixbuf);
  g_object_unref (self->child_window_pixbuf);

  g_object_unref (self->client);
  self->client = NULL;

  g_rand_free (self->rand);

  g_print ("Cleaned up!\n");
}

static void
_click_cb (XrdClient     *client,
           XrdClickEvent *event,
           Example       *self)
{
  (void) client;
  (void) self;
  g_print ("button %d %s at %f, %f\n",
           event->button, event->state ? "pressed" : "released",
           (double) event->position->x,
           (double) event->position->y);
}


static void
_move_cursor_cb (XrdClient          *client,
                 XrdMoveCursorEvent *event,
                 Example            *self)
{
  (void) client;
  (void) self;
  (void) event;
  /*
  g_print ("move: %f, %f\n",
           event->position->x, event->position->y);
   */
}


static void
_keyboard_press_cb (XrdClient   *client,
                    GdkEventKey *event,
                    Example     *self)
{
  (void) client;
  (void) self;
  g_print ("key (%d): %s\n", event->length, event->string);
}

static void
_request_quit_cb (XrdClient    *client,
                  GxrQuitEvent *event,
                  Example      *self)
{
  (void) client;
  (void) self;

  switch (event->reason)
  {
    case GXR_QUIT_SHUTDOWN:
    {
      g_print ("Quit event: Shutdown\n");
      g_main_loop_quit (self->loop);
    } break;
    case GXR_QUIT_PROCESS_QUIT:
    {
      g_print ("Quit event: Process quit\n");
      if (XRD_IS_OVERLAY_CLIENT (self->client) && !self->always_overlay_mode)
        g_timeout_add (0, perform_switch, self);
    } break;
    case GXR_QUIT_APPLICATION_TRANSITION:
    {
      g_print ("Quit event: Application transition\n");
      if (XRD_IS_SCENE_CLIENT (self->client))
        g_timeout_add (0, perform_switch, self);
    } break;
  }
}

static GSList*
_get_windows_by_saturation (Example *self, gboolean is_saturated)
{
  GSList *results = NULL;

  for (GSList *l = self->desktop_window_list; l; l = l->next)
    {
      DesktopWindow *window = l->data;
      XrdWindow *xrd_window = xrd_client_lookup_window (self->client, window);
      XrdWindowData *data = xrd_window_get_data (xrd_window);
      /* Ignore child window */
      if (window->is_desaturated != is_saturated && data->parent_window == NULL)
        results = g_slist_append (results, window);
    }

  return results;
}

static void*
_reupload_thread (gpointer data)
{
  Example *self = (Example*) data;

  VkImageLayout layout = xrd_client_get_upload_layout (self->client);

  while (!g_atomic_int_get (&self->shutdown_threads))
    {
      g_usleep (10000);

      GSList *todo_windows = _get_windows_by_saturation (self, !self->saturating);
      guint len = g_slist_length (todo_windows);

      if (len == 0)
        {
          self->saturating = !self->saturating;
          todo_windows = _get_windows_by_saturation (self, !self->saturating);
          len = g_slist_length (todo_windows);
          g_assert(len > 0);
        }

      gint32 id = g_rand_int_range (self->rand, 0, (gint32) len);
      GSList *nth = g_slist_nth (todo_windows, (guint) id);
      DesktopWindow *desktop_window = nth->data;

      g_slist_free (todo_windows);

      XrdWindow *xrd_window = xrd_client_lookup_window (self->client,
                                                        desktop_window);
      if (!xrd_window)
        {
          g_printerr ("Error processing frame, window is NULL\n");
          continue;
        }

      GdkPixbuf *pixbuf = NULL;
      if (self->saturating)
        pixbuf = self->window_pixbuf;
      else
        pixbuf = self->bw_window_pixbuf;

      GulkanTexture *tex = xrd_window_get_texture (xrd_window);
      if (!tex)
        continue;

      gulkan_texture_upload_pixbuf (tex, pixbuf, layout);
      xrd_window_submit_texture (xrd_window);
      desktop_window->is_desaturated = !self->saturating;
    }

  return NULL;
}

static gboolean
_init_threads(Example *self)
{
  GError *error = NULL;
  self->upload_thread = g_thread_try_new ("upload",
                                          (GThreadFunc) _reupload_thread,
                                          self,
                                         &error);
  if (error != NULL)
    {
      g_printerr ("Unable to start render thread: %s\n", error->message);
      g_error_free (error);
      return FALSE;
    }

  return TRUE;
}

static gboolean
_init_client (Example *self, XrdClient *client)
{
  if (!client)
    {
      g_printerr ("XrdClient did not initialize correctly.\n");
      return FALSE;
    }

  self->click_source = g_signal_connect (client, "click-event",
                                         (GCallback) _click_cb, self);
  self->move_source = g_signal_connect (client, "move-cursor-event",
                                        (GCallback) _move_cursor_cb, self);
  self->keyboard_source = g_signal_connect (client, "keyboard-press-event",
                                            (GCallback) _keyboard_press_cb,
                                            self);
  self->quit_source = g_signal_connect (client, "request-quit-event",
                                        (GCallback) _request_quit_cb, self);

  GulkanClient *gc = xrd_client_get_gulkan (client);
  if (!_init_cursor (self, gc))
    return FALSE;

  _init_buttons (self);

  return TRUE;
}

static gboolean
_init_example (Example *self, XrdClient *client)
{
  self->client = client;
  self->shutdown = FALSE;

  if (!_init_client (self, client))
    return FALSE;

  if (!_init_windows (self))
    return FALSE;

  g_unix_signal_add (SIGINT, _sigint_cb, self);

  _desktop_window_manager_update_loop_cb (self);

  if (XRD_IS_SCENE_CLIENT (client))
    {
      _init_threads(self);
    }

  return TRUE;
}

static gchar *mode_override;

static GOptionEntry entries[] =
{
  { "mode", 'm', 0, G_OPTION_ARG_STRING, &mode_override,
    "Override default mode from settings. [overlay, scene]", "M" },
};

static int
_run (gboolean *restart)
{
  Example self = {
    .loop = g_main_loop_new (NULL, FALSE),
    .window_pixbuf = load_gdk_pixbuf ("/res/hawk.jpg"),
    .bw_window_pixbuf = load_gdk_pixbuf ("/res/hawk.jpg"),
    .child_window_pixbuf = load_gdk_pixbuf ("/res/cat.jpg"),
    .desktop_window_list = NULL,
    .restart = FALSE,
    .rand = g_rand_new (),
    .shutdown_threads = 0,
    .always_overlay_mode = FALSE
  };

  gdk_pixbuf_saturate_and_pixelate (self.bw_window_pixbuf,
                                    self.bw_window_pixbuf,
                                    0.0, FALSE);

  GSettings *settings = xrd_settings_get_instance ();
  XrdClientMode mode = (guint) g_settings_get_enum (settings, "default-mode");

  if (mode_override)
    {
      if (g_strcmp0 (mode_override, "scene") == 0)
        mode = XRD_CLIENT_MODE_SCENE;
      else if (g_strcmp0 (mode_override, "overlay") == 0)
        mode = XRD_CLIENT_MODE_OVERLAY;
      else
        g_warning ("Invalid mode. Valid modes are [overlay, scene]. "
                   "Using default from Settings.");
    }

  if (mode == XRD_CLIENT_MODE_OVERLAY)
    self.always_overlay_mode = TRUE;

  if (mode == XRD_CLIENT_MODE_SCENE)
    {
      GxrApi api = (guint) g_settings_get_enum (settings, "default-api");
      GxrContext* gxr_context =
        gxr_context_new_headless_from_api (api, "xrdesktop client example",
                                           XRD_VERSION_HEX);
      if (!gxr_context)
        {
          g_print ("Failed to init XR runtime\n");
          return 1;
        }

      gboolean scene_available =
        !gxr_context_is_another_scene_running (gxr_context);
      g_object_unref (gxr_context);
      if (!scene_available)
        {
          mode = XRD_CLIENT_MODE_OVERLAY;
          g_warning ("Scene mode unavailable. Launching in overlay mode.");
        }
    }

  XrdClient *client = xrd_client_new_with_mode (mode);
  if (!_init_example (&self, client))
    return 1;

  /* start glib main loop */
  g_main_loop_run (self.loop);

  *restart = self.restart;

  /* don't clean up when quitting during switching */
  if (self.client != NULL)
    _cleanup (&self);

  g_main_loop_unref (self.loop);

  return 0;
}

int
main (int argc, char *argv[])
{
  GError *error = NULL;
  GOptionContext *context;

  context = g_option_context_new ("- xrdesktop client example.");
  g_option_context_add_main_entries (context, entries, NULL);
  if (!g_option_context_parse (context, &argc, &argv, &error))
  {
    g_print ("Wrong parameters: %s\n", error->message);
    g_option_context_free (context);
    exit (1);
  }
  g_option_context_free (context);

  if (!xrd_settings_is_schema_installed ())
  {
    g_print ("GSettings schema not found. Check xrdesktop installation!\n");
    return 1;
  }

  while (TRUE)
    {
      gboolean restart = FALSE;
      int res = _run (&restart);
      if (!restart)
        return res;

      g_print ("Restarting\n");
    }
}
