#include "Images.hpp"
#include "../../../../core/utils/logger.hpp"
#include <algorithm>
#include <string>
#include <array>

using namespace render::gl;

Image::~Image() {
    glDeleteTextures(1, &id);
}

GLenum getGLFormat(render::Image::Format format) {
    switch (format) {
    case render::Image::Format::BC2:
    case render::Image::Format::BC2_UNORM:
        return GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT;

    case render::Image::Format::BC3:
    case render::Image::Format::BC3_UNORM:
        return GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT;

    default:
        return 0;
    }
}

GLuint createImage(const Image::requirement& req, render::data_view data) {
    GLuint textureID;
    glGenTextures(1, &textureID);
    glBindTexture(GL_TEXTURE_2D, textureID);
    glPixelStorei(GL_UNPACK_ALIGNMENT,1);
    glTextureParameteri(textureID, GL_TEXTURE_MAX_LEVEL, req.mipmapLevels-1);

    GLenum format = getGLFormat(req.format);
    unsigned int blockSize = /*(format == GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT) ? 8 :*/ 16;
    unsigned int offset = 0;

    int width = req.size.width;
    int height = req.size.height;
    /* load the mipmaps */
    for (unsigned int level = 0; level < req.mipmapLevels && (width || height); ++level) {
        unsigned int size = ((width+3)/4)*((height+3)/4)*blockSize;
        assert(data.size >= offset + size);
        glCompressedTexImage2D(GL_TEXTURE_2D, level, format, width, height, 0, size, static_cast<const uint8_t*>(data.ptr) + offset);

        offset += size;
        width  /= 2;
        height /= 2;

        // Deal with Non-Power-Of-Two textures. This code is not included in the webpage to reduce clutter.
        if(width < 1) width = 1;
        if(height < 1) height = 1;
    }

    return textureID;
}
void applySampler(GLuint textureID, const Texture::sampling& props) {
    auto getFilter = [](bool linear, bool mipmap) { return linear ?
        (mipmap ? GL_LINEAR_MIPMAP_LINEAR : GL_LINEAR) : (mipmap ? GL_NEAREST_MIPMAP_NEAREST : GL_NEAREST); };
    auto wrap = [](Texture::Wrap wrap) {
        switch (wrap) {
        case Texture::Wrap::CLAMP_TO_EDGE:
            return GL_CLAMP_TO_EDGE;
        case Texture::Wrap::CLAMP_TO_BORDER:
            return GL_CLAMP_TO_BORDER;
        case Texture::Wrap::MIRRORED_REPEAT:
            return GL_MIRRORED_REPEAT;

        case Texture::Wrap::REPEAT:
        default:
            return GL_REPEAT;
        }
    }(props.wrap);

    glTextureParameteri(textureID, GL_TEXTURE_MAG_FILTER, getFilter(props.magLinear, props.mipmap));
    glTextureParameteri(textureID, GL_TEXTURE_MIN_FILTER, getFilter(props.minLinear, props.mipmap));
    glTextureParameterf(textureID, GL_TEXTURE_LOD_BIAS, props.mipmapLod);
    glTextureParameterf(textureID, GL_TEXTURE_MAX_ANISOTROPY, props.anisotropy);

    glTextureParameteri(textureID, GL_TEXTURE_WRAP_S, wrap);
    glTextureParameteri(textureID, GL_TEXTURE_WRAP_T, wrap);
    glTextureParameteri(textureID, GL_TEXTURE_WRAP_R, wrap);
}

std::unique_ptr<Texture> Texture::LoadFromFile(const std::string& path, const sampling& props) {
    std::vector<unsigned char> data;
    auto header = [&] {
        if (auto header = render::Image::Read(path, data)) {
            return header.value();
        }
        FATAL("Cannot read texture");
    }();

    auto id = createImage(requirement::Texture(header), data);
    if(!id) {
        FATAL("Cannot create texture image");
    }
    applySampler(id, props);

    return std::unique_ptr<Texture>(new Texture(id));
}

std::unique_ptr<TextureCube> TextureCube::LoadFromFiles(const std::array<std::string, 6>& paths, const sampling& props) {
    std::vector<unsigned char> data;
    auto header = [&] {
        if (auto header = render::Image::Read(paths.front(), data)) {
            return header.value();
        }
        FATAL("Cannot read first texture");
    }();
    auto req = requirement::Texture(header);


    GLuint textureID;
    {
        GLenum format = getGLFormat(header.format);
        glCreateTextures(GL_TEXTURE_CUBE_MAP, 1, &textureID);
        glTextureStorage2D(textureID, 1, format, header.size.width, header.size.height);

        uint16_t layer = 0;
        for (auto imagepath = paths.begin(); imagepath != paths.end(); ++imagepath, ++layer) {
            data.clear();
            if (!render::Image::Read(*imagepath, data).has_value()) {
                FATAL("Cannot read texture");
            }
            GLuint subTextureID = createImage(req, data);
            glCopyImageSubData(subTextureID, GL_TEXTURE_2D, 0, 0, 0, 0, textureID, GL_TEXTURE_CUBE_MAP, 0, 0, 0, layer, header.size.width, header.size.height, 1);
            glDeleteTextures(1, &subTextureID);
        }
    }
    glTextureParameteri(textureID, GL_TEXTURE_MAX_LEVEL, header.mipmapLevels-1);
    applySampler(textureID, props);

    return std::unique_ptr<TextureCube>(new TextureCube(textureID));
}
std::unique_ptr<TextureCube> TextureCube::LoadFromFiles(const std::string& prefix, const sampling& props) {
    const std::array<std::string, 6> faces {
        "right",
        "left",
        "top",
        "bottom",
        "front",
        "back"
    };
    std::array<std::string, 6> paths;
    std::transform(faces.begin(), faces.end(), paths.begin(),
        [prefix](const std::string &face) -> std::string { return prefix + "." + face + ".dds"; });
    return LoadFromFiles(paths, props);
}

std::unique_ptr<TextureArray> TextureArray::LoadFromFiles(const std::vector<std::string>& paths, const sampling& props) {
    std::vector<unsigned char> data;
    auto header = [&] {
        if (auto header = render::Image::Read(paths.front(), data)) {
            return header.value();
        }
        FATAL("Cannot read first texture");
    }();
    auto req = requirement::Texture(header);

    GLuint textureID;
    {
        GLenum format = getGLFormat(header.format);
        glCreateTextures(GL_TEXTURE_2D_ARRAY, 1, &textureID);
        glTextureStorage3D(textureID, header.mipmapLevels, format, header.size.width, header.size.height, paths.size());

        uint16_t layer = 0;
        for (auto imagepath = paths.begin(); imagepath != paths.end(); ++imagepath, ++layer) {
            data.clear();
            if (!render::Image::Read(*imagepath, data).has_value()) {
                FATAL("Cannot read texture");
            }
            GLuint subTextureID = createImage(req, data);
            auto width = header.size.width;
            auto height = header.size.height;
            for (uint32_t level = 0; level < header.mipmapLevels; level++) {
                glCopyImageSubData(subTextureID, GL_TEXTURE_2D, level, 0, 0, 0, textureID, GL_TEXTURE_2D_ARRAY, level, 0, 0, layer, width, height, 1);
                width  /= 2;
                height /= 2;

                if(width < 1) width = 1;
                if(height < 1) height = 1;
            }
            glDeleteTextures(1, &subTextureID);
        }
    }
    glTextureParameteri(textureID, GL_TEXTURE_MAX_LEVEL, header.mipmapLevels-1);
    applySampler(textureID, props);
    return std::unique_ptr<TextureArray>(new TextureArray(paths.size(), textureID));
}