/***************************************************************************
 *   Copyright (C) 2012~2012 by CSSlayer                                   *
 *                                                                         *
 *   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.,                                       *
 *   51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.              *
 ***************************************************************************/

#include "fcitxclient.h"
#include "fcitx/fcitx.h"
#include "fcitxconnection.h"
#include "frontend/ipc/ipc.h"
#include "marshall.h"
#include "module/dbus/dbusstuff.h"
#include <dbus/dbus.h>
#include <stdlib.h>
#include <unistd.h>

#ifdef _DEBUG
#define fcitx_gclient_debug(...)                                               \
    g_log("fcitx-client", G_LOG_LEVEL_DEBUG, __VA_ARGS__)

#else
#define fcitx_gclient_debug(...)
#endif
typedef struct _ProcessKeyStruct ProcessKeyStruct;

/**
 * FcitxClient:
 *
 * A #FcitxClient allow to create a input context via DBus
 */

enum {
    PROP_0,
    PROP_CONNECTION,
};

struct _ProcessKeyStruct {
    FcitxClient *self;
    GAsyncReadyCallback callback;
    void *user_data;
};

struct _FcitxClientPrivate {
    GDBusProxy *improxy;
    GDBusProxy *icproxy;
    char servicename[64];
    gchar *icname;
    gchar *display;
    gboolean is_portal;
    GCancellable *cancellable;
    FcitxConnection *connection;
};

static const gchar introspection_xml[] =
    "<node>"
    "  <interface name=\"" FCITX_IM_DBUS_INTERFACE "\">"
    "    <method name=\"CreateICv3\">\n"
    "      <arg name=\"appname\" direction=\"in\" type=\"s\"/>\n"
    "      <arg name=\"pid\" direction=\"in\" type=\"i\"/>\n"
    "      <arg name=\"icid\" direction=\"out\" type=\"i\"/>\n"
    "      <arg name=\"enable\" direction=\"out\" type=\"b\"/>\n"
    "      <arg name=\"keyval1\" direction=\"out\" type=\"u\"/>\n"
    "      <arg name=\"state1\" direction=\"out\" type=\"u\"/>\n"
    "      <arg name=\"keyval2\" direction=\"out\" type=\"u\"/>\n"
    "      <arg name=\"state2\" direction=\"out\" type=\"u\"/>\n"
    "    </method>\n"
    "  </interface>"
    "</node>";

static const gchar introspection_portal_xml[] =
    "<node>"
    "<interface name=\"org.fcitx.Fcitx.InputMethod1\">"
    "<method name=\"CreateInputContext\">"
    "<arg type=\"a(ss)\" direction=\"in\"/>"
    "<arg type=\"o\" direction=\"out\"/>"
    "<arg type=\"ay\" direction=\"out\"/>"
    "</method>"
    "</interface>"
    "</node>";

static const gchar ic_introspection_xml[] =
    "<node>\n"
    "  <interface name=\"" FCITX_IC_DBUS_INTERFACE "\">\n"
    "    <method name=\"EnableIC\">\n"
    "    </method>\n"
    "    <method name=\"CloseIC\">\n"
    "    </method>\n"
    "    <method name=\"FocusIn\">\n"
    "    </method>\n"
    "    <method name=\"FocusOut\">\n"
    "    </method>\n"
    "    <method name=\"Reset\">\n"
    "    </method>\n"
    "    <method name=\"SetCursorRect\">\n"
    "      <arg name=\"x\" direction=\"in\" type=\"i\"/>\n"
    "      <arg name=\"y\" direction=\"in\" type=\"i\"/>\n"
    "      <arg name=\"w\" direction=\"in\" type=\"i\"/>\n"
    "      <arg name=\"h\" direction=\"in\" type=\"i\"/>\n"
    "    </method>\n"
    "    <method name=\"SetCapacity\">\n"
    "      <arg name=\"caps\" direction=\"in\" type=\"u\"/>\n"
    "    </method>\n"
    "    <method name=\"SetSurroundingText\">\n"
    "      <arg name=\"text\" direction=\"in\" type=\"s\"/>\n"
    "      <arg name=\"cursor\" direction=\"in\" type=\"u\"/>\n"
    "      <arg name=\"anchor\" direction=\"in\" type=\"u\"/>\n"
    "    </method>\n"
    "    <method name=\"SetSurroundingTextPosition\">\n"
    "      <arg name=\"cursor\" direction=\"in\" type=\"u\"/>\n"
    "      <arg name=\"anchor\" direction=\"in\" type=\"u\"/>\n"
    "    </method>\n"
    "    <method name=\"DestroyIC\">\n"
    "    </method>\n"
    "    <method name=\"ProcessKeyEvent\">\n"
    "      <arg name=\"keyval\" direction=\"in\" type=\"u\"/>\n"
    "      <arg name=\"keycode\" direction=\"in\" type=\"u\"/>\n"
    "      <arg name=\"state\" direction=\"in\" type=\"u\"/>\n"
    "      <arg name=\"type\" direction=\"in\" type=\"i\"/>\n"
    "      <arg name=\"time\" direction=\"in\" type=\"u\"/>\n"
    "      <arg name=\"ret\" direction=\"out\" type=\"i\"/>\n"
    "    </method>\n"
    "    <signal name=\"EnableIM\">\n"
    "    </signal>\n"
    "    <signal name=\"CloseIM\">\n"
    "    </signal>\n"
    "    <signal name=\"CommitString\">\n"
    "      <arg name=\"str\" type=\"s\"/>\n"
    "    </signal>\n"
    "    <signal name=\"DeleteSurroundingText\">\n"
    "      <arg name=\"offset\" type=\"i\"/>\n"
    "      <arg name=\"nchar\" type=\"u\"/>\n"
    "    </signal>\n"
    "    <signal name=\"UpdateFormattedPreedit\">\n"
    "      <arg name=\"str\" type=\"a(si)\"/>\n"
    "      <arg name=\"cursorpos\" type=\"i\"/>\n"
    "    </signal>\n"
    "    <signal name=\"UpdateClientSideUI\">\n"
    "      <arg name=\"auxup\" type=\"s\"/>\n"
    "      <arg name=\"auxdown\" type=\"s\"/>\n"
    "      <arg name=\"preedit\" type=\"s\"/>\n"
    "      <arg name=\"candidateword\" type=\"s\"/>\n"
    "      <arg name=\"imname\" type=\"s\"/>\n"
    "      <arg name=\"cursorpos\" type=\"i\"/>\n"
    "    </signal>\n"
    "    <signal name=\"ForwardKey\">\n"
    "      <arg name=\"keyval\" type=\"u\"/>\n"
    "      <arg name=\"state\" type=\"u\"/>\n"
    "      <arg name=\"type\" type=\"i\"/>\n"
    "    </signal>\n"
    "  </interface>\n"
    "</node>\n";

const char *ic_introspection_portal_xml =
    "<node>"
    "<interface name=\"org.fcitx.Fcitx.InputContext1\">"
    "<method name=\"DestroyIC\">"
    "</method>"
    "<method name=\"FocusIn\">"
    "</method>"
    "<method name=\"FocusOut\">"
    "</method>"
    "<method name=\"ProcessKeyEvent\">"
    "<arg type=\"u\" direction=\"in\"/>"
    "<arg type=\"u\" direction=\"in\"/>"
    "<arg type=\"u\" direction=\"in\"/>"
    "<arg type=\"b\" direction=\"in\"/>"
    "<arg type=\"u\" direction=\"in\"/>"
    "<arg type=\"b\" direction=\"out\"/>"
    "</method>"
    "<method name=\"Reset\">"
    "</method>"
    "<method name=\"SetCapability\">"
    "<arg type=\"t\" direction=\"in\"/>"
    "</method>"
    "<method name=\"SetCursorRect\">"
    "<arg type=\"i\" direction=\"in\"/>"
    "<arg type=\"i\" direction=\"in\"/>"
    "<arg type=\"i\" direction=\"in\"/>"
    "<arg type=\"i\" direction=\"in\"/>"
    "</method>"
    "<method name=\"SetSurroundingText\">"
    "<arg type=\"s\" direction=\"in\"/>"
    "<arg type=\"u\" direction=\"in\"/>"
    "<arg type=\"u\" direction=\"in\"/>"
    "</method>"
    "<method name=\"SetSurroundingTextPosition\">"
    "<arg type=\"u\" direction=\"in\"/>"
    "<arg type=\"u\" direction=\"in\"/>"
    "</method>"
    "<signal name=\"CommitString\">"
    "<arg type=\"s\"/>"
    "</signal>"
    "<signal name=\"DeleteSurroundingText\">"
    "<arg type=\"i\"/>"
    "<arg type=\"u\"/>"
    "</signal>"
    "<signal name=\"ForwardKey\">"
    "<arg type=\"u\"/>"
    "<arg type=\"u\"/>"
    "<arg type=\"b\"/>"
    "</signal>"
    "<signal name=\"UpdateFormattedPreedit\">"
    "<arg type=\"a(si)\"/>"
    "<arg type=\"i\"/>"
    "</signal>"
    "</interface>"
    "</node>";

FCITX_EXPORT_API
GType fcitx_client_get_type(void) G_GNUC_CONST;

G_DEFINE_TYPE(FcitxClient, fcitx_client, G_TYPE_OBJECT);

#define FCITX_CLIENT_GET_PRIVATE(obj)                                          \
    (G_TYPE_INSTANCE_GET_PRIVATE((obj), FCITX_TYPE_CLIENT, FcitxClientPrivate))

enum {
    CONNECTED_SIGNAL,
    ENABLE_IM_SIGNAL,
    CLOSE_IM_SIGNAL,
    FORWARD_KEY_SIGNAL,
    COMMIT_STRING_SIGNAL,
    DELETE_SURROUNDING_TEXT_SIGNAL,
    UPDATED_FORMATTED_PREEDIT_SIGNAL,
    DISCONNECTED_SIGNAL,
    UPDATE_CLIENT_SIDE_UI_SIGNAL,
    LAST_SIGNAL
};

static guint signals[LAST_SIGNAL] = {0};

static GDBusInterfaceInfo *_fcitx_client_get_interface_info(void);
static GDBusInterfaceInfo *_fcitx_client_get_portal_interface_info(void);
static GDBusInterfaceInfo *_fcitx_client_get_clientic_info(void);
static GDBusInterfaceInfo *_fcitx_client_get_portal_clientic_info(void);
static void _fcitx_client_create_ic(FcitxConnection *connection,
                                    gpointer user_data);
static void _fcitx_client_disconnect(FcitxConnection *connection,
                                     gpointer user_data);
static void _fcitx_client_create_ic_phase1_finished(GObject *source_object,
                                                    GAsyncResult *res,
                                                    gpointer user_data);
static void _fcitx_client_create_ic_portal_phase1_finished(
    GObject *source_object, GAsyncResult *res, gpointer user_data);
static void _fcitx_client_create_ic_cb(GObject *source_object,
                                       GAsyncResult *res, gpointer user_data);
static void _fcitx_client_create_ic_portal_cb(GObject *source_object,
                                              GAsyncResult *res,
                                              gpointer user_data);
static void _fcitx_client_create_ic_phase2_finished(GObject *source_object,
                                                    GAsyncResult *res,
                                                    gpointer user_data);
static void _fcitx_client_create_ic_phase2_portal_finished(
    GObject *source_object, GAsyncResult *res, gpointer user_data);
static void _fcitx_client_g_signal(GDBusProxy *proxy, gchar *sender_name,
                                   gchar *signal_name, GVariant *parameters,
                                   gpointer user_data);
static void fcitx_client_init(FcitxClient *self);
static void fcitx_client_finalize(GObject *object);
static void fcitx_client_dispose(GObject *object);
static void fcitx_client_constructed(GObject *object);
static void _fcitx_client_clean_up(FcitxClient *self,
                                   gboolean dont_emit_disconn);

static void fcitx_client_set_property(GObject *gobject, guint prop_id,
                                      const GValue *value, GParamSpec *pspec);

static void fcitx_client_class_init(FcitxClientClass *klass);

static void _item_free(gpointer arg);

#define STATIC_INTERFACE_INFO(FUNCTION, XML)                                   \
    static GDBusInterfaceInfo *FUNCTION(void) {                                \
        static gsize has_info = 0;                                             \
        static GDBusInterfaceInfo *info = NULL;                                \
        if (g_once_init_enter(&has_info)) {                                    \
            GDBusNodeInfo *introspection_data;                                 \
            introspection_data = g_dbus_node_info_new_for_xml(XML, NULL);      \
            info = introspection_data->interfaces[0];                          \
            g_once_init_leave(&has_info, 1);                                   \
        }                                                                      \
        return info;                                                           \
    }

STATIC_INTERFACE_INFO(_fcitx_client_get_interface_info, introspection_xml)
STATIC_INTERFACE_INFO(_fcitx_client_get_portal_interface_info,
                      introspection_portal_xml)
STATIC_INTERFACE_INFO(_fcitx_client_get_clientic_info, ic_introspection_xml)
STATIC_INTERFACE_INFO(_fcitx_client_get_portal_clientic_info,
                      ic_introspection_portal_xml)

static void fcitx_client_finalize(GObject *object) {
    if (G_OBJECT_CLASS(fcitx_client_parent_class)->finalize != NULL)
        G_OBJECT_CLASS(fcitx_client_parent_class)->finalize(object);
}

static void fcitx_client_dispose(GObject *object) {
    FcitxClient *self = FCITX_CLIENT(object);

    if (self->priv->icproxy) {
        g_dbus_proxy_call(self->priv->icproxy, "DestroyIC", NULL,
                          G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
    }

#ifndef g_signal_handlers_disconnect_by_data
#define g_signal_handlers_disconnect_by_data(instance, data)                   \
    g_signal_handlers_disconnect_matched((instance), G_SIGNAL_MATCH_DATA, 0,   \
                                         0, NULL, NULL, (data))
#endif
    g_signal_handlers_disconnect_by_data(self->priv->connection, self);
    g_object_unref(self->priv->connection);
    _fcitx_client_clean_up(self, TRUE);

    if (G_OBJECT_CLASS(fcitx_client_parent_class)->dispose != NULL)
        G_OBJECT_CLASS(fcitx_client_parent_class)->dispose(object);

    g_free(self->priv->display);
    self->priv->display = NULL;
}

/**
 * fcitx_client_enable_ic:
 * @self: A #FcitxClient
 *
 * tell fcitx activate current ic
 **/
FCITX_EXPORT_API
void fcitx_client_enable_ic(FcitxClient *self) {
    if (self->priv->icproxy && !self->priv->is_portal) {
        g_dbus_proxy_call(self->priv->icproxy, "EnableIC", NULL,
                          G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
    }
}

/**
 * fcitx_client_close_ic:
 * @self: A #FcitxClient
 *
 * tell fcitx inactivate current ic
 **/
FCITX_EXPORT_API
void fcitx_client_close_ic(FcitxClient *self) {
    if (self->priv->icproxy && !self->priv->is_portal) {
        g_dbus_proxy_call(self->priv->icproxy, "CloseIC", NULL,
                          G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
    }
}

/**
 * fcitx_client_focus_in:
 * @self: A #FcitxClient
 *
 * tell fcitx current client has focus
 **/
FCITX_EXPORT_API
void fcitx_client_focus_in(FcitxClient *self) {
    if (self->priv->icproxy) {
        g_dbus_proxy_call(self->priv->icproxy, "FocusIn", NULL,
                          G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
    }
}

/**
 * fcitx_client_focus_out:
 * @self: A #FcitxClient
 *
 * tell fcitx current client has lost focus
 **/
FCITX_EXPORT_API
void fcitx_client_focus_out(FcitxClient *self) {
    if (self->priv->icproxy) {
        g_dbus_proxy_call(self->priv->icproxy, "FocusOut", NULL,
                          G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
    }
}

/**
 * fcitx_client_reset:
 * @self: A #FcitxClient
 *
 * tell fcitx current client is reset from client side
 **/
FCITX_EXPORT_API
void fcitx_client_reset(FcitxClient *self) {
    if (self->priv->icproxy) {
        g_dbus_proxy_call(self->priv->icproxy, "Reset", NULL,
                          G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
    }
}

/**
 * fcitx_client_set_capacity:
 * @self: A #FcitxClient
 * @flags: capacity
 *
 * set client capacity of Fcitx
 **/
FCITX_EXPORT_API
void fcitx_client_set_capacity(FcitxClient *self, guint flags) {
    if (self->priv->icproxy) {
        if (self->priv->is_portal) {
            guint64 iflags = flags;
            g_dbus_proxy_call(self->priv->icproxy, "SetCapability",
                              g_variant_new("(t)", iflags),
                              G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
        } else {
            guint32 iflags = flags;
            g_dbus_proxy_call(self->priv->icproxy, "SetCapacity",
                              g_variant_new("(u)", iflags),
                              G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
        }
    }
}

/**
 * fcitx_client_set_cursor_rect:
 * @self: A #FcitxClient
 * @x: x of cursor
 * @y: y of cursor
 * @w: width of cursor
 * @h: height of cursor
 *
 * tell fcitx current client's cursor geometry info
 **/
FCITX_EXPORT_API
void fcitx_client_set_cursor_rect(FcitxClient *self, int x, int y, int w,
                                  int h) {
    if (self->priv->icproxy) {
        g_dbus_proxy_call(self->priv->icproxy, "SetCursorRect",
                          g_variant_new("(iiii)", x, y, w, h),
                          G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
    }
}

/**
 * fcitx_client_set_surrounding_text:
 * @self: A #FcitxClient
 * @text: (transfer none) (allow-none): surroundng text
 * @cursor: cursor position coresponding to text
 * @anchor: anchor position coresponding to text
 **/
FCITX_EXPORT_API
void fcitx_client_set_surrounding_text(FcitxClient *self, gchar *text,
                                       guint cursor, guint anchor) {
    if (self->priv->icproxy) {
        if (text) {
            g_dbus_proxy_call(self->priv->icproxy, "SetSurroundingText",
                              g_variant_new("(suu)", text, cursor, anchor),
                              G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
        } else {
            g_dbus_proxy_call(self->priv->icproxy, "SetSurroundingTextPosition",
                              g_variant_new("(uu)", cursor, anchor),
                              G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
        }
    }
}

/**
 * fcitx_client_process_key_finish:
 * @self: A #FcitxClient
 * @res: result
 *
 * use this function with #fcitx_client_process_key_async
 *
 * Returns: process key result
 **/
FCITX_EXPORT_API
gint fcitx_client_process_key_finish(FcitxClient *self, GAsyncResult *res) {
    gint ret = -1;
    if (!self->priv->icproxy)
        return -1;

    GVariant *result = g_dbus_proxy_call_finish(self->priv->icproxy, res, NULL);
    if (result) {
        if (self->priv->is_portal) {
            gboolean bret;
            g_variant_get(result, "(b)", &bret);
            g_variant_unref(result);
            ret = bret ? 1 : 0;
        } else {
            g_variant_get(result, "(i)", &ret);
            g_variant_unref(result);
        }
    }
    return ret;
}

void _process_key_data_free(ProcessKeyStruct *pk) {
    g_object_unref(pk->self);
    g_free(pk);
}

void _fcitx_client_process_key_cb(GObject *source_object, GAsyncResult *res,
                                  gpointer user_data) {
    FCITX_UNUSED(source_object);
    ProcessKeyStruct *pk = user_data;
    pk->callback(G_OBJECT(pk->self), res, pk->user_data);
    _process_key_data_free(pk);
}

void _fcitx_client_process_key_cancelled(GCancellable *cancellable,
                                         gpointer user_data) {
    FCITX_UNUSED(cancellable);
    ProcessKeyStruct *pk = user_data;
    _process_key_data_free(pk);
}

/**
 * fcitx_client_process_key:
 * @self: A #FcitxClient
 * @keyval: key value
 * @keycode: hardware key code
 * @state: key state
 * @type: event type
 * @t: timestamp
 * @timeout_msec: timeout in millisecond
 * @cancellable: cancellable
 * @callback: (scope async) (closure user_data): callback
 * @user_data: (closure): user data
 *
 * use this function with #fcitx_client_process_key_finish
 **/
FCITX_EXPORT_API
void fcitx_client_process_key(FcitxClient *self, guint32 keyval,
                              guint32 keycode, guint32 state, gint type,
                              guint32 t, gint timeout_msec,
                              GCancellable *cancellable,
                              GAsyncReadyCallback callback,
                              gpointer user_data) {
    if (self->priv->icproxy) {
        ProcessKeyStruct *pk = g_new(ProcessKeyStruct, 1);
        pk->self = g_object_ref(self);
        pk->callback = callback;
        pk->user_data = user_data;
        if (self->priv->is_portal) {
            gboolean btype = type == 1;
            g_dbus_proxy_call(
                self->priv->icproxy, "ProcessKeyEvent",
                g_variant_new("(uuubu)", keyval, keycode, state, btype, t),
                G_DBUS_CALL_FLAGS_NONE, timeout_msec, cancellable,
                _fcitx_client_process_key_cb, pk);
        } else {
            gint32 itype = type;
            g_dbus_proxy_call(
                self->priv->icproxy, "ProcessKeyEvent",
                g_variant_new("(uuuiu)", keyval, keycode, state, itype, t),
                G_DBUS_CALL_FLAGS_NONE, timeout_msec, cancellable,
                _fcitx_client_process_key_cb, pk);
        }
    }
}

/**
 * fcitx_client_process_key_sync:
 * @self: A #FcitxClient
 * @keyval: key value
 * @keycode: hardware key code
 * @state: key state
 * @type: event type
 * @t: timestamp
 *
 * send a key event to fcitx synchronizely
 *
 * Returns: the key is processed or not
 */
FCITX_EXPORT_API
int fcitx_client_process_key_sync(FcitxClient *self, guint32 keyval,
                                  guint32 keycode, guint32 state, gint type,
                                  guint32 t) {
    if (self->priv->icproxy) {
        if (self->priv->is_portal) {
            gboolean ret = 0;
            gboolean is_release = type == 1;
            GVariant *result = g_dbus_proxy_call_sync(
                self->priv->icproxy, "ProcessKeyEvent",
                g_variant_new("(uuubu)", keyval, keycode, state, is_release, t),
                G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL);

            if (result) {
                g_variant_get(result, "(b)", &ret);
                g_variant_unref(result);
            }
            return ret ? 1 : 0;
        } else {
            int ret = -1;
            GVariant *result = g_dbus_proxy_call_sync(
                self->priv->icproxy, "ProcessKeyEvent",
                g_variant_new("(uuuiu)", keyval, keycode, state, type, t),
                G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL);

            if (result) {
                g_variant_get(result, "(i)", &ret);
                g_variant_unref(result);
            }
            return ret;
        }
    }

    return -1;
}

static void fcitx_client_init(FcitxClient *self) {
    self->priv = FCITX_CLIENT_GET_PRIVATE(self);

    sprintf(self->priv->servicename, "%s-%d", FCITX_DBUS_SERVICE,
            fcitx_utils_get_display_number());

    self->priv->connection = NULL;
    self->priv->cancellable = NULL;
    self->priv->improxy = NULL;
    self->priv->icproxy = NULL;
    self->priv->icname = NULL;
    self->priv->display = NULL;
}

static void fcitx_client_constructed(GObject *object) {
    FcitxClient *self = FCITX_CLIENT(object);
    G_OBJECT_CLASS(fcitx_client_parent_class)->constructed(object);
    if (!self->priv->connection) {
        self->priv->connection = fcitx_connection_new();
        g_object_ref_sink(self->priv->connection);
    }

    if (fcitx_connection_is_valid(self->priv->connection)) {
        _fcitx_client_create_ic(self->priv->connection, self);
    }
    g_signal_connect(self->priv->connection, "connected",
                     (GCallback)_fcitx_client_create_ic, self);
    g_signal_connect(self->priv->connection, "disconnected",
                     (GCallback)_fcitx_client_disconnect, self);
}

static void _fcitx_client_create_ic(FcitxConnection *connection,
                                    gpointer user_data) {
    FCITX_UNUSED(connection);
    fcitx_gclient_debug("_fcitx_client_create_ic");
    FcitxClient *self = user_data;

    _fcitx_client_clean_up(self, FALSE);

    g_object_ref(self);
    self->priv->cancellable = g_cancellable_new();
    g_dbus_proxy_new(
        fcitx_connection_get_g_dbus_connection(self->priv->connection),
        G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES,
        _fcitx_client_get_interface_info(), self->priv->servicename,
        FCITX_IM_DBUS_PATH, FCITX_IM_DBUS_INTERFACE, self->priv->cancellable,
        _fcitx_client_create_ic_phase1_finished, self);
}

static void _fcitx_client_create_ic_portal(FcitxClient *self) {
    fcitx_gclient_debug("_fcitx_client_create_ic_portal");

    self->priv->cancellable = g_cancellable_new();
    g_dbus_proxy_new(
        fcitx_connection_get_g_dbus_connection(self->priv->connection),
        G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES,
        _fcitx_client_get_portal_interface_info(),
        "org.freedesktop.portal.Fcitx", "/inputmethod",
        "org.fcitx.Fcitx.InputMethod1", self->priv->cancellable,
        _fcitx_client_create_ic_portal_phase1_finished, self);
}

static void _fcitx_client_disconnect(FcitxConnection *connection,
                                     gpointer user_data) {
    FCITX_UNUSED(connection);
    FcitxClient *self = user_data;
    _fcitx_client_clean_up(self, FALSE);
}

static void _fcitx_client_create_ic_phase1_finished(GObject *source_object,
                                                    GAsyncResult *res,
                                                    gpointer user_data) {
    FCITX_UNUSED(source_object);
    fcitx_gclient_debug("_fcitx_client_create_ic_phase1_finished");
    g_return_if_fail(user_data != NULL);
    g_return_if_fail(FCITX_IS_CLIENT(user_data));
    FcitxClient *self = (FcitxClient *)user_data;
    if (self->priv->cancellable) {
        g_object_unref(self->priv->cancellable);
        self->priv->cancellable = NULL;
    }
    if (self->priv->improxy)
        g_object_unref(self->priv->improxy);
    self->priv->improxy = g_dbus_proxy_new_finish(res, NULL);

    do {
        if (!self->priv->improxy) {
            break;
        }

        gchar *owner_name = g_dbus_proxy_get_name_owner(self->priv->improxy);

        if (!owner_name) {
            g_object_unref(self->priv->improxy);
            self->priv->improxy = NULL;
            break;
        }
        g_free(owner_name);
    } while (0);

    if (!self->priv->improxy) {
        _fcitx_client_create_ic_portal(self);
        return;
    }

    self->priv->cancellable = g_cancellable_new();
    char *appname = fcitx_utils_get_process_name();
    int pid = getpid();
    g_dbus_proxy_call(
        self->priv->improxy, "CreateICv3", g_variant_new("(si)", appname, pid),
        G_DBUS_CALL_FLAGS_NONE, -1, /* timeout */
        self->priv->cancellable, _fcitx_client_create_ic_cb, self);
    free(appname);
}

static void _fcitx_client_create_ic_portal_phase1_finished(
    GObject *source_object, GAsyncResult *res, gpointer user_data) {
    FCITX_UNUSED(source_object);
    fcitx_gclient_debug("_fcitx_client_create_ic_portal_phase1_finished");
    g_return_if_fail(user_data != NULL);
    g_return_if_fail(FCITX_IS_CLIENT(user_data));
    FcitxClient *self = (FcitxClient *)user_data;
    if (self->priv->cancellable) {
        g_object_unref(self->priv->cancellable);
        self->priv->cancellable = NULL;
    }
    if (self->priv->improxy)
        g_object_unref(self->priv->improxy);
    self->priv->improxy = g_dbus_proxy_new_finish(res, NULL);

    do {
        if (!self->priv->improxy) {
            break;
        }

        gchar *owner_name = g_dbus_proxy_get_name_owner(self->priv->improxy);

        if (!owner_name) {
            g_object_unref(self->priv->improxy);
            self->priv->improxy = NULL;
            break;
        }
        g_free(owner_name);
    } while (0);

    if (!self->priv->improxy) {
        /* unref for create_ic */
        g_object_unref(self);
        return;
    }

    self->priv->cancellable = g_cancellable_new();

    GVariantBuilder builder;
    g_variant_builder_init(&builder, G_VARIANT_TYPE("a(ss)"));
    if (self->priv->display) {
        g_variant_builder_add(&builder, "(ss)", "display,",
                              self->priv->display);
    }

    g_dbus_proxy_call(self->priv->improxy, "CreateInputContext",
                      g_variant_new("(a(ss))", &builder),
                      G_DBUS_CALL_FLAGS_NONE, -1, /* timeout */
                      self->priv->cancellable,
                      _fcitx_client_create_ic_portal_cb, self);
}

static void _fcitx_client_create_ic_cb(GObject *source_object,
                                       GAsyncResult *res, gpointer user_data) {
    FcitxClient *self = (FcitxClient *)user_data;
    if (self->priv->cancellable) {
        g_object_unref(self->priv->cancellable);
        self->priv->cancellable = NULL;
    }
    GVariant *result =
        g_dbus_proxy_call_finish(G_DBUS_PROXY(source_object), res, NULL);

    if (!result) {
        /* unref for _fcitx_client_phase1_finish */
        g_object_unref(self);
        return;
    }

    gboolean enable;
    guint32 key1, state1, key2, state2;
    int id;
    g_variant_get(result, "(ibuuuu)", &id, &enable, &key1, &state1, &key2,
                  &state2);
    g_variant_unref(result);

    self->priv->icname = g_strdup_printf(FCITX_IC_DBUS_PATH, id);

    self->priv->cancellable = g_cancellable_new();
    g_dbus_proxy_new(
        fcitx_connection_get_g_dbus_connection(self->priv->connection),
        G_DBUS_PROXY_FLAGS_NONE, _fcitx_client_get_clientic_info(),
        self->priv->servicename, self->priv->icname, FCITX_IC_DBUS_INTERFACE,
        self->priv->cancellable, _fcitx_client_create_ic_phase2_finished, self);
}

static void _fcitx_client_create_ic_portal_cb(GObject *source_object,
                                              GAsyncResult *res,
                                              gpointer user_data) {
    FcitxClient *self = (FcitxClient *)user_data;
    if (self->priv->cancellable) {
        g_object_unref(self->priv->cancellable);
        self->priv->cancellable = NULL;
    }
    GVariant *result =
        g_dbus_proxy_call_finish(G_DBUS_PROXY(source_object), res, NULL);

    if (!result) {
        /* unref for _fcitx_client_phase1_finish */
        g_object_unref(self);
        return;
    }

    gchar *path = NULL;
    GVariantIter *iter;
    g_variant_get(result, "(oay)", &path, &iter);
    g_variant_iter_free(iter);
    g_variant_unref(result);

    self->priv->icname = g_strdup(path);

    self->priv->cancellable = g_cancellable_new();
    g_dbus_proxy_new(
        fcitx_connection_get_g_dbus_connection(self->priv->connection),
        G_DBUS_PROXY_FLAGS_NONE, _fcitx_client_get_portal_clientic_info(),
        "org.freedesktop.portal.Fcitx", self->priv->icname,
        "org.fcitx.Fcitx.InputContext1", self->priv->cancellable,
        _fcitx_client_create_ic_phase2_portal_finished, self);
}

static void _fcitx_client_create_ic_phase2_finished(GObject *source_object,
                                                    GAsyncResult *res,
                                                    gpointer user_data) {
    FCITX_UNUSED(source_object);
    g_return_if_fail(user_data != NULL);
    g_return_if_fail(FCITX_IS_CLIENT(user_data));
    FcitxClient *self = (FcitxClient *)user_data;
    if (self->priv->cancellable) {
        g_object_unref(self->priv->cancellable);
        self->priv->cancellable = NULL;
    }
    if (self->priv->icproxy)
        g_object_unref(self->priv->icproxy);
    self->priv->icproxy = g_dbus_proxy_new_finish(res, NULL);

    do {
        if (!self->priv->icproxy)
            break;

        gchar *owner_name = g_dbus_proxy_get_name_owner(self->priv->icproxy);

        if (!owner_name) {
            g_object_unref(self->priv->icproxy);
            self->priv->icproxy = NULL;
            break;
        }
        g_free(owner_name);
    } while (0);

    if (self->priv->icproxy) {
        g_signal_connect(self->priv->icproxy, "g-signal",
                         G_CALLBACK(_fcitx_client_g_signal), self);
        g_signal_emit(user_data, signals[CONNECTED_SIGNAL], 0);
    }

    /* unref for _fcitx_client_create_ic_cb */
    g_object_unref(self);
}

static void _fcitx_client_create_ic_phase2_portal_finished(
    GObject *source_object, GAsyncResult *res, gpointer user_data) {
    FCITX_UNUSED(source_object);
    g_return_if_fail(user_data != NULL);
    g_return_if_fail(FCITX_IS_CLIENT(user_data));
    FcitxClient *self = (FcitxClient *)user_data;
    if (self->priv->cancellable) {
        g_object_unref(self->priv->cancellable);
        self->priv->cancellable = NULL;
    }
    if (self->priv->icproxy)
        g_object_unref(self->priv->icproxy);
    self->priv->icproxy = g_dbus_proxy_new_finish(res, NULL);

    do {
        if (!self->priv->icproxy)
            break;

        gchar *owner_name = g_dbus_proxy_get_name_owner(self->priv->icproxy);

        if (!owner_name) {
            g_object_unref(self->priv->icproxy);
            self->priv->icproxy = NULL;
            break;
        }
        g_free(owner_name);
    } while (0);

    if (self->priv->icproxy) {
        self->priv->is_portal = TRUE;
        g_signal_connect(self->priv->icproxy, "g-signal",
                         G_CALLBACK(_fcitx_client_g_signal), self);
        g_signal_emit(user_data, signals[CONNECTED_SIGNAL], 0);
    }

    /* unref for _fcitx_client_create_ic_cb */
    g_object_unref(self);
}

static void _item_free(gpointer arg) {
    FcitxPreeditItem *item = arg;
    free(item->string);
    free(item);
}

static void _fcitx_client_g_signal(GDBusProxy *proxy, gchar *sender_name,
                                   gchar *signal_name, GVariant *parameters,
                                   gpointer user_data) {
    FCITX_UNUSED(proxy);
    FCITX_UNUSED(sender_name);
    FcitxClient *self = user_data;
    if (strcmp(signal_name, "EnableIM") == 0) {
        g_signal_emit(user_data, signals[ENABLE_IM_SIGNAL], 0);
    } else if (strcmp(signal_name, "CloseIM") == 0) {
        g_signal_emit(user_data, signals[CLOSE_IM_SIGNAL], 0);
    } else if (strcmp(signal_name, "CommitString") == 0) {
        const gchar *data = NULL;
        g_variant_get(parameters, "(s)", &data);
        if (data) {
            g_signal_emit(user_data, signals[COMMIT_STRING_SIGNAL], 0, data);
        }
    } else if (strcmp(signal_name, "ForwardKey") == 0) {
        if (self->priv->is_portal) {
            guint32 key, state;
            gboolean is_release;
            g_variant_get(parameters, "(uub)", &key, &state, &is_release);
            g_signal_emit(user_data, signals[FORWARD_KEY_SIGNAL], 0, key, state,
                          (is_release ? 0 : 1));
        } else {
            guint32 key, state;
            gint32 type;
            g_variant_get(parameters, "(uui)", &key, &state, &type);
            g_signal_emit(user_data, signals[FORWARD_KEY_SIGNAL], 0, key, state,
                          type);
        }
    } else if (strcmp(signal_name, "DeleteSurroundingText") == 0) {
        guint32 nchar;
        gint32 offset;
        g_variant_get(parameters, "(iu)", &offset, &nchar);
        g_signal_emit(user_data, signals[DELETE_SURROUNDING_TEXT_SIGNAL], 0,
                      offset, nchar);
    } else if (strcmp(signal_name, "UpdateClientSideUI") == 0) {
        const gchar *auxup, *auxdown, *preedit, *candidate, *imname;
        int cursor;
        g_variant_get(parameters, "(sssssi)", &auxup, &auxdown, &preedit,
                      &candidate, &imname, &cursor);
        g_signal_emit(user_data, signals[UPDATE_CLIENT_SIDE_UI_SIGNAL], 0,
                      auxup, auxdown, preedit, candidate, imname, cursor);
    } else if (strcmp(signal_name, "UpdateFormattedPreedit") == 0) {
        int cursor_pos;
        GPtrArray *array = g_ptr_array_new_with_free_func(_item_free);
        GVariantIter *iter;
        g_variant_get(parameters, "(a(si)i)", &iter, &cursor_pos);

        gchar *string;
        int type;
        while (g_variant_iter_next(iter, "(si)", &string, &type, NULL)) {
            FcitxPreeditItem *item = g_malloc0(sizeof(FcitxPreeditItem));
            item->string = strdup(string);
            if (self->priv->is_portal) {
                // revert under line to fcitx 4 style.
                item->type = type ^ (1 << 3);
            } else {
                item->type = type;
            }
            g_ptr_array_add(array, item);
            g_free(string);
        }
        g_variant_iter_free(iter);
        g_signal_emit(user_data, signals[UPDATED_FORMATTED_PREEDIT_SIGNAL], 0,
                      array, cursor_pos);
        g_ptr_array_free(array, TRUE);
    }
}

static void fcitx_client_class_init(FcitxClientClass *klass) {
    GObjectClass *gobject_class;

    gobject_class = G_OBJECT_CLASS(klass);
    gobject_class->set_property = fcitx_client_set_property;
    gobject_class->dispose = fcitx_client_dispose;
    gobject_class->finalize = fcitx_client_finalize;
    gobject_class->constructed = fcitx_client_constructed;

    g_type_class_add_private(klass, sizeof(FcitxClientPrivate));

    g_object_class_install_property(
        gobject_class, PROP_CONNECTION,
        g_param_spec_object("connection", "Fcitx Connection",
                            "Fcitx Connection", FCITX_TYPE_CONNECTION,
                            G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));

    /* install signals */
    /**
     * FcitxClient::connected:
     * @self: A #FcitxClient
     *
     * Emit when connected to fcitx and created ic
     */
    signals[CONNECTED_SIGNAL] =
        g_signal_new("connected", FCITX_TYPE_CLIENT, G_SIGNAL_RUN_LAST, 0, NULL,
                     NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0);

    /**
     * FcitxClient::disconnected:
     * @self: A #FcitxClient
     *
     * Emit when disconnected from fcitx
     */
    signals[DISCONNECTED_SIGNAL] =
        g_signal_new("disconnected", FCITX_TYPE_CLIENT, G_SIGNAL_RUN_LAST, 0,
                     NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0);

    /**
     * FcitxClient::enable-im:
     * @self: A #FcitxClient
     *
     * Emit when input method is enabled
     */
    signals[ENABLE_IM_SIGNAL] =
        g_signal_new("enable-im", FCITX_TYPE_CLIENT, G_SIGNAL_RUN_LAST, 0, NULL,
                     NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0);
    /**
     * FcitxClient::close-im:
     * @self: A #FcitxClient
     *
     * Emit when input method is closed
     */
    signals[CLOSE_IM_SIGNAL] =
        g_signal_new("close-im", FCITX_TYPE_CLIENT, G_SIGNAL_RUN_LAST, 0, NULL,
                     NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0);
    /**
     * FcitxClient::forward-key:
     * @self: A #FcitxClient
     * @keyval: key value
     * @state: key state
     * @type: event type
     *
     * Emit when input method ask for forward a key
     */
    signals[FORWARD_KEY_SIGNAL] =
        g_signal_new("forward-key", FCITX_TYPE_CLIENT, G_SIGNAL_RUN_LAST, 0,
                     NULL, NULL, fcitx_marshall_VOID__UINT_UINT_INT,
                     G_TYPE_NONE, 3, G_TYPE_UINT, G_TYPE_INT, G_TYPE_INT);
    /**
     * FcitxClient::commit-string:
     * @self: A #FcitxClient
     * @string: string to be committed
     *
     * Emit when input method commit one string
     */
    signals[COMMIT_STRING_SIGNAL] = g_signal_new(
        "commit-string", FCITX_TYPE_CLIENT, G_SIGNAL_RUN_LAST, 0, NULL, NULL,
        g_cclosure_marshal_VOID__STRING, G_TYPE_NONE, 1, G_TYPE_STRING);

    /**
     * FcitxClient::delete-surrounding-text:
     * @self: A #FcitxClient
     * @cursor: deletion start
     * @len: deletion length
     *
     * Emit when input method need to delete surrounding text
     */
    signals[DELETE_SURROUNDING_TEXT_SIGNAL] = g_signal_new(
        "delete-surrounding-text", FCITX_TYPE_CLIENT, G_SIGNAL_RUN_LAST, 0,
        NULL, NULL, fcitx_marshall_VOID__INT_UINT, G_TYPE_NONE, 2, G_TYPE_INT,
        G_TYPE_UINT);

    /**
     * FcitxClient::update-client-side-ui:
     * @self: A #FcitxClient
     * @auxup: (transfer none): Aux up string
     * @auxdown: (transfer none): Aux down string
     * @preedit: (transfer none): preedit string
     * @candidateword: (transfer none): candidateword in one line
     * @imname: (transfer none): input method name
     * @cursor_pos: cursor position
     *
     * Emit when input method need to update client side ui
     */
    signals[UPDATE_CLIENT_SIDE_UI_SIGNAL] = g_signal_new(
        "update-client-side-ui", FCITX_TYPE_CLIENT, G_SIGNAL_RUN_LAST, 0, NULL,
        NULL, fcitx_marshall_VOID__STRING_STRING_STRING_STRING_STRING_INT,
        G_TYPE_NONE, 6, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING,
        G_TYPE_STRING, G_TYPE_STRING, G_TYPE_INT);

    /**
     * FcitxClient::update-formatted-preedit:
     * @self: A #FcitxClient
     * @preedit: (transfer none) (element-type FcitxPreeditItem): An
     * #FcitxPreeditItem List
     * @cursor: cursor position by utf8 byte
     *
     * Emit when input method need to delete surrounding text
     */
    signals[UPDATED_FORMATTED_PREEDIT_SIGNAL] = g_signal_new(
        "update-formatted-preedit", FCITX_TYPE_CLIENT, G_SIGNAL_RUN_LAST, 0,
        NULL, NULL, fcitx_marshall_VOID__BOXED_INT, G_TYPE_NONE, 2,
        G_TYPE_PTR_ARRAY, G_TYPE_INT);
}

/**
 * fcitx_client_new:
 *
 * New a #FcitxClient
 *
 * Returns: A newly allocated #FcitxClient
 **/
FCITX_EXPORT_API
FcitxClient *fcitx_client_new() {
    FcitxClient *self = g_object_new(FCITX_TYPE_CLIENT, NULL);
    return FCITX_CLIENT(self);
}

/**
 * fcitx_client_new_with_connection:
 * @connection: the #FcitxConnection to be used with this client
 *
 * New a #FcitxClient
 *
 * Returns: A newly allocated #FcitxClient
 **/
FCITX_EXPORT_API
FcitxClient *fcitx_client_new_with_connection(FcitxConnection *connection) {
    FcitxClient *self =
        g_object_new(FCITX_TYPE_CLIENT, "connection", connection, NULL);
    return FCITX_CLIENT(self);
}

FCITX_EXPORT_API
void fcitx_client_set_display(FcitxClient *self, const gchar *display) {
    g_free(self->priv->display);
    self->priv->display = g_strdup(display);
}

/**
 * fcitx_client_is_valid:
 * @self: A #FcitxClient
 *
 * Check #FcitxClient is valid to communicate with Fcitx
 *
 * Returns: #FcitxClient is valid or not
 **/
FCITX_EXPORT_API
gboolean fcitx_client_is_valid(FcitxClient *self) {
    return self->priv->icproxy != NULL;
}

static void fcitx_client_set_property(GObject *gobject, guint prop_id,
                                      const GValue *value, GParamSpec *pspec) {
    FcitxClient *self = FCITX_CLIENT(gobject);
    FcitxConnection *connection;
    switch (prop_id) {
    case PROP_CONNECTION:
        connection = g_value_get_object(value);
        if (connection) {
            self->priv->connection = connection;
            g_object_ref_sink(self->priv->connection);
        }
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(gobject, prop_id, pspec);
        break;
    }
}

static void _fcitx_client_clean_up(FcitxClient *self,
                                   gboolean dont_emit_disconn) {
    self->priv->is_portal = FALSE;
    if (self->priv->cancellable) {
        g_cancellable_cancel(self->priv->cancellable);
        g_object_unref(self->priv->cancellable);
        self->priv->cancellable = NULL;
    }

    if (self->priv->improxy) {
        g_object_unref(self->priv->improxy);
        self->priv->improxy = NULL;
    }

    g_free(self->priv->icname);
    self->priv->icname = NULL;

    if (self->priv->icproxy) {
        g_signal_handlers_disconnect_by_func(
            self->priv->icproxy, G_CALLBACK(_fcitx_client_g_signal), self);
        g_object_unref(self->priv->icproxy);
        self->priv->icproxy = NULL;
        if (!dont_emit_disconn)
            g_signal_emit(self, signals[DISCONNECTED_SIGNAL], 0);
    }
}

// kate: indent-mode cstyle; replace-tabs on;
