//
//  Lol Engine
//
//  Copyright © 2017—2019 Sam Hocevar <sam@hocevar.net>
//            © 2009—2015 Benjamin “Touky” Huet <huet.benjamin@gmail.com>
//
//  Lol Engine is free software. It comes without any warranty, to
//  the extent permitted by applicable law. You can redistribute it
//  and/or modify it under the terms of the Do What the Fuck You Want
//  to Public License, Version 2, as published by the WTFPL Task Force.
//  See http://www.wtfpl.net/ for more details.
//

#include <lol/engine-internal.h>

#include <cstdio>
#include <string>

//
// The Imgui integration
//

using namespace lol;

namespace
{
    enum key_enum
    {
        LeftClick = 300,
        RightClick,
        MiddleClick,
        Focus,
    };

    enum axis_enum
    {
        Scroll,
    };
}

static gui* g_gui = nullptr;

//-----------------------------------------------------------------------------
gui::gui(ImFontAtlas *shared_font_atlas)
{
    ImGui::CreateContext(shared_font_atlas);

    m_gamegroup = tickable::group::game::gui;
    m_drawgroup = tickable::group::draw::gui;

    // Build shader code -------------------------------------------------------
    ShaderVar out_vertex = ShaderVar::GetShaderOut(ShaderProgram::Vertex);
    ShaderVar out_pixel = ShaderVar::GetShaderOut(ShaderProgram::Pixel);

    ShaderVar pass_texcoord = ShaderVar(ShaderVariable::Varying, ShaderVariableType::Vec2, "texcoord");
    ShaderVar pass_color = ShaderVar(ShaderVariable::Varying, ShaderVariableType::Vec4, "color");

    ShaderVar in_position = ShaderVar(ShaderVariable::Attribute, ShaderVariableType::Vec2, "position");
    ShaderVar in_texcoord = ShaderVar(ShaderVariable::Attribute, ShaderVariableType::Vec2, "texcoord");
    ShaderVar in_color = ShaderVar(ShaderVariable::Attribute, ShaderVariableType::Vec4, "color");

    m_ortho.m_var = ShaderVar(ShaderVariable::Uniform, ShaderVariableType::Mat4, "ortho");
    m_texture.m_var = ShaderVar(ShaderVariable::Uniform, ShaderVariableType::sampler2D, "texture");

    ShaderBlock imgui_vertex("imgui_vertex");
    imgui_vertex
        << out_vertex << m_ortho << in_position
        << pass_texcoord << in_texcoord
        << pass_color << in_color;
    imgui_vertex.SetMainCode(lol::format(
        "%s = .5 * %s * vec4(%s, -1.0, 1.0);\n" "%s = %s;\n" "%s = %s;\n",
        out_vertex.tostring().c_str(),
        m_ortho.tostring().c_str(),
        in_position.tostring().c_str(),
        pass_texcoord.tostring().c_str(),
        in_texcoord.tostring().c_str(),
        pass_color.tostring().c_str(),
        in_color.tostring().c_str()));

    ShaderBlock imgui_pixel("imgui_pixel");
    imgui_pixel << m_texture << pass_texcoord << pass_color << out_pixel;
    imgui_pixel.SetMainCode(lol::format(
        "vec4 col = %s * texture2D(%s, %s);\n" "if (col.a == 0.0) discard;\n" "%s = col;\n",
        pass_color.tostring().c_str(),
        m_texture.tostring().c_str(),
        pass_texcoord.tostring().c_str(),
        out_pixel.tostring().c_str()));

    m_builder << ShaderProgram::Vertex << imgui_vertex
              << ShaderProgram::Pixel << imgui_pixel;

    // Input Setup -------------------------------------------------------------
    m_profile.register_default_keys();

    m_profile << InputProfile::MouseKey(key_enum::LeftClick, g_name_mouse_key_left);
    m_profile << InputProfile::MouseKey(key_enum::RightClick, g_name_mouse_key_right);
    m_profile << InputProfile::MouseKey(key_enum::MiddleClick, g_name_mouse_key_middle);
    m_profile << InputProfile::MouseKey(key_enum::Focus, g_name_mouse_key_in_screen);

    m_profile << InputProfile::MouseAxis(axis_enum::Scroll, g_name_mouse_axis_scroll);

    Ticker::Ref(m_controller = new Controller("ImGui_Controller"));
    m_controller->Init(m_profile);
    m_mouse = InputDevice::GetMouse();
    m_keyboard = InputDevice::GetKeyboard();
}

gui::~gui()
{
    ImGui::GetIO().Fonts->TexID = nullptr;
    Ticker::Unref(m_font);
    m_font = nullptr;

    ImGui::DestroyContext();
}

//-----------------------------------------------------------------------------
void gui::init(ImFontAtlas *shared_font_atlas)
{
    Ticker::Ref(g_gui = new gui(shared_font_atlas));

    ImGuiIO& io = ImGui::GetIO();
    //ImFont* font0 = io.Fonts->AddFontDefault();

    // Keyboard mapping; these are the only ones ImGui cares about, the
    // rest is just handled by the application.
    io.KeyMap[ImGuiKey_Tab]         = (int)input::key::SC_Tab;
    io.KeyMap[ImGuiKey_LeftArrow]   = (int)input::key::SC_Left;
    io.KeyMap[ImGuiKey_RightArrow]  = (int)input::key::SC_Right;
    io.KeyMap[ImGuiKey_UpArrow]     = (int)input::key::SC_Up;
    io.KeyMap[ImGuiKey_DownArrow]   = (int)input::key::SC_Down;
    io.KeyMap[ImGuiKey_Home]        = (int)input::key::SC_Home;
    io.KeyMap[ImGuiKey_End]         = (int)input::key::SC_End;
    io.KeyMap[ImGuiKey_Delete]      = (int)input::key::SC_Delete;
    io.KeyMap[ImGuiKey_Backspace]   = (int)input::key::SC_Backspace;
    io.KeyMap[ImGuiKey_Enter]       = (int)input::key::SC_Return;
    io.KeyMap[ImGuiKey_Escape]      = (int)input::key::SC_Escape;
    io.KeyMap[ImGuiKey_A]           = (int)input::key::SC_A;
    io.KeyMap[ImGuiKey_C]           = (int)input::key::SC_C;
    io.KeyMap[ImGuiKey_V]           = (int)input::key::SC_V;
    io.KeyMap[ImGuiKey_X]           = (int)input::key::SC_X;
    io.KeyMap[ImGuiKey_Y]           = (int)input::key::SC_Y;
    io.KeyMap[ImGuiKey_Z]           = (int)input::key::SC_Z;

    // Func pointer
    io.RenderDrawListsFn = gui::static_render_draw_lists;
    io.SetClipboardTextFn = gui::static_set_clipboard;
    io.GetClipboardTextFn = gui::static_get_clipboard;
    io.ClipboardUserData = &g_gui->m_clipboard;
}

/* CALLBACKS
void ImGui_ImplGlfw_CharCallback(GLFWwindow* window, unsigned int c)
{
ImGuiIO& io = ImGui::GetIO();
if (c > 0 && c < 0x10000)
io.AddInputCharacter((unsigned short)c);
}

*/

void gui::shutdown()
{
    ImGui::EndFrame();

    if (g_gui)
    {
        Ticker::Unref(g_gui);
        g_gui = nullptr;
    }
}

//-----------------------------------------------------------------------------
std::string gui::clipboard()
{
    return g_gui ? g_gui->m_clipboard : "";
}

void gui::static_set_clipboard(void *data, const char* text)
{
    std::string *clipboard = (std::string *)data;
    *clipboard = text;
}
const char* gui::static_get_clipboard(void *data)
{
    std::string *clipboard = (std::string *)data;
    return clipboard->c_str();
}

void gui::refresh_fonts()
{
    if (g_gui->m_font)
        Ticker::Unref(g_gui->m_font);

    // Build texture
    unsigned char* pixels;
    ivec2 size;
    ImGuiIO& io = ImGui::GetIO();
    io.Fonts->GetTexDataAsRGBA32(&pixels, &size.x, &size.y);

    Image* image = new Image();
    image->Copy(pixels, size, PixelFormat::RGBA_8);

    Ticker::Ref(g_gui->m_font = new TextureImage("", image));
}

//-----------------------------------------------------------------------------
void gui::tick_game(float seconds)
{
    super::tick_game(seconds);

    ImGuiIO& io = ImGui::GetIO();

    // Init Texture
    if (!m_font)
    {
        refresh_fonts();
    }

    // Texture has been created
    if (m_font && m_font->GetTexture())
    {
        io.Fonts->TexID = (void *)m_font->GetTexture();
    }

    // Setup display size (every frame to accommodate for window resizing)
    auto video_size = vec2(Video::GetSize());
    io.DisplaySize = video_size;

    // Setup time step
    io.DeltaTime = seconds;
    io.MouseDrawCursor = true;

    // Update Keyboard
    io.KeyCtrl = m_controller->IsKeyPressed((int)input::key::SC_LCtrl)
              || m_controller->IsKeyPressed((int)input::key::SC_RCtrl);
    io.KeyShift = m_controller->IsKeyPressed((int)input::key::SC_LShift)
               || m_controller->IsKeyPressed((int)input::key::SC_RShift);

    for (input::key k : input::all_keys())
        io.KeysDown[(int)k] = m_controller->IsKeyPressed((int)k);

    m_keyboard->SetTextInputActive(io.WantTextInput);

    //Update text input
    std::string text = m_keyboard->GetText();
    //text.case_change(io.KeyShift);
    for (auto ch : text)
        io.AddInputCharacter(ch);

    // Update mouse
    if (m_mouse)
    {
        vec2 cursor = m_mouse->GetCursor(0);
        cursor.y = 1.f - cursor.y;

        io.MousePos = cursor * video_size;
        //msg::debug("%.2f/%.2f\n", io.MousePos.x, io.MousePos.y);
        io.MouseWheel = m_controller->GetAxisValue(axis_enum::Scroll);

        io.MouseDown[0] = m_controller->IsKeyPressed(key_enum::LeftClick);
        io.MouseDown[1] = m_controller->IsKeyPressed(key_enum::RightClick);
        io.MouseDown[2] = m_controller->IsKeyPressed(key_enum::MiddleClick);
        // FIXME: handle key_enum::Focus?
    }

    // Start the frame
    ImGui::NewFrame();
}

//-----------------------------------------------------------------------------
void gui::tick_draw(float seconds, Scene &scene)
{
    super::tick_draw(seconds, scene);

    scene.AddPrimitiveRenderer(this, std::make_shared<primitive>());
}

void gui::primitive::Render(Scene& scene, std::shared_ptr<PrimitiveSource> primitive)
{
    UNUSED(scene, primitive);

    ImGui::Render();
    ImGui::EndFrame();
}

//// Data
//static GLFWwindow*  g_Window = NULL;
//static double       g_Time = 0.0f;
//static bool         g_MousePressed[3] = { false, false, false };
//static float        g_MouseWheel = 0.0f;
//static GLuint       g_FontTexture = 0;

//-------------------------------------------------------------------------
// This is the main rendering function that you have to implement and provide to ImGui (via setting up 'RenderDrawListsFn' in the ImGuiIO structure)
// If text or lines are blurry when integrating ImGui in your engine:
// - in your Render function, try translating your projection matrix by (0.5f,0.5f) or (0.375f,0.375f)
//-------------------------------------------------------------------------
void gui::static_render_draw_lists(ImDrawData* draw_data)
{
    g_gui->render_draw_lists(draw_data);
}

void gui::render_draw_lists(ImDrawData* draw_data)
{
    if (draw_data == nullptr)
        return;

    vec2 size = vec2(Video::GetSize());
    float alpha = 1.f;
    mat4 ortho = mat4::ortho(size.x * alpha, size.y * alpha, -1000.f, 1000.f)
        * mat4::lookat(vec3::axis_z, vec3::zero, vec3::axis_y)
        * mat4::scale(vec3::axis_x - vec3::axis_y - vec3::axis_z)
        * mat4::translate(-size.x * .5f * alpha, -size.y * .5f * alpha, 0.f);

    // Create shader
    if (!m_shader)
    {
        m_shader = Shader::Create(m_builder.GetName(), m_builder.Build());
        ASSERT(m_shader);

        m_ortho.m_uniform = m_shader->GetUniformLocation(m_ortho.m_var.tostring());
        m_texture.m_uniform = m_shader->GetUniformLocation(m_texture.m_var.tostring());

        m_attribs << m_shader->GetAttribLocation(VertexUsage::Position, 0)
                  << m_shader->GetAttribLocation(VertexUsage::TexCoord, 0)
                  << m_shader->GetAttribLocation(VertexUsage::Color, 0);

        m_vdecl = std::make_shared<VertexDeclaration>(
            VertexStream<vec2, vec2, u8vec4>(
                VertexUsage::Position,
                VertexUsage::TexCoord,
                VertexUsage::Color));
    }

    // Do not render without shader
    if (!m_shader)
        return;

    render_context rc(Scene::GetScene(0).get_renderer());
    rc.cull_mode(CullMode::Disabled);
    rc.depth_func(DepthFunc::Disabled);
    rc.scissor_mode(ScissorMode::Enabled);

    m_shader->Bind();

    // Register uniforms
    m_shader->SetUniform(m_ortho, ortho);

    for (int n = 0; n < draw_data->CmdListsCount; n++)
    {
        auto const &command_list = *draw_data->CmdLists[n];
        /*const unsigned char* vtx_buffer = (const unsigned char*)&command_list.VtxBuffer.front();*/

        struct Vertex
        {
            vec2 pos, tex;
            u8vec4 color;
        };

        auto vbo = std::make_shared<VertexBuffer>(command_list.VtxBuffer.Size * sizeof(ImDrawVert));
        ImDrawVert *vert = (ImDrawVert *)vbo->Lock(0, 0);
        memcpy(vert, command_list.VtxBuffer.Data, command_list.VtxBuffer.Size * sizeof(ImDrawVert));
        vbo->Unlock();

        auto ibo = std::make_shared<IndexBuffer>(command_list.IdxBuffer.Size * sizeof(ImDrawIdx));
        ImDrawIdx *indices = (ImDrawIdx *)ibo->Lock(0, 0);
        memcpy(indices, command_list.IdxBuffer.Data, command_list.IdxBuffer.Size * sizeof(ImDrawIdx));
        ibo->Unlock();

        ibo->Bind();
        m_vdecl->Bind();
        m_vdecl->SetStream(vbo, m_attribs[0], m_attribs[1], m_attribs[2]);

        const ImDrawIdx* idx_buffer_offset = 0;
        for (int cmd_i = 0; cmd_i < command_list.CmdBuffer.Size; cmd_i++)
        {
            auto const &command = command_list.CmdBuffer[cmd_i];
            Texture* texture = (Texture*)command.TextureId;
            if (texture)
            {
                texture->Bind();
                m_shader->SetUniform(m_texture, texture->GetTextureUniform(), 0);
            }

            rc.scissor_rect(command.ClipRect);

#ifdef SHOW_IMGUI_DEBUG
            //-----------------------------------------------------------------
            //<Debug render> --------------------------------------------------
            //-----------------------------------------------------------------
            //Doesn't work anymore ......
            static uint32_t idx_buffer_offset_i = 0;
            if (cmd_i == 0)
                idx_buffer_offset_i = 0;

            float mod = -200.f;
            vec3 off = vec3(vec2(-size.x, -size.y), 0.f);
            vec3 pos[4] = {
                (1.f / mod) * (off + vec3(0.f)),
                (1.f / mod) * (off + size.x * vec3::axis_x),
                (1.f / mod) * (off + size.x * vec3::axis_x + size.y * vec3::axis_y),
                (1.f / mod) * (off + size.y * vec3::axis_y)
            };
            for (int i = 0; i < 4; ++i)
                Debug::DrawLine(pos[i], pos[(i + 1) % 4], Color::white);
            ImDrawVert* buf = vert;
            for (uint16_t i = 0; i < command.ElemCount; i += 3)
            {
                uint16_t ib = indices[idx_buffer_offset_i + i];
                vec2 pos[3];
                pos[0] = vec2(buf[ib + 0].pos.x, buf[ib + 0].pos.y);
                pos[1] = vec2(buf[ib + 1].pos.x, buf[ib + 1].pos.y);
                pos[2] = vec2(buf[ib + 2].pos.x, buf[ib + 2].pos.y);
                vec4 col[3];
                col[0] = vec4(Color::FromRGBA32(buf[ib + 0].col).arg, 1.f);
                col[1] = vec4(Color::FromRGBA32(buf[ib + 1].col).arg, 1.f);
                col[2] = vec4(Color::FromRGBA32(buf[ib + 2].col).arg, 1.f);
                Debug::DrawLine((off + vec3(pos[0], 0.f)) / mod, (off + vec3(pos[1], 0.f)) / mod, col[0]);
                Debug::DrawLine((off + vec3(pos[1], 0.f)) / mod, (off + vec3(pos[2], 0.f)) / mod, col[1]);
                Debug::DrawLine((off + vec3(pos[2], 0.f)) / mod, (off + vec3(pos[0], 0.f)) / mod, col[2]);
            }
            idx_buffer_offset_i += command.ElemCount;

            //-----------------------------------------------------------------
            //<\Debug render> -------------------------------------------------
            //-----------------------------------------------------------------
#endif //SHOW_IMGUI_DEBUG
            //Debug::DrawLine(vec2::zero, vec2::axis_x /*, Color::green*/);

            m_vdecl->DrawIndexedElements(MeshPrimitive::Triangles, command.ElemCount, (const short*)idx_buffer_offset);

            idx_buffer_offset += command.ElemCount;
        }

        m_vdecl->Unbind();
        ibo->Unbind();
    }

    m_shader->Unbind();
}