#include <assert.h>

#include "N64.h"
#include "gSP.h"
#include "PostProcessor.h"
#include "FrameBuffer.h"
#include "Config.h"
#include "VI.h"

#include <Graphics/Context.h>
#include <Graphics/Parameters.h>
#include "DisplayWindow.h"

using namespace graphics;

PostProcessor::PostProcessor()
	: m_pTextureOriginal(nullptr)
{}

void PostProcessor::_createResultBuffer(const FrameBuffer * _pMainBuffer)
{
	m_pResultBuffer.reset(new FrameBuffer());
	m_pResultBuffer->m_width = _pMainBuffer->m_width;
	m_pResultBuffer->m_height = _pMainBuffer->m_height;
	m_pResultBuffer->m_scale = _pMainBuffer->m_scale;

	const CachedTexture * pMainTexture = _pMainBuffer->m_pTexture;
	CachedTexture * pTexture = m_pResultBuffer->m_pTexture;
	pTexture->format = G_IM_FMT_RGBA;
	pTexture->clampS = 1;
	pTexture->clampT = 1;
	pTexture->frameBufferTexture = CachedTexture::fbOneSample;
	pTexture->maskS = 0;
	pTexture->maskT = 0;
	pTexture->mirrorS = 0;
	pTexture->mirrorT = 0;
	pTexture->width = pMainTexture->width;
	pTexture->height = pMainTexture->height;
	pTexture->textureBytes = pTexture->width * pTexture->height * 4;

	Context::InitTextureParams initParams;
	initParams.handle = pTexture->name;
	initParams.width = pTexture->width;
	initParams.height = pTexture->height;
	initParams.internalFormat = gfxContext.convertInternalTextureFormat(u32(internalcolorFormat::RGBA8));
	initParams.format = colorFormat::RGBA;
	initParams.dataType = datatype::UNSIGNED_BYTE;
	gfxContext.init2DTexture(initParams);

	Context::TexParameters setParams;
	setParams.handle = pTexture->name;
	setParams.target = textureTarget::TEXTURE_2D;
	setParams.minFilter = textureParameters::FILTER_NEAREST;
	setParams.magFilter = textureParameters::FILTER_NEAREST;
	gfxContext.setTextureParameters(setParams);

	Context::FrameBufferRenderTarget bufTarget;
	bufTarget.bufferHandle = m_pResultBuffer->m_FBO;
	bufTarget.bufferTarget = bufferTarget::DRAW_FRAMEBUFFER;
	bufTarget.attachment = bufferAttachment::COLOR_ATTACHMENT0;
	bufTarget.textureTarget = textureTarget::TEXTURE_2D;
	bufTarget.textureHandle = pTexture->name;
	gfxContext.addFrameBufferRenderTarget(bufTarget);
	assert(!gfxContext.isFramebufferError());
}

void PostProcessor::init()
{
	m_gammaCorrectionProgram.reset(gfxContext.createGammaCorrectionShader());
	m_postprocessingList.emplace_front(std::mem_fn(&PostProcessor::_doGammaCorrection)); // std::mem_fn to fix compilation with VS 2013
	if (config.video.fxaa != 0) {
		m_FXAAProgram.reset(gfxContext.createFXAAShader());
		m_postprocessingList.emplace_front(std::mem_fn(&PostProcessor::_doFXAA));
	}
}

void PostProcessor::destroy()
{
	m_postprocessingList.clear();
	m_gammaCorrectionProgram.reset();
	m_FXAAProgram.reset();
	m_pResultBuffer.reset();
}

const PostProcessor::PostprocessingList & PostProcessor::getPostprocessingList() const
{
	return m_postprocessingList;
}

PostProcessor & PostProcessor::get()
{
	static PostProcessor processor;
	return processor;
}

void PostProcessor::_preDraw(FrameBuffer * _pBuffer)
{
	if (!m_pResultBuffer || m_pResultBuffer->m_width != _pBuffer->m_width || m_pResultBuffer->m_height != _pBuffer->m_height ||
		m_pResultBuffer->m_scale != _pBuffer->m_scale)
		_createResultBuffer(_pBuffer);

	if (_pBuffer->m_pTexture->frameBufferTexture == CachedTexture::fbMultiSample) {
		_pBuffer->resolveMultisampledTexture(true);
		m_pTextureOriginal = _pBuffer->m_pResolveTexture;
	} else
		m_pTextureOriginal = _pBuffer->m_pTexture;

	gfxContext.bindFramebuffer(bufferTarget::READ_FRAMEBUFFER,
		ObjectHandle::defaultFramebuffer);
}

void PostProcessor::_postDraw()
{
	gfxContext.bindFramebuffer(bufferTarget::DRAW_FRAMEBUFFER,
		ObjectHandle::defaultFramebuffer);

	gfxContext.resetShaderProgram();
}

FrameBuffer * PostProcessor::_doPostProcessing(FrameBuffer * _pBuffer, graphics::ShaderProgram * _pShader)
{
	_preDraw(_pBuffer);

	gfxContext.bindFramebuffer(bufferTarget::DRAW_FRAMEBUFFER,
		ObjectHandle(m_pResultBuffer->m_FBO));

	CachedTexture * pDstTex = m_pResultBuffer->m_pTexture;
	GraphicsDrawer::CopyRectParams copyParams;
	copyParams.srcX0 = 0;
	copyParams.srcY0 = 0;
	copyParams.srcX1 = m_pTextureOriginal->width;
	copyParams.srcY1 = m_pTextureOriginal->height;
	copyParams.srcWidth = m_pTextureOriginal->width;
	copyParams.srcHeight = m_pTextureOriginal->height;
	copyParams.dstX0 = 0;
	copyParams.dstY0 = 0;
	copyParams.dstX1 = pDstTex->width;
	copyParams.dstY1 = pDstTex->height;
	copyParams.dstWidth = pDstTex->width;
	copyParams.dstHeight = pDstTex->height;
	copyParams.tex[0] = m_pTextureOriginal;
	copyParams.filter = textureParameters::FILTER_NEAREST;
	copyParams.combiner = _pShader;

	dwnd().getDrawer().copyTexturedRect(copyParams);

	_postDraw();
	return m_pResultBuffer.get();
}

FrameBuffer * PostProcessor::_doGammaCorrection(FrameBuffer * _pBuffer)
{
	if (_pBuffer == nullptr)
		return nullptr;

	if (((*REG.VI_STATUS & VI_STATUS_GAMMA_ENABLED) | config.gammaCorrection.force) == 0)
		return _pBuffer;

	return _doPostProcessing(_pBuffer, m_gammaCorrectionProgram.get());
}

FrameBuffer * PostProcessor::_doFXAA(FrameBuffer * _pBuffer)
{
	if (_pBuffer == nullptr)
		return nullptr;

	if (config.video.fxaa == 0)
		return _pBuffer;

	return _doPostProcessing(_pBuffer, m_FXAAProgram.get());
}
