#include "Renderer.h"
#include "Log.h"
#include "AssetManager.h"
#include "Texture.h"
#include "Profiling.h"

#include <assert.h>

Renderer* Renderer::ptr = nullptr;

static const char* g_vert_src = "#version 300 es                                                                   \n"
                                "precision mediump float;                                                          \n"
                                "uniform vec2 res; // resolution                                                   \n"
                                "layout (location = 0) in vec2 v_p0;   // top left corner on screen                \n"
                                "layout (location = 1) in vec2 v_p1;   // top right corner on screen               \n"
                                "layout (location = 2) in vec2 v_src_p0;                                           \n"
                                "layout (location = 3) in vec2 v_src_p1;                                           \n"
                                "layout (location = 4) in vec4 v_color;    // color                                \n"
                                "layout (location = 5) in int v_expand_r; //                                       \n"
                                "out vec4 f_color;                                                                 \n"
                                "out vec2 f_uv;                                                                    \n"
                                "flat out int f_expand_r;                                                          \n"
                                "const vec2 vertices[6] = vec2[6](                                                 \n"
                                "        vec2(-1,-1),                                                              \n"
                                "        vec2(+1, -1),                                                             \n"
                                "        vec2(-1, +1),                                                             \n"
                                "        vec2(+1, -1),                                                             \n"
                                "        vec2(+1, +1),                                                             \n"
                                "        vec2(-1, +1)                                                              \n"
                                ");                                                                                \n"
                                "void main()                                                                       \n"
                                "{                                                                                 \n"
                                "    vec2 v = vertices[gl_VertexID];                                               \n"
                                "    // destination on screen                                                      \n"
                                "    //vec2 dst_half_size = (v_p1 - v_p0) / 2.0;                                   \n"
                                "    //vec2 dst_center = (v_p1 + v_p0) / 2.0;                                      \n"
                                "    vec2 dst_pos = v * ((v_p1 - v_p0) / 2.0) + ((v_p1 + v_p0) / 2.0);             \n"
                                "    dst_pos.y = res.y - dst_pos.y;                                                \n"
                                "    vec2 src = v * ((v_src_p1 - v_src_p0) / 2.0) + ((v_src_p1 + v_src_p0) / 2.0); \n"
                                "    gl_Position = vec4(2.0 * dst_pos.x / res.x - 1.0,                             \n"
                                "                       2.0 * dst_pos.y / res.y - 1.0,                             \n"
                                "                       0.0,                                                       \n"
                                "                       1.0);                                                      \n"
                                "    f_color = v_color;                                                            \n"
                                "    f_uv = src;                                                                   \n"
                                "    f_expand_r = v_expand_r;                                                      \n"
                                "}                                                                                 \n";

static const char* g_frag_src = "#version 300 es                                      \n"
                                "precision mediump float;                             \n"
                                "in vec2 f_uv;                                        \n"
                                "in vec4 f_color;                                     \n"
                                "flat in int f_expand_r;                              \n"
                                "layout (location = 0) out vec4 frag_color;           \n"
                                "uniform sampler2D s_texture;                         \n"
                                "void main()                                          \n"
                                "{                                                    \n"
                                "    vec4 tex_color = texture(s_texture, f_uv);       \n"
                                "    if (f_expand_r == 1)                             \n"
                                "        tex_color = tex_color.rrrr;                  \n"
                                "    frag_color = tex_color * f_color;                \n"
                                "}                                                    \n";

void Renderer::create()
{
    ZoneScoped;
    Renderer::ptr = new Renderer;
}

void Renderer::destroy()
{
    ZoneScoped;
    delete Renderer::ptr;
    Renderer::ptr = nullptr;
}

Renderer::Renderer()
{
    ZoneScoped;
    GLuint vertex = 0, fragment = 0;
    vertex = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertex, 1, &g_vert_src, nullptr);
    glCompileShader(vertex);
    int status = 0;
    glGetShaderiv(vertex, GL_COMPILE_STATUS, &status);
    if (status != GL_TRUE) {
        char info[512];
        glGetShaderInfoLog(vertex, 512, nullptr, info);
        ALOGE("Failed to compile ui vertex shader: %s", info);
    }

    fragment = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragment, 1, &g_frag_src, nullptr);
    glCompileShader(fragment);
    glGetShaderiv(fragment, GL_COMPILE_STATUS, &status);
    if (status != GL_TRUE) {
        char info[512];
        glGetShaderInfoLog(fragment, 512, nullptr, info);
        ALOGE("Failed to compile ui fragment shader: %s", info);
    }

    m_shader = glCreateProgram();
    glAttachShader(m_shader, vertex);
    glAttachShader(m_shader, fragment);
    glLinkProgram(m_shader);
    glGetProgramiv(m_shader, GL_LINK_STATUS, &status);
    if (status != GL_TRUE) {
        char info[512];
        glGetProgramInfoLog(m_shader, 512, nullptr, info);
        ALOGE("Failed to link ui shader: %s", info);
    }

    glDeleteShader(vertex);
    glDeleteShader(fragment);

    glGenBuffers(1, &m_vbo);
    glGenVertexArrays(1, &m_vao);
    glBindVertexArray(m_vao);
    glBindBuffer(GL_ARRAY_BUFFER, m_vbo);

    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Rect), (GLvoid*)0);
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(Rect), (GLvoid*)offsetof(Rect, dst_p1_x));
    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Rect), (GLvoid*)offsetof(Rect, src_p0_x));
    glVertexAttribPointer(3, 2, GL_FLOAT, GL_FALSE, sizeof(Rect), (GLvoid*)offsetof(Rect, src_p1_x));
    glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, sizeof(Rect), (GLvoid*)offsetof(Rect, r));
    glVertexAttribIPointer(5, 1, GL_INT, sizeof(Rect), (GLvoid*)offsetof(Rect, expand_r));
    glEnableVertexAttribArray(0);
    glEnableVertexAttribArray(1);
    glEnableVertexAttribArray(2);
    glEnableVertexAttribArray(3);
    glEnableVertexAttribArray(4);
    glEnableVertexAttribArray(5);
    glVertexAttribDivisor(0, 1);
    glVertexAttribDivisor(1, 1);
    glVertexAttribDivisor(2, 1);
    glVertexAttribDivisor(3, 1);
    glVertexAttribDivisor(4, 1);
    glVertexAttribDivisor(5, 1);
    glBindVertexArray(0);

    // Dummy texture (1x1px white)
    unsigned char tex[] = {255, 255, 255, 255};
    Texture dummy_texture(1, 1, tex);
    m_textures.insert(std::make_pair(0, dummy_texture));
    m_dummy_texture = 0;
}

Renderer::~Renderer()
{
    ZoneScoped;
    for (auto texture : m_textures)
        texture.second.destroy();
    glDeleteProgram(m_shader);
    glDeleteVertexArrays(1, &m_vao);
    glDeleteBuffers(1, &m_vbo);
}

void Renderer::addRect(float x, float y, float w, float h)
{
    ZoneScoped;
    addRect(x, y, w, h, 1.f, 1.f, 1.f, 1.f, m_dummy_texture, 0.f, 0.f, 1.f, 1.f);
}

void Renderer::addRect(float x, float y, float w, float h, StringHandle texture)
{
    ZoneScoped;
    addRect(x, y, w, h, 1.f, 1.f, 1.f, 1.f, texture, 0.f, 0.f, 1.f, 1.f);
}

void Renderer::addRect(float x, float y, float w, float h, float r, float g, float b, float a)
{
    ZoneScoped;
    addRect(x, y, w, h, r, g, b, a, m_dummy_texture, 0.f, 0.f, 1.f, 1.f);
}

void Renderer::addRect(float x, float y, float w, float h, float r, float g, float b, float a, StringHandle texture)
{
    ZoneScoped;
    addRect(x, y, w, h, r, g, b, a, texture, 0.f, 0.f, 1.f, 1.f);
}

void Renderer::addRect(float x,
                       float y,
                       float w,
                       float h,
                       StringHandle texture,
                       float src_x,
                       float src_y,
                       float src_w,
                       float src_h)
{
    ZoneScoped;
    addRect(x, y, w, h, 1.f, 1.f, 1.f, 1.f, texture, src_x, src_y, src_w, src_h);
}

void Renderer::addRect(float x,
                       float y,
                       float w,
                       float h,
                       float r,
                       float g,
                       float b,
                       float a,
                       StringHandle texture,
                       float src_x,
                       float src_y,
                       float src_w,
                       float src_h)
{
    ZoneScoped;
    if (m_textures.find(texture) == m_textures.end()) {
        Texture tex;
        if (!AssetManager::ptr->loadTexture(StringRepository::global->getString(texture), &tex)) {
            ALOGE("Failed to load texture");
            return;
        }
        m_textures.insert(std::make_pair(texture, tex));
    }

    Rect rect = {};
    rect.dst_p0_x = x;
    rect.dst_p0_y = y;
    rect.dst_p1_x = x + w;
    rect.dst_p1_y = y + h;
    rect.src_p0_x = src_x;
    rect.src_p0_y = src_y;
    rect.src_p1_x = src_x + src_w;
    rect.src_p1_y = src_y + src_h;
    rect.r = r;
    rect.g = g;
    rect.b = b;
    rect.a = a;
    rect.expand_r = 0;
    m_rects.push_back(rect);
    m_draw_textures.push_back(texture);
}

void Renderer::addFontRect(float x,
                           float y,
                           float w,
                           float h,
                           float char_height,
                           StringHandle font,
                           StringHandle id,
                           float src_x0,
                           float src_y0,
                           float src_x1,
                           float src_y1)
{
    ZoneScoped;
    addFontRect(x, y, w, h, 0.f, 0.f, 0.f, 1.f, char_height, font, id, src_x0, src_y0, src_x1, src_y1);
}

void Renderer::addFontRect(float x,
                           float y,
                           float w,
                           float h,
                           float r,
                           float g,
                           float b,
                           float a,
                           float char_height,
                           StringHandle font,
                           StringHandle id,
                           float src_x0,
                           float src_y0,
                           float src_x1,
                           float src_y1)
{
    ZoneScoped;
    if (m_textures.find(id) == m_textures.end()) {
        Texture tex;
        if (!AssetManager::ptr->loadFontBitmap(StringRepository::global->getString(font), char_height, &tex)) {
            ALOGE("Failed to load texture");
            return;
        }
        m_textures.insert(std::make_pair(id, tex));
    }

    Rect rect = {};
    rect.dst_p0_x = x;
    rect.dst_p0_y = y;
    rect.dst_p1_x = x + w;
    rect.dst_p1_y = y + h;
    rect.src_p0_x = src_x0;
    rect.src_p0_y = src_y0;
    rect.src_p1_x = src_x1;
    rect.src_p1_y = src_y1;
    rect.r = r;
    rect.g = g;
    rect.b = b;
    rect.a = a;
    rect.expand_r = 1;
    m_rects.push_back(rect);
    m_draw_textures.push_back(id);
}

void Renderer::renderFrame(float width, float height)
{
    ZoneScoped;
    assert(m_rects.size() == m_draw_textures.size());

    glViewport(0, 0, static_cast<int>(width), static_cast<int>(height));
    // glClearColor(0.8f, 0.3f, 0.3f, 1.f);
    glClearColor(0.0f, 0.0f, 0.0f, 1.f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    const auto rect_count = static_cast<GLsizei>(m_rects.size());
    if (rect_count == 0)
        return;

    glDisable(GL_DEPTH_TEST);
    glDisable(GL_CULL_FACE);
    glEnable(GL_BLEND);
    glBlendEquation(GL_FUNC_ADD);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    glBindVertexArray(m_vao);
    glUseProgram(m_shader);
    glUniform2f(glGetUniformLocation(m_shader, "res"), width, height);
    glUniform1i(glGetUniformLocation(m_shader, "s_texture"), 0);
    glBindBuffer(GL_ARRAY_BUFFER, m_vbo);

    size_t start = 0;
    StringHandle current_texture = m_draw_textures[0];
    while (start < rect_count) {
        glActiveTexture(GL_TEXTURE0);
        m_textures[current_texture].bind();
        for (size_t i = start; i < rect_count; ++i) {
            StringHandle texture = m_draw_textures[i];
            if ((texture != current_texture)) {
                // Draw everything in [start, i - 1]
                const size_t count = i - start;
                glBufferData(GL_ARRAY_BUFFER,
                             static_cast<GLsizeiptr>(sizeof(Rect) * count),
                             &m_rects[start],
                             GL_STREAM_DRAW);
                glDrawArraysInstanced(GL_TRIANGLES, 0, 6, static_cast<GLsizei>(count));

                start = i;
                current_texture = texture;
                break;
            }

            if (i == rect_count - 1) {
                // just draw it
                const size_t count = rect_count - start;
                glBufferData(GL_ARRAY_BUFFER,
                             static_cast<GLsizeiptr>(sizeof(Rect) * count),
                             &m_rects[start],
                             GL_STREAM_DRAW);
                glDrawArraysInstanced(GL_TRIANGLES, 0, 6, static_cast<GLsizei>(count));

                start = rect_count;
                current_texture = texture;
            }
        }
    }
    m_rects.clear();
    m_draw_textures.clear();
}

void Renderer::getTextureSize(StringHandle texture, unsigned int* w, unsigned int* h)
{
    ZoneScoped;
    if (m_textures.find(texture) == m_textures.end()) {
        Texture tex;
        if (!AssetManager::ptr->loadTexture(StringRepository::global->getString(texture), &tex)) {
            ALOGE("Failed to load texture");
            return;
        }
        m_textures.insert(std::make_pair(texture, tex));
    }
    Texture tex = m_textures[texture];
    tex.getTextureSize(w, h);
}