/*
 * Copyright (C) 2010-2011 Daiki Ueno <ueno@unixuser.org>
 * Copyright (C) 2010-2011 Red Hat, Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 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 <http://www.gnu.org/licenses/>.
 */
#include "config.h"

#include <gtk/gtk.h>
#include <glib/gi18n.h>

#include "eek/eek.h"
#include "eek/eek-gtk-keyboard.h"
#include "eek/layersurface.h"
#include "eekboard/eekboard-context-service.h"
#include "submission.h"
#include "wayland.h"
#include "server-context-service.h"

enum {
    PROP_0,
    PROP_VISIBLE,
    PROP_ENABLED,
    PROP_LAST
};

struct _ServerContextService {
    GObject parent;

    EekboardContextService *state; // unowned
    /// Needed for instantiating the widget
    struct submission *submission; // unowned
    struct squeek_layout_state *layout;
    struct ui_manager *manager; // unowned
    struct vis_manager *vis_manager; // owned

    gboolean visible;
    PhoshLayerSurface *window;
    GtkWidget *widget; // nullable
    guint hiding;
    guint last_requested_height;
};

G_DEFINE_TYPE(ServerContextService, server_context_service, G_TYPE_OBJECT);

static void
on_destroy (ServerContextService *self, GtkWidget *widget)
{
    g_return_if_fail (SERVER_IS_CONTEXT_SERVICE (self));

    g_assert (widget == GTK_WIDGET(self->window));

    self->window = NULL;
    self->widget = NULL;

    //eekboard_context_service_destroy (EEKBOARD_CONTEXT_SERVICE (context));
}

static void
on_notify_map (ServerContextService *self, GtkWidget *widget)
{
    g_return_if_fail (SERVER_IS_CONTEXT_SERVICE (self));

    g_object_set (self, "visible", TRUE, NULL);
}


static void
on_notify_unmap (ServerContextService *self, GtkWidget *widget)
{
    g_return_if_fail (SERVER_IS_CONTEXT_SERVICE (self));

    g_object_set (self, "visible", FALSE, NULL);
}

static uint32_t
calculate_height(int32_t width, GdkRectangle *geometry)
{
    uint32_t height;
    if (geometry->width > geometry->height) {
        // 1:5 ratio works fine on lanscape mode, and makes sure there's
        // room left for the app window
        height = width / 5;
    } else {
        if (width < 540 && width > 0) {
            height = ((unsigned)width * 7 / 12); // to match 360×210
        } else {
            // Here we switch to wide layout, less height needed
            height = ((unsigned)width * 7 / 22);
        }
    }
    return height;
}

static void
on_surface_configure(ServerContextService *self, PhoshLayerSurface *surface)
{
    GdkDisplay *display = NULL;
    GdkWindow *window = NULL;
    GdkMonitor *monitor = NULL;
    GdkRectangle geometry;
    gint width;
    gint height;

    g_return_if_fail (SERVER_IS_CONTEXT_SERVICE (self));
    g_return_if_fail (PHOSH_IS_LAYER_SURFACE (surface));

    g_object_get(G_OBJECT(surface),
                 "configured-width", &width,
                 "configured-height", &height,
                 NULL);

    // In order to improve height calculation, we need the monitor geometry so
    // we can use different algorithms for portrait and landscape mode.
    // Note: this is a temporary fix until the size manager is complete.
    display = gdk_display_get_default ();
    if (display) {
        window = gtk_widget_get_window (GTK_WIDGET (surface));
    }
    if (window) {
        monitor = gdk_display_get_monitor_at_window (display, window);
    }
    if (monitor) {
        gdk_monitor_get_geometry (monitor, &geometry);
    } else {
        geometry.width = geometry.height = 0;
    }

     // When the geometry event comes after surface.configure,
     // this entire height calculation does nothing.
     // guint desired_height = squeek_uiman_get_perceptual_height(context->manager);
     // Temporarily use old method, until the size manager is complete.
    guint desired_height = calculate_height(width, &geometry);

    guint configured_height = (guint)height;
    // if height was already requested once but a different one was given
    // (for the same set of surrounding properties),
    // then it's probably not reasonable to ask for it again,
    // as it's likely to create pointless loops
    // of request->reject->request_again->...
    if (desired_height != configured_height
            && self->last_requested_height != desired_height) {
        self->last_requested_height = desired_height;
        phosh_layer_surface_set_size(surface, 0,
                                     (gint)desired_height);
        phosh_layer_surface_set_exclusive_zone(surface, (gint)desired_height);
        phosh_layer_surface_wl_surface_commit (surface);
    }
}

static void
make_window (ServerContextService *self)
{
    if (self->window) {
        g_error("Window already present");
    }

    struct squeek_output_handle output = squeek_outputs_get_current(squeek_wayland->outputs);
    squeek_uiman_set_output(self->manager, output);
    uint32_t height = squeek_uiman_get_perceptual_height(self->manager);

    self->window = g_object_new (
        PHOSH_TYPE_LAYER_SURFACE,
        "layer-shell", squeek_wayland->layer_shell,
        "wl-output", output.output,
        "height", height,
        "anchor", ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM
                  | ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT
                  | ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT,
        "layer", ZWLR_LAYER_SHELL_V1_LAYER_TOP,
        "kbd-interactivity", FALSE,
        "exclusive-zone", height,
        "namespace", "osk",
        NULL
    );

    g_object_connect (self->window,
                      "swapped-signal::destroy", G_CALLBACK(on_destroy), self,
                      "swapped-signal::map", G_CALLBACK(on_notify_map), self,
                      "swapped-signal::unmap", G_CALLBACK(on_notify_unmap), self,
                      "swapped-signal::configured", G_CALLBACK(on_surface_configure), self,
                      NULL);

    // The properties below are just to make hacking easier.
    // The way we use layer-shell overrides some,
    // and there's no space in the protocol for others.
    // Those may still be useful in the future,
    // or for hacks with regular windows.
    gtk_widget_set_can_focus (GTK_WIDGET(self->window), FALSE);
    g_object_set (G_OBJECT(self->window), "accept_focus", FALSE, NULL);
    gtk_window_set_title (GTK_WINDOW(self->window),
                          _("Squeekboard"));
    gtk_window_set_icon_name (GTK_WINDOW(self->window), "squeekboard");
    gtk_window_set_keep_above (GTK_WINDOW(self->window), TRUE);
}

static void
destroy_window (ServerContextService *self)
{
    gtk_widget_destroy (GTK_WIDGET (self->window));
    self->window = NULL;
}

static void
make_widget (ServerContextService *self)
{
    if (self->widget) {
        gtk_widget_destroy(self->widget);
        self->widget = NULL;
    }
    self->widget = eek_gtk_keyboard_new (self->state, self->submission, self->layout);

    gtk_widget_set_has_tooltip (self->widget, TRUE);
    gtk_container_add (GTK_CONTAINER(self->window), self->widget);
    gtk_widget_show_all(self->widget);
}

static void
server_context_service_real_show_keyboard (ServerContextService *self)
{
    if (!self->window) {
        make_window (self);
    }
    if (!self->widget) {
        make_widget (self);
    }
    self->visible = TRUE;
    gtk_widget_show (GTK_WIDGET(self->window));
}

static gboolean
show_keyboard_source_func(ServerContextService *context)
{
    server_context_service_real_show_keyboard(context);
    return G_SOURCE_REMOVE;
}

static void
server_context_service_real_hide_keyboard (ServerContextService *self)
{
    gtk_widget_hide (GTK_WIDGET(self->window));
    self->visible = FALSE;
}

static gboolean
hide_keyboard_source_func(ServerContextService *context)
{
    server_context_service_real_hide_keyboard(context);
    return G_SOURCE_REMOVE;
}

static gboolean
on_hide (ServerContextService *self)
{
    server_context_service_real_hide_keyboard(self);
    self->hiding = 0;

    return G_SOURCE_REMOVE;
}

static void
server_context_service_show_keyboard (ServerContextService *self)
{
    g_return_if_fail (SERVER_IS_CONTEXT_SERVICE(self));

    if (self->hiding) {
        g_source_remove (self->hiding);
        self->hiding = 0;
    }

    if (!self->visible) {
        g_idle_add((GSourceFunc)show_keyboard_source_func, self);
    }
}

void
server_context_service_force_show_keyboard (ServerContextService *self)
{
    if (!submission_hint_available(self->submission)) {
        eekboard_context_service_set_hint_purpose(
            self->state,
            ZWP_TEXT_INPUT_V3_CONTENT_HINT_NONE,
            ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NORMAL
        );
    }
    server_context_service_show_keyboard(self);
}

void
server_context_service_hide_keyboard (ServerContextService *self)
{
    g_return_if_fail (SERVER_IS_CONTEXT_SERVICE(self));

    if (self->visible) {
        g_idle_add((GSourceFunc)hide_keyboard_source_func, self);
    }
}

/// Meant for use by the input-method handler:
/// the visible keyboard is no longer needed.
/// The implementation will delay it slightly,
/// because the release may be due to switching from one text field to another.
/// In this case, the user doesn't really need the keyboard surface
/// to disappear completely.
void
server_context_service_release_visibility (ServerContextService *self)
{
    g_return_if_fail (SERVER_IS_CONTEXT_SERVICE(self));

    if (!self->hiding && self->visible) {
        self->hiding = g_timeout_add (200, (GSourceFunc) on_hide, self);
    }
}

static void
server_context_service_set_physical_keyboard_present (ServerContextService *self, gboolean physical_keyboard_present)
{
    g_return_if_fail (SERVER_IS_CONTEXT_SERVICE (self));
    squeek_visman_set_keyboard_present(self->vis_manager, physical_keyboard_present);
}

static void
server_context_service_set_property (GObject      *object,
                                     guint         prop_id,
                                     const GValue *value,
                                     GParamSpec   *pspec)
{
    ServerContextService *self = SERVER_CONTEXT_SERVICE(object);

    switch (prop_id) {
    case PROP_VISIBLE:
        self->visible = g_value_get_boolean (value);
        break;
    case PROP_ENABLED:
        server_context_service_set_physical_keyboard_present (self, !g_value_get_boolean (value));
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
        break;
    }
}

static void
server_context_service_get_property (GObject    *object,
                                       guint       prop_id,
                                       GValue     *value,
                                       GParamSpec *pspec)
{
    ServerContextService *self = SERVER_CONTEXT_SERVICE(object);
    switch (prop_id) {
    case PROP_VISIBLE:
        g_value_set_boolean (value, self->visible);
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
        break;
    }
}

static void
server_context_service_dispose (GObject *object)
{
    ServerContextService *self = SERVER_CONTEXT_SERVICE(object);

    destroy_window (self);
    self->widget = NULL;

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

static void
server_context_service_class_init (ServerContextServiceClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
    GParamSpec *pspec;

    gobject_class->set_property = server_context_service_set_property;
    gobject_class->get_property = server_context_service_get_property;
    gobject_class->dispose = server_context_service_dispose;

    /**
     * Flag to indicate if keyboard is visible or not.
     */
    pspec = g_param_spec_boolean ("visible",
                                  "Visible",
                                  "Visible",
                                  FALSE,
                                  G_PARAM_READWRITE);
    g_object_class_install_property (gobject_class,
                                     PROP_VISIBLE,
                                     pspec);

    /**
     * ServerContextServie:keyboard:
     *
     * Does the user want the keyboard to show up automatically?
     */
    pspec =
        g_param_spec_boolean ("enabled",
                              "Enabled",
                              "Whether the keyboard is enabled",
                              TRUE,
                              G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
    g_object_class_install_property (gobject_class,
                                     PROP_ENABLED,
                                     pspec);
}

static void
server_context_service_init (ServerContextService *self) {}

static void
init (ServerContextService *self) {
    const char *schema_name = "org.gnome.desktop.a11y.applications";
    GSettingsSchemaSource *ssrc = g_settings_schema_source_get_default();
    g_autoptr(GSettingsSchema) schema = NULL;

    if (!ssrc) {
        g_warning("No gsettings schemas installed.");
        return;
    }
    schema = g_settings_schema_source_lookup(ssrc, schema_name, TRUE);
    if (schema) {
        g_autoptr(GSettings) settings = g_settings_new (schema_name);
        g_settings_bind (settings, "screen-keyboard-enabled",
                         self, "enabled", G_SETTINGS_BIND_GET);
    } else {
        g_warning("Gsettings schema %s is not installed on the system. "
                  "Enabling by default.", schema_name);
    }
}

ServerContextService *
server_context_service_new (EekboardContextService *self, struct submission *submission, struct squeek_layout_state *layout, struct ui_manager *uiman, struct vis_manager *visman)
{
    ServerContextService *ui = g_object_new (SERVER_TYPE_CONTEXT_SERVICE, NULL);
    ui->submission = submission;
    ui->state = self;
    ui->layout = layout;
    ui->manager = uiman;
    ui->vis_manager = visman;
    init(ui);
    return ui;
}

void
server_context_service_update_visible (ServerContextService *self, gboolean visible) {
    if (visible) {
        server_context_service_show_keyboard(self);
    } else {
        server_context_service_hide_keyboard(self);
    }
}

