/***********************************************************************
    created:    Sun, 6th April 2014
    author:     Lukas E Meindl
*************************************************************************/
/***************************************************************************
 *   Copyright (C) 2004 - 2014 Paul D Turner & The CEGUI Development Team
 *
 *   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 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 "CEGUI/RendererModules/Direct3D11/Texture.h"
#include "CEGUI/DataContainer.h"
#include "CEGUI/System.h"
#include "CEGUI/ResourceProvider.h"
#include "CEGUI/Exceptions.h"
#include "CEGUI/ImageCodec.h"
#include "CEGUI/Rectf.h"

#include <cstdint>

#include <d3d11.h>


// Start of CEGUI namespace section
namespace CEGUI
{
//----------------------------------------------------------------------------//
// helper to convert cegui pixel format enum to equivalent D3D format.
static DXGI_FORMAT toD3DPixelFormat(const Texture::PixelFormat fmt)
{
    switch (fmt)
    {
        case Texture::PixelFormat::Rgba:      return DXGI_FORMAT_R8G8B8A8_UNORM;
        case Texture::PixelFormat::Rgb:       return DXGI_FORMAT_R8G8B8A8_UNORM;
        case Texture::PixelFormat::RgbaDxt1: return DXGI_FORMAT_BC1_UNORM;
        case Texture::PixelFormat::RgbaDxt3: return DXGI_FORMAT_BC2_UNORM;
        case Texture::PixelFormat::RgbaDxt5: return DXGI_FORMAT_BC3_UNORM;
        default:                    return DXGI_FORMAT_UNKNOWN;
    }
}

//----------------------------------------------------------------------------//
// helper function to return byte width of given pixel width in given format
static size_t calculateDataWidth(const size_t width, Texture::PixelFormat fmt)
{
    switch (fmt)
    {
    case Texture::PixelFormat::Rgba:
    case Texture::PixelFormat::Rgb: // also 4 because we convert to RGBA
        return width * 4;

    case Texture::PixelFormat::RgbaDxt1:
        return ((width + 3) / 4) * 8;

    case Texture::PixelFormat::RgbaDxt3:
    case Texture::PixelFormat::RgbaDxt5:
        return ((width + 3) / 4) * 16;

    default:
        return 0;
    }
}

//----------------------------------------------------------------------------//
// Helper utility function that copies a region of a buffer containing D3DCOLOR
// values into a second buffer as RGBA values.
static void blitFromSurface(const std::uint32_t* src, std::uint32_t* dst,
                     const Sizef& sz, size_t source_pitch)
{
    for (unsigned int i = 0; i < sz.d_height; ++i)
    {
        for (unsigned int j = 0; j < sz.d_width; ++j)
        {
            const std::uint32_t pixel = src[j];
            const std::uint32_t tmp = pixel & 0x00FF00FF;
            dst[j] = (pixel & 0xFF00FF00) | (tmp << 16) | (tmp >> 16);
        }

        src += source_pitch / sizeof(std::uint32_t);
        dst += static_cast<std::uint32_t>(sz.d_width);
    }
}

//----------------------------------------------------------------------------//
void Direct3D11Texture::setDirect3DTexture(ID3D11Texture2D* tex)
{
    if (d_texture != tex)
    {
        cleanupDirect3D11Texture();
        d_dataSize.d_width = d_dataSize.d_height = 0;

        d_texture = tex;
        if (d_texture)
            d_texture->AddRef();
    }

    initialiseShaderResourceView();

    updateTextureSize();
    d_dataSize = d_size;
    updateCachedScaleValues();
}

//----------------------------------------------------------------------------//
ID3D11Texture2D* Direct3D11Texture::getDirect3DTexture() const
{
    return d_texture;
}

//----------------------------------------------------------------------------//
ID3D11ShaderResourceView* Direct3D11Texture::getDirect3DShaderResourceView() const
{
    return d_resourceView;
}

//----------------------------------------------------------------------------//
void Direct3D11Texture::setOriginalDataSize(const Sizef& sz)
{
    d_dataSize = sz;
    updateCachedScaleValues();
}

//----------------------------------------------------------------------------//
const String& Direct3D11Texture::getName() const
{
    return d_name;
}

//----------------------------------------------------------------------------//
const Sizef& Direct3D11Texture::getSize() const
{
    return d_size;
}

//----------------------------------------------------------------------------//
const Sizef& Direct3D11Texture::getOriginalDataSize() const
{
    return d_dataSize;
}

//----------------------------------------------------------------------------//
const glm::vec2& Direct3D11Texture::getTexelScaling() const
{
    return d_texelScaling;
}

//----------------------------------------------------------------------------//
void Direct3D11Texture::loadFromFile(const String& filename,
                                     const String& resourceGroup)
{
    // get and check existence of CEGUI::System object
    System* sys = System::getSingletonPtr();
    if (!sys)
        throw RendererException(
            "CEGUI::System object has not been created!");

    // load file to memory via resource provider
    RawDataContainer texFile;
    sys->getResourceProvider()->loadRawDataContainer(filename, texFile,
                                                     resourceGroup);

    Texture* res = sys->getImageCodec().load(texFile, this);

    // unload file data buffer
    sys->getResourceProvider()->unloadRawDataContainer(texFile);

    if (!res)
        // It's an error
        throw RendererException(
            sys->getImageCodec().getIdentifierString() +
            " failed to load image '" + filename + "'.");
}

//----------------------------------------------------------------------------//
void Direct3D11Texture::loadFromMemory(const void* buffer,
                                       const Sizef& buffer_size,
                                       PixelFormat pixel_format)
{
    if (!isPixelFormatSupported(pixel_format))
        throw InvalidRequestException(
            "Data was supplied in an unsupported pixel format.");

    cleanupDirect3D11Texture();

    const void* img_src = buffer;
    unsigned char* dest(0);
    if (pixel_format == PixelFormat::Rgb)
    {
        const unsigned char* src = static_cast<const unsigned char*>(buffer);
        dest = new unsigned char[static_cast<unsigned int>( buffer_size.d_width * buffer_size.d_height * 4 )];

        for (int i = 0; i < buffer_size.d_width * buffer_size.d_height; ++i)
        {
            dest[i * 4 + 0] = src[i * 3 + 0];
            dest[i * 4 + 1] = src[i * 3 + 1];
            dest[i * 4 + 2] = src[i * 3 + 2];
            dest[i * 4 + 3] = 0xFF;
        }

        img_src = dest;
    }

    D3D11_TEXTURE2D_DESC tex_desc;
    ZeroMemory(&tex_desc, sizeof(tex_desc));
    tex_desc.Width = static_cast<UINT>(buffer_size.d_width);
    tex_desc.Height = static_cast<UINT>(buffer_size.d_height);
    tex_desc.ArraySize = 1;
    tex_desc.SampleDesc.Count = 1;
    tex_desc.SampleDesc.Quality = 0;
    tex_desc.Format = toD3DPixelFormat(pixel_format);
    tex_desc.Usage = D3D11_USAGE_DEFAULT;
    tex_desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
    tex_desc.CPUAccessFlags = 0;
    tex_desc.MiscFlags = 0;
    tex_desc.MipLevels = 1;

    D3D11_SUBRESOURCE_DATA data;
    ZeroMemory(&data, sizeof(D3D11_SUBRESOURCE_DATA));
    data.pSysMem = img_src;
    data.SysMemPitch = static_cast<UINT>(calculateDataWidth(tex_desc.Width, pixel_format));

    HRESULT hr = d_device.CreateTexture2D(&tex_desc, &data, &d_texture);

    delete[] dest;

    if (FAILED(hr))
        throw RendererException(
            "Failed to create texture from memory buffer.");

    initialiseShaderResourceView();

    d_dataSize = buffer_size;
    updateTextureSize();
    updateCachedScaleValues();
}

//----------------------------------------------------------------------------//
void Direct3D11Texture::blitFromMemory(const void* sourceData, const Rectf& area)
{
    if (!d_texture)
        return;

    std::uint32_t* buff = new std::uint32_t[static_cast<size_t>(area.getWidth()) *
                              static_cast<size_t>(area.getHeight())];
    blitFromSurface(static_cast<const std::uint32_t*>(sourceData), buff,
                    area.getSize(), static_cast<size_t>(area.getWidth()) * 4);

    D3D11_BOX dst_box = {static_cast<UINT>(area.left()),
                         static_cast<UINT>(area.top()),
                         0,
                         static_cast<UINT>(area.right()),
                         static_cast<UINT>(area.bottom()),
                         1};

    d_deviceContext.UpdateSubresource(d_texture, 0, &dst_box, buff,
                                       static_cast<UINT>(area.getWidth()) * 4,
                                       0);

    delete[] buff;
}

//----------------------------------------------------------------------------//
void Direct3D11Texture::blitToMemory(void* targetData)
{
    if (!d_texture)
        return;

    String exception_msg;

    D3D11_TEXTURE2D_DESC tex_desc;
    d_texture->GetDesc(&tex_desc);

    tex_desc.Usage = D3D11_USAGE_STAGING;
    tex_desc.BindFlags = 0;
    tex_desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;

    ID3D11Texture2D* offscreen;
    if (SUCCEEDED(d_device.CreateTexture2D(&tex_desc, 0, &offscreen)))
    {
        d_deviceContext.CopyResource(offscreen, d_texture);

        D3D11_MAPPED_SUBRESOURCE mapped_tex;
        if (SUCCEEDED(d_deviceContext.Map(offscreen, 0, D3D11_MAP_READ,
                                              0, &mapped_tex)))
        {
            blitFromSurface(static_cast<std::uint32_t*>(mapped_tex.pData),
                            static_cast<std::uint32_t*>(targetData),
                            Sizef(static_cast<float>(tex_desc.Width),
                                   static_cast<float>(tex_desc.Height)),
                            mapped_tex.RowPitch);

            d_deviceContext.Unmap(offscreen, 0);
        }
        else
            exception_msg.assign("ID3D11Texture2D::Map failed.");

        offscreen->Release();
    }
    else
        exception_msg.assign(
            "ID3D11Device::CreateTexture2D failed for 'offscreen'.");

    if (!exception_msg.empty())
        throw RendererException(exception_msg);
}

//----------------------------------------------------------------------------//
void Direct3D11Texture::cleanupDirect3D11Texture()
{
    if (d_resourceView)
    {
        d_resourceView->Release();
        d_resourceView = 0;
    }

    if (d_texture)
    {
        d_texture->Release();
        d_texture = 0;
    }
}

//----------------------------------------------------------------------------//
void Direct3D11Texture::updateCachedScaleValues()
{
    //
    // calculate what to use for x scale
    //
    const float orgW = d_dataSize.d_width;
    const float texW = d_size.d_width;

    // if texture and original data width are the same, scale is based
    // on the original size.
    // if texture is wider (and source data was not stretched), scale
    // is based on the size of the resulting texture.
    d_texelScaling.x = 1.0f / ((orgW == texW) ? orgW : texW);

    //
    // calculate what to use for y scale
    //
    const float orgH = d_dataSize.d_height;
    const float texH = d_size.d_height;

    // if texture and original data height are the same, scale is based
    // on the original size.
    // if texture is taller (and source data was not stretched), scale
    // is based on the size of the resulting texture.
    d_texelScaling.y = 1.0f / ((orgH == texH) ? orgH : texH);
}

//----------------------------------------------------------------------------//
void Direct3D11Texture::updateTextureSize()
{
    if (d_texture)
    {
        D3D11_TEXTURE2D_DESC surfDesc;
        d_texture->GetDesc(&surfDesc);
        d_size.d_width  = static_cast<float>(surfDesc.Width);
        d_size.d_height = static_cast<float>(surfDesc.Height);
    }
    else
        d_size.d_height = d_size.d_width = 0.0f;
}

//----------------------------------------------------------------------------//
Direct3D11Texture::Direct3D11Texture(ID3D11Device& device, ID3D11DeviceContext& deviceContext, const String& name) :
    d_device(device),
    d_deviceContext(deviceContext),
    d_texture(0),
    d_resourceView(0),
    d_size(0, 0),
    d_dataSize(0, 0),
    d_texelScaling(0, 0),
    d_name(name)
{
}

//----------------------------------------------------------------------------//
Direct3D11Texture::Direct3D11Texture(ID3D11Device& device, ID3D11DeviceContext& deviceContext,
                                     const String& name,
                                     const String& filename,
                                     const String& resourceGroup) :
    d_device(device),
    d_deviceContext(deviceContext),
    d_texture(0),
    d_resourceView(0),
    d_size(0, 0),
    d_dataSize(0, 0),
    d_texelScaling(0, 0),
    d_name(name)
{
    loadFromFile(filename, resourceGroup);
}

//----------------------------------------------------------------------------//
Direct3D11Texture::Direct3D11Texture(ID3D11Device& device, ID3D11DeviceContext& deviceContext,
                                     const String& name,
                                     const Sizef& sz) :
    d_device(device),
    d_deviceContext(deviceContext),
    d_texture(0),
    d_resourceView(0),
    d_size(0, 0),
    d_dataSize(0, 0),
    d_texelScaling(0, 0),
    d_name(name)
{
    D3D11_TEXTURE2D_DESC tex_desc;
    ZeroMemory(&tex_desc, sizeof(tex_desc));
    tex_desc.Width = static_cast<UINT>(sz.d_width);
    tex_desc.Height = static_cast<UINT>(sz.d_height);
    tex_desc.ArraySize = 1;
    tex_desc.SampleDesc.Count = 1;
    tex_desc.SampleDesc.Quality = 0;
    tex_desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
    tex_desc.Usage = D3D11_USAGE_DEFAULT;
    tex_desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
    tex_desc.CPUAccessFlags = 0;
    tex_desc.MiscFlags = 0;
    tex_desc.MipLevels = 1;

    if (FAILED(d_device.CreateTexture2D(&tex_desc, 0, &d_texture)))
        throw RendererException(
            "Failed to create texture with specified size.");

    initialiseShaderResourceView();

    d_dataSize = sz;
    updateTextureSize();
    updateCachedScaleValues();
}

//----------------------------------------------------------------------------//
Direct3D11Texture::Direct3D11Texture(ID3D11Device& device, ID3D11DeviceContext& deviceContext,
                                     const String& name,
                                     ID3D11Texture2D* tex) :
    d_device(device),
    d_deviceContext(deviceContext),
    d_texture(0),
    d_resourceView(0),
    d_size(0, 0),
    d_dataSize(0, 0),
    d_texelScaling(0, 0),
    d_name(name)
{
    setDirect3DTexture(tex);
}

//----------------------------------------------------------------------------//
Direct3D11Texture::~Direct3D11Texture()
{
    cleanupDirect3D11Texture();
}

//----------------------------------------------------------------------------//
void Direct3D11Texture::initialiseShaderResourceView()
{
    if (!d_texture)
        return;

    D3D11_TEXTURE2D_DESC tex_desc;
    d_texture->GetDesc(&tex_desc);

    ID3D11Resource* resource = 0;
    d_texture->QueryInterface(__uuidof(ID3D11Resource),
                              reinterpret_cast<LPVOID*>(&resource));

    D3D11_SHADER_RESOURCE_VIEW_DESC srvd;
    srvd.Format = tex_desc.Format;
    srvd.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
    srvd.Texture2D.MostDetailedMip = 0;
    srvd.Texture2D.MipLevels = tex_desc.MipLevels;
    d_device.CreateShaderResourceView(resource, &srvd, &d_resourceView);
    resource->Release();
}

//----------------------------------------------------------------------------//
bool Direct3D11Texture::isPixelFormatSupported(const PixelFormat fmt) const
{
    switch (fmt)
    {
        case PixelFormat::Rgba:
        case PixelFormat::Rgb:
        case PixelFormat::RgbaDxt1:
        case PixelFormat::RgbaDxt3:
        case PixelFormat::RgbaDxt5:
            return true;

        default:
            return false;
    }
}

//----------------------------------------------------------------------------//

} // End of  CEGUI namespace section
