#include "Texture.hpp"
#include "Debug.hpp"
#include "Color.hpp"
#include "ErrorTextureData.hpp"
#include <filesystem>

TSE::Texture::Texture(const string &path)
{
    FREE_IMAGE_FORMAT fif = FREE_IMAGE_FORMAT::FIF_UNKNOWN;
    bmp = nullptr;
    imagePtr = nullptr;
    Size = Vector2(0,0);

    string name = std::filesystem::absolute(path).string();

    if(!std::filesystem::exists(name))
    {
        string msg = string("File dose not exist: ") + std::filesystem::absolute(path).string();
        TSE_ERROR(msg);
        Bpp = 24;
        makeError(*this);
        return;
    }
    
    fif = FreeImage_GetFileType(name.c_str(), 0);
    if(fif == FREE_IMAGE_FORMAT::FIF_UNKNOWN)
        fif = FreeImage_GetFIFFromFilename(name.c_str());
    if(fif == FREE_IMAGE_FORMAT::FIF_UNKNOWN)
    {
        TSE_ERROR("Failed to load image. Unsupported Format.");
        Bpp = 24;
        makeError(*this);
        return;
    }
    if(FreeImage_FIFSupportsReading(fif))
    {
        bmp = FreeImage_Load(fif, path.c_str());
        if(FreeImage_GetBPP(bmp) != 32)
        {
            auto tmpBmp = FreeImage_ConvertTo32Bits(bmp);
            FreeImage_Unload(bmp);
            bmp = tmpBmp;
        }
    }
    if(bmp == nullptr)
    {
        TSE_ERROR("Failed to load image. Bitmap was nullptr.");
        Bpp = 24;
        makeError(*this);
        return;
    }
    Bpp = FreeImage_GetBPP(bmp);
    imagePtr = FreeImage_GetBits(bmp);

    switch (Bpp)
    {
    case 32:
        chanels = 4;
        break;
    case 24:
        chanels = 3;
    case 8:
        chanels = 4;
    }

    Size = Vector2(FreeImage_GetWidth(bmp), FreeImage_GetHeight(bmp));
    regist();
}

TSE::Texture::Texture(const int &width, const int &height, int bpp)
{
    switch (bpp)
    {
    case 32:
        chanels = 4;
        break;
    case 24:
        chanels = 3;
    case 8:
        chanels = 4;
    }
    Bpp = bpp;
    Size = Vector2(width, height);
    bmp = FreeImage_Allocate(width, height, bpp);
    imagePtr = FreeImage_GetBits(bmp);
    regist();
}

TSE::Texture::Texture(const Vector2 &size, int bpp)
{
    switch (bpp)
    {
    case 32:
        chanels = 4;
        break;
    case 24:
        chanels = 3;
    case 8:
        chanels = 4;
    }
    Bpp = bpp;
    Size = size;
    bmp = FreeImage_Allocate(size.x, size.y, bpp);
    imagePtr = FreeImage_GetBits(bmp);
    regist();
}

TSE::Texture::~Texture()
{
    if(bmp != nullptr)
    {
        FreeImage_Unload(bmp);
        bmp = nullptr;
    }
    if(TextureID != 0)
    {
        //glDeleteTextures(1, &TextureID); --> need to do in platform openGL
        PlatformDestroy();
        TextureID = 0;
    }
}

TSE::uint TSE::Texture::bpp() const
{
    return Bpp;
}

TSE::Vector2 TSE::Texture::size() const
{
    return Size;
}

float TSE::Texture::Width() const
{
    return Size.x;
}

float TSE::Texture::Height() const
{
    return Size.y;
}

TSE::byte TSE::Texture::Chanels() const
{
    return chanels;
}

TSE::byte *TSE::Texture::GetImagePtr() const
{
    return imagePtr;
}

void TSE::Texture::SetPixel(const Vector2 &pos, const Color &c)
{
    SetPixel(pos.x, pos.y, c);
}

void TSE::Texture::GetPixel(const Vector2 &pos, Color &c) const
{
    GetPixel(pos.x, pos.y, c);
}

TSE::Texture TSE::Texture::CutOut(const Vector2 &pos, Vector2 &size) const
{
    Texture result = Texture(0,0);
    if(bmp == nullptr)
    {
        TSE_ERROR("Failed to cut out texture. Source bitmap was nullptr.");
        Texture::makeError(result);
        return result;
    }

    const int left = static_cast<int>(pos.x);
    const int top = static_cast<int>(pos.y);
    const int width = static_cast<int>(size.x);
    const int height = static_cast<int>(size.y);

    if(width <= 0 || height <= 0)
    {
        TSE_ERROR("Failed to cut out texture. Size must be greater than 0.");
        Texture::makeError(result);
        return result;
    }

    const int texWidth = static_cast<int>(Width());
    const int texHeight = static_cast<int>(Height());
    if(left < 0 || top < 0 || left + width > texWidth || top + height > texHeight)
    {
        TSE_ERROR("Failed to cut out texture. Region out of bounds.");
        Texture::makeError(result);
        return result;
    }

    FIBITMAP* cut = FreeImage_Copy(bmp, left, top, left + width, top + height);
    if(cut == nullptr)
    {
        TSE_ERROR("Failed to cut out texture. FreeImage_Copy returned nullptr.");
        Texture::makeError(result);
        return result;
    }

    Texture out = Texture(width, height, Bpp);
    if(out.bmp != nullptr)
        FreeImage_Unload(out.bmp);
    out.bmp = cut;
    out.imagePtr = FreeImage_GetBits(cut);
    out.Bpp = Bpp;
    out.chanels = chanels;
    out.Size = Vector2(width, height);
    out.Apply();
    return out;
}

void TSE::Texture::PasteIn(const Vector2 &pos, Texture &t)
{
    if(bmp == nullptr)
    {
        TSE_ERROR("Failed to paste texture. Destination bitmap was nullptr.");
        return;
    }
    if(t.bmp == nullptr)
    {
        TSE_ERROR("Failed to paste texture. Source bitmap was nullptr.");
        return;
    }
    if(Bpp != t.Bpp)
    {
        TSE_ERROR("Failed to paste texture. Source and destination Bpp do not match.");
        return;
    }

    const int left = static_cast<int>(pos.x);
    const int top = static_cast<int>(pos.y);
    const int srcWidth = static_cast<int>(t.Width());
    const int srcHeight = static_cast<int>(t.Height());
    const int texWidth = static_cast<int>(Width());
    const int texHeight = static_cast<int>(Height());

    if(left < 0 || top < 0 || left + srcWidth > texWidth || top + srcHeight > texHeight)
    {
        TSE_ERROR("Failed to paste texture. Region out of bounds.");
        return;
    }

    if(!FreeImage_Paste(bmp, t.bmp, left, top, 256))
    {
        TSE_ERROR("Failed to paste texture. FreeImage_Paste returned false.");
        return;
    }

    imagePtr = FreeImage_GetBits(bmp);
    Apply();
}

void TSE::Texture::SetPixelNoApply(const Vector2 &pos, const Color &c)
{
    SetPixelNoApply(pos.x, pos.y, c);
}

void TSE::Texture::ToSprite(Sprite &s)
{
    s = Sprite(this, Rect(0,0,1,1));
}

void TSE::Texture::SetChanels(const byte &ch)
{
    chanels = ch;
}

TSE::uint TSE::Texture::GetTextureId() const
{
    return TextureID;
}

void TSE::Texture::SetTextureId(uint id)
{
    TextureID = id;
}

void TSE::Texture::SetPixel(const int &x, const int &y, const Color &c)
{
    SetPixelNoApply(x,y,c);
    Apply();
}

void TSE::Texture::GetPixel(const int &x, const int &y, Color &c) const
{
    if(x >= Width() || x < 0 ||y >= Height() || y < 0)
    {
        TSE_WARNING("trying to access pixel outside of texture.\n pixel: (" + std::to_string(x) + ";" + std::to_string(y) + ")\nTexture size: (" + std::to_string(Width()) + ";" + std::to_string(Height()) );
        return;
    }
    byte* pixel = getPixelPointer(x,y);
    byte b = *pixel++;
    byte g = *pixel++;
    byte r = *pixel++;
    byte a = *pixel++;
    if(bpp() == 8)
        c = Color(r,r,r);
    else if(bpp() == 24)
        c = Color(r,g,b,a);
    else if(bpp() == 32)
        c = Color(r,g,b);
}

void TSE::Texture::Fill(const Color &c)
{
    for (int x = 0; x < Width(); x++)
    {
        for (int y = 0; y < Height(); y++)
        {
            SetPixelNoApply(x,y,c);
        }
        
    }
    Apply();
}

void TSE::Texture::SetPixelNoApply(const int &x, const int &y, const Color &c)
{
    if(x >= Width() || x < 0 ||y >= Height() || y < 0)
    {
        TSE_WARNING("trying to access pixel outside of texture.\n pixel: (" + std::to_string(x) + ";" + std::to_string(y) + ")\nTexture size: (" + std::to_string(Width()) + ";" + std::to_string(Height()) );
        return;
    }

    byte* pixel = getPixelPointer(x,y);
    if(chanels == 4 && bpp() == 8)
    {
        byte r2bit = static_cast<byte>(c.r * 3.0f);
        byte g2bit = static_cast<byte>(c.g * 3.0f);
        byte b2bit = static_cast<byte>(c.b * 3.0f);
        byte a2bit = static_cast<byte>(c.a * 3.0f);

        byte result = (r2bit << 6) | (g2bit << 4) | (b2bit << 2) | a2bit;
        *pixel++ = result;
        return;
    }
    *pixel++ = c.B();
    if(bpp() > 8)
    {
        *pixel++ = c.G();
        *pixel++ = c.R();
        if(bpp() > 24)
            *pixel++ = c.A();
    }
}

void TSE::Texture::AddPixelNoApply(const int &x, const int &y, const Color &c)
{
    if(x >= Width() || x < 0 ||y >= Height() || y < 0)
    {
        TSE_WARNING("trying to access pixel outside of texture.\n pixel: (" + std::to_string(x) + ";" + std::to_string(y) + ")\nTexture size: (" + std::to_string(Width()) + ";" + std::to_string(Height()) );
        return;
    }

    byte* pixel = getPixelPointer(x,y);
    if(chanels == 4 && bpp() == 8) //TODO add propper adding, becouse currently is only setting
    {
        byte r2bit = static_cast<byte>(c.r * 3.0f);
        byte g2bit = static_cast<byte>(c.g * 3.0f);
        byte b2bit = static_cast<byte>(c.b * 3.0f);
        byte a2bit = static_cast<byte>(c.a * 3.0f);

        byte result = (r2bit << 6) | (g2bit << 4) | (b2bit << 2) | a2bit;
        *pixel++ = result;
        return;
    }
    Color bc(pixel[2], pixel[1], pixel[0], pixel[3]);

    bc = c + bc;

    *pixel++ = bc.B();
    if(bpp() > 8)
    {
        *pixel++ = bc.G();
        *pixel++ = bc.R();
        if(bpp() > 24)
            *pixel++ = bc.A();
    }
}

void TSE::Texture::Recreate(const int &x, const int &y, const int &bpp, const int &chanels)
{
    if(bmp != nullptr)
    {
        FreeImage_Unload(bmp);
        bmp = nullptr;
    }
    if(TextureID != 0)
    {
        PlatformDestroy();
        TextureID = 0;
    }
    this->chanels = chanels;
    Bpp = bpp;
    Size = Vector2(x, y);
    bmp = FreeImage_Allocate(x, y, bpp);
    imagePtr = FreeImage_GetBits(bmp);
    regist();
}

void TSE::Texture::SavePNG(const std::string &path) const
{
    if(!FreeImage_Save(FREE_IMAGE_FORMAT::FIF_PNG, bmp, path.c_str(), PNG_Z_NO_COMPRESSION))
        TSE_ERROR("Failed to save Image to \"" + path + "\"");
}

TSE::byte *TSE::Texture::getPixelPointer(const int &x, const int &y) const
{
    int alphaoffset = y * 2;
    if(bpp() > 24)
        alphaoffset = 0;
    int offset = ((y * Width() + x) * (bpp() / 8) + alphaoffset);
    return imagePtr + offset;
}

void TSE::Texture::makeError(Texture &tex)
{
    tex.Bpp = errorTexBpp;
    tex.chanels = errorTexChannels;
    tex.Size = Vector2(errorTexWidth, errorTexHeight);

    tex.bmp = FreeImage_ConvertFromRawBits(
        const_cast<BYTE*>(errorTextureData), // wichtig: cast!
        errorTexWidth,
        errorTexHeight,
        errorTexWidth * errorTexChannels,
        errorTexBpp,
        FI_RGBA_RED_MASK,
        FI_RGBA_GREEN_MASK,
        FI_RGBA_BLUE_MASK,
        true  // top-down
    );

    tex.imagePtr = FreeImage_GetBits(tex.bmp);
    tex.regist();
}

void TSE::Texture::bind() const
{
    helper->Bind(this);
}

void TSE::Texture::unbind() const
{
    helper->UnBind(this);
}

void TSE::Texture::Apply()
{
    helper->Apply(this);
}

void TSE::Texture::regist()
{
    helper->Regist(this);
}

void TSE::Texture::PlatformDestroy()
{
    helper->PlatromDestroy(this);
}
