#include "pw/visual/texture.hpp"

#include "glad/glad.h"

#include <limits>

namespace pw {


struct texture::impl {

    texture &_host;

    uint32_t _texture_id { 0 };
    uint32_t _texture_sampler { 0 };
    uint64_t _change_count { std::numeric_limits<uint64_t>::max()};

    impl() = delete;

    impl(texture& host)
        : _host(host)
    {
    }

    ~impl()
    {
        destroy();
    }

    bool valid() const {
        return glIsTexture(_texture_id) > 0;
    }

    void bind()
    {
        glBindTexture(GL_TEXTURE_2D, _texture_id);
    }

    void unbind()
    {
        glBindTexture(GL_TEXTURE_2D, 0);
    }

    void update(const class image& i)
    {
//        debug::d() << __PRETTY_FUNCTION__;
        if (_change_count != i.change_count())
        {
            this->bind();

            glPixelStorei(GL_UNPACK_ALIGNMENT,1);

            glTexSubImage2D(GL_TEXTURE_2D, // target
                            0,  // level
                            0,0, // x/y offset
                            i.size().width,i.size().height, // width,height
                            GL_RGB, // format
                            GL_UNSIGNED_BYTE, // type
                            i.data()
                            );


//            this->unbind();

            auto error = glGetError();
            if (error != GL_NO_ERROR) {
                debug::e() << __PRETTY_FUNCTION__ << " " << __LINE__ << " GL error: " << error;
            }


            _change_count = i.change_count();
        }
    }

    bool create(const class image& i)
    {
        if (i.size().area() == 0)
        {
            debug::w() << __PRETTY_FUNCTION__ << " empty image";
            return false;
        }

        GLuint format = GL_RGB;

        glGenTextures(1, &_texture_id);
        glActiveTexture(GL_TEXTURE0 + 0);
        glBindTexture(GL_TEXTURE_2D,_texture_id);


        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);

        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

        // create texture
        glTexImage2D(GL_TEXTURE_2D,0,format,
                     i.size().width,i.size().height,
                     0,
                     format,
                     GL_UNSIGNED_BYTE,
                     nullptr
                     );

        // generate MipMaps
        glGenerateMipmap(GL_TEXTURE_2D);
        glGenerateTextureMipmap(_texture_id);

//        debug::e() << __PRETTY_FUNCTION__ << " " << __LINE__ << " GL error: " << glGetError();


        // create sampler

#if 0
        glGenSamplers(1, &_texture_sampler);
        glBindSampler(0,_texture_sampler);

        glSamplerParameteri(_texture_sampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
        glSamplerParameteri(_texture_sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST);

        // glSamplerParameteri(_texture_sampler, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
        glSamplerParameteri(_texture_sampler, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        glSamplerParameteri(_texture_sampler, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
#endif
        glBindTexture(GL_TEXTURE_2D,0);

//        debug::d() << "\t" << "     _texture_id " << _texture_id;
//        debug::d() << "\t" << "_texture_sampler " << _texture_sampler;


        // get errors
        auto error = glGetError();
        if (error != GL_NO_ERROR) {
            debug::e() << __PRETTY_FUNCTION__ << " " << __LINE__ << " GL error: " << error;
            return false;
        }
        return true;
    }


    void destroy()
    {
        glDeleteTextures(1,&_texture_id);
    }

};



//
// Wrapper
//

texture::texture()
    : _impl { make_unique<texture::impl>(*this) }
{
}

texture::texture(const image &i, texture::data_layout s)
    : texture()
{
    this->create(i);
}

texture::~texture()
{
}

bool texture::create(const image& i)
{
    return _impl->create(i);
}

void texture::update(const image &i)
{
    _impl->update(i);
}

void texture::bind()
{
    _impl->bind();
}

void texture::unbind()
{
    _impl->unbind();
}

bool texture::valid() const
{
    return _impl->valid();
}

void texture::set_shape(data_layout s)
{
    _shape = s;
}

void texture::set_wrap(wrap_mode w)
{
    _wrap = w;
}

uint32_t texture::native_handle() const
{
    return _impl->_texture_id;
}


}
