/* Clapper Application
 * Copyright (C) 2024 Rafał Dzięgiel <rafostar.github@gmail.com>
 *
 * 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 "config.h"

#include <math.h>
#include <glib/gi18n.h>
#include <gdk/gdk.h>
#include <adwaita.h>
#include <clapper/clapper.h>
#include <clapper-gtk/clapper-gtk.h>

#include "clapper-app-window.h"
#include "clapper-app-file-dialog.h"
#include "clapper-app-utils.h"

#define DEFAULT_WINDOW_WIDTH 1024
#define DEFAULT_WINDOW_HEIGHT 576

#define N_PROGRESSION_MODES 5

#define CLAPPER_APP_SEEK_UNIT_SECOND 0
#define CLAPPER_APP_SEEK_UNIT_MINUTE 1
#define CLAPPER_APP_SEEK_UNIT_PERCENTAGE 2

#define PERCENTAGE_ROUND(a) (round ((gdouble) a / 0.01) * 0.01)
#define AXIS_WINS_OVER(a,b) ((a > 0 && a - 0.3 > b) || (a < 0 && a + 0.3 < b))

#define GST_CAT_DEFAULT clapper_app_window_debug
GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT);

struct _ClapperAppWindow
{
  GtkApplicationWindow parent;

  GtkWidget *video;
  ClapperGtkBillboard *billboard;
  ClapperGtkSimpleControls *simple_controls;

  GtkDropTarget *drop_target;
  GtkCssProvider *provider;

  ClapperMediaItem *current_item;

  GSettings *settings;

  guint seek_timeout;

  gboolean key_held;
  gboolean scrolling;
  gboolean seeking;

  gboolean was_playing;
  gdouble pending_position;
  gdouble current_duration;

  gdouble last_volume;
};

#define parent_class clapper_app_window_parent_class
G_DEFINE_TYPE (ClapperAppWindow, clapper_app_window, GTK_TYPE_APPLICATION_WINDOW)

static void
_media_item_title_changed_cb (ClapperMediaItem *item,
    GParamSpec *pspec G_GNUC_UNUSED, ClapperAppWindow *self)
{
  gchar *title;

  if ((title = clapper_media_item_get_title (item))) {
    gtk_window_set_title (GTK_WINDOW (self), title);
    g_free (title);
  } else {
    gtk_window_set_title (GTK_WINDOW (self), CLAPPER_APP_NAME);
  }
}

static void
_queue_current_item_changed_cb (ClapperQueue *queue,
    GParamSpec *pspec G_GNUC_UNUSED, ClapperAppWindow *self)
{
  ClapperMediaItem *current_item = clapper_queue_get_current_item (queue);

  /* Disconnect signal from old item */
  if (self->current_item) {
    g_signal_handlers_disconnect_by_func (self->current_item,
        _media_item_title_changed_cb, self);
  }

  gst_object_replace ((GstObject **) &self->current_item, GST_OBJECT_CAST (current_item));
  GST_DEBUG_OBJECT (self, "Current item changed to: %" GST_PTR_FORMAT, self->current_item);

  /* Reconnect signal to new item */
  if (self->current_item) {
    g_signal_connect (self->current_item, "notify::title",
        G_CALLBACK (_media_item_title_changed_cb), self);
    _media_item_title_changed_cb (self->current_item, NULL, self);
  } else {
    gtk_window_set_title (GTK_WINDOW (self), CLAPPER_APP_NAME);
  }

  gst_clear_object (&current_item);
}

static gboolean
_get_seek_method_mapping (GValue *value,
    GVariant *variant, gpointer user_data G_GNUC_UNUSED)
{
  ClapperPlayerSeekMethod seek_method;

  seek_method = (ClapperPlayerSeekMethod) g_variant_get_int32 (variant);
  g_value_set_enum (value, seek_method);

  return TRUE;
}

static GtkWidget *
_pick_pointer_widget (ClapperAppWindow *self)
{
  GdkSurface *surface = gtk_native_get_surface (GTK_NATIVE (self));
  GdkDisplay *display = gtk_widget_get_display (GTK_WIDGET (self));
  GdkSeat *seat = gdk_display_get_default_seat (display);
  GtkWidget *widget = NULL;

  if (G_LIKELY (seat != NULL)) {
    GdkDevice *device = gdk_seat_get_pointer (seat);
    gdouble px = 0, py = 0, native_x = 0, native_y = 0;

    if (G_LIKELY (device != NULL))
      gdk_surface_get_device_position (surface, device, &px, &py, NULL);

    gtk_native_get_surface_transform (GTK_NATIVE (self), &native_x, &native_y);

    widget = gtk_widget_pick (GTK_WIDGET (self),
        px - native_x, py - native_y, GTK_PICK_DEFAULT);
  }

  return widget;
}

static void
_player_volume_changed_cb (ClapperPlayer *player,
    GParamSpec *pspec G_GNUC_UNUSED, ClapperAppWindow *self)
{
  gdouble volume = PERCENTAGE_ROUND (clapper_player_get_volume (player));

  /* Only notify when volume changes at least 1%. Remembering last volume
   * also prevents us from showing volume when it is restored on startup. */
  if (volume != self->last_volume) {
    clapper_gtk_billboard_announce_volume (self->billboard);
    self->last_volume = volume;
  }
}

static void
_player_speed_changed_cb (ClapperPlayer *player G_GNUC_UNUSED,
    GParamSpec *pspec G_GNUC_UNUSED, ClapperAppWindow *self)
{
  clapper_gtk_billboard_announce_speed (self->billboard);
}

static void
video_toggle_fullscreen_cb (ClapperGtkVideo *video, ClapperAppWindow *self)
{
  GtkWindow *window = GTK_WINDOW (self);

  g_object_set (window, "fullscreened", !gtk_window_is_fullscreen (window), NULL);
}

static void
video_map_cb (GtkWidget *widget, ClapperAppWindow *self)
{
  ClapperPlayer *player;
  gdouble speed;

  GST_TRACE_OBJECT (self, "Video map");

  player = clapper_gtk_video_get_player (CLAPPER_GTK_VIDEO_CAST (self->video));

  g_signal_connect (player, "notify::volume",
      G_CALLBACK (_player_volume_changed_cb), self);
  g_signal_connect (player, "notify::speed",
      G_CALLBACK (_player_speed_changed_cb), self);

  speed = clapper_player_get_speed (player);

  /* If we are starting with non-1x speed, notify user about it */
  if (!G_APPROX_VALUE (speed, 1.0, FLT_EPSILON))
    clapper_gtk_billboard_announce_speed (self->billboard);
}

static void
video_unmap_cb (GtkWidget *widget, ClapperAppWindow *self)
{
  ClapperPlayer *player;

  GST_TRACE_OBJECT (self, "Video unmap");

  player = clapper_gtk_video_get_player (CLAPPER_GTK_VIDEO_CAST (self->video));

  g_signal_handlers_disconnect_by_func (player, _player_volume_changed_cb, self);
  g_signal_handlers_disconnect_by_func (player, _player_speed_changed_cb, self);
}

static void
_open_subtitles_cb (ClapperGtkExtraMenuButton *button G_GNUC_UNUSED,
    ClapperMediaItem *item, ClapperAppWindow *self)
{
  GtkApplication *gtk_app = gtk_window_get_application (GTK_WINDOW (self));
  clapper_app_file_dialog_open_subtitles (gtk_app, item);
}

static void
right_click_pressed_cb (GtkGestureClick *click, gint n_press,
    gdouble x, gdouble y, ClapperAppWindow *self)
{
  GdkCursor *cursor;
  const gchar *cursor_name = NULL;

  GST_LOG_OBJECT (self, "Right click pressed");

  if ((cursor = gtk_widget_get_cursor (self->video)))
    cursor_name = gdk_cursor_get_name (cursor);

  /* Restore cursor if faded on video */
  if (g_strcmp0 (cursor_name, "none") == 0) {
    GdkCursor *new_cursor = gdk_cursor_new_from_name ("default", NULL);

    gtk_widget_set_cursor (self->video, new_cursor);
    g_object_unref (new_cursor);
  }
}

static void
right_click_released_cb (GtkGestureClick *click, gint n_press,
    gdouble x, gdouble y, ClapperAppWindow *self)
{
  GdkSurface *surface;
  GdkEventSequence *sequence;
  GdkEvent *event;

  GST_LOG_OBJECT (self, "Right click released");

  surface = gtk_native_get_surface (GTK_NATIVE (self));
  sequence = gtk_gesture_single_get_current_sequence (GTK_GESTURE_SINGLE (click));
  event = gtk_gesture_get_last_event (GTK_GESTURE (click), sequence);

  if (G_UNLIKELY (event == NULL))
    return;

  if (!gdk_toplevel_show_window_menu (GDK_TOPLEVEL (surface), event))
    GST_FIXME_OBJECT (self, "Implement fallback context menu");

  gtk_gesture_set_state (GTK_GESTURE (click), GTK_EVENT_SEQUENCE_CLAIMED);
}

static void
drag_begin_cb (GtkGestureDrag *drag,
    gdouble start_x, gdouble start_y, ClapperAppWindow *self)
{
  GtkWidget *widget, *pickup;

  widget = gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (drag));
  pickup = gtk_widget_pick (widget, start_x, start_y, GTK_PICK_DEFAULT);

  /* We do not want to cause drag on list view as it has
   * a GtkDragSource controller which acts on delay */
  if (GTK_IS_LIST_VIEW (pickup) || gtk_widget_get_ancestor (pickup, GTK_TYPE_LIST_VIEW)) {
    gtk_gesture_set_state (GTK_GESTURE (drag), GTK_EVENT_SEQUENCE_DENIED);
    gtk_event_controller_reset (GTK_EVENT_CONTROLLER (drag));

    GST_DEBUG_OBJECT (self, "Window drag denied");
  }
}

static void
drag_update_cb (GtkGestureDrag *drag,
    gdouble offset_x, gdouble offset_y, ClapperAppWindow *self)
{
  GtkSettings *settings = gtk_widget_get_settings (GTK_WIDGET (self));
  gint drag_threshold = 8; // initially set to default

  g_object_get (settings, "gtk-dnd-drag-threshold", &drag_threshold, NULL);

  if (ABS (offset_x) > drag_threshold || ABS (offset_y) > drag_threshold) {
    GdkSurface *surface = gtk_native_get_surface (GTK_NATIVE (self));
    gdouble start_x = 0, start_y = 0, native_x = 0, native_y = 0;

    gtk_gesture_set_state (GTK_GESTURE (drag), GTK_EVENT_SEQUENCE_CLAIMED);
    gtk_gesture_drag_get_start_point (drag, &start_x, &start_y);

    gtk_native_get_surface_transform (GTK_NATIVE (self), &native_x, &native_y);

    gdk_toplevel_begin_move (GDK_TOPLEVEL (surface),
        gtk_gesture_get_device (GTK_GESTURE (drag)),
        GDK_BUTTON_PRIMARY,
        start_x + native_x,
        start_y + native_y,
        gtk_event_controller_get_current_event_time (GTK_EVENT_CONTROLLER (drag)));

    gtk_event_controller_reset (GTK_EVENT_CONTROLLER (drag));
  }
}

static inline void
_alter_volume (ClapperAppWindow *self, gdouble dy)
{
  ClapperPlayer *player = clapper_gtk_video_get_player (CLAPPER_GTK_VIDEO_CAST (self->video));
  gdouble volume = clapper_player_get_volume (player);

  /* We do not want for volume to change too suddenly */
  if (dy > 2.0)
    dy = 2.0;
  else if (dy < -2.0)
    dy = -2.0;

  volume -= dy * 0.02;

  /* Prevent going out of range and make it easier to set exactly 100% */
  if (volume > 2.0)
    volume = 2.0;
  else if (volume < 0.0)
    volume = 0.0;

  clapper_player_set_volume (player, PERCENTAGE_ROUND (volume));
}

static inline void
_alter_speed (ClapperAppWindow *self, gdouble dx)
{
  ClapperPlayer *player = clapper_gtk_video_get_player (CLAPPER_GTK_VIDEO_CAST (self->video));
  gdouble speed = clapper_player_get_speed (player);

  speed -= dx * 0.02;

  /* Prevent going out of range and make it easier to set exactly 1.0x */
  if (speed > 2.0)
    speed = 2.0;
  else if (speed < 0.05)
    speed = 0.05;

  clapper_player_set_speed (player, PERCENTAGE_ROUND (speed));
}

static gboolean
_begin_seek_operation (ClapperAppWindow *self)
{
  ClapperPlayer *player;
  ClapperQueue *queue;
  ClapperMediaItem *current_item;

  if (self->seeking)
    return FALSE;

  player = clapper_gtk_video_get_player (
      CLAPPER_GTK_VIDEO_CAST (self->video));
  queue = clapper_player_get_queue (player);
  current_item = clapper_queue_get_current_item (queue);

  self->current_duration = (current_item != NULL)
      ? clapper_media_item_get_duration (current_item)
      : 0;

  gst_clear_object (&current_item);

  /* Live content or not a video */
  if (self->current_duration == 0)
    return FALSE;

  if ((self->was_playing = (
      clapper_player_get_state (player) == CLAPPER_PLAYER_STATE_PLAYING)))
    clapper_player_pause (player);

  self->pending_position = clapper_player_get_position (player);
  self->seeking = TRUE;

  return TRUE;
}

static void
_end_seek_operation (ClapperAppWindow *self)
{
  if (self->seeking && self->current_duration > 0) {
    ClapperPlayer *player = clapper_gtk_video_get_player (
        CLAPPER_GTK_VIDEO_CAST (self->video));

    clapper_player_seek_custom (player, self->pending_position,
        g_settings_get_int (self->settings, "seek-method"));

    if (self->was_playing)
      clapper_player_play (player);
  }

  /* Reset */
  self->was_playing = FALSE;
  self->pending_position = 0;
  self->current_duration = 0;

  self->seeking = FALSE;
}

static void
_announce_current_seek_position (ClapperAppWindow *self, gboolean forward)
{
  gchar *position_str = g_strdup_printf (
      "%" CLAPPER_TIME_FORMAT " / %" CLAPPER_TIME_FORMAT,
      CLAPPER_TIME_ARGS (self->pending_position),
      CLAPPER_TIME_ARGS (self->current_duration));

  clapper_gtk_billboard_post_message (self->billboard,
      (forward) ? "media-seek-forward-symbolic" : "media-seek-backward-symbolic",
      position_str);

  g_free (position_str);
}

static inline void
_alter_position (ClapperAppWindow *self, gdouble dx)
{
  gboolean forward;

  /* This can only work on devices that
   * can detect scrolling begin and end */
  if (!self->scrolling
      || (!self->seeking && !_begin_seek_operation (self)))
    return;

  forward = (dx > 0);
  self->pending_position += dx;

  if (!forward) {
    if (self->pending_position < 0)
      self->pending_position = 0;
  } else {
    if (self->pending_position > self->current_duration)
      self->pending_position = self->current_duration;
  }

  _announce_current_seek_position (self, forward);
}

static void
scroll_begin_cb (GtkEventControllerScroll *scroll, ClapperAppWindow *self)
{
  GST_LOG_OBJECT (self, "Scroll begin");

  /* Assume that if device can begin, it can also end */
  self->scrolling = TRUE;
}

static gboolean
scroll_cb (GtkEventControllerScroll *scroll,
    gdouble dx, gdouble dy, ClapperAppWindow *self)
{
  GtkWidget *pickup;
  GdkDevice *device;
  gboolean handled;

  pickup = _pick_pointer_widget (self);

  /* We do not want to accidentally allow this controller to handle
   * scrolls when hovering over widgets that also handle scroll */
  while (pickup && !CLAPPER_GTK_IS_VIDEO (pickup)) {
    if (GTK_IS_SCROLLED_WINDOW (pickup) || GTK_IS_RANGE (pickup))
      return FALSE;

    pickup = gtk_widget_get_parent (pickup);
  }

  device = gtk_event_controller_get_current_event_device (GTK_EVENT_CONTROLLER (scroll));

  switch (gdk_device_get_source (device)) {
    case GDK_SOURCE_TOUCHPAD:
    case GDK_SOURCE_TOUCHSCREEN:
      dx *= 0.4;
      dy *= 0.4;
      break;
    default:
      break;
  }

  if ((handled = AXIS_WINS_OVER (dy, dx)))
    _alter_volume (self, dy);
  else if ((handled = AXIS_WINS_OVER (dx, dy)))
    _alter_position (self, dx);

  return handled;
}

static void
scroll_end_cb (GtkEventControllerScroll *scroll, ClapperAppWindow *self)
{
  GST_LOG_OBJECT (self, "Scroll end");

  self->scrolling = FALSE;

  if (self->seeking)
    _end_seek_operation (self);
}

static void
_handle_seek_key_press (ClapperAppWindow *self, gboolean forward)
{
  gint unit;
  gdouble offset;

  if (!self->seeking && !_begin_seek_operation (self))
    return;

  offset = (gdouble) g_settings_get_int (self->settings, "seek-value");
  unit = g_settings_get_int (self->settings, "seek-unit");

  switch (unit) {
    case CLAPPER_APP_SEEK_UNIT_SECOND:
      break;
    case CLAPPER_APP_SEEK_UNIT_MINUTE:
      offset *= 60;
      break;
    case CLAPPER_APP_SEEK_UNIT_PERCENTAGE:
      offset = (offset / 100.) * self->current_duration;
      break;
    default:
      g_assert_not_reached ();
      break;
  }

  forward ^= (gtk_widget_get_default_direction () == GTK_TEXT_DIR_RTL);

  if (forward)
    self->pending_position += offset;
  else
    self->pending_position -= offset;

  if (!forward) {
    if (self->pending_position < 0)
      self->pending_position = 0;
  } else {
    if (self->pending_position > self->current_duration)
      self->pending_position = self->current_duration;
  }

  _announce_current_seek_position (self, forward);
}

static void
_handle_chapter_key_press (ClapperAppWindow *self, gboolean forward)
{
  ClapperPlayer *player = clapper_gtk_video_get_player (
      CLAPPER_GTK_VIDEO_CAST (self->video));
  ClapperQueue *queue = clapper_player_get_queue (player);
  ClapperMediaItem *current_item = clapper_queue_get_current_item (queue);
  ClapperTimeline *timeline;
  ClapperMarker *dest_marker = NULL;
  gdouble position;
  guint i;
  gboolean is_rtl;

  if (!current_item)
    return;

  timeline = clapper_media_item_get_timeline (current_item);
  i = clapper_timeline_get_n_markers (timeline);

  /* No markers to iterate */
  if (i == 0) {
    gst_object_unref (current_item);
    return;
  }

  is_rtl = (gtk_widget_get_default_direction () == GTK_TEXT_DIR_RTL);
  forward ^= is_rtl;
  position = clapper_player_get_position (player);

  /* When going backwards give small tolerance, so we can
   * still go to previous one even when directly at/after marker */
  if (!forward)
    position -= 1.5;

  while (i--) {
    ClapperMarker *marker = clapper_timeline_get_marker (timeline, i);
    ClapperMarkerType marker_type = clapper_marker_get_marker_type (marker);
    gdouble start;
    gboolean found = FALSE;

    /* Ignore custom markers */
    if (marker_type >= CLAPPER_MARKER_TYPE_CUSTOM_1) {
      gst_object_unref (marker);
      continue;
    }

    start = clapper_marker_get_start (marker);
    found = (start <= position);

    if (found) {
      if (!forward)
        dest_marker = marker;
      else
        gst_object_unref (marker);

      break;
    }

    if (forward)
      gst_object_replace ((GstObject **) &dest_marker, GST_OBJECT_CAST (marker));

    gst_object_unref (marker);
  }

  if (dest_marker) {
    const gchar *title;
    gdouble start, duration;
    gchar *text;

    title = clapper_marker_get_title (dest_marker);
    start = clapper_marker_get_start (dest_marker);
    duration = clapper_media_item_get_duration (current_item);

    /* XXX: When RTL with mixed numbers and text, we have to
     * switch positions of start <-> duration ourselves */
    text = g_strdup_printf (
        "%s\n%" CLAPPER_TIME_FORMAT " / %" CLAPPER_TIME_FORMAT, title,
        CLAPPER_TIME_ARGS ((!is_rtl) ? start : duration),
        CLAPPER_TIME_ARGS ((!is_rtl) ? duration : start));

    clapper_gtk_billboard_post_message (self->billboard,
        "user-bookmarks-symbolic", text);
    clapper_player_seek (player, start);

    g_free (text);
    gst_object_unref (dest_marker);
  }

  gst_object_unref (current_item);
}

static void
_handle_item_key_press (ClapperAppWindow *self, gboolean forward)
{
  ClapperPlayer *player = clapper_gtk_video_get_player (
      CLAPPER_GTK_VIDEO_CAST (self->video));
  ClapperQueue *queue = clapper_player_get_queue (player);
  guint prev_index, index;

  forward ^= (gtk_widget_get_default_direction () == GTK_TEXT_DIR_RTL);

  prev_index = clapper_queue_get_current_index (queue);
  gtk_widget_activate_action (self->video,
      (forward) ? "video.next-item" : "video.previous-item", NULL);
  index = clapper_queue_get_current_index (queue);

  /* Notify only when changed */
  if (prev_index != index) {
    clapper_gtk_billboard_post_message (self->billboard,
        "applications-multimedia-symbolic",
        gtk_window_get_title (GTK_WINDOW (self)));
  }
}

static void
_handle_speed_key_press (ClapperAppWindow *self, gboolean forward)
{
  forward ^= (gtk_widget_get_default_direction () == GTK_TEXT_DIR_RTL);

  gtk_widget_activate_action (self->video,
      (forward) ? "video.speed-up" : "video.speed-down", NULL);
}

static inline void
_handle_progression_key_press (ClapperAppWindow *self)
{
  ClapperPlayer *player = clapper_gtk_video_get_player (
      CLAPPER_GTK_VIDEO_CAST (self->video));
  ClapperQueue *queue = clapper_player_get_queue (player);
  ClapperQueueProgressionMode mode;
  const gchar *icon = NULL, *label = NULL;

  mode = ((clapper_queue_get_progression_mode (queue) + 1) % N_PROGRESSION_MODES);

  clapper_app_utils_parse_progression (mode, &icon, &label);
  clapper_queue_set_progression_mode (queue, mode);

  clapper_gtk_billboard_post_message (self->billboard, icon, label);
}

static gboolean
key_pressed_cb (GtkEventControllerKey *controller, guint keyval,
    guint keycode, GdkModifierType state, ClapperAppWindow *self)
{
  switch (keyval) {
    case GDK_KEY_Up:
      if ((state & GDK_MODIFIER_MASK) == 0)
        gtk_widget_activate_action (self->video, "video.volume-up", NULL);
      break;
    case GDK_KEY_Down:
      if ((state & GDK_MODIFIER_MASK) == 0)
        gtk_widget_activate_action (self->video, "video.volume-down", NULL);
      break;
    case GDK_KEY_Left:
      if ((state & GDK_MODIFIER_MASK) == 0) {
        _handle_seek_key_press (self, FALSE);
      } else if (!self->key_held && (state & GDK_SHIFT_MASK) == GDK_SHIFT_MASK) {
        _handle_chapter_key_press (self, FALSE);
      } else if ((state & GDK_CONTROL_MASK) == GDK_CONTROL_MASK) {
        _handle_item_key_press (self, FALSE);
      }
      break;
    case GDK_KEY_j:
      if ((state & GDK_MODIFIER_MASK) == 0)
        _handle_seek_key_press (self, FALSE);
      break;
    case GDK_KEY_Right:
      if ((state & GDK_MODIFIER_MASK) == 0) {
        _handle_seek_key_press (self, TRUE);
      } else if (!self->key_held && (state & GDK_SHIFT_MASK) == GDK_SHIFT_MASK) {
        _handle_chapter_key_press (self, TRUE);
      } else if ((state & GDK_CONTROL_MASK) == GDK_CONTROL_MASK) {
        _handle_item_key_press (self, TRUE);
      }
      break;
    case GDK_KEY_l:
      if ((state & GDK_MODIFIER_MASK) == 0)
        _handle_seek_key_press (self, TRUE);
      break;
    case GDK_KEY_space:
    case GDK_KEY_k:
      if (!self->key_held && (state & GDK_MODIFIER_MASK) == 0)
        gtk_widget_activate_action (self->video, "video.toggle-play", NULL);
      break;
    case GDK_KEY_less:
      if (!self->key_held) // Needs seek (action is slow)
        _handle_speed_key_press (self, FALSE);
      break;
    case GDK_KEY_greater:
      if (!self->key_held) // Needs seek (action is slow)
        _handle_speed_key_press (self, TRUE);
      break;
    case GDK_KEY_m:
      if (!self->key_held && (state & GDK_MODIFIER_MASK) == 0)
        gtk_widget_activate_action (self->video, "video.toggle-mute", NULL);
      break;
    case GDK_KEY_p:
      if (!self->key_held && (state & GDK_MODIFIER_MASK) == 0)
        _handle_progression_key_press (self);
      break;
    default:
      return FALSE;
  }

  self->key_held = TRUE;

  return TRUE;
}

static void
key_released_cb (GtkEventControllerKey *controller, guint keyval,
    guint keycode, GdkModifierType state, ClapperAppWindow *self)
{
  switch (keyval) {
    case GDK_KEY_Left:
    case GDK_KEY_j:
    case GDK_KEY_Right:
    case GDK_KEY_l:
      _end_seek_operation (self);
      break;
    default:
      break;
  }

  self->key_held = FALSE;
}

static void
_seek_delay_cb (ClapperAppWindow *self)
{
  GST_LOG_OBJECT (self, "Delayed seek handler reached");
  self->seek_timeout = 0;

  if (self->seeking)
    _end_seek_operation (self);
}

static void
video_seek_request_cb (ClapperGtkVideo *video, gboolean forward, ClapperAppWindow *self)
{
  g_clear_handle_id (&self->seek_timeout, g_source_remove);

  _handle_seek_key_press (self, forward);

  self->seek_timeout = g_timeout_add_once (500, (GSourceOnceFunc) _seek_delay_cb, self);
}

static void
drop_value_notify_cb (GtkDropTarget *drop_target,
    GParamSpec *pspec G_GNUC_UNUSED, ClapperAppWindow *self)
{
  GtkWidget *stack;
  const GValue *value = gtk_drop_target_get_value (drop_target);

  if (!value) {
    clapper_gtk_billboard_unpin_pinned_message (self->billboard);
    return;
  }

  if (!clapper_app_utils_value_for_item_is_valid (value)) {
    gtk_drop_target_reject (drop_target);
    return;
  }

  stack = gtk_window_get_child (GTK_WINDOW (self));

  /* Do not pin message when still in initial state */
  if (gtk_stack_get_visible_child (GTK_STACK (stack)) == self->video) {
    clapper_gtk_billboard_pin_message (self->billboard,
        "insert-object-symbolic",
        _("Drop on title bar to play now or anywhere else to enqueue."));
  }
}

static gboolean
drop_cb (GtkDropTarget *drop_target, const GValue *value,
    gdouble x, gdouble y, ClapperAppWindow *self)
{
  GFile **files = NULL;
  gint n_files = 0;
  gboolean success = FALSE;

  if (clapper_app_utils_files_from_value (value, &files, &n_files)) {
    ClapperPlayer *player = clapper_app_window_get_player (self);
    ClapperQueue *queue = clapper_player_get_queue (player);
    gint i;

    clapper_app_window_ensure_no_initial_state (self);

    for (i = 0; i < n_files; ++i) {
      ClapperMediaItem *item = clapper_media_item_new_from_file (files[i]);

      clapper_queue_add_item (queue, item);
      gst_object_unref (item);
    }

    clapper_app_utils_files_free (files);
    success = TRUE;
  }

  return success;
}

static void
toggle_fullscreen (GSimpleAction *action, GVariant *param, gpointer user_data)
{
  ClapperAppWindow *self = CLAPPER_APP_WINDOW_CAST (user_data);

  video_toggle_fullscreen_cb (CLAPPER_GTK_VIDEO_CAST (self->video), self);
}

static void
show_help_overlay (GSimpleAction *action, GVariant *param, gpointer user_data)
{
  ClapperAppWindow *self = CLAPPER_APP_WINDOW_CAST (user_data);
  GtkBuilder *builder;
  GtkWidget *help_overlay;

  builder = gtk_builder_new_from_resource (
      CLAPPER_APP_RESOURCE_PREFIX "/ui/clapper-app-help-overlay.ui");
  help_overlay = GTK_WIDGET (gtk_builder_get_object (builder, "help_overlay"));

  gtk_window_set_transient_for (GTK_WINDOW (help_overlay), GTK_WINDOW (self));
  gtk_window_present (GTK_WINDOW (help_overlay));

  g_object_unref (builder);
}

GtkWidget *
clapper_app_window_new (GtkApplication *application)
{
  return g_object_new (CLAPPER_APP_TYPE_WINDOW,
      "application", application,
      NULL);
}

GtkWidget *
clapper_app_window_get_video (ClapperAppWindow *self)
{
  return self->video;
}

ClapperPlayer *
clapper_app_window_get_player (ClapperAppWindow *self)
{
  return clapper_gtk_video_get_player (CLAPPER_GTK_VIDEO_CAST (self->video));
}

void
clapper_app_window_ensure_no_initial_state (ClapperAppWindow *self)
{
  GtkWidget *stack = gtk_window_get_child (GTK_WINDOW (self));
  const gchar *child_name = gtk_stack_get_visible_child_name (GTK_STACK (stack));

  if (g_strcmp0 (child_name, "initial_state") == 0)
    gtk_stack_set_visible_child (GTK_STACK (stack), self->video);
}

static gboolean
clapper_app_window_close_request (GtkWindow *window)
{
  ClapperAppWindow *self = CLAPPER_APP_WINDOW_CAST (window);
/* FIXME: Have GSettings again to store these values
  GSettings *settings = g_settings_new (CLAPPER_APP_ID);
  gint width = DEFAULT_WINDOW_WIDTH, height = DEFAULT_WINDOW_HEIGHT;
*/
  GST_DEBUG_OBJECT (self, "Close request");
/*
  gtk_window_get_default_size (window, &width, &height);

  g_settings_set (settings, "window-size", "(ii)", width, height);
  g_settings_set_boolean (settings, "maximized", gtk_window_is_maximized (window));
  g_settings_set_boolean (settings, "fullscreen", gtk_window_is_fullscreen (window));

  g_object_unref (settings);
*/

  return GTK_WINDOW_CLASS (parent_class)->close_request (window);
}

static void
clapper_app_window_realize (GtkWidget *widget)
{
  ClapperAppWindow *self = CLAPPER_APP_WINDOW_CAST (widget);

  GST_TRACE_OBJECT (self, "Realize");

  GTK_WIDGET_CLASS (parent_class)->realize (widget);

  gtk_style_context_add_provider_for_display (gtk_widget_get_display (widget),
      (GtkStyleProvider *) self->provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
}

static void
clapper_app_window_unrealize (GtkWidget *widget)
{
  ClapperAppWindow *self = CLAPPER_APP_WINDOW_CAST (widget);

  GST_TRACE_OBJECT (self, "Unrealize");

  gtk_style_context_remove_provider_for_display (gtk_widget_get_display (widget),
      (GtkStyleProvider *) self->provider);

  GTK_WIDGET_CLASS (parent_class)->unrealize (widget);
}

static void
clapper_app_window_init (ClapperAppWindow *self)
{
  GtkSettings *settings;
  GtkWidget *dummy_titlebar;
  gint distance = 0;

  gtk_widget_init_template (GTK_WIDGET (self));

  /* Make double tap easier to perform */
  settings = gtk_widget_get_settings (self->video);
  g_object_get (settings, "gtk-double-click-distance", &distance, NULL);
  g_object_set (settings, "gtk-double-click-distance", MAX (distance, 32), NULL);

  dummy_titlebar = g_object_new (GTK_TYPE_BOX,
      "can_focus", FALSE,
      "focusable", FALSE,
      "visible", FALSE,
      NULL);
  gtk_window_set_titlebar (GTK_WINDOW (self), dummy_titlebar);
  gtk_window_set_title (GTK_WINDOW (self), CLAPPER_APP_NAME);

  /* Prevent GTK from redrawing background for each frame */
  gtk_widget_remove_css_class (GTK_WIDGET (self), "background");

  gtk_drop_target_set_gtypes (self->drop_target,
      (GType[3]) { GDK_TYPE_FILE_LIST, G_TYPE_FILE, G_TYPE_STRING }, 3);
}

static void
clapper_app_window_constructed (GObject *object)
{
  ClapperAppWindow *self = CLAPPER_APP_WINDOW_CAST (object);
  ClapperPlayer *player = clapper_app_window_get_player (self);
  ClapperQueue *queue = clapper_player_get_queue (player);
  ClapperGtkExtraMenuButton *button;
  AdwStyleManager *manager;

  static const GActionEntry win_entries[] = {
    { "toggle-fullscreen", toggle_fullscreen, NULL, NULL, NULL },
    { "show-help-overlay", show_help_overlay, NULL, NULL, NULL },
  };

#if (CLAPPER_HAVE_MPRIS || CLAPPER_HAVE_SERVER || CLAPPER_HAVE_DISCOVERER)
  ClapperFeature *feature = NULL;
#endif

  self->settings = g_settings_new (CLAPPER_APP_ID);
  self->last_volume = PERCENTAGE_ROUND (g_settings_get_double (self->settings, "volume"));

#if CLAPPER_HAVE_MPRIS
  feature = CLAPPER_FEATURE (clapper_mpris_new (
      "org.mpris.MediaPlayer2.Clapper",
      "Clapper", CLAPPER_APP_ID));
  clapper_mpris_set_queue_controllable (CLAPPER_MPRIS (feature), TRUE);
  clapper_player_add_feature (player, feature);
  gst_object_unref (feature);
#endif

#if CLAPPER_HAVE_SERVER
  feature = CLAPPER_FEATURE (clapper_server_new ());
  clapper_server_set_queue_controllable (CLAPPER_SERVER (feature), TRUE);
  g_settings_bind (self->settings, "server-enabled",
      feature, "enabled", G_SETTINGS_BIND_GET);
  clapper_player_add_feature (player, feature);
  gst_object_unref (feature);
#endif

#if CLAPPER_HAVE_DISCOVERER
  feature = CLAPPER_FEATURE (clapper_discoverer_new ());
  clapper_player_add_feature (player, feature);
  gst_object_unref (feature);
#endif

  /* FIXME: Allow setting sink/filter elements from prefs window
   * (this should include parsing bin descriptions) */

  clapper_player_set_autoplay (player, TRUE);

  /* No need to also call this here, as item is selected
   * after application window is contructed */
  g_signal_connect (queue, "notify::current-item",
      G_CALLBACK (_queue_current_item_changed_cb), self);

  g_settings_bind (self->settings, "audio-offset",
      player, "audio-offset", G_SETTINGS_BIND_GET);
  g_settings_bind (self->settings, "subtitle-offset",
      player, "subtitle-offset", G_SETTINGS_BIND_GET);
  g_settings_bind (self->settings, "subtitle-font-desc",
      player, "subtitle-font-desc", G_SETTINGS_BIND_GET);

  button = clapper_gtk_simple_controls_get_extra_menu_button (
      self->simple_controls);

  g_settings_bind_with_mapping (self->settings, "seek-method",
      self->simple_controls, "seek-method", G_SETTINGS_BIND_GET,
      (GSettingsBindGetMapping) _get_seek_method_mapping,
      NULL, NULL, NULL);
  g_signal_connect (button, "open-subtitles",
      G_CALLBACK (_open_subtitles_cb), self);
  clapper_gtk_extra_menu_button_set_can_open_subtitles (button, TRUE);

  manager = adw_style_manager_get_default ();
  adw_style_manager_set_color_scheme (manager, ADW_COLOR_SCHEME_FORCE_DARK);

  self->provider = gtk_css_provider_new ();
  gtk_css_provider_load_from_resource (self->provider,
      CLAPPER_APP_RESOURCE_PREFIX "/css/styles.css");

  g_action_map_add_action_entries (G_ACTION_MAP (self),
      win_entries, G_N_ELEMENTS (win_entries), self);

  G_OBJECT_CLASS (parent_class)->constructed (object);
}

static void
clapper_app_window_dispose (GObject *object)
{
  ClapperAppWindow *self = CLAPPER_APP_WINDOW_CAST (object);

  g_clear_handle_id (&self->seek_timeout, g_source_remove);

  gtk_widget_dispose_template (GTK_WIDGET (object), CLAPPER_APP_TYPE_WINDOW);

  gst_clear_object (&self->current_item);

  G_OBJECT_CLASS (parent_class)->dispose (object);
}

static void
clapper_app_window_finalize (GObject *object)
{
  ClapperAppWindow *self = CLAPPER_APP_WINDOW_CAST (object);

  GST_TRACE_OBJECT (self, "Finalize");

  g_object_unref (self->settings);
  g_object_unref (self->provider);

  G_OBJECT_CLASS (parent_class)->finalize (object);
}

static void
clapper_app_window_class_init (ClapperAppWindowClass *klass)
{
  GObjectClass *gobject_class = (GObjectClass *) klass;
  GtkWidgetClass *widget_class = (GtkWidgetClass *) klass;
  GtkWindowClass *window_class = (GtkWindowClass *) klass;

  GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "clapperappwindow", 0,
      "Clapper App Window");

  gobject_class->constructed = clapper_app_window_constructed;
  gobject_class->dispose = clapper_app_window_dispose;
  gobject_class->finalize = clapper_app_window_finalize;

  widget_class->realize = clapper_app_window_realize;
  widget_class->unrealize = clapper_app_window_unrealize;

  window_class->close_request = clapper_app_window_close_request;

  gtk_widget_class_set_template_from_resource (widget_class,
      CLAPPER_APP_RESOURCE_PREFIX "/ui/clapper-app-window.ui");

  gtk_widget_class_bind_template_child (widget_class, ClapperAppWindow, video);
  gtk_widget_class_bind_template_child (widget_class, ClapperAppWindow, billboard);
  gtk_widget_class_bind_template_child (widget_class, ClapperAppWindow, simple_controls);
  gtk_widget_class_bind_template_child (widget_class, ClapperAppWindow, drop_target);

  gtk_widget_class_bind_template_callback (widget_class, video_toggle_fullscreen_cb);
  gtk_widget_class_bind_template_callback (widget_class, video_seek_request_cb);
  gtk_widget_class_bind_template_callback (widget_class, video_map_cb);
  gtk_widget_class_bind_template_callback (widget_class, video_unmap_cb);
  gtk_widget_class_bind_template_callback (widget_class, scroll_begin_cb);
  gtk_widget_class_bind_template_callback (widget_class, scroll_cb);
  gtk_widget_class_bind_template_callback (widget_class, scroll_end_cb);
  gtk_widget_class_bind_template_callback (widget_class, key_pressed_cb);
  gtk_widget_class_bind_template_callback (widget_class, key_released_cb);

  gtk_widget_class_bind_template_callback (widget_class, right_click_pressed_cb);
  gtk_widget_class_bind_template_callback (widget_class, right_click_released_cb);
  gtk_widget_class_bind_template_callback (widget_class, drag_begin_cb);
  gtk_widget_class_bind_template_callback (widget_class, drag_update_cb);

  gtk_widget_class_bind_template_callback (widget_class, drop_value_notify_cb);
  gtk_widget_class_bind_template_callback (widget_class, drop_cb);
}
