#include <GL/glew.h>

#include "../simdebug.h"
#include "../simworld.h"
#include "../dings/gebaeude.h"
#include "../besch/grund_besch.h"
#include "../dataobj/umgebung.h"

#include "static_geometry.h"

static_geometry::static_geometry(karte_t *world) : world(world), tile_height_step(tile_raster_scale_y(TILE_HEIGHT_STEP, get_base_tile_raster_width())), batch_wh(32), batches(NULL), batch_infos(NULL) {
	work.reserve(batch_wh * batch_wh * 6);
}

static_geometry::~static_geometry() {
	if (batches) {
		glDeleteBuffers(grid_w * grid_h, batches);
		delete[] batches;
		delete[] batch_infos;
	}
}

void static_geometry::reset() {
	if (batches) {
		glDeleteBuffers(grid_w * grid_h, batches);
		delete[] batches;
		batches = NULL;
		delete[] batch_infos;
		batch_infos = NULL;
	}
}

void static_geometry::display() {
	if (!batches) {
		regenerate();
	}

	do_dirty();

	//glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
	glBindTexture(GL_TEXTURE_2D, texture_atlas.get_textures()[0]);
	glEnableClientState(GL_VERTEX_ARRAY);
	glEnableClientState(GL_COLOR_ARRAY);
	glEnableClientState(GL_TEXTURE_COORD_ARRAY);


	for (int j = 0; j < grid_h; ++j) {
		for (int i = 0; i < grid_w; ++i) {
			int index = j * grid_w + i;

			glBindBuffer(GL_ARRAY_BUFFER, batches[index]);

			static const uint8 *base = 0;

			glVertexPointer(3, GL_FLOAT, sizeof(vertex_t), base + offsetof(vertex_t, pos));
			glColorPointer(3, GL_FLOAT, sizeof(vertex_t), base + offsetof(vertex_t, color));
			glTexCoordPointer(2, GL_FLOAT, sizeof(vertex_t), base + offsetof(vertex_t, uv));

			glDrawArrays(GL_TRIANGLES, 0, batch_infos[index].ground_primitives * 3 + batch_infos[index].underground_primitives * 3);
		}
	}

	glBindBuffer(GL_ARRAY_BUFFER, 0);
	glDisableClientState(GL_VERTEX_ARRAY);
	glDisableClientState(GL_COLOR_ARRAY);
	glDisableClientState(GL_TEXTURE_COORD_ARRAY);
	glBindTexture(GL_TEXTURE_2D, 0);
	//glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
}

void static_geometry::regenerate() {
	if (batches) {
		glDeleteBuffers(grid_w * grid_h, batches);
		delete[] batches;
		batches = NULL;
		delete[] batch_infos;
	}
	dirty.clear();

	grid_w = (world->get_size().x + batch_wh - 1) / batch_wh;
	grid_h = (world->get_size().y + batch_wh - 1) / batch_wh;

	int num_batches = grid_w * grid_h;
	if (num_batches > 0) {
		batches = new GLuint[num_batches];
		batch_infos = new batch_info_t[num_batches];

		glGenBuffers(num_batches, batches);

		dirty.resize(num_batches, true);
	}

	DBG_DEBUG("batch_grid::regenerate()", "grid_w=%d, grid_h=%d\n", grid_w, grid_h);
}

void static_geometry::do_dirty() {
	for (int j = 0; j < grid_h; ++j) {
		for (int i = 0; i < grid_w; ++i) {
			int index = j * grid_w + i;

			if (dirty[index]) {
				recreate_batch(i, j);
				dirty[index] = false;
			}
		}
	}
}

void static_geometry::recreate_batch(int i, int j) {
	DBG_DEBUG("batch_grid::recreate_batch()", "i=%d, j=%d\n", i, j);

	work.clear();
	work_underground.clear();

	const int ix = i * batch_wh;
	const int iy = j * batch_wh;
	const int ex = ix + batch_wh;
	const int ey = iy + batch_wh;
	const int wx = world->get_size().x;
	const int wy = world->get_size().y;

	int primitives = 0;
	int primitives_underground = 0;

	for (int y = iy; y < ey && y < wy; ++y) {
		for (int x = ix; x < ex && x < wx; ++x) {
			const planquadrat_t *pq = world->lookup(koord(x, y));
			const grund_t *ground = pq->get_kartenboden();
			primitives += add_tile(&work, ground);

			gebaeude_t *building = ground->find<gebaeude_t>();
			if (building) {
				primitives += add_static_object(&work, ground, building);
			}

			for (unsigned int l = 1; l < pq->get_boden_count(); ++l) {
				const grund_t *base = pq->get_boden_bei(l);
				if (base->ist_tunnel()) {
					primitives_underground += add_tile(&work_underground, base);
				}
				else {
					primitives += add_tile(&work, base);
				}
			}
		}
	}

	const int batch_index = j * grid_w + i;
	batch_infos[batch_index].ground_primitives = primitives;
	batch_infos[batch_index].underground_primitives = primitives_underground;

	work.insert(work.end(), work_underground.begin(), work_underground.end());

	glBindBuffer(GL_ARRAY_BUFFER, batches[batch_index]);

	glBufferData(GL_ARRAY_BUFFER, work.size() * sizeof(vertex_t), &work[0], GL_DYNAMIC_DRAW);

	glBindBuffer(GL_ARRAY_BUFFER, 0);
}

static float calc_v1(const texture_info_t *ti, int z, int z_se, int tile_height_step) {
	const float v_z = ti->v_per_pixel * tile_height_step;
//	const float v_half = get_base_tile_raster_width() * ti->v_per_pixel / 4.0f;

	if (z_se == z) {
		return ti->v2;
	}
	else if (z_se < z) { // Should not happen
		return ti->v2 + v_z;
	}
	else {
		return ti->v2 - v_z;
	}
}

static float calc_v2(const texture_info_t *ti, int z, int z_nw, int tile_height_step) {
	const float v_z = ti->v_per_pixel * tile_height_step;
	const float v_half = get_base_tile_raster_width() * ti->v_per_pixel / 4.0f;

	if (z_nw == z) {
		return ti->v2 - 2 * v_half;
	}
	else if (z_nw < z) { // Should not happen?
		return ti->v2 - 2 * v_half + v_z;
	}
	else {
		return ti->v2 - 2 * v_half - v_z;
	}
}

static float calc_v3(const texture_info_t *ti, int z, int z2, int tile_height_step) {
	const float v_z = ti->v_per_pixel * tile_height_step;
	const float v_half = get_base_tile_raster_width() * ti->v_per_pixel / 4.0f;

	if (z2 == z) {
		return ti->v2 - v_half;
	}
	else if (z2 < z) { // Should not happen?
		return ti->v2 - v_half + v_z;
	}
	else {
		return ti->v2 - v_half - v_z;
	}
}

int static_geometry::add_tile(std::vector<vertex_t> *buffer, const grund_t *base) {
	const int x = base->get_pos().x;
	const int y = base->get_pos().y;
	const int z = base->get_hoehe();
	const int z_nw = base->get_hoehe(hang_t::corner_NW);
	const int z_ne = base->get_hoehe(hang_t::corner_NE);
	const int z_se = base->get_hoehe(hang_t::corner_SE);
	const int z_sw = base->get_hoehe(hang_t::corner_SW);

	const int i = x / batch_wh;
	const int j = y / batch_wh;

	const float r = ((i + j) & 1) ? 1.0f : 0.0f;
	const float g = ((x + y) & 1) ? 1.0f : 0.0f;
	const float b = 1.0f;

	const texture_info_t *ti;
	if (base->get_typ() == grund_t::wasser) {
		image_id img = grund_besch_t::sea->get_bild(base->get_bild());
		ti = texture_atlas.get_texture_info(img);
	} else {
		ti = texture_atlas.get_texture_info(base->get_bild());
	}
	assert(ti);

	const float u_w = ti->u1;
	const float u_e = ti->u2;
	const float u_c = u_w + (u_e - u_w) / 2;

	const float v_se = calc_v1(ti, z, z_se, tile_height_step);
	const float v_nw = calc_v2(ti, z, z_nw, tile_height_step);
	const float v_ne = calc_v3(ti, z, z_ne, tile_height_step);
	const float v_sw = calc_v3(ti, z, z_sw, tile_height_step);

	if (z_sw == z_ne && z_nw != z_se) {
		buffer->push_back(vertex_t(x,   y,   z_nw, r, g, b, u_c, v_nw));
		buffer->push_back(vertex_t(x+1, y+1, z_se, r, g, b, u_c, v_se));
		buffer->push_back(vertex_t(x,   y+1, z_sw, r, g, b, u_w, v_sw));
		buffer->push_back(vertex_t(x,   y,   z_nw, r, g, b, u_c, v_nw));
		buffer->push_back(vertex_t(x+1, y,   z_ne, r, g, b, u_e, v_ne));
		buffer->push_back(vertex_t(x+1, y+1, z_se, r, g, b, u_c, v_se));
	}
	else {
		buffer->push_back(vertex_t(x,   y,   z_nw, r, g, b, u_c, v_nw));
		buffer->push_back(vertex_t(x+1, y,   z_ne, r, g, b, u_e, v_ne));
		buffer->push_back(vertex_t(x,   y+1, z_sw, r, g, b, u_w, v_sw));
		buffer->push_back(vertex_t(x+1, y,   z_ne, r, g, b, u_e, v_ne));
		buffer->push_back(vertex_t(x,   y+1, z_sw, r, g, b, u_w, v_sw));
		buffer->push_back(vertex_t(x+1, y+1, z_se, r, g, b, u_c, v_se));
	}
	return 2;
}

int static_geometry::add_static_object(std::vector<vertex_t> *buffer, const grund_t *base, const ding_t *object) {
	const int x = base->get_pos().x;
	const int y = base->get_pos().y;
	int z = base->get_hoehe();

	const int z_per_level = get_base_tile_raster_width() / tile_height_step;
	const int z_for_edge = get_base_tile_raster_width() / tile_height_step / 4;

	const float r = 1.0f;
	const float g = 1.0f;
	const float b = 1.0f;

	int primitives = 0;

	int i = 0;
	image_id img = object->get_bild();
	while (img != IMG_LEER) {
		const texture_info_t *ti = texture_atlas.get_texture_info(img);

		const float u_w = ti->u1;
		const float u_e = ti->u2;
		const float u_c = ti->u1 + (ti->u2 - ti->u1) / 2.0;

		const float v_b = ti->v2;
		const float v_t = ti->v1;

		buffer->push_back(vertex_t(x,        y + 1.0f, z - z_for_edge,               r, g, b, u_w, v_b));
		buffer->push_back(vertex_t(x,        y + 1.0f, z + z_per_level - z_for_edge, r, g, b, u_w, v_t));
		buffer->push_back(vertex_t(x + 1.0f, y + 1.0f, z + z_per_level,              r, g, b, u_c, v_t));
		buffer->push_back(vertex_t(x,        y + 1.0f, z - z_for_edge,               r, g, b, u_w, v_b));
		buffer->push_back(vertex_t(x + 1.0f, y + 1.0f, z + z_per_level,              r, g, b, u_c, v_t));
		buffer->push_back(vertex_t(x + 1.0f, y + 1.0f, z,                            r, g, b, u_c, v_b));

		buffer->push_back(vertex_t(x + 1.0f, y + 1.0f, z,                            r, g, b, u_c, v_b));
		buffer->push_back(vertex_t(x + 1.0f, y + 1.0f, z + z_per_level,              r, g, b, u_c, v_t));
		buffer->push_back(vertex_t(x + 1.0f, y,        z + z_per_level - z_for_edge, r, g, b, u_e, v_t));
		buffer->push_back(vertex_t(x + 1.0f, y + 1.0f, z,                            r, g, b, u_c, v_b));
		buffer->push_back(vertex_t(x + 1.0f, y,        z + z_per_level - z_for_edge, r, g, b, u_e, v_t));
		buffer->push_back(vertex_t(x + 1.0f, y,        z - z_for_edge,               r, g, b, u_e, v_b));

		primitives += 4;

		z += z_per_level;
		img = object->get_bild(++i);
	}

	return primitives;
}
