/*
 * Copyright 2023 Hartmut Seichter
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 */
#include "paradiso/shader.hpp"

#include "glad/glad.h"

#include <iostream>

namespace paradiso {

struct Shader::impl {
    std::reference_wrapper<Shader> _shader;

    GLuint _shader_program;
    std::vector<GLuint> _shader_stages;

    impl(Shader& s) : _shader(s) {}

    ~impl() { destroy(); }

    bool is_valid() {
        // we potentially haul in is_valid while no context is given
        return glIsProgram != nullptr && glIsProgram(_shader_program);
    }

    bool build() {
        //        if (!is_valid()) return false;

        for (const auto& [type, code] : _shader.get().source_) {
            GLuint shader_type = 0;
            switch (type) {
            case Shader::Type::Vertex:
                shader_type = GL_VERTEX_SHADER;
                break;
            case Shader::Type::Compute:
                shader_type = GL_COMPUTE_SHADER;
                break;
            case Shader::Type::Geometry:
                shader_type = GL_GEOMETRY_SHADER;
                break;
            case Shader::Type::Fragment:
                shader_type = GL_FRAGMENT_SHADER;
                break;
            default:
                std::cerr << " unknown shader type";
            }

            GLuint shaderId = glCreateShader(shader_type);

            char* src = const_cast<char*>(code.c_str());
            GLint size = static_cast<GLint>(code.length());

            glShaderSource(shaderId, 1, &src, &size);

            glCompileShader(shaderId);

            GLint is_compiled = GL_FALSE;
            glGetShaderiv(shaderId, GL_COMPILE_STATUS, &is_compiled);
            if (is_compiled == GL_FALSE) {

                GLint log_length;

                glGetShaderiv(shaderId, GL_INFO_LOG_LENGTH, &log_length);

                std::vector<char> log_buffer(static_cast<size_t>(log_length));

                glGetShaderInfoLog(shaderId, log_length, &log_length,
                                   log_buffer.data());

                // TODO - handle errors!
                std::cerr << log_buffer.data();

                return false;
            }

            _shader_stages.push_back(shaderId);
        }

        _shader_program = glCreateProgram();

        for (auto s : _shader_stages)
            glAttachShader(_shader_program, s);

        // TODO attribute binding ...

        /* Bind attribute index 0 (coordinates) to in_Position and attribute
         * index 1 (color) to in_Color */
        /* Attribute locations must be setup before calling glLinkProgram. */
        //		 glBindAttribLocation(shaderprogram, 0, "in_Position");
        //		 glBindAttribLocation(shaderprogram, 1, "in_Color");

        glLinkProgram(_shader_program);

        GLint is_linked = 0;
        glGetProgramiv(_shader_program, GL_LINK_STATUS, &is_linked);
        if (is_linked == GL_FALSE) {

            GLint log_length;

            /* Noticed that glGetProgramiv is used to get the length for a
             * shader program, not glGetShaderiv. */
            glGetProgramiv(_shader_program, GL_INFO_LOG_LENGTH, &log_length);

            /* The maxLength includes the NULL character */
            std::vector<char> info_log(static_cast<size_t>(log_length));

            /* Notice that glGetProgramInfoLog, not glGetShaderInfoLog. */
            glGetProgramInfoLog(_shader_program, log_length, &log_length,
                                info_log.data());

            std::cerr << info_log.data();

            /* Handle the error in an appropriate way such as displaying a
             * message or writing to a log file. */
            /* In this simple program, we'll just leave */

            return false;
        }

        return true;
    }

    void use() const { glUseProgram(_shader_program); }

    void destroy() {
        // potentially the GL driver hasn't been loaded
        if (is_valid()) {

            // deleting and detaching should happen much earlier

            for (auto s : _shader_stages) {
                glDeleteShader(s);
            }

            // only program needs to be deleted

            glDeleteProgram(_shader_program);
        }
    }

    int uniform_location(std::string const& name) const {
        return glGetUniformLocation(_shader_program, name.c_str());
    }

    // void bind(int location,const matrix3x3f& m)
    // {
    // 	glUniformMatrix3fv(location,1,GL_FALSE,m.ptr());
    // }

    // void bind(int location,const matrix4x4f& m)
    // {
    // 	glUniformMatrix4fv(location,1,GL_FALSE,m.ptr());
    // }

    // void bind(int location,const vector4f& v)
    // {
    // 	glUniform4fv(location,1,v.ptr());
    // }

    void bind(int location, const float& v) const { glUniform1f(location, v); }

    void bind(int location, const uint32_t& i) const { glUniform1ui(location, i); }

    void bind(int location, const int32_t& i) const { glUniform1i(location, i); }

    // void bind(int location,const texture& v)
    // {
    //     this->bind(location,(int)v.native_handle());

    // }
};

Shader::Shader() { impl_ = std::make_unique<impl>(*this); }

Shader::~Shader() {}

bool Shader::ready() const { return impl_->is_valid(); }

Shader& Shader::set_uniform_at_location(int location, float v) {
    impl_->bind(location, v);
    return *this;
}

Shader& Shader::set_uniform_at_location(int location, uint32_t v) {
    impl_->bind(location, v);
    return *this;
}

const Shader& Shader::set_uniform_at_location(int location, int32_t v) const {
    impl_->bind(location, v);
    return *this;
}

// Shader &Shader::set_uniform_at_location(int location, vector4f const &v)
// {
//     impl_->bind(location, v);
//     return *this;
// }

// Shader &Shader::set_uniform_at_location(int location, matrix4x4f const &v)
// {
//     impl_->bind(location, v);
//     return *this;
// }

// Shader &Shader::set_uniform_at_location(int location, texture const &v)
// {
//     impl_->bind(location, v);
//     return *this;
// }

bool Shader::build() { return impl_->build(); }

void Shader::use() const { impl_->use(); }

void Shader::set_uniforms(uniform_cache_t c) {

#if 0
        // TODO rewrite in proper C++17

        for (auto &u : c)
        {
            // get name
            std::string name = std::get<0>(u);
            // get location
            GLint loc = std::get<2>(u);

            // if lower 0 check for location
            if (loc < 0)
            {
                loc = impl_->uniform_location(name);
                std::get<2>(u) = loc; // cache location
            }

            auto var = std::get<1>(u);

            std::visit([this, loc](auto &&arg)
                       {

            using T = std::decay_t<decltype(arg)>;

            // TODO query the std::variant of uniform_t

			if constexpr ((std::is_same_v<T, vector4f>) ||
				(std::is_same_v<T, matrix4x4f>) ||
				(std::is_same_v<T, float>) ) {
                    set_uniform_at_location( loc, std::forward<T>(arg));
			} else {
                debug::e() << "unknown uniform type";
			} },
                       var);
        }

#endif
}

uint32_t Shader::native_handle() const { return impl_->_shader_program; }

int Shader::uniform_location(const std::string& name) const {
    return impl_->uniform_location(name);
}

} // namespace paradiso
