#ifndef GRAPHICS_CONTEXT_HPP_
#define GRAPHICS_CONTEXT_HPP_

#include <Utilities/RGBABytes.hpp>
#include <Utilities/Utilities.hpp>
#include <Vk/DescriptorSetLayout/DescriptorSetLayout.hpp>
#include <stack>
#include <memory>
#include <atomic>
#include <Material/Material.hpp>
#include <Vk/Instance/Instance.hpp>
#include <Vk/Surface/Surface.hpp>
#include <Vk/Device/Device.hpp>
#include <Vk/Swapchain/Swapchain.hpp>
#include <Vk/Pipeline/Pipeline.hpp>
#include <Vk/Framebuffer/Framebuffer.hpp>
#include <Vk/CommandPool/CommandPool.hpp>
#include <Vk/CommandBuffer/CommandBuffer.hpp>
#include <Vk/Semaphore/Semaphore.hpp>
#include <Vk/Fence/Fence.hpp>
#include <CommandQueue/CommandQueue.hpp>
#include <AverageTimeTracker/AverageTimeTracker.hpp>
#include <Vk/QueryPool/QueryPool.hpp>
#include <Font/Font.hpp>

class ToolTip;

/**
 * @brief Methods related to drawing.
 */
class GraphicsContext
{
public:
    /**
     * @brief The maximum number of frames in flight.
     */
    static constexpr int maxFramesInFlight = 2;

private:
    struct VirtualViewport
    {
        glm::ivec2 pos;
        glm::ivec2 size;

        glm::ivec2 clipPos;
        glm::ivec2 clipSize;

        void apply(Vk::CommandBuffer &cmdbuf);
    };

    struct FrameInFlight
    {
        std::shared_ptr<Vk::CommandBuffer> precmdbuf;
        std::shared_ptr<Vk::CommandBuffer> cmdbuf;
        std::shared_ptr<Vk::Semaphore> imageAvailableSemaphore;
        std::shared_ptr<Vk::Semaphore> renderFinishedSemaphore;
        std::shared_ptr<Vk::Fence> fence;
        std::shared_ptr<Vk::Buffer> screenshotBuffer;
        VkExtent2D screenshotExtent;
        std::unique_ptr<Vk::QueryPool> queryPool;
        bool timerQueryReady = false;
        Uint32 drawStarted;
        std::vector<std::shared_ptr<void>> heldResources;
    };

    SDL_Window *m_sdlwin;
    std::shared_ptr<Vk::Instance> m_instance;
    std::shared_ptr<Vk::Surface> m_surface;
    std::shared_ptr<Vk::Device> m_device;
    std::shared_ptr<Vk::Swapchain> m_swapchain;
    std::shared_ptr<Vk::RenderPass> m_guiRenderPass;
    std::shared_ptr<Vk::RenderPass> m_clearRenderPass;
    std::shared_ptr<Vk::Pipeline> m_guiPipeline;
    std::vector<std::shared_ptr<Vk::Framebuffer>> m_presentFramebuffers;
    std::shared_ptr<Vk::CommandPool> m_graphicsCommandPool;
    FrameInFlight m_framesInFlight[maxFramesInFlight];
    std::shared_ptr<Vk::CommandBuffer> m_precmdbuf;
    std::shared_ptr<Vk::CommandBuffer> m_cmdbuf;
    std::shared_ptr<Vk::Framebuffer> m_framebuffer;
    int m_nextFrameInFlight;
    int m_thisFrame;

    int m_windowWidth;
    int m_windowHeight;

    glm::mat4 makeRectMatrix(int x, int y, int width, int height);
    void recreateSwapchain();

    bool m_fullscreen;
    bool m_swapchainDirty;

    std::stack<VirtualViewport> m_viewportStack;
    static CommandQueue<GraphicsContext> m_asyncCommandQueue;
    static std::atomic<int> m_numPendingSubmissions;
    static std::atomic<uint64_t> m_frameCounter;

    ToolTip *m_tooltip;

    bool screenshotRequested = false;

    void checkFrameTime();

public:
    AverageTimeTracker drawTracker;

    /**
     * @brief Represents the constants for GUI rects.
     */
    struct GuiRectInfo
    {
        glm::mat4 rectMatrix;
        glm::mat4 texMatrix;
        glm::vec4 color;
    };

    static GraphicsContext *instance;

    static glm::vec4 color;
    static std::shared_ptr<Vk::DescriptorSetLayout> materialLayout;
    static std::shared_ptr<Vk::DescriptorSetLayout> voxelPaletteDescriptorSetLayout;
    static std::shared_ptr<Vk::DescriptorSetLayout> arrayDescriptorSetLayout;
    static std::shared_ptr<Vk::Pipeline> svoUpdatePipeline;

    static constexpr int pixelSize = 4;
    
    /**
     * @brief Constructor.
     * 
     * This should only ever be ran once: inside `main()`. This creates the window and
     * the associated context.
     */
    GraphicsContext();
    ~GraphicsContext();

    /**
     * @brief Toggle fullscreen.
     */
    void toggleFullscreen();

    /**
     * @brief Draw the next frame.
     * 
     * @return `true` if the frame was drawn, `false` if not ready.
     */
    bool drawFrame();

    /**
     * @brief Clear the GUI framebuffer with the specified color.
     */
    void clearGui(glm::vec4 color);

    /**
     * @brief Begin the GUI render pass.
     */
    void beginGui();

    /**
     * @brief Draw a GUI rectangle according to the information.
     */
    void drawGuiRect(const GuiRectInfo &info);

    /**
     * @brief End the GUI render pass.
     */
    void endGui();

    /**
     * @brief Draw a material (image) at the specified coordinates.
     * 
     * @param mat The material to draw.
     * @param x The X coordinate.
     * @param y The Y coordinate.
     */
    void drawMaterial(Material &mat, int x, int y);

    /**
     * @brief Draw and scale a material.
     * 
     * @param mat The material.
     * @param x The X coordinate.
     * @param y The Y coordinate.
     * @param width The width.
     * @param height The height.
     */
    void drawMaterial(Material &mat, int x, int y, int width, int height);

    /**
     * @brief Clip, scale and draw a material.
     * 
     * @param mat The material.
     * @param sx Source X to clip from.
     * @param sy Source Y to clip from.
     * @param sw Source width to clip.
     * @param sh Source height to clip.
     * @param dx Destination X to draw at.
     * @param dy Destination Y to draw at.
     * @param dw Destination width to scale to.
     * @param dh Destination height to scale to.
     */
    void drawMaterial(Material &mat, int sx, int sy, int sw, int sh, int dx, int dy, int dw, int dh);

    /**
     * @brief Draw a rectangle.
     * 
     * @param x Start X.
     * @param y Start Y.
     * @param width Width.
     * @param height Height.
     */
    void drawRect(int x, int y, int width, int height);

    /**
     * @brief Get the width (in physical pixels) of the window.
     */
    int getWindowWidth();

    /**
     * @brief Get the height (in physical pixels) of the window.
     */
    int getWindowHeight();

    /**
     * @brief Get the width in virtual pixels.
     */
    int getVirtualWidth();

    /**
     * @brief Get the height in virtual pixels.
     */
    int getVirtualHeight();

    /**
     * @brief Lay out characters in a string.
     * 
     * @param font The font to use.
     * @param text The text to lay out.
     * @param setColor Called whenever the color escape sequence is encountered.
     * @param placeGlyph Called whenever a glyph's location is decided.
     */
    template<
        std::invocable<glm::vec4> ColorCallback,
        std::invocable<Font::Sheet&, glm::ivec2, glm::ivec2, glm::ivec2, size_t> GlyphCallback
    >
    static void layoutText(Font &font, std::string text, ColorCallback setColor, GlyphCallback placeGlyph, bool masked)
    {
        glm::ivec2 pos(0);

        size_t offset = 0;
        while (offset != text.size())
        {
            size_t thisCodepointOffset = offset;
            uint64_t codepoint = utf8read(text, offset);

            if (codepoint == '\a')
            {
                if (offset + 6 >= text.size())
                {
                    return;
                }

                glm::vec4 color;

                try
                {
                    color = glm::vec4(
                        (float) std::stoi(text.substr(offset + 0, 2), nullptr, 16) / 255.0f,
                        (float) std::stoi(text.substr(offset + 2, 2), nullptr, 16) / 255.0f,
                        (float) std::stoi(text.substr(offset + 4, 2), nullptr, 16) / 255.0f,
                        1.0f
                    );
                }
                catch (const std::exception &e)
                {
                    color = glm::vec4(1.0f, 0.0f, 0.0f, 1.0f);
                }

                setColor(color);
                offset += 6;
            }
            else if (codepoint == '\n')
            {
                pos.x = 0;
                pos.y += font.glyphHeight + 1;
            }
            else
            {
                auto glyphIt = font.codepointGlyphs.find(masked ? UTF8_MASK_CHAR : codepoint);
                if (glyphIt == font.codepointGlyphs.end())
                {
                    glyphIt = font.codepointGlyphs.find(UTF8_INVALID_CHAR);
                }

                if (glyphIt == font.codepointGlyphs.end())
                {
                    crash("This font is missing the 'invalid' character, and an invalid character was encountered!");
                }

                placeGlyph(*glyphIt->second.sheet, pos, glyphIt->second.sheetBase, glm::ivec2(glyphIt->second.width, font.glyphHeight), thisCodepointOffset);
                pos.x += glyphIt->second.width + 1;
            }
        }
    }

    /**
     * @brief Draw text in GUI mode.
     * 
     * @param font The font to use.
     * @param x Starting X position.
     * @param y Starting Y position.
     * @param text The text.
     * @param masked If true, the characters are masked.
     */
    void drawText(Font &font, int x, int y, std::string text, bool masked = false);

    /**
     * @brief Get the size of the specified text.
     * 
     * @param font The font.
     * @param text The text.
     * @param masked If true, the characters are masked.
     * @return The size of the text in pixels.
     */
    static glm::ivec2 measureText(Font &font, std::string text, bool maksed = false);

    /**
     * @brief Returns a "color string" for the specified normalized RGBA color.
     * 
     * The alpha is ignored. the string has the form "\aRRGGBB", with the value given in
     * hex, and is accepted by the text-drawing methods in this class to mean "change the
     * color of subsequent text to this".
     */
    static std::string colorstr(glm::vec4 color);

    /**
     * @brief Returns a "color string" for the specified RGBA bytes.
     * 
     * The alpha is ignored. the string has the form "\aRRGGBB", with the value given in
     * hex, and is accepted by the text-drawing methods in this class to mean "change the
     * color of subsequent text to this".
     */
    static std::string colorstr(const RGBABytes &bytes);

    /**
     * @brief Push the current GUI viewport onto the viewport stack.
     */
    void pushViewport();

    /**
     * @brief Pop off the previous viewport and restore it.
     */
    void popViewport();

    /**
     * @brief Clip the current viewport.
     * 
     * All subsequent drawing will happen relative to, and within, the new viewport.
     * 
     * @param pos The position to clip from (relative to current viewport).
     * @param size The (maximum) size of the viewport.
     */
    void clipViewport(glm::ivec2 pos, glm::ivec2 size);

    /**
     * @brief Called from `main` if the window has been resized.
     */
    void onResized();

    /**
     * @brief Draw the currently-set tooltip if any.
     */
    void drawToolTip(int x, int y);

    /**
     * @brief Create a tool tip to later display.
     * 
     * @return The reference to the tooltip.
     */
    ToolTip& createToolTip();

    /**
     * @brief Get a reference to the swapchain.
     */
    Vk::Swapchain& swapchain();

    /**
     * @brief Get the pre-command buffer for the current frame.
     * 
     * Use this buffer to schedule updates to data that should happen before the
     * frame's draws.
     */
    Vk::CommandBuffer& precmdbuf();

    /**
     * @brief Get the command buffer for the current frame.
     * 
     * Use this command buffer inside draw functions.
     */
    Vk::CommandBuffer& cmdbuf();

    /**
     * @brief Create a command buffer which can be submitted to the graphics queue.
     */
    std::shared_ptr<Vk::CommandBuffer> createAsyncCommandBuffer();

    /**
     * @brief Get the Device object
     * 
     * @return std::shared_ptr<Vk::Device> 
     */
    std::shared_ptr<Vk::Device> getDevice();

    /**
     * @brief Run the specified function on the next frame.
     * 
     * You can call this from other threads, to schedule work to be done on the main thread,
     * right before a frame.
     * 
     * @param cmd The command.
     */
    static void onNextFrame(CommandQueue<GraphicsContext>::Command cmd);

    /**
     * @brief Returns a reference to the current frame's framebuffer.
     */
    Vk::Framebuffer& framebuffer();

    /**
     * @brief Return the index of this frame-in-flight.
     */
    int getThisFrameIndex();

    /**
     * @brief Return the number of asynchronous submissions in the queue.
     */
    static int getPendingSubmissions();

    /**
     * @brief Return the current value of the frame counter.
     * 
     * The frame counter is incremented every time a frame render actually begins (and
     * commands will actually be submitted to the GPU).
     * 
     * The counter is atomic and this function can be called from any thread without any
     * synchronization.
     */
    static uint64_t getFrameCounter();

    /**
     * @brief Returns the total VRAM usage.
     */
    size_t getTotalVramUsage();

    /**
     * @brief Return an absolute position given a viewport-relative position.
     */
    glm::ivec2 getAbsPos(glm::ivec2 relPos);

    /**
     * @brief Requests a screenshot of the next frame.
     */
    void requestScreenshot();

    /**
     * @brief Center the mouse in the window.
     */
    void centerMouse();

    /**
     * @brief Hold the specified resource.
     * 
     * The specified object will be "held": it will not be deleted until at least the
     * current frame is done rendering. This is needed for example if we have a buffer
     * which is used by this frame, but may no longer be needed once this frame is done
     * rendering.
     */
    void holdResource(std::shared_ptr<void> res);

    /**
     * @brief Create a material from a grid of pixels.
     *
     * The material will be available to draw in the current frame.
     */
    std::shared_ptr<Material> createMaterialFromPixels(glm::ivec2 size, const RGBABytes *pixels);

    /**
     * @brief Returns the mouse position in physical pixels.
     */
    glm::ivec2 getPhysicalMousePos();

    /**
     * @brief Maximize the game window.
     */
    void maximizeWindow();

    /**
     * @brief Set the relative mouse mode.
     */
    void setRelativeMouseMode(bool enabled);

    friend class BlockItem;
    friend class WidgetDisplay;
};

#endif