#if defined(OS_MAC_OS_X) || defined(OS_IOS)
#include <malloc/malloc.h>
#else
#include <malloc.h>
#endif
#include <assert.h>
#include "Combiner.h"
#include "FrameBuffer.h"
#include "DepthBuffer.h"
#include "VI.h"
#include "Config.h"
#include "DebugDump.h"
#include <Graphics/Context.h>
#include <Graphics/Parameters.h>
#include "DisplayWindow.h"

using namespace graphics;

DepthBuffer::DepthBuffer()
{
	m_copyFBO = gfxContext.createFramebuffer();
}

DepthBuffer::~DepthBuffer()
{
	gfxContext.deleteFramebuffer(m_depthRenderbuffer);
	gfxContext.deleteFramebuffer(m_copyFBO);
	gfxContext.deleteFramebuffer(m_ZTextureClearFBO);
	gfxContext.deleteFramebuffer(m_DeltaZTextureClearFBO);

	textureCache().removeFrameBufferTexture(m_pDepthImageZTexture);
	textureCache().removeFrameBufferTexture(m_pDepthImageDeltaZTexture);
	textureCache().removeFrameBufferTexture(m_pDepthBufferTexture);
	textureCache().removeFrameBufferTexture(m_pResolveDepthBufferTexture);
	textureCache().removeFrameBufferTexture(m_pDepthBufferCopyTexture);
}

void DepthBuffer::_initDepthImageTexture(FrameBuffer * _pBuffer, CachedTexture& _cachedTexture, graphics::ObjectHandle & _clearFBO)
{
	const FramebufferTextureFormats & fbTexFormat = gfxContext.getFramebufferTextureFormats();

	_cachedTexture.width = _pBuffer->m_pTexture->width;
	_cachedTexture.height = _pBuffer->m_pTexture->height;
	_cachedTexture.format = 0;
	_cachedTexture.size = 2;
	_cachedTexture.clampS = 1;
	_cachedTexture.clampT = 1;
	_cachedTexture.address = _pBuffer->m_startAddress;
	_cachedTexture.clampWidth = static_cast<u16>(_pBuffer->m_width);
	_cachedTexture.clampHeight = static_cast<u16>(_pBuffer->m_height);
	_cachedTexture.frameBufferTexture = CachedTexture::fbOneSample;
	_cachedTexture.maskS = 0;
	_cachedTexture.maskT = 0;
	_cachedTexture.mirrorS = 0;
	_cachedTexture.mirrorT = 0;
	_cachedTexture.textureBytes = _cachedTexture.width * _cachedTexture.height * fbTexFormat.depthImageFormatBytes;

	{
		Context::InitTextureParams params;
		params.handle = _cachedTexture.name;
		params.width = _cachedTexture.width;
		params.height = _cachedTexture.height;
		params.internalFormat = fbTexFormat.depthImageInternalFormat;
		params.format = fbTexFormat.depthImageFormat;
		params.dataType = fbTexFormat.depthImageType;
		gfxContext.init2DTexture(params);
	}
	{
		Context::TexParameters params;
		params.handle = _cachedTexture.name;
		params.target = textureTarget::TEXTURE_2D;
		params.textureUnitIndex = textureIndices::Tex[0];
		params.minFilter = textureParameters::FILTER_NEAREST;
		params.magFilter = textureParameters::FILTER_NEAREST;
		gfxContext.setTextureParameters(params);
	}
	{
		Context::FrameBufferRenderTarget targetParams;
		targetParams.bufferHandle = _clearFBO;
		targetParams.bufferTarget = bufferTarget::DRAW_FRAMEBUFFER;
		targetParams.attachment = bufferAttachment::COLOR_ATTACHMENT0;
		targetParams.textureHandle = _cachedTexture.name;
		targetParams.textureTarget = textureTarget::TEXTURE_2D;
		gfxContext.addFrameBufferRenderTarget(targetParams);
	}
}

void DepthBuffer::initDepthImageTexture(FrameBuffer * _pBuffer)
{
	if (config.frameBufferEmulation.N64DepthCompare == Config::dcDisable || m_pDepthImageZTexture != nullptr)
		return;

	m_pDepthImageZTexture = textureCache().addFrameBufferTexture(textureTarget::TEXTURE_2D);
	m_ZTextureClearFBO = gfxContext.createFramebuffer();
	m_pDepthImageDeltaZTexture = textureCache().addFrameBufferTexture(textureTarget::TEXTURE_2D);
	m_DeltaZTextureClearFBO = gfxContext.createFramebuffer();

	_initDepthImageTexture(_pBuffer, *m_pDepthImageZTexture, m_ZTextureClearFBO);
	_initDepthImageTexture(_pBuffer, *m_pDepthImageDeltaZTexture, m_DeltaZTextureClearFBO);

	depthBufferList().clearBuffer();
}

void DepthBuffer::_initDepthBufferTexture(const FrameBuffer * _pBuffer, CachedTexture * _pTexture, bool _multisample)
{
	const FramebufferTextureFormats & fbTexFormat = gfxContext.getFramebufferTextureFormats();

	if (_pBuffer != nullptr) {
		_pTexture->width = _pBuffer->m_pTexture->width;
		_pTexture->height = _pBuffer->m_pTexture->height;
		_pTexture->address = _pBuffer->m_startAddress;
		_pTexture->clampWidth = static_cast<u16>(_pBuffer->m_width);
		_pTexture->clampHeight = VI_GetMaxBufferHeight(static_cast<u16>(_pBuffer->m_width));
		_pTexture->hdRatioS = _pBuffer->m_scale;
		_pTexture->hdRatioT = _pBuffer->m_scale;
	} else {
		const u16 maxHeight = VI_GetMaxBufferHeight(static_cast<u16>(VI.width));
		if (config.frameBufferEmulation.nativeResFactor == 0) {
			_pTexture->width = static_cast<u16>(dwnd().getWidth());
			_pTexture->height = static_cast<u16>(static_cast<u32>(static_cast<f32>(maxHeight) * dwnd().getScaleX()));
		} else {
			_pTexture->width = static_cast<u16>(VI.width * config.frameBufferEmulation.nativeResFactor);
			_pTexture->height = static_cast<u16>(maxHeight * config.frameBufferEmulation.nativeResFactor);
		}
		_pTexture->address = gDP.depthImageAddress;
		_pTexture->clampWidth = static_cast<u16>(VI.width);
		_pTexture->clampHeight = maxHeight;
	}
	_pTexture->format = 0;
	_pTexture->size = 2;
	_pTexture->clampS = 1;
	_pTexture->clampT = 1;
	_pTexture->frameBufferTexture = CachedTexture::fbOneSample;
	_pTexture->maskS = 0;
	_pTexture->maskT = 0;
	_pTexture->mirrorS = 0;
	_pTexture->mirrorT = 0;
	_pTexture->textureBytes = _pTexture->width * _pTexture->height * fbTexFormat.depthFormatBytes;

	Context::InitTextureParams initParams;
	initParams.handle = _pTexture->name;
	initParams.msaaLevel = _multisample ? config.video.multisampling : 0U;
	initParams.width = _pTexture->width;
	initParams.height = _pTexture->height;
	initParams.internalFormat = fbTexFormat.depthInternalFormat;
	initParams.format = fbTexFormat.depthFormat;
	initParams.dataType = fbTexFormat.depthType;
	gfxContext.init2DTexture(initParams);

	if (!_multisample) {
		_pTexture->frameBufferTexture = CachedTexture::fbOneSample;
		Context::TexParameters texParams;
		texParams.handle = _pTexture->name;
		texParams.target = textureTarget::TEXTURE_2D;
		texParams.textureUnitIndex = textureIndices::Tex[0];
		texParams.minFilter = textureParameters::FILTER_NEAREST;
		texParams.magFilter = textureParameters::FILTER_NEAREST;
		gfxContext.setTextureParameters(texParams);
	} else {
		_pTexture->frameBufferTexture = CachedTexture::fbMultiSample;
	}
}

void DepthBuffer::_initDepthBufferRenderbuffer(FrameBuffer * _pBuffer)
{
	if (m_depthRenderbuffer.isNotNull())
		return;
	u32 height;
	if (_pBuffer != nullptr) {
		m_depthRenderbufferWidth = _pBuffer->m_pTexture->width;
		height = _pBuffer->m_pTexture->height;
	} else {
		if (config.frameBufferEmulation.nativeResFactor == 0) {
			m_depthRenderbufferWidth = dwnd().getWidth();
			height = static_cast<u32>(static_cast<f32>(VI_GetMaxBufferHeight(static_cast<u16>(VI.width))) * dwnd().getScaleX());
		} else {
			m_depthRenderbufferWidth = VI.width * config.frameBufferEmulation.nativeResFactor;
			height = VI_GetMaxBufferHeight(static_cast<u16>(VI.width)) * config.frameBufferEmulation.nativeResFactor;
		}
	}

	m_depthRenderbuffer = gfxContext.createRenderbuffer();
	Context::InitRenderbufferParams params;
	params.handle = m_depthRenderbuffer;
	params.target = textureTarget::RENDERBUFFER;
	params.format = gfxContext.getFramebufferTextureFormats().depthInternalFormat;
	params.width = m_depthRenderbufferWidth;
	params.height = height;
	gfxContext.initRenderbuffer(params);
}

void DepthBuffer::setDepthAttachment(ObjectHandle _fbo, BufferTargetParam _target)
{
	Context::FrameBufferRenderTarget params;
	params.attachment = bufferAttachment::DEPTH_ATTACHMENT;
	params.bufferHandle = _fbo;
	params.bufferTarget = _target;
	if (Context::DepthFramebufferTextures) {
		params.textureHandle = m_pDepthBufferTexture->name;
		params.textureTarget = config.video.multisampling != 0 ? textureTarget::TEXTURE_2D_MULTISAMPLE : textureTarget::TEXTURE_2D;
	} else {
		params.textureHandle = m_depthRenderbuffer;
		params.textureTarget = textureTarget::RENDERBUFFER;
	}
	gfxContext.addFrameBufferRenderTarget(params);

	m_copied = false;
	m_resolved = false;
}

void DepthBuffer::initDepthBufferTexture(FrameBuffer * _pBuffer)
{
	if (Context::DepthFramebufferTextures) {
		if (m_pDepthBufferTexture == nullptr) {
			m_pDepthBufferTexture = textureCache().addFrameBufferTexture(config.video.multisampling != 0 ?
					textureTarget::TEXTURE_2D_MULTISAMPLE : textureTarget::TEXTURE_2D);
			_initDepthBufferTexture(_pBuffer, m_pDepthBufferTexture, config.video.multisampling != 0);
		}
	} else {
		_initDepthBufferRenderbuffer(_pBuffer);
	}

	if (config.video.multisampling != 0 && m_pResolveDepthBufferTexture == nullptr) {
		m_pResolveDepthBufferTexture = textureCache().addFrameBufferTexture(textureTarget::TEXTURE_2D);
		_initDepthBufferTexture(_pBuffer, m_pResolveDepthBufferTexture, false);
	}
}

CachedTexture * DepthBuffer::resolveDepthBufferTexture(FrameBuffer * _pBuffer)
{
	if (config.video.multisampling == 0)
		return m_pDepthBufferTexture;

	if (m_resolved)
		return m_pResolveDepthBufferTexture;

	Context::FrameBufferRenderTarget targetParams;
	targetParams.attachment = bufferAttachment::DEPTH_ATTACHMENT;
	targetParams.bufferHandle = _pBuffer->m_resolveFBO;
	targetParams.bufferTarget = bufferTarget::DRAW_FRAMEBUFFER;
	targetParams.textureHandle = m_pResolveDepthBufferTexture->name;
	targetParams.textureTarget = textureTarget::TEXTURE_2D;
	gfxContext.addFrameBufferRenderTarget(targetParams);

	Context::BlitFramebuffersParams blitParams;
	blitParams.readBuffer = _pBuffer->m_FBO;
	blitParams.drawBuffer = _pBuffer->m_resolveFBO;
	blitParams.srcX0 = 0;
	blitParams.srcY0 = 0;
	blitParams.srcX1 = m_pDepthBufferTexture->width;
	blitParams.srcY1 = m_pDepthBufferTexture->height;
	blitParams.dstX0 = 0;
	blitParams.dstY0 = 0;
	blitParams.dstX1 = m_pResolveDepthBufferTexture->width;
	blitParams.dstY1 = m_pResolveDepthBufferTexture->height;
	blitParams.mask = blitMask::DEPTH_BUFFER;
	blitParams.filter = textureParameters::FILTER_NEAREST;

	gfxContext.blitFramebuffers(blitParams);

	gfxContext.bindFramebuffer(bufferTarget::READ_FRAMEBUFFER, ObjectHandle::defaultFramebuffer);
	gfxContext.bindFramebuffer(bufferTarget::DRAW_FRAMEBUFFER, _pBuffer->m_FBO);

	m_resolved = true;
	return m_pResolveDepthBufferTexture;
}

void DepthBuffer::copyDepthBufferTexture(FrameBuffer * _pBuffer, CachedTexture *& _pTexture, graphics::ObjectHandle _copyFBO)
{
	if (_pTexture == nullptr) {
		_pTexture = textureCache().addFrameBufferTexture(textureTarget::TEXTURE_2D);
		_initDepthBufferTexture(_pBuffer, _pTexture, false);
	}

	Context::FrameBufferRenderTarget targetParams;
	targetParams.bufferHandle = _copyFBO;
	targetParams.bufferTarget = bufferTarget::DRAW_FRAMEBUFFER;
	targetParams.attachment = bufferAttachment::COLOR_ATTACHMENT0;
	targetParams.textureHandle = _pBuffer->m_pTexture->frameBufferTexture == CachedTexture::fbMultiSample ?
		_pBuffer->m_pResolveTexture->name :
		_pBuffer->m_pTexture->name;
	targetParams.textureTarget = textureTarget::TEXTURE_2D;

	gfxContext.addFrameBufferRenderTarget(targetParams);

	targetParams.attachment = bufferAttachment::DEPTH_ATTACHMENT;
	targetParams.textureHandle = _pTexture->name;

	gfxContext.addFrameBufferRenderTarget(targetParams);

	Context::BlitFramebuffersParams blitParams;
	blitParams.readBuffer = _pBuffer->m_FBO;
	blitParams.drawBuffer = _copyFBO;
	blitParams.srcX0 = blitParams.dstX0 = 0;
	blitParams.srcY0 = blitParams.dstY0 = 0;
	blitParams.srcX1 = blitParams.dstX1 = _pTexture->width;
	blitParams.srcY1 = blitParams.dstY1 = _pTexture->height;
	blitParams.mask = blitMask::DEPTH_BUFFER;
	blitParams.filter = textureParameters::FILTER_NEAREST;

	gfxContext.blitFramebuffers(blitParams);

	gfxContext.bindFramebuffer(bufferTarget::READ_FRAMEBUFFER, ObjectHandle::defaultFramebuffer);
	gfxContext.bindFramebuffer(bufferTarget::DRAW_FRAMEBUFFER, _pBuffer->m_FBO);
}

CachedTexture * DepthBuffer::copyDepthBufferTexture(FrameBuffer * _pBuffer)
{
	if (m_copied)
		return m_pDepthBufferCopyTexture;

	DepthBuffer::copyDepthBufferTexture(_pBuffer, m_pDepthBufferCopyTexture, m_copyFBO);
	m_copied = true;
	return m_pDepthBufferCopyTexture;
}

void DepthBuffer::activateDepthBufferTexture(FrameBuffer * _pBuffer)
{
	textureCache().activateTexture(0, resolveDepthBufferTexture(_pBuffer));
	gfxContext.textureBarrier();
}

void DepthBuffer::bindDepthImageTexture(ObjectHandle _fbo)
{
	if (Context::FramebufferFetchDepth) {
		Context::FrameBufferRenderTarget targetParams;
		targetParams.bufferHandle = _fbo;
		targetParams.bufferTarget = bufferTarget::DRAW_FRAMEBUFFER;
		targetParams.attachment = bufferAttachment::COLOR_ATTACHMENT1;
		targetParams.textureHandle = m_pDepthImageZTexture->name;
		targetParams.textureTarget = textureTarget::TEXTURE_2D;
		gfxContext.addFrameBufferRenderTarget(targetParams);

		targetParams.attachment = bufferAttachment::COLOR_ATTACHMENT2;
		targetParams.textureHandle = m_pDepthImageDeltaZTexture->name;
		gfxContext.addFrameBufferRenderTarget(targetParams);

		gfxContext.setDrawBuffers(3);
	} else if (Context::ImageTextures) {
		Context::BindImageTextureParameters bindParams;
		bindParams.imageUnit = textureImageUnits::DepthZ;
		bindParams.texture = m_pDepthImageZTexture->name;
		bindParams.accessMode = textureImageAccessMode::READ_WRITE;
		bindParams.textureFormat = gfxContext.getFramebufferTextureFormats().depthImageInternalFormat;
		gfxContext.bindImageTexture(bindParams);

		bindParams.imageUnit = textureImageUnits::DepthDeltaZ;
		bindParams.texture = m_pDepthImageDeltaZTexture->name;
		gfxContext.bindImageTexture(bindParams);
	}
}

DepthBufferList::DepthBufferList() : m_pCurrent(nullptr), m_pzLUT(nullptr)
{
	m_pzLUT = new u16[0x40000];
	for (u32 i = 0; i<0x40000; i++) {
		u32 exponent = 0;
		u32 testbit = 1 << 17;
		while ((i & testbit) && (exponent < 7)) {
			exponent++;
			testbit = 1 << (17 - exponent);
		}

		const u32 mantissa = (i >> (6 - (6 < exponent ? 6 : exponent))) & 0x7ff;
		m_pzLUT[i] = static_cast<u16>(((exponent << 11) | mantissa) << 2);
	}
}

DepthBufferList::~DepthBufferList()
{
	delete[] m_pzLUT;
	m_pzLUT = nullptr;
	m_list.clear();
}

DepthBufferList & DepthBufferList::get()
{
	static DepthBufferList depthBufferList;
	return depthBufferList;
}

void DepthBufferList::init()
{
	m_pCurrent = nullptr;
}

void DepthBufferList::destroy()
{
	m_pCurrent = nullptr;
	m_list.clear();
}

void DepthBufferList::setCleared(bool _cleared)
{
	for (DepthBuffers::iterator iter = m_list.begin(); iter != m_list.end(); ++iter)
		iter->m_cleared = _cleared;
}

DepthBuffer * DepthBufferList::findBuffer(u32 _address)
{
	for (DepthBuffers::iterator iter = m_list.begin(); iter != m_list.end(); ++iter)
		if (iter->m_address == _address)
				return &(*iter);
	return nullptr;
}

void DepthBufferList::removeBuffer(u32 _address )
{
	for (DepthBuffers::iterator iter = m_list.begin(); iter != m_list.end(); ++iter)
		if (iter->m_address == _address) {
			frameBufferList().clearDepthBuffer(&(*iter));
			m_list.erase(iter);
			return;
		}
}

void DepthBufferList::_createScreenSizeBuffer(u32 _address)
{
	FrameBuffer * pFrameBuffer = frameBufferList().findBuffer(VI.width*2);
	if (pFrameBuffer == nullptr)
		return;

	m_list.emplace_front();
	DepthBuffer & buffer = m_list.front();

	buffer.m_address = _address;
	buffer.m_width = pFrameBuffer->m_width;

	buffer.initDepthBufferTexture(pFrameBuffer);

	m_pCurrent = &buffer;
	frameBufferList().attachDepthBuffer();
	m_pCurrent = nullptr;
}

void DepthBufferList::saveBuffer(u32 _address)
{
	if (config.frameBufferEmulation.enable == 0) {
		if (m_list.empty())
			_createScreenSizeBuffer(_address);
		return;
	}

	FrameBuffer * pFrameBuffer = frameBufferList().findBuffer(_address);
	if (pFrameBuffer != nullptr)
		pFrameBuffer->m_isDepthBuffer = true;

	DepthBuffer * pDepthBuffer = nullptr;
	if (pFrameBuffer != nullptr && pFrameBuffer->m_startAddress != _address)
		pDepthBuffer = findBuffer(pFrameBuffer->m_startAddress);
	else
		pDepthBuffer = findBuffer(_address);

	if (pDepthBuffer != nullptr && pFrameBuffer != nullptr && pDepthBuffer->m_width != pFrameBuffer->m_width) {
		removeBuffer(_address);
		pDepthBuffer = nullptr;
	}

	if (pDepthBuffer == nullptr) {
		m_list.emplace_front();
		DepthBuffer & buffer = m_list.front();

		buffer.m_address = _address;
		buffer.m_width = pFrameBuffer != nullptr ? pFrameBuffer->m_width : VI.width;

		buffer.initDepthBufferTexture(pFrameBuffer);

		pDepthBuffer = &buffer;
	}

	DepthBuffer * pCurrent = m_pCurrent;
	m_pCurrent = pDepthBuffer;
	frameBufferList().attachDepthBuffer();
	if (pFrameBuffer == nullptr && (config.generalEmulation.hacks & hack_clearAloneDepthBuffer) != 0)
		clearBuffer();
	if (pDepthBuffer->m_address != gDP.depthImageAddress)
		m_pCurrent = pCurrent;
}

void DepthBufferList::clearBuffer()
{
	if (m_pCurrent != nullptr)
		m_pCurrent->m_cleared = true;

	if (config.frameBufferEmulation.enable == 0 || config.frameBufferEmulation.N64DepthCompare == Config::dcDisable) {
		gfxContext.clearDepthBuffer();
		return;
	}

	// Clear depth image texture
	FrameBuffer * pColorBuffer = frameBufferList().getCurrent();
	if (pColorBuffer == nullptr || pColorBuffer->m_pDepthBuffer == nullptr)
		return;
	DepthBuffer * pDepthBuffer = pColorBuffer->m_pDepthBuffer;

	//if (pColorBuffer->m_pTexture->realWidth == pDepthBuffer->m_pDepthImageZTexture->realWidth)
	{
		gfxContext.bindFramebuffer(bufferTarget::DRAW_FRAMEBUFFER, pDepthBuffer->m_ZTextureClearFBO);
		gfxContext.clearColorBuffer(1.0f, 0.0f, 0.0f, 0.0f);

		gfxContext.bindFramebuffer(bufferTarget::DRAW_FRAMEBUFFER, pDepthBuffer->m_DeltaZTextureClearFBO);
		gfxContext.clearColorBuffer(1.0f, 0.0f, 0.0f, 0.0f);
	}
	//else {
	//	gDP.rectColor.g = gDP.rectColor.b = gDP.rectColor.a = 0.0f;
	//	u32 cycleType = gDP.otherMode.cycleType;
	//	u32 fillcolor = gDP.fillColor.color;
	//	gDP.otherMode.cycleType = G_CYC_FILL;
	//	gDP.fillColor.color = 0;
	//	const int lrx = pColorBuffer->m_width;
	//	const int lry = VI_GetMaxBufferHeight(pColorBuffer->m_width);
	//	gDP.rectColor.r = 1.0f;
	//	gfxContext.bindFramebuffer(bufferTarget::DRAW_FRAMEBUFFER, pDepthBuffer->m_ZTextureClearFBO);
	//	dwnd().getDrawer().drawRect(0, 0, lrx, lry);
	//	gDP.rectColor.r = 0.0f;
	//	gfxContext.bindFramebuffer(bufferTarget::DRAW_FRAMEBUFFER, pDepthBuffer->m_DeltaZTextureClearFBO);
	//	dwnd().getDrawer().drawRect(0, 0, lrx, lry);
	//	gDP.otherMode.cycleType = cycleType;
	//	gDP.fillColor.color = fillcolor;
	//}
	frameBufferList().setCurrentDrawBuffer();
}

void DepthBuffer_Init()
{
	depthBufferList().init();
}

void DepthBuffer_Destroy()
{
	depthBufferList().destroy();
}
