#include "vulkan_swapchain.hpp"
#include "vulkan/vulkan_structs.hpp"
#include <thread>

namespace Vulkan
{

Swapchain::Swapchain(vk::Device device_, vk::PhysicalDevice physical_device_, vk::Queue queue_, vk::SurfaceKHR surface_, vk::CommandPool command_pool_)
    : surface(surface_),
      command_pool(command_pool_),
      physical_device(physical_device_),
      queue(queue_)
{
    device = device_;
    create_render_pass();
    end_render_pass_function = nullptr;
}

Swapchain::~Swapchain()
{
}

bool Swapchain::set_vsync(bool new_setting)
{
    if (new_setting == vsync)
        return false;

    vsync = new_setting;
    return true;
}

void Swapchain::on_render_pass_end(std::function<void ()> function)
{
    end_render_pass_function = function;
}

void Swapchain::create_render_pass()
{
    auto attachment_description = vk::AttachmentDescription{}
        .setFormat(vk::Format::eB8G8R8A8Unorm)
        .setSamples(vk::SampleCountFlagBits::e1)
        .setLoadOp(vk::AttachmentLoadOp::eClear)
        .setStoreOp(vk::AttachmentStoreOp::eStore)
        .setStencilLoadOp(vk::AttachmentLoadOp::eDontCare)
        .setStencilStoreOp(vk::AttachmentStoreOp::eStore)
        .setInitialLayout(vk::ImageLayout::eUndefined)
        .setFinalLayout(vk::ImageLayout::ePresentSrcKHR);

    auto attachment_reference = vk::AttachmentReference{}
        .setAttachment(0)
        .setLayout(vk::ImageLayout::eColorAttachmentOptimal);

    std::array<vk::SubpassDependency, 2> subpass_dependency{};
    subpass_dependency[0]
        .setSrcSubpass(VK_SUBPASS_EXTERNAL)
        .setDstSubpass(0)
        .setSrcStageMask(vk::PipelineStageFlagBits::eColorAttachmentOutput)
        .setSrcAccessMask(vk::AccessFlagBits(0))
        .setDstStageMask(vk::PipelineStageFlagBits::eColorAttachmentOutput)
        .setDstAccessMask(vk::AccessFlagBits::eColorAttachmentWrite);
    subpass_dependency[1]
        .setSrcSubpass(VK_SUBPASS_EXTERNAL)
        .setDstSubpass(0)
        .setSrcStageMask(vk::PipelineStageFlagBits::eColorAttachmentOutput)
        .setSrcAccessMask(vk::AccessFlagBits::eColorAttachmentWrite)
        .setDstStageMask(vk::PipelineStageFlagBits::eFragmentShader)
        .setDstAccessMask(vk::AccessFlagBits::eShaderRead);


    auto subpass_description = vk::SubpassDescription{}
        .setColorAttachments(attachment_reference)
        .setPipelineBindPoint(vk::PipelineBindPoint::eGraphics);

    auto render_pass_create_info = vk::RenderPassCreateInfo{}
        .setSubpasses(subpass_description)
        .setDependencies(subpass_dependency)
        .setAttachments(attachment_description);

    render_pass = device.createRenderPassUnique(render_pass_create_info);
}

bool Swapchain::recreate(int new_width, int new_height)
{
    device.waitIdle();
    return create(num_swapchain_images, new_width, new_height);
}

vk::Image Swapchain::get_image()
{
    return imageviewfbs[current_swapchain_image].image;
}

bool Swapchain::create(unsigned int desired_num_swapchain_images, int new_width, int new_height)
{
    frames.clear();
    imageviewfbs.clear();

    auto surface_capabilities = physical_device.getSurfaceCapabilitiesKHR(surface);

    if (surface_capabilities.minImageCount > desired_num_swapchain_images)
        num_swapchain_images = surface_capabilities.minImageCount;
    else
        num_swapchain_images = desired_num_swapchain_images;

    extents = surface_capabilities.currentExtent;

    if (new_width > 0 && new_height > 0)
    {
        // No buffer is allocated for surface yet
        extents.width = new_width;
        extents.height = new_height;
    }
    else if (extents.width < 1 || extents.height < 1)
    {
        // Surface is likely hidden
        printf("Extents too small.\n");
        swapchain_object.reset();
        return false;
    }
    else if (extents.width > 8192 || extents.height > 8192)
    {
        extents.width = 512;
        extents.height = 512;
    }

    auto swapchain_create_info = vk::SwapchainCreateInfoKHR{}
        .setMinImageCount(num_swapchain_images)
        .setImageFormat(vk::Format::eB8G8R8A8Unorm)
        .setImageExtent(extents)
        .setImageColorSpace(vk::ColorSpaceKHR::eSrgbNonlinear)
        .setImageSharingMode(vk::SharingMode::eExclusive)
        .setImageUsage(vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eTransferSrc)
        .setCompositeAlpha(vk::CompositeAlphaFlagBitsKHR::eOpaque)
        .setClipped(true)
        .setPresentMode(vsync ? vk::PresentModeKHR::eFifo : vk::PresentModeKHR::eImmediate)
        .setSurface(surface)
        .setImageArrayLayers(1);

    if (swapchain_object)
        swapchain_create_info.setOldSwapchain(swapchain_object.get());

    swapchain_object = device.createSwapchainKHRUnique(swapchain_create_info);
    if (!swapchain_object)
        return false;

    auto swapchain_images = device.getSwapchainImagesKHR(swapchain_object.get());
    vk::CommandBufferAllocateInfo command_buffer_allocate_info(command_pool, vk::CommandBufferLevel::ePrimary, max_latency);
    auto command_buffers = device.allocateCommandBuffersUnique(command_buffer_allocate_info);

    if (imageviewfbs.size() > num_swapchain_images)
        num_swapchain_images = imageviewfbs.size();

    frames.resize(max_latency);
    imageviewfbs.resize(num_swapchain_images);

    vk::FenceCreateInfo fence_create_info(vk::FenceCreateFlagBits::eSignaled);

    for (int i = 0; i < max_latency; i++)
    {
        // Create frame queue resources
        auto &frame = frames[i];
        frame.command_buffer = std::move(command_buffers[i]);
        frame.fence = device.createFenceUnique(fence_create_info);
        frame.acquire = device.createSemaphoreUnique({});
        frame.complete = device.createSemaphoreUnique({});
    }
    current_frame = 0;

    for (unsigned int i = 0; i < num_swapchain_images; i++)
    {
        // Create resources associated with swapchain images
        auto &image = imageviewfbs[i];
        image.image = swapchain_images[i];
        auto image_view_create_info = vk::ImageViewCreateInfo{}
            .setImage(swapchain_images[i])
            .setViewType(vk::ImageViewType::e2D)
            .setFormat(vk::Format::eB8G8R8A8Unorm)
            .setComponents(vk::ComponentMapping())
            .setSubresourceRange(vk::ImageSubresourceRange(vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1));
        image.image_view = device.createImageViewUnique(image_view_create_info);

        auto framebuffer_create_info = vk::FramebufferCreateInfo{}
            .setAttachments(image.image_view.get())
            .setWidth(extents.width)
            .setHeight(extents.height)
            .setLayers(1)
            .setRenderPass(render_pass.get());
        image.framebuffer = device.createFramebufferUnique(framebuffer_create_info);
    }

    device.waitIdle();

    current_swapchain_image = 0;

    return true;
}

bool Swapchain::begin_frame()
{
    if (!swapchain_object || extents.width < 1 || extents.height < 1)
    {
        printf ("Extents too small\n");
        return false;
    }

    auto &frame = frames[current_frame];

    auto result = device.waitForFences(frame.fence.get(), true, UINT64_MAX);
    if (result != vk::Result::eSuccess)
    {
        printf("Failed fence\n");
        return false;
    }

    auto result_value = device.acquireNextImageKHR(swapchain_object.get(), UINT64_MAX, frame.acquire.get());
    if (result_value.result == vk::Result::eErrorOutOfDateKHR ||
        result_value.result == vk::Result::eSuboptimalKHR)
    {
        printf("Out of date\n");
        recreate();
        return begin_frame();
    }

    if (result_value.result != vk::Result::eSuccess)
    {
        printf("Unable to acquire swapchain image: %s\n", vk::to_string(result_value.result).c_str());
        return false;
    }

    current_swapchain_image = result_value.value;

    device.resetFences(frame.fence.get());

    vk::CommandBufferBeginInfo command_buffer_begin_info(vk::CommandBufferUsageFlags{vk::CommandBufferUsageFlagBits::eOneTimeSubmit});
    frame.command_buffer->begin(command_buffer_begin_info);

    return true;
}

void Swapchain::end_frame_without_swap()
{
    auto &frame = frames[current_frame];
    frame.command_buffer->end();

    vk::PipelineStageFlags flags = vk::PipelineStageFlagBits::eColorAttachmentOutput;
    vk::SubmitInfo submit_info(
        frame.acquire.get(),
        flags,
        frame.command_buffer.get(),
        frame.complete.get());

    queue.submit(submit_info, frame.fence.get());
}

bool Swapchain::swap()
{
    auto present_info = vk::PresentInfoKHR{}
        .setWaitSemaphores(frames[current_frame].complete.get())
        .setSwapchains(swapchain_object.get())
        .setImageIndices(current_swapchain_image);

    auto result = queue.presentKHR(present_info);

    current_frame = (current_frame + 1) % max_latency;

    if (result != vk::Result::eSuccess)
        return false;
    return true;
}

bool Swapchain::end_frame()
{
    end_frame_without_swap();
    return swap();
}

vk::Framebuffer Swapchain::get_framebuffer()
{
    return imageviewfbs[current_swapchain_image].framebuffer.get();
}

vk::CommandBuffer &Swapchain::get_cmd()
{
    return frames[current_frame].command_buffer.get();
}

void Swapchain::begin_render_pass()
{
    vk::ClearColorValue colorval;
    colorval.setFloat32({ 0.0f, 0.0f, 0.0f, 1.0f });
    vk::ClearValue value;
    value.setColor(colorval);

    auto render_pass_begin_info = vk::RenderPassBeginInfo{}
        .setRenderPass(render_pass.get())
        .setFramebuffer(imageviewfbs[current_swapchain_image].framebuffer.get())
        .setRenderArea(vk::Rect2D({}, extents))
        .setClearValues(value);
    get_cmd().beginRenderPass(render_pass_begin_info, vk::SubpassContents::eInline);
}

void Swapchain::end_render_pass()
{
    if (end_render_pass_function)
    {
        end_render_pass_function();
        end_render_pass_function = nullptr;
    }

    get_cmd().endRenderPass();
}

unsigned int Swapchain::get_current_frame()
{
    return current_frame;
}

bool Swapchain::wait_on_frame(int frame_num)
{
    auto result = device.waitForFences(frames[frame_num].fence.get(), true, 33000000);
    return (result == vk::Result::eSuccess);
}

vk::Extent2D Swapchain::get_extents()
{
    return extents;
}

vk::RenderPass &Swapchain::get_render_pass()
{
    return render_pass.get();
}

unsigned int Swapchain::get_num_frames()
{
    return max_latency;
}

} // namespace Vulkan