/*
 *  <Short Description>
 *  Copyright (C) 2023  Brett Terpstra
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

#ifndef BLT_TEXTURE_H
#define BLT_TEXTURE_H

#include <blt/gfx/gl_includes.h>
#include <blt/logging/logging.h>
#include <string>
#include <vector>
#include <utility>
#include "blt/std/types.h"

namespace blt::gfx
{
    class texture_file;
    
    class texture_data
    {
            friend texture_file;
        private:
            unsigned char* m_data = nullptr;
            int m_width = 0, m_height = 0, m_channels = 0;
        public:
            texture_data(unsigned char* data, int width, int height, int channels):
                    m_data(data), m_width(width), m_height(height), m_channels(channels)
            {}
            
            texture_data(int width, int height, int channels = 4): m_width(width), m_height(height), m_channels(channels)
            {
                m_data = static_cast<unsigned char*>(std::malloc(width * height * channels));
            }
            
            texture_data() = default;
            
            inline unsigned char* data()
            {
                return m_data;
            }
            
            [[nodiscard]] inline const unsigned char* data() const
            {
                return m_data;
            }
            
            [[nodiscard]] inline int width() const
            {
                return m_width;
            }
            
            [[nodiscard]] inline int height() const
            {
                return m_height;
            }
            
            [[nodiscard]] inline int channels() const
            {
                return m_channels;
            }
            
            ~texture_data()
            {
                std::free(m_data);
            }
    };
    
    class texture_file
    {
        private:
            std::string m_name;
            std::string m_path;
            blt::i32 desired_channels;
            mutable texture_data m_texture;
        public:
            /**
             * @param path path to the texture file
             * @param name reference name for this texture. If empty the texture will use path as its identifier
             */
            explicit texture_file(const std::string& path, const std::string& name = "", blt::i32 desired_channels = 0);
            
            texture_file& resize(int target_width, int target_height);
            
            inline texture_data& texture() const
            {
                return m_texture;
            }
            
            [[nodiscard]] inline int channels() const
            {
                return m_texture.m_channels;
            }
            
            [[nodiscard]] inline int width() const
            {
                return m_texture.m_width;
            }
            
            [[nodiscard]] inline int height() const
            {
                return m_texture.m_height;
            }
            
            [[nodiscard]] inline const std::string& getName() const
            {
                return m_name;
            }
    };
    
    struct texture_gl
    {
        protected:
            unsigned int textureID = 0;
            GLint textureBindType;
            GLint textureColorMode;
            int m_width, m_height;
            
            texture_gl(int width, int height, GLint bind_type = GL_TEXTURE_2D, GLint color_mode = GL_RGBA):
                    textureBindType(bind_type), textureColorMode(color_mode), m_width(width), m_height(height)
            {
                glGenTextures(1, &textureID);
            }
        
        protected:
            void setDefaults(GLint type, GLint wrap = GL_REPEAT) const;
            
            inline void generateMipmaps() const
            {
                glGenerateMipmap(textureBindType);
            }
        
        public:
            inline void bind() const
            {
                glBindTexture(textureBindType, textureID);
            }
            
            inline void unbind() const
            {
                glBindTexture(textureBindType, 0);
            }
            
            [[nodiscard]] inline unsigned int getTextureID() const
            {
                return textureID;
            }
            
            [[nodiscard]] inline GLint getBindType() const
            {
                return textureBindType;
            }
            
            virtual void updateSize(blt::i32, blt::i32)
            {}
            
            [[nodiscard]] int getHeight() const
            {
                return m_height;
            }
            
            [[nodiscard]] int getWidth() const
            {
                return m_width;
            }
            
            virtual ~texture_gl()
            {
                glDeleteTextures(1, &textureID);
            }
    };
    
    struct texture_gl2D : public texture_gl
    {
        public:
            explicit texture_gl2D(const texture_data& data);
            
            texture_gl2D(int width, int height, GLint colorMode = GL_RGBA8);
            
            void upload(void* data, GLint dataColorMode = GL_RGBA, int level = 0, int x_offset = 0, int y_offset = 0, int sub_width = -1,
                        int sub_height = -1, GLint dataMode = GL_UNSIGNED_BYTE) const;
            
            void upload(void* data, int width, int height, GLint dataColorMode = GL_RGBA, GLint dataMode = GL_UNSIGNED_BYTE);
            
            void upload(const texture_data& file_data) const;
            
            inline void updateSize(blt::i32 width, blt::i32 height) final
            {
                resize(width, height);
            }
            
            /**
             * Resizes the internal memory for the texture but does NOT resize the texture image stored
             */
            void resize(int width, int height);
    };
    
    struct texture_gl2D_multisample : public texture_gl
    {
        private:
            blt::i32 samples;
        public:
            texture_gl2D_multisample(int width, int height, blt::i32 samples, GLint colorMode = GL_RGBA8);
            
            inline void updateSize(blt::i32 width, blt::i32 height) final
            {
                resize(width, height);
            }
            
            /**
             * Resizes the internal memory for the texture but does NOT resize the texture image stored
             */
            void resize(int width, int height);
    };
    
    struct gl_texture2D_array : public texture_gl
    {
        protected:
            int m_layers;
        public:
            gl_texture2D_array(int width, int height, int layers, GLint colorMode = GL_RGBA8);
            
            void upload(void* data, int index, GLint dataColorMode = GL_RGBA, int level = 0, int x_offset = 0, int y_offset = 0, int sub_width = -1,
                        int sub_height = -1) const;
    };
    
    class gl_buffer_texture : public texture_gl2D
    {
        private:
        
        public:
            // make sure a framebuffer is bound before constructing!
            gl_buffer_texture(
                    int width, int height, GLint format = GL_RGB32F,
                    GLint colorAttachment = GL_COLOR_ATTACHMENT0
                             ): texture_gl2D(width, height, format)
            {
                bind();
                // no mipmaping and no interpolation to position textures!
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
                //
                glFramebufferTexture2D(
                        GL_FRAMEBUFFER, colorAttachment, GL_TEXTURE_2D, this->textureID, 0
                );
                
            }
        
        
    };
}

#endif //BLT_TEXTURE_H
