//
// Created by sfd on 25-4-24.
//

#include "OpenGLShader.h"

#include <fstream>
#include <vector>
#include <array>
#include <Hazel/Core/Log.h>
#include <Hazel/Debug/Instrumentor.h>

#include "glm/gtc/type_ptr.hpp"
#include "spdlog/fmt/bundled/compile.h"

namespace Hazel
{
    static GLenum ShaderTypeFromString(const std::string& type)
    {
        if (type == "vertex") return GL_VERTEX_SHADER;
        if (type == "fragment" || type == "pixel") return GL_FRAGMENT_SHADER;
        if (type == "geometry") return GL_GEOMETRY_SHADER;
        if (type == "compute") return GL_COMPUTE_SHADER;
        HZ_CORE_ERROR("Unknown Shader Type");
        return 0;
    }


    OpenGLShader::OpenGLShader(const std::string& filepath)
    {
        HZ_PROFILE_FUNCTION();
        std::string shaderSrc = ReadFile(filepath);
        auto shaderSources = PreProcess(shaderSrc);
        Compile(shaderSources);

        // example: /assets/shaders/Texture.glsl
        auto lastSlash = filepath.find_last_of("\\/");
        auto lastDot = filepath.find_last_of(".");
        m_Name = filepath.substr(lastSlash + 1, lastDot - lastSlash - 1);


        /*
        auto lastSlash = filepath.find_last_of("\\/");
        lastSlash = lastSlash == std::string::npos ? 0 : lastSlash + 1;
        auto lastDot = filepath.rfind(".");
        auto count = lastDot == std::string::npos ? filepath.size() - lastSlash : lastDot - lastSlash;
        m_Name = filepath.substr(lastSlash, count);
    */
    }

    OpenGLShader::OpenGLShader(const std::string& name, const std::string& vertexSrc, const std::string& fragmentSrc): m_Name(name)
    {
        HZ_PROFILE_FUNCTION();
        std::unordered_map<GLenum, std::string> shaderSources;
        shaderSources[GL_VERTEX_SHADER] = vertexSrc;
        shaderSources[GL_FRAGMENT_SHADER] = fragmentSrc;
        Compile(shaderSources);
    }


    OpenGLShader::~OpenGLShader()
    {
        HZ_PROFILE_FUNCTION();
        glDeleteProgram(m_RendererID);
    }

    void OpenGLShader::Bind() const
    {
        HZ_PROFILE_FUNCTION();
        glUseProgram(m_RendererID);
    }

    void OpenGLShader::Unbind() const
    {
        HZ_PROFILE_FUNCTION();
        glUseProgram(0);
    }

    void OpenGLShader::SetFloat(const std::string& name, const float value)
    {
        HZ_PROFILE_FUNCTION();
        UploadUniformFloat(name, value);
    }

    void OpenGLShader::SetFloat3(const std::string& name, const glm::vec3& value)
    {
        HZ_PROFILE_FUNCTION();
        UploadUniformFloat3(name, value);
    }

    void OpenGLShader::SetFloat4(const std::string& name, const glm::vec4& value)
    {
        HZ_PROFILE_FUNCTION();
        UploadUniformFloat4(name, value);
    }

    void OpenGLShader::SetMat3(const std::string& name, const glm::mat3& value)
    {
        HZ_PROFILE_FUNCTION();
        UploadUniformMat3(name, value);
    }

    void OpenGLShader::SetMat4(const std::string& name, const glm::mat4& value)
    {
        HZ_PROFILE_FUNCTION();
        UploadUniformMat4(name, value);
    }

    void OpenGLShader::SetInt(const std::string& name, int value)
    {
        HZ_PROFILE_FUNCTION();
        UploadUniformInt(name, value);
    }

    void OpenGLShader::SetIntv(const std::string& name, int* values, uint32_t count)
    {
        HZ_PROFILE_FUNCTION();
        UploadUniformIntv(name, values, count);
    }


    std::string OpenGLShader::ReadFile(const std::string& filepath)
    {
        HZ_PROFILE_FUNCTION();
        std::ifstream in(filepath, std::ios::in | std::ios::binary);
        // std::ifstream in(filepath, std::ios::in, std::ios::binary); // window platform api
        if (!in.is_open())
        {
            HZ_CORE_ERROR("Failed to open shader file {0}", filepath);
            return "";
        }

        std::string result;
        in.seekg(0, std::ios::end);
        result.resize(in.tellg());
        in.seekg(0, std::ios::beg);
        in.read(&result[0], result.size());
        in.close();

        return result;
    }

    std::unordered_map<GLenum, std::string> OpenGLShader::PreProcess(const std::string& source)
    {
        HZ_PROFILE_FUNCTION();
        std::unordered_map<GLenum, std::string> shaderSource;
        const char* typeToken = "#type";
        size_t typeTokenLength = strlen(typeToken);
        size_t pos = source.find(typeToken, 0);

        while (pos != std::string::npos)
        {
            size_t eol = source.find_first_of("\r\n", pos);
            if (eol == std::string::npos)
            {
                HZ_CORE_ERROR("[{0}:{1}]: Failed to parse shader source", __FILE__, __LINE__);
                return shaderSource;
            }
            size_t start = pos + typeTokenLength + 1;
            std::string type = source.substr(start, eol - start);
            if (!ShaderTypeFromString(type))
            {
                HZ_CORE_ERROR("[{0}:{1}]: Unsupported shader type", __FILE__, __LINE__);
                return shaderSource;
            }

            size_t nextLinePos = source.find_first_of("\r\n", eol);
            pos = source.find(typeToken, nextLinePos);
            shaderSource[ShaderTypeFromString(type)] = source.substr(nextLinePos, pos - (nextLinePos == std::string::npos ? source.size() - 1 : nextLinePos));

        }
        return shaderSource;

    }

    void OpenGLShader::Compile(std::unordered_map<GLenum, std::string>& shaderSource)
    {
        HZ_PROFILE_FUNCTION();
        GLuint program = glCreateProgram();
        // std::vector<GLenum> glShaderIDs;
        // glShaderIDs.reserve(shaderSource.size());

        if (shaderSource.size() > 2)
        {
            HZ_CORE_ERROR("Only support 2 shaders Now!");
        }
        std::array<GLenum, 2> glShaderIDs;
        int glShaderIDIndex = 0;
        for (auto& shaderSource : shaderSource)
        {
            GLenum type = shaderSource.first;
            const std::string source = shaderSource.second;

            GLuint shader = glCreateShader(type);

            const char* shaderSrc = source.c_str();
            glShaderSource(shader, 1, &shaderSrc, 0);

            glCompileShader(shader);

            GLint compileSuccess = 0;
            glGetShaderiv(shader, GL_COMPILE_STATUS, &compileSuccess);
            if (compileSuccess == GL_FALSE)
            {
                int length = 0;
                glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);

                std::vector<GLchar> infoLog(length);
                glGetShaderInfoLog(shader, length, &length, &infoLog[0]);

                glDeleteShader(shader);
                HZ_CORE_ERROR("shader compilation failed!");
                HZ_CORE_ERROR("{0}", infoLog.data());
                return;
            }

            glAttachShader(program, shader);
            glShaderIDs[glShaderIDIndex++] = shader;
        }



        glLinkProgram(program);

        GLint compileSuccess = 0;
        glGetProgramiv(program, GL_LINK_STATUS, &compileSuccess);
        if (compileSuccess == GL_FALSE)
        {
            int length = 0;
            glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
            std::vector<GLchar> infoLog(length);
            glGetProgramInfoLog(program, length, &length, &infoLog[0]);
            for (const auto id : glShaderIDs)
            {
                glDeleteShader(id);
            }

            HZ_CORE_ERROR("shaderProgram link failed!");
            HZ_CORE_ERROR("{0}", infoLog.data());
            return;
        }

        for (const auto id : glShaderIDs)
        {
            glDetachShader(program, id);
            glDeleteShader(id);
        }

        m_RendererID = program;
    }

    void OpenGLShader::UploadUniformFloat3(const std::string& name, const glm::vec3& value) const
    {
        GLint location = glGetUniformLocation(m_RendererID, name.c_str());
        glUniform4fv(location, 1, glm::value_ptr(value));
    }

    void OpenGLShader::UploadUniformFloat4(const std::string& name, const glm::vec4& value) const
    {
        GLint location = glGetUniformLocation(m_RendererID, name.c_str());
        glUniform4fv(location, 1, glm::value_ptr(value));
    }

    void OpenGLShader::UploadUniformMat3(const std::string& name, const glm::mat3& matrix) const
    {
        GLint location = glGetUniformLocation(m_RendererID, name.c_str());
        glUniformMatrix3fv(location, 1, GL_FALSE, glm::value_ptr(matrix));
    }

    void OpenGLShader::UploadUniformMat4(const std::string& name, const glm::mat4& matrix) const
    {
        GLint location = glGetUniformLocation(m_RendererID, name.c_str());
        glUniformMatrix4fv(location, 1, GL_FALSE, glm::value_ptr(matrix));
    }

    void OpenGLShader::UploadUniformInt(const std::string& name, const int value) const
    {
        const GLint location = glGetUniformLocation(m_RendererID, name.c_str());
        glUniform1i(location, value);
    }

    void OpenGLShader::UploadUniformIntv(const std::string& name, int* values, uint32_t count) const
    {
        const GLint location = glGetUniformLocation(m_RendererID, name.c_str());
        glUniform1iv(location, count, values);
    }

    void OpenGLShader::UploadUniformFloat(const std::string& name, const float value) const
    {
        const GLint location = glGetUniformLocation(m_RendererID, name.c_str());
        glUniform1f(location, value);
    }
}
