/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */

/*
 * Copyright (C) 2001 Havoc Pennington
 * Copyright (C) 2002, 2003, 2004 Red Hat, Inc.
 * Copyright (C) 2003, 2004 Rob Adams
 * Copyright (C) 2004-2006 Elijah Newren
 *
 * 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 "core/events.h"

#include "backends/meta-cursor-tracker-private.h"
#include "backends/meta-idle-manager.h"
#include "backends/x11/meta-backend-x11.h"
#include "backends/x11/meta-input-device-x11.h"
#include "compositor/meta-window-actor-private.h"
#include "core/display-private.h"
#include "core/window-private.h"
#include "meta/meta-backend.h"

#ifdef HAVE_NATIVE_BACKEND
#include "backends/native/meta-backend-native.h"
#endif

#ifdef HAVE_WAYLAND
#include "wayland/meta-wayland-private.h"
#endif

#define IS_GESTURE_EVENT(e) ((e)->type == CLUTTER_TOUCHPAD_SWIPE || \
                             (e)->type == CLUTTER_TOUCHPAD_PINCH || \
                             (e)->type == CLUTTER_TOUCH_BEGIN || \
                             (e)->type == CLUTTER_TOUCH_UPDATE || \
                             (e)->type == CLUTTER_TOUCH_END || \
                             (e)->type == CLUTTER_TOUCH_CANCEL)

#define IS_KEY_EVENT(e) ((e)->type == CLUTTER_KEY_PRESS || \
                         (e)->type == CLUTTER_KEY_RELEASE)

typedef enum
{
  EVENTS_UNFREEZE_SYNC,
  EVENTS_UNFREEZE_REPLAY,
} EventsUnfreezeMethod;

static gboolean
stage_has_key_focus (void)
{
  MetaBackend *backend = meta_get_backend ();
  ClutterActor *stage = meta_backend_get_stage (backend);

  return clutter_stage_get_key_focus (CLUTTER_STAGE (stage)) == stage;
}

static MetaWindow *
get_window_for_event (MetaDisplay        *display,
                      const ClutterEvent *event)
{
  switch (display->event_route)
    {
    case META_EVENT_ROUTE_NORMAL:
      {
        ClutterActor *source;
        MetaWindowActor *window_actor;

        /* Always use the key focused window for key events. */
        if (IS_KEY_EVENT (event))
            return stage_has_key_focus () ? display->focus_window : NULL;

        source = clutter_event_get_source (event);
        window_actor = meta_window_actor_from_actor (source);
        if (window_actor)
          return meta_window_actor_get_meta_window (window_actor);
        else
          return NULL;
      }
    case META_EVENT_ROUTE_WINDOW_OP:
    case META_EVENT_ROUTE_COMPOSITOR_GRAB:
    case META_EVENT_ROUTE_WAYLAND_POPUP:
    case META_EVENT_ROUTE_FRAME_BUTTON:
      return display->grab_window;
    default:
      g_assert_not_reached ();
      return NULL;
    }
}

static void
handle_idletime_for_event (const ClutterEvent *event)
{
  MetaBackend *backend = meta_get_backend ();
  MetaIdleManager *idle_manager;

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

  if (event->any.flags & CLUTTER_EVENT_FLAG_SYNTHETIC ||
      event->type == CLUTTER_ENTER ||
      event->type == CLUTTER_LEAVE)
    return;

  idle_manager = meta_backend_get_idle_manager (backend);
  meta_idle_manager_reset_idle_time (idle_manager);
}

static gboolean
sequence_is_pointer_emulated (MetaDisplay        *display,
                              const ClutterEvent *event)
{
  ClutterEventSequence *sequence;

  sequence = clutter_event_get_event_sequence (event);

  if (!sequence)
    return FALSE;

  if (clutter_event_is_pointer_emulated (event))
    return TRUE;

#ifdef HAVE_NATIVE_BACKEND
  MetaBackend *backend = meta_get_backend ();

  /* When using Clutter's native input backend there is no concept of
   * pointer emulating sequence, we still must make up our own to be
   * able to implement single-touch (hence pointer alike) behavior.
   *
   * This is implemented similarly to X11, where only the first touch
   * on screen gets the "pointer emulated" flag, and it won't get assigned
   * to another sequence until the next first touch on an idle touchscreen.
   */
  if (META_IS_BACKEND_NATIVE (backend))
    {
      MetaGestureTracker *tracker;

      tracker = meta_display_get_gesture_tracker (display);

      if (event->type == CLUTTER_TOUCH_BEGIN &&
          meta_gesture_tracker_get_n_current_touches (tracker) == 0)
        return TRUE;
    }
#endif /* HAVE_NATIVE_BACKEND */

  return FALSE;
}

static void
maybe_unfreeze_pointer_events (MetaBackend          *backend,
                               const ClutterEvent   *event,
                               EventsUnfreezeMethod  unfreeze_method)
{
  ClutterInputDevice *device;
  Display *xdisplay;
  int event_mode;
  int device_id;

  if (event->type != CLUTTER_BUTTON_PRESS)
    return;

  if (!META_IS_BACKEND_X11 (backend))
    return;

  device = clutter_event_get_device (event);
  device_id = meta_input_device_x11_get_device_id (device);
  switch (unfreeze_method)
    {
    case EVENTS_UNFREEZE_SYNC:
      event_mode = XISyncDevice;
      meta_verbose ("Syncing events time %u device %i",
                    (unsigned int) event->button.time, device_id);
      break;
    case EVENTS_UNFREEZE_REPLAY:
      event_mode = XIReplayDevice;
      meta_verbose ("Replaying events time %u device %i",
                    (unsigned int) event->button.time, device_id);
      break;
    default:
      g_assert_not_reached ();
      return;
    }

  xdisplay = meta_backend_x11_get_xdisplay (META_BACKEND_X11 (backend));
  XIAllowEvents (xdisplay, device_id, event_mode, event->button.time);
}

static gboolean
meta_display_handle_event (MetaDisplay        *display,
                           const ClutterEvent *event)
{
  MetaBackend *backend = meta_get_backend ();
  MetaWindow *window;
  gboolean bypass_clutter = FALSE;
  G_GNUC_UNUSED gboolean bypass_wayland = FALSE;
  MetaGestureTracker *gesture_tracker;
  ClutterEventSequence *sequence;

  sequence = clutter_event_get_event_sequence (event);

  /* Set the pointer emulating sequence on touch begin, if eligible */
  if (event->type == CLUTTER_TOUCH_BEGIN)
    {
      if (sequence_is_pointer_emulated (display, event))
        {
          /* This is the new pointer emulating sequence */
          display->pointer_emulating_sequence = sequence;
        }
      else if (display->pointer_emulating_sequence == sequence)
        {
          /* This sequence was "pointer emulating" in a prior incarnation,
           * but now it isn't. We unset the pointer emulating sequence at
           * this point so the current sequence is not mistaken as pointer
           * emulating, while we've ensured that it's been deemed
           * "pointer emulating" throughout all of the event processing
           * of the previous incarnation.
           */
          display->pointer_emulating_sequence = NULL;
        }
    }

#ifdef HAVE_WAYLAND
  MetaWaylandCompositor *compositor = NULL;
  if (meta_is_wayland_compositor ())
    {
      compositor = meta_wayland_compositor_get_default ();
      meta_wayland_compositor_update (compositor, event);
    }
#endif

  if (event->type == CLUTTER_PAD_BUTTON_PRESS ||
      event->type == CLUTTER_PAD_BUTTON_RELEASE ||
      event->type == CLUTTER_PAD_RING ||
      event->type == CLUTTER_PAD_STRIP)
    {
      gboolean handle_pad_event;
      gboolean is_mode_switch = FALSE;

      if (event->type == CLUTTER_PAD_BUTTON_PRESS ||
          event->type == CLUTTER_PAD_BUTTON_RELEASE)
        {
          ClutterInputDevice *pad;
          uint32_t button;

          pad = clutter_event_get_source_device (event);
          button = clutter_event_get_button (event);

          is_mode_switch =
            clutter_input_device_get_mode_switch_button_group (pad, button) >= 0;
        }

      handle_pad_event = !display->current_pad_osd || is_mode_switch;

      if (handle_pad_event &&
          meta_pad_action_mapper_handle_event (display->pad_action_mapper, event))
        {
          bypass_wayland = bypass_clutter = TRUE;
          goto out;
        }
    }

  if (event->type != CLUTTER_DEVICE_ADDED &&
      event->type != CLUTTER_DEVICE_REMOVED)
    {
      ClutterInputDevice *source;

      handle_idletime_for_event (event);
      source = clutter_event_get_source_device (event);

      if (source)
        meta_backend_update_last_device (backend, source);
    }

#ifdef HAVE_WAYLAND
  if (meta_is_wayland_compositor () && event->type == CLUTTER_MOTION)
    {
      MetaCursorRenderer *cursor_renderer;
      ClutterInputDevice *device;

      device = clutter_event_get_device (event);
      cursor_renderer = meta_backend_get_cursor_renderer_for_device (backend,
                                                                     device);
      if (cursor_renderer)
        meta_cursor_renderer_update_position (cursor_renderer);

      if (device == clutter_seat_get_pointer (clutter_input_device_get_seat (device)))
        {
          MetaCursorTracker *cursor_tracker =
            meta_backend_get_cursor_tracker (backend);

          meta_cursor_tracker_invalidate_position (cursor_tracker);
        }
    }
#endif

  window = get_window_for_event (display, event);

  display->current_time = event->any.time;

  if (window && !window->override_redirect &&
      (event->type == CLUTTER_KEY_PRESS ||
       event->type == CLUTTER_BUTTON_PRESS ||
       event->type == CLUTTER_TOUCH_BEGIN))
    {
      if (META_CURRENT_TIME == display->current_time)
        {
          /* We can't use missing (i.e. invalid) timestamps to set user time,
           * nor do we want to use them to sanity check other timestamps.
           * See bug 313490 for more details.
           */
          meta_warning ("Event has no timestamp! You may be using a broken "
                        "program such as xse.  Please ask the authors of that "
                        "program to fix it.");
        }
      else
        {
          meta_window_set_user_time (window, display->current_time);
          meta_display_sanity_check_timestamps (display, display->current_time);
        }
    }

  gesture_tracker = meta_display_get_gesture_tracker (display);

  if (meta_gesture_tracker_handle_event (gesture_tracker, event))
    {
      bypass_wayland = bypass_clutter = TRUE;
      goto out;
    }

  if (display->event_route == META_EVENT_ROUTE_WINDOW_OP)
    {
      if (meta_window_handle_mouse_grab_op_event (window, event))
        {
          bypass_clutter = TRUE;
          bypass_wayland = TRUE;
          goto out;
        }
    }

  /* For key events, it's important to enforce single-handling, or
   * we can get into a confused state. So if a keybinding is
   * handled (because it's one of our hot-keys, or because we are
   * in a keyboard-grabbed mode like moving a window, we don't
   * want to pass the key event to the compositor or Wayland at all.
   */
  if (meta_keybindings_process_event (display, window, event))
    {
      bypass_clutter = TRUE;
      bypass_wayland = TRUE;
      goto out;
    }

  /* Do not pass keyboard events to Wayland if key focus is not on the
   * stage in normal mode (e.g. during keynav in the panel)
   */
  if (display->event_route == META_EVENT_ROUTE_NORMAL)
    {
      if (IS_KEY_EVENT (event) && !stage_has_key_focus ())
        {
          bypass_wayland = TRUE;
          goto out;
        }
    }

  if (meta_is_wayland_compositor () &&
      event->type == CLUTTER_SCROLL &&
      meta_prefs_get_mouse_button_mods () > 0)
    {
      ClutterModifierType grab_mods;

      grab_mods = meta_display_get_compositor_modifiers (display);
      if ((clutter_event_get_state (event) & grab_mods) != 0)
        {
          bypass_wayland = TRUE;
          goto out;
        }
    }

  if (display->current_pad_osd)
    {
      bypass_wayland = TRUE;
      goto out;
    }

  if (window)
    {
      /* Events that are likely to trigger compositor gestures should
       * be known to clutter so they can propagate along the hierarchy.
       * Gesture-wise, there's two groups of events we should be getting
       * here:
       * - CLUTTER_TOUCH_* with a touch sequence that's not yet accepted
       *   by the gesture tracker, these might trigger gesture actions
       *   into recognition. Already accepted touch sequences are handled
       *   directly by meta_gesture_tracker_handle_event().
       * - CLUTTER_TOUCHPAD_* events over windows. These can likewise
       *   trigger ::captured-event handlers along the way.
       */
      bypass_clutter = !IS_GESTURE_EVENT (event);

      /* When double clicking to un-maximize an X11 window under Wayland,
       * there is a race between X11 and Wayland protocols and the X11
       * XConfigureWindow may be processed by Xwayland before the button
       * press event is forwarded via the Wayland protocol.
       * As a result, the second click may reach another X11 window placed
       * immediately underneath in the X11 stack.
       * The following is to make sure we do not forward the button press
       * event to Wayland if it was handled by the frame UI.
       * See: https://gitlab.gnome.org/GNOME/mutter/issues/88
       */
      if (meta_window_handle_ui_frame_event (window, event))
        bypass_wayland = (event->type == CLUTTER_BUTTON_PRESS ||
                          event->type == CLUTTER_TOUCH_BEGIN);
      else
        meta_window_handle_ungrabbed_event (window, event);

      /* This might start a grab op. If it does, then filter out the
       * event, and if it doesn't, replay the event to release our
       * own sync grab. */

      if (display->event_route == META_EVENT_ROUTE_WINDOW_OP ||
          display->event_route == META_EVENT_ROUTE_FRAME_BUTTON)
        {
          bypass_clutter = TRUE;
          bypass_wayland = TRUE;
        }
      else
        {
          /* Only replay button press events, since that's where we
           * have the synchronous grab. */
          maybe_unfreeze_pointer_events (backend, event, EVENTS_UNFREEZE_REPLAY);

          /* If the focus window has an active close dialog let clutter
           * events go through, so fancy clutter dialogs can get to handle
           * all events.
           */
          if (window->close_dialog &&
              meta_close_dialog_is_visible (window->close_dialog))
            {
              bypass_wayland = TRUE;
              bypass_clutter = FALSE;
            }
        }

      goto out;
    }
  else
    {
      /* We could not match the event with a window, make sure we sync
       * the pointer to discard the sequence and don't keep events frozen.
       */
       maybe_unfreeze_pointer_events (backend, event, EVENTS_UNFREEZE_SYNC);
    }

 out:
  /* If the compositor has a grab, don't pass that through to Wayland */
  if (display->event_route == META_EVENT_ROUTE_COMPOSITOR_GRAB)
    bypass_wayland = TRUE;

  /* If a Wayland client has a grab, don't pass that through to Clutter */
  if (display->event_route == META_EVENT_ROUTE_WAYLAND_POPUP)
    bypass_clutter = TRUE;

#ifdef HAVE_WAYLAND
  if (compositor && !bypass_wayland)
    {
      if (meta_wayland_compositor_handle_event (compositor, event))
        bypass_clutter = TRUE;
    }
#endif

  display->current_time = META_CURRENT_TIME;
  return bypass_clutter;
}

static gboolean
event_callback (const ClutterEvent *event,
                gpointer            data)
{
  MetaDisplay *display = data;

  return meta_display_handle_event (display, event);
}

void
meta_display_init_events (MetaDisplay *display)
{
  display->clutter_event_filter = clutter_event_add_filter (NULL,
                                                            event_callback,
                                                            NULL,
                                                            display);
}

void
meta_display_free_events (MetaDisplay *display)
{
  clutter_event_remove_filter (display->clutter_event_filter);
  display->clutter_event_filter = 0;
}
