#include <iostream>
#include <sstream>
#include <map>
#include <string>

#include "IMG_savepng.h"
#include "asserts.hpp"
#include "draw_tile.hpp"
#include "filesystem.hpp"
#include "json_parser.hpp"
#include "level_object.hpp"
#include "module.hpp"
#include "preferences.hpp"
#include "raster.hpp"
#include "string_utils.hpp"
#include "surface.hpp"
#include "surface_cache.hpp"
#include "surface_palette.hpp"
#include "unit_test.hpp"
#include "variant_utils.hpp"

namespace {
typedef std::map<std::string,const_level_object_ptr> tiles_map;
tiles_map tiles_cache;

const int BaseTileSize = 16;
}

bool level_tile::is_solid(int xpos, int ypos) const
{
	return object->is_solid(face_right ? (xpos - x) : (x + object->width() - xpos - 1), ypos - y);
}

std::vector<const_level_object_ptr> level_object::all()
{
	std::vector<const_level_object_ptr> res;
	for(tiles_map::const_iterator i = tiles_cache.begin(); i != tiles_cache.end(); ++i) {
		res.push_back(i->second);
	}

	return res;
}

level_tile level_object::build_tile(variant node)
{
	level_tile res;
	res.x = node["x"].as_int();
	res.y = node["y"].as_int();
	res.zorder = node["zorder"].as_int();
	if(tiles_cache.count(node["tile"].as_string())) {
		res.object = tiles_cache[node["tile"].as_string()].get();
	}
	res.face_right = node["face_right"].as_bool();
	return res;
}

namespace {

typedef boost::shared_ptr<variant> obj_variant_ptr;
typedef boost::shared_ptr<const variant> const_obj_variant_ptr;

std::vector<obj_variant_ptr> level_object_index;
std::vector<const_obj_variant_ptr> original_level_object_nodes;
std::map<std::pair<const_obj_variant_ptr, int>, level_object_ptr> secondary_zorder_objects;

std::map<obj_variant_ptr, int> tile_nodes_to_zorders;

std::map<std::string, int> tile_str_to_palette;


typedef std::pair<std::string, int> filename_palette_pair;

//a tile identifier made up of a filename, palette and tile position.
typedef std::pair<filename_palette_pair, int> tile_id;
std::map<tile_id, int> compiled_tile_ids;
}

//defined in texture.cpp
namespace graphics {
void set_alpha_for_transparent_colors_in_rgba_surface(SDL_Surface* s, int options=0);
}

void create_compiled_tiles_image()
{
	//the number of tiles that can fit in a tiesheet.
	const int TilesInSheet = (1024*1024)/(BaseTileSize*BaseTileSize);

	//which zorders require an alpha channel?
	std::set<int> zorder_with_alpha_channel;

	//calculate how many tiles are in each zorder
	std::map<int, int> zorder_to_num_tiles;
	for(std::map<obj_variant_ptr, int>::const_iterator i = tile_nodes_to_zorders.begin(); i != tile_nodes_to_zorders.end(); ++i) {
		obj_variant_ptr node = i->first;
		std::vector<std::string> tiles_vec = util::split((*node)["tiles"].as_string(), '|');
		zorder_to_num_tiles[i->second] += tiles_vec.size();

		const static std::string UsesAlphaChannelStr = "uses_alpha_channel";
		if(i->first->has_key(UsesAlphaChannelStr)) {
			zorder_with_alpha_channel.insert(i->second);
		}
	}

	//now work out which zorders should go in which tilesheets.
	//all tiles of the same zorder always go in the same sheet.
	std::map<int, int> zorder_to_sheet_number;
	std::vector<int> tiles_in_sheet;
	std::vector<graphics::surface> sheets;
	std::vector<int> sheet_next_image_index;

	//two passes, since we do all zorders with alpha channel first, so
	//they'll go in the first tilesheet together, then those without.
	for(int use_alpha_channel = 1; use_alpha_channel >= 0; --use_alpha_channel) {
		fprintf(stderr, "ZORDER_PROC\n");
		for(std::map<int, int>::const_iterator i = zorder_to_num_tiles.begin();
		    i != zorder_to_num_tiles.end(); ++i) {
			if(zorder_with_alpha_channel.count(i->first) != use_alpha_channel) {
				continue;
			}

			fprintf(stderr, "ZORDER_PROC: %d %d\n", i->first, i->second);

			int sheet = 0;
			for(; sheet != tiles_in_sheet.size(); ++sheet) {
				if(tiles_in_sheet[sheet] + i->second <= TilesInSheet) {
					std::cerr << "ZORDER_ALLOC " << i->first << " (" << i->second << ") -> " << sheet << "\n";
					break;
				}
			}
	

			if(sheet == tiles_in_sheet.size()) {
				const int num_sheets = 1 + i->second/TilesInSheet;
				std::cerr << "ZORDER_ALLOC " << i->first << " (" << i->second << ") -> NEW SHEET " << sheet << "(" << num_sheets << ")\n";
				for(int n = 0; n != num_sheets; ++n) {
					tiles_in_sheet.push_back(0);
					sheet_next_image_index.push_back(0);
					sheets.push_back(graphics::surface(SDL_CreateRGBSurface(SDL_SWSURFACE, 1024, 1024, 32, SURFACE_MASK)));
					tiles_in_sheet[sheet+n] += i->second;
				}
			} else {
				tiles_in_sheet[sheet] += i->second;
			}

			if(!(tiles_in_sheet[sheet] <= TilesInSheet)) {
				std::cerr << "TOO MANY TILES IN SHEET " << sheet << "/" << tiles_in_sheet.size() << ": " << tiles_in_sheet[sheet] << "/" << TilesInSheet << " (zorder = " << i->first << ")\n";
			}
			zorder_to_sheet_number[i->first] = sheet;
		}
	}

	std::cerr << "NUM_TILES: " << tile_nodes_to_zorders.size() << " / " << TilesInSheet << "\n";


	graphics::surface s(SDL_CreateRGBSurface(SDL_SWSURFACE, 1024, (compiled_tile_ids.size()/64 + 1)*BaseTileSize, 32, SURFACE_MASK));
	for(std::map<tile_id, int>::const_iterator itor = compiled_tile_ids.begin();
	    itor != compiled_tile_ids.end(); ++itor) {
		const tile_id& tile_info = itor->first;
		const std::string& filename = tile_info.first.first;
		const int palette = tile_info.first.second;
		const int tile_pos = tile_info.second;

		std::cerr << "WRITING PALETTE: " << palette << "\n";

		graphics::surface src = graphics::surface_cache::get(filename);
		if(palette >= 0) {
			src = graphics::map_palette(src, palette);
		}

		SDL_SetAlpha(src.get(), 0, SDL_ALPHA_OPAQUE);
		const int width = std::max<int>(src->w, src->h)/BaseTileSize;

		const int src_x = (tile_pos%width) * BaseTileSize;
		const int src_y = (tile_pos/width) * BaseTileSize;
		
		const int dst_x = (itor->second%64) * BaseTileSize;
		const int dst_y = (itor->second/64) * BaseTileSize;

		SDL_Rect src_rect = { src_x, src_y, BaseTileSize, BaseTileSize };
		SDL_Rect dst_rect = { dst_x, dst_y, BaseTileSize, BaseTileSize };

		SDL_BlitSurface(src.get(), &src_rect, s.get(), &dst_rect);
	}

	graphics::set_alpha_for_transparent_colors_in_rgba_surface(s.get());

//  don't need to save the main compiled tile anymore.
//	IMG_SavePNG("images/tiles-compiled.png", s.get(), 5);

	SDL_SetAlpha(s.get(), 0, SDL_ALPHA_OPAQUE);

	for(std::map<obj_variant_ptr, int>::const_iterator i = tile_nodes_to_zorders.begin(); i != tile_nodes_to_zorders.end(); ++i) {
		const int num_tiles = zorder_to_num_tiles[i->second];
		const int num_sheets = 1 + num_tiles/TilesInSheet;
		int sheet = zorder_to_sheet_number[i->second];

		obj_variant_ptr node = i->first;
		if(num_sheets > 1) {
			int offset = abs(tile_str_to_palette[(*node)["tiles"].as_string()])%num_sheets;
			
			const static std::string UsesAlphaChannelStr = "uses_alpha_channel";
			if(i->first->has_key(UsesAlphaChannelStr)) {
				//try to put all alpha tiles in the first sheet.
				offset = 0;
			}

			int count = 0;
			while(sheet_next_image_index[sheet + offset] >= TilesInSheet) {
				offset = (offset+1)%num_sheets;
				++count;
				ASSERT_LOG(count <= num_sheets, "COULD NOT PLACE TILES IN SHEET\n");
			}

			sheet += offset;
		}
		std::cerr << "NODE: " << node->write_json() << " -> " << i->second << "\n";
		std::map<int, int> dst_index_map;

		std::vector<std::string> tiles_vec = util::split((*node)["tiles"].as_string(), '|');
		std::string tiles_val;

		foreach(const std::string& tiles_str, tiles_vec) {
			ASSERT_EQ(tiles_str[0], '+');

			const int tile_num = atoi(tiles_str.c_str() + 1);
			const int src_x = (tile_num%64) * BaseTileSize;
			const int src_y = (tile_num/64) * BaseTileSize;

			int dst_tile;
			if(dst_index_map.count(tile_num)) {
				dst_tile = dst_index_map[tile_num];
			} else {
				dst_tile = sheet_next_image_index[sheet]++;
				ASSERT_LOG(dst_tile < tiles_in_sheet[sheet], "TOO MANY TILES ON SHEET: " << sheet << ": " << tiles_in_sheet[sheet] << " ZORDER: " << i->second);
				dst_index_map[tile_num] = dst_tile;

				const int dst_x = (dst_tile%64) * BaseTileSize;
				const int dst_y = (dst_tile/64) * BaseTileSize;

				SDL_Rect src_rect = { src_x, src_y, BaseTileSize, BaseTileSize };
				SDL_Rect dst_rect = { dst_x, dst_y, BaseTileSize, BaseTileSize };

				SDL_BlitSurface(s.get(), &src_rect, sheets[sheet].get(), &dst_rect);
			}

			if(!tiles_val.empty()) {
				tiles_val += "|";
			}

			char buf[128];
			sprintf(buf, "+%d", dst_tile);
			tiles_val += buf;
		}

		*node = node->add_attr(variant("tiles"), variant(tiles_val));

		std::stringstream str;
		str << "tiles-compiled-" << std::dec << sheet << ".png";

		*node = node->add_attr(variant("image"), variant(str.str()));
	}

	for(int n = 0; n != sheets.size(); ++n) {
		std::stringstream str;
		str << "images/tiles-compiled-" << std::dec << n << ".png";
		
#if !defined(__native_client__)
		IMG_SavePNG(module::map_file(str.str()).c_str(), sheets[n], 5);
#endif
	}
}

namespace {
unsigned int current_palette_set = 0;
std::set<level_object*>& palette_level_objects() {
	//we never want this to be destroyed, since it's too hard to
	//guarantee destruction order.
	static std::set<level_object*>* instance = new std::set<level_object*>;
	return *instance;
}
}

palette_scope::palette_scope(const std::vector<std::string>& v)
  : original_value(current_palette_set)
{
	current_palette_set = 0;	
	foreach(const std::string& pal, v) {
		const int id = graphics::get_palette_id(pal);
		current_palette_set |= 1 << id;
	}
}

palette_scope::~palette_scope()
{
	current_palette_set = original_value;
}

void level_object::set_current_palette(unsigned int palette)
{
	foreach(level_object* obj, palette_level_objects()) {
		obj->set_palette(palette);
	}
}

level_object::level_object(variant node, const char* id)
  : id_(node["id"].as_string_default()), image_(node["image"].as_string()),
    info_(node["info"].as_string_default()),
    t_(graphics::texture::get(image_)),
	all_solid_(node["solid"].is_bool() ? node["solid"].as_bool() : node["solid"].as_string_default() == "yes"),
    passthrough_(node["passthrough"].as_bool()),
    flip_(node["flip"].as_bool(false)),
    friction_(node["friction"].as_int(100)),
    traction_(node["traction"].as_int(100)),
    damage_(node["damage"].as_int(0)),
	opaque_(node["opaque"].as_bool(false)),
	draw_area_(0, 0, BaseTileSize, BaseTileSize),
	tile_index_(-1),
	palettes_recognized_(current_palette_set),
	current_palettes_(0)
{
	if(id_.empty() && id != NULL && *id) {
		id_ = id;
	}

	if(node.has_key("palettes")) {
		palettes_recognized_ = 0;
		std::vector<std::string> p = parse_variant_list_or_csv_string(node["palettes"]);
		foreach(const std::string& pal, p) {
			const int id = graphics::get_palette_id(pal);
			palettes_recognized_ |= 1 << id;
		}
	}

	if(palettes_recognized_) {
		palette_level_objects().insert(this);
	}

	if(node.has_key("solid_color")) {
		solid_color_ = boost::intrusive_ptr<graphics::color>(new graphics::color(node["solid_color"].as_string_default()));
		if(preferences::use_16bpp_textures()) {
			*solid_color_ = graphics::color(graphics::map_color_to_16bpp(solid_color_->rgba()));
		}
	}

	if(node.has_key("draw_area")) {
		draw_area_ = rect(node["draw_area"].as_string_default());
	}

	//TODO: Fix up the JSON to be consistent and use a list.
	std::string tiles_str;
	variant tiles_variant = node["tiles"];
	if(tiles_variant.is_int()) {
		tiles_str = tiles_variant.string_cast();
	} else {
		tiles_str = tiles_variant.as_string();
	}

	std::vector<std::string> tile_variations = util::split(tiles_str, '|');
	foreach(const std::string& variation, tile_variations) {
		if(!variation.empty() && variation[0] == '+') {
			//a + symbol at the start of tiles means that it's just a base-10
			//number. This is generally what is used for compiled tiles.
			tiles_.push_back(strtol(variation.c_str()+1, NULL, 10));
		} else {
			const int width = std::max<int>(t_.width(), t_.height());
			assert(width%BaseTileSize == 0);
			const int base = std::min<int>(32, width/BaseTileSize);
			tiles_.push_back((base == 1) ? 0 : strtol(variation.c_str(), NULL, base));
		}
	}

	if(tiles_.empty()) {
		tiles_.resize(1);
	}

	if(node.has_key("solid_map")) {
		solid_.resize(width()*height());
		graphics::surface surf(graphics::surface_cache::get(node["solid_map"].as_string()).convert_opengl_format());
		if(surf.get()) {
			const uint32_t* p = reinterpret_cast<const uint32_t*>(surf->pixels);
			for(int n = 0; n != surf->w*surf->h && n != solid_.size(); ++n) {
				uint8_t r, g, b, alpha;
				SDL_GetRGBA(p[n], surf->format, &r, &g, &b, &alpha);
				if(alpha > 64) {
					solid_[n] = true;
				}
			}
		}
	}
	
	std::vector<std::string> solid_attr;
	
	if(!node["solid"].is_bool()) {
		solid_attr = util::split(node["solid"].as_string_default());
	}

	if(all_solid_ || std::find(solid_attr.begin(), solid_attr.end(), "flat") != solid_attr.end()) {
		if(passthrough_){
			solid_.resize(width()*height());
			for(int x = 0; x < width(); ++x) {
				for(int y = 0; y < height(); ++y) {
					const int index = y*width() + x;
					solid_[index] = (y == 0);
				}
			}
			//set all_solid_ to false because it's not longer the case.
			all_solid_ = false;
		}else{
			solid_ = std::vector<bool>(width()*height(), true);
		}
	}
		
	if(std::find(solid_attr.begin(), solid_attr.end(), "diagonal") != solid_attr.end()) {
		solid_.resize(width()*height());
		for(int x = 0; x < width(); ++x) {
			for(int y = 0; y < height(); ++y) {
				const int index = y*width() + x;
				solid_[index] = solid_[index] || (passthrough_? (y==x) : (y >= x));
			}
		}
	}
	
	if(std::find(solid_attr.begin(), solid_attr.end(), "reverse_diagonal") != solid_attr.end()) {
		solid_.resize(width()*height());
		for(int x = 0; x < width(); ++x) {
			for(int y = 0; y < height(); ++y) {
				const int index = y*width() + x;
				solid_[index] = solid_[index] || (passthrough_? (y == (width() - (x+1))) : (y >= (width() - (x+1))));
			}
		}
	}
	
	if(std::find(solid_attr.begin(), solid_attr.end(), "upward_diagonal") != solid_attr.end()) {
		solid_.resize(width()*height());
		for(int x = 0; x < width(); ++x) {
			for(int y = 0; y < height(); ++y) {
				const int index = y*width() + x;
				solid_[index] = solid_[index] || (passthrough_? (y==x) : (y <= x));
			}
		}
	}

	if(std::find(solid_attr.begin(), solid_attr.end(), "upward_reverse_diagonal") != solid_attr.end()) {
		solid_.resize(width()*height());
		for(int x = 0; x < width(); ++x) {
			for(int y = 0; y < height(); ++y) {
				const int index = y*width() + x;
				solid_[index] = solid_[index] || (passthrough_? (y == (width() - (x+1))) : (y <= (width() - (x+1))));
			}
		}
	}

	if(std::find(solid_attr.begin(), solid_attr.end(), "quarter_diagonal_lower") != solid_attr.end()) {
		solid_.resize(width()*height());
		for(int x = 0; x < width(); ++x) {
			for(int y = 0; y < height(); ++y) {
				const int index = y*width() + x;
				solid_[index] = solid_[index] || (passthrough_? (y == (x/2 + width()/2)) : (y >= (x/2 + width()/2)));
			}
		}
	}
	
	if(std::find(solid_attr.begin(), solid_attr.end(), "quarter_diagonal_upper") != solid_attr.end()) {
		solid_.resize(width()*height());
		for(int x = 0; x < width(); ++x) {
			for(int y = 0; y < height(); ++y) {
				const int index = y*width() + x;
				solid_[index] = solid_[index] || (passthrough_? (y == x/2) : (y >= x/2));
			}
		}
	}
	
	if(std::find(solid_attr.begin(), solid_attr.end(), "reverse_quarter_diagonal_lower") != solid_attr.end()) {
		solid_.resize(width()*height());
		for(int x = 0; x < width(); ++x) {
			for(int y = 0; y < height(); ++y) {
				const int index = y*width() + x;
				solid_[index] = solid_[index] || (passthrough_? (y == (width() - x/2) -1) : (y >= (width() - x/2)));
			}
		}
	}
	
	if(std::find(solid_attr.begin(), solid_attr.end(), "reverse_quarter_diagonal_upper") != solid_attr.end()) {
		solid_.resize(width()*height());
		for(int x = 0; x < width(); ++x) {
			for(int y = 0; y < height(); ++y) {
				const int index = y*width() + x;
				solid_[index] = solid_[index] || (passthrough_? (y == (width()/2 - x/2) -1) : (y >= (width()/2 - x/2)));
			}
		}
	}
	
	if(node.has_key("solid_heights")) {
		//this is a list of heights which represent the solids
		std::vector<int> heights = node["solid_heights"].as_list_int();
		if(!heights.empty()) {
			solid_.resize(width()*height());
			for(int x = 0; x < width(); ++x) {
				const int heights_index = (heights.size()*x)/width();
				assert(heights_index >= 0 && heights_index < heights.size());
				const int h = heights[heights_index];
				for(int y = height() - h; y < height(); ++y) {
					const int index = y*width() + x;
					solid_[index] = true;
				}
			}
		}
	}

	foreach(variant r, node["rect"].as_list()) {
		const int x = r["x"].as_int();
		const int y = r["y"].as_int();
		const int w = r["w"].as_int();
		const int h = r["h"].as_int();

		if(solid_.empty()) {
			solid_.resize(width()*height());
		}
		for(int xpos = x; xpos != x+w; ++xpos) {
			for(int ypos = y; ypos != y+h; ++ypos) {
				if(xpos >= 0 && xpos < width() && ypos >= 0 && ypos < height()) {
					const int index = ypos*width() + xpos;
					assert(index >= 0 && index < solid_.size());
					solid_[index] = true;
				}
			}
		}
	}
	
	if(preferences::compiling_tiles) {
		tile_index_ = level_object_index.size();

		//set solid colors to always false if we're compiling, since having
		//solid colors will confuse the compilation.
		solid_color_ = boost::intrusive_ptr<graphics::color>();

		const bool uses_alpha_channel = calculate_uses_alpha_channel();

		std::vector<int> palettes;
		palettes.push_back(-1);
		get_palettes_used(palettes);

		foreach(int palette, palettes) {
			variant node_copy = node.add_attr(variant("palettes"), variant());
			node_copy = node_copy.add_attr(variant("id"), variant(id_));
			if(calculate_opaque()) {
				node_copy = node_copy.add_attr(variant("opaque"), variant(true));
				opaque_ = true;
			}

			if(uses_alpha_channel) {
				node_copy = node_copy.add_attr(variant("uses_alpha_channel"), variant(true));
			}

			graphics::color col;
			if(calculate_is_solid_color(col)) {
				if(palette >= 0) {
					col = graphics::map_palette(col, palette);
				}
				node_copy = node_copy.add_attr(variant("solid_color"), variant(graphics::color_transform(col).to_string()));
			}

			if(calculate_draw_area()) {
				node_copy = node_copy.add_attr(variant("draw_area"), variant(draw_area_.to_string()));
			}

			std::string tiles_str;

			foreach(int tile, tiles_) {
				tile_id id(filename_palette_pair(node_copy["image"].as_string(), palette), tile);
				std::map<tile_id, int>::const_iterator itor = compiled_tile_ids.find(id);
				if(itor == compiled_tile_ids.end()) {
					compiled_tile_ids[id] = compiled_tile_ids.size();
					itor = compiled_tile_ids.find(id);
				}

				const int index = itor->second;

				char tile_pos[64];
				sprintf(tile_pos, "+%d", index);
				if(!tiles_str.empty()) {
					tiles_str += "|";
				}

				tiles_str += tile_pos;
			}

//			node_copy = node_copy.add_attr(variant("image"), variant("tiles-compiled.png"));
			node_copy = node_copy.add_attr(variant("tiles"), variant(tiles_str));
			tile_str_to_palette[tiles_str] = palette;
	
			level_object_index.push_back(obj_variant_ptr(new variant(node_copy)));
			original_level_object_nodes.push_back(const_obj_variant_ptr(new variant(node)));
		}
	}
}

level_object::~level_object()
{
	if(palettes_recognized_) {
		palette_level_objects().erase(this);
	}
}

namespace {
const char base64_chars[65] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz.!";

std::vector<int> create_base64_char_to_int()
{
	std::vector<int> result(256);
	int index = 0;
	foreach(char c, (char const *)base64_chars) {
		result[c] = index++;
	}

	return result;
}

std::vector<int> base64_char_to_int = create_base64_char_to_int();

void base64_encode(int num, char* buf, int nplaces)
{
	buf[nplaces--] = 0;
	while(num > 0 && nplaces >= 0) {
		buf[nplaces--] = base64_chars[num%64];
		num /= 64;
	}

	while(nplaces >= 0) {
		buf[nplaces--] = '0';
	}
}

int base64_unencode(const char* begin, const char* end)
{
	int result = 0;
	while(begin != end) {
		result = result*64 + base64_char_to_int[*begin];
		++begin;
	}

	return result;
}

}

void level_object::write_compiled()
{
	create_compiled_tiles_image();

	for(int n = 0; n <= level_object_index.size()/64; ++n) {
		std::stringstream ss;
		ss << std::dec << n;
		const std::string filename = ss.str() + ".cfg";
		variant_builder tiles_node;
		for(int m = n*64; m < level_object_index.size() && m < (n+1)*64; ++m) {
			tiles_node.add("tiles", *level_object_index[m]);
		}

		module::write_file("data/compiled/tiles/" + filename, tiles_node.build().write_json(true));
	}
}

namespace {
std::vector<const_level_object_ptr> compiled_tiles;

void load_compiled_tiles(int index)
{
	int starting_index = index*64;
	char buf[128];
	sprintf(buf, "%d", index);
	variant node(json::parse_from_file("data/compiled/tiles/" + std::string(buf) + ".cfg"));
	int count = 0;

	foreach(variant tile_node, node["tiles"].as_list()) {
		if(starting_index >= compiled_tiles.size()) {
			compiled_tiles.resize(starting_index+64);
		}
		compiled_tiles[starting_index++].reset(new level_object(tile_node));
		++count;
	}
}

}

const_level_object_ptr level_object::get_compiled(const char* buf)
{
	const int index = base64_unencode(buf, buf+3);
	if(index >= compiled_tiles.size() || !compiled_tiles[index]) {
		load_compiled_tiles(base64_unencode(buf, buf+2));
	}

	ASSERT_LOG(index >= compiled_tiles.size() || compiled_tiles[index], "COULD NOT LOAD COMPILED TILE: " << std::string(buf, buf+3) << " -> " << index);

	return compiled_tiles[index];
}

level_object_ptr level_object::record_zorder(int zorder) const
{
	std::vector<int>::const_iterator i = std::find(zorders_.begin(), zorders_.end(), zorder);
	if(i == zorders_.end()) {
		zorders_.push_back(zorder);
		if(zorders_.size() > 1) {
			level_object_ptr result(new level_object(*original_level_object_nodes[tile_index_]));
			result->zorders_.push_back(zorder);
			std::pair<const_obj_variant_ptr, int> key(original_level_object_nodes[tile_index_], zorder);

			secondary_zorder_objects[key] = result;

			std::vector<int> palettes;
			palettes.push_back(-1);
			result->get_palettes_used(palettes);

			for(int n = 0; n != palettes.size(); ++n) {
				tile_nodes_to_zorders[level_object_index[result->tile_index_ + n]] = zorder;
			}

			return result;
		} else {
			std::vector<int> palettes;
			palettes.push_back(-1);
			get_palettes_used(palettes);

			for(int n = 0; n != palettes.size(); ++n) {
				tile_nodes_to_zorders[level_object_index[tile_index_ + n]] = zorder;
			}
		}
	} else if(i != zorders_.begin()) {
		std::pair<const_obj_variant_ptr, int> key(original_level_object_nodes[tile_index_], zorder);
		return secondary_zorder_objects[key];
	}

	return level_object_ptr();
}

void level_object::write_compiled_index(char* buf) const
{
	if(current_palettes_ == 0) {
		base64_encode(tile_index_, buf, 3);
	} else {
		unsigned int mask = current_palettes_;
		int npalette = 0;
		while(!(mask&1)) {
			mask >>= 1;
			++npalette;
		}

		std::vector<int> palettes;
		get_palettes_used(palettes);
		std::vector<int>::const_iterator i = std::find(palettes.begin(), palettes.end(), npalette);
		ASSERT_LOG(i != palettes.end(), "PALETTE NOT FOUND: " << npalette);
		base64_encode(tile_index_ + 1 + (i - palettes.begin()), buf, 3);
	}
}

int level_object::width() const
{
	return BaseTileSize*2;
}

int level_object::height() const
{
	return BaseTileSize*2;
}

bool level_object::is_solid(int x, int y) const
{
	if(solid_.empty()) {
		return false;
	}

	if(x < 0 || y < 0 || x >= width() || y >= height()) {
		return false;
	}

	const int index = y*width() + x;
	assert(index >= 0 && index < solid_.size());
	return solid_[index];
}

namespace {
int hash_level_object(int x, int y) {
	x /= 32;
	y /= 32;

	static const unsigned int x_rng[] = {31,29,62,59,14,2,64,50,17,74,72,47,69,92,89,79,5,21,36,83,81,35,58,44,88,5,51,4,23,54,87,39,44,52,86,6,95,23,72,77,48,97,38,20,45,58,86,8,80,7,65,0,17,85,84,11,68,19,63,30,32,57,62,70,50,47,41,0,39,24,14,6,18,45,56,54,77,61,2,68,92,20,93,68,66,24,5,29,61,48,5,64,39,91,20,69,39,59,96,33,81,63,49,98,48,28,80,96,34,20,65,84,19,87,43,4,54,21,35,54,66,28,42,22,62,13,59,42,17,66,67,67,55,65,20,68,75,62,58,69,95,50,34,46,56,57,71,79,80,47,56,31,35,55,95,60,12,76,53,52,94,90,72,37,8,58,9,70,5,89,61,27,28,51,38,58,60,46,25,86,46,0,73,7,66,91,13,92,78,58,28,2,56,3,70,81,19,98,50,50,4,0,57,49,36,4,51,78,10,7,26,44,28,43,53,56,53,13,6,71,95,36,87,49,62,63,30,45,75,41,59,51,77,0,72,28,24,25,35,4,4,56,87,23,25,21,4,58,57,19,4,97,78,31,38,80,};
	static const unsigned int y_rng[] = {91,80,42,50,40,7,82,67,81,3,54,31,74,49,30,98,49,93,7,62,10,4,67,93,28,53,74,20,36,62,54,64,60,33,85,31,31,6,22,2,29,16,63,46,83,78,2,11,18,39,62,56,36,56,0,39,26,45,72,46,11,4,49,13,24,40,47,51,17,99,80,64,27,21,20,4,1,37,33,25,9,87,87,36,44,4,77,72,23,73,76,47,28,41,94,69,48,81,82,0,41,7,90,75,4,37,8,86,64,14,1,89,91,0,29,44,35,36,78,89,40,86,19,5,39,52,24,42,44,74,71,96,78,29,54,72,35,96,86,11,49,96,90,79,79,70,50,36,15,50,34,31,86,99,77,97,19,15,32,54,58,87,79,85,49,71,91,78,98,64,18,82,55,66,39,35,86,63,87,41,25,73,79,99,43,2,29,16,53,42,43,26,45,45,95,70,35,75,55,73,58,62,45,86,46,90,12,10,72,88,29,77,10,8,92,72,22,3,1,49,5,51,41,86,65,66,95,23,60,87,64,86,55,30,48,76,21,76,43,52,52,23,40,64,69,43,69,97,34,39,18,87,46,8,96,50,};

	return x_rng[x%(sizeof(x_rng)/sizeof(*x_rng))] +
	       y_rng[y%(sizeof(y_rng)/sizeof(*y_rng))];
}
}

void level_object::queue_draw(graphics::blit_queue& q, const level_tile& t)
{
	const int random_index = hash_level_object(t.x,t.y);
	const int tile = t.object->tiles_[random_index%t.object->tiles_.size()];

	queue_draw_from_tilesheet(q, t.object->t_, t.object->draw_area_, tile, t.x, t.y, t.face_right);
}

int level_object::calculate_tile_corners(tile_corner* result, const level_tile& t)
{
	const int random_index = hash_level_object(t.x,t.y);
	const int tile = t.object->tiles_[random_index%t.object->tiles_.size()];

	return get_tile_corners(result, t.object->t_, t.object->draw_area_, tile, t.x, t.y, t.face_right);
}

bool level_object::calculate_opaque() const
{
	foreach(int tile, tiles_) {
		if(!is_tile_opaque(t_, tile)) {
			return false;
		}
	}

	return true;
}

bool level_object::calculate_uses_alpha_channel() const
{
	foreach(int tile, tiles_) {
		if(is_tile_using_alpha_channel(t_, tile)) {
			return true;
		}
	}

	return false;
}

bool level_object::calculate_is_solid_color(graphics::color& col) const
{
	foreach(int tile, tiles_) {
		if(!is_tile_solid_color(t_, tile, col)) {
			return false;
		}
	}

	return true;
}

bool level_object::calculate_draw_area()
{
	draw_area_ = rect();
	foreach(int tile, tiles_) {
		draw_area_ = rect_union(draw_area_, get_tile_non_alpha_area(t_, tile));
	}

	return draw_area_ != rect(0, 0, BaseTileSize, BaseTileSize);
}

void level_object::set_palette(unsigned int palette)
{
	palette &= palettes_recognized_;
	if(palette == current_palettes_) {
		return;
	}

	current_palettes_ = palette;

	if(palette == 0) {
		t_ = graphics::texture::get(image_);
	} else {
		int npalette = 0;
		while((palette&1) == 0) {
			palette >>= 1;
			++npalette;
		}

		t_ = graphics::texture::get_palette_mapped(image_, npalette);
	}
}

void level_object::get_palettes_used(std::vector<int>& v) const
{
	unsigned int p = palettes_recognized_;
	int palette = 0;
	while(p) {
		if(p&1) {
			v.push_back(palette);
		}

		p >>= 1;
		++palette;
	}
}

variant level_object::get_value(const std::string& key) const
{
	if(key == "id") {
		return variant(id_);
	} else if(key == "info") {
		return variant(info_);
	} else if(key == "damage") {
		return variant(damage_);
	} else if(key == "friction") {
		return variant(friction_);
	} else if(key == "traction") {
		return variant(traction_);
	} else {
		return variant();
	}
}

UNIT_TEST(level_object_base64)
{
	const char* s = "4O0";
	const char* s2 = s + strlen(s);
	const int num = base64_unencode(s, s2);
	char buf[3];
	base64_encode(num, buf, 3);
	CHECK_EQ(buf[0], s[0]);
	CHECK_EQ(buf[1], s[1]);
	CHECK_EQ(buf[2], s[2]);
}
