using System.Runtime.InteropServices;

using Aprillz.MewUI.Diagnostics;
using Aprillz.MewUI.Input;
using Aprillz.MewUI.Native;
using Aprillz.MewUI.Resources;

using NativeX11 = Aprillz.MewUI.Native.X11;

namespace Aprillz.MewUI.Platform.Linux.X11;

internal sealed class X11WindowBackend : IWindowBackend
{
    private static readonly EnvDebugLog.Logger ImeLogger = new("MEWUI_IME_DEBUG", "[X11][IME]");

    private readonly X11PlatformHost _host;

    private bool _shown;
    private bool _disposed;
    private bool _cleanupDone;
    private bool _closedRaised;
    private nint _wmDeleteWindowAtom;
    private nint _wmProtocolsAtom;
    private nint _netWmWindowOpacityAtom;
    private nint _cardinalAtom;
    private nint _motifWmHintsAtom;
    private long _lastRenderTick;
    private X11GlxVisualInfo? _glxVisualInfo;

    private readonly ClickCountTracker _clickCountTracker = new();
    private readonly int[] _lastPressClickCounts = new int[5];
    private IconSource? _icon;
    private double _opacity = 1.0;
    private bool _allowsTransparency;
    private bool _enabled = true;
    private nint _currentCursor;
    private nint _xim;
    private nint _xic;
    private nint _imePreeditAttributesList;
    private nint _imeSpotLocationPtr;
    private bool _imeUsesPreeditPosition;

    internal bool NeedsRender { get; private set; }

    public nint Handle { get; private set; }

    public nint Display { get; private set; }

    internal X11WindowBackend(X11PlatformHost host, Window window)
    {
        _host = host;
        Window = window;
    }

    internal Window Window { get; }

    public void SetResizable(bool resizable)
    {
        if (Display == 0 || Handle == 0)
        {
            return;
        }

        ApplyResizeMode();
    }

    public void Show()
    {
        if (_shown)
        {
            return;
        }

        CreateWindow();
        _shown = true;
        NativeX11.XMapWindow(Display, Handle);
        NativeX11.XFlush(Display);

        // Some IM modules don't start preedit until the IC is focused.
        // Relying solely on FocusIn can miss cases where focus is already on the window when mapped.
        if (_xic != 0)
        {
            ImeLogger.Write($"Show -> XSetICFocus ic=0x{_xic.ToInt64():X}");
            try { NativeX11.XSetICFocus(_xic); } catch { }
        }
    }

    public void Hide()
    {
        // TODO: XUnmapWindow
    }

    public void Close()
    {
        // Cleanup immediately to avoid X errors from late render/layout passes
        // that may query window attributes after the server has destroyed the window.
        var handle = Handle;
        if (Display == 0 || handle == 0)
        {
            return;
        }

        RaiseClosedOnce();
        Cleanup(handle, destroyWindow: true);
    }

    public void Invalidate(bool erase)
    {
        if (Display == 0 || Handle == 0)
        {
            return;
        }

        // Coalesce invalidations; render will be performed by the platform host loop.
        NeedsRender = true;
        _host.RequestWake();
    }

    public void SetTitle(string title)
    {
        if (Display == 0 || Handle == 0)
        {
            return;
        }

        NativeX11.XStoreName(Display, Handle, title ?? string.Empty);
        NativeX11.XFlush(Display);
    }

    public void SetIcon(IconSource? icon)
    {
        _icon = icon;
        if (Display == 0 || Handle == 0)
        {
            return;
        }

        ApplyIcon();
    }

    public void SetClientSize(double widthDip, double heightDip)
    {
        // TODO: XResizeWindow
    }

    public Point GetPosition()
    {
        if (Display == 0 || Handle == 0)
        {
            return default;
        }

        double dpiScale = Window.DpiScale <= 0 ? 1.0 : Window.DpiScale;
        int screen = NativeX11.XDefaultScreen(Display);
        nint root = NativeX11.XRootWindow(Display, screen);

        // Translate window origin (0,0) to root coordinates.
        NativeX11.XTranslateCoordinates(Display, Handle, root, 0, 0, out int rx, out int ry, out _);
        return new Point(rx / dpiScale, ry / dpiScale);
    }

    public void SetPosition(double leftDip, double topDip)
    {
        if (Display == 0 || Handle == 0)
        {
            return;
        }

        double dpiScale = Window.DpiScale <= 0 ? 1.0 : Window.DpiScale;
        int x = (int)Math.Round(leftDip * dpiScale);
        int y = (int)Math.Round(topDip * dpiScale);

        NativeX11.XMoveWindow(Display, Handle, x, y);
        NativeX11.XFlush(Display);
    }

    public void CaptureMouse()
    {
        // TODO: XGrabPointer
    }

    public void ReleaseMouseCapture()
    {
        // TODO: XUngrabPointer
    }

    public Point ClientToScreen(Point clientPointDip)
    {
        if (Display == 0 || Handle == 0)
        {
            throw new InvalidOperationException("Window is not initialized.");
        }

        int screen = NativeX11.XDefaultScreen(Display);
        nint root = NativeX11.XRootWindow(Display, screen);

        int x = (int)Math.Round(clientPointDip.X * Window.DpiScale);
        int y = (int)Math.Round(clientPointDip.Y * Window.DpiScale);

        NativeX11.XTranslateCoordinates(Display, Handle, root, x, y, out int rx, out int ry, out _);
        return new Point(rx, ry);
    }

    public Point ScreenToClient(Point screenPointPx)
    {
        if (Display == 0 || Handle == 0)
        {
            throw new InvalidOperationException("Window is not initialized.");
        }

        int screen = NativeX11.XDefaultScreen(Display);
        nint root = NativeX11.XRootWindow(Display, screen);

        int x = (int)Math.Round(screenPointPx.X);
        int y = (int)Math.Round(screenPointPx.Y);

        NativeX11.XTranslateCoordinates(Display, root, Handle, x, y, out int cx, out int cy, out _);
        return new Point(cx / Window.DpiScale, cy / Window.DpiScale);
    }

    private void CreateWindow()
    {
        Display = _host.Display;
        if (Display == 0)
        {
            throw new InvalidOperationException("X11 display not initialized.");
        }

        _allowsTransparency = Window.AllowsTransparency;

        int screen = NativeX11.XDefaultScreen(Display);
        nint root = NativeX11.XRootWindow(Display, screen);

        uint dpi = _host.GetDpiForWindow(0);
        Window.SetDpi(dpi);
        double dpiScale = Window.DpiScale;

        uint width = (uint)Math.Max(1, (int)Math.Round(Window.Width * dpiScale));
        uint height = (uint)Math.Max(1, (int)Math.Round(Window.Height * dpiScale));

        int x = 0;
        int y = 0;
        if (Window.StartupLocation == WindowStartupLocation.CenterScreen &&
            NativeX11.XGetWindowAttributes(Display, root, out var rootAttrs) != 0)
        {
            x = Math.Max(0, (rootAttrs.width - (int)width) / 2);
            y = Math.Max(0, (rootAttrs.height - (int)height) / 2);
        }

        // Choose a GLX visual for OpenGL rendering and create the window with that visual.
        // When transparency is requested, prefer a 32-bit ARGB visual via FBConfig.
        // Try MSAA first to reduce jaggies on filled primitives (RoundRect, Ellipse, etc).
        // GLX_SAMPLE_BUFFERS / GLX_SAMPLES are from GLX_ARB_multisample.
        const int GLX_SAMPLE_BUFFERS = 100000;
        const int GLX_SAMPLES = 100001;
        const int GLX_X_RENDERABLE = 0x8012;
        const int GLX_DRAWABLE_TYPE = 0x8010;
        const int GLX_RENDER_TYPE = 0x8011;
        const int GLX_WINDOW_BIT = 0x00000001;
        const int GLX_RGBA_BIT = 0x00000001;
        const int GLX_ALPHA_SIZE = 11;
        const int GLX_DEPTH_SIZE = 12;
        const int GLX_STENCIL_SIZE = 13;

        XVisualInfo visualInfo = default;
        bool usedFbConfig = false;
        bool hasVisualInfo = false;

        if (_allowsTransparency)
        {
            int[] fbAttribs =
            {
                GLX_X_RENDERABLE, 1,
                GLX_DRAWABLE_TYPE, GLX_WINDOW_BIT,
                GLX_RENDER_TYPE, GLX_RGBA_BIT,
                4,  // GLX_RGBA
                5,  // GLX_DOUBLEBUFFER
                8,  // GLX_RED_SIZE
                8,
                9,  // GLX_GREEN_SIZE
                8,
                10, // GLX_BLUE_SIZE
                8,
                GLX_ALPHA_SIZE,
                8,
                GLX_DEPTH_SIZE,
                24,
                GLX_STENCIL_SIZE,
                8,
                GLX_SAMPLE_BUFFERS, 1,
                GLX_SAMPLES, 4,
                0
            };

            nint fbConfigs = LibGL.glXChooseFBConfig(Display, screen, fbAttribs, out int fbCount);
            if (fbConfigs != 0 && fbCount > 0)
            {
                XVisualInfo? best = null;
                int bestSamples = -1;
                int bestStencil = -1;

                for (int i = 0; i < fbCount; i++)
                {
                    nint fb = Marshal.ReadIntPtr(fbConfigs, i * IntPtr.Size);
                    if (fb == 0)
                    {
                        continue;
                    }

                    int alphaSize = 0;
                    _ = LibGL.glXGetFBConfigAttrib(Display, fb, GLX_ALPHA_SIZE, out alphaSize);
                    if (alphaSize < 8)
                    {
                        continue;
                    }

                    int depthSize = 0;
                    _ = LibGL.glXGetFBConfigAttrib(Display, fb, GLX_DEPTH_SIZE, out depthSize);

                    int stencilSize = 0;
                    _ = LibGL.glXGetFBConfigAttrib(Display, fb, GLX_STENCIL_SIZE, out stencilSize);
                    if (depthSize < 16 || stencilSize < 8)
                    {
                        continue;
                    }

                    nint viPtr = LibGL.glXGetVisualFromFBConfig(Display, fb);
                    if (viPtr == 0)
                    {
                        continue;
                    }

                    var vi = Marshal.PtrToStructure<XVisualInfo>(viPtr);
                    NativeX11.XFree(viPtr);

                    if (vi.depth != 32)
                    {
                        continue;
                    }

                    int sampleBuffers = 0;
                    int samples = 0;
                    _ = LibGL.glXGetFBConfigAttrib(Display, fb, GLX_SAMPLE_BUFFERS, out sampleBuffers);
                    _ = LibGL.glXGetFBConfigAttrib(Display, fb, GLX_SAMPLES, out samples);
                    int score = sampleBuffers > 0 ? samples : 0;

                    // Prefer configs with a stencil buffer (required for rounded clip via stencil),
                    // then prefer higher MSAA.
                    if (best == null ||
                        stencilSize > bestStencil ||
                        (stencilSize == bestStencil && score > bestSamples))
                    {
                        best = vi;
                        bestSamples = score;
                        bestStencil = stencilSize;
                    }
                }

                NativeX11.XFree(fbConfigs);

                if (best.HasValue)
                {
                    visualInfo = best.Value;
                    usedFbConfig = true;
                    hasVisualInfo = true;
                }
            }
        }

        if (!usedFbConfig)
        {
            int[] attribsMsaa =
            {
                4,  // GLX_RGBA
                5,  // GLX_DOUBLEBUFFER
                8,  // GLX_RED_SIZE
                8,
                9,  // GLX_GREEN_SIZE
                8,
                10, // GLX_BLUE_SIZE
                8,
                GLX_ALPHA_SIZE,
                8,
                GLX_DEPTH_SIZE,
                24,
                GLX_STENCIL_SIZE,
                8,
                GLX_SAMPLE_BUFFERS, 1,
                GLX_SAMPLES, 4,
                0
            };

            nint visualInfoPtr;
            unsafe
            {
                fixed (int* p = attribsMsaa)
                {
                    visualInfoPtr = LibGL.glXChooseVisual(Display, screen, (nint)p);
                }
            }

            if (visualInfoPtr == 0)
            {
                int[] attribs =
                {
                    4,  // GLX_RGBA
                    5,  // GLX_DOUBLEBUFFER
                    8,  // GLX_RED_SIZE
                    8,
                    9,  // GLX_GREEN_SIZE
                    8,
                    10, // GLX_BLUE_SIZE
                    8,
                    GLX_ALPHA_SIZE,
                    8,
                    GLX_DEPTH_SIZE,
                    24,
                    GLX_STENCIL_SIZE,
                    8,
                    0
                };

                unsafe
                {
                    fixed (int* p = attribs)
                    {
                        visualInfoPtr = LibGL.glXChooseVisual(Display, screen, (nint)p);
                    }
                }

                if (visualInfoPtr == 0)
                {
                    // Last resort: allow a visual without stencil (rounded clip may not work).
                    int[] attribsNoStencil =
                    {
                        4,  // GLX_RGBA
                        5,  // GLX_DOUBLEBUFFER
                        8,  // GLX_RED_SIZE
                        8,
                        9,  // GLX_GREEN_SIZE
                        8,
                        10, // GLX_BLUE_SIZE
                        8,
                        GLX_ALPHA_SIZE,
                        8,
                        GLX_DEPTH_SIZE,
                        24,
                        0
                    };

                    unsafe
                    {
                        fixed (int* p = attribsNoStencil)
                        {
                            visualInfoPtr = LibGL.glXChooseVisual(Display, screen, (nint)p);
                        }
                    }

                    if (visualInfoPtr == 0)
                    {
                        throw new InvalidOperationException("glXChooseVisual failed.");
                    }
                }
            }

            visualInfo = Marshal.PtrToStructure<XVisualInfo>(visualInfoPtr);
            NativeX11.XFree(visualInfoPtr);
            hasVisualInfo = true;
        }

        if (!hasVisualInfo)
        {
            throw new InvalidOperationException("Failed to select a suitable X11 visual.");
        } 
        _glxVisualInfo = new X11GlxVisualInfo(
            visualInfo.visual,
            visualInfo.visualid,
            visualInfo.screen,
            visualInfo.depth,
            visualInfo.@class,
            visualInfo.red_mask,
            visualInfo.green_mask,
            visualInfo.blue_mask,
            visualInfo.colormap_size,
            visualInfo.bits_per_rgb);

        const int AllocNone = 0;
        const ulong CWBackPixel = 1UL << 1;
        const ulong CWBorderPixel = 1UL << 3;
        const ulong CWEventMask = 1UL << 11;
        const ulong CWColormap = 1UL << 13;

        var attrs = new XSetWindowAttributes
        {
            colormap = NativeX11.XCreateColormap(Display, root, visualInfo.visual, AllocNone),
            event_mask = (nint)(X11EventMask.ExposureMask | X11EventMask.StructureNotifyMask |
                               X11EventMask.KeyPressMask | X11EventMask.KeyReleaseMask |
                               X11EventMask.ButtonPressMask | X11EventMask.ButtonReleaseMask |
                               X11EventMask.PointerMotionMask | X11EventMask.FocusChangeMask),
        };

        ulong valueMask = CWEventMask | CWColormap;
        if (_allowsTransparency)
        {
            attrs.background_pixel = 0;
            attrs.border_pixel = 0;
            valueMask |= CWBackPixel | CWBorderPixel;
        }

        Handle = NativeX11.XCreateWindow(
            Display,
            root,
            x, y,
            width, height,
            0,
            visualInfo.depth,
            1, // InputOutput
            visualInfo.visual,
            valueMask,
            ref attrs);

        if (Handle == 0)
        {
            throw new InvalidOperationException("XCreateWindow failed.");
        }

        DiagLog.Write($"X11 window created: display=0x{Display.ToInt64():X} window=0x{Handle.ToInt64():X} {width}x{height}");

        _host.RegisterWindow(Handle, this);
        Window.AttachBackend(this);
        Window.SetClientSizeDip(width / dpiScale, height / dpiScale);

        SetTitle(Window.Title);
        ApplyIcon();

        // WM_DELETE_WINDOW
        _wmProtocolsAtom = NativeX11.XInternAtom(Display, "WM_PROTOCOLS", false);
        _wmDeleteWindowAtom = NativeX11.XInternAtom(Display, "WM_DELETE_WINDOW", false);
        _netWmWindowOpacityAtom = NativeX11.XInternAtom(Display, "_NET_WM_WINDOW_OPACITY", true);
        _cardinalAtom = NativeX11.XInternAtom(Display, "CARDINAL", false);
        _motifWmHintsAtom = NativeX11.XInternAtom(Display, "_MOTIF_WM_HINTS", false);
        if (_wmProtocolsAtom != 0 && _wmDeleteWindowAtom != 0)
        {
            NativeX11.XSetWMProtocols(Display, Handle, ref _wmDeleteWindowAtom, 1);
        }

        // Apply transparency-related window hints after atoms are initialized.
        if (_allowsTransparency)
        {
            SetAllowsTransparency(_allowsTransparency);
        }

        ApplyOpacity();
        ApplyResizeMode();

        if (X11Ime.TryCreateInputContext(
            Display,
            Handle,
            out _xim,
            out _xic,
            out _imePreeditAttributesList,
            out _imeSpotLocationPtr,
            out _imeUsesPreeditPosition))
        {
            DiagLog.Write($"[X11] XIM enabled: im=0x{_xim.ToInt64():X} ic=0x{_xic.ToInt64():X} preeditPosition={_imeUsesPreeditPosition}");
            ImeLogger.Write($"XIM enabled: im=0x{_xim.ToInt64():X} ic=0x{_xic.ToInt64():X} preeditPosition={_imeUsesPreeditPosition}");
        }
        else
        {
            ImeLogger.Write(
                $"XIM disabled: im=0x{_xim.ToInt64():X} ic=0x{_xic.ToInt64():X} " +
                $"XMODIFIERS='{Environment.GetEnvironmentVariable("XMODIFIERS") ?? ""}' " +
                $"GTK_IM_MODULE='{Environment.GetEnvironmentVariable("GTK_IM_MODULE") ?? ""}' " +
                $"QT_IM_MODULE='{Environment.GetEnvironmentVariable("QT_IM_MODULE") ?? ""}'");
        }

        NeedsRender = true;
    }

    private unsafe void ApplyIcon()
    {
        if (Display == 0 || Handle == 0)
        {
            return;
        }

        var iconAtom = NativeX11.XInternAtom(Display, "_NET_WM_ICON", false);
        if (iconAtom == 0)
        {
            return;
        }

        if (_icon == null)
        {
            NativeX11.XDeleteProperty(Display, Handle, iconAtom);
            NativeX11.XFlush(Display);
            return;
        }

        var cardinalAtom = _cardinalAtom;
        if (cardinalAtom == 0)
        {
            return;
        }

        var dpiScale = Window.DpiScale <= 0 ? 1.0 : Window.DpiScale;
        int smallPx = Math.Max(16, (int)Math.Round(16 * dpiScale));
        int bigPx = Math.Max(32, (int)Math.Round(32 * dpiScale));

        var payload = new List<uint>(capacity: 16);
        AppendNetWmIconPayload(payload, _icon, smallPx);
        if (bigPx != smallPx)
        {
            AppendNetWmIconPayload(payload, _icon, bigPx);
        }

        if (payload.Count == 0)
        {
            NativeX11.XDeleteProperty(Display, Handle, iconAtom);
            NativeX11.XFlush(Display);
            return;
        }

        var data = payload.ToArray();
        fixed (uint* p = data)
        {
            NativeX11.XChangeProperty(
                display: Display,
                window: Handle,
                property: iconAtom,
                type: cardinalAtom,
                format: 32,
                mode: 0, // PropModeReplace
                data: (nint)p,
                nelements: data.Length);
        }

        NativeX11.XFlush(Display);
    }

    private void ApplyOpacity()
    {
        if (Display == 0 || Handle == 0 || _netWmWindowOpacityAtom == 0 || _cardinalAtom == 0)
        {
            return;
        }

        // X11 opacity is a 32-bit CARDINAL (0..0xFFFFFFFF). Requires a compositor to take effect.
        if (_opacity >= 0.999)
        {
            NativeX11.XDeleteProperty(Display, Handle, _netWmWindowOpacityAtom);
            NativeX11.XFlush(Display);
            return;
        }

        uint value32 = (uint)Math.Round(_opacity * uint.MaxValue);
        unsafe
        {
            nuint value = value32;
            NativeX11.XChangeProperty(
                display: Display,
                window: Handle,
                property: _netWmWindowOpacityAtom,
                type: _cardinalAtom,
                format: 32,
                mode: 0, // PropModeReplace
                data: (nint)(&value),
                nelements: 1);
        }

        NativeX11.XFlush(Display);
    }

    private static void AppendNetWmIconPayload(List<uint> dst, IconSource icon, int desiredSizePx)
    {
        var src = icon.Pick(desiredSizePx);
        if (src == null)
        {
            return;
        }

        if (!ImageDecoders.TryDecode(src.Data, out var bmp))
        {
            return;
        }

        int w = Math.Max(1, bmp.WidthPx);
        int h = Math.Max(1, bmp.HeightPx);
        dst.Add((uint)w);
        dst.Add((uint)h);

        var pixels = bmp.Data;
        int idx = 0;
        int pixelCount = checked(w * h);
        for (int i = 0; i < pixelCount; i++)
        {
            byte b = pixels[idx++];
            byte g = pixels[idx++];
            byte r = pixels[idx++];
            byte a = pixels[idx++];
            dst.Add(((uint)a << 24) | ((uint)r << 16) | ((uint)g << 8) | b);
        }
    }

    private void ApplyResizeMode()
    {
        if (Display == 0 || Handle == 0)
        {
            return;
        }

        var hints = new XSizeHints();
        if (!Window.WindowSize.IsResizable)
        {
            hints.flags = XSizeHintsFlags.PMinSize | XSizeHintsFlags.PMaxSize;
            hints.min_width = (int)Math.Max(1, Math.Round(Window.Width * Window.DpiScale));
            hints.min_height = (int)Math.Max(1, Math.Round(Window.Height * Window.DpiScale));
            hints.max_width = hints.min_width;
            hints.max_height = hints.min_height;
        }
        else
        {
            double dpiScale = Window.DpiScale > 0 ? Window.DpiScale : 1.0;
            var ws = Window.WindowSize;
            double minW = ws.MinWidth;
            double minH = ws.MinHeight;
            double maxW = ws.MaxWidth;
            double maxH = ws.MaxHeight;

            if (minW > 0 || minH > 0)
            {
                hints.flags |= XSizeHintsFlags.PMinSize;
                hints.min_width = minW > 0 ? (int)Math.Ceiling(minW * dpiScale) : 1;
                hints.min_height = minH > 0 ? (int)Math.Ceiling(minH * dpiScale) : 1;
            }

            if (!double.IsPositiveInfinity(maxW) || !double.IsPositiveInfinity(maxH))
            {
                hints.flags |= XSizeHintsFlags.PMaxSize;
                hints.max_width = !double.IsPositiveInfinity(maxW) ? (int)Math.Ceiling(maxW * dpiScale) : 32767;
                hints.max_height = !double.IsPositiveInfinity(maxH) ? (int)Math.Ceiling(maxH * dpiScale) : 32767;
            }
        }

        NativeX11.XSetWMNormalHints(Display, Handle, ref hints);

        // Clamp the current window size if it violates the new constraints.
        if (hints.flags != 0)
        {
            double dpiScale = Window.DpiScale > 0 ? Window.DpiScale : 1.0;
            int curW = (int)Math.Round(Window.Width * dpiScale);
            int curH = (int)Math.Round(Window.Height * dpiScale);
            int clampedW = curW;
            int clampedH = curH;

            if ((hints.flags & XSizeHintsFlags.PMinSize) != 0)
            {
                clampedW = Math.Max(clampedW, hints.min_width);
                clampedH = Math.Max(clampedH, hints.min_height);
            }
            if ((hints.flags & XSizeHintsFlags.PMaxSize) != 0)
            {
                clampedW = Math.Min(clampedW, hints.max_width);
                clampedH = Math.Min(clampedH, hints.max_height);
            }

            if (clampedW != curW || clampedH != curH)
            {
                NativeX11.XResizeWindow(Display, Handle, (uint)Math.Max(1, clampedW), (uint)Math.Max(1, clampedH));
            }
        }

        NativeX11.XFlush(Display);
    }

    internal void PumpEventsOnce()
    {
        if (Display == 0)
        {
            return;
        }

        while (NativeX11.XPending(Display) != 0)
        {
            NativeX11.XNextEvent(Display, out var ev);
            ProcessEvent(ref ev);
        }
    }

    internal void ProcessEvent(ref XEvent ev)
    {
        const int Expose = 12;
        const int DestroyNotify = 17;
        const int ConfigureNotify = 22;
        const int ClientMessage = 33;
        const int KeyPress = 2;
        const int KeyRelease = 3;
        const int ButtonPress = 4;
        const int ButtonRelease = 5;
        const int MotionNotify = 6;
        const int FocusIn = 9;
        const int FocusOut = 10;

        if (!_enabled && (ev.type == KeyPress || ev.type == KeyRelease || ev.type == ButtonPress || ev.type == ButtonRelease || ev.type == MotionNotify))
        {
            return;
        }

        switch (ev.type)
        {
            case Expose:
                // X11 can deliver multiple expose events; render once for the last in the batch.
                if (ev.xexpose.count == 0)
                {
                    NeedsRender = true;
                }

                break;

            case ConfigureNotify:
                var cfg = ev.xconfigure;
                Window.SetClientSizeDip(cfg.width / Window.DpiScale, cfg.height / Window.DpiScale);
                NeedsRender = true;
                break;

            case ClientMessage:
                unsafe
                {
                    var client = ev.xclient;
                    if (_wmProtocolsAtom != 0 &&
                        _wmDeleteWindowAtom != 0 &&
                        client.message_type == _wmProtocolsAtom &&
                        client.format == 32 &&
                        (nint)client.data[0] == _wmDeleteWindowAtom)
                    {
                        // Ask the window to close; it will destroy the X11 window.
                        // Cleanup happens on DestroyNotify.
                        Window.Close();
                    }
                }
                break;

            case DestroyNotify:
                // Ensure we unregister and release resources even if the window is destroyed externally.
                RaiseClosedOnce();
                Cleanup(ev.xdestroywindow.window, destroyWindow: false);
                break;

            case KeyPress:
            case KeyRelease:
                // XIM handling:
                // - Always call XFilterEvent when we have an IC so IME hotkeys (e.g. Shift+Space) can work.
                // - Even if XFilterEvent returns true, Xutf8LookupString may still deliver committed text for this event.
                bool filteredByIme = _xic != 0 && NativeX11.XFilterEvent(ref ev, Handle) != 0;
                if (_xic != 0)
                {
                    ImeLogger.Write($"XFilterEvent type={ev.type} filtered={filteredByIme} xic=0x{_xic.ToInt64():X}");
                }

                // Avalonia-style:
                // - When using preedit-position, a filtered event is owned by the IME and should not be processed.
                // - In commit-only mode, some IM modules still report filtered=true while requiring Xutf8LookupString
                //   to obtain committed text. So only short-circuit when preedit-position is active.
                if (filteredByIme && _imeUsesPreeditPosition)
                {
                    break;
                }

                HandleKey(ev.xkey, isDown: ev.type == KeyPress, isFilteredByIme: filteredByIme);
                break;

            case ButtonPress:
            case ButtonRelease:
                HandleButton(ev.xbutton, isDown: ev.type == ButtonPress);
                break;

            case MotionNotify:
                HandleMotion(ev.xmotion);
                break;

            case FocusIn:
                if (_xic != 0)
                {
                    ImeLogger.Write($"FocusIn -> XSetICFocus ic=0x{_xic.ToInt64():X}");
                    NativeX11.XSetICFocus(_xic);
                }
                break;

            case FocusOut:
                if (_xic != 0)
                {
                    ImeLogger.Write($"FocusOut -> XUnsetICFocus ic=0x{_xic.ToInt64():X}");
                    NativeX11.XUnsetICFocus(_xic);
                }
                break;
        }
    }

    internal void RenderIfNeeded()
    {
        if (!NeedsRender)
        {
            return;
        }

        // If the window is not renderable (already closed/destroyed), clear the flag
        // to avoid keeping the platform host loop hot.
        if (Handle == 0 || Display == 0)
        {
            NeedsRender = false;
            return;
        }

        // Simple throttle to reduce CPU/GPU pressure on software-rendered VMs.
        long now = Environment.TickCount64;
        if (now - _lastRenderTick < 16)
        {
            return;
        }

        _lastRenderTick = now;

        NeedsRender = false;
        RenderNowCore();
    }

    internal int GetRenderDelayMs()
    {
        if (!NeedsRender)
        {
            return int.MaxValue;
        }

        long now = Environment.TickCount64;
        long elapsed = now - _lastRenderTick;
        if (elapsed >= 16)
        {
            return 0;
        }

        return (int)Math.Clamp(16 - elapsed, 0, 16);
    }

    internal void RenderNow()
    {
        if (Handle == 0 || Display == 0)
        {
            return;
        }

        NeedsRender = false;
        RenderNowCore();
    }

    private void RenderNowCore()
    {
        Render();
    }

    private void HandleKey(XKeyEvent e, bool isDown, bool isFilteredByIme)
    {
        if (Window.Content == null)
        {
            return;
        }

        // KeyDown/Up (keysym based).
        var ks = NativeX11.XLookupKeysym(ref e, 0).ToInt64();
        var key = MapKeysymToKey(ks);
        var args = new KeyEventArgs(key, platformKey: (int)ks, modifiers: GetModifiers(e.state), isRepeat: false);

        if (isDown)
        {
            // If XIM filtered this key press, don't route KeyDown/Tab navigation into the UI:
            // it is likely being used for IME candidate navigation or composition control.
            // Some setups report filtered=true even in commit-only mode; don't break key routing in that case.
            bool routeKeyDown = !(isFilteredByIme && _imeUsesPreeditPosition);

            // Many window managers send WM_DELETE_WINDOW for Alt+F4, but not all do (especially for secondary windows).
            // Handle it ourselves to ensure consistent close behavior.
            const long XK_F4 = 0xFFC1;
            if (ks == XK_F4 && args.Modifiers.HasFlag(ModifierKeys.Alt))
            {
                Window.Close();
                return;
            }

                if (routeKeyDown)
                {
                    Window.RaisePreviewKeyDown(args);
                    if (!args.Handled)
                    {
                        WindowInputRouter.KeyDown(Window, args);
                    }

                    // WPF-like Tab behavior:
                    // - Always let the focused element see KeyDown first.
                    // - Only perform focus navigation if the key is still unhandled.
                    if (!args.Handled && args.Key == Key.Tab)
                    {
                        if (args.Modifiers.HasFlag(ModifierKeys.Shift))
                        {
                            Window.FocusManager.MoveFocusPrevious();
                        }
                        else
                        {
                            Window.FocusManager.MoveFocusNext();
                        }

                        args.Handled = true;
                        return;
                    }
                }

                // Text input after key handling (best-effort).
            // Do not gate text input on KeyDown handling: text controls may handle Enter/Backspace/etc. while
            // still requiring IME commits (or dead-key composed text) to flow through TextInput.
            // Instead, only suppress obvious shortcut-modifier combinations.
            if (!args.Modifiers.HasFlag(ModifierKeys.Control) && !args.Modifiers.HasFlag(ModifierKeys.Alt))
            {
                // Avalonia-style XIM behavior:
                // - When using preedit-position, filtered events are owned by the IME and should not emit text.
                // - In commit-only mode, some IM modules still report filtered=true while requiring Xutf8LookupString
                //   to obtain committed text (and even for plain English input). So only suppress in preedit-position mode.
                if (isFilteredByIme && _imeUsesPreeditPosition)
                {
                    return;
                }

                Span<byte> buf = stackalloc byte[128];
                int byteCount = 0;
                int lookupStatus = 0;
                unsafe
                {
                    fixed (byte* p = buf)
                    {
                        if (_xic != 0)
                        {
                            byteCount = NativeX11.Xutf8LookupString(_xic, ref e, p, buf.Length, out _, out lookupStatus);
                        }
                        else
                        {
                            byteCount = NativeX11.XLookupString(ref e, p, buf.Length, out _, out _);
                            lookupStatus = 2; // XLookupChars (best-effort)
                        }
                    }
                }

                ImeLogger.Write($"Xutf8LookupString status={lookupStatus} bytes={byteCount} filtered={isFilteredByIme} key={args.Key} mods={args.Modifiers}");

                // XLookupChars=2, XLookupBoth=4. Anything else means "no committed chars".
                if ((lookupStatus == 2 || lookupStatus == 4) && byteCount > 0)
                {
                    byteCount = Math.Min(byteCount, buf.Length);
                    string s = System.Text.Encoding.UTF8.GetString(buf[..byteCount]);
                    ImeLogger.Write($"  -> text '{s}'");
                    if (!string.IsNullOrEmpty(s))
                    {
                        // KeyDown-handled Enter/Tab should not also emit committed text input.
                        // (e.g. TextBox may insert '\n'/'\t' on KeyDown and then receive duplicate text from Xutf8LookupString.)
                        if (TextInputSuppression.ShouldSuppressCommittedText(args, s))
                        {
                            return;
                        }

                        // Many IMEs use Shift+Space to toggle input mode (e.g., Hangul/English).
                        // When that happens, some setups still report a literal space; suppress it.
                        if (_xic != 0 && isFilteredByIme && args.Key == Key.Space && args.Modifiers == ModifierKeys.Shift && s == " ")
                        {
                            return;
                        }

                        // Filter control characters so Tab doesn't get inserted into TextBox.
                        // (Tab is handled above for focus navigation)
                        if (s.Length == 1 && char.IsControl(s[0]) && s[0] != '\r' && s[0] != '\n')
                        {
                            return;
                        }

                        var ti = new TextInputEventArgs(s);
                        Window.RaisePreviewTextInput(ti);
                        if (!ti.Handled)
                        {
                            if (Window.FocusManager.FocusedElement is ITextInputClient client)
                            {
                                client.HandleTextInput(ti);
                            }
                        }
                    }
                }
            }
        }
        else
        {
            if (!(isFilteredByIme && _imeUsesPreeditPosition))
            {
                Window.RaisePreviewKeyUp(args);
                if (!args.Handled)
                {
                    WindowInputRouter.KeyUp(Window, args);
                }
            }

            Window.RequerySuggested();
        }
    }

    private void HandleButton(XButtonEvent e, bool isDown)
    {
        int xPx = e.x;
        int yPx = e.y;
        var pos = new Point(xPx / Window.DpiScale, yPx / Window.DpiScale);

        // X11 button: 1 left, 2 middle, 3 right, 4/5 wheel.
        if (e.button == 4 || e.button == 5)
        {
            if (!isDown)
            {
                return;
            }

            var wheelElement = WindowInputRouter.HitTest(Window, pos);
            WindowInputRouter.UpdateMouseOver(Window, wheelElement);

            int delta = e.button == 4 ? 120 : -120;
            bool leftDown = (e.state & (1u << 8)) != 0;
            bool middleDown = (e.state & (1u << 9)) != 0;
            bool rightDown = (e.state & (1u << 10)) != 0;
            WindowInputRouter.MouseWheel(Window, pos, pos, delta, isHorizontal: false, leftDown, rightDown, middleDown);

            return;
        }

        var btn = e.button switch
        {
            1 => MouseButton.Left,
            2 => MouseButton.Middle,
            3 => MouseButton.Right,
            _ => MouseButton.Left
        };

        bool left = (e.state & (1u << 8)) != 0;
        bool middle = (e.state & (1u << 9)) != 0;
        bool right = (e.state & (1u << 10)) != 0;

        // Include the current transition.
        switch (btn)
        {
            case MouseButton.Left:
                left = isDown;
                break;
            case MouseButton.Middle:
                middle = isDown;
                break;
            case MouseButton.Right:
                right = isDown;
                break;
        }

        int clickCount = 1;
        int buttonIndex = (int)btn;
        if ((uint)buttonIndex < (uint)_lastPressClickCounts.Length)
        {
            if (isDown)
            {
                const uint defaultMaxDelayMs = 500;
                int maxDist = (int)Math.Round(4 * Window.DpiScale);
                clickCount = _clickCountTracker.Update(btn, xPx, yPx, unchecked((uint)e.time), defaultMaxDelayMs, maxDist, maxDist);
                _lastPressClickCounts[buttonIndex] = clickCount;
            }
            else
            {
                clickCount = _lastPressClickCounts[buttonIndex];
                if (clickCount <= 0)
                {
                    clickCount = 1;
                }
            }
        }

        WindowInputRouter.MouseButton(
            Window,
            pos,
            pos,
            btn,
            isDown,
            leftDown: left,
            rightDown: right,
            middleDown: middle,
            clickCount: clickCount);
    }

    private void HandleMotion(XMotionEvent e)
    {
        var pos = new Point(e.x / Window.DpiScale, e.y / Window.DpiScale);

        bool left = (e.state & (1u << 8)) != 0;
        bool middle = (e.state & (1u << 9)) != 0;
        bool right = (e.state & (1u << 10)) != 0;

        WindowInputRouter.MouseMove(Window, pos, pos, leftDown: left, rightDown: right, middleDown: middle);
    }

    internal void NotifyDpiChanged(uint oldDpi, uint newDpi)
    {
        if (Display == 0 || Handle == 0 || oldDpi == newDpi)
        {
            return;
        }

        Window.SetDpi(newDpi);
        Window.RaiseDpiChanged(oldDpi, newDpi);

        if (NativeX11.XGetWindowAttributes(Display, Handle, out var attrs) != 0)
        {
            Window.SetClientSizeDip(attrs.width / Window.DpiScale, attrs.height / Window.DpiScale);
        }

        // Force layout recalculation with the correct DPI
        Window.PerformLayout();
        NeedsRender = true;
    }

    private static Key MapKeysymToKey(long keysym)
    {
        // Function keys
        if (keysym is >= 0xFFBE and <= 0xFFD5) // XK_F1..XK_F24
        {
            return (Key)((int)Key.F1 + (int)(keysym - 0xFFBE));
        }

        // Minimal mapping for navigation/editing.
        // KeySyms for ASCII letters/numbers are their Unicode code points
        // (e.g. 'A' == 0x41, 'a' == 0x61, '0' == 0x30).
        if (keysym is >= 0x41 and <= 0x5A)
        {
            return (Key)((int)Key.A + (int)(keysym - 0x41));
        }

        if (keysym is >= 0x61 and <= 0x7A)
        {
            return (Key)((int)Key.A + (int)(keysym - 0x61));
        }

        if (keysym is >= 0x30 and <= 0x39)
        {
            return (Key)((int)Key.D0 + (int)(keysym - 0x30));
        }

        return keysym switch
        {
            0xFF08 => Key.Backspace,
            0xFF09 => Key.Tab,
            0xFF0D => Key.Enter,
            0xFF1B => Key.Escape,
            0xFF50 => Key.Home,
            0xFF51 => Key.Left,
            0xFF52 => Key.Up,
            0xFF53 => Key.Right,
            0xFF54 => Key.Down,
            0xFF55 => Key.PageUp,
            0xFF56 => Key.PageDown,
            0xFF57 => Key.End,
            0xFFFF => Key.Delete,
            _ => Key.None
        };
    }

    private static ModifierKeys GetModifiers(uint x11State)
    {
        // X11 state masks (X.h)
        const uint ShiftMask = 1u << 0;
        const uint ControlMask = 1u << 2;
        const uint Mod1Mask = 1u << 3; // usually Alt
        // const uint Mod4Mask = 1u << 6; // usually Super/Win (ignored for now)

        ModifierKeys modifiers = ModifierKeys.None;
        if ((x11State & ShiftMask) != 0)
        {
            modifiers |= ModifierKeys.Shift;
        }

        if ((x11State & ControlMask) != 0)
        {
            modifiers |= ModifierKeys.Control;
        }

        if ((x11State & Mod1Mask) != 0)
        {
            modifiers |= ModifierKeys.Alt;
        }

        return modifiers;
    }

    private void Render()
    {
        if (Handle == 0 || Display == 0)
        {
            return;
        }

        Window.PerformLayout();
        if (_glxVisualInfo is not { } visualInfo)
        {
            return;
        }

        var client = Window.ClientSize;
        int pixelWidth = (int)Math.Max(1, Math.Ceiling(client.Width * Window.DpiScale));
        int pixelHeight = (int)Math.Max(1, Math.Ceiling(client.Height * Window.DpiScale));
        var surface = new X11GlxWindowSurface(Display, Handle, visualInfo, Window.DpiScale, pixelWidth, pixelHeight);
        Window.RenderFrame(surface);
    }

    public void Dispose()
    {
        if (_disposed)
        {
            return;
        }

        _disposed = true;

        Window.ClearMouseOverState();
        Window.ClearMouseCaptureState();

        RaiseClosedOnce();
        Cleanup(Handle, destroyWindow: true);

        // Display lifetime is managed by the platform host (shared across windows).
    }

    private void Cleanup(nint handle, bool destroyWindow)
    {
        if (_cleanupDone)
        {
            return;
        }

        _cleanupDone = true;

        if (handle == 0 || Display == 0)
        {
            return;
        }

        try
        {
            if (_currentCursor != 0)
            {
                NativeX11.XFreeCursor(Display, _currentCursor);
                _currentCursor = 0;
            }

            if (_xic != 0)
            {
                NativeX11.XDestroyIC(_xic);
                _xic = 0;
            }
            if (_xim != 0)
            {
                _ = NativeX11.XCloseIM(_xim);
                _xim = 0;
            }
        }
        catch
        {
        }

        try
        {
            if (_imePreeditAttributesList != 0)
            {
                NativeX11.XFree(_imePreeditAttributesList);
                _imePreeditAttributesList = 0;
            }
            if (_imeSpotLocationPtr != 0)
            {
                Marshal.FreeHGlobal(_imeSpotLocationPtr);
                _imeSpotLocationPtr = 0;
            }
        }
        catch
        {
        }

        if (destroyWindow)
        {
            try { NativeX11.XDestroyWindow(Display, handle); }
            catch { }
        }

        try
        {
            Window.ReleaseWindowGraphicsResources(handle);
        }
        catch { }

        try { _host.UnregisterWindow(handle); } catch { }
        try { Window.DisposeVisualTree(); } catch { }


        if (Handle == handle)
        {
            Handle = 0;
        }
    }

    private void RaiseClosedOnce()
    {
        if (_closedRaised)
        {
            return;
        }

        _closedRaised = true;
        try { Window.RaiseClosed(); } catch { }
    }

    public void EnsureTheme(bool isDark)
    {
    }

    public void Activate()
    {
        if (Display == 0 || Handle == 0)
        {
            return;
        }

        try
        {
            NativeX11.XRaiseWindow(Display, Handle);
            if (NativeX11.XGetWindowAttributes(Display, Handle, out var attrs) != 0 && attrs.map_state == 2 /*IsViewable*/)
            {
                NativeX11.XSetInputFocus(Display, Handle, revert_to: 1, time: 0); // RevertToPointerRoot=1
            }
            NativeX11.XFlush(Display);
        }
        catch
        {
            // Best-effort.
        }
    }

    public void SetOwner(nint ownerHandle)
    {
        if (Display == 0 || Handle == 0)
        {
            return;
        }

        if (ownerHandle == 0)
        {
            return;
        }

        try
        {
            NativeX11.XSetTransientForHint(Display, Handle, ownerHandle);

            // Best-effort EWMH modal hint.
            var netWmState = NativeX11.XInternAtom(Display, "_NET_WM_STATE", false);
            var netWmStateModal = NativeX11.XInternAtom(Display, "_NET_WM_STATE_MODAL", false);
            if (netWmState != 0 && netWmStateModal != 0)
            {
                unsafe
                {
                    nint data = netWmStateModal;
                    NativeX11.XChangeProperty(Display, Handle, netWmState, type: 4 /*ATOM*/, format: 32, mode: 0 /*Replace*/, (nint)(&data), nelements: 1);
                }
            }
        }
        catch
        {
            // Best-effort.
        }
    }

    public void SetEnabled(bool enabled)
    {
        _enabled = enabled;
        if (!enabled)
        {
            Window.ClearMouseOverState();
            Window.ClearMouseCaptureState();
        }
    }

    public void SetOpacity(double opacity)
    {
        _opacity = Math.Clamp(opacity, 0.0, 1.0);
        ApplyOpacity();
    }

    public void SetAllowsTransparency(bool allowsTransparency)
    {
        _allowsTransparency = allowsTransparency;
        if (Display == 0 || Handle == 0 || _motifWmHintsAtom == 0)
        {
            return;
        }

        try
        {
            var hints = new MotifWmHints
            {
                flags = MotifFlags.Decorations,
                functions = 0,
                decorations = allowsTransparency ? 0u : 1u,
                inputMode = 0,
                status = 0
            };

            unsafe
            {
                NativeX11.XChangeProperty(
                    Display,
                    Handle,
                    _motifWmHintsAtom,
                    _motifWmHintsAtom,
                    format: 32,
                    mode: 0 /*Replace*/,
                    (nint)(&hints),
                    nelements: 5);
            }

            NativeX11.XFlush(Display);
        }
        catch
        {
            // Best-effort.
        }
    }

    public void SetCursor(CursorType cursorType)
    {
        if (Display == 0 || Handle == 0)
        {
            return;
        }

        // X11 standard cursor font shape constants (X11/cursorfont.h)
        const uint XC_left_ptr = 68;
        const uint XC_xterm = 152;
        const uint XC_watch = 150;
        const uint XC_crosshair = 34;
        const uint XC_sb_h_double_arrow = 108;
        const uint XC_sb_v_double_arrow = 116;
        const uint XC_fleur = 52;
        const uint XC_X_cursor = 0;
        const uint XC_hand2 = 60;
        const uint XC_question_arrow = 92;
        const uint XC_top_left_corner = 134;
        const uint XC_top_right_corner = 136;
        const uint XC_center_ptr = 22;

        uint shape = cursorType switch
        {
            CursorType.Arrow => XC_left_ptr,
            CursorType.IBeam => XC_xterm,
            CursorType.Wait => XC_watch,
            CursorType.Cross => XC_crosshair,
            CursorType.UpArrow => XC_center_ptr,
            CursorType.SizeNWSE => XC_top_left_corner,
            CursorType.SizeNESW => XC_top_right_corner,
            CursorType.SizeWE => XC_sb_h_double_arrow,
            CursorType.SizeNS => XC_sb_v_double_arrow,
            CursorType.SizeAll => XC_fleur,
            CursorType.No => XC_X_cursor,
            CursorType.Hand => XC_hand2,
            CursorType.Help => XC_question_arrow,
            _ => XC_left_ptr,
        };

        try
        {
            nint newCursor = NativeX11.XCreateFontCursor(Display, shape);
            if (newCursor != 0)
            {
                NativeX11.XDefineCursor(Display, Handle, newCursor);

                if (_currentCursor != 0)
                {
                    NativeX11.XFreeCursor(Display, _currentCursor);
                }

                _currentCursor = newCursor;
                NativeX11.XFlush(Display);
            }
        }
        catch
        {
            // Best-effort.
        }
    }

    private sealed class X11GlxWindowSurface : IX11GlxWindowSurface
    {
        public WindowSurfaceKind Kind => WindowSurfaceKind.OpenGL;

        public nint Handle => Window;

        public int PixelWidth { get; }

        public int PixelHeight { get; }

        public double DpiScale { get; }

        public nint Display { get; }

        public nint Window { get; }

        public X11GlxVisualInfo VisualInfo { get; }

        public X11GlxWindowSurface(nint display, nint window, X11GlxVisualInfo visualInfo, double dpiScale, int pixelWidth, int pixelHeight)
        {
            Display = display;
            Window = window;
            VisualInfo = visualInfo;
            DpiScale = dpiScale <= 0 ? 1.0 : dpiScale;
            PixelWidth = pixelWidth;
            PixelHeight = pixelHeight;
        }
    }
}

[StructLayout(LayoutKind.Sequential)]
internal struct MotifWmHints
{
    public uint flags;
    public uint functions;
    public uint decorations;
    public int inputMode;
    public uint status;
}

internal static class MotifFlags
{
    public const uint Decorations = 1u << 1;
}
