#include <alloca.h>
#include <pch.hpp>
#include "logging.hpp"

#include <cstddef>
#include <fstream>

#include <opengl/opengl_shader.hpp>

#include <glad/glad.h>
#include <glm/glm.hpp>
#include <glm/gtc/type_ptr.hpp>

namespace OpenEngine {
    static GLenum ShaderTypeFromString(const std::string& type)
    {
        if (type == "vertex")
            return GL_VERTEX_SHADER;
        if (type == "fragment" || type == "pixel")
            return GL_FRAGMENT_SHADER;

        OE_CORE_ERROR("Could not compile shader! Unkown shader type: {}", type);

        return 0;
    }

    OpenGLShader::OpenGLShader(const std::string& name, const std::string& vertex_src, const std::string& fragment_src)
        : name(name)
    {
        std::unordered_map<GLenum, std::string> sources;
        sources[GL_VERTEX_SHADER] = vertex_src;
        sources[GL_FRAGMENT_SHADER] = fragment_src;

        Compile(sources);
    }

    OpenGLShader::OpenGLShader(const std::string& shader_path)
    {
        std::string source = ReadFile(shader_path);
        auto shader_sources = PreProcess(source);
        Compile(shader_sources);

        // Getting file name
        auto last_slash = shader_path.find_last_of("/\\");
        last_slash = last_slash == std::string::npos ? 0 : last_slash + 1;
        auto last_dot = shader_path.rfind('.');

        auto count = last_dot == std::string::npos ? shader_path.size() - last_slash : last_dot - last_slash;
        name = shader_path.substr(last_slash, count);
    }

    void OpenGLShader::Compile(const std::unordered_map<GLenum, std::string> sources)
    {
        GLuint program = glCreateProgram();
        GLenum *gl_shader_ids = (GLenum*)alloca(sources.size());

        size_t shader_index = 0;
		for (auto& kv_pair : sources)
		{
			GLenum type = kv_pair.first;
			const std::string& source = kv_pair.second;

			GLuint shader = glCreateShader(type);

			const GLchar* source_cstr = source.c_str();
			glShaderSource(shader, 1, &source_cstr, 0);

			glCompileShader(shader);

			GLint is_compiled = 0;
			glGetShaderiv(shader, GL_COMPILE_STATUS, &is_compiled);
			if (is_compiled == GL_FALSE)
			{
				GLint max_length = 0;
				glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &max_length);

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

				glDeleteShader(shader);

				OE_CORE_ERROR("{0}", info_log.data());
				OE_CORE_ERROR("Shader compilation failure!");
				break;
			}

			glAttachShader(program, shader);
			gl_shader_ids[shader_index++] = shader;
		}
		
		id = program;

		glLinkProgram(program);

		GLint is_linked = 0;
		glGetProgramiv(program, GL_LINK_STATUS, (int*)&is_linked);
		if (is_linked == GL_FALSE)
		{
			GLint max_length = 0;
			glGetProgramiv(program, GL_INFO_LOG_LENGTH, &max_length);

			// The maxLength includes the NULL character
			std::vector<GLchar> infoLog(max_length);
			glGetProgramInfoLog(program, max_length, &max_length, &infoLog[0]);

			// We don't need the program anymore.
			glDeleteProgram(program);
			
		    for (int i = 0; i < sources.size(); i++)
				glDeleteShader(gl_shader_ids[i]);

			OE_CORE_ERROR("{0}", infoLog.data());
			OE_CORE_ERROR("Shader link failure!");
			return;
		}

		for (int i = 0; i < sources.size(); i++)
			glDetachShader(program, gl_shader_ids[i]);
    }

    std::unordered_map<GLenum, std::string> OpenGLShader::PreProcess(const std::string& shader_source)
    {
        // TODO: Perhaps consider using getline to get the position of errors. or even regexes
        std::unordered_map<GLenum, std::string> sources;
        const char* type_token = "#type";

        size_t type_token_lenght = strlen(type_token);
        size_t pos = shader_source.find(type_token, 0);
        while (pos != std::string::npos) {
            size_t eol = shader_source.find("\n", pos);
            if (eol == std::string::npos)
                OE_CORE_ERROR("Syntax error.");
            size_t begin = pos + type_token_lenght + 1;
            std::string type = shader_source.substr(begin, eol - begin);
            if (type != "vertex" && type != "fragment" && type != "pixel")
                OE_CORE_ERROR("Invalid shader type.");

            size_t next_line_pos = shader_source.find_first_not_of("\n", eol);
            pos = shader_source.find(type_token, next_line_pos);
            sources[ShaderTypeFromString(type)] =
                shader_source.substr(next_line_pos, pos - (next_line_pos == std::string::npos ? shader_source.size() - 1 : next_line_pos));
        }

        return sources;
    }

    std::string OpenGLShader::ReadFile(const std::string& shader_path)
    {
        std::string result;

        std::ifstream input(shader_path);
        if (input) {
            input.seekg(0, std::ios::end);
            result.resize(input.tellg());
            input.seekg(0, std::ios::beg);
            input.read(&result[0], result.size());
            input.close();
        } else {
            OE_CORE_ERROR("Shader file could not be open or does not exist: {}", shader_path);
        }

        return result;
    }

    void OpenGLShader::Bind() const
    { 
        glUseProgram(id); 
    }

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

    void OpenGLShader::SetBool(const std::string &name, bool value) const
    {         
        glUniform1i(glGetUniformLocation(id, name.c_str()), (int)value); 
    }

    void OpenGLShader::SetInt(const std::string &name, int value) const
    { 
        glUniform1i(glGetUniformLocation(id, name.c_str()), value); 
    }

    void OpenGLShader::SetFloat(const std::string &name, float value) const
    { 
        glUniform1f(glGetUniformLocation(id, name.c_str()), value); 
    }
    void OpenGLShader::SetMat4(const std::string &name, const glm::mat4& value) const
    {
        glUniformMatrix4fv(glGetUniformLocation(id, name.c_str()), 1, GL_FALSE, glm::value_ptr(value)); 
    }
    void OpenGLShader::SetVec3(const std::string &name, const glm::vec3& value) const
    {
        glUniform3fv(glGetUniformLocation(id, name.c_str()), 1, glm::value_ptr(value)); 
    }

    void OpenGLShader::CheckCompileErrors(unsigned int shader, const std::string& type)
    {
        int success = GL_FALSE;
        char infoLog[1024];
        if (type != "PROGRAM") {
            glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
            if (success == GL_FALSE) {
                glGetShaderInfoLog(shader, 1024, NULL, infoLog);
                OE_CORE_ERROR("Shader of type {}, couldn't be compiled:", type);
                OE_CORE_ERROR("\t{}", infoLog);
            }
        } else {
            if (glGetProgramiv == nullptr) {
                OE_CORE_ERROR("OpenGL functions are not loaded!");
                return;
            }
            glGetProgramiv(shader, GL_LINK_STATUS, &success);
            if (success == GL_FALSE)
            {
                glGetProgramInfoLog(shader, 1024, NULL, infoLog);
                OE_CORE_ERROR("Program of type {}, couldn't be compiled:", type);
                OE_CORE_ERROR("\t{}", infoLog);
            }
        }
    }
}
