#include "pch.h"
#include "log.h"
#include "shader.h"
#include "asset.h"
#include "app.h"
#include "legacy_gl_shader_dispatch.h"
#include "renderer_gl/opengl_capabilities.h"
#include "renderer_gl/shader_bindings.h"

#include <cstdint>

std::map<kShader, Shader> ShaderManager::m_shaders;
Shader* ShaderManager::m_current;
pp::renderer::RenderDeviceFeatures ShaderManager::m_render_device_features {};
bool ShaderManager::ext_framebuffer_fetch = false;
bool ShaderManager::ext_float32 = false;
bool ShaderManager::ext_float32_linear = false;
bool ShaderManager::ext_float16 = false;
bool ShaderManager::ext_map_aligned = false;

std::string Shader::read(const std::string& path)
{
    Asset a;
    std::string ret;
    if (a.open(path.c_str()))
    {
        struct stat tmp_info;
        std::string abs_path = Asset::absolute(path);
        if (stat(abs_path.c_str(), &tmp_info) == 0)
            m_deps[path] = tmp_info;

        std::regex reg_include(R"!(#include "([^"]+)")!");
        std::string data((char*)a.read_all(), a.m_len);

        // split path
        std::string base;
        std::regex reg_path(R"((.*)[\\/]([^\\/]+)\.(\w+)$)");
        std::smatch m;
        if (std::regex_search(path, m, reg_path))
        {
            base = m[1].str();
        }

        for (const auto& l : split(data, '\n'))
        {
            std::smatch include_match;
            if (std::regex_search(l, include_match, reg_include))
            {
                std::string inc = base + "/" + include_match[1].str();
                if (Asset::exist(inc.c_str()))
                {
                    std::string subdata = read(inc);
                    ret.append(subdata + "\n");
                }
            }
            else
            {
                ret.append(l + "\n");
            }
        }
    }
    return ret;
}

void Shader::parse_error(const std::string& msg, const std::string& code)
{
    auto code_lines = split(code, '\n');
    auto error_lines = split(msg, '\n');
    std::smatch m;
    std::regex r(R"(\((\d+)\))");
    for (const auto& line : error_lines)
    {
        LOG("%s", line.c_str());
        if (std::regex_search(line, m, r))
        {
            int ln = std::stoi(m[1].str());
            if (ln < code_lines.size())
            {
                int n = 2;
                int s = std::max(ln - n, 1);
                int e = std::min((int)code_lines.size() - 1, ln + n);
                for (int i = s; i < e; i++)
                {
                    LOG("- line %02d: %s", i, code_lines[i - 1].c_str());
                }
            }
        }
    }
}

bool Shader::load(const std::string& path)
{
    std::string data = read(path);
    if (data.empty()) return false;

    std::map<std::string, std::shared_ptr<std::string>> sections;
    std::shared_ptr<std::string> current_section = nullptr;
    std::regex reg_section(R"!(\[\[(.*)\]\])!");
    for (const auto& l : split(data, '\n'))
    {
        std::smatch m;
        if (std::regex_search(l, m, reg_section))
        {
            std::string section_name = m[1].str();
            if (!sections[section_name])
                sections[section_name] = std::make_shared<std::string>();
            current_section = sections[section_name];
        }
        else
        {
            // create an un-named section
            if (!current_section)
                current_section = sections[""] = std::make_shared<std::string>();
            current_section->append(l + "\n");
        }
    }

    if (sections.find("vertex") != sections.end() && sections.find("fragment") != sections.end())
    {
        *sections["vertex"] = SHADER_VERSION + *sections["vertex"];
        *sections["fragment"] = SHADER_VERSION + *sections["fragment"];
        if (create(*sections["vertex"], *sections["fragment"]))
        {
            m_path = path;
            return true;
        }
    }
    else
    {
        LOG("could not find [[vertex]] and [[fragment]] sections on %s", path.c_str());
    }

    return false;
}

bool Shader::reload()
{
    if (m_path.empty())
        return false;

    bool dirty = false;
    struct stat tmp_info;
    for (auto& d : m_deps)
    {
        std::string abs_path = Asset::absolute(d.first);
        if (stat(abs_path.c_str(), &tmp_info) != 0)
            continue;
        if (tmp_info.st_mtime > d.second.st_mtime)
        {
            d.second = tmp_info;
            dirty = true;
        }
    }

    if (dirty)
    {
        LOG("reload shader %s", m_path.c_str());
        destroy();
        return load(m_path);
    }
    
    return false;
}

bool Shader::create(const std::string& vertex, const std::string& fragment)
{
    bool ret = true;
    App::I->render_task([this, &ret, vertex, fragment]
    {
        static char infolog[4096];
        const auto vertex_shader = pp::renderer::gl::compile_opengl_shader_source(
            pp::renderer::gl::vertex_shader_stage(),
            vertex.c_str(),
            infolog,
            static_cast<std::int32_t>(sizeof(infolog)),
            pp::legacy::gl_shader::shader_compile_dispatch());
        if (!vertex_shader.ok())
        {
            ret = false;
            LOG("Shader::create() vertex compile failed because: %s", vertex_shader.status().message);
            return;
        }
        if (vertex_shader.value().info_log_length > 0)
        {
            LOG("\nVERTEX SHADER: %s", m_path.c_str());
            parse_error(infolog, vertex);
        }
        if (vertex_shader.value().compile_status == 0)
        {
            const auto status = pp::renderer::gl::delete_opengl_shader(
                vertex_shader.value().shader_id,
                pp::legacy::gl_shader::shader_delete_dispatch());
            if (!status.ok())
                LOG("Shader::create() vertex shader cleanup failed because: %s", status.message);
            ret = false;
            return;
        }

        const auto fragment_shader = pp::renderer::gl::compile_opengl_shader_source(
            pp::renderer::gl::fragment_shader_stage(),
            fragment.c_str(),
            infolog,
            static_cast<std::int32_t>(sizeof(infolog)),
            pp::legacy::gl_shader::shader_compile_dispatch());
        if (!fragment_shader.ok())
        {
            const auto status = pp::renderer::gl::delete_opengl_shader(
                vertex_shader.value().shader_id,
                pp::legacy::gl_shader::shader_delete_dispatch());
            if (!status.ok())
                LOG("Shader::create() vertex shader cleanup failed because: %s", status.message);
            ret = false;
            LOG("Shader::create() fragment compile failed because: %s", fragment_shader.status().message);
            return;
        }
        if (fragment_shader.value().info_log_length > 0)
        {
            LOG("\nFRAGMENT SHADER: %s", m_path.c_str());
            parse_error(infolog, fragment);
        }
        if (fragment_shader.value().compile_status == 0)
        {
            const auto vertex_cleanup = pp::renderer::gl::delete_opengl_shader(
                vertex_shader.value().shader_id,
                pp::legacy::gl_shader::shader_delete_dispatch());
            const auto fragment_cleanup = pp::renderer::gl::delete_opengl_shader(
                fragment_shader.value().shader_id,
                pp::legacy::gl_shader::shader_delete_dispatch());
            if (!vertex_cleanup.ok())
                LOG("Shader::create() vertex shader cleanup failed because: %s", vertex_cleanup.message);
            if (!fragment_cleanup.ok())
                LOG("Shader::create() fragment shader cleanup failed because: %s", fragment_cleanup.message);
            ret = false;
            return;
        }

        const auto program = pp::renderer::gl::link_opengl_shader_program(
            vertex_shader.value().shader_id,
            fragment_shader.value().shader_id,
            pp::renderer::gl::panopainter_shader_attribute_bindings(),
            infolog,
            static_cast<std::int32_t>(sizeof(infolog)),
            pp::legacy::gl_shader::program_link_dispatch());
        if (!program.ok())
        {
            const auto vertex_cleanup = pp::renderer::gl::delete_opengl_shader(
                vertex_shader.value().shader_id,
                pp::legacy::gl_shader::shader_delete_dispatch());
            const auto fragment_cleanup = pp::renderer::gl::delete_opengl_shader(
                fragment_shader.value().shader_id,
                pp::legacy::gl_shader::shader_delete_dispatch());
            if (!vertex_cleanup.ok())
                LOG("Shader::create() vertex shader cleanup failed because: %s", vertex_cleanup.message);
            if (!fragment_cleanup.ok())
                LOG("Shader::create() fragment shader cleanup failed because: %s", fragment_cleanup.message);
            ret = false;
            LOG("Shader::create() program link failed because: %s", program.status().message);
            return;
        }
        if (program.value().info_log_length > 0)
            LOG("LINK SHADER: %s\n%s", m_path.c_str(), infolog);
        if (program.value().link_status == 0)
        {
            const auto status = pp::renderer::gl::delete_opengl_program(
                program.value().program_id,
                pp::legacy::gl_shader::program_delete_dispatch());
            if (!status.ok())
                LOG("Shader::create() program cleanup failed because: %s", status.message);
            ret = false;
            return;
        }

        const auto cleanup_program = [](std::uint32_t program_id) noexcept {
            const auto status = pp::renderer::gl::delete_opengl_program(
                program_id,
                pp::legacy::gl_shader::program_delete_dispatch());
            if (!status.ok())
                LOG("Shader::create() program cleanup failed because: %s", status.message);
        };

        // Parse shader uniforms
        {
            const auto uniform_count = pp::renderer::gl::query_opengl_program_integer(
                program.value().program_id,
                pp::renderer::gl::active_uniform_count_query(),
                pp::legacy::gl_shader::program_integer_dispatch());
            if (!uniform_count.ok())
            {
                LOG("Shader::create() uniform discovery failed because: %s", uniform_count.status().message);
                cleanup_program(program.value().program_id);
                ret = false;
                return;
            }
            constexpr std::int32_t bufSize = 64; // maximum name length
            char uniform_name[bufSize]; // variable name in GLSL
            const auto count = uniform_count.value();
            for (int i = 0; i < count; i++)
            {
                const auto uniform = pp::renderer::gl::get_opengl_active_uniform(
                    program.value().program_id,
                    static_cast<std::uint32_t>(i),
                    uniform_name,
                    bufSize,
                    pp::legacy::gl_shader::active_uniform_dispatch());
                if (!uniform.ok())
                {
                    LOG("Shader::create() active uniform discovery failed because: %s", uniform.status().message);
                    cleanup_program(program.value().program_id);
                    ret = false;
                    return;
                }
                kShaderUniform id = static_cast<kShaderUniform>(pp::renderer::gl::shader_uniform_id(uniform_name));
                if (m_umap.find(id) != m_umap.end())
                    LOG("UNIFORM ALREADY DEFINED: %s", uniform_name);
                const auto location = pp::renderer::gl::get_opengl_uniform_location(
                    program.value().program_id,
                    uniform_name,
                    pp::legacy::gl_shader::uniform_location_dispatch());
                if (!location.ok())
                {
                    LOG("Shader::create() uniform location failed because: %s", location.status().message);
                    cleanup_program(program.value().program_id);
                    ret = false;
                    return;
                }
                m_umap[id] = location.value();
            }
        }

        prog = program.value().program_id;
    });
    
    return ret;
}

void Shader::destroy()
{
    if (prog)
    {
        App::I->render_task_async([prog=prog]
        {
            const auto status = pp::renderer::gl::delete_opengl_program(
                static_cast<std::uint32_t>(prog),
                pp::legacy::gl_shader::program_delete_dispatch());
            if (!status.ok())
                LOG("Shader::destroy() failed because: %s", status.message);
        });
        prog = 0;
    }
    m_umap.clear();
}

void Shader::use()
{
    const auto status = pp::renderer::gl::use_opengl_program(
        static_cast<std::uint32_t>(prog),
        pp::legacy::gl_shader::program_use_dispatch());
    if (!status.ok())
        LOG("Shader::use() failed because: %s", status.message);
}
void Shader::u_vec4(kShaderUniform id, const glm::vec4& v)
{
    if (m_umap.count(id) == 0)
        LOG("UNIFORM vec4 %d NOT FOUND in shader %d", (int)id, (int)name)
    else
    {
        const auto status = pp::renderer::gl::set_opengl_uniform_vec4(
            static_cast<std::int32_t>(m_umap[id]),
            glm::value_ptr(v),
            pp::legacy::gl_shader::uniform_vec4_dispatch());
        if (!status.ok())
            LOG("Shader::u_vec4() failed because: %s", status.message);
    }
}
void Shader::u_vec3(kShaderUniform id, const glm::vec3& v)
{
    if (m_umap.count(id) == 0)
        LOG("UNIFORM vec3 %d NOT FOUND in shader %d", (int)id, (int)name)
    else
    {
        const auto status = pp::renderer::gl::set_opengl_uniform_vec3(
            static_cast<std::int32_t>(m_umap[id]),
            glm::value_ptr(v),
            pp::legacy::gl_shader::uniform_vec3_dispatch());
        if (!status.ok())
            LOG("Shader::u_vec3() failed because: %s", status.message);
    }
}

void Shader::u_vec2(kShaderUniform id, const glm::vec2& v)
{
    if (m_umap.count(id) == 0)
        LOG("UNIFORM vec2 %d NOT FOUND in shader %d", (int)id, (int)name)
    else
    {
        const auto status = pp::renderer::gl::set_opengl_uniform_vec2(
            static_cast<std::int32_t>(m_umap[id]),
            glm::value_ptr(v),
            pp::legacy::gl_shader::uniform_vec2_dispatch());
        if (!status.ok())
            LOG("Shader::u_vec2() failed because: %s", status.message);
    }
}

void Shader::u_mat4(kShaderUniform id, const glm::mat4& m)
{
    if (m_umap.count(id) == 0)
        LOG("UNIFORM mat4 %d NOT FOUND in shader %d", (int)id, (int)name)
    else
    {
        const auto status = pp::renderer::gl::set_opengl_uniform_mat4(
            static_cast<std::int32_t>(m_umap[id]),
            glm::value_ptr(m),
            pp::legacy::gl_shader::uniform_mat4_dispatch());
        if (!status.ok())
            LOG("Shader::u_mat4() failed because: %s", status.message);
    }
}
void Shader::u_int(kShaderUniform id, int i)
{
    if (m_umap.count(id) == 0)
        LOG("UNIFORM int %d NOT FOUND in shader %d", (int)id, (int)name)
    else
    {
        const auto status = pp::renderer::gl::set_opengl_uniform_int(
            static_cast<std::int32_t>(m_umap[id]),
            static_cast<std::int32_t>(i),
            pp::legacy::gl_shader::uniform_int_dispatch());
        if (!status.ok())
            LOG("Shader::u_int() failed because: %s", status.message);
    }
}
void Shader::u_int(const char* uniform_name, int i)
{
    const auto location = pp::renderer::gl::get_opengl_attribute_location(
        static_cast<std::uint32_t>(prog),
        uniform_name,
        pp::legacy::gl_shader::attribute_location_dispatch());
    if (!location.ok())
    {
        LOG("Shader::u_int(name) lookup failed because: %s", location.status().message);
        return;
    }
    const auto status = pp::renderer::gl::set_opengl_uniform_int(
        location.value(),
        static_cast<std::int32_t>(i),
        pp::legacy::gl_shader::uniform_int_dispatch());
    if (!status.ok())
        LOG("Shader::u_int(name) failed because: %s", status.message);
}
void Shader::u_float(kShaderUniform id, float f)
{
    if (m_umap.count(id) == 0)
        LOG("UNIFORM float %d NOT FOUND in shader %d", (int)id, (int)name)
    else
    {
        const auto status = pp::renderer::gl::set_opengl_uniform_float(
            static_cast<std::int32_t>(m_umap[id]),
            f,
            pp::legacy::gl_shader::uniform_float_dispatch());
        if (!status.ok())
            LOG("Shader::u_float() failed because: %s", status.message);
    }
}
GLint Shader::GetAttribLocation(const char* attribute_name)
{
    const auto location = pp::renderer::gl::get_opengl_attribute_location(
        static_cast<std::uint32_t>(prog),
        attribute_name,
        pp::legacy::gl_shader::attribute_location_dispatch());
    if (!location.ok())
    {
        LOG("Shader::GetAttribLocation() failed because: %s", location.status().message);
        return -1;
    }
    return static_cast<GLint>(location.value());
}

bool ShaderManager::load(kShader id, const std::string& path)
{
    m_shaders[id].name = id;
    return m_shaders[id].load(path);
}

bool ShaderManager::reload()
{
    bool success = false;
    for (auto& s : m_shaders)
        success |= s.second.reload();
    return success;
}

bool ShaderManager::create(kShader id, const std::string& vertex, const std::string& fragment)
{
    m_shaders[id].name = id;
    return m_shaders[id].create(vertex, fragment);
}

void ShaderManager::use(kShader id)
{
    m_current = &m_shaders[id];
    m_current->use();
}

void ShaderManager::use(const char* name)
{
    m_current = &m_shaders[(kShader)const_hash(name)];
    m_current->use();
}

void ShaderManager::u_vec4(kShaderUniform id, const glm::vec4& v)
{
    m_current->u_vec4(id, v);
}

void ShaderManager::u_vec3(kShaderUniform id, const glm::vec3& v)
{
    m_current->u_vec3(id, v);
}

void ShaderManager::u_vec2(kShaderUniform id, const glm::vec2& v)
{
    m_current->u_vec2(id, v);
}

void ShaderManager::u_mat4(kShaderUniform id, const glm::mat4& m)
{
    m_current->u_mat4(id, m);
}

void ShaderManager::u_int(kShaderUniform id, int i)
{
    m_current->u_int(id, i);
}

void ShaderManager::u_int(const char* name, int i)
{
    m_current->u_int(name, i);
}

Shader* ShaderManager::get(kShader id)
{
    auto it = m_shaders.find(id);
    return (it == m_shaders.end()) ? nullptr : &it->second;
}

void ShaderManager::u_float(kShaderUniform id, float f)
{
    m_current->u_float(id, f);
}

void ShaderManager::set_render_device_features(pp::renderer::RenderDeviceFeatures features) noexcept
{
    m_render_device_features = features;
}

pp::renderer::RenderDeviceFeatures ShaderManager::render_device_features() noexcept
{
    return m_render_device_features;
}

void ShaderManager::invalidate()
{
    m_shaders.clear();
}

bool check_uniform_uniqueness()
{
    return pp::renderer::gl::validate_shader_uniform_names(
               pp::renderer::gl::panopainter_shader_uniform_names())
        .ok();
}
