/******************************************************************************
*	File		-	shader.cpp
*	Author		-	Joey Pollack
*	Date		-	2022/08/04 (y/m/d)
*	Mod Date	-	2022/08/04 (y/m/d)
*	Description	-	opengl shader system
******************************************************************************/

#include "shader.h"
#include <utils/logger.h>

#include <glad/gl.h>
#include <string>

namespace lunarium
{
    Shader::Shader(const char* vert_source, const char* geo_source, const char* frag_source)
        : mGLID(0)
    {
        // Validate parameters
        if (!vert_source)
        {
            Logger::Error(LogCategory::GRAPHICS, "Failed to create Shader - vert_source can not be nullptr!");
            return;
        }

        if (!frag_source)
        {
            Logger::Error(LogCategory::GRAPHICS, "Failed to create Shader - vert_source can not be nullptr!");
            return;
        }

        // Compile source code
        u32 vert_id = CompileSource(vert_source, GL_VERTEX_SHADER);
        u32 geo_id = (geo_source ? CompileSource(geo_source, GL_GEOMETRY_SHADER) : 0 );
        u32 frag_id = CompileSource(frag_source, GL_FRAGMENT_SHADER);

        // Validate compilation
        if (vert_id < 1 || frag_id < 1)
        {
            // Free any succesfully compiled shader sources
            if (vert_id > 0) glDeleteShader(vert_id);
            if (geo_id > 0) glDeleteShader(geo_id);
            if (frag_id > 0) glDeleteShader(frag_id);

            Logger::Error(LogCategory::GRAPHICS, "Failed to create shader program");
            return;
        }

        // Link shader program
        mGLID = glCreateProgram();
        glAttachShader(mGLID, vert_id);
        if (geo_id > 0) glAttachShader(mGLID, geo_id);
        glAttachShader(mGLID, frag_id);
        glLinkProgram(mGLID);

        // check for linking errors
        int success;
        const int buffer_size = 512;
        char info_log[buffer_size];
        glGetProgramiv(mGLID, GL_LINK_STATUS, &success);
        if (!success) 
        {
            glGetProgramInfoLog(mGLID, 512, NULL, info_log);
            Logger::Error(LogCategory::GRAPHICS, "Failed to link shader program: %s", info_log);
            glDeleteProgram(mGLID);
            mGLID = 0;
        }

        // Clean up
        if (vert_id > 0) glDeleteShader(vert_id);
        if (geo_id > 0) glDeleteShader(geo_id);
        if (frag_id > 0) glDeleteShader(frag_id);
    }

    Shader::~Shader()
    {
        if (IsValid())
        {
            glDeleteProgram(mGLID);
            mGLID = 0;
        }
    }

    bool Shader::IsValid() const
    {
        return mGLID > 0;
    }

    
    void Shader::Use() const
    {
        glUseProgram(mGLID);
    }

    OpRes Shader::GetAllUniforms(std::vector<Uniform>& uniforms)
    {
        uniforms.clear();

        GLint size; // size of the variable
		GLenum type; // type of the variable (float, vec3 or mat4, etc)

		const GLsizei buffer_size = 16; // maximum name length
		GLchar name[buffer_size]; // variable name in GLSL
		GLsizei length; // name length
		int count = -1;

		glGetProgramiv(mGLID, GL_ACTIVE_UNIFORMS, &count);
		Logger::Debug(LogCategory::GRAPHICS, "Active Uniforms for shader %n: %n", mGLID, count);

		for (int i = 0; i < count; i++)
		{
			glGetActiveUniform(mGLID, (GLuint)i, buffer_size, &length, &size, &type, name);
            u32 location = glGetUniformLocation(mGLID, name);
			Logger::Debug(LogCategory::GRAPHICS, "Uniform #%n Type: %u Name: %s Location: %n", i, type, name, location);
            Uniform u;
            u.Location = location;
            u.Type = GLToUniformType(type);
            u.Name = std::string(name);
            uniforms.push_back(u);
		}

        return OpRes::OK();
    }


    void Shader::GetUniformLocation(Uniform& uniform)
    {
        uniform.Location = glGetUniformLocation(mGLID, uniform.Name.c_str());
    }


    OpRes Shader::SetUniform(Uniform& uniform, void* values)
    {
        if (uniform.Location == -1)
        {
            GetUniformLocation(uniform);
            if (uniform.Location == -1)
            {
                return OpRes::Fail("Could not find location for uniform: %s", uniform.Name.c_str());
            }
        }

        switch (uniform.Type)
        {
            case UniformType::F1: glUniform1fv(uniform.Location, 1, (GLfloat*)values); break;
            case UniformType::F2: glUniform2fv(uniform.Location, 1, (GLfloat*)values); break;
            case UniformType::F3: glUniform3fv(uniform.Location, 1, (GLfloat*)values); break;
            case UniformType::F4: glUniform4fv(uniform.Location, 1, (GLfloat*)values); break;

            case UniformType::I1: glUniform1iv(uniform.Location, 1, (GLint*)values); break;
            case UniformType::I2: glUniform2iv(uniform.Location, 1, (GLint*)values); break;
            case UniformType::I3: glUniform3iv(uniform.Location, 1, (GLint*)values); break;
            case UniformType::I4: glUniform4iv(uniform.Location, 1, (GLint*)values); break;

            case UniformType::FMAT3: glUniformMatrix3fv(uniform.Location, 1, GL_FALSE, (GLfloat*)values); break;
            case UniformType::FMAT4: glUniformMatrix4fv(uniform.Location, 1, GL_FALSE, (GLfloat*)values); break;

            default: return OpRes::Fail("Can not set uniform value - Unknown type");
        }

        return OpRes::OK();
    }


    u32 Shader::CompileSource(const char* source, u32 source_type)
    {
        u32 id = glCreateShader(source_type);
        glShaderSource(id, 1, &source, NULL);
		glCompileShader(id);

        int success;
        const int buffer_size = 512;
        char info_log[buffer_size];
        glGetShaderiv(id, GL_COMPILE_STATUS, &success);
        if (!success)
        {
            glGetShaderInfoLog(id, buffer_size, NULL, info_log);
            std::string type = "";
            switch (source_type)
            {
                case GL_VERTEX_SHADER: type = "Vertex"; break;
                case GL_GEOMETRY_SHADER: type = "Geometry"; break;
                case GL_FRAGMENT_SHADER: type = "Fragment"; break;
            }
            Logger::Error(LogCategory::GRAPHICS, "Failed to compile %s shader source: %s", type.c_str(), info_log); 
            return 0;
        }

        return id;
    }

    
    UniformType Shader::GLToUniformType(u32 type)
    {
        switch (type)
        {
            case GL_FLOAT: return UniformType::F1;
            case GL_FLOAT_VEC2: return UniformType::F2;
            case GL_FLOAT_VEC3: return UniformType::F3;
            case GL_FLOAT_VEC4: return UniformType::F4;

            case GL_INT: return UniformType::I1;
            case GL_INT_VEC2: return UniformType::I2;
            case GL_INT_VEC3: return UniformType::I3;
            case GL_INT_VEC4: return UniformType::I4;

            case GL_FLOAT_MAT3: return UniformType::FMAT3;
            case GL_FLOAT_MAT4: return UniformType::FMAT4;
        }

        Logger::Warn(LogCategory::GRAPHICS, "Can not convert GL uniform type - Unknown type: %u", type);
        return UniformType::UNKNOWN;
    }
}