/*
Crafter®.Graphics
Copyright (C) 2026 Catcrafts®
catcrafts.net

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License version 3.0 as published by the Free Software Foundation;

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA
*/
module;
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <linux/input-event-codes.h>
#include "../lib/xdg-shell-client-protocol.h"
#include "../lib/wayland-xdg-decoration-unstable-v1-client-protocol.h"
#include "../lib/fractional-scale-v1.h"
#include "../lib/viewporter.h"
#include <string.h>
#include <cassert>
#include <linux/input.h>
#include <sys/mman.h>
#include <wayland-cursor.h>
#include <xkbcommon/xkbcommon.h>
#include <errno.h>
#include <fcntl.h>
#include <print>
#include <wayland-client.h>
#include <wayland-client-protocol.h>
#include <sys/stat.h>
#include <time.h>
#endif
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
#include <windows.h>
#include <cassert>
#endif
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
#include "vulkan/vulkan.h"
#endif
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
#include "vulkan/vulkan_wayland.h"
#endif
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
#include "vulkan/vulkan_win32.h"
#endif
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "../lib/stb_image_write.h"
#endif
module Crafter.Graphics:Window_impl;
import :Window;
import :Device;
import :Gamepad;
// The Vulkan-typed partitions exist as empty stubs in DOM builds (the
// build system scans `import :X` statements pre-preprocessor, so even
// guarded imports must resolve to a real partition). Their bodies are
// gated under !CRAFTER_GRAPHICS_WINDOW_DOM so DOM compiles see empty
// modules. Cheap.
import :VulkanTransition;
import :DescriptorHeapVulkan;
import :RenderPass;
#ifdef CRAFTER_GRAPHICS_WINDOW_DOM
import :WebGPU;
import :DescriptorHeapWebGPU;
#endif
import std;

using namespace Crafter;

#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
void randname(char *buf) {
	struct timespec ts;
	clock_gettime(CLOCK_REALTIME, &ts);
	long r = ts.tv_nsec;
	for (int i = 0; i < 6; ++i) {
		buf[i] = 'A'+(r&15)+(r&16)*2;
		r >>= 5;
	}
}

int anonymous_shm_open(void) {
	char name[] = "/hello-wayland-XXXXXX";
	int retries = 100;

	do {
		randname(name + strlen(name) - 6);

		--retries;
		// shm_open guarantees that O_CLOEXEC is set
		int fd = shm_open(name, O_RDWR | O_CREAT | O_EXCL, 0600);
		if (fd >= 0) {
			shm_unlink(name);
			return fd;
		}
	} while (retries > 0 && errno == EEXIST);

	return -1;
}

int create_shm_file(off_t size) {
	int fd = anonymous_shm_open();
	if (fd < 0) {
		return fd;
	}

	if (ftruncate(fd, size) < 0) {
		close(fd);
		return -1;
	}

	return fd;
}
#endif

#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
// Extract the layout-independent raw key code from a WM_KEY* lParam. Bits
// 16-23 hold the PS/2 set-1 scancode byte; bit 24 is the extended-key flag
// (the 0xE0-prefixed variants — RightCtrl, RightAlt, the cursor cluster,
// keypad Enter/Slash, the Windows keys). We pack the extended flag into bit
// 8 of the returned KeyCode so it round-trips with the compile-time
// `Key(CrafterKeys::...)` table in :Keys.
static inline KeyCode KeyCodeFromLParam(LPARAM lParam) {
    return ((KeyCode)((lParam >> 16) & 0xFF))
         | (((lParam >> 24) & 1u) << 8);
}

// Define a window class name
const char g_szClassName[] = "myWindowClass";

// Window procedure function that processes messages
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {

    Window* window = nullptr;

    if (msg == WM_NCCREATE)
    {
        CREATESTRUCT* pCreate = reinterpret_cast<CREATESTRUCT*>(lParam);
        window = static_cast<Window*>(pCreate->lpCreateParams);

        SetWindowLongPtr(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(window));

        return TRUE;
    }
    else
    {
        window = reinterpret_cast<Window*>(
            GetWindowLongPtr(hwnd, GWLP_USERDATA)
        );
    }

    switch (msg) {
        case WM_DESTROY:{
            PostQuitMessage(0);
            break;
        }
        case WM_SIZE: {
            // SIZE_MINIMIZED reports (0, 0) — Resize() short-circuits, so
            // we just propagate the values directly. WM_SIZE fires
            // synchronously during a drag-resize loop; the StartSync
            // pump runs WndProc between frames, so the swapchain is
            // never touched mid-Render.
            if (window) {
                window->Resize(LOWORD(lParam), HIWORD(lParam));
            }
            break;
        }
        case WM_KEYDOWN:
        case WM_SYSKEYDOWN: {  // SYSKEYDOWN catches Alt combos, F10, etc.
            KeyCode code = KeyCodeFromLParam(lParam);
            bool isRepeat = (lParam & (1 << 30)) != 0;

            if (isRepeat) {
                window->onRawKeyHold.Invoke(code);
            } else {
                window->heldKeys.insert(code);
                window->onRawKeyDown.Invoke(code);
            }
            break;
        }

        case WM_KEYUP:
        case WM_SYSKEYUP: {
            KeyCode code = KeyCodeFromLParam(lParam);
            window->heldKeys.erase(code);
            window->onRawKeyUp.Invoke(code);
            break;
        }

        case WM_CHAR: {
            // wParam is a UTF-16 code unit. May be a surrogate — buffer until we have a pair.
            wchar_t wc = (wchar_t)wParam;

            // Filter control characters (backspace=0x08, tab=0x09, enter=0x0D, escape=0x1B, etc.)
            if (wc < 0x20 || wc == 0x7f) break;

            // Handle UTF-16 surrogate pairs (characters outside the BMP, e.g. emoji).
            static wchar_t highSurrogate = 0;
            wchar_t utf16[2];
            int utf16Len;

            if (wc >= 0xD800 && wc <= 0xDBFF) {
                // High surrogate — stash it and wait for the low surrogate.
                highSurrogate = wc;
                break;
            } else if (wc >= 0xDC00 && wc <= 0xDFFF) {
                // Low surrogate — pair with the stashed high surrogate.
                if (highSurrogate == 0) break;  // orphaned low surrogate, ignore
                utf16[0] = highSurrogate;
                utf16[1] = wc;
                utf16Len = 2;
                highSurrogate = 0;
            } else {
                utf16[0] = wc;
                utf16Len = 1;
            }

            // Convert UTF-16 to UTF-8.
            char utf8[8];
            int n = WideCharToMultiByte(CP_UTF8, 0, utf16, utf16Len, utf8, sizeof(utf8), nullptr, nullptr);
            if (n > 0) {
                window->onTextInput.Invoke(std::string(utf8, n));
            }
            break;
        }
        case WM_LBUTTONDOWN: {
            window->mouseLeftHeld = true;
			window->onMouseLeftClick.Invoke();
            break;
        }

        case WM_LBUTTONUP: {
            window->mouseLeftHeld = false;
			window->onMouseLeftRelease.Invoke();	
            break;
        }

        case WM_RBUTTONDOWN: {
            window->mouseRightHeld = true;
			window->onMouseRightClick.Invoke();
            break;
        }

        case WM_RBUTTONUP: {
            window->mouseRightHeld = false;
			window->onMouseRightRelease.Invoke();
            break;
        }

        case WM_MOUSEWHEEL: {
            // High word of wParam is the signed wheel movement, WHEEL_DELTA
            // (120) per detent, positive = wheel away from the user (scroll
            // up). Window::onMouseScroll speaks whole detents with +1 =
            // wheel down (the DOM deltaY / Wayland axis sign), so negate.
            // Free-spinning wheels report sub-WHEEL_DELTA steps — accumulate
            // so they add up instead of each truncating to zero.
            static int wheelRemainder = 0;
            wheelRemainder += -static_cast<int>(GET_WHEEL_DELTA_WPARAM(wParam));
            int detents = wheelRemainder / WHEEL_DELTA;  // trunc toward zero
            if (detents != 0) {
                wheelRemainder -= detents * WHEEL_DELTA;
                // Event payload is uint32 — preserve sign via two's complement.
                window->onMouseScroll.Invoke(
                    static_cast<std::uint32_t>(static_cast<std::int32_t>(detents)));
            }
            break;
        }

        case WM_SETCURSOR: {
            if (LOWORD(lParam) == HTCLIENT && window->cursorHandle) {
                SetCursor(window->cursorHandle);
                return TRUE;
            }
            break;
        }

        default: return DefWindowProc(hwnd, msg, wParam, lParam);
    }
    return 0;
}

#endif


Window::Window(std::uint32_t width, std::uint32_t height, const std::string_view title) : Window(width, height) {
    SetTitle(title);
}

Window::Window(std::uint32_t width, std::uint32_t height) : width(width), height(height) {
    #ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
    Device::windows.push_back(this);
	surface = wl_compositor_create_surface(Device::compositor);
	xdgSurface = xdg_wm_base_get_xdg_surface(Device::xdgWmBase, surface);
	xdgToplevel = xdg_surface_get_toplevel(xdgSurface);

	xdg_surface_add_listener(xdgSurface, &xdg_surface_listener, this);
	xdg_toplevel_add_listener(xdgToplevel, &xdg_toplevel_listener, this);
	wl_surface_commit(surface);

    wp_scale = wp_fractional_scale_manager_v1_get_fractional_scale(Device::fractionalScaleManager, surface);
    wp_fractional_scale_v1_add_listener(wp_scale, &wp_fractional_scale_v1_listener, this);

	while (wl_display_dispatch(Device::display) != -1 && !configured) {}

	wl_surface_commit(surface);

	zxdg_toplevel_decoration_v1* decoration = zxdg_decoration_manager_v1_get_toplevel_decoration(Device::manager, xdgToplevel);
	zxdg_toplevel_decoration_v1_set_mode(decoration, ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE);

    wpViewport = wp_viewporter_get_viewport(Device::wpViewporter, surface);
    wp_viewport_set_destination(wpViewport, std::ceil(width/scale), std::ceil(height/scale));

    wl_surface_commit(surface);
    #endif

    #ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
    // Initialize the window class
    WNDCLASS wc = {0};
    wc.lpfnWndProc = WndProc;           // Set window procedure
    wc.hInstance = GetModuleHandle(NULL); // Get instance handle
    wc.lpszClassName = g_szClassName;
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);

    if (!RegisterClass(&wc)) {
        MessageBox(NULL, "Window Class Registration Failed!", "Error", MB_ICONERROR);
    }

    RECT rc = {0, 0, static_cast<LONG>(width), static_cast<LONG>(height)};
    AdjustWindowRect(&rc, WS_OVERLAPPEDWINDOW, FALSE);

    HWND hwnd = CreateWindowEx(
        0,
        g_szClassName,
        "",
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT,
        rc.right - rc.left,
        rc.bottom - rc.top,
        NULL, NULL, wc.hInstance, this
    );


    if (hwnd == NULL) {
        MessageBox(NULL, "Window Creation Failed!", "Error", MB_ICONERROR);
    }

    // Show the window
    ShowWindow(hwnd, SW_SHOWNORMAL);
    UpdateWindow(hwnd);

    MSG msg;
    while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    VkWin32SurfaceCreateInfoKHR createInfo = {};
    createInfo.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR;
    createInfo.hinstance = wc.hInstance;
    createInfo.hwnd = hwnd;
    Device::CheckVkResult(vkCreateWin32SurfaceKHR(Device::instance, &createInfo, NULL, &vulkanSurface));
    #endif

    #ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
    VkWaylandSurfaceCreateInfoKHR createInfo = {};
    createInfo.sType = VK_STRUCTURE_TYPE_WAYLAND_SURFACE_CREATE_INFO_KHR;
    createInfo.display = Device::display;
    createInfo.surface = surface;
    Device::CheckVkResult(vkCreateWaylandSurfaceKHR(Device::instance, &createInfo, NULL, &vulkanSurface));
    #endif

	// Get list of supported surface formats
	std::uint32_t formatCount;
	Device::CheckVkResult(vkGetPhysicalDeviceSurfaceFormatsKHR(Device::physDevice, vulkanSurface, &formatCount, NULL));
	assert(formatCount > 0);

	std::vector<VkSurfaceFormatKHR> surfaceFormats(formatCount);
	Device::CheckVkResult(vkGetPhysicalDeviceSurfaceFormatsKHR(Device::physDevice, vulkanSurface, &formatCount, surfaceFormats.data()));

	// We want to get a format that best suits our needs, so we try to get one from a set of preferred formats
	// Initialize the format to the first one returned by the implementation in case we can't find one of the preffered formats
	VkSurfaceFormatKHR selectedFormat = surfaceFormats[0];
	std::vector<VkFormat> preferredImageFormats = { 
		VK_FORMAT_R8G8B8A8_UNORM, 
        VK_FORMAT_B8G8R8A8_UNORM
	};

	for (auto& availableFormat : surfaceFormats) {
		if (std::find(preferredImageFormats.begin(), preferredImageFormats.end(), availableFormat.format) != preferredImageFormats.end()) {
			selectedFormat = availableFormat;
			break;
		}
	}

	colorFormat = selectedFormat.format;
	colorSpace = selectedFormat.colorSpace;

	// Select a supported composite alpha format (not all devices support alpha
	// opaque). This is a surface property, so pick it once here rather than on
	// every CreateSwapchain. Simply select the first available.
	{
		VkSurfaceCapabilitiesKHR surfCaps;
		Device::CheckVkResult(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(Device::physDevice, vulkanSurface, &surfCaps));
		const VkCompositeAlphaFlagBitsKHR compositeAlphaFlags[] = {
			VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR,
			VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR,
			VK_COMPOSITE_ALPHA_POST_MULTIPLIED_BIT_KHR,
			VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR,
		};
		for (const VkCompositeAlphaFlagBitsKHR compositeAlphaFlag : compositeAlphaFlags) {
			if (surfCaps.supportedCompositeAlpha & compositeAlphaFlag) {
				compositeAlpha = compositeAlphaFlag;
				break;
			}
		}
	}

    CreateSwapchain();

    VkCommandBufferAllocateInfo cmdBufAllocateInfo {};
    cmdBufAllocateInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
    cmdBufAllocateInfo.commandPool = Device::commandPool;
    cmdBufAllocateInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
    cmdBufAllocateInfo.commandBufferCount = numFrames;
	Device::CheckVkResult(vkAllocateCommandBuffers(Device::device, &cmdBufAllocateInfo, drawCmdBuffers));

    // Per-frame-in-flight synchronisation objects (issue #40). The fences are
    // created signaled so the first wait on each image's fence — before that
    // image has ever been submitted — returns immediately instead of
    // deadlocking. The submitInfo is rebuilt per frame in Render() now that
    // the wait/signal semaphores vary by frame.
    VkSemaphoreCreateInfo semaphoreCreateInfo {};
	semaphoreCreateInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
    VkFenceCreateInfo fenceCreateInfo {};
    fenceCreateInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
    fenceCreateInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT;
    for (std::uint8_t i = 0; i < numFrames; i++) {
        Device::CheckVkResult(vkCreateSemaphore(Device::device, &semaphoreCreateInfo, nullptr, &renderFinishedSemaphores[i]));
        Device::CheckVkResult(vkCreateFence(Device::device, &fenceCreateInfo, nullptr, &waitFences[i]));
    }
    for (std::uint8_t i = 0; i < numAcquireSemaphores; i++) {
        Device::CheckVkResult(vkCreateSemaphore(Device::device, &semaphoreCreateInfo, nullptr, &imageAcquiredSemaphores[i]));
    }

    // Per-frame info structs: everything that never varies between frames is
    // set here once. Render() patches the barriers' image/oldLayout and the
    // sync handles that follow currentBuffer/frameCounter (submitInfo's
    // wait/signal semaphores + command buffer, presentInfo's wait semaphore).
    cmdBufInfo = {};
    cmdBufInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;

    // Submit: stage mask + the 1/1/1 counts are invariant; the wait/signal
    // semaphore handles and the command buffer are patched per frame.
    submitInfo = {};
    submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
    submitInfo.pNext = VK_NULL_HANDLE;
    submitInfo.pWaitDstStageMask = &submitPipelineStages;
    submitInfo.waitSemaphoreCount = 1;
    submitInfo.signalSemaphoreCount = 1;
    submitInfo.commandBufferCount = 1;

    const VkImageSubresourceRange range {
        .aspectMask     = VK_IMAGE_ASPECT_COLOR_BIT,
        .baseMipLevel   = 0,
        .levelCount     = VK_REMAINING_MIP_LEVELS,
        .baseArrayLayer = 0,
        .layerCount     = VK_REMAINING_ARRAY_LAYERS,
    };

    // Transition into VK_IMAGE_LAYOUT_GENERAL before recording passes. image
    // and oldLayout are patched per frame (oldLayout depends on first use).
    acquireBarrier = {
        .sType               = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
        .srcAccessMask       = 0,
        .dstAccessMask       = VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT,
        .oldLayout           = VK_IMAGE_LAYOUT_UNDEFINED,
        .newLayout           = VK_IMAGE_LAYOUT_GENERAL,
        .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
        .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
        .image               = VK_NULL_HANDLE,
        .subresourceRange    = range,
    };

    // Transition back to PRESENT_SRC_KHR after the passes. Only image varies.
    presentBarrier = {
        .sType               = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
        .srcAccessMask       = VK_ACCESS_SHADER_WRITE_BIT,
        .dstAccessMask       = 0,
        .oldLayout           = VK_IMAGE_LAYOUT_GENERAL,
        .newLayout           = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
        .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
        .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
        .image               = VK_NULL_HANDLE,
        .subresourceRange    = range,
    };

    // pImageIndices/pSwapchains point at members, so they follow value changes
    // (currentBuffer each frame, swapChain across recreation) on their own. The
    // wait semaphore is the per-image render-finished semaphore, patched per
    // frame in Render() (it follows currentBuffer).
    presentInfo = {};
    presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
    presentInfo.pNext = NULL;
    presentInfo.swapchainCount = 1;
    presentInfo.pSwapchains = &swapChain;
    presentInfo.pImageIndices = &currentBuffer;
    presentInfo.waitSemaphoreCount = 1;

    lastMousePos = {0,0};
    mouseDelta = {0,0};
    currentMousePos = {0,0};
}

void Window::Resize(std::uint32_t newWidth, std::uint32_t newHeight) {
    // Skip degenerate resizes. Win32 minimised windows give (0, 0); Wayland
    // sometimes echoes the current size in a configure.
    if (newWidth == 0 || newHeight == 0) return;
    if (newWidth == width && newHeight == height) return;
    // Win32 fires WM_SIZE synchronously inside CreateWindowEx (before the
    // constructor's CreateSwapchain). Defer the first resize to that
    // CreateSwapchain call instead of trying to recreate a non-existent
    // swapchain.
    if (swapChain == VK_NULL_HANDLE) {
        width = newWidth;
        height = newHeight;
        return;
    }

    width = newWidth;
    height = newHeight;

    // Caller (configure handler / WM_SIZE) runs between frames, but be
    // defensive: ensure no in-flight commands reference the old swapchain.
    Device::CheckVkResult(vkQueueWaitIdle(Device::queue));

    #ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
    if (wpViewport) {
        wp_viewport_set_destination(wpViewport,
            static_cast<int>(std::ceil(width  / scale)),
            static_cast<int>(std::ceil(height / scale)));
    }
    #endif

    RecreateSwapchainAndImages();
    onResize.Invoke();
}

void Window::RecreateSwapchainAndImages() {
    // CreateSwapchain leaves the new swapchain images in
    // VK_IMAGE_LAYOUT_UNDEFINED and resets imageInitialised. The images are
    // NOT transitioned here: a presentable image may only be touched after
    // vkAcquireNextImageKHR returns it, so Render() performs each image's
    // first layout transition lazily (from UNDEFINED) once it has been
    // acquired. Pre-transitioning unacquired images is a validation error.
    CreateSwapchain();
}

void Window::SetTitle(const std::string_view title) {
    #ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
    xdg_toplevel_set_title(xdgToplevel, title.data());
    #endif
}

void Window::SetCursorImage(std::uint16_t width, std::uint16_t height,
                            std::uint16_t hotspotX, std::uint16_t hotspotY,
                            const std::uint8_t* pixels) {
    #ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
    if (width == 0 || height == 0 || pixels == nullptr) {
        SetDefaultCursor();
        return;
    }

    if (cursorSurface == nullptr) {
        cursorSurface = wl_compositor_create_surface(Device::compositor);
    }

    int stride = width * 4;
    int size   = stride * height;

    // Reuse the existing mmap+buffer if the size is unchanged; otherwise
    // tear down and re-allocate.
    if (cursorWlBuffer != nullptr &&
        cursorBufferOldSize == static_cast<std::uint32_t>(size)) {
        // size unchanged — keep the buffer and mmap.
    } else {
        if (cursorMmap_) {
            munmap(cursorMmap_, cursorBufferOldSize);
            cursorMmap_ = nullptr;
        }
        if (cursorWlBuffer) {
            wl_buffer_destroy(cursorWlBuffer);
            cursorWlBuffer = nullptr;
        }

        int fd = create_shm_file(size);
        if (fd < 0) {
            throw std::runtime_error(std::format(
                "Window::SetCursorImage: shm allocation for {}B failed", size));
        }
        void* mapped = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
        if (mapped == MAP_FAILED) {
            close(fd);
            throw std::runtime_error("Window::SetCursorImage: mmap failed");
        }
        cursorMmap_ = static_cast<std::uint8_t*>(mapped);

        wl_shm_pool* pool = wl_shm_create_pool(Device::shm, fd, size);
        cursorWlBuffer = wl_shm_pool_create_buffer(
            pool, 0, width, height, stride, WL_SHM_FORMAT_ARGB8888);
        wl_shm_pool_destroy(pool);
        close(fd);

        cursorBufferOldSize = static_cast<std::uint32_t>(size);
    }

    // Convert the user's straight-alpha RGBA8 pixels into the compositor's
    // expected premultiplied BGRA8 (= ARGB8888 little-endian byte order).
    for (int i = 0; i < width * height; ++i) {
        std::uint8_t r = pixels[i * 4 + 0];
        std::uint8_t g = pixels[i * 4 + 1];
        std::uint8_t b = pixels[i * 4 + 2];
        std::uint8_t a = pixels[i * 4 + 3];
        cursorMmap_[i * 4 + 0] = static_cast<std::uint8_t>((b * a) / 255);
        cursorMmap_[i * 4 + 1] = static_cast<std::uint8_t>((g * a) / 255);
        cursorMmap_[i * 4 + 2] = static_cast<std::uint8_t>((r * a) / 255);
        cursorMmap_[i * 4 + 3] = a;
    }

    cursorHotspotX_ = hotspotX;
    cursorHotspotY_ = hotspotY;

    wl_surface_attach(cursorSurface, cursorWlBuffer, 0, 0);
    wl_surface_damage(cursorSurface, 0, 0, width, height);
    wl_surface_commit(cursorSurface);

    // If the pointer is currently inside our window, re-apply the cursor
    // so the new hotspot takes effect immediately. Otherwise the next
    // pointer-enter event will pick it up.
    if (Device::wlPointer && Device::focusedWindow == this && lastPointerSerial_) {
        wl_pointer_set_cursor(Device::wlPointer, lastPointerSerial_,
                              cursorSurface, hotspotX, hotspotY);
    }
    #endif
    #ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
    // Win32 cursor support is not implemented for the v2 Window.
    (void)width; (void)height; (void)hotspotX; (void)hotspotY; (void)pixels;
    #endif
}

void Window::SetDefaultCursor() {
    #ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
    if (cursorMmap_) {
        munmap(cursorMmap_, cursorBufferOldSize);
        cursorMmap_ = nullptr;
    }
    if (cursorWlBuffer) {
        wl_buffer_destroy(cursorWlBuffer);
        cursorWlBuffer = nullptr;
    }
    if (cursorSurface) {
        wl_surface_destroy(cursorSurface);
        cursorSurface = nullptr;
    }
    cursorBufferOldSize = 0;
    cursorHotspotX_ = 0;
    cursorHotspotY_ = 0;
    // Tell the compositor to drop our cursor surface — passing nullptr
    // makes it fall back to the system default.
    if (Device::wlPointer && Device::focusedWindow == this && lastPointerSerial_) {
        wl_pointer_set_cursor(Device::wlPointer, lastPointerSerial_, nullptr, 0, 0);
    }
    #endif
}

void Window::StartSync() {
    #ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
	while (open && wl_display_dispatch(Device::display) != -1) {
        Gamepad::Tick();
        onBeforeUpdate.Invoke();
	}
    #endif
    #ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
    while(open) {
        MSG msg;
        while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
        Gamepad::Tick();
        onBeforeUpdate.Invoke();
        if(updating) {
            Update();
        }
    }
    #endif
}

void Window::StartUpdate() {
    lastFrameBegin = std::chrono::high_resolution_clock::now();
    updating = true;
    #ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
    cb = wl_surface_frame(surface);
	wl_callback_add_listener(cb, &wl_callback_listener, this);
    #endif
}

void Window::StopUpdate() {
    updating = false;
}

std::chrono::time_point<std::chrono::high_resolution_clock> startTime;

void Window::Update() {
    startTime = std::chrono::high_resolution_clock::now();
	#ifdef CRAFTER_TIMING
	vblank = duration_cast<std::chrono::milliseconds>(startTime - frameEnd);
	#endif

    mouseDelta = {currentMousePos.x-lastMousePos.x, currentMousePos.y-lastMousePos.y};
    currentFrameTime = {startTime, startTime-lastFrameBegin};
    #ifdef CRAFTER_TIMING
    auto renderStart = std::chrono::high_resolution_clock::now();
    renderTimings.clear();
    #endif
    Render();
    #ifdef CRAFTER_TIMING
    auto renderEnd = std::chrono::high_resolution_clock::now();
    totalRender = renderEnd - renderStart;
    #endif

    lastMousePos = currentMousePos;

	#ifdef CRAFTER_TIMING
	frameEnd = std::chrono::high_resolution_clock::now();
	
	// Ring buffer: overwrite the oldest entry once full. Order doesn't matter
	// to LogTiming, so we never need to shift elements.
	frameTimes[frameTimesHead] = totalUpdate+totalRender;
	frameTimesHead = (frameTimesHead + 1) % frameTimeCapacity;
	if (frameTimesCount < frameTimeCapacity) {
		++frameTimesCount;
	}
	#endif
	lastFrameBegin = startTime;
}

void Window::Render() {
    // Acquire the next image from the swap chain. If the surface has
    // changed size out from under us (compositor/Win32 resize delivered
    // between Render calls), recreate and retry once.
    // Pick this frame's acquire semaphore from a free-running CPU counter: the
    // image index (currentBuffer) isn't known until acquire returns, so the
    // acquire's signal semaphore can't be keyed by the image.
    const std::uint32_t acquireSlot = static_cast<std::uint32_t>(frameCounter % numAcquireSemaphores);
    VkSemaphore imageAcquired = imageAcquiredSemaphores[acquireSlot];
    {
        VkResult acquire = vkAcquireNextImageKHR(Device::device, swapChain, UINT64_MAX,
            imageAcquired, (VkFence)nullptr, &currentBuffer);
        if (acquire == VK_ERROR_OUT_OF_DATE_KHR) {
            Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
            RecreateSwapchainAndImages();
            onResize.Invoke();
            acquire = vkAcquireNextImageKHR(Device::device, swapChain, UINT64_MAX,
                imageAcquired, (VkFence)nullptr, &currentBuffer);
        }
        if (acquire != VK_SUBOPTIMAL_KHR) {
            Device::CheckVkResult(acquire);
        }
    }

    // drawCmdBuffers[currentBuffer] and this image's descriptor-heap slot are
    // about to be re-recorded. Wait on the fence guarding this image's
    // previous submission (keyed by image index, since acquire returns an
    // arbitrary index), then reset it for this submit. This — not a full-queue
    // wait-idle — is what bounds frames-in-flight and gives the CPU/GPU overlap.
    Device::CheckVkResult(vkWaitForFences(Device::device, 1, &waitFences[currentBuffer], VK_TRUE, UINT64_MAX));
    Device::CheckVkResult(vkResetFences(Device::device, 1, &waitFences[currentBuffer]));

    Device::CheckVkResult(vkBeginCommandBuffer(drawCmdBuffers[currentBuffer], &cmdBufInfo));

    // On an image's first use after (re)creating the swapchain it is still in
    // VK_IMAGE_LAYOUT_UNDEFINED; every subsequent frame leaves it in
    // PRESENT_SRC_KHR. Transitioning from the wrong oldLayout is a validation
    // error, so pick based on whether this image has been rendered before.
    // The image was just acquired above, so touching it here is legal.
    const bool firstUse = !imageInitialised[currentBuffer];
    imageInitialised[currentBuffer] = true;

    acquireBarrier.oldLayout = firstUse ? VK_IMAGE_LAYOUT_UNDEFINED
                                        : VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
    acquireBarrier.image = images[currentBuffer];

    vkCmdPipelineBarrier(drawCmdBuffers[currentBuffer], VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &acquireBarrier);

    // Synthesise key-repeat events before listeners run, so the focused
    // widget's OnTextInput / OnKeyDown sees them in the same frame.
    Device::TickKeyRepeats();

    // Bind the descriptor heaps BEFORE the user's update event fires.
    // Any compute work the update lambda records (e.g. physics dispatches)
    // needs the heaps bound at execution time; recording order in the cmd
    // buffer dictates GPU execution order, so the bind must come first.
    // Pass-side dispatches still run with the same heaps bound — moving
    // the bind earlier doesn't change anything for them.
    if (descriptorHeap) {
        // Rebuild the cached per-frame bind structs only when the heap changes.
        // The address/size of each frame's heap and the reservedRangeOffset
        // bitmask are stable for a given heap, so this runs once per assignment
        // rather than every frame.
        if (descriptorHeap != cachedDescriptorHeap) {
            for (std::uint8_t i = 0; i < numFrames; ++i) {
                resourceHeapInfos[i] = {
                    .sType = VK_STRUCTURE_TYPE_BIND_HEAP_INFO_EXT,
                    .heapRange = {
                        .address = descriptorHeap->resourceHeap[i].address,
                        .size = static_cast<std::uint32_t>(descriptorHeap->resourceHeap[i].size)
                    },
                    .reservedRangeOffset = (descriptorHeap->resourceHeap[i].size  - Device::descriptorHeapProperties.minResourceHeapReservedRange) & ~(Device::descriptorHeapProperties.imageDescriptorAlignment - 1),
                    .reservedRangeSize = Device::descriptorHeapProperties.minResourceHeapReservedRange
                };
                samplerHeapInfos[i] = {
                    .sType = VK_STRUCTURE_TYPE_BIND_HEAP_INFO_EXT,
                    .heapRange = {
                        .address = descriptorHeap->samplerHeap[i].address,
                        .size = static_cast<std::uint32_t>(descriptorHeap->samplerHeap[i].size)
                    },
                    .reservedRangeOffset = descriptorHeap->samplerHeap[i].size - Device::descriptorHeapProperties.minSamplerHeapReservedRange,
                    .reservedRangeSize = Device::descriptorHeapProperties.minSamplerHeapReservedRange
                };
            }
            cachedDescriptorHeap = descriptorHeap;
        }
        Device::vkCmdBindResourceHeapEXT(drawCmdBuffers[currentBuffer], &resourceHeapInfos[currentBuffer]);
        Device::vkCmdBindSamplerHeapEXT(drawCmdBuffers[currentBuffer], &samplerHeapInfos[currentBuffer]);
    }

    onUpdate.Invoke({startTime, startTime-lastFrameBegin});
    #ifdef CRAFTER_TIMING
    totalUpdate = std::chrono::nanoseconds(0);
    updateTimings.clear();
    for (const std::pair<const EventListener<FrameTime>*, std::chrono::nanoseconds>& entry : onUpdate.listenerTimes) {
        updateTimings.push_back(entry);
        totalUpdate += entry.second;
    }
    #endif

    // Note: vkCmdClearColorImage is unavailable here — the swapchain is
    // created with VK_IMAGE_USAGE_STORAGE_BIT only (no TRANSFER_DST_BIT).
    // Passes that need a background should write one explicitly (UIScene
    // exposes a `background()` setter for this purpose).
    (void)clearColor;

    for (std::size_t i = 0; i < passes.size(); ++i) {
        passes[i]->Record(drawCmdBuffers[currentBuffer], currentBuffer, *this);

        if (i + 1 < passes.size()) {
            VkMemoryBarrier mb {
                .sType         = VK_STRUCTURE_TYPE_MEMORY_BARRIER,
                .srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT,
                .dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT,
            };
            vkCmdPipelineBarrier(drawCmdBuffers[currentBuffer], VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1, &mb, 0, nullptr, 0, nullptr);
        }
    }

    presentBarrier.image = images[currentBuffer];

    vkCmdPipelineBarrier(drawCmdBuffers[currentBuffer], VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr, 1, &presentBarrier);

    Device::CheckVkResult(vkEndCommandBuffer(drawCmdBuffers[currentBuffer]));

    // Patch the per-frame fields of the cached submitInfo/presentInfo (the
    // invariant fields were set once in the constructor): submit waits on this
    // frame's acquire semaphore and signals this image's render-finished
    // semaphore, present waits on that same render-finished semaphore. The
    // fence (keyed by image index) lets a future Render() reuse this image's
    // command buffer + heap slot safely. The semaphore handles live in member
    // arrays, so taking their addresses here is stable for the call.
    submitInfo.pWaitSemaphores   = &imageAcquiredSemaphores[acquireSlot];
    submitInfo.pSignalSemaphores = &renderFinishedSemaphores[currentBuffer];
    submitInfo.pCommandBuffers   = &drawCmdBuffers[currentBuffer];
    Device::CheckVkResult(vkQueueSubmit(Device::queue, 1, &submitInfo, waitFences[currentBuffer]));

    presentInfo.pWaitSemaphores = &renderFinishedSemaphores[currentBuffer];

    VkResult result = vkQueuePresentKHR(Device::queue, &presentInfo);
    if (result == VK_SUBOPTIMAL_KHR || result == VK_ERROR_OUT_OF_DATE_KHR) {
        // Surface size changed mid-present. Drain the queue (the only place we
        // still wait-idle, alongside Resize() and the acquire OUT_OF_DATE
        // path), rebuild the swapchain, and let dependents (descriptors
        // holding old image handles) re-bind via onResize before the next
        // frame. The wait-idle here also re-settles every per-frame fence to a
        // signaled state, so the next Render()'s fence wait passes through.
        Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
        RecreateSwapchainAndImages();
        onResize.Invoke();
    } else {
        Device::CheckVkResult(result);
    }
    // No vkQueueWaitIdle in the steady-state path: per-frame fences now gate
    // command-buffer reuse, so the CPU is free to acquire/record frame N+1
    // while the GPU is still executing frame N.
    frameCounter++;
}

#ifdef CRAFTER_TIMING
void Window::LogTiming() {
    std::cout << std::format("Update: {}", duration_cast<std::chrono::milliseconds>(totalUpdate)) << std::endl;
    for (const std::pair<const EventListener<FrameTime>*, std::chrono::nanoseconds>& entry : updateTimings) {
        std::cout << std::format("\t{} {}", reinterpret_cast<const void*>(entry.first), duration_cast<std::chrono::microseconds>(entry.second)) << std::endl;
    }
    std::cout << std::format("Render: {}", duration_cast<std::chrono::milliseconds>(totalRender)) << std::endl;
    for (const std::tuple<const RenderingElement*, std::uint32_t, std::uint32_t, std::chrono::nanoseconds>& entry : renderer.renderTimings) {
        std::cout << std::format("\t{} {}x{} {}", reinterpret_cast<const void*>(std::get<0>(entry)), std::get<1>(entry), std::get<2>(entry), duration_cast<std::chrono::microseconds>(std::get<3>(entry))) << std::endl;
    }
    std::cout << std::format("Total: {}", duration_cast<std::chrono::milliseconds>(totalUpdate+totalRender)) << std::endl;
    std::cout << std::format("Vblank: {}", duration_cast<std::chrono::milliseconds>(vblank)) << std::endl;
    
    // Add 100-frame average and min-max timing info. Only the first
    // frameTimesCount entries of the ring buffer hold valid samples.
    if (frameTimesCount != 0) {
        // Calculate average
        std::chrono::nanoseconds sum(0);
        for (std::size_t i = 0; i < frameTimesCount; ++i) {
            sum += frameTimes[i];
        }
        auto average = sum / frameTimesCount;

        // Find min and max
        auto min = frameTimes[0];
        auto max = frameTimes[0];
        for (std::size_t i = 0; i < frameTimesCount; ++i) {
            if (frameTimes[i] < min) min = frameTimes[i];
            if (frameTimes[i] > max) max = frameTimes[i];
        }
        
        std::cout << std::format("Last 100 Frame Times - Avg: {}, Min: {}, Max: {}", 
            duration_cast<std::chrono::milliseconds>(average),
            duration_cast<std::chrono::milliseconds>(min),
            duration_cast<std::chrono::milliseconds>(max)) << std::endl;
    }
}
#endif

void Window::CreateSwapchain()
{
	// Store the current swap chain handle so we can use it later on to ease up recreation
	VkSwapchainKHR oldSwapchain = swapChain;

	// Get physical device surface properties and formats
	VkSurfaceCapabilitiesKHR surfCaps;
	Device::CheckVkResult(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(Device::physDevice, vulkanSurface, &surfCaps));

	VkExtent2D swapchainExtent = {};
	// If width (and height) equals the special value 0xFFFFFFFF, the size of the surface will be set by the swapchain
	if (surfCaps.currentExtent.width == (uint32_t)-1)
	{
		// If the surface size is undefined, the size is set to the size of the images requested
		swapchainExtent.width = width;
		swapchainExtent.height = height;
	}
	else
	{
		// If the surface size is defined, the swap chain size must match
		swapchainExtent = surfCaps.currentExtent;
		width = surfCaps.currentExtent.width;
		height = surfCaps.currentExtent.height;
	}


	// Find the transformation of the surface
	VkSurfaceTransformFlagsKHR preTransform;
	if (surfCaps.supportedTransforms & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR)
	{
		// We prefer a non-rotated transform
		preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
	}
	else
	{
		preTransform = surfCaps.currentTransform;
	}

	VkSwapchainCreateInfoKHR swapchainCI = {};
	swapchainCI.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
	swapchainCI.surface = vulkanSurface;
	swapchainCI.minImageCount = numFrames;
	swapchainCI.imageFormat = colorFormat;
	swapchainCI.imageColorSpace = colorSpace;
	swapchainCI.imageExtent = { swapchainExtent.width, swapchainExtent.height };
	swapchainCI.imageUsage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
	swapchainCI.preTransform = (VkSurfaceTransformFlagBitsKHR)preTransform;
	swapchainCI.imageArrayLayers = 1;
	swapchainCI.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
	swapchainCI.queueFamilyIndexCount = 0;
	// VK_PRESENT_MODE_FIFO_KHR ("v-sync") is guaranteed available per spec, so
	// it is used unconditionally — no present-mode enumeration is needed.
	swapchainCI.presentMode = VK_PRESENT_MODE_FIFO_KHR;
	// Setting oldSwapChain to the saved handle of the previous swapchain aids in resource reuse and makes sure that we can still present already acquired images
	swapchainCI.oldSwapchain = oldSwapchain;
	// Setting clipped to VK_TRUE allows the implementation to discard rendering outside of the surface area
	swapchainCI.clipped = VK_TRUE;
	swapchainCI.compositeAlpha = compositeAlpha;

	Device::CheckVkResult(vkCreateSwapchainKHR(Device::device, &swapchainCI, nullptr, &swapChain));

	// If an existing swap chain is re-created, destroy the old swap chain and the ressources owned by the application (image views, images are owned by the swap chain)
	if (oldSwapchain != VK_NULL_HANDLE) { 
		vkDestroySwapchainKHR(Device::device, oldSwapchain, nullptr);
	}
	uint32_t imageCount{ 0 };
	Device::CheckVkResult(vkGetSwapchainImagesKHR(Device::device, swapChain, &imageCount, nullptr));

	// Get the swap chain images
	Device::CheckVkResult(vkGetSwapchainImagesKHR(Device::device, swapChain, &imageCount, images));

	// Brand-new swapchain images are in VK_IMAGE_LAYOUT_UNDEFINED; none have
	// been rendered/presented yet. Render() consults this to pick the correct
	// oldLayout for each image's first post-acquire transition.
	imageInitialised.fill(false);

	for (std::uint8_t i = 0; i < numFrames; i++) {
        imageViews[i] = {
            .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
            .flags = 0,
            .image = images[i],
            .viewType = VK_IMAGE_VIEW_TYPE_2D,
            .format = colorFormat,
            .components = {
                VK_COMPONENT_SWIZZLE_R,
                VK_COMPONENT_SWIZZLE_G,
                VK_COMPONENT_SWIZZLE_B,
                VK_COMPONENT_SWIZZLE_A
            },
            .subresourceRange = {
                .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
                .baseMipLevel = 0,
                .levelCount = 1,
                .baseArrayLayer = 0,
                .layerCount = 1,
            },
        };
	}
}

VkCommandBuffer Window::StartInit() {
    VkCommandBufferBeginInfo cmdBufInfo {};
	cmdBufInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
	Device::CheckVkResult(vkBeginCommandBuffer(drawCmdBuffers[currentBuffer], &cmdBufInfo));

    // The swapchain images are deliberately NOT transitioned here. They are
    // presentable images and may only be touched after vkAcquireNextImageKHR
    // returns them; transitioning an unacquired presentable image is a
    // validation error. Render() instead transitions each image from
    // VK_IMAGE_LAYOUT_UNDEFINED on its first acquired use (see
    // imageInitialised). This command buffer is for the caller's one-time
    // setup work (uploads, acceleration-structure builds, etc.).
	return drawCmdBuffers[currentBuffer];
}

void Window::FinishInit() {
	VkSubmitInfo submitInfo{};
    submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
    submitInfo.commandBufferCount = 1;
    submitInfo.pCommandBuffers = &drawCmdBuffers[currentBuffer];
	Device::CheckVkResult(vkEndCommandBuffer(drawCmdBuffers[currentBuffer]));
	Device::CheckVkResult(vkQueueSubmit(Device::queue, 1, &submitInfo, VK_NULL_HANDLE));
    Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
}

VkCommandBuffer Window::GetCmd() {
    VkCommandBufferBeginInfo cmdBufInfo {};
	cmdBufInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
	Device::CheckVkResult(vkBeginCommandBuffer(drawCmdBuffers[currentBuffer], &cmdBufInfo));

    VkImageSubresourceRange range{};
    range.aspectMask     = VK_IMAGE_ASPECT_COLOR_BIT;
    range.baseMipLevel   = 0;
    range.levelCount     = VK_REMAINING_MIP_LEVELS;
    range.baseArrayLayer = 0;
    range.layerCount     = VK_REMAINING_ARRAY_LAYERS;
    
	return drawCmdBuffers[currentBuffer];
}

void Window::EndCmd(VkCommandBuffer cmd) {
	VkSubmitInfo submitInfo{};
    submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
    submitInfo.commandBufferCount = 1;
    submitInfo.pCommandBuffers = &drawCmdBuffers[currentBuffer];
	Device::CheckVkResult(vkEndCommandBuffer(drawCmdBuffers[currentBuffer]));
	Device::CheckVkResult(vkQueueSubmit(Device::queue, 1, &submitInfo, VK_NULL_HANDLE));
    Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
}

#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
void Window::wl_surface_frame_done(void* data, struct wl_callback *cb, uint32_t time) {
	wl_callback_destroy(cb);
	Window* window = reinterpret_cast<Window*>(data);

    if(window->updating) {
        cb = wl_surface_frame(window->surface);
	    wl_callback_add_listener(cb, &Window::wl_callback_listener, window);
        window->Update();
    } else {
        cb = nullptr;
    }
}


void Window::xdg_toplevel_configure(void* data, xdg_toplevel*, std::int32_t width, std::int32_t height, wl_array*){
	// xdg-shell batches state — width/height are pending until the matching
	// xdg_surface.configure arrives. Width/height are in surface-local
	// (logical DP) units; (0, 0) means "compositor has no preference".
	Window* window = reinterpret_cast<Window*>(data);
	window->pendingLogicalWidth  = width;
	window->pendingLogicalHeight = height;
}

void Window::xdg_toplevel_handle_close(void* data, xdg_toplevel*) {
	Window* window = reinterpret_cast<Window*>(data);
	window->onClose.Invoke();
	window->open = false;
}

void Window::xdg_surface_handle_configure(void* data, xdg_surface* xdg_surface, std::uint32_t serial) {
	Window* window = reinterpret_cast<Window*>(data);
	// The compositor configures our surface, acknowledge the configure event
	xdg_surface_ack_configure(xdg_surface, serial);

	if (window->configured) {
		// Subsequent configure: if the toplevel asked for a new size
		// (non-zero, different from current), drive the resize end-to-end.
		// (0, 0) means "compositor has no preference, keep current size".
		// The swapchain may not exist yet on the very first frame between
		// the constructor's wait loop and CreateSwapchain — the Resize
		// guard against equal sizes already covers that path.
		if (window->pendingLogicalWidth > 0 && window->pendingLogicalHeight > 0 &&
		    window->swapChain != VK_NULL_HANDLE) {
			std::uint32_t newWidth  = static_cast<std::uint32_t>(
				std::ceil(window->pendingLogicalWidth  * window->scale));
			std::uint32_t newHeight = static_cast<std::uint32_t>(
				std::ceil(window->pendingLogicalHeight * window->scale));
			window->Resize(newWidth, newHeight);
		}
		wl_surface_commit(window->surface);
	}

	window->configured = true;
}

void Window::xdg_surface_handle_preferred_scale(void* data, wp_fractional_scale_v1*, std::uint32_t scale) {
    Window* window = reinterpret_cast<Window*>(data);
    window->scale = scale / 120.0f;
}

#endif

void Window::SaveFrame(const std::filesystem::path& path) {
    // Staging buffer big enough for one RGBA frame.
    VkDeviceSize bufSize = static_cast<VkDeviceSize>(width) * height * 4;

    VkBufferCreateInfo bci{
        .sType       = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
        .size        = bufSize,
        .usage       = VK_BUFFER_USAGE_TRANSFER_DST_BIT,
        .sharingMode = VK_SHARING_MODE_EXCLUSIVE,
    };
    VkBuffer stagingBuf = VK_NULL_HANDLE;
    Device::CheckVkResult(vkCreateBuffer(Device::device, &bci, nullptr, &stagingBuf));

    VkMemoryRequirements memReqs;
    vkGetBufferMemoryRequirements(Device::device, stagingBuf, &memReqs);
    VkMemoryAllocateInfo mai{
        .sType           = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
        .allocationSize  = memReqs.size,
        .memoryTypeIndex = Device::GetMemoryType(memReqs.memoryTypeBits,
            VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT),
    };
    VkDeviceMemory stagingMem = VK_NULL_HANDLE;
    Device::CheckVkResult(vkAllocateMemory(Device::device, &mai, nullptr, &stagingMem));
    Device::CheckVkResult(vkBindBufferMemory(Device::device, stagingBuf, stagingMem, 0));

    // One-shot command buffer so we don't trash the per-frame ones.
    VkCommandBufferAllocateInfo cba{
        .sType       = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,
        .commandPool = Device::commandPool,
        .level       = VK_COMMAND_BUFFER_LEVEL_PRIMARY,
        .commandBufferCount = 1,
    };
    VkCommandBuffer cmd = VK_NULL_HANDLE;
    Device::CheckVkResult(vkAllocateCommandBuffers(Device::device, &cba, &cmd));

    VkCommandBufferBeginInfo cbi{
        .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
        .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT,
    };
    Device::CheckVkResult(vkBeginCommandBuffer(cmd, &cbi));

    VkImageSubresourceRange range{
        .aspectMask     = VK_IMAGE_ASPECT_COLOR_BIT,
        .baseMipLevel   = 0,
        .levelCount     = 1,
        .baseArrayLayer = 0,
        .layerCount     = 1,
    };

    // Render() leaves the image in PRESENT_SRC_KHR.
    VkImageMemoryBarrier toSrc{
        .sType               = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
        .srcAccessMask       = 0,
        .dstAccessMask       = VK_ACCESS_TRANSFER_READ_BIT,
        .oldLayout           = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
        .newLayout           = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
        .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
        .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
        .image               = images[currentBuffer],
        .subresourceRange    = range,
    };
    vkCmdPipelineBarrier(cmd,
        VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
        0, 0, nullptr, 0, nullptr, 1, &toSrc);

    VkBufferImageCopy region{
        .bufferOffset      = 0,
        .bufferRowLength   = 0,
        .bufferImageHeight = 0,
        .imageSubresource  = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 },
        .imageOffset       = { 0, 0, 0 },
        .imageExtent       = { width, height, 1 },
    };
    vkCmdCopyImageToBuffer(cmd, images[currentBuffer],
        VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, stagingBuf, 1, &region);

    VkImageMemoryBarrier back{
        .sType               = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
        .srcAccessMask       = VK_ACCESS_TRANSFER_READ_BIT,
        .dstAccessMask       = 0,
        .oldLayout           = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
        .newLayout           = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
        .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
        .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
        .image               = images[currentBuffer],
        .subresourceRange    = range,
    };
    vkCmdPipelineBarrier(cmd,
        VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
        0, 0, nullptr, 0, nullptr, 1, &back);

    Device::CheckVkResult(vkEndCommandBuffer(cmd));

    VkSubmitInfo si{
        .sType              = VK_STRUCTURE_TYPE_SUBMIT_INFO,
        .commandBufferCount = 1,
        .pCommandBuffers    = &cmd,
    };
    Device::CheckVkResult(vkQueueSubmit(Device::queue, 1, &si, VK_NULL_HANDLE));
    Device::CheckVkResult(vkQueueWaitIdle(Device::queue));

    // Read back, swizzle BGRA → RGBA if needed, write PNG.
    void* mapped = nullptr;
    Device::CheckVkResult(vkMapMemory(Device::device, stagingMem, 0, VK_WHOLE_SIZE, 0, &mapped));
    const std::uint8_t* src = static_cast<const std::uint8_t*>(mapped);

    std::vector<std::uint8_t> rgba(static_cast<std::size_t>(width) * height * 4);
    bool bgr = (colorFormat == VK_FORMAT_B8G8R8A8_UNORM);
    for (std::uint32_t i = 0; i < width * height; ++i) {
        if (bgr) {
            rgba[i*4+0] = src[i*4+2];
            rgba[i*4+1] = src[i*4+1];
            rgba[i*4+2] = src[i*4+0];
            rgba[i*4+3] = src[i*4+3];
        } else {
            rgba[i*4+0] = src[i*4+0];
            rgba[i*4+1] = src[i*4+1];
            rgba[i*4+2] = src[i*4+2];
            rgba[i*4+3] = src[i*4+3];
        }
    }
    vkUnmapMemory(Device::device, stagingMem);

    stbi_write_png(path.string().c_str(), static_cast<int>(width), static_cast<int>(height),
                   4, rgba.data(), static_cast<int>(width) * 4);

    vkFreeCommandBuffers(Device::device, Device::commandPool, 1, &cmd);
    vkDestroyBuffer(Device::device, stagingBuf, nullptr);
    vkFreeMemory(Device::device, stagingMem, nullptr);
}
#endif  // !CRAFTER_GRAPHICS_WINDOW_DOM

// ──────────────────────────────────────────────────────────────────────
// DOM backend
// ──────────────────────────────────────────────────────────────────────
//
// In DOM mode the "window" IS the browser page; there is no separate
// surface to create, no swapchain to manage, no GPU pipeline to wait on.
// All Window does here is:
//   - mirror requested title onto document.title
//   - register itself with the JS bridge so DOM-level events route into
//     its event objects
//   - hand the frame loop to requestAnimationFrame; `Update` runs on
//     each rAF tick when `updating` is true
//
// The C exports (__crafterDom_*) below are how the JS bridge reaches
// back into the live Window instance. We keep a process-global pointer
// for V1 — only one Window per page — and lookups are O(1).

#ifdef CRAFTER_GRAPHICS_WINDOW_DOM

// The JS runtime initializes itself before main() runs, so there's
// nothing to do here. Defined as a no-op so user code calling
// `Device::Initialize()` links the same way it does on native.
void Device::Initialize() {}

namespace {
    Window* g_domWindow = nullptr;
}

namespace Crafter::DomEnv {
    __attribute__((import_module("env"), import_name("domAttachWindow")))
    void domAttachWindow(std::int32_t handle);
    __attribute__((import_module("env"), import_name("domSetTitle")))
    void domSetTitle(const char* title, std::int32_t titleLen);
    __attribute__((import_module("env"), import_name("domGetInnerWidth")))
    std::int32_t domGetInnerWidth();
    __attribute__((import_module("env"), import_name("domGetInnerHeight")))
    std::int32_t domGetInnerHeight();
    __attribute__((import_module("env"), import_name("domStartFrameLoop")))
    void domStartFrameLoop();
    __attribute__((import_module("env"), import_name("domStopFrameLoop")))
    void domStopFrameLoop();
}

// Compile-time string hash matching what dom-env.js sends through. The
// JS bridge marshals `KeyboardEvent.code` as a UTF-8 string; we hash it
// to a 32-bit KeyCode here so the same value compares equal against the
// table in :Keys (DOM branch). FNV-1a, deterministic, no allocation.
namespace {
    constexpr KeyCode HashKeyCode(const char* p, std::size_t n) {
        std::uint32_t h = 2166136261u;
        for (std::size_t i = 0; i < n; ++i) {
            h ^= static_cast<std::uint8_t>(p[i]);
            h *= 16777619u;
        }
        return h;
    }
}

Window::Window(std::uint32_t w, std::uint32_t h, const std::string_view title)
    : Window(w, h) {
    SetTitle(title);
}

Window::Window(std::uint32_t w, std::uint32_t h) : width(w), height(h) {
    if (g_domWindow != nullptr) {
        std::println("Crafter::Window: only one DOM Window per page; "
                     "overwriting the previous instance.");
    }
    g_domWindow = this;

    // Browser owns the real surface size. The width/height passed in are
    // advisory only — useful as a native-side hint, ignored on DOM. We
    // always sync to innerWidth/innerHeight so:
    //   - window.width/.height match the canvas's CSS pixel size,
    //   - MouseEvent.clientX/.clientY (CSS pixels) compare correctly
    //     against any layout done with window.width/.height,
    //   - the dispatch group count from window.width/8 covers the
    //     canvas exactly.
    (void)w; (void)h;
    width  = static_cast<std::uint32_t>(Crafter::DomEnv::domGetInnerWidth());
    height = static_cast<std::uint32_t>(Crafter::DomEnv::domGetInnerHeight());

    // The handle passed to attach is just a non-zero token the JS side
    // includes back in every dispatcher call. We don't use it on the
    // C++ side (g_domWindow is the lookup) but it has to be non-zero so
    // the JS bridge treats the window as "attached".
    Crafter::DomEnv::domAttachWindow(1);

    lastMousePos    = {0, 0};
    currentMousePos = {0, 0};
    mouseDelta      = {0, 0};
}

Window::~Window() {
    // Clear the global pointer iff it still references us — defensive
    // against a stack-allocated Window in main() that goes out of scope
    // while rAF / DOM event callbacks are still queued. After this, the
    // JS-side dispatchers (__crafterDom_*) early-return harmlessly. A
    // shrill warning to the console flags the (almost certainly
    // unintended) lifetime mistake so the user notices before everything
    // mysteriously stops working.
    if (g_domWindow == this) {
        g_domWindow = nullptr;
        std::println("Crafter::Window: destroyed while DOM mode is active. "
                     "Browser events will no-op until a new Window is constructed. "
                     "Did you forget to put the Window in `static` / `new`d storage?");
    }
}

void Window::SetTitle(const std::string_view title) {
    Crafter::DomEnv::domSetTitle(title.data(), static_cast<std::int32_t>(title.size()));
}

void Window::Resize(std::uint32_t newWidth, std::uint32_t newHeight) {
    if (newWidth == 0 || newHeight == 0) return;
    if (newWidth == width && newHeight == height) return;
    width  = newWidth;
    height = newHeight;
    onResize.Invoke();
}

void Window::SetCursorImage(std::uint16_t /*cw*/, std::uint16_t /*ch*/,
                            std::uint16_t /*hx*/, std::uint16_t /*hy*/,
                            const std::uint8_t* /*pixels*/) {
    // V1: not wired. The natural impl is to base64-encode an inline PNG
    // and assign it via document.body.style.cursor = `url(data:...) hx hy, auto`.
    // Left for a follow-up so the first DOM build can ship without an
    // inline PNG encoder.
}

void Window::SetDefaultCursor() {
    // Mirror SetCursorImage stub. Future impl: clear body.style.cursor.
}

void Window::StartSync() {
    // Hand the loop to rAF, then exit the wasm via _Exit so wasi-libc
    // skips __wasm_call_dtors. If we let main return normally, _start
    // calls __wasm_call_dtors → static destructors fire (including
    // Window's own), then __wasi_proc_exit → wasm trap. Subsequent rAF
    // calls into the wasm would then trap too, killing rendering.
    // _Exit jumps straight to __wasi_proc_exit, which our runtime.js
    // catches via a thrown sentinel so the instance stays alive while
    // every static-allocated object (Window, UIRenderer, GPU buffers,
    // event listeners) remains untouched. Callers' code after
    // StartSync() never runs — match that contract on native too.
    Crafter::DomEnv::domStartFrameLoop();
    std::_Exit(0);
}

void Window::StartUpdate() {
    lastFrameBegin = std::chrono::high_resolution_clock::now();
    updating = true;
}

void Window::StopUpdate() {
    updating = false;
    Crafter::DomEnv::domStopFrameLoop();
}

void Window::Update() {
    auto now = std::chrono::high_resolution_clock::now();
    mouseDelta = {currentMousePos.x - lastMousePos.x,
                  currentMousePos.y - lastMousePos.y};
    currentFrameTime = {now, now - lastFrameBegin};
    onUpdate.Invoke(currentFrameTime);
    lastMousePos = currentMousePos;
    lastFrameBegin = now;
}

void Window::Render() {
    if (!open) return;
    Crafter::WebGPU::wgpuFrameBegin();
    for (RenderPass* p : passes) {
        if (p) p->Record(/*cmd*/ 0u, currentBuffer, *this);
    }
    Crafter::WebGPU::wgpuFrameEnd();
}

WebGPUCommandEncoderRef Window::StartInit() {
    // DOM init: no command buffer needed — texture / buffer creation goes
    // through synchronous wgpu* imports. Return 0 as a placeholder; the
    // value is opaque to user code (auto-typed in HelloUI).
    Crafter::WebGPU::wgpuInit();
    return 0;
}

void Window::FinishInit() {
    // Nothing to submit in DOM mode; all init writes are queued at call
    // time via queue.writeBuffer / writeTexture.
}

// ─── C exports the JS bridge calls back into ──────────────────────────

extern "C" {
    __attribute__((export_name("__crafterDom_frame")))
    void __crafterDom_frame(std::int32_t /*handle*/) {
        if (!g_domWindow) return;
        Gamepad::Tick();
        g_domWindow->onBeforeUpdate.Invoke();
        if (g_domWindow->updating) {
            g_domWindow->Update();
            g_domWindow->Render();
        }
    }

    __attribute__((export_name("__crafterDom_mouseMove")))
    void __crafterDom_mouseMove(std::int32_t /*handle*/, double x, double y) {
        if (!g_domWindow) return;
        g_domWindow->currentMousePos = {static_cast<float>(x), static_cast<float>(y)};
        g_domWindow->onMouseMove.Invoke();
    }

    __attribute__((export_name("__crafterDom_mouseDown")))
    void __crafterDom_mouseDown(std::int32_t /*handle*/, std::int32_t button) {
        if (!g_domWindow) return;
        // MouseEvent.button: 0=left, 1=middle, 2=right
        if (button == 0) {
            g_domWindow->mouseLeftHeld = true;
            g_domWindow->onMouseLeftClick.Invoke();
        } else if (button == 2) {
            g_domWindow->mouseRightHeld = true;
            g_domWindow->onMouseRightClick.Invoke();
        }
    }

    __attribute__((export_name("__crafterDom_mouseUp")))
    void __crafterDom_mouseUp(std::int32_t /*handle*/, std::int32_t button) {
        if (!g_domWindow) return;
        if (button == 0) {
            g_domWindow->mouseLeftHeld = false;
            g_domWindow->onMouseLeftRelease.Invoke();
        } else if (button == 2) {
            g_domWindow->mouseRightHeld = false;
            g_domWindow->onMouseRightRelease.Invoke();
        }
    }

    __attribute__((export_name("__crafterDom_wheel")))
    void __crafterDom_wheel(std::int32_t /*handle*/, double detents) {
        if (!g_domWindow) return;
        // dom-env.js has already normalized WheelEvent deltas to whole
        // detents (+1 = wheel down) — see __windowListeners.wheel.
        // Window::onMouseScroll is uint32 — preserve sign via two's complement.
        g_domWindow->onMouseScroll.Invoke(static_cast<std::uint32_t>(static_cast<std::int32_t>(detents)));
    }

    __attribute__((export_name("__crafterDom_keyDown")))
    void __crafterDom_keyDown(std::int32_t /*handle*/,
                              const char* codePtr, std::int32_t codeLen,
                              const char* keyPtr,  std::int32_t keyLen,
                              bool repeat) {
        if (!g_domWindow) return;
        KeyCode code = HashKeyCode(codePtr, static_cast<std::size_t>(codeLen));
        if (repeat) {
            g_domWindow->onRawKeyHold.Invoke(code);
        } else {
            g_domWindow->heldKeys.insert(code);
            g_domWindow->onRawKeyDown.Invoke(code);
        }
        // KeyboardEvent.key is the printable form for character keys, BUT
        // it's also the named identifier for non-character keys —
        // "Backspace", "Shift", "ArrowLeft", "Enter", … Those are
        // multi-byte ASCII strings, and a naive `keyLen > 1` check
        // forwards them straight into input fields as literal text.
        //
        // The reliable discriminator: multi-byte UTF-8 has the high bit
        // set on its first byte (lead byte ≥ 0xC0; continuation bytes
        // ≥ 0x80). Named keys are pure ASCII letters → first byte < 0x80.
        // So forward when:
        //   • keyLen == 1 and the byte is a printable ASCII char, OR
        //   • keyLen  > 1 and the first byte is ≥ 0x80 (true UTF-8).
        // The onRawKeyDown event above already carries the key code for
        // control-key consumers (InputField_OnKey handles Backspace/etc
        // through that channel).
        const auto first = static_cast<unsigned char>(keyPtr[0]);
        if (keyLen == 1 && first >= 0x20 && first != 0x7F) {
            g_domWindow->onTextInput.Invoke(std::string_view(keyPtr, static_cast<std::size_t>(keyLen)));
        } else if (keyLen > 1 && first >= 0x80) {
            // Real multi-byte UTF-8 (non-ASCII printable). Forward as-is —
            // dom-env.js always sends valid UTF-8.
            g_domWindow->onTextInput.Invoke(std::string_view(keyPtr, static_cast<std::size_t>(keyLen)));
        }
    }

    __attribute__((export_name("__crafterDom_keyUp")))
    void __crafterDom_keyUp(std::int32_t /*handle*/, const char* codePtr, std::int32_t codeLen) {
        if (!g_domWindow) return;
        KeyCode code = HashKeyCode(codePtr, static_cast<std::size_t>(codeLen));
        g_domWindow->heldKeys.erase(code);
        g_domWindow->onRawKeyUp.Invoke(code);
    }

    __attribute__((export_name("__crafterDom_resize")))
    void __crafterDom_resize(std::int32_t /*handle*/, std::int32_t newW, std::int32_t newH) {
        if (!g_domWindow) return;
        g_domWindow->Resize(static_cast<std::uint32_t>(newW),
                            static_cast<std::uint32_t>(newH));
    }

    __attribute__((export_name("__crafterDom_close")))
    void __crafterDom_close(std::int32_t /*handle*/) {
        if (!g_domWindow) return;
        g_domWindow->open = false;
        g_domWindow->onClose.Invoke();
    }
}

#endif  // CRAFTER_GRAPHICS_WINDOW_DOM