// Copyright (C) 2002-2012 Nikolaus Gebhardt
// This file is part of the "Irrlicht Engine".
// For conditions of distribution and use, see copyright notice in irrlicht.h

#include "IrrCompileConfig.h"
#ifdef _IRR_COMPILE_WITH_DIRECT3D_9_

#define _IRR_DONT_DO_MEMORY_DEBUGGING_HERE
#include "CD3D9Texture.h"
#include "CD3D9Driver.h"
#include "os.h"

#include <d3dx9tex.h>

namespace irr
{
namespace video
{

//! rendertarget constructor
CD3D9Texture::CD3D9Texture(CD3D9Driver* driver, const core::dimension2d<u32>& size, const io::path& name, const ECOLOR_FORMAT format)
	: ITexture(name), Texture(0), RTTSurface(0), Driver(driver), HardwareMipMaps(false), IsCompressed(false)
{
#ifdef _DEBUG
	setDebugName("CD3D9Texture");
#endif

	Device=driver->getExposedVideoData().D3D9.D3DDev9;
	if (Device)
		Device->AddRef();

	DriverType = EDT_DIRECT3D9;
	OriginalSize = size;
	Size = size;
	IsRenderTarget = true;

	createRenderTarget(format);
}


//! constructor
CD3D9Texture::CD3D9Texture(IImage* image, CD3D9Driver* driver, u32 flags, const io::path& name, void* mipmapData)
	: ITexture(name), Texture(0), RTTSurface(0), Driver(driver), HardwareMipMaps(false), IsCompressed(false)
{
#ifdef _DEBUG
	setDebugName("CD3D9Texture");
#endif

	DriverType = EDT_DIRECT3D9;
	HasMipMaps = Driver->getTextureCreationFlag(video::ETCF_CREATE_MIP_MAPS);

	Device=driver->getExposedVideoData().D3D9.D3DDev9;
	if (Device)
		Device->AddRef();

	if (image)
	{
		if (IImage::isCompressedFormat(image->getColorFormat()))
		{
			if(!Driver->queryFeature(EVDF_TEXTURE_COMPRESSED_DXT))
			{
				os::Printer::log("DXT texture compression not available.", ELL_ERROR);
				return;
			}
		}

		if (createTexture(flags, image))
		{
			if (copyTexture(image))
			{
				if (IsCompressed && !mipmapData)
					if (HasMipMaps && image->hasMipMaps())
					{
						u32 compressedDataSize = 0;

						if(ColorFormat == ECF_DXT1)
							compressedDataSize = ((image->getDimension().Width + 3) / 4) * ((image->getDimension().Height + 3) / 4) * 8;
						else if (ColorFormat == ECF_DXT2 || ColorFormat == ECF_DXT3 || ColorFormat == ECF_DXT4 || ColorFormat == ECF_DXT5)
							compressedDataSize = ((image->getDimension().Width + 3) / 4) * ((image->getDimension().Height + 3) / 4) * 16;

						mipmapData = static_cast<u8*>(image->lock())+compressedDataSize;
					}
					else
						HasMipMaps = false;

				regenerateMipMapLevels(mipmapData);
			}
		}
		else
			os::Printer::log("Could not create DIRECT3D9 Texture.", ELL_WARNING);
	}
}


//! destructor
CD3D9Texture::~CD3D9Texture()
{
	if (Texture)
		Texture->Release();

	if (RTTSurface)
		RTTSurface->Release();

	if (Device)
		Device->Release();
}


void CD3D9Texture::createRenderTarget(const ECOLOR_FORMAT format)
{
	// are texture size restrictions there ?
	if(!Driver->queryFeature(EVDF_TEXTURE_NPOT))
	{
		if (Size != OriginalSize)
			os::Printer::log("RenderTarget size has to be a power of two", ELL_INFORMATION);
	}

	Size = Size.getOptimalSize(!Driver->queryFeature(EVDF_TEXTURE_NPOT), !Driver->queryFeature(EVDF_TEXTURE_NSQUARE), true, Driver->Caps.MaxTextureWidth);

	D3DFORMAT d3dformat = Driver->getD3DColorFormat();

	if(ColorFormat == ECF_UNKNOWN)
	{
		// get irrlicht format from backbuffer
		// (This will get overwritten by the custom format if it is provided, else kept.)
		ColorFormat = Driver->getColorFormat();
		setPitch(d3dformat);

		// Use color format if provided.
		if(format != ECF_UNKNOWN)
		{
			ColorFormat = format;
			d3dformat = Driver->getD3DFormatFromColorFormat(format);
			setPitch(d3dformat); // This will likely set pitch to 0 for now.
		}
	}
	else
	{
		d3dformat = Driver->getD3DFormatFromColorFormat(ColorFormat);
	}

	switch (ColorFormat)
	{
	case ECF_A8R8G8B8:
	case ECF_A1R5G5B5:
	case ECF_A16B16G16R16F:
	case ECF_A32B32G32R32F:
		HasAlpha = true;
		break;
	default:
		break;
	}

	// create texture
	DWORD usage = (IImage::isDepthFormat(ColorFormat)) ? D3DUSAGE_DEPTHSTENCIL : D3DUSAGE_RENDERTARGET;

	HRESULT hr = Device->CreateTexture(Size.Width, Size.Height, 1, usage, d3dformat, D3DPOOL_DEFAULT, &Texture, NULL);

	if (FAILED(hr))
	{
		if (D3DERR_INVALIDCALL == hr)
			os::Printer::log("Could not create render target texture", "Invalid Call");
		else
		if (D3DERR_OUTOFVIDEOMEMORY == hr)
			os::Printer::log("Could not create render target texture", "Out of Video Memory");
		else
		if (E_OUTOFMEMORY == hr)
			os::Printer::log("Could not create render target texture", "Out of Memory");
		else
			os::Printer::log("Could not create render target texture");
	}
}


bool CD3D9Texture::createMipMaps(u32 level)
{
	if (level==0)
		return true;

	if (HardwareMipMaps && Texture)
	{
		// generate mipmaps in hardware
		Texture->GenerateMipSubLevels();
		return true;
	}

	// manual mipmap generation
	IDirect3DSurface9* upperSurface = 0;
	IDirect3DSurface9* lowerSurface = 0;

	// get upper level
	HRESULT hr = Texture->GetSurfaceLevel(level-1, &upperSurface);
	if (FAILED(hr) || !upperSurface)
	{
		os::Printer::log("Could not get upper surface level for mip map generation", ELL_WARNING);
		return false;
	}

	// get lower level
	hr = Texture->GetSurfaceLevel(level, &lowerSurface);
	if (FAILED(hr) || !lowerSurface)
	{
		os::Printer::log("Could not get lower surface level for mip map generation", ELL_WARNING);
		upperSurface->Release();
		return false;
	}

	D3DSURFACE_DESC upperDesc, lowerDesc;
	upperSurface->GetDesc(&upperDesc);
	lowerSurface->GetDesc(&lowerDesc);

	D3DLOCKED_RECT upperlr;
	D3DLOCKED_RECT lowerlr;

	// lock upper surface
	if (FAILED(upperSurface->LockRect(&upperlr, NULL, 0)))
	{
		upperSurface->Release();
		lowerSurface->Release();
		os::Printer::log("Could not lock upper texture for mip map generation", ELL_WARNING);
		return false;
	}

	// lock lower surface
	if (FAILED(lowerSurface->LockRect(&lowerlr, NULL, 0)))
	{
		upperSurface->UnlockRect();
		upperSurface->Release();
		lowerSurface->Release();
		os::Printer::log("Could not lock lower texture for mip map generation", ELL_WARNING);
		return false;
	}

	if (upperDesc.Format != lowerDesc.Format)
	{
		os::Printer::log("Cannot copy mip maps with different formats.", ELL_WARNING);
	}
	else
	{
		if ((upperDesc.Format == D3DFMT_A1R5G5B5) || (upperDesc.Format == D3DFMT_R5G6B5))
			copy16BitMipMap((char*)upperlr.pBits, (char*)lowerlr.pBits,
					upperDesc.Width, upperDesc.Height,
					lowerDesc.Width, lowerDesc.Height,
					upperlr.Pitch, lowerlr.Pitch);
		else
		if (upperDesc.Format == D3DFMT_A8R8G8B8)
			copy32BitMipMap((char*)upperlr.pBits, (char*)lowerlr.pBits,
					upperDesc.Width, upperDesc.Height,
					lowerDesc.Width, lowerDesc.Height,
					upperlr.Pitch, lowerlr.Pitch);
		else
			os::Printer::log("Unsupported mipmap format, cannot copy.", ELL_WARNING);
	}

	bool result=true;
	// unlock
	if (FAILED(upperSurface->UnlockRect()))
		result=false;
	if (FAILED(lowerSurface->UnlockRect()))
		result=false;

	// release
	upperSurface->Release();
	lowerSurface->Release();

	if (!result || (upperDesc.Width <= 3 && upperDesc.Height <= 3))
		return result; // stop generating levels

	// generate next level
	return createMipMaps(level+1);
}


//! creates the hardware texture
bool CD3D9Texture::createTexture(u32 flags, IImage * image)
{
	OriginalSize = image->getDimension();

	core::dimension2d<u32> optSize = OriginalSize.getOptimalSize(!Driver->queryFeature(EVDF_TEXTURE_NPOT), !Driver->queryFeature(EVDF_TEXTURE_NSQUARE), true, Driver->Caps.MaxTextureWidth);

	D3DFORMAT format = D3DFMT_A1R5G5B5;

	switch(getTextureFormatFromFlags(flags))
	{
	case ETCF_ALWAYS_16_BIT:
		format = D3DFMT_A1R5G5B5; break;
	case ETCF_ALWAYS_32_BIT:
		format = D3DFMT_A8R8G8B8; break;
	case ETCF_OPTIMIZED_FOR_QUALITY:
		{
			switch(image->getColorFormat())
			{
			case ECF_R8G8B8:
			case ECF_A8R8G8B8:
				format = D3DFMT_A8R8G8B8; break;
			case ECF_A1R5G5B5:
			case ECF_R5G6B5:
				format = D3DFMT_A1R5G5B5; break;
			}
		}
		break;
	case ETCF_OPTIMIZED_FOR_SPEED:
		format = D3DFMT_A1R5G5B5;
		break;
	default:
		break;
	}
	if (Driver->getTextureCreationFlag(video::ETCF_NO_ALPHA_CHANNEL))
	{
		if (format == D3DFMT_A8R8G8B8)
			format = D3DFMT_R8G8B8;
		else if (format == D3DFMT_A1R5G5B5)
			format = D3DFMT_R5G6B5;
	}

	if (IImage::isCompressedFormat(image->getColorFormat()))
	{
		ColorFormat = image->getColorFormat();

		switch(ColorFormat)
		{
			case ECF_DXT1:
				format = D3DFMT_DXT1;
				break;
			case ECF_DXT2:
				format = D3DFMT_DXT2;
				break;
			case ECF_DXT3:
				format = D3DFMT_DXT3;
				break;
			case ECF_DXT4:
				format = D3DFMT_DXT4;
				break;
			case ECF_DXT5:
				format = D3DFMT_DXT5;
				break;
			default:
				break;
		}

		IsCompressed = true;
	}

	bool mipmaps = Driver->getTextureCreationFlag(video::ETCF_CREATE_MIP_MAPS);

	if (IsCompressed && !image->hasMipMaps())
		mipmaps = false;

	DWORD usage = 0;

	// This enables hardware mip map generation.
	if (!IsCompressed && mipmaps && Driver->queryFeature(EVDF_MIP_MAP_AUTO_UPDATE))
	{
		LPDIRECT3D9 intf = Driver->getExposedVideoData().D3D9.D3D9;
		D3DDISPLAYMODE d3ddm;
		intf->GetAdapterDisplayMode(Driver->Params.DisplayAdapter, &d3ddm);

		if (D3D_OK==intf->CheckDeviceFormat(Driver->Params.DisplayAdapter,D3DDEVTYPE_HAL,d3ddm.Format,D3DUSAGE_AUTOGENMIPMAP,D3DRTYPE_TEXTURE,format))
		{
			usage = D3DUSAGE_AUTOGENMIPMAP;
			HardwareMipMaps = true;
		}
	}

	HRESULT hr = Device->CreateTexture(optSize.Width, optSize.Height,
		mipmaps ? 0 : 1, // number of mipmaplevels (0 = automatic all)
		usage, // usage
		format, D3DPOOL_MANAGED , &Texture, NULL);

	if (!IsCompressed && FAILED(hr))
	{
		// try brute force 16 bit
		HardwareMipMaps = false;
		if (format == D3DFMT_A8R8G8B8)
			format = D3DFMT_A1R5G5B5;
		else if (format == D3DFMT_R8G8B8)
			format = D3DFMT_R5G6B5;
		else
			return false;

		hr = Device->CreateTexture(optSize.Width, optSize.Height,
			mipmaps ? 0 : 1, // number of mipmaplevels (0 = automatic all)
			0, format, D3DPOOL_MANAGED, &Texture, NULL);
	}

	if (!IsCompressed)
		ColorFormat = Driver->getColorFormatFromD3DFormat(format);

	switch (ColorFormat)
	{
	case ECF_A8R8G8B8:
	case ECF_A1R5G5B5:
	case ECF_DXT1:
	case ECF_DXT2:
	case ECF_DXT3:
	case ECF_DXT4:
	case ECF_DXT5:
	case ECF_A16B16G16R16F:
	case ECF_A32B32G32R32F:
		HasAlpha = true;
		break;
	default:
		break;
	}

	setPitch(format);

	return (SUCCEEDED(hr));
}


//! copies the image to the texture
bool CD3D9Texture::copyTexture(IImage * image)
{
	if (Texture && image)
	{
		D3DSURFACE_DESC desc;
		Texture->GetLevelDesc(0, &desc);

		Size.Width = desc.Width;
		Size.Height = desc.Height;

		D3DLOCKED_RECT rect;
		HRESULT hr = Texture->LockRect(0, &rect, 0, 0);
		if (FAILED(hr))
		{
			os::Printer::log("Texture data not copied", "Could not LockRect D3D9 Texture.", ELL_ERROR);
			return false;
		}

		if (IsCompressed)
		{
			u32 compressedDataSize = 0;

			if(ColorFormat == ECF_DXT1)
				compressedDataSize = ((Size.Width + 3) / 4) * ((Size.Height + 3) / 4) * 8;
			else if (ColorFormat == ECF_DXT2 || ColorFormat == ECF_DXT3 || ColorFormat == ECF_DXT4 || ColorFormat == ECF_DXT5)
				compressedDataSize = ((Size.Width + 3) / 4) * ((Size.Height + 3) / 4) * 16;

			memcpy(rect.pBits, image->lock(), compressedDataSize);
		}
		else
		{
			Pitch = rect.Pitch;
			image->copyToScaling(rect.pBits, Size.Width, Size.Height, ColorFormat, Pitch);
		}

		hr = Texture->UnlockRect(0);
		if (FAILED(hr))
		{
			os::Printer::log("Texture data not copied", "Could not UnlockRect D3D9 Texture.", ELL_ERROR);
			return false;
		}
	}

	return true;
}


//! lock function
void* CD3D9Texture::lock(E_TEXTURE_LOCK_MODE mode, u32 mipmapLevel)
{
	if (IsCompressed) // TO-DO
		return 0;

	if (!Texture)
		return 0;

	MipLevelLocked=mipmapLevel;
	HRESULT hr;
	D3DLOCKED_RECT rect;
	if(!IsRenderTarget)
	{
		hr = Texture->LockRect(mipmapLevel, &rect, 0, (mode==ETLM_READ_ONLY)?D3DLOCK_READONLY:0);
		if (FAILED(hr))
		{
			os::Printer::log("Could not lock DIRECT3D9 Texture.", ELL_ERROR);
			return 0;
		}
	}
	else
	{
		if (!RTTSurface)
		{
			// Make RTT surface large enough for all miplevels (including 0)
			D3DSURFACE_DESC desc;
			Texture->GetLevelDesc(0, &desc);
			hr = Device->CreateOffscreenPlainSurface(desc.Width, desc.Height, desc.Format, D3DPOOL_SYSTEMMEM, &RTTSurface, 0);
			if (FAILED(hr))
			{
				os::Printer::log("Could not lock DIRECT3D9 Texture", "Offscreen surface creation failed.", ELL_ERROR);
				return 0;
			}
		}

		IDirect3DSurface9 *surface = 0;
		hr = Texture->GetSurfaceLevel(mipmapLevel, &surface);
		if (FAILED(hr))
		{
			os::Printer::log("Could not lock DIRECT3D9 Texture", "Could not get surface.", ELL_ERROR);
			return 0;
		}
		hr = Device->GetRenderTargetData(surface, RTTSurface);
		surface->Release();
		if(FAILED(hr))
		{
			os::Printer::log("Could not lock DIRECT3D9 Texture", "Data copy failed.", ELL_ERROR);
			return 0;
		}
		hr = RTTSurface->LockRect(&rect, 0, (mode==ETLM_READ_ONLY)?D3DLOCK_READONLY:0);
		if(FAILED(hr))
		{
			os::Printer::log("Could not lock DIRECT3D9 Texture", "LockRect failed.", ELL_ERROR);
			return 0;
		}
	}
	return rect.pBits;
}


//! unlock function
void CD3D9Texture::unlock()
{
	if (IsCompressed) // TO-DO
		return;

	if (!Texture)
		return;

	if (!IsRenderTarget)
		Texture->UnlockRect(MipLevelLocked);
	else if (RTTSurface)
		RTTSurface->UnlockRect();
}


//! returns the DIRECT3D9 Texture
IDirect3DTexture9* CD3D9Texture::getDX9Texture() const
{
	return Texture;
}


void CD3D9Texture::copy16BitMipMap(char* src, char* tgt,
				   const s32 srcWidth, const s32 srcHeight,
				   const s32 width, const s32 height,
				   const s32 pitchsrc, const s32 pitchtgt) const
{
	const s32 dy_max = (srcHeight==1?1:2);
	const s32 dx_max = (srcWidth==1?1:2);
	const s32 blockcount= dx_max*dy_max;
	for (s32 y=0; y<height; ++y)
	{
		for (s32 x=0; x<width; ++x)
		{
			u32 a=0, r=0, g=0, b=0;
			for (s32 dy=0; dy<dy_max; ++dy)
			{
				const s32 tgy = (y*2)+dy;
				for (s32 dx=0; dx<dx_max; ++dx)
				{
					const s32 tgx = (x*2)+dx;

					SColor c;
					if (ColorFormat == ECF_A1R5G5B5)
						c = A1R5G5B5toA8R8G8B8(*(u16*)(&src[(tgx*2)+(tgy*pitchsrc)]));
					else
						c = R5G6B5toA8R8G8B8(*(u16*)(&src[(tgx*2)+(tgy*pitchsrc)]));

					a += c.getAlpha();
					r += c.getRed();
					g += c.getGreen();
					b += c.getBlue();
				}
			}

			a /= blockcount;
			r /= blockcount;
			g /= blockcount;
			b /= blockcount;

			u16 c;
			if (ColorFormat == ECF_A1R5G5B5)
				c = RGBA16(r,g,b,a);
			else
				c = A8R8G8B8toR5G6B5(SColor(a,r,g,b).color);
			*(u16*)(&tgt[(x*2)+(y*pitchtgt)]) = c;
		}
	}
}


void CD3D9Texture::copy32BitMipMap(char* src, char* tgt,
				   const s32 srcWidth, const s32 srcHeight,
				   const s32 width, const s32 height,
				   const s32 pitchsrc, const s32 pitchtgt) const
{
	const s32 dy_max = (srcHeight==1?1:2);
	const s32 dx_max = (srcWidth==1?1:2);
	const s32 blockcount= dx_max*dy_max;
	for (s32 y=0; y<height; ++y)
	{
		for (s32 x=0; x<width; ++x)
		{
			u32 a=0, r=0, g=0, b=0;
			SColor c;

			for (s32 dy=0; dy<dy_max; ++dy)
			{
				const s32 tgy = (y*2)+dy;
				for (s32 dx=0; dx<dx_max; ++dx)
				{
					const s32 tgx = (x*2)+dx;

					c = *(u32*)(&src[(tgx*4)+(tgy*pitchsrc)]);

					a += c.getAlpha();
					r += c.getRed();
					g += c.getGreen();
					b += c.getBlue();
				}
			}

			a /= blockcount;
			r /= blockcount;
			g /= blockcount;
			b /= blockcount;

			c.set(a, r, g, b);
			*(u32*)(&tgt[(x*4)+(y*pitchtgt)]) = c.color;
		}
	}
}


//! Regenerates the mip map levels of the texture. Useful after locking and
//! modifying the texture
void CD3D9Texture::regenerateMipMapLevels(void* mipmapData)
{
	if (!HasMipMaps)
		return;

	if (IsCompressed && !mipmapData)
		return;

	if (mipmapData)
	{
		u32 compressedDataSize = 0;
		core::dimension2du size = Size;
		u32 level=0;
		do
		{
			if (size.Width>1)
				size.Width /=2;
			if (size.Height>1)
				size.Height /=2;
			++level;

			IDirect3DSurface9* mipSurface = 0;
			HRESULT hr = Texture->GetSurfaceLevel(level, &mipSurface);
			if (FAILED(hr) || !mipSurface)
			{
				os::Printer::log("Could not get mipmap level", ELL_WARNING);
				return;
			}
			D3DSURFACE_DESC mipDesc;
			mipSurface->GetDesc(&mipDesc);
			D3DLOCKED_RECT miplr;

			// lock mipmap surface
			if (FAILED(mipSurface->LockRect(&miplr, NULL, 0)))
			{
				mipSurface->Release();
				os::Printer::log("Could not lock texture", ELL_WARNING);
				return;
			}

			if (IsCompressed)
			{
				if(ColorFormat == ECF_DXT1)
					compressedDataSize = ((size.Width + 3) / 4) * ((size.Height + 3) / 4) * 8;
				else if (ColorFormat == ECF_DXT2 || ColorFormat == ECF_DXT3 || ColorFormat == ECF_DXT4 || ColorFormat == ECF_DXT5)
					compressedDataSize = ((size.Width + 3) / 4) * ((size.Height + 3) / 4) * 16;

				memcpy(miplr.pBits, mipmapData, compressedDataSize);
				mipmapData = static_cast<u8*>(mipmapData)+compressedDataSize;
			}
			else
			{
				memcpy(miplr.pBits, mipmapData, size.getArea()*getPitch() / Size.Width);
				mipmapData = (u8*)mipmapData + size.getArea()*getPitch() / Size.Width;
			}

			// unlock
			mipSurface->UnlockRect();
			// release
			mipSurface->Release();
		} while (size.Width != 1 || size.Height != 1);
	}
	else if (HasMipMaps)
	{
		createMipMaps();
	}
}


void CD3D9Texture::setPitch(D3DFORMAT d3dformat)
{
	switch(d3dformat)
	{
	case D3DFMT_X1R5G5B5:
	case D3DFMT_A1R5G5B5:
		Pitch = Size.Width * 2;
	break;
	case D3DFMT_A8B8G8R8:
	case D3DFMT_A8R8G8B8:
	case D3DFMT_X8R8G8B8:
		Pitch = Size.Width * 4;
	break;
	case D3DFMT_R5G6B5:
		Pitch = Size.Width * 2;
	break;
	case D3DFMT_R8G8B8:
		Pitch = Size.Width * 3;
	break;
	case D3DFMT_DXT1:
		Pitch = Size.Width * 2;
	break;
	case D3DFMT_DXT2:
	case D3DFMT_DXT3:
	case D3DFMT_DXT4:
	case D3DFMT_DXT5:
		Pitch = Size.Width * 4;
	default:
		Pitch = 0;
	};
}


} // end namespace video
} // end namespace irr

#endif // _IRR_COMPILE_WITH_DIRECT3D_9_
