/* 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 "AsyncClipboardRequest.h"
#include "RetrievalContextX11.h"
#include "mozilla/TimeStamp.h"
#include "mozilla/WidgetUtilsGtk.h"

#include <gtk/gtk.h>

// For manipulation of the X event queue
#include <X11/Xlib.h>
#include <poll.h>
#include <gdk/gdkx.h>
#include <sys/time.h>
#include <sys/types.h>
#include <errno.h>
#include <unistd.h>
#include "X11UndefineNone.h"

using namespace mozilla;
using namespace mozilla::widget;

constinit ClipboardTargets RetrievalContextX11::sClipboardTargets;
constinit ClipboardTargets RetrievalContextX11::sPrimaryTargets;

RetrievalContextX11::RetrievalContextX11() = default;

RetrievalContextX11::~RetrievalContextX11() {
  sClipboardTargets.Clear();
  sPrimaryTargets.Clear();
}

static void DispatchSelectionNotifyEvent(GtkWidget* widget, XEvent* xevent) {
  GdkWindow* window = gtk_widget_get_window(widget);
  if (window) {
    GdkEvent event = {};
    event.selection.type = GDK_SELECTION_NOTIFY;
    event.selection.window = window;
    event.selection.selection =
        gdk_x11_xatom_to_atom(xevent->xselection.selection);
    event.selection.target = gdk_x11_xatom_to_atom(xevent->xselection.target);
    event.selection.property =
        gdk_x11_xatom_to_atom(xevent->xselection.property);
    event.selection.time = xevent->xselection.time;

    gtk_widget_event(widget, &event);
  }
}

static void DispatchPropertyNotifyEvent(GtkWidget* widget, XEvent* xevent) {
  GdkWindow* window = gtk_widget_get_window(widget);
  if (window && ((gdk_window_get_events(window)) & GDK_PROPERTY_CHANGE_MASK)) {
    GdkEvent event = {};
    event.property.type = GDK_PROPERTY_NOTIFY;
    event.property.window = window;
    event.property.atom = gdk_x11_xatom_to_atom(xevent->xproperty.atom);
    event.property.time = xevent->xproperty.time;
    event.property.state = xevent->xproperty.state;

    gtk_widget_event(widget, &event);
  }
}

struct checkEventContext {
  GtkWidget* cbWidget;
  Atom selAtom;
};

static Bool checkEventProc(Display* display, XEvent* event, XPointer arg) {
  checkEventContext* context = (checkEventContext*)arg;

  if (event->xany.type == SelectionNotify ||
      (event->xany.type == PropertyNotify &&
       event->xproperty.atom == context->selAtom)) {
    GdkWindow* cbWindow = gdk_x11_window_lookup_for_display(
        gdk_x11_lookup_xdisplay(display), event->xany.window);
    if (cbWindow) {
      GtkWidget* cbWidget = nullptr;
      gdk_window_get_user_data(cbWindow, (gpointer*)&cbWidget);
      if (cbWidget && GTK_IS_WIDGET(cbWidget)) {
        context->cbWidget = cbWidget;
        return X11True;
      }
    }
  }

  return X11False;
}

ClipboardData RetrievalContextX11::WaitForClipboardData(
    ClipboardDataType aDataType, int32_t aWhichClipboard,
    const char* aMimeType) {
  AsyncGtkClipboardRequest request(aDataType, aWhichClipboard, aMimeType);
  if (request.HasCompleted()) {
    // the request completed synchronously
    return request.TakeResult();
  }

  GdkDisplay* gdkDisplay = gdk_display_get_default();
  // gdk_display_get_default() returns null on headless
  if (widget::GdkIsX11Display(gdkDisplay)) {
    Display* xDisplay = GDK_DISPLAY_XDISPLAY(gdkDisplay);
    checkEventContext context;
    context.cbWidget = nullptr;
    context.selAtom =
        gdk_x11_atom_to_xatom(gdk_atom_intern("GDK_SELECTION", FALSE));

    // Send X events which are relevant to the ongoing selection retrieval
    // to the clipboard widget.  Wait until either the operation completes, or
    // we hit our timeout.  All other X events remain queued.

    int poll_result;

    struct pollfd pfd;
    pfd.fd = ConnectionNumber(xDisplay);
    pfd.events = POLLIN;
    TimeStamp start = TimeStamp::Now();

    do {
      XEvent xevent;

      while (XCheckIfEvent(xDisplay, &xevent, checkEventProc,
                           (XPointer)&context)) {
        if (xevent.xany.type == SelectionNotify)
          DispatchSelectionNotifyEvent(context.cbWidget, &xevent);
        else
          DispatchPropertyNotifyEvent(context.cbWidget, &xevent);

        if (request.HasCompleted()) {
          return request.TakeResult();
        }
      }

      TimeStamp now = TimeStamp::Now();
      int timeout = std::max<int>(
          0, kClipboardTimeout / 1000 - (now - start).ToMilliseconds());
      poll_result = poll(&pfd, 1, timeout);
    } while ((poll_result == 1 && (pfd.revents & (POLLHUP | POLLERR)) == 0) ||
             (poll_result == -1 && errno == EINTR));
  }

  MOZ_CLIPBOARD_LOG("exceeded clipboard timeout");
  return {};
}

ClipboardTargets RetrievalContextX11::GetTargetsImpl(int32_t aWhichClipboard) {
  MOZ_CLIPBOARD_LOG("RetrievalContextX11::GetTargetsImpl(%s)\n",
                    aWhichClipboard == nsClipboard::kSelectionClipboard
                        ? "primary"
                        : "clipboard");
  return WaitForClipboardData(ClipboardDataType::Targets, aWhichClipboard)
      .ExtractTargets();
}

ClipboardTargets RetrievalContextX11::GetTargets(int32_t aWhichClipboard) {
  MOZ_CLIPBOARD_LOG("RetrievalContextX11::GetTargets(%s)\n",
                    aWhichClipboard == nsClipboard::kSelectionClipboard
                        ? "primary"
                        : "clipboard");
  ClipboardTargets& storedTargets =
      (aWhichClipboard == nsClipboard::kSelectionClipboard) ? sPrimaryTargets
                                                            : sClipboardTargets;
  if (!storedTargets) {
    MOZ_CLIPBOARD_LOG("  getting targets from system");
    storedTargets.Set(GetTargetsImpl(aWhichClipboard));
  } else {
    MOZ_CLIPBOARD_LOG("  using cached targets");
  }
  return storedTargets.Clone();
}

void RetrievalContextX11::ClearCachedTargets(int32_t aWhichClipboard) {
  MOZ_CLIPBOARD_LOG("RetrievalContextX11::ClearCachedTargets(%s)",
                    aWhichClipboard == nsClipboard::kSelectionClipboard
                        ? "primary"
                        : "clipboard");
  if (aWhichClipboard == nsClipboard::kSelectionClipboard) {
    sPrimaryTargets.Clear();
  } else {
    sClipboardTargets.Clear();
  }
}

ClipboardData RetrievalContextX11::GetClipboardData(const char* aMimeType,
                                                    int32_t aWhichClipboard) {
  MOZ_CLIPBOARD_LOG("RetrievalContextX11::GetClipboardData(%s) MIME %s\n",
                    aWhichClipboard == nsClipboard::kSelectionClipboard
                        ? "primary"
                        : "clipboard",
                    aMimeType);

  return WaitForClipboardData(ClipboardDataType::Data, aWhichClipboard,
                              aMimeType);
}

GUniquePtr<char> RetrievalContextX11::GetClipboardText(
    int32_t aWhichClipboard) {
  MOZ_CLIPBOARD_LOG("RetrievalContextX11::GetClipboardText(%s)\n",
                    aWhichClipboard == nsClipboard::kSelectionClipboard
                        ? "primary"
                        : "clipboard");

  return WaitForClipboardData(ClipboardDataType::Text, aWhichClipboard)
      .ExtractText();
}
