// This source code is property of the Computer Graphics and Visualization
// chair of the TU Dresden. Do not distribute!
// Copyright (C) CGV TU Dresden - All Rights Reserved

#include "Viewer.h"

#include <nanogui/window.h>
#include <nanogui/layout.h>
#include <nanogui/checkbox.h>

#include <gui/SliderHelper.h>

#include <stb_image.h>

#include "glsl.h"
#include "textures.h"

#include <fstream>
#include <iostream>
#include <sstream>
#include <cmath>

constexpr uint32_t PATCH_SIZE = 256; //number of vertices along one side of the terrain patch
constexpr int max_offset = 5;

Viewer::Viewer()
	: AbstractViewer("CG1 Exercise 2"),
	  terrainPositions(nse::gui::VertexBuffer), terrainIndices(nse::gui::IndexBuffer),
	  offsetBuffer(nse::gui::VertexBuffer)
{
	LoadShaders();
	CreateGeometry();

	grass_texture_location = terrainShader.uniform("grass");
	rock_texture_location = terrainShader.uniform("rock");
	alpha_texture_location = terrainShader.uniform("alpha");
	road_texture_location = terrainShader.uniform("road");
	road_specular_location = terrainShader.uniform("specular_road");
	offset_buffer_location = terrainShader.attrib("offset");
	road_normal_location = terrainShader.attrib("normal_road");

	//Create a texture and framebuffer for the background
	glGenFramebuffers(1, &backgroundFBO);
	glGenTextures(1, &backgroundTexture);

	//Align camera to view a reasonable part of the terrain
	camera().SetSceneExtent(nse::math::BoundingBox<float, 3>(Eigen::Vector3f(0, 0, 0), Eigen::Vector3f(PATCH_SIZE - 1, 0, PATCH_SIZE - 1)));
	camera().FocusOnPoint(0.5f * Eigen::Vector3f(PATCH_SIZE - 1, 15, PATCH_SIZE - 1));
	camera().Zoom(-30);
	camera().RotateAroundFocusPointLocal(Eigen::AngleAxisf(-0.5f, Eigen::Vector3f::UnitY()) * Eigen::AngleAxisf(-0.05f, Eigen::Vector3f::UnitX()));
	camera().FixClippingPlanes(0.1, 1000);

	for (int j = -max_offset; j < max_offset; j++)
		for (int i = -max_offset; i < max_offset; i++)
		{
			offsets.emplace_back((float)(i * (int32_t)PATCH_SIZE), (float)(j * (int32_t)PATCH_SIZE));
		}
}

bool Viewer::resizeEvent(const Eigen::Vector2i &)
{
	//Re-generate the texture and FBO for the background
	glBindFramebuffer(GL_FRAMEBUFFER, backgroundFBO);
	glBindTexture(GL_TEXTURE_2D, backgroundTexture);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width(), height(), 0, GL_RGB, GL_UNSIGNED_BYTE, nullptr);
	glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, backgroundTexture, 0);
	auto fboStatus = glCheckFramebufferStatus(GL_FRAMEBUFFER);
	if (fboStatus != GL_FRAMEBUFFER_COMPLETE)
		std::cout << "Warning: Background framebuffer is not complete: " << fboStatus << std::endl;
	glBindFramebuffer(GL_FRAMEBUFFER, 0);

	return false;
}

inline std::string loadShaderText(const char *path)
{
	std::ifstream is(path);
	std::string s_save;

	if (is.is_open())
	{
		s_save.assign(std::istreambuf_iterator<char>(is), std::istreambuf_iterator<char>());
	}
	else
	{
		std::cout << "could not open " << path << std::endl;
	}

	is.close();
	return s_save;
}

void Viewer::LoadShaders()
{
	std::string sky_vs = loadShaderText("exercise2/glsl/sky.vert");
	std::string sky_fs = loadShaderText("exercise2/glsl/sky.frag");

	std::string terrain_vs = loadShaderText("exercise2/glsl/terrain.vert");
	std::string terrain_fs = loadShaderText("exercise2/glsl/terrain.frag");

	skyShader.init("Sky Shader", sky_vs, sky_fs);
	terrainShader.init("Terrain Shader", terrain_vs, terrain_fs);
}

GLuint CreateTexture(const unsigned char *fileData, size_t fileLength, bool repeat = true)
{
	GLuint textureName;
	int textureWidth, textureHeight, textureChannels;
	auto pixelData = stbi_load_from_memory(fileData, fileLength, &textureWidth, &textureHeight, &textureChannels, 3);

	GLenum provided_format;

	if (textureChannels == 1)
	{
		provided_format = GL_R;
	}
	else if (textureChannels == 2)
	{
		// wild assumption
		provided_format = GL_RG;
	}
	else if (textureChannels == 3)
	{
		provided_format = GL_RGB;
	}
	else if (textureChannels == 4)
	{
		provided_format = GL_RGBA;
	}
	else
	{
		// not handled format
		abort();
	}

	glGenTextures(1, &textureName);
	glBindTexture(GL_TEXTURE_2D, textureName);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, textureWidth, textureHeight, 0, provided_format, GL_UNSIGNED_BYTE, pixelData);

	glGenerateMipmap(GL_TEXTURE_2D);

	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR_MIPMAP_LINEAR);

	if (repeat)
	{
		glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
		glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
	}
	else
	{
		glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
		glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
	}

	stbi_image_free(pixelData);
	return textureName;
}

std::string vec4_to_str(Eigen::Vector4f &v)
{
	std::stringstream ss;

	ss << "(" << v.x() << ", "
	   << v.y() << ", "
	   << v.z() << ", "
	   << v.w() << ")";

	return ss.str();
}

std::string vec3_to_str(Eigen::Vector3f &v)
{
	std::stringstream ss;

	ss << "(" << v.x() << ", "
	   << v.y() << ", "
	   << v.z() << ")";

	return ss.str();
}

void Viewer::CreateGeometry()
{
	//empty VAO for sky
	emptyVAO.generate();

	//terrain VAO
	terrainVAO.generate();
	terrainVAO.bind();

	std::vector<Eigen::Vector4f> positions;
	std::vector<uint32_t> indices;

	/*Generate positions and indices for a terrain patch with a
	  single triangle strip */

	for (int i = 0; i < PATCH_SIZE; i++)
	{
		for (int j = 0; j < PATCH_SIZE; j++)
		{
			positions.emplace_back((float)j, 0.0f, (float)i, 1.0f);
		}
	}

	// full x direction, decreased y direction by 1
	int count = (PATCH_SIZE) * (PATCH_SIZE - 1);

	int current_target = PATCH_SIZE - 1;
	// our increment value
	int current_direction = 1;
	int current_index = 0;
	int current_column = 0;

	for (int k = 0; k < count; k++)
	{
		indices.emplace_back(current_index);
		indices.emplace_back(current_index + PATCH_SIZE);

		if (current_index == current_target)
		{
			current_index += PATCH_SIZE;
			current_column++;

			if (current_direction == -1)
			{
				current_target = (PATCH_SIZE - 1) + current_column * PATCH_SIZE;
			}
			else if (current_direction == 1)
			{
				current_target += 1;
			}
			else
			{
				// we've done something wrong
				abort();
			}

			current_direction = -current_direction;
		}
		else
		{
			current_index += current_direction;
		}
	}

	terrainShader.bind();
	terrainPositions.uploadData(positions).bindToAttribute("position");
	terrainIndices.uploadData(indices.size() * sizeof(uint32_t), indices.data());

	//textures
	grassTexture = CreateTexture((unsigned char *)grass_jpg, grass_jpg_size);
	rockTexture = CreateTexture((unsigned char *)rock_jpg, rock_jpg_size);
	roadColorTexture = CreateTexture((unsigned char *)roadcolor_jpg, roadcolor_jpg_size);
	roadNormalMap = CreateTexture((unsigned char *)roadnormals_jpg, roadnormals_jpg_size);
	roadSpecularMap = CreateTexture((unsigned char *)roadspecular_jpg, roadspecular_jpg_size);
	alphaMap = CreateTexture((unsigned char *)alpha_jpg, alpha_jpg_size, false);
}

void Viewer::RenderSky()
{
	Eigen::Matrix4f skyView = view;
	for (int i = 0; i < 3; ++i)
		skyView.col(i).normalize();
	skyView.col(3).head<3>().setZero();
	Eigen::Matrix4f skyMvp = proj * skyView;
	glDepthMask(GL_FALSE);
	glEnable(GL_DEPTH_CLAMP);
	emptyVAO.bind();
	skyShader.bind();
	skyShader.setUniform("mvp", skyMvp);
	glDrawArrays(GL_TRIANGLE_STRIP, 0, 6);
	glDisable(GL_DEPTH_CLAMP);
	glDepthMask(GL_TRUE);

	glBindFramebuffer(GL_DRAW_FRAMEBUFFER, backgroundFBO);
	glBlitFramebuffer(0, 0, width(), height(), 0, 0, width(), height(), GL_COLOR_BUFFER_BIT, GL_NEAREST);
	glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
}

void CalculateViewFrustum(const Eigen::Matrix4f &mvp, Eigen::Vector4f *frustumPlanes, nse::math::BoundingBox<float, 3> &bbox)
{
	frustumPlanes[0] = (mvp.row(3) + mvp.row(0)).transpose();
	frustumPlanes[1] = (mvp.row(3) - mvp.row(0)).transpose();
	frustumPlanes[2] = (mvp.row(3) + mvp.row(1)).transpose();
	frustumPlanes[3] = (mvp.row(3) - mvp.row(1)).transpose();
	frustumPlanes[4] = (mvp.row(3) + mvp.row(2)).transpose();
	frustumPlanes[5] = (mvp.row(3) - mvp.row(2)).transpose();

	Eigen::Matrix4f invMvp = mvp.inverse();
	bbox.reset();
	for (int x = -1; x <= 1; x += 2)
		for (int y = -1; y <= 1; y += 2)
			for (int z = -1; z <= 1; z += 2)
			{
				Eigen::Vector4f corner = invMvp * Eigen::Vector4f(x, y, z, 1);
				corner /= corner.w();
				bbox.expand(corner.head<3>());
			}
}

bool IsBoxCompletelyBehindPlane(const Eigen::Vector3f &boxMin, const Eigen::Vector3f &boxMax, const Eigen::Vector4f &plane)
{
	return plane.dot(Eigen::Vector4f(boxMin.x(), boxMin.y(), boxMin.z(), 1)) < 0 &&
		   plane.dot(Eigen::Vector4f(boxMin.x(), boxMin.y(), boxMax.z(), 1)) < 0 &&
		   plane.dot(Eigen::Vector4f(boxMin.x(), boxMax.y(), boxMin.z(), 1)) < 0 &&
		   plane.dot(Eigen::Vector4f(boxMin.x(), boxMax.y(), boxMin.z(), 1)) < 0 &&
		   plane.dot(Eigen::Vector4f(boxMax.x(), boxMin.y(), boxMin.z(), 1)) < 0 &&
		   plane.dot(Eigen::Vector4f(boxMax.x(), boxMin.y(), boxMax.z(), 1)) < 0 &&
		   plane.dot(Eigen::Vector4f(boxMax.x(), boxMax.y(), boxMin.z(), 1)) < 0 &&
		   plane.dot(Eigen::Vector4f(boxMax.x(), boxMax.y(), boxMin.z(), 1)) < 0;
}

void Viewer::drawContents()
{
	camera().ComputeCameraMatrices(view, proj);

	Eigen::Matrix4f mvp = proj * view;
	Eigen::Vector3f cameraPosition = view.inverse().col(3).head<3>();

	Eigen::Vector4f frustum_planes[6];
	nse::math::BoundingBox<float, 3> bbox;

	CalculateViewFrustum(mvp, frustum_planes, bbox);

	std::vector<Eigen::Vector2f> instances;

	for (Eigen::Vector2f v : offsets)
	{
		Eigen::Vector3f min(v.x(), v.y(), 0.0f);
		Eigen::Vector3f max(v.x() + (float)PATCH_SIZE, v.y() + (float)PATCH_SIZE, 15.0f);

		bool is_inside = false;

		for (int i = 0; i < 6; i++)
		{
			if (IsBoxCompletelyBehindPlane(min, max, frustum_planes[i]))
			{
				is_inside = true;
				break;
			}
		}

		if (is_inside)
			instances.emplace_back(v);
	}

	RenderSky();

	//render terrain
	glEnable(GL_DEPTH_TEST);
	terrainVAO.bind();

	/*
	for (auto &i : instances)
	{
		std::cout << i.x() << " " << i.y() << std::endl;
	}

	abort();
	*/

	offsetBuffer.bind();
	offsetBuffer.uploadData(instances);
	glVertexAttribDivisor(offset_buffer_location, 1);

	terrainShader.bind();

	terrainShader.setUniform("screenSize", Eigen::Vector2f(width(), height()), false);
	terrainShader.setUniform("mvp", mvp);
	terrainShader.setUniform("cameraPos", cameraPosition, false);

	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, grassTexture);
	glUniform1i(grass_texture_location, 0);

	glActiveTexture(GL_TEXTURE1);
	glBindTexture(GL_TEXTURE_2D, rockTexture);
	glUniform1i(rock_texture_location, 1);

	glActiveTexture(GL_TEXTURE2);
	glBindTexture(GL_TEXTURE_2D, alphaMap);
	glUniform1i(alpha_texture_location, 2);

	glActiveTexture(GL_TEXTURE3);
	glBindTexture(GL_TEXTURE_2D, roadColorTexture);
	glUniform1i(road_texture_location, 3);


	glActiveTexture(GL_TEXTURE4);
	glBindTexture(GL_TEXTURE_2D, roadSpecularMap);
	glUniform1i(road_specular_location, 4);

	glActiveTexture(GL_TEXTURE5);
	glBindTexture(GL_TEXTURE_2D, roadNormalMap);
	glUniform1i(road_specular_location, 5);


	glDrawElementsInstanced(GL_TRIANGLE_STRIP, (PATCH_SIZE - 1) * PATCH_SIZE * 2, GL_UNSIGNED_INT, 0, instances.size());

	//Render text
	nvgBeginFrame(mNVGContext, width(), height(), mPixelRatio);
	std::string text = "Patches visible: " + std::to_string(instances.size());
	nvgText(mNVGContext, 10, 20, text.c_str(), nullptr);
	nvgEndFrame(mNVGContext);
}