/*
 * Copyright (C) 2017, 2018 Red Hat
 *
 * 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, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
 * 02111-1307, USA.
 *
 * Author: Carlos Garnacho <carlosg@gnome.org>
 */

#include "config.h"

#include <wayland-server.h>

#include "wayland/meta-wayland-private.h"
#include "wayland/meta-wayland-seat.h"
#include "wayland/meta-wayland-text-input-legacy.h"
#include "wayland/meta-wayland-versions.h"

#include "gtk-text-input-server-protocol.h"

#define META_TYPE_WAYLAND_GTK_TEXT_INPUT_FOCUS (meta_wayland_gtk_text_input_focus_get_type ())

typedef enum
{
  META_WAYLAND_PENDING_STATE_NONE             = 0,
  META_WAYLAND_PENDING_STATE_INPUT_RECT       = 1 << 0,
  META_WAYLAND_PENDING_STATE_CONTENT_TYPE     = 1 << 1,
  META_WAYLAND_PENDING_STATE_SURROUNDING_TEXT = 1 << 2,
} MetaWaylandTextInputPendingState;

typedef struct _MetaWaylandGtkTextInput MetaWaylandGtkTextInput;

struct _MetaWaylandGtkTextInput
{
  MetaWaylandSeat *seat;
  ClutterInputFocus *input_focus;

  struct wl_list resource_list;
  struct wl_list focus_resource_list;
  MetaWaylandSurface *surface;
  struct wl_listener surface_listener;
  uint32_t focus_serial;

  MetaWaylandTextInputPendingState pending_state;

  struct
  {
    char *text;
    uint32_t cursor;
    uint32_t anchor;
  } surrounding;

  cairo_rectangle_int_t cursor_rect;

  uint32_t content_type_hint;
  uint32_t content_type_purpose;
};

struct _MetaWaylandGtkTextInputFocus
{
  ClutterInputFocus parent_instance;
  MetaWaylandGtkTextInput *text_input;
};

G_DECLARE_FINAL_TYPE (MetaWaylandGtkTextInputFocus,
                      meta_wayland_gtk_text_input_focus,
                      META, WAYLAND_GTK_TEXT_INPUT_FOCUS, ClutterInputFocus)
G_DEFINE_TYPE (MetaWaylandGtkTextInputFocus, meta_wayland_gtk_text_input_focus,
               CLUTTER_TYPE_INPUT_FOCUS)

static void
meta_wayland_text_input_focus_request_surrounding (ClutterInputFocus *focus)
{
  MetaWaylandGtkTextInput *text_input;

  text_input = META_WAYLAND_GTK_TEXT_INPUT_FOCUS (focus)->text_input;
  clutter_input_focus_set_surrounding (focus,
				       text_input->surrounding.text,
				       text_input->surrounding.cursor,
                                       text_input->surrounding.anchor);
}

static void
meta_wayland_text_input_focus_delete_surrounding (ClutterInputFocus *focus,
                                                  int                offset,
                                                  guint              len)
{
  MetaWaylandGtkTextInput *text_input;
  uint32_t before_length;
  uint32_t after_length;
  struct wl_resource *resource;

  text_input = META_WAYLAND_GTK_TEXT_INPUT_FOCUS (focus)->text_input;
  before_length = ABS (MIN (offset, 0));
  after_length = MAX (0, offset + len);
  g_warn_if_fail (ABS (offset) <= len);

  wl_resource_for_each (resource, &text_input->focus_resource_list)
    {
      gtk_text_input_send_delete_surrounding_text (resource,
                                                   before_length,
                                                   after_length);
    }
}

static void
meta_wayland_text_input_focus_commit_text (ClutterInputFocus *focus,
                                           const gchar       *text)
{
  MetaWaylandGtkTextInput *text_input;
  struct wl_resource *resource;

  text_input = META_WAYLAND_GTK_TEXT_INPUT_FOCUS (focus)->text_input;

  wl_resource_for_each (resource, &text_input->focus_resource_list)
    {
      gtk_text_input_send_preedit_string (resource, NULL, 0);
      gtk_text_input_send_commit_string (resource, text);
    }
}

static void
meta_wayland_text_input_focus_set_preedit_text (ClutterInputFocus *focus,
                                                const gchar       *text,
                                                guint              cursor)
{
  MetaWaylandGtkTextInput *text_input;
  struct wl_resource *resource;

  text_input = META_WAYLAND_GTK_TEXT_INPUT_FOCUS (focus)->text_input;

  wl_resource_for_each (resource, &text_input->focus_resource_list)
    {
      gtk_text_input_send_preedit_string (resource, text, cursor);
    }
}

static void
meta_wayland_gtk_text_input_focus_class_init (MetaWaylandGtkTextInputFocusClass *klass)
{
  ClutterInputFocusClass *focus_class = CLUTTER_INPUT_FOCUS_CLASS (klass);

  focus_class->request_surrounding = meta_wayland_text_input_focus_request_surrounding;
  focus_class->delete_surrounding = meta_wayland_text_input_focus_delete_surrounding;
  focus_class->commit_text = meta_wayland_text_input_focus_commit_text;
  focus_class->set_preedit_text = meta_wayland_text_input_focus_set_preedit_text;
}

static void
meta_wayland_gtk_text_input_focus_init (MetaWaylandGtkTextInputFocus *focus)
{
}

static ClutterInputFocus *
meta_wayland_text_input_focus_new (MetaWaylandGtkTextInput *text_input)
{
  MetaWaylandGtkTextInputFocus *focus;

  focus = g_object_new (META_TYPE_WAYLAND_GTK_TEXT_INPUT_FOCUS, NULL);
  focus->text_input = text_input;

  return CLUTTER_INPUT_FOCUS (focus);
}

static void
text_input_handle_focus_surface_destroy (struct wl_listener *listener,
					 void               *data)
{
  MetaWaylandGtkTextInput *text_input = wl_container_of (listener, text_input,
                                                         surface_listener);

  meta_wayland_gtk_text_input_set_focus (text_input, NULL);
}

static void
move_resources (struct wl_list *destination, struct wl_list *source)
{
  wl_list_insert_list (destination, source);
  wl_list_init (source);
}

static void
move_resources_for_client (struct wl_list *destination,
			   struct wl_list *source,
			   struct wl_client *client)
{
  struct wl_resource *resource, *tmp;
  wl_resource_for_each_safe (resource, tmp, source)
    {
      if (wl_resource_get_client (resource) == client)
        {
          wl_list_remove (wl_resource_get_link (resource));
          wl_list_insert (destination, wl_resource_get_link (resource));
        }
    }
}

void
meta_wayland_gtk_text_input_set_focus (MetaWaylandGtkTextInput *text_input,
                                       MetaWaylandSurface      *surface)
{
  if (text_input->surface == surface)
    return;

  text_input->pending_state = META_WAYLAND_PENDING_STATE_NONE;

  if (text_input->surface)
    {
      if (!wl_list_empty (&text_input->focus_resource_list))
        {
          ClutterInputFocus *focus = text_input->input_focus;
          ClutterInputMethod *input_method;
          struct wl_resource *resource;
          uint32_t serial;

          if (clutter_input_focus_is_focused (focus))
            {
              input_method = clutter_backend_get_input_method (clutter_get_default_backend ());
              clutter_input_method_focus_out (input_method);
            }

          serial = wl_display_next_serial (text_input->seat->wl_display);

          wl_resource_for_each (resource, &text_input->focus_resource_list)
            {
              gtk_text_input_send_leave (resource, serial,
                                         text_input->surface->resource);
            }

          move_resources (&text_input->resource_list,
                          &text_input->focus_resource_list);
        }

      wl_list_remove (&text_input->surface_listener.link);
      text_input->surface = NULL;
    }

  if (surface)
    {
      struct wl_resource *focus_surface_resource;

      text_input->surface = surface;
      focus_surface_resource = text_input->surface->resource;
      wl_resource_add_destroy_listener (focus_surface_resource,
                                        &text_input->surface_listener);

      move_resources_for_client (&text_input->focus_resource_list,
                                 &text_input->resource_list,
                                 wl_resource_get_client (focus_surface_resource));

      if (!wl_list_empty (&text_input->focus_resource_list))
        {
          struct wl_resource *resource;

          text_input->focus_serial =
            wl_display_next_serial (text_input->seat->wl_display);

          wl_resource_for_each (resource, &text_input->focus_resource_list)
            {
              gtk_text_input_send_enter (resource, text_input->focus_serial,
                                         surface->resource);
            }
        }
    }
}

static void
unbind_resource (struct wl_resource *resource)
{
  wl_list_remove (wl_resource_get_link (resource));
}

static void
text_input_destroy (struct wl_client   *client,
                    struct wl_resource *resource)
{
  wl_resource_destroy (resource);
}

static void
text_input_enable (struct wl_client   *client,
                   struct wl_resource *resource,
                   uint32_t            serial,
                   uint32_t            flags)
{
  MetaWaylandGtkTextInput *text_input = wl_resource_get_user_data (resource);
  ClutterInputFocus *focus = text_input->input_focus;
  ClutterInputMethod *input_method;
  gboolean show_preedit;

  if (serial != text_input->focus_serial)
    return;

  if (!clutter_input_focus_is_focused (focus))
    {
      input_method = clutter_backend_get_input_method (clutter_get_default_backend ());
      if (input_method)
        clutter_input_method_focus_in (input_method, focus);
      else
        return;
    }

  show_preedit = (flags & GTK_TEXT_INPUT_ENABLE_FLAGS_CAN_SHOW_PREEDIT) != 0;
  clutter_input_focus_set_can_show_preedit (focus, show_preedit);

  if (flags & GTK_TEXT_INPUT_ENABLE_FLAGS_TOGGLE_INPUT_PANEL)
    clutter_input_focus_set_input_panel_state (focus, CLUTTER_INPUT_PANEL_STATE_TOGGLE);
}

static void
text_input_disable (struct wl_client   *client,
                    struct wl_resource *resource)
{
  MetaWaylandGtkTextInput *text_input = wl_resource_get_user_data (resource);
  ClutterInputFocus *focus = text_input->input_focus;
  ClutterInputMethod *input_method;

  if (!clutter_input_focus_is_focused (focus))
    return;

  clutter_input_focus_reset (text_input->input_focus);
  text_input->pending_state = META_WAYLAND_PENDING_STATE_NONE;

  input_method = clutter_backend_get_input_method (clutter_get_default_backend ());
  clutter_input_method_focus_out (input_method);
}

static void
text_input_set_surrounding_text (struct wl_client   *client,
                                 struct wl_resource *resource,
                                 const char         *text,
                                 int32_t             cursor,
                                 int32_t             anchor)
{
  MetaWaylandGtkTextInput *text_input = wl_resource_get_user_data (resource);

  g_free (text_input->surrounding.text);
  text_input->surrounding.text = g_strdup (text);
  text_input->surrounding.cursor = cursor;
  text_input->surrounding.anchor = anchor;
  text_input->pending_state |= META_WAYLAND_PENDING_STATE_SURROUNDING_TEXT;
}

static ClutterInputContentHintFlags
translate_hints (uint32_t hints)
{
  ClutterInputContentHintFlags clutter_hints = 0;

  if (hints & GTK_TEXT_INPUT_CONTENT_HINT_COMPLETION)
    clutter_hints |= CLUTTER_INPUT_CONTENT_HINT_COMPLETION;
  if (hints & GTK_TEXT_INPUT_CONTENT_HINT_SPELLCHECK)
    clutter_hints |= CLUTTER_INPUT_CONTENT_HINT_SPELLCHECK;
  if (hints & GTK_TEXT_INPUT_CONTENT_HINT_AUTO_CAPITALIZATION)
    clutter_hints |= CLUTTER_INPUT_CONTENT_HINT_AUTO_CAPITALIZATION;
  if (hints & GTK_TEXT_INPUT_CONTENT_HINT_LOWERCASE)
    clutter_hints |= CLUTTER_INPUT_CONTENT_HINT_LOWERCASE;
  if (hints & GTK_TEXT_INPUT_CONTENT_HINT_UPPERCASE)
    clutter_hints |= CLUTTER_INPUT_CONTENT_HINT_UPPERCASE;
  if (hints & GTK_TEXT_INPUT_CONTENT_HINT_TITLECASE)
    clutter_hints |= CLUTTER_INPUT_CONTENT_HINT_TITLECASE;
  if (hints & GTK_TEXT_INPUT_CONTENT_HINT_HIDDEN_TEXT)
    clutter_hints |= CLUTTER_INPUT_CONTENT_HINT_HIDDEN_TEXT;
  if (hints & GTK_TEXT_INPUT_CONTENT_HINT_SENSITIVE_DATA)
    clutter_hints |= CLUTTER_INPUT_CONTENT_HINT_SENSITIVE_DATA;
  if (hints & GTK_TEXT_INPUT_CONTENT_HINT_LATIN)
    clutter_hints |= CLUTTER_INPUT_CONTENT_HINT_LATIN;
  if (hints & GTK_TEXT_INPUT_CONTENT_HINT_MULTILINE)
    clutter_hints |= CLUTTER_INPUT_CONTENT_HINT_MULTILINE;

  return clutter_hints;
}

static ClutterInputContentPurpose
translate_purpose (uint32_t purpose)
{
  switch (purpose)
    {
    case GTK_TEXT_INPUT_CONTENT_PURPOSE_NORMAL:
      return CLUTTER_INPUT_CONTENT_PURPOSE_NORMAL;
    case GTK_TEXT_INPUT_CONTENT_PURPOSE_ALPHA:
      return CLUTTER_INPUT_CONTENT_PURPOSE_ALPHA;
    case GTK_TEXT_INPUT_CONTENT_PURPOSE_DIGITS:
      return CLUTTER_INPUT_CONTENT_PURPOSE_DIGITS;
    case GTK_TEXT_INPUT_CONTENT_PURPOSE_NUMBER:
      return CLUTTER_INPUT_CONTENT_PURPOSE_NUMBER;
    case GTK_TEXT_INPUT_CONTENT_PURPOSE_PHONE:
      return CLUTTER_INPUT_CONTENT_PURPOSE_PHONE;
    case GTK_TEXT_INPUT_CONTENT_PURPOSE_URL:
      return CLUTTER_INPUT_CONTENT_PURPOSE_URL;
    case GTK_TEXT_INPUT_CONTENT_PURPOSE_EMAIL:
      return CLUTTER_INPUT_CONTENT_PURPOSE_EMAIL;
    case GTK_TEXT_INPUT_CONTENT_PURPOSE_NAME:
      return CLUTTER_INPUT_CONTENT_PURPOSE_NAME;
    case GTK_TEXT_INPUT_CONTENT_PURPOSE_PASSWORD:
      return CLUTTER_INPUT_CONTENT_PURPOSE_PASSWORD;
    case GTK_TEXT_INPUT_CONTENT_PURPOSE_DATE:
      return CLUTTER_INPUT_CONTENT_PURPOSE_DATE;
    case GTK_TEXT_INPUT_CONTENT_PURPOSE_TIME:
      return CLUTTER_INPUT_CONTENT_PURPOSE_TIME;
    case GTK_TEXT_INPUT_CONTENT_PURPOSE_DATETIME:
      return CLUTTER_INPUT_CONTENT_PURPOSE_DATETIME;
    case GTK_TEXT_INPUT_CONTENT_PURPOSE_TERMINAL:
      return CLUTTER_INPUT_CONTENT_PURPOSE_TERMINAL;
    }

  g_warn_if_reached ();
  return CLUTTER_INPUT_CONTENT_PURPOSE_NORMAL;
}

static void
text_input_set_content_type (struct wl_client   *client,
                             struct wl_resource *resource,
                             uint32_t            hint,
                             uint32_t            purpose)
{
  MetaWaylandGtkTextInput *text_input = wl_resource_get_user_data (resource);

  if (!text_input->surface)
    return;

  text_input->content_type_hint = hint;
  text_input->content_type_purpose = purpose;
  text_input->pending_state |= META_WAYLAND_PENDING_STATE_CONTENT_TYPE;
}

static void
text_input_set_cursor_rectangle (struct wl_client   *client,
                                 struct wl_resource *resource,
                                 int32_t             x,
                                 int32_t             y,
                                 int32_t             width,
                                 int32_t             height)
{
  MetaWaylandGtkTextInput *text_input = wl_resource_get_user_data (resource);

  if (!text_input->surface)
    return;

  text_input->cursor_rect = (cairo_rectangle_int_t) { x, y, width, height };
  text_input->pending_state |= META_WAYLAND_PENDING_STATE_INPUT_RECT;
}

static void
text_input_commit_state (struct wl_client   *client,
                         struct wl_resource *resource)
{
  MetaWaylandGtkTextInput *text_input = wl_resource_get_user_data (resource);
  ClutterInputFocus *focus = text_input->input_focus;

  if (!clutter_input_focus_is_focused (focus))
    return;
  if (text_input->surface == NULL)
    return;

  if (text_input->pending_state & META_WAYLAND_PENDING_STATE_CONTENT_TYPE)
    {
      clutter_input_focus_set_content_hints (text_input->input_focus,
                                             translate_hints (text_input->content_type_hint));
      clutter_input_focus_set_content_purpose (text_input->input_focus,
                                               translate_purpose (text_input->content_type_purpose));
    }

  if (text_input->pending_state & META_WAYLAND_PENDING_STATE_SURROUNDING_TEXT)
    {
      clutter_input_focus_set_surrounding (text_input->input_focus,
                                           text_input->surrounding.text,
                                           text_input->surrounding.cursor,
                                           text_input->surrounding.anchor);
    }

  if (text_input->pending_state & META_WAYLAND_PENDING_STATE_INPUT_RECT)
    {
      graphene_rect_t cursor_rect;
      float x1, y1, x2, y2;
      cairo_rectangle_int_t rect;

      rect = text_input->cursor_rect;
      meta_wayland_surface_get_absolute_coordinates (text_input->surface,
                                                     rect.x, rect.y, &x1, &y1);
      meta_wayland_surface_get_absolute_coordinates (text_input->surface,
                                                     rect.x + rect.width,
                                                     rect.y + rect.height,
                                                     &x2, &y2);

      graphene_rect_init (&cursor_rect, x1, y1, x2 - x1, y2 - y1);
      clutter_input_focus_set_cursor_location (text_input->input_focus,
                                               &cursor_rect);
    }

  text_input->pending_state = META_WAYLAND_PENDING_STATE_NONE;
}

static struct gtk_text_input_interface meta_text_input_interface = {
  text_input_destroy,
  text_input_enable,
  text_input_disable,
  text_input_set_surrounding_text,
  text_input_set_content_type,
  text_input_set_cursor_rectangle,
  text_input_commit_state,
};

MetaWaylandGtkTextInput *
meta_wayland_gtk_text_input_new (MetaWaylandSeat *seat)
{
  MetaWaylandGtkTextInput *text_input;

  text_input = g_new0 (MetaWaylandGtkTextInput, 1);
  text_input->input_focus = meta_wayland_text_input_focus_new (text_input);
  text_input->seat = seat;

  wl_list_init (&text_input->resource_list);
  wl_list_init (&text_input->focus_resource_list);
  text_input->surface_listener.notify = text_input_handle_focus_surface_destroy;

  return text_input;
}

void
meta_wayland_gtk_text_input_destroy (MetaWaylandGtkTextInput *text_input)
{
  meta_wayland_gtk_text_input_set_focus (text_input, NULL);
  g_object_unref (text_input->input_focus);
  g_free (text_input);
}

static void
meta_wayland_text_input_create_new_resource (MetaWaylandGtkTextInput *text_input,
                                             struct wl_client        *client,
                                             struct wl_resource      *seat_resource,
                                             uint32_t                 id)
{
  struct wl_resource *text_input_resource;

  text_input_resource = wl_resource_create (client,
                                            &gtk_text_input_interface,
                                            META_GTK_TEXT_INPUT_VERSION,
                                            id);

  wl_resource_set_implementation (text_input_resource,
                                  &meta_text_input_interface,
                                  text_input, unbind_resource);

  if (text_input->surface &&
      wl_resource_get_client (text_input->surface->resource) == client)
    {
      wl_list_insert (&text_input->focus_resource_list,
                      wl_resource_get_link (text_input_resource));

      gtk_text_input_send_enter (text_input_resource,
                                 text_input->focus_serial,
                                 text_input->surface->resource);
    }
  else
    {
      wl_list_insert (&text_input->resource_list,
                      wl_resource_get_link (text_input_resource));
    }
}

static void
text_input_manager_destroy (struct wl_client   *client,
                            struct wl_resource *resource)
{
  wl_resource_destroy (resource);
}

static void
text_input_manager_get_text_input (struct wl_client   *client,
                                   struct wl_resource *resource,
                                   uint32_t            id,
                                   struct wl_resource *seat_resource)
{
  MetaWaylandSeat *seat = wl_resource_get_user_data (seat_resource);

  meta_wayland_text_input_create_new_resource (seat->gtk_text_input, client,
                                               seat_resource, id);
}

static struct gtk_text_input_manager_interface meta_text_input_manager_interface = {
  text_input_manager_destroy,
  text_input_manager_get_text_input,
};

static void
bind_text_input (struct wl_client *client,
		 void             *data,
		 uint32_t          version,
		 uint32_t          id)
{
  struct wl_resource *resource;

  resource = wl_resource_create (client,
                                 &gtk_text_input_manager_interface,
				 META_GTK_TEXT_INPUT_VERSION,
                                 id);
  wl_resource_set_implementation (resource,
                                  &meta_text_input_manager_interface,
                                  NULL, NULL);
}

gboolean
meta_wayland_gtk_text_input_init (MetaWaylandCompositor *compositor)
{
  return (wl_global_create (compositor->wayland_display,
                            &gtk_text_input_manager_interface,
                            META_GTK_TEXT_INPUT_VERSION,
                            compositor->seat->gtk_text_input,
                            bind_text_input) != NULL);
}

gboolean
meta_wayland_gtk_text_input_handle_event (MetaWaylandGtkTextInput *text_input,
                                          const ClutterEvent      *event)
{
  if (!text_input->surface ||
      !clutter_input_focus_is_focused (text_input->input_focus))
    return FALSE;

  return clutter_input_focus_filter_event (text_input->input_focus, event);
}
