#include <GL/glew.h>
#include "chunk.hpp"
#include "tile/tile_base.hpp"
#include <glm/detail/qualifier.hpp>
#include <glm/ext/matrix_transform.hpp>
#include <glm/ext/quaternion_transform.hpp>
#include <glm/ext/scalar_constants.hpp>
#include <iterator>
#include <memory>
#include <utility>

using World::Chunk;
using World::Tile::TileBase;

constexpr Graphics::Primitive<4, 6> PRIMITIVE_B = {
	.m_vertices = {
		{.m_pos = {-0.5, -0.5, 0, 1}, .m_colour = {1, 0, 0, 1}},
		{.m_pos = {-0.5, +0.5, 0, 1}, .m_colour = {1, 1, 0, 1}},
		{.m_pos = {+0.5, -0.5, 0, 1}, .m_colour = {0, 1, 0, 1}},
		{.m_pos = {+0.5, +0.5, 0, 1}, .m_colour = {0, 0, 1, 1}},
	},
	.m_indices = {
		0, 2, 3,
		0, 3, 1,
	},
};
constexpr Graphics::Primitive<4, 6> PRIMITIVE_0 = {
	.m_vertices = {
		{.m_pos = {-0.5, -0.5, 0.5, 1}, .m_colour = {1, 0, 0, 1}},
		{.m_pos = {-0.5, -0.5, 0, 1}, .m_colour = {1, 1, 0, 1}},
		{.m_pos = {+0.5, -0.5, 0.5, 1}, .m_colour = {0, 1, 0, 1}},
		{.m_pos = {+0.5, -0.5, 0, 1}, .m_colour = {0, 0, 1, 1}},
	},
	.m_indices = {
		0, 2, 3,
		0, 3, 1,
	},
};

static const Graphics::Primitive<4, 6> PRIMITIVE_S[4] = {
	PRIMITIVE_0.with_matrix(glm::rotate(glm::mat4(1), glm::pi<float>() * 0.5f, {0, 0, 1})),
	PRIMITIVE_0.with_matrix(glm::rotate(glm::mat4(1), glm::pi<float>() * 1.0f, {0, 0, 1})),
	PRIMITIVE_0.with_matrix(glm::rotate(glm::mat4(1), glm::pi<float>() * 1.5f, {0, 0, 1})),
	PRIMITIVE_0,
};

constexpr glm::vec<2, int> get_pos_mod(glm::vec<2, int> pos) {
	return (pos % Chunk::N + Chunk::N) % Chunk::N;
}

Chunk::Chunk(glm::vec<2, int> pos) {
	m_pos = pos;
}

TileBase* Chunk::get(glm::vec<2, int> p) {
	p = get_pos_mod(p);
	return m_tiles[p.x * N + p.y].get();
}

const TileBase* Chunk::get(glm::vec<2, int> p) const {
	p = get_pos_mod(p);
	return m_tiles[p.x * N + p.y].get();
}

TileBase* Chunk::set(glm::vec<2, int> p, std::unique_ptr<TileBase> v) {
	TileBase* r = v.get();
	p = get_pos_mod(p);
	m_tiles[p.x * N + p.y] = std::move(v);
	m_dirty = true;
	return r;
}

void Chunk::update(Map& map) {
	for(auto& tile : m_tiles) {
		if(tile) {
			tile->update();
		}
	}
	if(!m_dirty) {
		return;
	}
	Graphics::Mesh mesh;
	const Chunk* chunks[4] = {
		map.get_chunk(m_pos + glm::vec<2, int>(N, 0)),
		map.get_chunk(m_pos + glm::vec<2, int>(0, N)),
		map.get_chunk(m_pos + glm::vec<2, int>(-N, 0)),
		map.get_chunk(m_pos + glm::vec<2, int>(0, -N)),
	};
	const glm::vec<2, int> neighbours[4] = {
		{1, 0},
		{0, 1},
		{-1, 0},
		{0, -1},
	};

	for(int x = 0; x < N; x++) {
		for(int y = 0; y < N; y++) {
			if(!get({x, y})) {
				continue;
			}
			
			glm::vec<2, int> t_off(m_pos.x + x, m_pos.y + y);
			mesh.add_primitive(PRIMITIVE_B.with_translation({t_off, 0}));

			for(int i = 0; i < std::size(neighbours); i++) {
				glm::vec<2, int> n_off = neighbours[i] + glm::vec<2, int>(x, y);
				const Chunk* chunk_check = this;
				if(n_off.x == N) {
					chunk_check = chunks[0];
				} else if(n_off.y == N) {
					chunk_check = chunks[1];
				} else if(n_off.x == -1) {
					chunk_check = chunks[2];
				} else if(n_off.y == -1) {
					chunk_check = chunks[3];
				}
				if(!chunk_check || !chunk_check->get(n_off)) {
					mesh.add_primitive(PRIMITIVE_S[i].with_translation({t_off, 0}));
				}
			}
		}
	}
	m_model.bind();
	m_model.set(mesh, GL_DYNAMIC_DRAW);
	Graphics::Vertex::set_vertex_attribs();
	m_dirty = false;
}

void Chunk::render(Graphics::Context& ctx) const {
	m_model.bind();
	m_model.render(GL_TRIANGLES);
	for(auto& tile : m_tiles) {
		if(tile) {
			tile->render();
		}
	}
}