package net.halbear.Terrain4J.EngineCore.Vulkan.Structure.DisplayToScreen;

import net.halbear.Terrain4J.EngineCore.Display.Window;
import net.halbear.Terrain4J.EngineCore.Logic.Rendering.VulkanContext;
import net.halbear.Terrain4J.EngineCore.Vulkan.Structure.DeviceLayers.Device;
import net.halbear.Terrain4J.EngineCore.Vulkan.Structure.DeviceLayers.GPUSynchronisation.Semaphore;
import net.halbear.Terrain4J.EngineCore.Vulkan.Structure.DeviceLayers.Queues.Queue;
import org.lwjgl.system.MemoryStack;
import org.lwjgl.system.MemoryUtil;
import org.lwjgl.vulkan.*;
import org.tinylog.Logger;

import java.nio.IntBuffer;
import java.nio.LongBuffer;
import java.util.Arrays;

import static net.halbear.Terrain4J.EngineCore.Vulkan.Util.VulkanUtils.vkCheck;
import static org.lwjgl.vulkan.VK13.*;

public class SwapChain {
    private final ImageView[] ImageViewArray;
    private final int ImageCount;
    private final VkExtent2D SwapChainExtent;
    private final long VulkanSwapChain;

    public SwapChain(Window window, Device device, Surface surface, int RequestedImages, boolean VSYNC){
        Logger.debug("Creating Vulkan SwapChain");
        try(var MemStack = MemoryStack.stackPush()){
            VkSurfaceCapabilitiesKHR SurfaceCapabilities = surface.GetSurfaceCapabilities();
            int requestedImages = CalculateImageCount(SurfaceCapabilities, RequestedImages);
            SwapChainExtent = CalculateSwapChainExtent(window, SurfaceCapabilities);

            Surface.SurfaceFormat surfaceFormat = surface.GetSurfaceFormat();
            var VulkanSwapChainCreateInfo = VkSwapchainCreateInfoKHR.calloc(MemStack)
                    .sType$Default()
                    .surface(surface.GetVkSurface())
                    .minImageCount(requestedImages)
                    .imageFormat(surfaceFormat.ImageFormat())
                    .imageColorSpace(surfaceFormat.ColourSpace())
                    .imageExtent(SwapChainExtent)
                    .imageArrayLayers(1)
                    .imageUsage(VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT)
                    .preTransform(SurfaceCapabilities.currentTransform())
                    .compositeAlpha(KHRSurface.VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR)
                    .clipped(true);
            if (VSYNC){
                VulkanSwapChainCreateInfo.presentMode(KHRSurface.VK_PRESENT_MODE_FIFO_KHR);
            } else{
                VulkanSwapChainCreateInfo.presentMode(KHRSurface.VK_PRESENT_MODE_IMMEDIATE_KHR);
            }

            LongBuffer LongPointer = MemStack.mallocLong(1);
            vkCheck(KHRSwapchain.vkCreateSwapchainKHR(device.FetchVulkanDevice(),VulkanSwapChainCreateInfo, null, LongPointer), "Failed to create vulkan Swap Chain");
            VulkanSwapChain = LongPointer.get(0);

            ImageViewArray = CreateImageViews(MemStack, device, VulkanSwapChain, surfaceFormat.ImageFormat());
            ImageCount = ImageViewArray.length;
        }
    }

    public static int CalculateImageCount(VkSurfaceCapabilitiesKHR SurfaceCapabilities, int RequestedImages){
        int MaxImages = SurfaceCapabilities.maxImageCount();
        int MinImages = SurfaceCapabilities.minImageCount();
        int ReturnValue = MinImages;
        if(MaxImages != 0){
            return Math.min(RequestedImages, MaxImages);
        }
        ReturnValue = Math.max(ReturnValue, MinImages);
        return ReturnValue;
    }
    public static VkExtent2D CalculateSwapChainExtent(Window window, VkSurfaceCapabilitiesKHR SurfaceCapabilities){
        var ReturnValue = VkExtent2D.calloc();
        if(SurfaceCapabilities.currentExtent().width() == 0xFFFFFFFF){
            int Width = Math.min(window.getWidth(), SurfaceCapabilities.maxImageExtent().width());
            Width= Math.max(Width, SurfaceCapabilities.minImageExtent().width());

            int Height = Math.min(window.getHeight(), SurfaceCapabilities.maxImageExtent().height());
            Height = Math.max(Height, SurfaceCapabilities.minImageExtent().height());

            ReturnValue.width(Width);
            ReturnValue.height(Height);
        } else{
            ReturnValue.set(SurfaceCapabilities.currentExtent());
        }
        return ReturnValue;
    }
    public static ImageView[] CreateImageViews(MemoryStack MemStack, Device device, long SwapChain, int Format){
        IntBuffer IntPointer = MemStack.mallocInt(1);
        vkCheck(KHRSwapchain.vkGetSwapchainImagesKHR(device.FetchVulkanDevice(), SwapChain, IntPointer, null),"Failed to get number of surface Images");
        int ImageCount = IntPointer.get(0);
        LongBuffer SwapChainImages = MemStack.mallocLong(ImageCount);
        vkCheck(KHRSwapchain.vkGetSwapchainImagesKHR(device.FetchVulkanDevice(), SwapChain, IntPointer, SwapChainImages),"Failed to get surface Images");

        var ReturnResult = new ImageView[ImageCount];
        var ImageViewData = new ImageView.ImageViewData().Format(Format).AspectMask(VK_IMAGE_ASPECT_COLOR_BIT);
        for(int i = 0; i < ImageCount; i++){
            ReturnResult[i] = new ImageView(device, SwapChainImages.get(i),ImageViewData,false);
        }
        return ReturnResult;
    }

    public int FetchNextImage(Device device, Semaphore ImageFetchSemaphore){
        int ImageIndex;
        try(var MemStack = MemoryStack.stackPush()){
            IntBuffer IntPointer = MemStack.mallocInt(1);
            int Error = KHRSwapchain.vkAcquireNextImageKHR(device.FetchVulkanDevice(), VulkanSwapChain, ~0L, ImageFetchSemaphore.GetSemaphore(), MemoryUtil.NULL, IntPointer);
            if (Error == KHRSwapchain.VK_ERROR_OUT_OF_DATE_KHR){
                return -1;
            }
            else if (Error == KHRSwapchain.VK_SUBOPTIMAL_KHR){
                //Logger.warn("Suboptimal surface properties match, not fatal");
            } else if (Error != VK_SUCCESS){
                throw new RuntimeException("Failed to fetch next image in the swapchain: " + Error);
            }
            ImageIndex = IntPointer.get(0);
        }
        return ImageIndex;
    }
    public boolean PresentImage(Queue queue,Semaphore RenderCompleteSemaphores, int ImageIndex){
        boolean resize = false;
        try(var MemStack = MemoryStack.stackPush()){
            VkPresentInfoKHR Present = VkPresentInfoKHR.calloc(MemStack)
                    .sType$Default()
                    .pWaitSemaphores(MemStack.longs(RenderCompleteSemaphores.GetSemaphore()))
                    .swapchainCount(1)
                    .pSwapchains(MemStack.longs(VulkanSwapChain))
                    .pImageIndices(MemStack.ints(ImageIndex));
            int Error = KHRSwapchain.vkQueuePresentKHR(queue.GetVulkanQueue(), Present);
            if (Error == KHRSwapchain.VK_ERROR_OUT_OF_DATE_KHR){
                resize = true;
                //Logger.debug("Detected Resize");
                //vkQueueWaitIdle(queue.GetVulkanQueue());
            } else if (Error == KHRSwapchain.VK_SUBOPTIMAL_KHR){
                //Logger.warn("Suboptimal surface properties match, not fatal");
            } else if (Error != VK_SUCCESS){
                Logger.debug("Failure in Present Image");
                throw new RuntimeException("failed to present KHR: " + Error);
            }
        }
        return resize;
    }

    public void cleanup(Device device){
        Logger.debug("Destroying Vulkan Swapchain");
        SwapChainExtent.free();
        Arrays.asList(ImageViewArray).forEach(i->i.cleanup(device));
        KHRSwapchain.vkDestroySwapchainKHR(device.FetchVulkanDevice(), VulkanSwapChain, null);
        VulkanContext.VulkanLayersOpen--;
    }

    public ImageView GetVulkanImageView(int index){
        return ImageViewArray[index];
    }

    public int GetImageCount(){
        return ImageCount;
    }
    public VkExtent2D GetSwapChainExtent(){
        return SwapChainExtent;
    }
}
