#include "core/engine.hpp"

#include "components/camera.hpp"
#include "components/lights.hpp"
#include "components/resources.hpp"
#include "core/application.hpp"
#include "flecs/addons/cpp/c_types.hpp"
#include "graphics/opengl/program.hpp"
#include "graphics/opengl/shader.hpp"
#include "logging/engine_logger.hpp"
#include "maths/transform.hpp"
#include "renderer/gl_renderer.hpp"
#include "resources/model.hpp"
#include "resources/resource_manager.hpp"
#include "utils/assert.hpp"

#include <flecs.h>
#include <stb_image.h>

#include <algorithm> // std::clamp
#include <chrono>
#include <cmath>
#include <cmath> // std::fmod
#include <filesystem>
#include <memory>
#include <utility>

using namespace kuiper;

engine::engine(std::unique_ptr<kuiper::application>&& app)
    : m_application(std::move(app)),
      m_window(std::make_unique<glfw_window>(m_application->get_app_spec().width,
                                             m_application->get_app_spec().height,
                                             m_application->get_app_spec().name,
                                             nullptr,
                                             nullptr)) {
    return;
}

engine::~engine() {
    // Should be in reverse order of subsystems initialised in Initialise()

    // NOTE: Should free window ptr here?

    return;
}

std::expected<void, std::error_code> engine::initialise(const cli& cli_args) noexcept {
    // OpenGL expects { 0.0, 0.0 } to be the bottom-left corner of the image
    stbi_set_flip_vertically_on_load(true);

    // Engine working directory
    if (!cli_args.root_path.empty()) {
        std::filesystem::current_path(cli_args.root_path);
    }

    // GLFW
    glfw_window::initialise_glfw(cli_args.prefer_x11);

    return {};
}

void engine::start() {
    auto logger = engine_logger::get();
    logger.info("Kuiper Engine starting");

    m_window->lock_cursor();
    m_window->enable_raw_input();
    m_application->initialise(this);
    auto renderer = gl::renderer::init();

    // Local player & camera

    auto local_player = m_world.entity("Local player");
    local_player.set<maths::transform_euler>({});

    auto local_camera = m_world.entity("Local camera");
    local_camera.set<maths::transform_euler>({});
    local_camera.set<component::camera_props>({});
    local_camera.add(flecs::ChildOf, local_player);

    // Per-frame flecs queries

    const auto renderables =
        world().query_builder<maths::transform_euler, component::model_resource>().cached().build();
    const auto lights = world().query_builder<maths::transform_euler, component::point_light>().cached().build();

    auto last_time_point = std::chrono::high_resolution_clock::now(); // Initialised with first time point
    while (!m_window->should_close()) {
        const auto                         time_now   = std::chrono::high_resolution_clock::now();
        const std::chrono::duration<float> delta_time = time_now - last_time_point;
        last_time_point                               = time_now;

        // Input

        m_window->poll_events();

        // State
        // TODO: [update world state here]

        m_application->pre_render(delta_time.count());

        // Rendering

        {
            auto rendering_start = std::chrono::high_resolution_clock::now();

            lights.each([&renderer](flecs::entity                 e,
                                    const maths::transform_euler& transform,
                                    const component::point_light& light) {
                renderer.add_light(transform.position, light);
            });

            // Render every entity with a transform & model attached
            renderables.each([this, &renderer](flecs::entity                    e,
                                               const maths::transform_euler&    transform,
                                               const component::model_resource& resource) {
                const auto& model = m_resources.get<resource::model>(resource.id);
                if (!model.has_value())
                    return;

                renderer.draw_model(model.value(), transform);
            });

            // Draw to framebuffers
            for (const auto& target : m_render_targets) {
                renderer.render(target, m_cam_pos, m_cam_props);
            }

            // Draw to default framebuffer
            if (m_render_to_default) {
                renderer.render(m_window->back_buffer(), m_cam_pos, m_cam_props);
            }

            // Debug metrics

            m_metrics.renderer_metrics = renderer.metrics();

            // Flush the renderer state, hand over the default frame buffer to the application

            renderer.flush();

            std::chrono::duration<float, std::milli> render_dur =
                std::chrono::high_resolution_clock::now() - rendering_start;
            m_metrics.rendering_time = render_dur.count();
        }

        m_window->back_buffer()->bind(); // so the user application renders to the window

        m_application->post_render(delta_time.count());
        m_window->swap_buffers();

        // End
    }

    m_application->shutdown();

    return;
}

void engine::add_render_target(const gl::framebuffer::s_ptr& target) noexcept {
    return m_render_targets.push_back(target);
}

void engine::remove_render_target(const gl::framebuffer::s_ptr& target) noexcept {
    std::ignore = std::remove(m_render_targets.begin(), m_render_targets.end(), target);
    return;
}

void engine::disable_default_framebuffer() noexcept {
    m_render_to_default = false;
    return;
}

void engine::enable_default_framebuffer() noexcept {
    m_render_to_default = true;
    return;
}

void engine::set_render_camera(const glm::vec3& pos, const component::camera_props& cam_props) noexcept {
    m_cam_pos   = pos;
    m_cam_props = cam_props;
    return;
}
