/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "ApplicationAccessible.h"
#include "mozilla/Likely.h"
#include "nsAccessibilityService.h"
#include "nsMai.h"

#include <atk/atkobject.h>
#include <atk/atkutil.h>
#include <gtk/gtk.h>
#include <string.h>

using namespace mozilla;
using namespace mozilla::a11y;

typedef AtkUtil MaiUtil;
typedef AtkUtilClass MaiUtilClass;

#define MAI_VERSION MOZILLA_VERSION
#define MAI_NAME "Gecko"

extern "C" {
static guint (*gail_add_global_event_listener)(GSignalEmissionHook listener,
                                               const gchar* event_type);
static void (*gail_remove_global_event_listener)(guint remove_listener);
static void (*gail_remove_key_event_listener)(guint remove_listener);
static AtkObject* (*gail_get_root)();
}

struct MaiUtilListenerInfo {
  gint key;
  guint signal_id;
  gulong hook_id;
  // For window create/destory/minimize/maximize/restore/activate/deactivate
  // events, we'll chain gail_util's add/remove_global_event_listener.
  // So we store the listenerid returned by gail's add_global_event_listener
  // in this structure to call gail's remove_global_event_listener later.
  guint gail_listenerid;
};

static GHashTable* sListener_list = nullptr;
static gint sListener_idx = 1;

extern "C" {
static guint add_listener(GSignalEmissionHook listener,
                          const gchar* object_type, const gchar* signal,
                          const gchar* hook_data, guint gail_listenerid = 0) {
  GType type;
  guint signal_id;
  gint rc = 0;

  type = g_type_from_name(object_type);
  if (type) {
    signal_id = g_signal_lookup(signal, type);
    if (signal_id > 0) {
      MaiUtilListenerInfo* listener_info;

      rc = sListener_idx;

      listener_info =
          (MaiUtilListenerInfo*)g_malloc(sizeof(MaiUtilListenerInfo));
      listener_info->key = sListener_idx;
      listener_info->hook_id = g_signal_add_emission_hook(
          signal_id, 0, listener, g_strdup(hook_data), (GDestroyNotify)g_free);
      listener_info->signal_id = signal_id;
      listener_info->gail_listenerid = gail_listenerid;

      g_hash_table_insert(sListener_list, &(listener_info->key), listener_info);
      sListener_idx++;
    } else {
      g_warning("Invalid signal type %s\n", signal);
    }
  } else {
    g_warning("Invalid object type %s\n", object_type);
  }
  return rc;
}

static guint mai_util_add_global_event_listener(GSignalEmissionHook listener,
                                                const gchar* event_type) {
  guint rc = 0;
  gchar** split_string;

  split_string = g_strsplit(event_type, ":", 3);

  if (split_string) {
    if (!strcmp("window", split_string[0])) {
      guint gail_listenerid = 0;
      if (gail_add_global_event_listener) {
        // call gail's function to track gtk native window events
        gail_listenerid = gail_add_global_event_listener(listener, event_type);
      }

      rc = add_listener(listener, "MaiAtkObject", split_string[1], event_type,
                        gail_listenerid);
    } else {
      rc = add_listener(listener, split_string[1], split_string[2], event_type);
    }
    g_strfreev(split_string);
  }
  return rc;
}

static void mai_util_remove_global_event_listener(guint remove_listener) {
  if (remove_listener > 0) {
    MaiUtilListenerInfo* listener_info;
    gint tmp_idx = remove_listener;

    listener_info =
        (MaiUtilListenerInfo*)g_hash_table_lookup(sListener_list, &tmp_idx);

    if (listener_info != nullptr) {
      if (gail_remove_global_event_listener && listener_info->gail_listenerid) {
        gail_remove_global_event_listener(listener_info->gail_listenerid);
      }

      /* Hook id of 0 and signal id of 0 are invalid */
      if (listener_info->hook_id != 0 && listener_info->signal_id != 0) {
        /* Remove the emission hook */
        g_signal_remove_emission_hook(listener_info->signal_id,
                                      listener_info->hook_id);

        /* Remove the element from the hash */
        g_hash_table_remove(sListener_list, &tmp_idx);
      } else {
        g_warning("Invalid listener hook_id %ld or signal_id %d\n",
                  listener_info->hook_id, listener_info->signal_id);
      }
    } else {
      // atk-bridge is initialized with gail (e.g. yelp)
      // try gail_remove_global_event_listener
      if (gail_remove_global_event_listener) {
        return gail_remove_global_event_listener(remove_listener);
      }

      g_warning("No listener with the specified listener id %d",
                remove_listener);
    }
  } else {
    g_warning("Invalid listener_id %d", remove_listener);
  }
}

static AtkKeyEventStruct* atk_key_event_from_gdk_event_key(GdkEventKey* key) {
  AtkKeyEventStruct* event = g_new0(AtkKeyEventStruct, 1);
  switch (key->type) {
    case GDK_KEY_PRESS:
      event->type = ATK_KEY_EVENT_PRESS;
      break;
    case GDK_KEY_RELEASE:
      event->type = ATK_KEY_EVENT_RELEASE;
      break;
    default:
      g_assert_not_reached();
      return nullptr;
  }
  event->state = key->state;
  event->keyval = key->keyval;
  event->length = key->length;
  if (key->string && key->string[0] &&
      g_unichar_isgraph(g_utf8_get_char(key->string))) {
    event->string = key->string;
  } else if (key->type == GDK_KEY_PRESS || key->type == GDK_KEY_RELEASE) {
    event->string = gdk_keyval_name(key->keyval);
  }
  event->keycode = key->hardware_keycode;
  event->timestamp = key->time;

  return event;
}

struct MaiKeyEventInfo {
  AtkKeyEventStruct* key_event;
  gpointer func_data;
};

union AtkKeySnoopFuncPointer {
  AtkKeySnoopFunc func_ptr;
  gpointer data;
};

static gboolean notify_hf(gpointer key, gpointer value, gpointer data) {
  MaiKeyEventInfo* info = (MaiKeyEventInfo*)data;
  AtkKeySnoopFuncPointer atkKeySnoop;
  atkKeySnoop.data = value;
  return (atkKeySnoop.func_ptr)(info->key_event, info->func_data) ? TRUE
                                                                  : FALSE;
}

static void insert_hf(gpointer key, gpointer value, gpointer data) {
  GHashTable* new_table = (GHashTable*)data;
  g_hash_table_insert(new_table, key, value);
}

static GHashTable* sKey_listener_list = nullptr;

static gint mai_key_snooper(GtkWidget* the_widget, GdkEventKey* event,
                            gpointer func_data) {
  /* notify each AtkKeySnoopFunc in turn... */

  MaiKeyEventInfo* info = g_new0(MaiKeyEventInfo, 1);
  gint consumed = 0;
  if (sKey_listener_list) {
    GHashTable* new_hash = g_hash_table_new(nullptr, nullptr);
    g_hash_table_foreach(sKey_listener_list, insert_hf, new_hash);
    info->key_event = atk_key_event_from_gdk_event_key(event);
    info->func_data = func_data;
    consumed = g_hash_table_foreach_steal(new_hash, notify_hf, info);
    g_hash_table_destroy(new_hash);
    g_free(info->key_event);
  }
  g_free(info);
  return (consumed ? 1 : 0);
}

static guint sKey_snooper_id = 0;

static guint mai_util_add_key_event_listener(AtkKeySnoopFunc listener,
                                             gpointer data) {
  if (MOZ_UNLIKELY(!listener)) {
    return 0;
  }

  static guint key = 0;

  if (!sKey_listener_list) {
    sKey_listener_list = g_hash_table_new(nullptr, nullptr);
  }

  // If we have no registered event listeners then we need to (re)install the
  // key event snooper.
  if (g_hash_table_size(sKey_listener_list) == 0) {
    sKey_snooper_id = gtk_key_snooper_install(mai_key_snooper, data);
  }

  AtkKeySnoopFuncPointer atkKeySnoop;
  atkKeySnoop.func_ptr = listener;
  key++;
  g_hash_table_insert(sKey_listener_list, GUINT_TO_POINTER(key),
                      atkKeySnoop.data);
  return key;
}

static void mai_util_remove_key_event_listener(guint remove_listener) {
  if (!sKey_listener_list) {
    // atk-bridge is initialized with gail (e.g. yelp)
    // try gail_remove_key_event_listener
    return gail_remove_key_event_listener(remove_listener);
  }

  g_hash_table_remove(sKey_listener_list, GUINT_TO_POINTER(remove_listener));
  if (g_hash_table_size(sKey_listener_list) == 0) {
    gtk_key_snooper_remove(sKey_snooper_id);
  }
}

static AtkObject* mai_util_get_root() {
  ApplicationAccessible* app = ApplicationAcc();
  if (app) return app->GetAtkObject();

  // We've shutdown, try to use gail instead
  // (to avoid assert in spi_atk_tidy_windows())
  // XXX tbsaunde then why didn't we replace the gail atk_util impl?
  if (gail_get_root) return gail_get_root();

  return nullptr;
}

static const gchar* mai_util_get_toolkit_name() { return MAI_NAME; }

static const gchar* mai_util_get_toolkit_version() { return MAI_VERSION; }

static void _listener_info_destroy(gpointer data) { g_free(data); }

static void window_added(AtkObject* atk_obj, guint index, AtkObject* child) {
  if (!IS_MAI_OBJECT(child)) return;

  static guint id = g_signal_lookup("create", MAI_TYPE_ATK_OBJECT);
  g_signal_emit(child, id, 0);
}

static void window_removed(AtkObject* atk_obj, guint index, AtkObject* child) {
  if (!IS_MAI_OBJECT(child)) return;

  static guint id = g_signal_lookup("destroy", MAI_TYPE_ATK_OBJECT);
  g_signal_emit(child, id, 0);
}

static void UtilInterfaceInit(MaiUtilClass* klass) {
  AtkUtilClass* atk_class;
  gpointer data;

  data = g_type_class_peek(ATK_TYPE_UTIL);
  atk_class = ATK_UTIL_CLASS(data);

  // save gail function pointer
  gail_add_global_event_listener = atk_class->add_global_event_listener;
  gail_remove_global_event_listener = atk_class->remove_global_event_listener;
  gail_remove_key_event_listener = atk_class->remove_key_event_listener;
  gail_get_root = atk_class->get_root;

  atk_class->add_global_event_listener = mai_util_add_global_event_listener;
  atk_class->remove_global_event_listener =
      mai_util_remove_global_event_listener;
  atk_class->add_key_event_listener = mai_util_add_key_event_listener;
  atk_class->remove_key_event_listener = mai_util_remove_key_event_listener;
  atk_class->get_root = mai_util_get_root;
  atk_class->get_toolkit_name = mai_util_get_toolkit_name;
  atk_class->get_toolkit_version = mai_util_get_toolkit_version;

  sListener_list = g_hash_table_new_full(g_int_hash, g_int_equal, nullptr,
                                         _listener_info_destroy);
  // Keep track of added/removed windows.
  AtkObject* root = atk_get_root();
  g_signal_connect(root, "children-changed::add", (GCallback)window_added,
                   nullptr);
  g_signal_connect(root, "children-changed::remove", (GCallback)window_removed,
                   nullptr);
}
}

GType mai_util_get_type() {
  static GType type = 0;

  if (!type) {
    static const GTypeInfo tinfo = {
        sizeof(MaiUtilClass),
        (GBaseInitFunc) nullptr,           /* base init */
        (GBaseFinalizeFunc) nullptr,       /* base finalize */
        (GClassInitFunc)UtilInterfaceInit, /* class init */
        (GClassFinalizeFunc) nullptr,      /* class finalize */
        nullptr,                           /* class data */
        sizeof(MaiUtil),                   /* instance size */
        0,                                 /* nb preallocs */
        (GInstanceInitFunc) nullptr,       /* instance init */
        nullptr                            /* value table */
    };

    type =
        g_type_register_static(ATK_TYPE_UTIL, "MaiUtil", &tinfo, GTypeFlags(0));
  }
  return type;
}
