#include <cassert>
#include <cstring>
#include <vector>
#include <algorithm>

//#define DUMP_ATLAS
#ifdef DUMP_ATLAS
#include <SDL.h>
#endif

#include "../simdebug.h"
#include "../simgraph.h"
#include "../simmem.h"
#include "../simtools.h"
#include "../besch/bild_besch.h"

#include "texture_atlas.h"

static std::vector<const bild_t *> images;

extern int closest_pot(const int number);
extern int get_tex_max_size();

static bool compare_image_height(const bild_t *a, const bild_t *b) {
	return max(a->y + a->h, get_base_tile_raster_width()) > max(b->y + b->h, get_base_tile_raster_width());
}

// Copied, trimmed and adapted from simgraph16.cc
// TODO: Player colors
typedef uint16 pixel_t;
static void draw_image(const bild_t *image, pixel_t *texture, uint32 xp, uint32 yp, uint32 disp_width) {
	const uint16 *sp = image->data;
	sint16 h = image->h;

	if (h > 0) {
		pixel_t *tp = texture + xp + image->x + (yp + image->y) * disp_width;

		do { // zeilen dekodieren
			uint16 runlen = *sp++;
			pixel_t *p = tp;

			// eine Zeile dekodieren
			do {
				// Start with clear run
				p += runlen;

				// jetzt kommen farbige pixel
				runlen = *sp++;

				while (runlen--) {
					uint16 value = *sp++;
					if (value & 0x8000) {
						value = 0xFFFF;
					} else {
						value |= 0x8000;
					}
					*p++ = value;
				}

				runlen = *sp++;
			} while (runlen != 0);

			tp += disp_width;
		} while (--h > 0);
	}
}

#ifdef DUMP_ATLAS
static void dump_atlas(unsigned int t, pixel_t *pixels, uint32 w, uint32 h) {
	char buf[64];
	SDL_Surface *s = SDL_CreateRGBSurfaceFrom(pixels, w, h, 16, w * sizeof(pixel_t), 0x00007C00, 0x000003E0, 0x0000001F, 0x00008000);
	sprintf(buf, "atlas%u.bmp", t);
	SDL_SaveBMP(s, buf);
	SDL_FreeSurface(s);
}
#endif // DUMP_ATLAS

void register_texture(const bild_t *image) {
	images.push_back(image);
}

texture_atlas_t::texture_atlas_t() : textures(NULL) {
}

void texture_atlas_t::init() {
	if (textures) {
		glDeleteTextures(num_textures, textures);
		delete textures;
		textures = NULL;
	}

	std::vector<const bild_t *> sorted_images(images.begin(), images.end());
	std::sort(sorted_images.begin(), sorted_images.end(), compare_image_height);

	const sint32 max_size = get_tex_max_size();

	// Figure out how many textures are needed
	sint32 x = 0;
	sint32 y = 0;
	sint32 line_height = max(sorted_images[0]->y + sorted_images[0]->h, get_base_tile_raster_width());
	num_textures = 1;
	if (line_height > max_size) {
		dbg->fatal("texture_atlas::texture_atlas()", "Largest image is higher (%u) than texture size (%u)", line_height, max_size);
	}
	for (std::vector<const bild_t *>::iterator iter = sorted_images.begin(); iter != sorted_images.end(); ++iter) {
		const bild_t *image = *iter;
		const sint32 img_w = max(image->x + image->w, get_base_tile_raster_width());
		if (img_w > max_size) {
			dbg->fatal("texture_atlas::texture_atlas()", "Image is wider (%u) than texture size (%u)", image->w, max_size);
		}
		x += img_w;
		if (x > max_size) {
			x = img_w;
			y += line_height;
			line_height = max(image->y + image->h, get_base_tile_raster_width());
			if ((y + line_height) > max_size) {
				++num_textures;
				y = 0;
			}
		}
	}

	const uint32 last_atlas_height = closest_pot(y + line_height);

	DBG_DEBUG("texture_atlas::texture_atlas()", "allocating %u textures of size %ux%u/%u", num_textures, max_size, max_size, last_atlas_height);

	textures = new GLuint[num_textures];
	glGenTextures(num_textures, textures);

	glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
	glPixelStorei(GL_UNPACK_ALIGNMENT, 2);
	glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0);
	glPixelStorei(GL_UNPACK_SKIP_ROWS, 0);

	const sint32 texture_width = max_size;
	const sint32 texture_height = (num_textures == 1 ? last_atlas_height : max_size);

	pixel_t *data = MALLOCN(pixel_t, texture_width * texture_height);
	memset(data, 0, sizeof(pixel_t) * texture_width * texture_height);

	int t = 0;
	x = 0;
	y = 0;
	line_height = max(sorted_images[0]->y + sorted_images[0]->h, get_base_tile_raster_width());
	for (std::vector<const bild_t *>::iterator iter = sorted_images.begin(); iter != sorted_images.end(); ++iter) {
		const bild_t *image = *iter;
		const sint32 img_w = max(image->x + image->w, get_base_tile_raster_width());
		if (x + img_w > texture_width) {
			x = 0;
			y += line_height;
			line_height = max(image->y + image->h, get_base_tile_raster_width());

			if (y + line_height > texture_height) {
				glBindTexture(GL_TEXTURE_2D, textures[t]);
				glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
				glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
				glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB5_A1, texture_width, texture_height, 0, GL_BGRA, GL_UNSIGNED_SHORT_1_5_5_5_REV, data);
#ifdef DUMP_ATLAS
				dump_atlas(t, data, texture_width, texture_height);
#endif
				++t;
				memset(data, 0, sizeof(uint16) * texture_width * texture_height);
				y = 0;
			}
		}

		draw_image(image, data, x, y, texture_width);

		const uint32 actual_texture_height = t < (num_textures - 1) ? texture_height : last_atlas_height;

		texture_info_t info;
		info.texture = textures[t];
		info.x = image->x;
		info.y = image->y;
		info.w = image->w;
		info.h = image->h;
		info.u1 = float(x) / float(texture_width);
		info.v1 = float(y) / float(actual_texture_height);
		info.u2 = float(x + img_w) / float(texture_width);
		info.v2 = float(y + line_height) / float(actual_texture_height);
		info.v_per_pixel = 1.0 / float(actual_texture_height);
		atlas.put(image->bild_nr, info);

		x += img_w;
	}
	glBindTexture(GL_TEXTURE_2D, textures[t]);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB5_A1, texture_width, last_atlas_height, 0, GL_BGRA, GL_UNSIGNED_SHORT_1_5_5_5_REV, data);
	glBindTexture(GL_TEXTURE_2D, 0);
#ifdef DUMP_ATLAS
	dump_atlas(t, data, texture_width, last_atlas_height);
#endif

	free(data);
}

texture_atlas_t::~texture_atlas_t() {
	if (textures) {
		glDeleteTextures(num_textures, textures);
		delete textures;
	}
}
