/**
 * GTK3 FFI — direct calls to libgtk-3.so.0 for window management.
 *
 * Used for begin_resize_drag and begin_move_drag which Electrobun
 * doesn't expose natively. These delegate to the window manager
 * for smooth, zero-CPU resize/move behavior.
 */

import { dlopen, FFIType, ptr, CString } from "bun:ffi";

// GdkWindowEdge values
export const GDK_EDGE = {
  NW: 0, N: 1, NE: 2,
  W: 3,        E: 4,
  SW: 5, S: 6, SE: 7,
} as const;

type GdkEdge = (typeof GDK_EDGE)[keyof typeof GDK_EDGE];

let gtk3: ReturnType<typeof dlopen> | null = null;
let gdk3: ReturnType<typeof dlopen> | null = null;
let x11: ReturnType<typeof dlopen> | null = null;

function getX11() {
  if (x11) return x11;
  try {
    x11 = dlopen("libX11.so.6", {
      XUngrabPointer: { args: [FFIType.ptr, FFIType.u64], returns: FFIType.i32 },
      XFlush: { args: [FFIType.ptr], returns: FFIType.i32 },
      XMoveResizeWindow: { args: [FFIType.ptr, FFIType.u64, FFIType.i32, FFIType.i32, FFIType.u32, FFIType.u32], returns: FFIType.i32 },
    });
    return x11;
  } catch { return null; }
}

function getGdk() {
  if (gdk3) return gdk3;
  try {
    gdk3 = dlopen("libgdk-3.so.0", {
      gdk_display_get_default: { args: [], returns: FFIType.ptr },
      gdk_x11_display_get_xdisplay: { args: [FFIType.ptr], returns: FFIType.ptr },
      gdk_x11_window_get_xid: { args: [FFIType.ptr], returns: FFIType.u64 },
    });
    return gdk3;
  } catch { return null; }
}

/**
 * Release ALL X11 pointer grabs on the default display.
 * This is the SDL2 pattern: XUngrabPointer(dpy, CurrentTime=0) + XFlush.
 * Must be called BEFORE _NET_WM_MOVERESIZE so the WM can grab the pointer.
 */
function releaseX11Grab() {
  const gdkLib = getGdk();
  const x11Lib = getX11();
  if (!gdkLib || !x11Lib) return false;
  try {
    const gdkDisplay = gdkLib.symbols.gdk_display_get_default();
    if (!gdkDisplay) return false;
    const xDisplay = gdkLib.symbols.gdk_x11_display_get_xdisplay(gdkDisplay);
    if (!xDisplay) return false;
    x11Lib.symbols.XUngrabPointer(xDisplay, BigInt(0)); // CurrentTime = 0
    x11Lib.symbols.XFlush(xDisplay);
    return true;
  } catch (err) {
    console.error("[gtk-window] releaseX11Grab failed:", err);
    return false;
  }
}

function getGtk() {
  if (gtk3) return gtk3;
  try {
    gtk3 = dlopen("libgtk-3.so.0", {
      gtk_window_begin_resize_drag: {
        args: [FFIType.ptr, FFIType.i32, FFIType.i32, FFIType.i32, FFIType.i32, FFIType.u32],
        returns: FFIType.void,
      },
      gtk_window_begin_move_drag: {
        args: [FFIType.ptr, FFIType.i32, FFIType.i32, FFIType.i32, FFIType.u32],
        returns: FFIType.void,
      },
      gtk_window_get_resizable: {
        args: [FFIType.ptr],
        returns: FFIType.bool,
      },
      gtk_window_set_resizable: {
        args: [FFIType.ptr, FFIType.bool],
        returns: FFIType.void,
      },
      // GtkWidget — controls minimum size request
      gtk_widget_set_size_request: {
        args: [FFIType.ptr, FFIType.i32, FFIType.i32],
        returns: FFIType.void,
      },
      // GtkWindow — override geometry hints (min/max size sent to WM)
      gtk_window_set_geometry_hints: {
        args: [FFIType.ptr, FFIType.ptr, FFIType.ptr, FFIType.i32],
        returns: FFIType.void,
      },
      gtk_window_resize: {
        args: [FFIType.ptr, FFIType.i32, FFIType.i32],
        returns: FFIType.void,
      },
      gtk_window_move: {
        args: [FFIType.ptr, FFIType.i32, FFIType.i32],
        returns: FFIType.void,
      },
      // Container traversal
      gtk_bin_get_child: {
        args: [FFIType.ptr],
        returns: FFIType.ptr,
      },
      gtk_container_get_children: {
        args: [FFIType.ptr],
        returns: FFIType.ptr, // GList*
      },
      // GtkScrolledWindow — wraps WebView to decouple natural size
      gtk_scrolled_window_new: {
        args: [FFIType.ptr, FFIType.ptr], // hadjustment, vadjustment (both null)
        returns: FFIType.ptr,
      },
      gtk_scrolled_window_set_policy: {
        args: [FFIType.ptr, FFIType.i32, FFIType.i32], // hscrollbar_policy, vscrollbar_policy
        returns: FFIType.void,
      },
      gtk_scrolled_window_set_propagate_natural_width: {
        args: [FFIType.ptr, FFIType.bool],
        returns: FFIType.void,
      },
      gtk_scrolled_window_set_propagate_natural_height: {
        args: [FFIType.ptr, FFIType.bool],
        returns: FFIType.void,
      },
      gtk_scrolled_window_set_min_content_width: {
        args: [FFIType.ptr, FFIType.i32],
        returns: FFIType.void,
      },
      gtk_scrolled_window_set_min_content_height: {
        args: [FFIType.ptr, FFIType.i32],
        returns: FFIType.void,
      },
      // Reparenting
      gtk_container_remove: {
        args: [FFIType.ptr, FFIType.ptr], // container, widget
        returns: FFIType.void,
      },
      gtk_container_add: {
        args: [FFIType.ptr, FFIType.ptr], // container, widget
        returns: FFIType.void,
      },
      gtk_widget_show_all: {
        args: [FFIType.ptr],
        returns: FFIType.void,
      },
      // Get GdkWindow from GtkWidget (needed for X11 window ID)
      gtk_widget_get_window: {
        args: [FFIType.ptr],
        returns: FFIType.ptr,
      },
      // Sensitivity (disable input processing on widget)
      gtk_widget_set_sensitive: {
        args: [FFIType.ptr, FFIType.bool],
        returns: FFIType.void,
      },
      gtk_widget_get_sensitive: {
        args: [FFIType.ptr],
        returns: FFIType.bool,
      },
      // Expand flags
      gtk_widget_set_hexpand: {
        args: [FFIType.ptr, FFIType.bool],
        returns: FFIType.void,
      },
      gtk_widget_set_vexpand: {
        args: [FFIType.ptr, FFIType.bool],
        returns: FFIType.void,
      },
      // Reference counting (prevent widget destruction during reparent)
      g_object_ref: {
        args: [FFIType.ptr],
        returns: FFIType.ptr,
      },
      g_object_unref: {
        args: [FFIType.ptr],
        returns: FFIType.void,
      },
      // GList traversal
      g_list_length: {
        args: [FFIType.ptr],
        returns: FFIType.u32,
      },
      g_list_nth_data: {
        args: [FFIType.ptr, FFIType.u32],
        returns: FFIType.ptr,
      },
      g_list_free: {
        args: [FFIType.ptr],
        returns: FFIType.void,
      },
    });
    return gtk3;
  } catch (err) {
    console.error("[gtk-window] Failed to dlopen libgtk-3.so.0:", err);
    return null;
  }
}

/**
 * Ensure window is resizable at GTK level. Must be called after window creation.
 */
export function ensureResizable(windowPtr: number | bigint): boolean {
  const lib = getGtk();
  if (!lib) return false;
  try {
    const isResizable = lib.symbols.gtk_window_get_resizable(windowPtr as any);
    console.log(`[gtk-window] gtk_window_get_resizable = ${isResizable}`);
    if (!isResizable) {
      console.log("[gtk-window] Window NOT resizable — forcing set_resizable(true)");
      lib.symbols.gtk_window_set_resizable(windowPtr as any, true);
      const nowResizable = lib.symbols.gtk_window_get_resizable(windowPtr as any);
      console.log(`[gtk-window] After set_resizable(true): ${nowResizable}`);
    }
    // Force small min-size on entire widget tree (1×1 per widget, 400×300 via geometry hints)
    console.log("[gtk-window] Forcing small min-size on widget tree");
    forceSmallMinSize(lib, windowPtr as any);
    return true;
  } catch (err) {
    console.error("[gtk-window] ensureResizable failed:", err);
    return false;
  }
}

/**
 * Clear min-size constraints on the widget tree right before resize.
 * WebKitWebView re-propagates content size as minimum on every layout cycle,
 * so we must clear it each time — not just at init.
 */
/**
 * Force small minimum size on the entire widget tree.
 * Key insight (from Codex review): set_size_request(-1, -1) means "use preferred size"
 * which RE-ENABLES the WebView's content-based minimum. Using set_size_request(1, 1)
 * FORCES a 1×1 minimum, overriding the preferred size.
 */
function forceSmallMinSize(lib: NonNullable<typeof gtk3>, windowPtr: any) {
  function walk(widget: any, depth: number) {
    if (!widget || depth > 10) return;
    // Force 1x1 minimum — NOT -1,-1 which means "use preferred size"
    lib.symbols.gtk_widget_set_size_request(widget, 1, 1);
    try {
      const child = lib.symbols.gtk_bin_get_child(widget);
      if (child) walk(child, depth + 1);
    } catch { /* not a GtkBin */ }
    try {
      const list = lib.symbols.gtk_container_get_children(widget);
      if (list) {
        const len = lib.symbols.g_list_length(list);
        for (let i = 0; i < len && i < 20; i++) {
          const child = lib.symbols.g_list_nth_data(list, i);
          if (child) walk(child, depth + 1);
        }
        lib.symbols.g_list_free(list);
      }
    } catch { /* not a GtkContainer */ }
  }
  walk(windowPtr, 0);
  // Set geometry hints: min=400×300 (our real minimum), max=32767×32767
  try {
    const buf = new ArrayBuffer(72);
    const view = new Int32Array(buf);
    view[0] = 400; view[1] = 300; view[2] = 32767; view[3] = 32767;
    lib.symbols.gtk_window_set_geometry_hints(windowPtr, null, ptr(buf) as any, 2 | 4);
  } catch { /* ignore */ }
}

/**
 * Set window frame directly via X11, BYPASSING GTK's size negotiation.
 * GTK receives ConfigureNotify after the fact and adjusts.
 * This prevents the WebView's preferred size from fighting the resize.
 */
export function x11SetFrame(
  windowPtr: number | bigint,
  x: number, y: number, width: number, height: number,
): boolean {
  const gtkLib = getGtk();
  const gdkLib = getGdk();
  const x11Lib = getX11();
  if (!gtkLib || !gdkLib || !x11Lib) return false;
  try {
    const gdkDisplay = gdkLib.symbols.gdk_display_get_default();
    if (!gdkDisplay) return false;
    const xDisplay = gdkLib.symbols.gdk_x11_display_get_xdisplay(gdkDisplay);
    if (!xDisplay) return false;
    const gdkWindow = gtkLib.symbols.gtk_widget_get_window(windowPtr as any);
    if (!gdkWindow) return false;
    const xid = gdkLib.symbols.gdk_x11_window_get_xid(gdkWindow);
    if (!xid) return false;
    x11Lib.symbols.XMoveResizeWindow(
      xDisplay, xid,
      Math.round(x), Math.round(y),
      Math.max(400, Math.round(width)),
      Math.max(300, Math.round(height)),
    );
    x11Lib.symbols.XFlush(xDisplay);
    return true;
  } catch (err) {
    console.error("[gtk-window] x11SetFrame failed:", err);
    return false;
  }
}

/**
 * Delegate resize to the window manager.
 * Releases X11 grabs first (SDL2 pattern) so WM can take the pointer.
 */
export function beginResizeDrag(
  windowPtr: number | bigint,
  edge: GdkEdge,
  button: number,
  rootX: number,
  rootY: number,
) {
  const lib = getGtk();
  if (!lib) return false;
  try {
    // 1. Force small min-size so WM allows shrink
    forceSmallMinSize(lib, windowPtr as any);
    // 2. Release ALL X11 pointer grabs (SDL2 pattern)
    // WebKitGTK holds an implicit grab from the button-press event.
    // gdk_seat_ungrab inside begin_resize_drag targets the wrong device.
    // Direct XUngrabPointer releases ALL grabs on this display connection.
    releaseX11Grab();
    // 3. Start WM resize — WM can now grab the pointer successfully
    lib.symbols.gtk_window_begin_resize_drag(
      windowPtr as any,
      edge,
      button,
      Math.round(rootX),
      Math.round(rootY),
      0, // GDK_CURRENT_TIME — WM generates its own timestamp
    );
    return true;
  } catch (err) {
    console.error("[gtk-window] begin_resize_drag failed:", err);
    return false;
  }
}

/**
 * Delegate move to the window manager.
 */
export function beginMoveDrag(
  windowPtr: number | bigint,
  button: number,
  rootX: number,
  rootY: number,
) {
  const lib = getGtk();
  if (!lib) return false;
  try {
    lib.symbols.gtk_window_begin_move_drag(
      windowPtr as any,
      button,
      Math.round(rootX),
      Math.round(rootY),
      0,
    );
    return true;
  } catch (err) {
    console.error("[gtk-window] begin_move_drag failed:", err);
    return false;
  }
}

/**
 * Direct GTK resize — bypasses Electrobun's setSize which respects WebView min-size.
 * Clears min-size tree first, then calls gtk_window_resize + gtk_window_move.
 */
export function gtkSetFrame(
  windowPtr: number | bigint,
  x: number, y: number, width: number, height: number,
) {
  const lib = getGtk();
  if (!lib) return false;
  try {
    // Clear min-size on every frame update during resize
    forceSmallMinSize(lib, windowPtr as any);
    lib.symbols.gtk_window_resize(windowPtr as any, Math.max(400, Math.round(width)), Math.max(300, Math.round(height)));
    lib.symbols.gtk_window_move(windowPtr as any, Math.round(x), Math.round(y));
    return true;
  } catch (err) {
    console.error("[gtk-window] gtkSetFrame failed:", err);
    return false;
  }
}

/**
 * Wrap the WebKitWebView inside a GtkScrolledWindow to decouple its
 * natural/preferred size from the window's size negotiation.
 * This prevents the "rubber band" effect during resize-in.
 *
 * Codex review: GtkScrolledWindow has propagate-natural-width/height = FALSE
 * by default, which stops the child's natural size from reaching the window.
 */
export function wrapWebViewInScrolledWindow(windowPtr: number | bigint): boolean {
  const lib = getGtk();
  if (!lib) return false;
  try {
    // Get the window's child (container holding the WebView)
    const container = lib.symbols.gtk_bin_get_child(windowPtr as any);
    if (!container) { console.log("[gtk-window] No child container found"); return false; }

    // Get the WebView from the container
    const webview = lib.symbols.gtk_bin_get_child(container as any);
    if (!webview) { console.log("[gtk-window] No WebView found in container"); return false; }

    console.log("[gtk-window] Wrapping WebView in GtkScrolledWindow");

    // Ref the WebView so it's not destroyed when removed from container
    lib.symbols.g_object_ref(webview as any);

    // Remove WebView from its current parent
    lib.symbols.gtk_container_remove(container as any, webview as any);

    // Create a GtkScrolledWindow
    const scrolled = lib.symbols.gtk_scrolled_window_new(null, null);
    if (!scrolled) {
      // Put WebView back if scrolled window creation failed
      lib.symbols.gtk_container_add(container as any, webview as any);
      lib.symbols.g_object_unref(webview as any);
      return false;
    }

    // Configure: no scrollbar policy (automatic), don't propagate natural size
    const GTK_POLICY_AUTOMATIC = 1;
    const GTK_POLICY_NEVER = 2;
    lib.symbols.gtk_scrolled_window_set_policy(scrolled as any, GTK_POLICY_NEVER, GTK_POLICY_NEVER);
    lib.symbols.gtk_scrolled_window_set_propagate_natural_width(scrolled as any, false);
    lib.symbols.gtk_scrolled_window_set_propagate_natural_height(scrolled as any, false);
    lib.symbols.gtk_scrolled_window_set_min_content_width(scrolled as any, 1);
    lib.symbols.gtk_scrolled_window_set_min_content_height(scrolled as any, 1);

    // Set expand flags
    lib.symbols.gtk_widget_set_hexpand(scrolled as any, true);
    lib.symbols.gtk_widget_set_vexpand(scrolled as any, true);
    lib.symbols.gtk_widget_set_size_request(scrolled as any, 1, 1);

    // Add WebView to ScrolledWindow
    lib.symbols.gtk_container_add(scrolled as any, webview as any);
    lib.symbols.g_object_unref(webview as any); // balance the ref

    // Add ScrolledWindow to the original container
    lib.symbols.gtk_container_add(container as any, scrolled as any);

    // Show everything
    lib.symbols.gtk_widget_show_all(scrolled as any);

    console.log("[gtk-window] WebView successfully wrapped in GtkScrolledWindow");
    return true;
  } catch (err) {
    console.error("[gtk-window] wrapWebViewInScrolledWindow failed:", err);
    return false;
  }
}

// Edge string → GDK_EDGE mapping
const EDGE_MAP: Record<string, GdkEdge> = {
  n: GDK_EDGE.N, s: GDK_EDGE.S, e: GDK_EDGE.E, w: GDK_EDGE.W,
  ne: GDK_EDGE.NE, nw: GDK_EDGE.NW, se: GDK_EDGE.SE, sw: GDK_EDGE.SW,
};

export function edgeStringToGdk(edge: string): GdkEdge | null {
  return EDGE_MAP[edge] ?? null;
}
