#include <glm/detail/qualifier.hpp>
#include <glm/ext/matrix_clip_space.hpp>
#include <glm/ext/matrix_transform.hpp>
#include <glm/glm.hpp>
#include <glm/gtx/transform.hpp>

#include <initializer_list>
#include <iostream>
#include <fstream>
#include <ratio>
#include <vector>
#include <chrono>

#include <cimg_fb.hpp>
#include <mesh.hpp>
#include <obj_loader.hpp>
#include <object.hpp>

int DIM_WIDTH = 1280;
int DIM_HEIGHT = 720;

namespace gb {
    /*
     * prototype Camera
     * const glm::mat4& view_mat(void) const
     */
    class flat_camera
    {
    public:
        explicit flat_camera(unsigned int dimx, unsigned int dimy,
                glm::vec3 pos = {0.f, 0.f, 0.f},
                glm::vec3 focus = {1.f, 0.f, 0.f})
            : m_dimx(dimx), m_dimy(dimy), m_pos(pos), m_focus(focus)
        {
            update_mat();
        }

        void set_camara_pos(glm::vec3 pos)
        {
            m_pos = pos;
            update_mat();
        }

        void set_camara_focus(glm::vec3 focus)
        {
            m_focus = focus;
            update_mat();
        }

        const glm::mat4& view_mat(void) const
        {
            return m_mat;
        }

    private:
        void update_mat(void)
        {
            // camara pos
            m_mat = glm::lookAt(m_pos, m_focus, { 0.f, 0.f, 1.f });
            // perspective
            m_mat = glm::perspective(
                    glm::radians(60.f),
                    float(1.f * DIM_WIDTH / DIM_HEIGHT),
                    0.1f,
                    100.f
                    ) * m_mat;

            // (-1, 1) to (0, 1)
            m_mat = glm::scale(glm::mat4(1), { 0.5f, 0.5f, 0.5f }) * m_mat;
            m_mat = glm::translate(glm::mat4(1), glm::vec3({0.5f, 0.5f, 0.f})) * m_mat;

            // viewport transformation
            m_mat = glm::mat4({1.f, 0.f, 0.f, 0.f, 0.f, -1.f, 0.f, 0.f, 0.f, 0.f, 1.f, 0.f, 0.f, 0.f, 0.f, 1.f}) * m_mat;
            m_mat = glm::translate(glm::mat4(1), glm::vec3({0.f, 1.f, 0.f})) * m_mat;
            m_mat = glm::scale(glm::mat4(1), {1.f * m_dimx, 1.f * m_dimy, 1}) * m_mat;
        }

    private:
        int m_dimx, m_dimy;

        glm::vec3 m_pos;
        glm::vec3 m_focus;
        glm::mat4 m_mat;
    };

    template <typename T>
    static inline T min(std::initializer_list<T> args)
    {
        T min = *args.begin();
        for (T arg : args)
        {
            if (arg < min) min = arg;
        }
        return min;
    }

    template <typename T>
    static inline T max(std::initializer_list<T> args)
    {
        T max = *args.begin();
        for (T arg : args)
        {
            if (arg > max) max = arg;
        }
        return max;
    }

    template <typename Framebuffer>
    static void draw_mesh(Framebuffer& fb, const gb::mesh_compute& mesh)
    {
        glm::vec<4, int> pos({0, 0, 0, 1});
        glm::vec4 pos_f({0.f, 0.f, 0.f, 1.f});

        using color_t = glm::vec<3, unsigned char>;

        static int color_index = 0;
        static color_t colors[] =  { color_t { 0x88, 0xcc, 0xff }, color_t { 0xff, 0xaa, 0xaa }, color_t {0xaa, 0xff, 0xaa} };

        float x_start = max({ 0.f, min( { mesh.m_va.x, mesh.m_vb.x, mesh.m_vc.x } ) }) + 0.5f;
        float y_start = max({ 0.f, min( { mesh.m_va.y, mesh.m_vb.y, mesh.m_vc.y } ) }) + 0.5f;
        float x_end   = min({ (float)fb.dimx()-1, max({ mesh.m_va.x, mesh.m_vb.x, mesh.m_vc.x }) }) + 0.5f;
        float y_end   = min({ (float)fb.dimy()-1, max({ mesh.m_va.y, mesh.m_vb.y, mesh.m_vc.y }) }) + 0.5f;

        for (pos.y = y_start, pos_f.y = y_start; pos_f.y < y_end; ++pos.y, ++pos_f.y) {
            for (pos.x = x_start, pos_f.x = x_start; pos_f.x < x_end; ++pos.x, ++pos_f.x) {
                if (mesh.sample(pos_f)) {
                    fb.set_pixel(pos, colors[color_index]);
                }
            }
        }

        if (++color_index == 3) {
            color_index = 0;
        }
    }

static std::chrono::steady_clock::time_point time_start;
void bench_start(void)
{
    time_start = std::chrono::steady_clock::now();
}

void bench_end(const char* msg)
{
    auto end = std::chrono::steady_clock::now();
    std::cout << "[Benchmark] " << std::chrono::duration_cast<std::chrono::milliseconds>(end - time_start).count() << "ms in " << msg << std::endl;
}
}

int main(void) {
    std::vector<gb::Object> rendering_queue;
    gb::bench_start();
    gb::obj_loader loader;

    {
        std::fstream f_model("./res/tea-pot.obj", std::ios::in);
        rendering_queue.push_back(loader.load_object(f_model));
        std::cout << "finish preparing the model" << std::endl;
    }
    gb::bench_end("loading model");


    gb::cimg_framebuffer fb("output.bmp", DIM_WIDTH, DIM_HEIGHT);
    gb::flat_camera camera(DIM_WIDTH, DIM_HEIGHT, {0.3f, 0.3f, 0.3f}, {0.f, 0.f, 0.f});

    gb::bench_start();
    for (auto& obj : rendering_queue)
    {
        obj.draw(fb, camera);
    }
    gb::bench_end("rendering");
    return 0;
}
