/*
** graphics.cpp
**
** This file is part of mkxp.
**
** Copyright (C) 2013 Jonas Kulla <Nyocurio@gmail.com>
**
** mkxp is free software: you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation, either version 2 of the License, or
** (at your option) any later version.
**
** mkxp is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with mkxp.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "graphics.h"

#include "util.h"
#include "gl-util.h"
#include "globalstate.h"
#include "glstate.h"
#include "shader.h"
#include "scene.h"
#include "quad.h"
#include "eventthread.h"
#include "texpool.h"
#include "bitmap.h"
#include "etc-internal.h"
#include "binding.h"

#include "SDL2/SDL_video.h"
#include "SDL2/SDL_timer.h"

#include "stdint.h"

#include "SFML/System/Clock.hpp"

struct TimerQuery
{
	GLuint query;
	static bool queryActive;
	bool thisQueryActive;

	TimerQuery()
	    : thisQueryActive(false)
	{
		glGenQueries(1, &query);
	}

	void begin()
	{
		if (queryActive)
			return;

		if (thisQueryActive)
			return;

		glBeginQuery(GL_TIME_ELAPSED, query);
		queryActive = true;
		thisQueryActive = true;
	}

	void end()
	{
		if (!thisQueryActive)
			return;

		glEndQuery(GL_TIME_ELAPSED);
		queryActive = false;
		thisQueryActive = false;
	}

	bool getResult(GLuint64 *result)
	{
		if (thisQueryActive)
			return false;

		GLint isReady;
		glGetQueryObjectiv(query, GL_QUERY_RESULT_AVAILABLE, &isReady);

		if (isReady != GL_TRUE)
		{
//			qDebug() << "TimerQuery result not ready";
			return false;
		}

		glGetQueryObjectui64v(query, GL_QUERY_RESULT, result);

		if (glGetError() == GL_INVALID_OPERATION)
		{
			qDebug() << "Something went wrong with getting TimerQuery results";
			return false;
		}

		return true;
	}

	GLuint64 getResultSync()
	{
		if (thisQueryActive)
			return 0;

		GLuint64 result;
		GLint isReady = GL_FALSE;

		while (isReady == GL_FALSE)
			glGetQueryObjectiv(query, GL_QUERY_RESULT_AVAILABLE, &isReady);

		glGetQueryObjectui64v(query, GL_QUERY_RESULT, &result);

		return result;
	}

	~TimerQuery()
	{
		if (thisQueryActive)
			end();

		glDeleteQueries(1, &query);
	}
};

bool TimerQuery::queryActive = false;

struct GPUTimer
{
	TimerQuery queries[2];
	const int iter;

	uchar ind;
	uint64_t acc;
	int32_t counter;
	bool first;

	GPUTimer(int iter)
	    : iter(iter),
	      ind(0),
	      acc(0),
	      counter(0),
	      first(true)
	{}

	void startTiming()
	{
		queries[ind].begin();
	}

	void endTiming()
	{
		queries[ind].end();

		if (first)
		{
			first = false;
			return;
		}

		swapInd();

		GLuint64 result;
		if (!queries[ind].getResult(&result))
			return;

		acc += result;

		if (++counter < iter)
			return;

		qDebug() << "  Avg. GPU time:" << ((double) acc / (iter * 1000 * 1000)) << "ms";
		acc = counter = 0;
	}

	void swapInd()
	{
		ind = ind ? 0 : 1;
	}
};

struct CPUTimer
{
	const int iter;

	uint64_t acc;
	int32_t counter;
	sf::Clock clock;

	CPUTimer(int iter)
	    : iter(iter),
	      acc(0),
	      counter(0)
	{
	}

	void startTiming()
	{
		clock.restart();
	}

	void endTiming()
	{
		acc += clock.getElapsedTime().asMicroseconds();

		if (++counter < iter)
			return;

		qDebug() << "Avg. CPU time:" << ((double) acc / (iter * 1000)) << "ms";
		acc = counter = 0;
	}
};

struct PingPong
{
	TexFBO rt[2];
	unsigned srcInd, dstInd;
	int screenW, screenH;

	PingPong(int screenW, int screenH)
	    : srcInd(0), dstInd(1),
	      screenW(screenW), screenH(screenH)
	{
		for (int i = 0; i < 2; ++i)
		{
			TexFBO::init(rt[i]);
			TexFBO::allocEmpty(rt[i], screenW, screenH);
			TexFBO::linkFBO(rt[i]);
			glClearColor(0, 0, 0, 1);
			glClear(GL_COLOR_BUFFER_BIT);
		}
	}

	~PingPong()
	{
		for (int i = 0; i < 2; ++i)
			TexFBO::fini(rt[i]);
	}

	/* Binds FBO of last good buffer for reading */
	void bindLastBuffer()
	{
		FBO::bind(rt[dstInd].fbo, FBO::Read);
	}

	/* Better not call this during render cycles */
	void resize(int width, int height)
	{
		screenW = width;
		screenH = height;
		for (int i = 0; i < 2; ++i)
		{
			Tex::bind(rt[i].tex);
			Tex::allocEmpty(width, height);
		}
	}

	void startRender()
	{
		bind();
	}

	void swapRender()
	{
		swapIndices();

		/* Discard dest buffer */
		Tex::bind(rt[dstInd].tex);
		Tex::allocEmpty(screenW, screenH);

		bind();
	}

	void blitFBOs()
	{
		FBO::blit(0, 0, 0, 0, screenW, screenH);
	}

	void finishRender()
	{
		FBO::unbind(FBO::Draw);
		FBO::bind(rt[dstInd].fbo, FBO::Read);
	}

private:
	void bind()
	{
		Tex::bindWithMatrix(rt[srcInd].tex, screenW, screenH, true);
		FBO::bind(rt[srcInd].fbo, FBO::Read);
		FBO::bind(rt[dstInd].fbo, FBO::Draw);
	}

	void swapIndices()
	{
		unsigned tmp = srcInd;
		srcInd = dstInd;
		dstInd = tmp;
	}
};

class ScreenScene : public Scene
{
public:
	ScreenScene(int width, int height)
	    : pp(width, height),
	      brightEffect(false),
	      actW(width), actH(height)
	{
		updateReso(width, height);
		brightnessQuad.setColor(Vec4());
	}

	void composite()
	{
		const int w = geometry.rect.w;
		const int h = geometry.rect.h;

		gState->prepareDraw();

		pp.startRender();

		glState.setViewport(w, h);

		glClear(GL_COLOR_BUFFER_BIT);

		Scene::composite();

		if (brightEffect)
		{
			glState.texture2D.pushSet(false);
			brightnessQuad.draw();
			glState.texture2D.pop();
		}

		pp.finishRender();
	}

	void requestViewportRender(Vec4 &c, Vec4 &f, Vec4 &t)
	{
		pp.swapRender();
		pp.blitFBOs();

		SpriteShader &shader = gState->spriteShader();
		shader.bind();
		shader.resetUniforms();
		shader.setColor(c);
		shader.setFlash(f);
		shader.setTone(t);

		glState.blendMode.pushSet(BlendNone);

		Tex::bindMatrix(geometry.rect.w, geometry.rect.h);
		screenQuad.draw();

		glState.blendMode.pop();
		shader.unbind();
	}

	void setBrightness(float norm)
	{
		brightnessQuad.setColor(Vec4(0, 0, 0, 1.0 - norm));

		brightEffect = norm < 1.0;
	}

	void updateReso(int width, int height)
	{
		geometry.rect.w = width;
		geometry.rect.h = height;

		screenQuad.setTexPosRect(geometry.rect, geometry.rect);
		brightnessQuad.setTexPosRect(geometry.rect, geometry.rect);

		notifyGeometryChange();
	}

	void setResolution(int width, int height)
	{
		pp.resize(width, height);
		updateReso(width, height);
	}

	void setScreenSize(int width, int height)
	{
		actW = width;
		actH = height;
	}

	PingPong &getPP()
	{
		return pp;
	}

private:
	PingPong pp;
	Quad screenQuad;
	Quad brightnessQuad;
	bool brightEffect;
	int actW, actH;
};

struct FPSLimiter
{
	unsigned lastTickCount;
	unsigned mspf; /* ms per frame */

	FPSLimiter(unsigned desiredFPS)
	    : lastTickCount(SDL_GetTicks())
	{
		setDesiredFPS(desiredFPS);
	}

	void setDesiredFPS(unsigned value)
	{
		mspf = 1000 / value;
	}

	void delay()
	{
		unsigned tmpTicks = SDL_GetTicks();
		unsigned tickDelta = tmpTicks - lastTickCount;
		lastTickCount = tmpTicks;

		int toDelay = mspf - tickDelta;
		if (toDelay < 0)
			toDelay = 0;

		SDL_Delay(toDelay);
		lastTickCount = SDL_GetTicks();
	}
};

struct Timer
{
	uint64_t lastTicks;
	uint64_t acc;
	int counter;

	Timer()
	    : lastTicks(SDL_GetPerformanceCounter()),
	      acc(0),
	      counter(0)
	{}
};

struct GraphicsPrivate
{
	/* Screen resolution */
	Vec2i scRes;
	/* Actual screen size */
	Vec2i scSize;

	ScreenScene screen;
	RGSSThreadData *threadData;

	int frameRate;
	int frameCount;
	int brightness;

	FPSLimiter fpsLimiter;

	GPUTimer gpuTimer;
	CPUTimer cpuTimer;

	bool frozen;
	TexFBO frozenScene;
	TexFBO currentScene;
	Quad screenQuad;
	RBOFBO transBuffer;

	GraphicsPrivate()
	    : scRes(640, 480),
	      scSize(scRes),
	      screen(scRes.x, scRes.y),
	      frameRate(40),
	      frameCount(0),
	      brightness(255),
	      fpsLimiter(frameRate),
	      gpuTimer(frameRate),
	      cpuTimer(frameRate),
	      frozen(false)
	{
		TexFBO::init(frozenScene);
		TexFBO::allocEmpty(frozenScene, scRes.x, scRes.y);
		TexFBO::linkFBO(frozenScene);

		TexFBO::init(currentScene);
		TexFBO::allocEmpty(currentScene, scRes.x, scRes.y);
		TexFBO::linkFBO(currentScene);

		FloatRect screenRect(0, 0, scRes.x, scRes.y);
		screenQuad.setTexPosRect(screenRect, screenRect);

		RBOFBO::init(transBuffer);
		RBOFBO::allocEmpty(transBuffer, scRes.x, scRes.y);
		RBOFBO::linkFBO(transBuffer);
	}

	~GraphicsPrivate()
	{
		TexFBO::fini(frozenScene);
		TexFBO::fini(currentScene);

		RBOFBO::fini(transBuffer);
	}

	void updateScreenResoRatio()
	{
		Vec2 &ratio = gState->rtData().sizeResoRatio;
		ratio.x = (float) scRes.x / scSize.x;
		ratio.y = (float) scRes.y / scSize.y;
	}

	void checkResize()
	{
		if (threadData->windowSizeMsg.pollChange(&scSize.x, &scSize.y))
		{
			screen.setScreenSize(scSize.x, scSize.y);
			updateScreenResoRatio();
		}
	}

	void shutdown()
	{
		threadData->rqTermAck = true;
		gState->texPool().disable();

		scriptBinding->terminate();
	}

	void swapGLBuffer()
	{
		SDL_GL_SwapWindow(threadData->window);
		fpsLimiter.delay();

		++frameCount;
	}

	void compositeToBuffer(FBO::ID fbo)
	{
		screen.composite();
		FBO::bind(fbo, FBO::Draw);
		FBO::blit(0, 0, 0, 0, scRes.x, scRes.y);
	}

	void blitBuffer()
	{
		FBO::blit(0, 0, 0, 0, scRes.x, scRes.y);
	}

	void blitBufferScaled()
	{
		FBO::blit(0, 0, scRes.x, scRes.y, 0, 0, scSize.x, scSize.y);
	}

	void blitBufferFlippedScaled()
	{
		FBO::blit(0, 0, scRes.x, scRes.y, 0, scSize.y, scSize.x, -scSize.y);
	}

	/* Blits currently bound read FBO to screen (upside-down) */
	void blitToScreen()
	{
		FBO::unbind(FBO::Draw);
		glClear(GL_COLOR_BUFFER_BIT);
		blitBufferFlippedScaled();
	}

	void redrawScreen()
	{
		screen.composite();
		blitToScreen();

		swapGLBuffer();
	}
};

Graphics::Graphics(RGSSThreadData *data)
{
	p = new GraphicsPrivate;
	p->threadData = data;
}

Graphics::~Graphics()
{
	delete p;
}

void Graphics::update()
{
	gState->checkShutdown();

//	p->cpuTimer.endTiming();
//	p->gpuTimer.startTiming();

	if (p->frozen)
		return;

	p->checkResize();
	p->redrawScreen();

//	p->gpuTimer.endTiming();
//	p->cpuTimer.startTiming();
}

void Graphics::wait(int duration)
{
	for (int i = 0; i < duration; ++i)
	{
		gState->checkShutdown();
		p->checkResize();
		p->redrawScreen();
	}
}

void Graphics::fadeout(int duration)
{
	if (p->frozen)
		FBO::bind(p->frozenScene.fbo, FBO::Read);

	for (int i = duration-1; i > -1; --i)
	{
		setBrightness((255.0 / duration) * i);

		if (p->frozen)
		{
			p->blitToScreen();
			p->swapGLBuffer();
		}
		else
		{
			update();
		}
	}
}

void Graphics::fadein(int duration)
{
	if (p->frozen)
		FBO::bind(p->frozenScene.fbo, FBO::Read);

	for (int i = 0; i < duration; ++i)
	{
		setBrightness((255.0 / duration) * i);

		if (p->frozen)
		{
			p->blitToScreen();
			p->swapGLBuffer();
		}
		else
		{
			update();
		}
	}
}

void Graphics::freeze()
{
	p->frozen = true;

	gState->checkShutdown();
	p->checkResize();

	/* Capture scene into frozen buffer */
	p->compositeToBuffer(p->frozenScene.fbo);
}

void Graphics::transition(int duration,
                          const char *filename,
                          int vague)
{
	vague = clamp(vague, 0, 512);
	Bitmap *transMap = filename ? new Bitmap(filename) : 0;

	setBrightness(255);

	/* Capture new scene */
	p->compositeToBuffer(p->currentScene.fbo);

	/* If no transition bitmap is provided,
	 * we can use a simplified shader */
	if (transMap)
	{
		TransShader &shader = gState->transShader();
		shader.bind();
		shader.setFrozenScene(p->frozenScene.tex);
		shader.setCurrentScene(p->currentScene.tex);
		shader.setTransMap(transMap->getGLTypes().tex);
		shader.setVague(vague / 512.0f);
	}
	else
	{
		SimpleTransShader &shader = gState->sTransShader();
		shader.bind();
		shader.setFrozenScene(p->frozenScene.tex);
		shader.setCurrentScene(p->currentScene.tex);
	}

	Tex::bindMatrix(p->scRes.x, p->scRes.y);

	glState.blendMode.pushSet(BlendNone);

	for (int i = 0; i < duration; ++i)
	{
		if (p->threadData->rqTerm)
		{
			FragShader::unbind();
			delete transMap;
			p->shutdown();
		}

		const float prog = i * (1.0 / duration);

		if (transMap)
			gState->transShader().setProg(prog);
		else
			gState->sTransShader().setProg(prog);

		/* Draw the composed frame to a buffer first
		 * (we need this because we're skipping PingPong) */
		FBO::bind(p->transBuffer.fbo);
		glClear(GL_COLOR_BUFFER_BIT);
		p->screenQuad.draw();

		p->checkResize();

		/* Then blit it flipped and scaled to the screen */
		FBO::bind(p->transBuffer.fbo, FBO::Read);
		p->blitToScreen();

		p->swapGLBuffer();
	}

	glState.blendMode.pop();

	FragShader::unbind();

	delete transMap;

	p->frozen = false;
}

Bitmap *Graphics::snapToBitmap()
{
	Bitmap *bitmap = new Bitmap(width(), height());

	p->compositeToBuffer(bitmap->getGLTypes().fbo);

	return bitmap;
}

void Graphics::frameReset()
{

}

int Graphics::width() const
{
	return p->scRes.x;
}

int Graphics::height() const
{
	return p->scRes.y;
}

void Graphics::resizeScreen(int width, int height)
{
	width = clamp(width, 1, 640);
	height = clamp(height, 1, 480);

	Vec2i size(width, height);

	if (p->scRes == size)
		return;

	gState->eThread().requestWindowResize(width, height);

	p->scRes = size;

	p->screen.setResolution(width, height);

	Tex::bind(p->frozenScene.tex);
	Tex::allocEmpty(width, height);
	Tex::bind(p->currentScene.tex);
	Tex::allocEmpty(width, height);

	FloatRect screenRect(0, 0, width, height);
	p->screenQuad.setTexPosRect(screenRect, screenRect);

	RBO::bind(p->transBuffer.rbo);
	RBO::allocEmpty(width, height);

	p->updateScreenResoRatio();
}

#undef RET_IF_DISP
#define RET_IF_DISP(x)

#undef CHK_DISP
#define CHK_DISP

DEF_ATTR_RD_SIMPLE(Graphics, FrameRate, int, p->frameRate)
DEF_ATTR_RD_SIMPLE(Graphics, Brightness, int, p->brightness)

DEF_ATTR_SIMPLE(Graphics, FrameCount, int, p->frameCount)

void Graphics::setFrameRate(int value)
{
	p->frameRate = clamp(value, 10, 120);
	p->fpsLimiter.setDesiredFPS(p->frameRate);
}

void Graphics::setBrightness(int value)
{
	value = clamp(value, 0, 255);

	if (p->brightness == value)
		return;

	p->brightness = value;
	p->screen.setBrightness(value / 255.0);
}

bool Graphics::getFullscreen() const
{
	return p->threadData->ethread->getFullscreen();
}

void Graphics::setFullscreen(bool value)
{
	p->threadData->ethread->requestFullscreenMode(value);
}

Scene *Graphics::getScreen() const
{
	return &p->screen;
}

void Graphics::repaintWait(volatile bool *exitCond)
{
	if (*exitCond)
		return;

	/* Repaint the screen with the last good frame we drew */
	p->screen.getPP().bindLastBuffer();
	FBO::unbind(FBO::Draw);

	while (!*exitCond)
	{
		gState->checkShutdown();

		glClear(GL_COLOR_BUFFER_BIT);
		p->blitBufferFlippedScaled();
		SDL_GL_SwapWindow(p->threadData->window);
		p->fpsLimiter.delay();
	}
}