// ---------------------------------------------------------------------------
// 
//   Lily Engine alpha
// 
//   Copyright (c) 2010 Clifford Jolly
//     http://cliffjolly.com
//     cliff@cliffjolly.com
// 
// ---------------------------------------------------------------------------
// 
//   This program is free software; you can redistribute it and/or modify
//   it under the terms of the GNU General Public License as published by
//   the Free Software Foundation; either version 2 of the License, or
//   (at your option) any later version.
//  
//   This program is distributed in the hope that it will be useful, but
//   WITHOUT ANY WARRANTY; without even the implied warranty of
//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
//   General Public License for more details.
// 
//   You should have received a copy of the GNU General Public License
//   along with this program; if not, write to the Free Software
//   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
//   USA
// -------------------------- END HEADER -------------------------------------

#include "../hacks/winhacks.h"

#include <iostream>
#include <assert.h>
#include <cmath>
#include <cstdlib>
#include <GL/gl.h>
using namespace std;

#include "../console/console.h"
using namespace Console;

#include "../texturemanager/texture.h"
#include "../texturemanager/texturemanager.h"
#include "../image/image.h"
#include "../compress/compress.h"
#include "../string/string.h"
#include "../filesystem/filesystem.h"

#include "tilepane.h"
#include "world.h"
#include "editorui.h"
#include "renderer.h"

int roundUpToPowerOfTwo(int num) {
	int i = 1;
	while(i < num && i > 0) {
		i <<= 1;
	}
	
	assert(i > 0);
	
	return i;
}

TilePane::TilePane(void) {
	textureNum = -1;
	tileSize = 32;
	width = 0;
	height = 0;
	tiles = NULL;
	
	displayListSize = 32;
	displayLists = NULL;
	
	world = NULL;
}

void TilePane::setWorld(World *world) {
	this->world = world;
}

TilePane::~TilePane(void) {
	if(tiles) delete[] tiles;
	clearLists();
}

/*
int TilePane::getTile(int x, int y) const {
	if(tiles) {
		if(x > -width / 2 && y > -height / 2 && x < width / 2 && y < height / 2) {
			//return tiles[x + y * width];
			return tiles[getTileIndex(x, y, width, height)];
		}
	}
	return 0;
}
*/

void TilePane::setTile(int x, int y, int tile, bool skipRebuild) {
	if(x <= -width / 2 || x >= width / 2) {
		resize(roundUpToPowerOfTwo(abs(x) * 2 + 1), height);
	}
	if(y <= -height / 2 || y >= height / 2) {
		resize(width, roundUpToPowerOfTwo(abs(y) * 2 + 1));
	}

	tiles[getTileIndex(x, y, width, height)] = tile;
	
	if(!skipRebuild) {
		// Rebuild this list.
		int listX;
		int listY;
		
		if(x < 0) {
			listX = (x + 1) / displayListSize;
			listX--;
		} else {
			listX = x / displayListSize;
		}
		if(y < 0) {
			listY = (y + 1) / displayListSize;
			listY--;
		} else {
			listY = y / displayListSize;
		}
	
		buildSingleList(listX, listY);
	}
}

void TilePane::resize(int width, int height) {
	clearLists();
	
	int realWidth = roundUpToPowerOfTwo(width);
	int realHeight = roundUpToPowerOfTwo(height);
	
	if(realWidth < displayListSize * 2) {
		realWidth = displayListSize * 2;
	}
	if(realHeight < displayListSize * 2) {
		realHeight = displayListSize * 2;
	}
	
	int *newTiles = new int[realWidth * realHeight];
	
	for(int x = -realWidth / 2; x < realWidth / 2; x++) {
		for(int y = -realHeight / 2; y < realHeight / 2; y++) {
			newTiles[getTileIndex(x, y, realWidth, realHeight)] = getTile(x, y);
		}
	}
	
	if(tiles) {
		delete[] tiles;
	}
	tiles = newTiles;
	this->width = realWidth;
	this->height = realHeight;
	
	rebuildLists();
}

void TilePane::clear(void) {
	clearLists();
	textureNum = -1;
	tileSize = 16;
	width = 0;
	height = 0;
	delete[] tiles;
	tiles = NULL;
}

void TilePane::clearLists(void) {
	if(displayLists) {
		int numLists = (width / displayListSize) * (height / displayListSize);
		for(int i = 0; i < numLists; i++) {
			//glDeleteLists(displayLists[i], 1);
			if(displayLists[i] != -1) {
				world->getRenderer()->freeList(displayLists[i]);
			}
		}
		delete[] displayLists;
		displayLists = NULL;
	}
}

void TilePane::save(gzFile out) {

	// TODO: Remove deprecated gzip stuff.

	shrinkToFit();
	
	gzwrite(out, &width, sizeof(width));
	gzwrite(out, &height, sizeof(height));
	gzwrite(out, &tileSize, sizeof(tileSize));
	
	string texName = world->getTextureManager()->getTextureNameByIndex(textureNum);
	
	// TODO: Fix to handle directory hierarchies.
	texName = FileSystem::getBaseName(texName);
	
	int texNameSize = texName.size();
	gzwrite(out, &texNameSize, sizeof(texNameSize));
	if(texNameSize > 0) {
		gzwrite(out, texName.c_str(), texNameSize);
	}
	
	if(width > 0 && height > 0) {
		gzwrite(out, tiles, width * height * sizeof(int));
	}
	
}

void TilePane::saveSimpleBuf(SimpleBuffer *buf) {

	shrinkToFit();
	
	buf->addData(&width, sizeof(width));
	buf->addData(&height, sizeof(height));
	buf->addData(&tileSize, sizeof(tileSize));
	
	string texName = world->getTextureManager()->getTextureNameByIndex(textureNum);
	
	// TODO: Fix to handle directory hierarchies.
	texName = FileSystem::getBaseName(texName);
	
	int texNameSize = texName.size();
	buf->addData(&texNameSize, sizeof(texNameSize));
	
	if(texNameSize > 0) {
		buf->addData(texName.c_str(), texNameSize);
	}
	
	if(width > 0 && height > 0) {
		
		unsigned int outLength = 0;
		char *compressed = compressRLE((char*)tiles, width * height * sizeof(int), &outLength, sizeof(int));
		buf->addData(&outLength, sizeof(outLength));
		buf->addData(compressed, outLength);
		delete[] compressed;
	}
	
}

void TilePane::load(gzFile in) {
	
	// TODO: Remove deprecated gzip stuff.
	
	clear();
	
	gzread(in, &width, sizeof(width));
	gzread(in, &height, sizeof(height));
	gzread(in, &tileSize, sizeof(tileSize));
	
	int texNameSize = 0;
	gzread(in, &texNameSize, sizeof(texNameSize));
	if(texNameSize > 0) {
		char *texNameIn = new char[texNameSize + 1];
		gzread(in, texNameIn, texNameSize);
		texNameIn[texNameSize] = 0;
		
		//textureNum = world->getTextureManager()->getTextureIndexByName(string(texNameIn));
		string fixedName = world->getRealFile(string("tilesets/") + fixDeprecatedPath(texNameIn));
		textureNum = world->getTextureManager()->getTextureIndexByName(fixedName);

		delete[] texNameIn;
		
		// TODO: Better defaults handling!
		if(textureNum >= 0) {
			world->getTextureManager()->setTextureFilter(textureNum, false);
		}
		
	} else {
		textureNum = -1;
	}

	if(width > 0 && height > 0) {
		tiles = new int[width * height];
		gzread(in, tiles, width * height * sizeof(int));
	} 
	
	rebuildLists();
}

void TilePane::loadSimpleBuf(SimpleBuffer *buf) {
	clear();
	
	buf->getDataAndAdvance(&width, sizeof(width));
	buf->getDataAndAdvance(&height, sizeof(height));
	buf->getDataAndAdvance(&tileSize, sizeof(tileSize));
	
	int texNameSize = 0;
	
	buf->getDataAndAdvance(&texNameSize, sizeof(texNameSize));
	
	if(texNameSize > 0) {
		
		char *texNameIn = new char[texNameSize + 1];
		
		buf->getDataAndAdvance(texNameIn, texNameSize);
		
		texNameIn[texNameSize] = 0;
		
		//textureNum = world->getTextureManager()->getTextureIndexByName(string(texNameIn));

		string fixedName = world->getRealFile(string("tilesets/") + fixDeprecatedPath(texNameIn));
		textureNum = world->getTextureManager()->getTextureIndexByName(fixedName);
		
		delete[] texNameIn;
		
		// TODO: Better defaults handling!
		if(textureNum >= 0) {
			
			world->getTextureManager()->setTextureFilter(textureNum, false);
		}
		
	} else {
		textureNum = -1;
	}

	if(width > 0 && height > 0) {
		
		unsigned int storedSize = 0;
		unsigned int uncompressedSize = 0;
		buf->getDataAndAdvance(&storedSize, sizeof(storedSize));
		const char *compressed = buf->getDataAndAdvance(NULL, storedSize);
		tiles = (int*)decompressRLE(compressed, storedSize, &uncompressedSize, sizeof(int));

	} 
	
	rebuildLists();
}

void TilePane::draw(const BoundingBox &visibleArea, bool isActiveLayer) {
	if(!tiles) return;
	
	Renderer *renderer = world->getRenderer();
	glEnable(GL_TEXTURE_2D);
	
	int left   = int(visibleArea.upperLeft.x)  / tileSize;
	int right  = int(visibleArea.lowerRight.x) / tileSize + 1;
	int top    = int(visibleArea.upperLeft.y)  / tileSize + 1;
	int bottom = int(visibleArea.lowerRight.y) / tileSize;

	if(visibleArea.lowerRight.x < 0) right--;
	if(visibleArea.lowerRight.y < 0) bottom--;
	if(visibleArea.upperLeft.x < 0) left--;
	if(visibleArea.upperLeft.y < 0) top--;
	
	Texture *tex = world->getTextureManager()->getTextureByIndex(textureNum);
	if(tex) {
		tex->bindTexture();
	} else {
		renderer->unbindTexture();
	}
	
	// This will only affect OpenGL code.
	// Colors can be set outside verts.
	renderer->setColor(1, 1, 1, 1);
	EditorUI *editor = world->getEditor();
	if(editor) {
		if(editor->getVisible()) {
			if(!isActiveLayer && editor->junkInterface.getTintInactiveLayers()) {
				renderer->setColor(0.0, 0.5, 0.0, 1);
			}
		}
	}
	
	int minListX = (visibleArea.upperLeft.x  / tileSize) / displayListSize;
	int maxListX = (visibleArea.lowerRight.x / tileSize) / displayListSize;
	int minListY = (visibleArea.lowerRight.y / tileSize) / displayListSize;
	int maxListY = (visibleArea.upperLeft.y  / tileSize) / displayListSize;
	
	if(visibleArea.upperLeft.x < 0) minListX--;
	if(visibleArea.lowerRight.x < 0) maxListX--;
	if(visibleArea.upperLeft.y < 0) maxListY--;
	if(visibleArea.lowerRight.y < 0) minListY--;
	
	for(int x = minListX; x <= maxListX; x++) {
		for(int y = minListY; y <= maxListY; y++) {
			
			// This block mainly just affects DirectX code.
			// Colors need to be in the vertices.
			if(editor) {
				if(editor->getVisible()) {
					int listNumber = getListNumber(x, y);
					if(listNumber > -1) {
						if(!isActiveLayer) {
							renderer->setListColor(listNumber, 0, 0.5, 0, 1);
						} else {
							renderer->setListColor(listNumber, 1, 1, 1, 1);
						}
					}
				}
			}

			drawSingleList(x, y);
			
		}
	}
	
	// Finally, if the editor is visible, let's draw a bounding box
	// around the area of the tiles so we know how huge things are
	// getting
	world->getRenderer()->unbindTexture();
	if(editor && isActiveLayer) {
		if(editor->getVisible()) {
			BoundingBox bb(-width * tileSize / 2, height * tileSize / 2, width * tileSize / 2, -height * tileSize / 2);
			bb.draw(world->getRenderer());
		}
	}
}

void TilePane::shrinkToFit(void) {
	int maxx = 0;
	int maxy = 0;
	
	for(int x = -width / 2 + 1; x < width / 2; x++) {
		for(int y = -height / 2 + 1; y < height / 2; y++) {
			
			if(getTile(x, y) != NULLTILE) {
				if(abs(x) + 1> maxx) {
					maxx = abs(x) + 1;
				}
				if(abs(y) + 1> maxy) {
					maxy = abs(y) + 1;
				}
			}
			
		}
	}
	
	int newWidth = roundUpToPowerOfTwo(maxx * 2);
	int newHeight = roundUpToPowerOfTwo(maxy * 2);
	
	if(maxx == 0 || maxy == 0) {
		clear();
	} else if(newWidth != width || newHeight != height) {
		resize(newWidth, newHeight);
	}
}


void TilePane::buildSingleList(int x, int y) {
	
	Renderer *renderer = world->getRenderer();
	
	int listIndex = getListIndex(x, y);
	
	if(displayLists[listIndex] != -1) {
		renderer->freeList(displayLists[listIndex]);
		displayLists[listIndex] = -1;
	}	
	
	Texture *tex = world->getTextureManager()->getTextureByIndex(textureNum);
	if(!tex) return;
	
	int minx = x * displayListSize;
	int maxx = (x + 1) * displayListSize;
	int miny = y * displayListSize;
	int maxy = (y + 1) * displayListSize;
	
	vector<Vertex3D> points;
	points.reserve(displayListSize * displayListSize * 4);
	int numQuads = 0;
	
	for(int x = minx; x < maxx; x++) {
		
		for(int y = miny; y < maxy; y++) {
			
			int tile = getTile(x, y);
			int rx = tileSize * x;
			int ry = tileSize * y;
			
			if(tile != NULLTILE) {
				
				float u = 0;
				float v = 0;
				float uw = 1;
				float vw = 1;
				
                Vector2D origSize = world->getTextureManager()->getTextureSize(tex->getName());

                int origWidth = origSize.x;
                int origHeight = origSize.y;

#ifdef CAMERA_OFFSET_WHOLE_PIXELS
                float uvFudgeAmount = 0;
                float fudgeAmount = 0;
#else
                float uvFudgeAmount = 0.0005;
                float fudgeAmount = 0.002;
#endif

				if(tex) {
					
					u = float((tile * tileSize) % origWidth) / float(origWidth) + uvFudgeAmount;
					v = float(tile / (origWidth / tileSize)) * tileSize / float(origHeight) + uvFudgeAmount;
					uw = u + float(tileSize) / float(origWidth) - uvFudgeAmount;
                    vw = v + float(tileSize) / float(origHeight) - uvFudgeAmount;
				}

				Vertex3D v1 = {rx - fudgeAmount, ry - fudgeAmount, 0, 1, 1, 1, 1, u, vw};
				Vertex3D v2 = {rx + tileSize + fudgeAmount, ry - fudgeAmount, 0, 1, 1, 1, 1, uw, vw};
				Vertex3D v3 = {rx + tileSize + fudgeAmount, ry + tileSize + fudgeAmount, 0, 1, 1, 1, 1, uw, v};
				Vertex3D v4 = {rx - fudgeAmount, ry + tileSize + fudgeAmount, 0, 1, 1, 1, 1, u, v};

				points.push_back(v1);
				points.push_back(v2);
				points.push_back(v3);
				points.push_back(v4);

				numQuads++;
			}
		}
	}
	
	//renderer->drawQuads(&(points[0]), numQuads);
	
	if(points.size()) {
		
		displayLists[listIndex] = renderer->createList(&(points[0]), points.size(), RENDERER_LIST_QUADS);
		
		out("graphics") << "Layer #" << layer << " created list #" << displayLists[listIndex] << endl;
	}
}


void TilePane::drawSingleList(int x, int y) {
	int listNumber = getListNumber(x, y);
	if(listNumber == -1) return;
	
	//cout << "Layer #" << layer << " drawing list #" << listNumber << endl;
	world->getRenderer()->callList(listNumber);
}

int TilePane::getListIndex(int x, int y) {
	if(!displayLists) return -1;
	
	if( x * displayListSize >= width / 2  || x * displayListSize < -width / 2 ||
		y * displayListSize >= height / 2 || y * displayListSize < -height / 2) {
		
		return -1;
	}
	
	int horizSpan = width  / displayListSize;
	int vertSpan =  height / displayListSize;
	int index = (x + horizSpan / 2) + (y + vertSpan / 2) * horizSpan;
	
	assert(index > -1);
	assert(index < (width / displayListSize) * (height / displayListSize));
	
	return index;
}

int TilePane::getListNumber(int x, int y) {
	
	int index = getListIndex(x, y);
	
	if(index == -1) return -1;
	
	return displayLists[index];
}

void TilePane::createLists(void) {
	assert(displayLists == NULL);
	
	int numLists = (width / displayListSize) * (height / displayListSize);
	displayLists = new int[numLists];
	for(int i = 0; i < numLists; i++) {
		//displayLists[i] = glGenLists(1);
		displayLists[i] = -1;
	}
}

void TilePane::rebuildLists(void) {
	if(displayLists) {
		clearLists();
	}
	createLists();
	
	int minx = (-width / 2) / displayListSize;
	int maxx = (width / 2) / displayListSize;
	int miny = (-height / 2) / displayListSize;
	int maxy = (height / 2) / displayListSize;
	
	for(int x = minx; x < maxx; x++) {
		for(int y = miny; y < maxy; y++) {
			buildSingleList(x, y);
		}
	}
	
}

void TilePane::setTileSize(int tileSize) {
	this->tileSize = tileSize;
	rebuildLists();
}

int TilePane::getTileSize(void) const {
	return tileSize;
}

void TilePane::setTextureNum(int textureNum) {
	this->textureNum = textureNum;
	rebuildLists();
	
	// TODO: Better defaults handling!
	if(textureNum >= 0) {
		world->getTextureManager()->setTextureFilter(textureNum, false);
	}
}

int TilePane::getTextureNum(void) const {
	return textureNum;
}
		
void TilePane::resetListColors(void) {
	int minx = (-width / 2) / displayListSize;
	int maxx = (width / 2) / displayListSize;
	int miny = (-height / 2) / displayListSize;
	int maxy = (height / 2) / displayListSize;
	
	for(int x = minx; x < maxx; x++) {
		for(int y = miny; y < maxy; y++) {
			int listNum = getListNumber(x, y);
			if(listNum > -1) 
				world->getRenderer()->setListColor(listNum, 1, 1, 1, 1);
		}
	}
}

void TilePane::shiftAllTiles(int sx, int sy) {
	int oldWidth = width;
	int oldHeight = height;
	int *oldTiles = new int[oldWidth * oldHeight];
	
	memcpy(oldTiles, tiles, oldWidth * oldHeight * sizeof(int));
	
	for(int x = 0; x < oldWidth; x++) {
		for(int y = 0; y < oldHeight; y++) {
			int tile = oldTiles[x + y * oldWidth];
			//if(tile != NULLTILE) {
				setTile(x + sx - oldWidth / 2, y + sy - oldHeight / 2, tile, true);
			//}
		}
	}
	
	delete[] oldTiles;
	
	rebuildLists();
	
}

