#ifdef _WIN32
#include <windows.h> // for GetTempPathA, MAX_PATH
#else
#include <limits.h>  // for PATH_MAX
#endif
#include <stdio.h>
#include <QMessageBox>
#include <QOpenGLFunctions_3_3_Core>
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
#include <QOpenGLVersionFunctionsFactory>
#endif
#include "previewWidget.hpp"
#include "debug.hpp"

static const GLfloat rectangle[] = {
	 1.0f,  1.0f,  1.0f,  0.0f,
	-1.0f,  1.0f,  0.0f,  0.0f,
	-1.0f, -1.0f,  0.0f,  1.0f,

	 1.0f,  1.0f,  1.0f,  0.0f,
	-1.0f, -1.0f,  0.0f,  1.0f,
	 1.0f, -1.0f,  1.0f,  1.0f
};

static const char *vert_src = R"END(
#version 330 core
layout(location = 0) in vec2 vert_pos;
layout(location = 1) in vec2 vert_uv;

out vec2 UV;

uniform vec2 scale_vec;

void main(){
	gl_Position = vec4(vert_pos * scale_vec, 0.0f, 1.0f);
	UV = vert_uv;
}
)END";

static const char *frag_src = R"END(
#version 330 core
in vec2 UV;
out vec4 color;
uniform sampler2D tex;
void main(){
	color = vec4(texture(tex, UV).rgb, 1.0f);
}
)END";

static void compileShader(GLuint shaderId, QOpenGLFunctions_3_3_Core *f){
	f->glCompileShader(shaderId);

	GLint ret = GL_FALSE;
	int len;

	f->glGetShaderiv(shaderId, GL_COMPILE_STATUS, &ret);
	f->glGetShaderiv(shaderId, GL_INFO_LOG_LENGTH, &len);
	if (len > 0){
		std::vector<char> errorMsg(len+1);
		f->glGetShaderInfoLog(shaderId, len, NULL, &errorMsg[0]);
		printf("%s\n", errorMsg.data());
	}
}


static unsigned char pixels[] = {
	255, 0, 0,   0, 255, 0,   0, 0, 255,   255, 255, 255,
	255, 0, 0,   0, 255, 0,   0, 0, 255,   255, 255, 255,
	255, 0, 0,   0, 255, 0,   0, 0, 255,   255, 255, 255,
	255, 0, 0,   0, 255, 0,   0, 0, 255,   255, 255, 255
};

static QOpenGLFunctions_3_3_Core *getOpenGLFuncs(){
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
	return QOpenGLContext::currentContext()->versionFunctions<QOpenGLFunctions_3_3_Core>();
#else
	return QOpenGLVersionFunctionsFactory::get<QOpenGLFunctions_3_3_Core>(QOpenGLContext::currentContext());
#endif
}

PreviewWidget::PreviewWidget(QWidget *parent) :
	QOpenGLWidget(parent),
	ipc_frame(ipc_frame_new())
{
	connect(&timer, SIGNAL(timeout()), this, SLOT(update()));
}

PreviewWidget::~PreviewWidget(){
	makeCurrent();

	auto f = getOpenGLFuncs();
	f->glDeleteBuffers(1, &vertexBuffer);
	f->glDeleteProgram(program);
	f->glDeleteTextures(1, &frame_texture);
	f->glDeleteTextures(1, &testbars_texture);
	vao.destroy();
	doneCurrent();
}

void PreviewWidget::initializeGL(){
	auto f = getOpenGLFuncs();

	if(!f) {
		QMessageBox warningBox(this);
		warningBox.setText("OpenGL 3.3 is required!");
		warningBox.exec();
		exit(1);
	}

	vao.create();
	vao.bind();

	f->glClearColor(0.0f, 0.0f, 0.0f, 1.0f);

	f->glDisable(GL_DEPTH_TEST);
	f->glDisable(GL_SCISSOR_TEST);
	f->glDisable(GL_STENCIL_TEST);

	GLuint vertexShader = f->glCreateShader(GL_VERTEX_SHADER);
	GLuint fragShader = f->glCreateShader(GL_FRAGMENT_SHADER);

	f->glShaderSource(vertexShader, 1, &vert_src, NULL);
	compileShader(vertexShader, f);
	f->glShaderSource(fragShader, 1, &frag_src, NULL);
	compileShader(fragShader, f);

	program = f->glCreateProgram();
	f->glAttachShader(program, vertexShader);
	f->glAttachShader(program, fragShader);
	f->glLinkProgram(program);
	f->glUseProgram(program);

	f->glDetachShader(program, vertexShader);
	f->glDetachShader(program, fragShader);
	f->glDeleteShader(vertexShader);
	f->glDeleteShader(fragShader);

	f->glGenTextures(1, &testbars_texture);
	f->glBindTexture(GL_TEXTURE_2D, testbars_texture);
	f->glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 4, 4, 0, GL_RGB, GL_UNSIGNED_BYTE, pixels);
	f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

	f->glGenTextures(1, &frame_texture);
	f->glBindTexture(GL_TEXTURE_2D, frame_texture);
	f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	f->glPixelStorei(GL_UNPACK_ALIGNMENT, 1); //UltraGrid formats dont have padding between rows

	f->glBindTexture(GL_TEXTURE_2D, 0);

	f->glGenBuffers(1, &vertexBuffer);
	f->glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
	f->glBufferData(GL_ARRAY_BUFFER, sizeof(rectangle), rectangle, GL_STATIC_DRAW);

	f->glEnableVertexAttribArray(0);
	f->glVertexAttribPointer(
			0,
			2,
			GL_FLOAT,
			GL_FALSE,
			4 * sizeof(float),
			(void*)0
			);
	f->glEnableVertexAttribArray(1);
	f->glVertexAttribPointer(
			1,
			2,
			GL_FLOAT,
			GL_FALSE,
			4 * sizeof(float),
			(void*)(2 * sizeof(float))
			);

	f->glBindBuffer(GL_ARRAY_BUFFER, 0);

	vao.release();

	scaleVec[0] = 0.75f;
	scaleVec[1] = 0.5;

	vidW = 1280;
	vidH = 720;
}

void PreviewWidget::calculateScale(){
	double videoAspect = (double) vidW / vidH;
	double widgetAspect = (double) width / height;

	if(videoAspect > widgetAspect){
		float scale = widgetAspect / videoAspect;
		scaleVec[0] = 1;
		scaleVec[1] = scale;
	} else {
		float scale = videoAspect / widgetAspect;
		scaleVec[0] = scale;
		scaleVec[1] = 1;
	}
}

void PreviewWidget::resizeGL(int w, int h){
	width = w;
	height = h;

	calculateScale();
}

void PreviewWidget::setVidSize(int w, int h){
	if(vidW == w && vidH == h)
		return;

	vidW = w;
	vidH = h;
	calculateScale();
}

bool PreviewWidget::loadFrame(){
	auto f = getOpenGLFuncs();

	if(!ipc_frame_reader || !ipc_frame_reader_is_connected(ipc_frame_reader.get())){
		selected_texture = testbars_texture;
		return true;
	}


	if(ipc_frame_reader_has_frame(ipc_frame_reader.get())){
		if(!ipc_frame_reader_read(ipc_frame_reader.get(), ipc_frame.get()))
			return false;

		assert(ipc_frame->header.color_spec == IPC_FRAME_COLOR_RGB);
		selected_texture = frame_texture;
		f->glBindTexture(GL_TEXTURE_2D, frame_texture);
		f->glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB,
				ipc_frame->header.width, ipc_frame->header.height,
				0, GL_RGB, GL_UNSIGNED_BYTE, ipc_frame->data);
		setVidSize(ipc_frame->header.width, ipc_frame->header.height);
	}

	return true;
}

void PreviewWidget::render(){
	auto f = getOpenGLFuncs();

	vao.bind();
	f->glClear(GL_COLOR_BUFFER_BIT);

	f->glUseProgram(program);

	f->glBindTexture(GL_TEXTURE_2D, selected_texture);
	GLuint loc;
	loc = f->glGetUniformLocation(program, "scale_vec");
	f->glUniform2fv(loc, 1, scaleVec);

	f->glDrawArrays(GL_TRIANGLES, 0, 6);

	vao.release();
}

void PreviewWidget::paintGL(){
	loadFrame();
	render();
}

static const char *get_temp_dir(void)
{
#ifdef _WIN32
        static __thread char temp_dir[MAX_PATH + 1];
        if (GetTempPathA(sizeof temp_dir, temp_dir) == 0) {
                return NULL;
        }
#else
        static __thread char temp_dir[PATH_MAX];
        if (char *req_tmp_dir = getenv("TMPDIR")) {
                temp_dir[sizeof temp_dir - 1] = '\0';
                strncpy(temp_dir, req_tmp_dir, sizeof temp_dir - 1);
        } else {
                strcpy(temp_dir, P_tmpdir);
        }
        strncat(temp_dir, "/", sizeof temp_dir - strlen(temp_dir) - 1);
#endif

        return temp_dir;
}

void PreviewWidget::setKey(std::string_view key){
	std::string path = get_temp_dir();
	path += key;
	ipc_frame_reader.reset(ipc_frame_reader_new(path.c_str()));
}

void PreviewWidget::start(){
	timer.start(1000/24);
}

void PreviewWidget::stop(){
	timer.stop();
}
