/* See LICENSE file for license and copyright information */

#include "dbus-interface.h"
#include "dbus-interface-definitions.h"
#include "synctex.h"
#include "macros.h"
#include "zathura.h"
#include "document.h"
#include "utils.h"
#include "adjustment.h"

#include <girara/session.h>
#include <girara/utils.h>
#include <girara/settings.h>
#include <gio/gio.h>
#include <sys/types.h>
#include <string.h>
#include <unistd.h>

G_DEFINE_TYPE(ZathuraDbus, zathura_dbus, G_TYPE_OBJECT)

/* template for bus name */
static const char DBUS_NAME_TEMPLATE[] = "org.pwmt.zathura.PID-%d";
/* object path */
static const char DBUS_OBJPATH[] = "/org/pwmt/zathura";
/* interface name */
static const char DBUS_INTERFACE[] = "org.pwmt.zathura";

typedef struct private_s {
  zathura_t* zathura;
  GDBusNodeInfo* introspection_data;
  GDBusConnection* connection;
  guint owner_id;
  guint registration_id;
} private_t;

#define GET_PRIVATE(obj) \
  (G_TYPE_INSTANCE_GET_PRIVATE((obj), ZATHURA_TYPE_DBUS, private_t))

static const GDBusInterfaceVTable interface_vtable;

static void
finalize(GObject* object)
{
  ZathuraDbus* dbus = ZATHURA_DBUS(object);
  private_t* priv   = GET_PRIVATE(dbus);

  if (priv->connection != NULL && priv->registration_id > 0) {
    g_dbus_connection_unregister_object(priv->connection, priv->registration_id);
  }

  if (priv->owner_id > 0) {
    g_bus_unown_name(priv->owner_id);
  }

  if (priv->introspection_data != NULL) {
    g_dbus_node_info_unref(priv->introspection_data);
  }

  G_OBJECT_CLASS(zathura_dbus_parent_class)->finalize(object);
}

static void
zathura_dbus_class_init(ZathuraDbusClass* class)
{
  /* add private members */
  g_type_class_add_private(class, sizeof(private_t));

  /* overwrite methods */
  GObjectClass* object_class = G_OBJECT_CLASS(class);
  object_class->finalize     = finalize;
}

static void
zathura_dbus_init(ZathuraDbus* dbus)
{
  private_t* priv          = GET_PRIVATE(dbus);
  priv->zathura            = NULL;
  priv->introspection_data = NULL;
  priv->connection         = NULL;
  priv->owner_id           = 0;
  priv->registration_id    = 0;
}

static void
gdbus_connection_closed(GDBusConnection* UNUSED(connection),
    gboolean UNUSED(remote_peer_vanished), GError* error, void* UNUSED(data))
{
  if (error != NULL) {
    girara_debug("D-Bus connection closed: %s", error->message);
  }
}

static void
bus_acquired(GDBusConnection* connection, const gchar* name, void* data)
{
  girara_debug("Bus acquired at '%s'.", name);

  /* register callback for GDBusConnection's closed signal */
  g_signal_connect(G_OBJECT(connection), "closed",
                   G_CALLBACK(gdbus_connection_closed), NULL);

  ZathuraDbus* dbus = data;
  private_t* priv   = GET_PRIVATE(dbus);

  GError* error = NULL;
  priv->registration_id = g_dbus_connection_register_object(
      connection, DBUS_OBJPATH, priv->introspection_data->interfaces[0],
      &interface_vtable, dbus, NULL, &error);
  if (priv->registration_id == 0) {
    girara_warning("Failed to register object on D-Bus connection: %s",
                   error->message);
    g_error_free(error);
    return;
  }

  priv->connection = connection;
}

static void
name_acquired(GDBusConnection* UNUSED(connection), const gchar* name,
              void* UNUSED(data))
{
  girara_debug("Acquired '%s' on session bus.", name);
}

static void
name_lost(GDBusConnection* UNUSED(connection), const gchar* name,
          void* UNUSED(data))
{
  girara_debug("Lost connection or failed to acquire '%s' on session bus.",
               name);
}

ZathuraDbus*
zathura_dbus_new(zathura_t* zathura)
{
  GObject* obj = g_object_new(ZATHURA_TYPE_DBUS, NULL);
  if (obj == NULL) {
    return NULL;
  }

  ZathuraDbus* dbus = ZATHURA_DBUS(obj);
  private_t* priv   = GET_PRIVATE(dbus);
  priv->zathura     = zathura;

  GError* error = NULL;
  priv->introspection_data = g_dbus_node_info_new_for_xml(DBUS_INTERFACE_XML,
                                                          &error);
  if (priv->introspection_data == NULL) {
    girara_warning("Failed to parse introspection data: %s", error->message);
    g_error_free(error);
    g_object_unref(obj);
    return NULL;
  }

  char* well_known_name = g_strdup_printf(DBUS_NAME_TEMPLATE, getpid());
  priv->owner_id = g_bus_own_name(G_BUS_TYPE_SESSION, well_known_name,
                                  G_BUS_NAME_OWNER_FLAGS_NONE, bus_acquired,
                                  name_acquired, name_lost, dbus, NULL);
  g_free(well_known_name);

  return dbus;
}

void
zathura_dbus_edit(ZathuraDbus* edit, unsigned int page, unsigned int x, unsigned int y) {
  private_t* priv = GET_PRIVATE(edit);

  const char* filename = zathura_document_get_path(priv->zathura->document);

  char* input_file = NULL;
  unsigned int line = 0;
  unsigned int column = 0;

  if (synctex_get_input_line_column(filename, page, x, y, &input_file, &line,
        &column) == false) {
    return;
  }

  GError* error = NULL;
  g_dbus_connection_emit_signal(priv->connection, NULL, DBUS_OBJPATH,
    DBUS_INTERFACE, "Edit", g_variant_new("(suu)", input_file, x, y), &error);

  g_free(input_file);

  if (error != NULL) {
    girara_debug("Failed to emit 'Edit' signal: %s", error->message);
    g_error_free(error);
  }
}

/* D-Bus handler */

static void
highlight_rects(zathura_t* zathura, unsigned int page,
                girara_list_t** rectangles)
{
  const unsigned int number_of_pages = zathura_document_get_number_of_pages(zathura->document);

  for (unsigned int p = 0; p != number_of_pages; ++p) {
    GObject* widget = G_OBJECT(zathura->pages[p]);

    g_object_set(widget, "draw-links", FALSE, "search-results", rectangles[p],
                 NULL);
    if (p == page) {
      g_object_set(widget, "search-current", 0, NULL);
    }
  }

  document_draw_search_results(zathura, true);

  girara_list_t* rect_list = rectangles[page];
  if (rect_list == NULL || girara_list_size(rect_list) == 0) {
    girara_debug("No rectangles for the given page. Jumping to page %u.", page);
    page_set(zathura, page);
    return;
  }

  bool search_hadjust = true;
  girara_setting_get(zathura->ui.session, "search-hadjust", &search_hadjust);

  /* compute the position of the center of the page */
  double pos_x = 0;
  double pos_y = 0;
  page_number_to_position(zathura->document, page, 0.5, 0.5, &pos_x, &pos_y);

  /* correction to center the current result                          */
  /* NOTE: rectangle is in viewport units, already scaled and rotated */
  unsigned int cell_height = 0;
  unsigned int cell_width = 0;
  zathura_document_get_cell_size(zathura->document, &cell_height, &cell_width);

  unsigned int doc_height = 0;
  unsigned int doc_width = 0;
  zathura_document_get_document_size(zathura->document, &doc_height, &doc_width);

  /* Need to adjust rectangle to page scale and orientation */
  zathura_page_t* doc_page = zathura_document_get_page(zathura->document, page);
  zathura_rectangle_t* rect = girara_list_nth(rect_list, 0);
  if (rect == NULL) {
    girara_debug("List of rectangles is broken. Jumping to page %u.", page);
    page_set(zathura, page);
    return;
  }

  zathura_rectangle_t rectangle = recalc_rectangle(doc_page, *rect);

  /* compute the center of the rectangle, which will be aligned to the center
     of the viewport */
  double center_x = (rectangle.x1 + rectangle.x2) / 2;
  double center_y = (rectangle.y1 + rectangle.y2) / 2;

  pos_y += (center_y - (double)cell_height/2) / (double)doc_height;
  if (search_hadjust == true) {
    pos_x += (center_x - (double)cell_width/2) / (double)doc_width;
  }

  /* move to position */
  girara_debug("Jumping to page %u position (%f, %f).", page, pos_x, pos_y);
  zathura_jumplist_add(zathura);
  position_set(zathura, pos_x, pos_y);
  zathura_jumplist_add(zathura);
}

static void
handle_method_call(GDBusConnection* UNUSED(connection),
                   const gchar* UNUSED(sender), const gchar* object_path,
                   const gchar* interface_name, const gchar* method_name,
                   GVariant* parameters, GDBusMethodInvocation* invocation,
                   void* data)
{
  ZathuraDbus* dbus = data;
  private_t* priv   = GET_PRIVATE(dbus);

  girara_debug("Handling call '%s.%s' on '%s'.", interface_name, method_name,
               object_path);

  /* methods that work without open document */
  if (g_strcmp0(method_name, "OpenDocument") == 0) {
    gchar* filename = NULL;
    gchar* password = NULL;
    gint page = ZATHURA_PAGE_NUMBER_UNSPECIFIED;
    g_variant_get(parameters, "(ssi)", &filename, &password, &page);

    document_close(priv->zathura, false);
    const bool ret = document_open(priv->zathura, filename,
                                   strlen(password) > 0 ? password : NULL,
                                   page);
    g_free(filename);
    g_free(password);

    GVariant* result = g_variant_new("(b)", ret);
    g_dbus_method_invocation_return_value(invocation, result);
    return;
  } else if (g_strcmp0(method_name, "CloseDocument") == 0) {
    const bool ret = document_close(priv->zathura, false);

    GVariant* result = g_variant_new("(b)", ret);
    g_dbus_method_invocation_return_value(invocation, result);
    return;
  }

  if (priv->zathura->document == NULL) {
    g_dbus_method_invocation_return_dbus_error(invocation,
                                               "org.pwmt.zathura.NoOpenDocumen",
                                               "No document has been opened.");
    return;
  }

  const unsigned int number_of_pages = zathura_document_get_number_of_pages(priv->zathura->document);

  /* methods that require an open document */
  if (g_strcmp0(method_name, "GotoPage") == 0) {
    guint page = 0;
    g_variant_get(parameters, "(u)", &page);

    bool ret = true;
    if (page >= number_of_pages) {
      ret = false;
    } else {
      page_set(priv->zathura, page);
    }

    GVariant* result = g_variant_new("(b)", ret);
    g_dbus_method_invocation_return_value(invocation, result);
  } else if (g_strcmp0(method_name, "HighlightRects") == 0) {
    guint page = 0;
    GVariantIter* iter = NULL;
    GVariantIter* secondary_iter = NULL;
    g_variant_get(parameters, "(ua(dddd)a(udddd))", &page, &iter,
                  &secondary_iter);

    if (page >= number_of_pages) {
      girara_debug("Got invalid page number.");
      GVariant* result = g_variant_new("(b)", false);
      g_variant_iter_free(iter);
      g_variant_iter_free(secondary_iter);
      g_dbus_method_invocation_return_value(invocation, result);
      return;
    }

    /* get rectangles */
    girara_list_t** rectangles = g_try_malloc0(number_of_pages * sizeof(girara_list_t*));
    if (rectangles == NULL) {
      g_variant_iter_free(iter);
      g_variant_iter_free(secondary_iter);
      g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR,
                                            G_DBUS_ERROR_NO_MEMORY,
                                            "Failed to allocate memory.");
      return;
    }

    rectangles[page] = girara_list_new2(g_free);
    if (rectangles[page] == NULL) {
      g_free(rectangles);
      g_variant_iter_free(iter);
      g_variant_iter_free(secondary_iter);
      g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR,
                                            G_DBUS_ERROR_NO_MEMORY,
                                            "Failed to allocate memory.");
      return;
    }

    zathura_rectangle_t temp_rect = { 0, 0, 0, 0 };
    while (g_variant_iter_loop(iter, "(dddd)", &temp_rect.x1, &temp_rect.x2,
                               &temp_rect.y1, &temp_rect.y2)) {
      zathura_rectangle_t* rect = g_try_malloc0(sizeof(zathura_rectangle_t));
      if (rect == NULL) {
        g_variant_iter_free(iter);
        g_variant_iter_free(secondary_iter);
        girara_list_free(rectangles[page]);
        g_free(rectangles);
        g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR,
                                              G_DBUS_ERROR_NO_MEMORY,
                                              "Failed to allocate memory.");
        return;
      }

      *rect = temp_rect;
      girara_list_append(rectangles[page], rect);
    }
    g_variant_iter_free(iter);

    /* get secondary rectangles */
    guint temp_page = 0;
    while (g_variant_iter_loop(secondary_iter, "(udddd)", &temp_page,
                               &temp_rect.x1, &temp_rect.x2, &temp_rect.y1,
                               &temp_rect.y2)) {
      if (temp_page >= number_of_pages) {
        /* error out here? */
        girara_debug("Got invalid page number.");
        continue;
      }

      if (rectangles[temp_page] == NULL) {
        rectangles[temp_page] = girara_list_new2(g_free);
      }

      zathura_rectangle_t* rect = g_try_malloc0(sizeof(zathura_rectangle_t));
      if (rect == NULL || rectangles[temp_page] == NULL) {
        g_variant_iter_free(secondary_iter);
        for (unsigned int p = 0; p != number_of_pages; ++p) {
          girara_list_free(rectangles[p]);
        }
        g_free(rectangles);
        g_free(rect);
        g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR,
                                              G_DBUS_ERROR_NO_MEMORY,
                                              "Failed to allocate memory.");
        return;
      }

      *rect = temp_rect;
      girara_list_append(rectangles[temp_page], rect);
    }
    g_variant_iter_free(secondary_iter);

    highlight_rects(priv->zathura, page, rectangles);
    g_free(rectangles);

    GVariant* result = g_variant_new("(b)", true);
    g_dbus_method_invocation_return_value(invocation, result);
  } else if (g_strcmp0(method_name, "SynctexView") == 0) {
    gchar* input_file = NULL;
    guint line = 0;
    guint column = 0;
    g_variant_get(parameters, "(suu)", &input_file, &line, &column);

    unsigned int page = 0;
    girara_list_t* secondary_rects = NULL;
    girara_list_t* rectangles = synctex_rectangles_from_position(
      zathura_document_get_path(priv->zathura->document), input_file, line,
      column, &page, &secondary_rects);
    g_free(input_file);

    if (rectangles == NULL) {
      GVariant* result = g_variant_new("(b)", false);
      g_dbus_method_invocation_return_value(invocation, result);
      return;
    }

    girara_list_t** all_rectangles = g_try_malloc0(number_of_pages * sizeof(girara_list_t*));
    for (unsigned int p = 0; p != number_of_pages; ++p) {
      if (p == page) {
        all_rectangles[p] = rectangles;
      } else {
        all_rectangles[p] = girara_list_new2(g_free);
      }
    }

    if (secondary_rects != NULL) {
      GIRARA_LIST_FOREACH(secondary_rects, synctex_page_rect_t*, iter, rect)
        zathura_rectangle_t* newrect = g_try_malloc0(sizeof(zathura_rectangle_t));
        *newrect = rect->rect;
        girara_list_append(all_rectangles[rect->page], newrect);
      GIRARA_LIST_FOREACH_END(secondary_rects, synctex_page_rect_t*, iter, rect);
    }

    highlight_rects(priv->zathura, page, all_rectangles);

    girara_list_free(secondary_rects);
    g_free(all_rectangles);

    GVariant* result = g_variant_new("(b)", true);
    g_dbus_method_invocation_return_value(invocation, result);

  }
}

static GVariant*
handle_get_property(GDBusConnection* UNUSED(connection),
                    const gchar* UNUSED(sender),
                    const gchar* UNUSED(object_path),
                    const gchar* UNUSED(interface_name),
                    const gchar* property_name, GError** error, void* data)
{
  ZathuraDbus* dbus = data;
  private_t* priv   = GET_PRIVATE(dbus);

  if (priv->zathura->document == NULL) {
    g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "No document open.");
    return NULL;
  }

  if (g_strcmp0(property_name, "filename") == 0) {
    return g_variant_new_string(zathura_document_get_path(priv->zathura->document));
  } else if (g_strcmp0(property_name, "pagenumber") == 0) {
    return g_variant_new_uint32(zathura_document_get_current_page_number(priv->zathura->document));
  } else if (g_strcmp0(property_name, "numberofpages") == 0) {
    return g_variant_new_uint32(zathura_document_get_number_of_pages(priv->zathura->document));
  }

  return NULL;
}

static const GDBusInterfaceVTable interface_vtable =
{
  .method_call  = handle_method_call,
  .get_property = handle_get_property,
  .set_property = NULL
};

static const unsigned int TIMEOUT = 3000;

static bool
call_synctex_view(GDBusConnection* connection, const char* filename,
                  const char* name, const char* input_file, unsigned int line,
                  unsigned int column)
{
  GError* error       = NULL;
  GVariant* vfilename = g_dbus_connection_call_sync(
      connection, name, DBUS_OBJPATH, "org.freedesktop.DBus.Properties", "Get",
      g_variant_new("(ss)", DBUS_INTERFACE, "filename"), G_VARIANT_TYPE("(v)"),
      G_DBUS_CALL_FLAGS_NONE, TIMEOUT, NULL, &error);
  if (vfilename == NULL) {
    girara_error("Failed to query 'filename' property from '%s': %s",
                  name, error->message);
    g_error_free(error);
    return false;
  }

  GVariant* tmp = NULL;
  g_variant_get(vfilename, "(v)", &tmp);
  gchar* remote_filename = g_variant_dup_string(tmp, NULL);
  girara_debug("Filename from '%s': %s", name, remote_filename);
  g_variant_unref(tmp);
  g_variant_unref(vfilename);

  if (g_strcmp0(filename, remote_filename) != 0) {
    g_free(remote_filename);
    return false;
  }

  g_free(remote_filename);

  GVariant* ret = g_dbus_connection_call_sync(
      connection, name, DBUS_OBJPATH, DBUS_INTERFACE, "SynctexView",
      g_variant_new("(suu)", input_file, line, column),
      G_VARIANT_TYPE("(b)"), G_DBUS_CALL_FLAGS_NONE, TIMEOUT, NULL, &error);
  if (ret == NULL) {
    girara_error("Failed to run SynctexView on '%s': %s", name,
                 error->message);
    g_error_free(error);
    return false;
  }

  g_variant_unref(ret);
  return true;
}

static bool
iterate_instances_call_synctex_view(const char* filename,
                                    const char* input_file, unsigned int line,
                                    unsigned int column, pid_t hint)
{
  if (filename == NULL) {
    return false;
  }

  GError* error = NULL;
  GDBusConnection* connection = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL,
                                               &error);
  if (connection == NULL) {
    girara_error("Could not connect to session bus: %s", error->message);
    g_error_free(error);
    return false;
  }

  if (hint != -1) {
    char* well_known_name = g_strdup_printf(DBUS_NAME_TEMPLATE, hint);
    const bool ret = call_synctex_view(connection, filename, well_known_name,
                                       input_file, line, column);
    g_free(well_known_name);
    return ret;
  }

  GVariant* vnames = g_dbus_connection_call_sync(
      connection, "org.freedesktop.DBus", "/org/freedesktop/DBus",
      "org.freedesktop.DBus", "ListNames", NULL, G_VARIANT_TYPE("(as)"),
      G_DBUS_CALL_FLAGS_NONE, TIMEOUT, NULL, &error);
  if (vnames == NULL) {
    girara_error("Could not list available names: %s", error->message);
    g_error_free(error);
    g_object_unref(connection);
    return false;
  }

  GVariantIter* iter = NULL;
  g_variant_get(vnames, "(as)", &iter);

  gchar* name = NULL;
  bool found_one = false;
  while (g_variant_iter_loop(iter, "s", &name) == TRUE) {
    if (g_str_has_prefix(name, "org.pwmt.zathura.PID") == FALSE) {
      continue;
    }
    girara_debug("Found name: %s", name);

    if (call_synctex_view(connection, filename, name, input_file, line, column)
        == true) {
      found_one = true;
    }
  }
  g_variant_iter_free(iter);
  g_variant_unref(vnames);
  g_object_unref(connection);

  return found_one;
}

bool
zathura_dbus_synctex_position(const char* filename, const char* input_file,
                              int line, int column, pid_t hint)
{
  if (filename == NULL || input_file == NULL || line < 0 || column < 0) {
    return false;
  }

  return iterate_instances_call_synctex_view(filename, input_file, line, column, hint);
}

