/*
 * Copyright (c) Adubbz
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms and conditions of the GNU General Public License,
 * version 2, as published by the Free Software Foundation.
 *
 * This program is distributed in the hope it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
#include <optional>
#include <switch.h>
#include <nanovg.h>
#include <nanovg_dk.h>
#include <nanovg/framework/CApplication.h>
#include "ui.hpp"
#include "ams_su.h"

extern "C" {

    void userAppInit(void) {
        Result rc = 0;

        if (R_FAILED(rc = romfsInit())) {
            fatalThrow(rc);
        }

        if (R_FAILED(rc = spsmInitialize())) {
            fatalThrow(rc);
        }

        if (R_FAILED(rc = plInitialize(PlServiceType_User))) {
            fatalThrow(rc);
        }

        if (R_FAILED(rc = splInitialize())) {
            fatalThrow(rc);
        }

        if (R_FAILED(rc = nsInitialize())) {
            fatalThrow(rc);
        }

        if (R_FAILED(rc = hiddbgInitialize())) {
            fatalThrow(rc);
        }

    }

    void userAppExit(void) {
        hiddbgExit();
        nsExit();
        splExit();
        plExit();
        spsmExit();
        romfsExit();
        amssuExit();
    }

}

namespace {

    static constexpr u32 FramebufferWidth = 1280;
    static constexpr u32 FramebufferHeight = 720;

}

class Daybreak : public CApplication {
    private:
        static constexpr unsigned NumFramebuffers = 2;
        static constexpr unsigned StaticCmdSize = 0x1000;

        dk::UniqueDevice m_device;
        dk::UniqueQueue m_queue;
        dk::UniqueSwapchain m_swapchain;

        std::optional<CMemPool> m_pool_images;
        std::optional<CMemPool> m_pool_code;
        std::optional<CMemPool> m_pool_data;

        dk::UniqueCmdBuf m_cmd_buf;
        DkCmdList m_render_cmdlist;

        dk::Image m_depth_buffer;
        CMemPool::Handle m_depth_buffer_mem;
        dk::Image m_framebuffers[NumFramebuffers];
        CMemPool::Handle m_framebuffers_mem[NumFramebuffers];
        DkCmdList m_framebuffer_cmdlists[NumFramebuffers];

        std::optional<nvg::DkRenderer> m_renderer;
        NVGcontext *m_vg;
        int m_standard_font;
    public:
        Daybreak() {
            Result rc = 0;

            /* Create the deko3d device. */
            m_device = dk::DeviceMaker{}.create();

            /* Create the main queue. */
            m_queue = dk::QueueMaker{m_device}.setFlags(DkQueueFlags_Graphics).create();

            /* Create the memory pools. */
            m_pool_images.emplace(m_device, DkMemBlockFlags_GpuCached | DkMemBlockFlags_Image, 16*1024*1024);
            m_pool_code.emplace(m_device, DkMemBlockFlags_CpuUncached | DkMemBlockFlags_GpuCached | DkMemBlockFlags_Code, 128*1024);
            m_pool_data.emplace(m_device, DkMemBlockFlags_CpuUncached | DkMemBlockFlags_GpuCached, 1*1024*1024);

            /* Create the static command buffer and feed it freshly allocated memory. */
            m_cmd_buf = dk::CmdBufMaker{m_device}.create();
            CMemPool::Handle cmdmem = m_pool_data->allocate(StaticCmdSize);
            m_cmd_buf.addMemory(cmdmem.getMemBlock(), cmdmem.getOffset(), cmdmem.getSize());

            /* Create the framebuffer resources. */
            this->CreateFramebufferResources();

            m_renderer.emplace(FramebufferWidth, FramebufferHeight, m_device, m_queue, *m_pool_images, *m_pool_code, *m_pool_data);
            m_vg = nvgCreateDk(&*m_renderer, NVG_ANTIALIAS | NVG_STENCIL_STROKES);


            PlFontData font;
            if (R_FAILED(rc = plGetSharedFontByType(&font, PlSharedFontType_Standard))) {
                fatalThrow(rc);
            }

            m_standard_font = nvgCreateFontMem(m_vg, "switch-standard", static_cast<u8 *>(font.address), font.size, 0);
        }

        ~Daybreak() {
            /* Destroy the framebuffer resources. This should be done first. */
            this->DestroyFramebufferResources();

            /* Cleanup vg. */
            nvgDeleteDk(m_vg);

            /* Destroy the renderer. */
            m_renderer.reset();
        }
    private:
        void CreateFramebufferResources() {
            /* Create layout for the depth buffer. */
            dk::ImageLayout layout_depth_buffer;
            dk::ImageLayoutMaker{m_device}
                .setFlags(DkImageFlags_UsageRender | DkImageFlags_HwCompression)
                .setFormat(DkImageFormat_S8)
                .setDimensions(FramebufferWidth, FramebufferHeight)
                .initialize(layout_depth_buffer);

            /* Create the depth buffer. */
            m_depth_buffer_mem = m_pool_images->allocate(layout_depth_buffer.getSize(), layout_depth_buffer.getAlignment());
            m_depth_buffer.initialize(layout_depth_buffer, m_depth_buffer_mem.getMemBlock(), m_depth_buffer_mem.getOffset());

            /* Create layout for the framebuffers. */
            dk::ImageLayout layout_framebuffer;
            dk::ImageLayoutMaker{m_device}
                .setFlags(DkImageFlags_UsageRender | DkImageFlags_UsagePresent | DkImageFlags_HwCompression)
                .setFormat(DkImageFormat_RGBA8_Unorm)
                .setDimensions(FramebufferWidth, FramebufferHeight)
                .initialize(layout_framebuffer);

            /* Create the framebuffers. */
            std::array<DkImage const*, NumFramebuffers> fb_array;
            const u64 fb_size  = layout_framebuffer.getSize();
            const u32 fb_align = layout_framebuffer.getAlignment();

            for (unsigned int i = 0; i < NumFramebuffers; i++) {
                /* Allocate a framebuffer. */
                m_framebuffers_mem[i] = m_pool_images->allocate(fb_size, fb_align);
                m_framebuffers[i].initialize(layout_framebuffer, m_framebuffers_mem[i].getMemBlock(), m_framebuffers_mem[i].getOffset());

                /* Generate a command list that binds it. */
                dk::ImageView color_target{ m_framebuffers[i] }, depth_target{ m_depth_buffer };
                m_cmd_buf.bindRenderTargets(&color_target, &depth_target);
                m_framebuffer_cmdlists[i] = m_cmd_buf.finishList();

                /* Fill in the array for use later by the swapchain creation code. */
                fb_array[i] = &m_framebuffers[i];
            }

            /* Create the swapchain using the framebuffers. */
            m_swapchain = dk::SwapchainMaker{m_device, nwindowGetDefault(), fb_array}.create();

            /* Generate the main rendering cmdlist. */
            this->RecordStaticCommands();
        }

        void DestroyFramebufferResources() {
            /* Return early if we have nothing to destroy. */
            if (!m_swapchain) return;

            /* Make sure the queue is idle before destroying anything. */
            m_queue.waitIdle();

            /* Clear the static cmdbuf, destroying the static cmdlists in the process. */
            m_cmd_buf.clear();

            /* Destroy the swapchain. */
            m_swapchain.destroy();

            /* Destroy the framebuffers. */
            for (unsigned int i = 0; i < NumFramebuffers; i ++) {
                m_framebuffers_mem[i].destroy();
            }

            /* Destroy the depth buffer. */
            m_depth_buffer_mem.destroy();
        }

        void RecordStaticCommands() {
            /* Initialize state structs with deko3d defaults. */
            dk::RasterizerState rasterizer_state;
            dk::ColorState color_state;
            dk::ColorWriteState color_write_state;

            /* Configure the viewport and scissor. */
            m_cmd_buf.setViewports(0, { { 0.0f, 0.0f, FramebufferWidth, FramebufferHeight, 0.0f, 1.0f } });
            m_cmd_buf.setScissors(0, { { 0, 0, FramebufferWidth, FramebufferHeight } });

            /* Clear the color and depth buffers. */
            m_cmd_buf.clearColor(0, DkColorMask_RGBA, 0.f, 0.f, 0.f, 1.0f);
            m_cmd_buf.clearDepthStencil(true, 1.0f, 0xFF, 0);

            /* Bind required state. */
            m_cmd_buf.bindRasterizerState(rasterizer_state);
            m_cmd_buf.bindColorState(color_state);
            m_cmd_buf.bindColorWriteState(color_write_state);

            m_render_cmdlist = m_cmd_buf.finishList();
        }

        void Render(u64 ns) {
            /* Acquire a framebuffer from the swapchain (and wait for it to be available). */
            int slot = m_queue.acquireImage(m_swapchain);

            /* Run the command list that attaches said framebuffer to the queue. */
            m_queue.submitCommands(m_framebuffer_cmdlists[slot]);

            /* Run the main rendering command list. */
            m_queue.submitCommands(m_render_cmdlist);

            nvgBeginFrame(m_vg, FramebufferWidth, FramebufferHeight, 1.0f);
            dbk::RenderMenu(m_vg, ns);
            nvgEndFrame(m_vg);

            /* Now that we are done rendering, present it to the screen. */
            m_queue.presentImage(m_swapchain, slot);
        }

    public:
        bool onFrame(u64 ns) override {
            dbk::UpdateMenu(ns);
            this->Render(ns);
            return !dbk::IsExitRequested();
        }
};

int main(int argc, char **argv) {
    /* Initialize the menu. */
    if (argc > 1)
        dbk::InitializeMenu(FramebufferWidth, FramebufferHeight, argv[1]);
    else
        dbk::InitializeMenu(FramebufferWidth, FramebufferHeight);

    Daybreak daybreak;
    daybreak.run();
    return 0;
}
