/*
    QGL Framebuffer
    Ivan Pelipenko peri4ko@yandex.ru

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program 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 Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "glframebuffer.h"

#include <QOpenGLExtraFunctions>
#include <QTime>


Framebuffer::Framebuffer(QOpenGLExtraFunctions * f_, int colorAttachments_, bool withDepth, GLenum colorFormat_, GLenum _target)
	: f(f_)
	, pbo(GL_PIXEL_PACK_BUFFER, GL_STREAM_DRAW) {
	is_depth = withDepth;
	target_  = _target;
	color_formats.fill(colorFormat_, colorAttachments_);
	colors.fill(0, colorAttachments_);
	fbo = drbo = 0;
	tex_d      = 0;
	wid = hei   = 0;
	pbo_queried = 0;
	is_changed  = false;
}


Framebuffer::Framebuffer(QOpenGLExtraFunctions * f_, QVector<GLenum> colors_, bool withDepth, GLenum _target)
	: f(f_)
	, pbo(GL_PIXEL_PACK_BUFFER, GL_STREAM_DRAW) {
	is_depth      = withDepth;
	target_       = _target;
	color_formats = colors_;
	colors.fill(0, colors_.size());
	fbo = drbo = 0;
	tex_d      = 0;
	wid = hei   = 0;
	pbo_queried = 0;
	is_changed  = false;
}


Framebuffer::~Framebuffer() {
	if (fbo > 0) deleteGLFramebuffer(fbo);
	deleteGLRenderbuffer(drbo);
	for (int i = 0; i < colors.size(); ++i)
		deleteGLTexture(f, colors[i]);
	deleteGLTexture(f, tex_d);
}


void Framebuffer::resize(int width, int height, bool force) {
	if (fbo > 0) {
		if ((wid == width) && (hei == height) && !force) return;
	}
	wid = width;
	hei = height;
	if (fbo > 0) deleteGLFramebuffer(fbo);
	f->glGenFramebuffers(1, &fbo);
	f->glBindFramebuffer(GL_FRAMEBUFFER, fbo);
	// qDebug() << "resize" << f << wid << hei << fbo << colors.size();
	for (int i = 0; i < colors.size(); ++i) {
		deleteGLTexture(f, colors[i]);
		createGLTexture(f, colors[i], width, height, color_formats[i], target_);
		f->glTexParameteri(target_, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
		f->glTexParameteri(target_, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
		f->glTexParameteri(target_, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
		f->glTexParameteri(target_, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
		f->glTexParameteri(target_, GL_TEXTURE_MAX_LEVEL, 4);
		f->glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + i, target_, colors[i], 0);
	}
	if (is_depth) {
		deleteGLTexture(f, tex_d);
		if (drbo > 0) deleteGLRenderbuffer(drbo);
		f->glGenRenderbuffers(1, &drbo);
		f->glBindRenderbuffer(GL_RENDERBUFFER, drbo);
		f->glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, width, height);
		f->glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, drbo);
		createGLTexture(f, tex_d, width, height, GL_DEPTH_COMPONENT);
		f->glTexParameteri(target_, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
		f->glTexParameteri(target_, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
		f->glTexParameteri(target_, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
		f->glTexParameteri(target_, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
		f->glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, target_, tex_d, 0);
		// f->glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, target_, tex_d, 0);
	}
	f->glBindFramebuffer(GL_FRAMEBUFFER, 0);
	if (pbo.isInit()) {
		enablePixelBuffer();
	}
	is_changed = false;
}


void Framebuffer::reinit(QOpenGLExtraFunctions * f_) {
	if (f_) f = f_;
	pbo.reinit();
	colors.fill(0);
	fbo = drbo  = 0;
	tex_d       = 0;
	pbo_queried = 0;
	is_changed  = true;
}


QImage Framebuffer::grab() const {
	return QImage();
}


void Framebuffer::queryPoint(int index, QPoint p) {
	pbo_queried = 0;
	if (index < 0 || index >= colors.size()) return;
	if (!rect().contains(p) || !pbo.isInit()) return;
	f->glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
	f->glReadBuffer(GL_COLOR_ATTACHMENT0 + index);
	pbo.bind(f);
	f->glReadPixels(p.x(), height() - p.y(), 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, 0);
	pbo_queried = 1;
	pbo.release(f);
}


void Framebuffer::queryPoints(int index, QRect rect_, GLenum pixel_format) {
	pbo_queried = 0;
	if (index < 0 || index >= colors.size()) return;
	rect_ &= rect();
	if (rect_.isEmpty() || !pbo.isInit()) return;
	f->glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
	f->glReadBuffer(GL_COLOR_ATTACHMENT0 + index);
	pbo.bind(f);
	f->glReadPixels(rect_.x(), height() - rect_.bottom(), rect_.width(), rect_.height(), GL_RGBA, pixel_format, 0);
	pbo_queried = rect_.width() * rect_.height();
	pbo.release(f);
}


void Framebuffer::queryImage(int index) {
	queryPoints(index, rect());
}


uint Framebuffer::getPoint() const {
	if (!pbo.isInit() || (pbo_queried == 0)) return 0;
	uint ret = 0;
	pbo.bind(f);
	void * map = pbo.map(f, GL_MAP_READ_BIT, sizeof(uint));
	if (map) memcpy(&ret, map, sizeof(uint));
	pbo.unmap(f);
	pbo.release(f);
	return ret;
}


QVector<uint> Framebuffer::getPointsByte() const {
	QVector<uint> ret;
	if (!pbo.isInit() || (pbo_queried == 0)) return ret;
	ret.resize(pbo_queried);
	pbo.bind(f);
	void * map = pbo.map(f, GL_MAP_READ_BIT, pbo_queried * sizeof(uint));
	if (map) memcpy(ret.data(), map, pbo_queried * sizeof(uint));
	pbo.unmap(f);
	pbo.release(f);
	return ret;
}


QVector<QVector4D> Framebuffer::getPointsFloat() const {
	QVector<QVector4D> ret;
	if (!pbo.isInit() || (pbo_queried == 0)) return ret;
	ret.resize(pbo_queried);
	pbo.bind(f);
	void * map = pbo.map(f, GL_MAP_READ_BIT, pbo_queried * sizeof(QVector4D));
	if (map) memcpy(ret.data(), map, pbo_queried * sizeof(QVector4D));
	pbo.unmap(f);
	pbo.release(f);
	return ret;
}


QImage Framebuffer::getImage() const {
	QImage ret;
	if (!pbo.isInit() || (pbo_queried == 0)) return ret;
	ret       = QImage(size(), QImage::Format_RGBA8888);
	int bytes = width() * height() * 4;
	pbo.bind(f);
	void * map = pbo.map(f, GL_MAP_READ_BIT, bytes);
	if (map) memcpy(ret.bits(), map, bytes);
	pbo.unmap(f);
	pbo.release(f);
	return ret;
}


QVector<float> Framebuffer::grabF(int index) const {
	QVector<float> ret;
	if (index < 0 || index >= colors.size()) return ret;
	f->glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
	f->glReadBuffer(GL_COLOR_ATTACHMENT0 + index);
	ret.resize(wid * hei * 4);
	f->glReadPixels(0, 0, wid, hei, GL_RGBA, GL_FLOAT, ret.data());
	return ret;
}


void Framebuffer::blit(int index_from, GLuint fb_to, int index_to, QRect from, QRect to, GLbitfield mask, GLenum filter) const {
	if (index_from < 0 || index_from >= colors.size()) return;
	GLenum e = GL_COLOR_ATTACHMENT0 + index_to;
	f->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fb_to);
	f->glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
	f->glReadBuffer(GL_COLOR_ATTACHMENT0 + index_from);
	f->glDrawBuffers(1, &e);
	f->glBlitFramebuffer(from.x(), from.y(), from.right(), from.bottom(), to.x(), to.y(), to.right(), to.bottom(), mask, filter);
}


void Framebuffer::bind() {
	if (is_changed) resize(wid, hei);
	if (fbo == 0) return;
	f->glGetIntegerv(GL_VIEWPORT, prev_view);
	f->glBindFramebuffer(GL_FRAMEBUFFER, fbo);
	setWriteBuffers();
	f->glReadBuffer(GL_COLOR_ATTACHMENT0);
	f->glViewport(0, 0, wid, hei);
}


void Framebuffer::release() {
	is_changed = false;
	if (fbo == 0) return;
	f->glBindFramebuffer(GL_FRAMEBUFFER, 0);
	f->glViewport(prev_view[0], prev_view[1], prev_view[2], prev_view[3]);
}


void Framebuffer::setWriteBuffer(int index) {
	unsetWriteBuffers();
	GLenum e = GL_COLOR_ATTACHMENT0 + index;
	f->glDrawBuffers(1, &e);
}


void Framebuffer::setWriteBuffers(const int * indeces, int count) {
	unsetWriteBuffers();
	QVector<GLenum> buffers;
	for (int i = 0; i < count; ++i)
		buffers << GL_COLOR_ATTACHMENT0 + indeces[i];
	f->glDrawBuffers(buffers.size(), buffers.constData());
}


void Framebuffer::setWriteBuffers() {
	QVector<GLenum> buffers;
	for (int i = 0; i < colors.size(); ++i)
		buffers << GL_COLOR_ATTACHMENT0 + i;
	f->glDrawBuffers(buffers.size(), buffers.constData());
}


void Framebuffer::unsetWriteBuffers() {
	QVector<GLenum> buffers(colors.size(), GL_NONE);
	f->glDrawBuffers(buffers.size(), buffers.constData());
}


void Framebuffer::enablePixelBuffer() {
	pbo.init(f);
	pbo.bind(f);
	pbo.resize(f, width() * height() * 4 * 4);
	pbo.release(f);
}


void Framebuffer::setColorTextureFiltering(int index, GLenum filter) {
	bindColorTexture(index);
	f->glTexParameteri(target_, GL_TEXTURE_MIN_FILTER, filter);
	f->glTexParameteri(target_, GL_TEXTURE_MAG_FILTER, filter);
}


void Framebuffer::bindColorTexture(int index, int channel) {
	if (index < 0 || index >= colors.size()) return;
	f->glActiveTexture(GL_TEXTURE0 + channel);
	f->glBindTexture(GL_TEXTURE_2D, colors[index]);
}


void Framebuffer::bindColorTextures(const QVector<int> & indeces) {
	for (int i = indeces.size() - 1; i >= 0; --i) {
		f->glActiveTexture(GL_TEXTURE0 + i);
		f->glBindTexture(GL_TEXTURE_2D, colors[indeces[i]]);
	}
}


void Framebuffer::bindColorTextures() {
	for (int i = colors.size() - 1; i >= 0; --i) {
		f->glActiveTexture(GL_TEXTURE0 + i);
		f->glBindTexture(GL_TEXTURE_2D, colors[i]);
	}
}


void Framebuffer::bindDepthTexture(int channel) {
	f->glActiveTexture(GL_TEXTURE0 + channel);
	f->glBindTexture(GL_TEXTURE_2D, tex_d);
}


void Framebuffer::deleteGLRenderbuffer(GLuint & drbo) {
	if (drbo != 0) f->glDeleteRenderbuffers(1, &drbo);
	drbo = 0;
}


void Framebuffer::deleteGLFramebuffer(GLuint & fbo) {
	if (fbo != 0) f->glDeleteFramebuffers(1, &fbo);
	fbo = 0;
}
