#include "engine.hpp"

#include "os/timer.hpp"

#include "geometry/transform.hpp"
#include "graphics/program.hpp"
#include "graphics/location.hpp"
#include "graphics/shape.hpp"
#include "graphics/mesh.hpp"
#include "graphics/material.hpp"
#include "graphics/model.hpp"

#include <utility>      // move


using namespace frog::geo;
using namespace frog::gl;
using namespace frog;


frog::engine::engine(settings set, ptr<state> _global)
    : window(set.width, set.height, set.window_name.c_str())
    , renderer(set.gl_major, set.gl_minor)
    , global(std::move(_global))
{
    window.make_current_context();

    renderer.viewport(set.width, set.height);
    renderer.clear_color(set.clear_color.r
                        , set.clear_color.g
                        , set.clear_color.b);

    // so that no callbacks are missed
    // (actually not needed unless we use polling functions, e.g. getKeyState)
    // window.set_input_mode(GLFW_STICKY_KEYS, GLFW_TRUE);

    window.set_callbacks(input.get());

    camera.screen_size(set.width, set.height);
}


void frog::engine::play()
{
    os::timer timer;
    // DO NOT MAKE THIS VARIABLE UNSIGNED!
    int accum = 0;
    unsigned delta = global->physics_delta_us;

    init();

    while (!global->quit && !window.should_close())
    {
        update_controls();
        frame_update();

        while (accum >= delta)
        {
            stable_update();
            accum -= delta;
        }

        auto frame = timer.reset_duration<os::us>();
        global->frame_time_us = frame;
        accum += frame;

        render(frame / static_cast<double>(delta));
    }
}


void frog::engine::init()
{
    programs.add("main",
        program("engine/assets/main.vert",
                "engine/assets/main.frag"));

    textures.add("", texture("engine/assets/empty.png"));

    scenes->init(*this);
}


void frog::engine::update_controls()
{
    input->reset();
    glfwPollEvents();

    // resize renderer if window has been resized
    if (input->has_resized())
    {
        const auto& [w, h] = *(input->has_resized());
        renderer.viewport(w, h);
        camera.screen_size(w, h);
    }
}


void frog::engine::stable_update()
{
    // TODO
    using namespace os;

    auto[x, y]= input->cursor_pos();

    // camera.look_from(player_pos, (x - 740) / 180, (y - 370) / 180);

    camera.look_towards({ 0, 0, 0 }, 10, (x - 740) / 180, (y - 370) / 180);

    scenes->stable_update(*this);
    scenes->cleanup(*this);
}


void frog::engine::frame_update()
{
    using namespace os;

    // TODO remove
    if (input->is_down(key{ GLFW_KEY_ESCAPE }))
        global->quit = true;

    scenes->frame_update(*this);
}


void frog::engine::render(double between)
{
    renderer.clear();

    renderer.prepare3D();

    draw3D(between);

    renderer.prepare2D();

    drawUI(between);

    window.swap_buffers();
}


void frog::engine::draw3D(double between)
{
    auto& main = programs.at("main");
    main.use();

    main.uniform_mat(camera.projection(), location::ProjectionMatrix);
    main.uniform_mat(camera.view(), location::ViewMatrix);

    main.uniform_vec(camera.eye_position(), location::EyePos);

    scenes->for_each_object([&](auto& obj)
        {
            if (obj.model().mesh_tag.empty())
                return;

            const auto& model = obj.model();

            model.mat.send(main, location::Material);
            textures.at(model.texture_tag).bind(location::TextureBinding);
            main.uniform_mat(model.matrix(), location::ModelMatrix);
            meshes.at(model.mesh_tag).draw();
        });
}


void frog::engine::drawUI(double between)
{

}
