#include <vkcv/Swapchain.hpp>
#include <utility>

#include <GLFW/glfw3.h>

namespace vkcv
{
    /**
    * creates surface and checks availability
    * @param window current window for the surface
    * @param instance Vulkan-Instance
    * @param physicalDevice Vulkan-PhysicalDevice
    * @return created surface
    */
    vk::SurfaceKHR createSurface(GLFWwindow* window, const vk::Instance& instance, const vk::PhysicalDevice& physicalDevice) {
        //create surface
        VkSurfaceKHR surface;
        if (glfwCreateWindowSurface(VkInstance(instance), window, nullptr, &surface) != VK_SUCCESS) {
            throw std::runtime_error("failed to create a window surface!");
        }
        vk::Bool32 surfaceSupport = false;
        if (physicalDevice.getSurfaceSupportKHR(0, vk::SurfaceKHR(surface), &surfaceSupport) != vk::Result::eSuccess && surfaceSupport != true) {
            throw std::runtime_error("surface is not supported by the device!");
        }

        return vk::SurfaceKHR(surface);
    }

    Swapchain::Swapchain(const Surface &surface,
                         vk::SwapchainKHR swapchain,
                         vk::Format format,
                         vk::ColorSpaceKHR colorSpace,
                         vk::PresentModeKHR presentMode,
                         uint32_t imageCount,
						 vk::Extent2D extent) noexcept :
			m_Surface(surface),
			m_Swapchain(swapchain),
			m_Format(format),
			m_ColorSpace(colorSpace),
			m_PresentMode(presentMode),
			m_ImageCount(imageCount),
			m_Extent(extent),
			m_RecreationRequired(false)
    {}
    
    Swapchain::Swapchain(const Swapchain &other) :
			m_Surface(other.m_Surface),
			m_Swapchain(other.m_Swapchain),
			m_Format(other.m_Format),
			m_ColorSpace(other.m_ColorSpace),
			m_PresentMode(other.m_PresentMode),
			m_ImageCount(other.m_ImageCount),
			m_Extent(other.m_Extent),
			m_RecreationRequired(other.m_RecreationRequired.load())
	{}

    const vk::SwapchainKHR& Swapchain::getSwapchain() const {
        return m_Swapchain;
    }

    vk::SurfaceKHR Swapchain::getSurface() const {
        return m_Surface.handle;
    }

    vk::Format Swapchain::getFormat() const{
        return m_Format;
    }

    /**
     * chooses Extent and clapms values to the available
     * @param physicalDevice Vulkan-PhysicalDevice
     * @param surface of the swapchain
     * @param window of the current application
     * @return chosen Extent for the surface
     */
    vk::Extent2D chooseExtent(vk::PhysicalDevice physicalDevice, vk::SurfaceKHR surface, const Window &window){
        vk::SurfaceCapabilitiesKHR surfaceCapabilities;
        if(physicalDevice.getSurfaceCapabilitiesKHR(surface,&surfaceCapabilities) != vk::Result::eSuccess){
            throw std::runtime_error("cannot get surface capabilities. There is an issue with the surface.");
        }
        
        int fb_width, fb_height;
        window.getFramebufferSize(fb_width, fb_height);
        
        VkExtent2D extent2D = {
                static_cast<uint32_t>(fb_width),
                static_cast<uint32_t>(fb_height)
        };
        
        extent2D.width = std::max(surfaceCapabilities.minImageExtent.width, std::min(surfaceCapabilities.maxImageExtent.width, extent2D.width));
        extent2D.height = std::max(surfaceCapabilities.minImageExtent.height, std::min(surfaceCapabilities.maxImageExtent.height, extent2D.height));

        return extent2D;
    }

    /**
     * chooses Surface Format for the current surface
     * @param physicalDevice Vulkan-PhysicalDevice
     * @param surface of the swapchain
     * @return available Format
     */
    vk::SurfaceFormatKHR chooseSurfaceFormat(vk::PhysicalDevice physicalDevice, vk::SurfaceKHR surface) {
        uint32_t formatCount;
        physicalDevice.getSurfaceFormatsKHR(surface, &formatCount, nullptr);
        std::vector<vk::SurfaceFormatKHR> availableFormats(formatCount);
        if (physicalDevice.getSurfaceFormatsKHR(surface, &formatCount, &availableFormats[0]) != vk::Result::eSuccess) {
            throw std::runtime_error("Failed to get surface formats");
        }

        for (const auto& availableFormat : availableFormats) {
            if (availableFormat.format == vk::Format::eB8G8R8A8Unorm  && availableFormat.colorSpace == vk::ColorSpaceKHR::eSrgbNonlinear) {
                return availableFormat;
            }
        }
        return availableFormats[0];
    }

    /**
     * returns vk::PresentModeKHR::eMailbox if available or vk::PresentModeKHR::eFifo otherwise
     * @param physicalDevice Vulkan-PhysicalDevice
     * @param surface of the swapchain
     * @return available PresentationMode
     */
    vk::PresentModeKHR choosePresentMode(vk::PhysicalDevice physicalDevice, vk::SurfaceKHR surface) {
        uint32_t modeCount;
        physicalDevice.getSurfacePresentModesKHR( surface, &modeCount, nullptr );
        std::vector<vk::PresentModeKHR> availablePresentModes(modeCount);
        if (physicalDevice.getSurfacePresentModesKHR(surface, &modeCount, &availablePresentModes[0]) != vk::Result::eSuccess) {
            throw std::runtime_error("Failed to get presentation modes");
        }

        for (const auto& availablePresentMode : availablePresentModes) {
            if (availablePresentMode == vk::PresentModeKHR::eMailbox) {
                return availablePresentMode;
            }
        }
        // The FIFO present mode is guaranteed by the spec to be supported
        return vk::PresentModeKHR::eFifo;
    }

    /**
     * returns the minImageCount +1 for at least doublebuffering, if it's greater than maxImageCount return maxImageCount
     * @param physicalDevice Vulkan-PhysicalDevice
     * @param surface of the swapchain
     * @return available ImageCount
     */
    uint32_t chooseImageCount(vk::PhysicalDevice physicalDevice, vk::SurfaceKHR surface) {
        vk::SurfaceCapabilitiesKHR surfaceCapabilities;
        if(physicalDevice.getSurfaceCapabilitiesKHR(surface, &surfaceCapabilities) != vk::Result::eSuccess){
            throw std::runtime_error("cannot get surface capabilities. There is an issue with the surface.");
        }

        uint32_t imageCount = surfaceCapabilities.minImageCount + 1;    // minImageCount should always be at least 2; set to 3 for triple buffering
        // check if requested image count is supported
        if (surfaceCapabilities.maxImageCount > 0 && imageCount > surfaceCapabilities.maxImageCount) {
            imageCount = surfaceCapabilities.maxImageCount;
        }

        return imageCount;
    }
    /**
     * creates and returns a swapchain with default specs
     * @param window of the current application
     * @param context that keeps instance, physicalDevice and a device.
     * @return swapchain
     */
    Swapchain Swapchain::create(const Window &window, const Context &context) {
        const vk::Instance& instance = context.getInstance();
        const vk::PhysicalDevice& physicalDevice = context.getPhysicalDevice();
        const vk::Device& device = context.getDevice();

        Surface surface;
        surface.handle       = createSurface(window.getWindow(), instance, physicalDevice);
        surface.formats      = physicalDevice.getSurfaceFormatsKHR(surface.handle);
        surface.capabilities = physicalDevice.getSurfaceCapabilitiesKHR(surface.handle);
        surface.presentModes = physicalDevice.getSurfacePresentModesKHR(surface.handle);

        vk::Extent2D chosenExtent = chooseExtent(physicalDevice, surface.handle, window);
        vk::SurfaceFormatKHR chosenSurfaceFormat = chooseSurfaceFormat(physicalDevice, surface.handle);
        vk::PresentModeKHR chosenPresentMode = choosePresentMode(physicalDevice, surface.handle);
        uint32_t chosenImageCount = chooseImageCount(physicalDevice, surface.handle);

        vk::SwapchainCreateInfoKHR swapchainCreateInfo(
                vk::SwapchainCreateFlagsKHR(),  //flags
                surface.handle,    // surface
                chosenImageCount,  // minImageCount TODO: how many do we need for our application?? "must be less than or equal to the value returned in maxImageCount" -> 3 for Triple Buffering, else 2 for Double Buffering (should be the standard)
                chosenSurfaceFormat.format,   // imageFormat
                chosenSurfaceFormat.colorSpace,   // imageColorSpace
                chosenExtent,   // imageExtent
                1,  // imageArrayLayers TODO: should we only allow non-stereoscopic applications? yes -> 1, no -> ? "must be greater than 0, less or equal to maxImageArrayLayers"
                vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eStorage,  // imageUsage TODO: what attachments? only color? depth?
                vk::SharingMode::eExclusive,    // imageSharingMode TODO: which sharing mode? "VK_SHARING_MODE_EXCLUSIV access exclusive to a single queue family, better performance", "VK_SHARING_MODE_CONCURRENT access from multiple queues"
                0,  // queueFamilyIndexCount, the number of queue families having access to the image(s) of the swapchain when imageSharingMode is VK_SHARING_MODE_CONCURRENT
                nullptr,    // pQueueFamilyIndices, the pointer to an array of queue family indices having access to the images(s) of the swapchain when imageSharingMode is VK_SHARING_MODE_CONCURRENT
                vk::SurfaceTransformFlagBitsKHR::eIdentity, // preTransform, transformations applied onto the image before display
                vk::CompositeAlphaFlagBitsKHR::eOpaque, // compositeAlpha, TODO: how to handle transparent pixels? do we need transparency? If no -> opaque
                chosenPresentMode,    // presentMode
                true,   // clipped
                nullptr // oldSwapchain
        );

        vk::SwapchainKHR swapchain = device.createSwapchainKHR(swapchainCreateInfo);

        return Swapchain(surface,
                         swapchain,
                         chosenSurfaceFormat.format,
                         chosenSurfaceFormat.colorSpace,
                         chosenPresentMode,
                         chosenImageCount,
						 chosenExtent);
    }
    
    bool Swapchain::shouldUpdateSwapchain() const {
    	return m_RecreationRequired;
    }
    
    void Swapchain::updateSwapchain(const Context &context, const Window &window) {
    	if (!m_RecreationRequired.exchange(false))
    		return;
    	
		vk::SwapchainKHR oldSwapchain = m_Swapchain;
		vk::Extent2D extent2D = chooseExtent(context.getPhysicalDevice(), m_Surface.handle, window);
	
		if ((extent2D.width >= MIN_SWAPCHAIN_SIZE) && (extent2D.height >= MIN_SWAPCHAIN_SIZE)) {
			vk::SwapchainCreateInfoKHR swapchainCreateInfo(
					vk::SwapchainCreateFlagsKHR(),
					m_Surface.handle,
					m_ImageCount,
					m_Format,
					m_ColorSpace,
					extent2D,
					1,
					vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eStorage,
					vk::SharingMode::eExclusive,
					0,
					nullptr,
					vk::SurfaceTransformFlagBitsKHR::eIdentity,
					vk::CompositeAlphaFlagBitsKHR::eOpaque,
					m_PresentMode,
					true,
					oldSwapchain
			);
			
			m_Swapchain = context.getDevice().createSwapchainKHR(swapchainCreateInfo);
		} else {
			m_Swapchain = nullptr;
			
			signalSwapchainRecreation();
		}
		
		if (oldSwapchain) {
			context.getDevice().destroySwapchainKHR(oldSwapchain);
		}
		
		m_Extent = extent2D;
    }

    void Swapchain::signalSwapchainRecreation() {
		m_RecreationRequired = true;
    }
    
    const vk::Extent2D& Swapchain::getExtent() const {
    	return m_Extent;
    }

    Swapchain::~Swapchain() {
        // needs to be destroyed by creator
    }

	uint32_t Swapchain::getImageCount() const {
		return m_ImageCount;
	}
}