using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using SDL3;
using TerraFX.Interop.Windows;
using TerraFX.Interop.Xlib;
#if WINDOWS
using BOOL = TerraFX.Interop.Windows.BOOL;
using Windows = TerraFX.Interop.Windows.Windows;
#endif
using GLAttr = SDL3.SDL.SDL_GLAttr;
using X11Window = TerraFX.Interop.Xlib.Window;

namespace Robust.Client.Graphics.Clyde;

internal partial class Clyde
{
    private sealed partial class Sdl3WindowingImpl
    {
        private int _nextWindowId = 1;
        private bool _progressUnavailable;

        public (WindowReg?, string? error) WindowCreate(
            GLContextSpec? spec,
            WindowCreateParameters parameters,
            WindowReg? share,
            WindowReg? owner)
        {
            nint shareWindow = 0;
            nint shareContext = 0;
            if (share is Sdl3WindowReg shareReg)
            {
                shareWindow = shareReg.Sdl3Window;
                shareContext = shareReg.GlContext;
            }

            nint ownerPtr = 0;
            if (owner is Sdl3WindowReg ownerReg)
                ownerPtr = ownerReg.Sdl3Window;

            var task = SharedWindowCreate(spec, parameters, shareWindow, shareContext, ownerPtr);

            // Block the main thread (to avoid stuff like texture uploads being problematic).
            WaitWindowCreate(task);

#pragma warning disable RA0004
            // Block above ensured task is done, this is safe.
            var result = task.Result;
#pragma warning restore RA0004
            if (result.Reg != null)
            {
                result.Reg.Owner = result.Reg.Handle;
            }

            return (result.Reg, result.Error);
        }

        private void WaitWindowCreate(Task<Sdl3WindowCreateResult> windowTask)
        {
            while (!windowTask.IsCompleted)
            {
                // Keep processing events until the window task gives either an error or success.
                WaitEvents();
                ProcessEvents(single: true);
            }
        }

        private Task<Sdl3WindowCreateResult> SharedWindowCreate(
            GLContextSpec? glSpec,
            WindowCreateParameters parameters,
            nint shareWindow,
            nint shareContext,
            nint owner)
        {
            //
            // IF YOU'RE WONDERING WHY THIS IS TASK-BASED:
            // I originally wanted this to be async so we could avoid blocking the main thread
            // while the OS takes its stupid 100~ms just to initialize a fucking GL context.
            // This doesn't *work* because
            // we have to release the GL context while the shared context is being created.
            // (at least on WGL, I didn't test other platforms and I don't care to.)
            // Not worth it to avoid a main thread blockage by allowing Clyde to temporarily release the GL context,
            // because rendering would be locked up *anyways*.
            //
            // Basically what I'm saying is that everything about OpenGL is a fucking mistake
            // and I should get on either Veldrid or Vulkan some time.
            // Probably Veldrid tbh.
            //

            // Yes we ping-pong this TCS through the window thread and back, deal with it.
            var tcs = new TaskCompletionSource<Sdl3WindowCreateResult>();
            SendCmd(new CmdWinCreate
            {
                GLSpec = glSpec,
                Parameters = parameters,
                ShareWindow = shareWindow,
                ShareContext = shareContext,
                OwnerWindow = owner,
                Tcs = tcs
            });
            return tcs.Task;
        }

        private static void FinishWindowCreate(EventWindowCreate ev)
        {
            ev.Tcs.TrySetResult(ev.Result);
        }

        private void WinThreadWinCreate(CmdWinCreate cmd)
        {
            var (window, context) = CreateSdl3WindowForRenderer(
                cmd.GLSpec,
                cmd.Parameters,
                cmd.ShareWindow,
                cmd.ShareContext,
                cmd.OwnerWindow);

            if (window == 0)
            {
                var err = SDL.SDL_GetError();

                SendEvent(new EventWindowCreate
                {
                    Result = new Sdl3WindowCreateResult { Error = err },
                    Tcs = cmd.Tcs
                });
                return;
            }

            // We can't invoke the TCS directly from the windowing thread because:
            // * it'd hit the synchronization context,
            //   which would make (blocking) main window init more annoying.
            // * it'd not be synchronized to other incoming window events correctly which might be icky.
            // So we send the TCS back to the game thread
            // which processes events in the correct order and has better control of stuff during init.
            var reg = WinThreadSetupWindow(window, context);

            SendEvent(new EventWindowCreate
            {
                Result = new Sdl3WindowCreateResult { Reg = reg },
                Tcs = cmd.Tcs
            });
        }

        private void WinThreadWinDestroy(CmdWinDestroy cmd)
        {
            SDL.SDL_DestroyWindow(cmd.Window);
#if MACOS
            SendEvent(new EventWindowDestroyed());
#endif
        }

        private (nint window, nint context) CreateSdl3WindowForRenderer(
            GLContextSpec? spec,
            WindowCreateParameters parameters,
            nint shareWindow,
            nint shareContext,
            nint ownerWindow)
        {
            var createProps = SDL.SDL_CreateProperties();
            SDL.SDL_SetBooleanProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_HIDDEN_BOOLEAN, true);
            SDL.SDL_SetBooleanProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_RESIZABLE_BOOLEAN, true);
            SDL.SDL_SetBooleanProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_HIGH_PIXEL_DENSITY_BOOLEAN, true);

            if (spec is { } s)
            {
                SDL.SDL_SetBooleanProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_OPENGL_BOOLEAN, true);

                SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_RED_SIZE, 8);
                SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_GREEN_SIZE, 8);
                SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_BLUE_SIZE, 8);
                SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_ALPHA_SIZE, 8);
                SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_STENCIL_SIZE, 8);
                SDL.SDL_GL_SetAttribute(
                    GLAttr.SDL_GL_FRAMEBUFFER_SRGB_CAPABLE,
                    s.Profile == GLContextProfile.Es ? 0 : 1);
                int ctxFlags = 0;
#if DEBUG
                ctxFlags |= SDL.SDL_GL_CONTEXT_DEBUG_FLAG;
#endif
                if (s.Profile == GLContextProfile.Core)
                    ctxFlags |= SDL.SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG;

                SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_CONTEXT_FLAGS, (int)ctxFlags);

                if (shareContext != 0)
                {
                    SDL.SDL_GL_MakeCurrent(shareWindow, shareContext);
                    SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 1);
                }
                else
                {
                    SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 0);
                }

                var profile = s.Profile switch
                {
                    GLContextProfile.Compatibility => SDL.SDL_GL_CONTEXT_PROFILE_COMPATIBILITY,
                    GLContextProfile.Core => SDL.SDL_GL_CONTEXT_PROFILE_CORE,
                    GLContextProfile.Es => SDL.SDL_GL_CONTEXT_PROFILE_ES,
                    _ => SDL.SDL_GL_CONTEXT_PROFILE_COMPATIBILITY,
                };

                SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_CONTEXT_PROFILE_MASK, profile);
                SDL.SDL_SetHint(SDL.SDL_HINT_OPENGL_ES_DRIVER, s.CreationApi == GLContextCreationApi.Egl ? "1" : "0");

                SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_CONTEXT_MAJOR_VERSION, s.Major);
                SDL.SDL_GL_SetAttribute(GLAttr.SDL_GL_CONTEXT_MINOR_VERSION, s.Minor);

                if (s.CreationApi == GLContextCreationApi.Egl)
                    WsiShared.EnsureEglAvailable();
            }

            if (parameters.Fullscreen)
                SDL.SDL_SetBooleanProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_FULLSCREEN_BOOLEAN, true);

            if ((parameters.Styles & OSWindowStyles.NoTitleBar) != 0)
                SDL.SDL_SetBooleanProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_BORDERLESS_BOOLEAN, true);

            if (ownerWindow != 0)
            {
                SDL.SDL_SetPointerProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_PARENT_POINTER, ownerWindow);

                if (parameters.StartupLocation == WindowStartupLocation.CenterOwner)
                {
                    SDL.SDL_GetWindowSize(ownerWindow, out var parentW, out var parentH);
                    SDL.SDL_GetWindowPosition(ownerWindow, out var parentX, out var parentY);

                    SDL.SDL_SetNumberProperty(
                        createProps,
                        SDL.SDL_PROP_WINDOW_CREATE_X_NUMBER,
                        parentX + (parentW - parameters.Width) / 2);
                    SDL.SDL_SetNumberProperty(
                        createProps,
                        SDL.SDL_PROP_WINDOW_CREATE_Y_NUMBER,
                        parentY + (parentH - parameters.Height) / 2);
                }
            }

            SDL.SDL_SetNumberProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, parameters.Width);
            SDL.SDL_SetNumberProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, parameters.Height);
            SDL.SDL_SetStringProperty(createProps, SDL.SDL_PROP_WINDOW_CREATE_TITLE_STRING, parameters.Title);

            // ---> CREATE <---
            var window = SDL.SDL_CreateWindowWithProperties(createProps);

            SDL.SDL_DestroyProperties(createProps);

            if (window == 0)
                return default;

            nint glContext = SDL.SDL_GL_CreateContext(window);
            if (glContext == 0)
            {
                SDL.SDL_DestroyWindow(window);
                return default;
            }

            if ((parameters.Styles & OSWindowStyles.NoTitleOptions) != 0)
            {
                var props = SDL.SDL_GetWindowProperties(window);
                switch (_videoDriver)
                {
                    case SdlVideoDriver.Windows:
                    {
                        var hWnd = SDL.SDL_GetPointerProperty(
                            props,
                            SDL.SDL_PROP_WINDOW_WIN32_HWND_POINTER,
                            0);
                        WsiShared.SetWindowStyleNoTitleOptionsWindows((HWND)hWnd);
                        break;
                    }
                    case SdlVideoDriver.X11:
                        unsafe
                        {
                            var x11Display = (Display*)SDL.SDL_GetPointerProperty(
                                props,
                                SDL.SDL_PROP_WINDOW_X11_DISPLAY_POINTER,
                                0);
                            var x11Window = (X11Window)SDL.SDL_GetNumberProperty(
                                props,
                                SDL.SDL_PROP_WINDOW_X11_WINDOW_NUMBER,
                                0);
                            WsiShared.SetWindowStyleNoTitleOptionsX11(x11Display, x11Window);
                            break;
                        }

                    default:
                        _sawmill.Warning("OSWindowStyles.NoTitleOptions not implemented on this video driver");
                        break;
                }
            }

            // TODO: Monitors, window maximize.

            // Make sure window thread doesn't keep hold of the GL context.
            SDL.SDL_GL_MakeCurrent(IntPtr.Zero, IntPtr.Zero);

            if (parameters.Visible)
                SDL.SDL_ShowWindow(window);

            return (window, glContext);
        }

        private Sdl3WindowReg WinThreadSetupWindow(nint window, nint context)
        {
            var reg = new Sdl3WindowReg
            {
                Sdl3Window = window,
                GlContext = context,
                WindowId = SDL.SDL_GetWindowID(window),
                Id = new WindowId(_nextWindowId++)
            };
            var handle = new WindowHandle(_clyde, reg);
            reg.Handle = handle;

            var windowProps = SDL.SDL_GetWindowProperties(window);
            switch (_videoDriver)
            {
                case SdlVideoDriver.Windows:
                    reg.WindowsHwnd = SDL.SDL_GetPointerProperty(
                        windowProps,
                        SDL.SDL_PROP_WINDOW_WIN32_HWND_POINTER,
                        0);
                    break;
                case SdlVideoDriver.X11:
                    reg.X11Display = SDL.SDL_GetPointerProperty(
                        windowProps,
                        SDL.SDL_PROP_WINDOW_X11_DISPLAY_POINTER,
                        0);
                    reg.X11Id = (uint)SDL.SDL_GetNumberProperty(windowProps, SDL.SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0);
                    break;
            }

            AssignWindowIconToWindow(window);

            SDL.SDL_GetWindowSizeInPixels(window, out var fbW, out var fbH);
            reg.FramebufferSize = (fbW, fbH);

            var scale = SDL.SDL_GetWindowDisplayScale(window);
            reg.WindowScale = new Vector2(scale, scale);

            SDL.SDL_GetWindowSize(window, out var w, out var h);
            reg.PrevWindowSize = reg.WindowSize = (w, h);

            SDL.SDL_GetWindowPosition(window, out var x, out var y);
            reg.PrevWindowPos = reg.WindowPos = (x, y);

            reg.PixelRatio = reg.FramebufferSize / (Vector2)reg.WindowSize;

            return reg;
        }

        public void WindowDestroy(WindowReg window)
        {
            var reg = (Sdl3WindowReg)window;
            SendCmd(new CmdWinDestroy
            {
                Window = reg.Sdl3Window,
                HadOwner = window.Owner != null
            });
        }

        public void UpdateMainWindowMode()
        {
            if (_clyde._mainWindow == null)
                return;

            var win = (Sdl3WindowReg)_clyde._mainWindow;

            if (_clyde._windowMode == WindowMode.Fullscreen)
            {
                win.PrevWindowSize = win.WindowSize;
                win.PrevWindowPos = win.WindowPos;

                SendCmd(new CmdWinWinSetFullscreen
                {
                    Window = win.Sdl3Window,
                });
            }
            else
            {
                SendCmd(new CmdWinSetWindowed
                {
                    Window = win.Sdl3Window,
                    Width = win.PrevWindowSize.X,
                    Height = win.PrevWindowSize.Y,
                    PosX = win.PrevWindowPos.X,
                    PosY = win.PrevWindowPos.Y
                });
            }
        }

        private static void WinThreadWinSetFullscreen(CmdWinWinSetFullscreen cmd)
        {
            SDL.SDL_SetWindowFullscreen(cmd.Window, true);
        }

        private static void WinThreadWinSetWindowed(CmdWinSetWindowed cmd)
        {
            SDL.SDL_SetWindowFullscreen(cmd.Window, false);
            SDL.SDL_SetWindowSize(cmd.Window, cmd.Width, cmd.Height);
            SDL.SDL_SetWindowPosition(cmd.Window, cmd.PosX, cmd.PosY);
        }

        public void WindowSetTitle(WindowReg window, string title)
        {
            SendCmd(new CmdWinSetTitle
            {
                Window = WinPtr(window),
                Title = title,
            });
        }

        private static void WinThreadWinSetTitle(CmdWinSetTitle cmd)
        {
            SDL.SDL_SetWindowTitle(cmd.Window, cmd.Title);
        }

        public void WindowSetMonitor(WindowReg window, IClydeMonitor monitor)
        {
            // API isn't really used and kinda wack, don't feel like figuring it out for SDL3 yet.
            _sawmill.Warning("WindowSetMonitor not implemented on SDL3");
        }

        public void WindowSetSize(WindowReg window, Vector2i size)
        {
            SendCmd(new CmdWinSetSize { Window = WinPtr(window), W = size.X, H = size.Y });
        }

        public void WindowSetVisible(WindowReg window, bool visible)
        {
            window.IsVisible = visible;
            SendCmd(new CmdWinSetVisible { Window = WinPtr(window), Visible = visible });
        }

        private static void WinThreadWinSetSize(CmdWinSetSize cmd)
        {
            var density = SDL.SDL_GetWindowPixelDensity(cmd.Window);
            SDL.SDL_SetWindowSize(cmd.Window, (int)(cmd.W / density), (int)(cmd.H / density));
        }

        private static void WinThreadWinSetVisible(CmdWinSetVisible cmd)
        {
            if (cmd.Visible)
                SDL.SDL_ShowWindow(cmd.Window);
            else
                SDL.SDL_HideWindow(cmd.Window);
        }

        public void WindowRequestAttention(WindowReg window)
        {
            SendCmd(new CmdWinRequestAttention { Window = WinPtr(window) });
        }

        private void WinThreadWinRequestAttention(CmdWinRequestAttention cmd)
        {
            var res = SDL.SDL_FlashWindow(cmd.Window, SDL.SDL_FlashOperation.SDL_FLASH_UNTIL_FOCUSED);
            if (!res)
                _sawmill.Error("Failed to flash window: {error}", SDL.SDL_GetError());
        }

        public void WindowSetProgress(WindowReg window, WindowProgressState state, float value)
        {
            SendCmd(new CmdWinSetProgress
            {
                Window = WinPtr(window),
                State = (SDL.SDL_ProgressState)state,
                Value = value
            });
        }

        private void WinThreadWinSetProgress(CmdWinSetProgress cmd)
        {
            if (_progressUnavailable)
                return;

            try
            {
                var res = SDL.SDL_SetWindowProgressState(cmd.Window, cmd.State);
                if (!res)
                {
                    _sawmill.Error("Failed to set window progress state: {error}", SDL.SDL_GetError());
                    return;
                }

                res = SDL.SDL_SetWindowProgressValue(cmd.Window, cmd.Value);
                if (!res)
                    _sawmill.Error("Failed to set window progress value: {error}", SDL.SDL_GetError());
            }
            catch (EntryPointNotFoundException)
            {
                // Allowing it to fail means I don't have to update the launcher immediately :)
                _progressUnavailable = true;
                _sawmill.Debug("SDL3 progress APIs unavailable");
            }
        }

        public unsafe void WindowSwapBuffers(WindowReg window)
        {
            var reg = (Sdl3WindowReg)window;
            var windowPtr = WinPtr(reg);

#if WINDOWS
            // On Windows, SwapBuffers does not correctly sync to the DWM compositor.
            // This means OpenGL vsync is effectively broken by default on Windows.
            // We manually sync via DwmFlush(). GLFW does this automatically, SDL3 does not.
            //
            // Windows DwmFlush logic partly taken from:
            // https://github.com/love2d/love/blob/5175b0d1b599ea4c7b929f6b4282dd379fa116b8/src/modules/window/sdl/Window.cpp#L1018
            // https://github.com/glfw/glfw/blob/d3ede7b6847b66cf30b067214b2b4b126d4c729b/src/wgl_context.c#L321-L340
            // See also: https://github.com/libsdl-org/SDL/issues/5797

            var dwmFlush = false;
            var swapInterval = 0;

            if (!reg.Fullscreen && reg.SwapInterval > 0)
            {
                BOOL compositing;
                // 6.2 is Windows 8
                // https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_osversioninfoexw
                if (OperatingSystem.IsWindowsVersionAtLeast(6, 2)
                    || Windows.SUCCEEDED(Windows.DwmIsCompositionEnabled(&compositing)) && compositing)
                {
                    var curCtx = SDL.SDL_GL_GetCurrentContext();
                    var curWin = SDL.SDL_GL_GetCurrentWindow();

                    if (curCtx != reg.GlContext || curWin != reg.Sdl3Window)
                        throw new InvalidOperationException("Window context must be current!");

                    SDL.SDL_GL_SetSwapInterval(0);
                    dwmFlush = true;
                    swapInterval = reg.SwapInterval;
                }
            }
#endif

            //_sawmill.Debug($"Swapping: {window.Id} @ {_clyde._gameTiming.CurFrame}");
            SDL.SDL_GL_SwapWindow(windowPtr);

#if WINDOWS
            if (dwmFlush)
            {
                var i = swapInterval;
                while (i-- > 0)
                {
                    Windows.DwmFlush();
                }

                SDL.SDL_GL_SetSwapInterval(swapInterval);
            }
#endif
        }

        public uint? WindowGetX11Id(WindowReg window)
        {
            CheckWindowDisposed(window);

            if (_videoDriver != SdlVideoDriver.X11)
                return null;

            var reg = (Sdl3WindowReg)window;
            return reg.X11Id;
        }

        public nint? WindowGetX11Display(WindowReg window)
        {
            CheckWindowDisposed(window);

            if (_videoDriver != SdlVideoDriver.X11)
                return null;

            var reg = (Sdl3WindowReg)window;
            return reg.X11Display;
        }

        public nint? WindowGetWin32Window(WindowReg window)
        {
            CheckWindowDisposed(window);

            if (_videoDriver != SdlVideoDriver.Windows)
                return null;

            var reg = (Sdl3WindowReg)window;
            return reg.WindowsHwnd;
        }

        public void RunOnWindowThread(Action a)
        {
            SendCmd(new CmdRunAction { Action = a });
        }

        public void TextInputSetRect(WindowReg reg, UIBox2i rect, int cursor)
        {
            var ratio = ((Sdl3WindowReg)reg).PixelRatio;
            SendCmd(new CmdTextInputSetRect
            {
                Window = WinPtr(reg),
                Rect = new SDL.SDL_Rect
                {
                    x = (int)(rect.Left / ratio.X),
                    y = (int)(rect.Top / ratio.Y),
                    w = (int)(rect.Width / ratio.X),
                    h = (int)(rect.Height / ratio.Y)
                },
                Cursor = (int)(cursor / ratio.X)
            });
        }

        private static void WinThreadSetTextInputRect(CmdTextInputSetRect cmdTextInput)
        {
            var rect = cmdTextInput.Rect;
            SDL.SDL_SetTextInputArea(cmdTextInput.Window, ref rect, cmdTextInput.Cursor);
        }

        public void TextInputStart(WindowReg reg)
        {
            SendCmd(new CmdTextInputStart { Window = WinPtr(reg) });
        }

        private static void WinThreadStartTextInput(CmdTextInputStart cmd)
        {
            SDL.SDL_StartTextInput(cmd.Window);
        }

        public void TextInputStop(WindowReg reg)
        {
            SendCmd(new CmdTextInputStop { Window = WinPtr(reg) });
        }

        private static void WinThreadStopTextInput(CmdTextInputStop cmd)
        {
            SDL.SDL_StopTextInput(cmd.Window);
        }

        public void ClipboardSetText(WindowReg mainWindow, string text)
        {
            SendCmd(new CmdSetClipboard { Text = text });
        }

        private void WinThreadSetClipboard(CmdSetClipboard cmd)
        {
            var res = SDL.SDL_SetClipboardText(cmd.Text);
            if (!res)
                _sawmill.Error("Failed to set clipboard text: {error}", SDL.SDL_GetError());
        }

        public Task<string> ClipboardGetText(WindowReg mainWindow)
        {
            var tcs = new TaskCompletionSource<string>();
            SendCmd(new CmdGetClipboard { Tcs = tcs });
            return tcs.Task;
        }

        private static void WinThreadGetClipboard(CmdGetClipboard cmd)
        {
            cmd.Tcs.TrySetResult(SDL.SDL_GetClipboardText());
        }

        private static void CheckWindowDisposed(WindowReg reg)
        {
            if (reg.IsDisposed)
                throw new ObjectDisposedException("Window disposed");
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static nint WinPtr(WindowReg reg) => ((Sdl3WindowReg)reg).Sdl3Window;

        private WindowReg? FindWindow(uint windowId)
        {
            foreach (var windowReg in _clyde._windows)
            {
                var glfwReg = (Sdl3WindowReg)windowReg;
                if (glfwReg.WindowId == windowId)
                    return windowReg;
            }

            return null;
        }


        private sealed class Sdl3WindowReg : WindowReg
        {
            public nint Sdl3Window;
            public uint WindowId;
            public nint GlContext;
#pragma warning disable CS0649
            public bool Fullscreen;
#pragma warning restore CS0649
            public int SwapInterval;

            // Kept around to avoid it being GCd.
            public CursorImpl? Cursor;

            public nint WindowsHwnd;
            public nint X11Display;
            public uint X11Id;
        }
    }
}
