#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;


// TODO remove
vec3 player_pos;

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;
    unsigned 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;
            scenes->cleanup(*this);
        }

        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;

    if (input->is_down(key{ GLFW_KEY_D }))
        player_pos.x() += 0.2;

    if (input->is_down(key{ GLFW_KEY_A }))
        player_pos.x() -= 0.2;

    if (input->is_down(key{ GLFW_KEY_W }))
        player_pos.z() -= 0.2;

    if (input->is_down(key{ GLFW_KEY_S }))
        player_pos.z() += 0.2;

    if (input->is_down(key{ GLFW_KEY_SPACE }))
        player_pos.y() += 0.2;

    if (input->is_down(key{ GLFW_KEY_LEFT_SHIFT }))
        player_pos.y() -= 0.2;

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

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

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

    scenes->stable_update(*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();

    static float angle = 0;
    auto model_mat = translate({ 0, 0, -10 })
                    // * rotate(y_axis{}, angle / 7)
                    // * rotate(x_axis{}, angle / 8)
                    // * rotate(z_axis{}, angle / 13)
                    ;
    angle += 0.1;

    auto& main = programs.at("main");
    main.use();

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

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

    material mat
    {
        vec4{ 0.1, 0.1,  0.2,   1 },
        vec4{ 0.6, 0.15, 0.26,  1 },
        vec4{ 0.9, 0.8,  0.75, 20 },
    };
    mat.send(main, location::Material);

    meshes.at("test").draw();

    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();
        });

    renderer.prepare2D();
    // TODO draw UI stuff

    window.swap_buffers();
}
