/* SPDX-License-Identifier: Zlib */

#include "links.h"

#include <glib.h>
#include <glib/gi18n.h>
#include <girara/utils.h>
#include <girara-gtk/session.h>
#include <girara-gtk/settings.h>

#include "adjustment.h"
#include "zathura.h"
#include "document.h"
#include "document-widget.h"
#include "utils.h"
#include "page.h"
#include "render.h"

struct zathura_link_s {
  zathura_rectangle_t position; /**< Position of the link */
  zathura_link_target_t target; /**< Link target */
  zathura_link_type_t type;     /**< Link type */
};

zathura_link_t* zathura_link_new(zathura_link_type_t type, zathura_rectangle_t position, zathura_link_target_t target) {
  zathura_link_t* link = g_try_malloc0(sizeof(zathura_link_t));
  if (link == NULL) {
    return NULL;
  }

  link->position = position;
  link->target   = target;
  link->type     = type;

  /* duplicate target.value if necessary */
  switch (type) {
  case ZATHURA_LINK_NONE:
  case ZATHURA_LINK_GOTO_DEST:
    if (target.value != NULL) {
      link->target.value = g_strdup(target.value);
    }
    break;
  case ZATHURA_LINK_GOTO_REMOTE:
  case ZATHURA_LINK_URI:
  case ZATHURA_LINK_LAUNCH:
  case ZATHURA_LINK_NAMED:
    /* target.value is required for these cases */
    if (target.value == NULL) {
      g_free(link);
      return NULL;
    }

    link->target.value = g_strdup(target.value);
    break;
  default:
    g_free(link);
    return NULL;
  }

  return link;
}

void zathura_link_free(zathura_link_t* link) {
  if (link == NULL) {
    return;
  }

  switch (link->type) {
  case ZATHURA_LINK_NONE:
  case ZATHURA_LINK_GOTO_DEST:
  case ZATHURA_LINK_GOTO_REMOTE:
  case ZATHURA_LINK_URI:
  case ZATHURA_LINK_LAUNCH:
  case ZATHURA_LINK_NAMED:
    if (link->target.value != NULL) {
      g_free(link->target.value);
    }
    break;
  default:
    break;
  }

  g_free(link);
}

zathura_link_type_t zathura_link_get_type(zathura_link_t* link) {
  if (link == NULL) {
    return ZATHURA_LINK_INVALID;
  }

  return link->type;
}

zathura_rectangle_t zathura_link_get_position(zathura_link_t* link) {
  if (link == NULL) {
    const zathura_rectangle_t position = {0, 0, 0, 0};
    return position;
  }

  return link->position;
}

zathura_link_target_t zathura_link_get_target(zathura_link_t* link) {
  if (link == NULL) {
    const zathura_link_target_t target = {0, NULL, 0, 0, 0, 0, 0, 0};
    return target;
  }

  return link->target;
}

static void link_goto_dest(zathura_t* zathura, const zathura_link_t* link) {
  if (link->target.destination_type == ZATHURA_LINK_DESTINATION_UNKNOWN) {
    girara_warning("link destination type unknown");
    return;
  }

  bool link_zoom = true;
  girara_setting_get(zathura->ui.session, "link-zoom", &link_zoom);

  zathura_document_t* document = zathura_get_document(zathura);
  if (link->target.zoom >= DBL_EPSILON && link_zoom == true) {
    zathura_document_set_zoom(document, zathura_correct_zoom_value(zathura->ui.session, link->target.zoom));
    adjust_view(zathura);
    zathura_document_widget_render_all(zathura->ui.document_widget);
  }

  /* get page */
  zathura_page_t* page = zathura_document_get_page(document, link->target.page_number);
  if (page == NULL) {
    girara_warning("link to non-existing page %u", link->target.page_number);
    return;
  }

  /* compute the position with the page aligned to the top and left
     of the viewport */
  double pos_x = 0;
  double pos_y = 0;
  page_number_to_position(zathura, link->target.page_number, 0.0, 0.0, &pos_x, &pos_y);

  /* correct to place the target position at the top of the viewport     */
  /* NOTE: link->target is in page units, needs to be scaled and rotated */
  unsigned int cell_height = 0;
  unsigned int cell_width  = 0;
  zathura_document_widget_get_cell_size(ZATHURA_DOCUMENT_WIDGET(zathura->ui.document_widget), link->target.page_number,
                                        &cell_height, &cell_width);

  unsigned int doc_height = 0;
  unsigned int doc_width  = 0;
  zathura_document_widget_get_document_size(ZATHURA_DOCUMENT_WIDGET(zathura->ui.document_widget), &doc_height,
                                            &doc_width);

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

  /* scale and rotate */
  const double scale = zathura_document_get_scale(document);
  double shiftx      = link->target.left * scale / cell_width;
  double shifty      = link->target.top * scale / cell_height;
  page_calc_position(document, shiftx, shifty, &shiftx, &shifty);

  /* shift the position or set to auto */
  if (link->target.destination_type == ZATHURA_LINK_DESTINATION_XYZ && link->target.left != -1 &&
      link_hadjust == true) {
    pos_x += shiftx * cell_width / doc_width;
  } else {
    pos_x = -1; /* -1 means automatic */
  }

  if (link->target.destination_type == ZATHURA_LINK_DESTINATION_XYZ && link->target.top != -1) {
    pos_y += shifty * cell_height / doc_height;
  } else {
    pos_y = -1; /* -1 means automatic */
  }

  /* move to position */
  zathura_jumplist_add(zathura);
  zathura_document_set_current_page_number(document, link->target.page_number);
  position_set(zathura, pos_x, pos_y);
  zathura_jumplist_add(zathura);
}

#ifndef WITH_SANDBOX
static void link_remote(zathura_t* zathura, const char* file) {
  if (zathura_has_document(zathura) == false || file == NULL) {
    return;
  }

  const char* path     = zathura_document_get_path(zathura_get_document(zathura));
  g_autofree char* dir = g_path_get_dirname(path);
  g_autofree char* uri = g_build_filename(file, NULL);

  char* argv[] = {*zathura->global.arguments, uri, NULL};

  g_autoptr(GError) error = NULL;
  if (g_spawn_async(dir, argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, NULL, &error) == FALSE) {
    girara_error("Failed to execute command: %s", error->message);
  }
}

static void link_launch(zathura_t* zathura, const char* link) {
  if (link == NULL) {
    return;
  }

  const char* document = zathura_document_get_path(zathura_get_document(zathura));
  g_autofree char* dir = g_path_get_dirname(document);

  if (girara_xdg_open_with_working_directory(link, dir) == false) {
    girara_notify(zathura->ui.session, GIRARA_ERROR, _("Failed to run xdg-open."));
  }
}

/**
 * Context passed to the external link confirmation dialog callback
 */
typedef struct {
  zathura_t* zathura;
  zathura_link_type_t type;
  char* value;         /**< Owned copy of the link target */
  gulong hide_handler; /**< id of inputbar "hide" handler, 0 if none */
} link_confirm_data_t;

static void link_confirm_data_free(link_confirm_data_t* data) {
  if (data == NULL) {
    return;
  }
  if (data->hide_handler != 0) {
    g_signal_handler_disconnect(data->zathura->ui.session->gtk.inputbar_dialog, data->hide_handler);
  }
  g_free(data->value);
  g_free(data);
}

static void cb_link_confirm_hide(GtkWidget* UNUSED(w), void* data) {
  link_confirm_data_free(data);
}

/**
 * Callback for the external link confirmation dialog
 * Opens the link only if the user pressed Enter with empty input or 'y'
 */
static gboolean cb_link_confirm(GtkEntry* entry, void* data) {
  link_confirm_data_t* ctx = data;
  if (entry == NULL || ctx == NULL) {
    link_confirm_data_free(ctx);
    return true;
  }

  g_autofree char* input = gtk_editable_get_chars(GTK_EDITABLE(entry), 0, -1);

  /* Accept: empty string (bare Enter), or confirmation string */
  const bool confirmed =
      (input == NULL || input[0] == '\0' || g_strcmp0(input, _("y")) == 0 || g_strcmp0(input, _("Y")) == 0);

  if (confirmed) {
    switch (ctx->type) {
    case ZATHURA_LINK_GOTO_REMOTE:
      link_remote(ctx->zathura, ctx->value);
      break;
    case ZATHURA_LINK_URI:
    case ZATHURA_LINK_LAUNCH: {
      link_launch(ctx->zathura, ctx->value);
      break;
    }
    default:
      break;
    }
  } else {
    girara_notify(ctx->zathura->ui.session, GIRARA_INFO, _("Cancelled."));
  }

  link_confirm_data_free(ctx);
  return true;
}

/**
 * Prompt the user for confirmation before opening an external link
 */
static gboolean link_confirm_spawn(void* data) {
  link_confirm_data_t* ctx = data;

  g_autofree gchar* escaped = g_markup_escape_text(ctx->value, -1);
  g_autofree gchar* prompt  = g_strdup_printf(_("Open external link <b>%s</b>? [Y/n]"), escaped);

  ctx->hide_handler =
      g_signal_connect(ctx->zathura->ui.session->gtk.inputbar_dialog, "hide", G_CALLBACK(cb_link_confirm_hide), ctx);
  girara_dialog(ctx->zathura->ui.session, prompt, false, NULL, cb_link_confirm, ctx);
  return FALSE;
}

static void link_confirm(zathura_t* zathura, zathura_link_type_t type, const char* value) {
  if (value == NULL) {
    return;
  }

  link_confirm_data_t* ctx = g_try_malloc0(sizeof(link_confirm_data_t));
  if (ctx == NULL) {
    return;
  }

  ctx->zathura = zathura;
  ctx->type    = type;
  ctx->value   = g_strdup(value);

  gdk_threads_add_idle(link_confirm_spawn, ctx);
}
#endif

void zathura_link_evaluate(zathura_t* zathura, zathura_link_t* link) {
  if (zathura_has_document(zathura) == false || link == NULL) {
    return;
  }

#ifdef WITH_SANDBOX
  if (link->type != ZATHURA_LINK_GOTO_DEST) {
    girara_notify(zathura->ui.session, GIRARA_ERROR,
                  _("Opening external applications in strict sandbox mode is not permitted"));
    return;
  }
#endif

  switch (link->type) {
  case ZATHURA_LINK_GOTO_DEST:
    girara_debug("Going to link destination: page: %d", link->target.page_number);
    link_goto_dest(zathura, link);
    break;
#ifndef WITH_SANDBOX
  case ZATHURA_LINK_GOTO_REMOTE:
    girara_debug("Going to remote destination: %s", link->target.value);
    link_confirm(zathura, link->type, link->target.value);
    break;
  case ZATHURA_LINK_URI:
    girara_debug("Opening URI: %s", link->target.value);
    link_confirm(zathura, link->type, link->target.value);
    break;
  case ZATHURA_LINK_LAUNCH:
    girara_debug("Launching link: %s", link->target.value);
    link_confirm(zathura, link->type, link->target.value);
    break;
#endif
  default:
    girara_error("Unhandled link type: %d", link->type);
    break;
  }
}

void zathura_link_display(zathura_t* zathura, zathura_link_t* link) {
  zathura_link_type_t type     = zathura_link_get_type(link);
  zathura_link_target_t target = zathura_link_get_target(link);
  switch (type) {
  case ZATHURA_LINK_GOTO_DEST:
    girara_notify(zathura->ui.session, GIRARA_INFO, _("Link: page %d"), target.page_number);
    break;
  case ZATHURA_LINK_GOTO_REMOTE:
  case ZATHURA_LINK_URI:
  case ZATHURA_LINK_LAUNCH:
  case ZATHURA_LINK_NAMED: {
    g_autofree gchar* escaped_value = g_markup_escape_text(target.value, -1);
    girara_notify(zathura->ui.session, GIRARA_INFO, _("Link: %s"), escaped_value);
    break;
  }
  default:
    girara_notify(zathura->ui.session, GIRARA_ERROR, _("Link: Invalid"));
  }
}

void zathura_link_copy(zathura_t* zathura, zathura_link_t* link, GdkAtom* selection) {
  zathura_link_type_t type     = zathura_link_get_type(link);
  zathura_link_target_t target = zathura_link_get_target(link);
  switch (type) {
  case ZATHURA_LINK_GOTO_DEST: {
    g_autofree gchar* tmp = g_strdup_printf("%d", target.page_number);
    gtk_clipboard_set_text(gtk_clipboard_get(*selection), tmp, -1);
    girara_notify(zathura->ui.session, GIRARA_INFO, _("Copied page number: %d"), target.page_number);
    break;
  }
  case ZATHURA_LINK_GOTO_REMOTE:
  case ZATHURA_LINK_URI:
  case ZATHURA_LINK_LAUNCH:
  case ZATHURA_LINK_NAMED: {
    gtk_clipboard_set_text(gtk_clipboard_get(*selection), target.value, -1);
    g_autofree gchar* escaped_value = g_markup_escape_text(target.value, -1);
    girara_notify(zathura->ui.session, GIRARA_INFO, _("Copied link: %s"), escaped_value);
    break;
  }
  default:
    girara_notify(zathura->ui.session, GIRARA_ERROR, _("Link: Invalid"));
  }
}
