/*
VERSION:	1.9
	1.9		Fixed compression & de-compression code
	1.8		Added:  Now checks if a new map has started loading while a previous map was still loading, and aborts the previous map-load
				Change:  All errors return an error object  {type, message}		type is either "fail" or "abort"
	1.7		Fixed glitch in isCompressed()
	1.6		removed redundant width & height parameters from makeMap()
	1.5		useCache defaults to true
	1.4		Added useCache to drawMap() so it can optionally be forced to reload all chipsets
	1.3		Added checks to decompressMapData() & compressMapData() to avoid accidentally attempting to compress already-compressed data,  and avoid decompressing already-decompressed data.  Suitable data simply passes through.
	1.2		Removed legacy properties:  collision_array, width, height, tileSize  because "map_formats.as" adds them instead.
				Modified scroll() so that it no longer relies on legacy width & height properties.
	1.1		Added scroll()  &  x,y properties


DEPENDENCIES:
	copyObject.as
	VOW.as
	VOW/loadBitmap.as
		VOW/loadBitmap/loadImage.as
		VOW/loadBitmap/nextDepth.as
	nextDepth.as
	
	
USAGE:
	// Displaying a map.
		#include "functions/map3.as"
		map_mc = makeMap();
		map_mc.drawMap( myMapData, true );
		
		
	// Modifying a map tile.
		var tileData = mapData.layers[0][0][1] = {
			chipset: 1,
			x: 6,
			y: 0
		}
		var updateLocation = {
			layer: 0,
			x: 0,
			y: 1
		}
		map_mc.updateTile( updateLocation, tileData, newData.tileSize );
		
		
	// Using isCompressed() to detect whether map data needs to be de-compressed.
		if( !isCompressed( mapData )  )
		{// if:  data is compressed
			uncompressedMapData = decompressMapData( uncompressedMapData );
			mapData = uncompressedMapData;
		}// if:  data is compressed
		
		
	// Compressing map data for efficient storage / text transfer.
		compressedMapData = compressMapData( uncompressedMapData );
		
		
	// De-compressing map data for convenient editing.
		decompressed = decompressMapData( compressed );
		
		
	// Scrolling the map.
		onEnterFrame = function(){
			map_mc.scroll( 100, 64, 64, 64, 0.05);
		}// loop()


PROPERTIES:
	x,y										The scroll position of the map.  Use these instead of _x & _y.  These behave like _x & _y  (but the map's movieClip size is locked to screen-size)

	
FUNCTIONS:
	drawMap()							[promise]		Updates the displayed tiles based on provided mapData.  (removes extra layers,  resizes map,  and loads chipset files as needed)
	updateTile()					[null]			Updates a specified tile using the specified tile-data.  (does not create new layers)
	isCompressed()				[t/f]				Checks provided mapData and reports whether or not it is compressed.
	compressMapData()			[compressedMapData]			Receives uncompressed data and returns compressed data suitable for efficient saving.  (can be used as a static function)
	decompressMapData()		[uncompressedMapData]		Receives compressed data and returns decompressed data suitable for editing.  (can be used as a static function)
	scroll()							[null]			Scrolls the map using scrollRect
	
	
FUNCTION PARAMS
	movieClip = 						makeMap( [instanceName], [target_mc], [depth], [width], [height] )
	null =									updateTile( location, tileData, [tileSize] )
	t/f =										isCompressed( mapData )
	compressedMapData =			compressMapData( uncompressedMapData )
	uncompressedMapData =		decompressMapData( compressedMapData )
	
	
INTERNAL DEPTHS:
	0				map layer  (first)
	10			map layer
	20			map layer
	...			
	350			map layer  (max)
	2001		chipsetLoader_mc
	
	Layers are assigned depths in incriments of 10
	This allows movieClips to be inserted between them.
	
	
NOTE:
	chipsets can be either external files, or bitmaps with linkage.
*/
function makeMap( instanceName, target_mc, depth )
{
	////////////////////////////////////////////////////////////////////
	// DEPENDENCIES
	
	#include "copyObject.as"
	#include "VOW.as"
	#include "VOW/loadBitmap.as"
		// VOW/loadBitmap/loadImage.as
		// VOW/loadBitmap/nextDepth.as
	#include "nextDepth.as"
	
	
	
	////////////////////////////////////////////////////////////////////
	// ONE-TIME INIT
	
	// static settings
	var layerDepthOffset = 10;
	var encodingRadix = 36;		// base-36 is the maximum that Flash allows
	
	// create container movieClip
	var target_mc = target_mc || this;
	var depth = (depth != undefined) ? depth : nextDepth(target_mc);
	var instanceName = instanceName || "map"+depth;
	var _this = target_mc.createEmptyMovieClip( instanceName, depth );
	
	// movieClip structure
	_this.layers_mc_array = [];
	_this.chipset_array = [];
	
	// internal shortcuts
	//var data = _this.data;
	var chipsetLoader_mc = _this.createEmptyMovieClip( "chipsetLoader_mc", 2001 );
	chipsetLoader_mc._visible = false;
	var layers_mc_array = _this.layers_mc_array;
	var chipset_array = _this.chipset_array;
	var oldTilesize = _this.tileSize;
	var BitmapData = flash.display.BitmapData;
	var Rectangle = flash.geom.Rectangle;
	var Point = flash.geom.Point;
	var currentChipsetLoad = {};		// used to detect when new map-loads interrupt previous map-loading
	currentChipsetLoad.chipsetLoad_prom = null;
	
	
	
	////////////////////////////////////////////////////////////////////
	// drawMap()
	
	function drawMap( uncompressedMapData, useCache )
	{
		if(useCache==undefined)		var useCache=12;
		var drawMap_vow = VOW.make();
		if( !isCompressed( uncompressedMapData )){
			uncompressedMapData = decompressMapData( uncompressedMapData );
		}// if:  data is compressed
		var newData = uncompressedMapData;
		
		// break down
		removeExtraLayers();
		resizeExistingLayers();
		// build up
		addNewLayers();
		loadNewChipsets()
		.then( drawAllTiles, drawMapError )
		.then( finished );
		
		
		function removeExtraLayers()
		{
			// compare known layers against the map-newData
			for(var l=layers_mc_array.length; l>=0; l--)
			{// for:  each existing layer
				var newDataLayer = newData.layers[l];
				if(newDataLayer == undefined)
				{// if:  the newData doesn't have this layer
					// remove this layer from the movieClip
					layers_mc_array[l].pic.dispose();				// delete layer's bitmapData
					layers_mc_array[l].removeMovieClip();		// remove layer's movieClip
					layers_mc_array.splice( l, 1 );					// forget about this layer
				}// if:  the newData doesn't have this layer
			}// for:  each existing layer
		}// removeExtraLayers()
		
		
		function resizeExistingLayers()
		{
			var oldWidth = layers_mc_array[0].pic.width;
			var oldHeight = layers_mc_array[0].pic.height;
			if( (oldWidth != undefined)  &&  (oldTilesize != newData.tileSize  ||  oldWidth != newData.width || oldHeight != newData.height) )
			{// if:  width OR height differ from current bitmaps
				var newWidth = newData.width * newData.tileSize;
				var newHeight = newData.height * newData.tileSize;
				for(var l=layers_mc_array.length; l>=0; l--)
				{// for:  each existing layer
					var thisLayer_mc = layers_mc_array[l];
					thisLayer_mc.pic.dispose();
					thisLayer_mc.pic = new BitmapData( newWidth, newHeight, true, 0 );
					thisLayer_mc.attachBitmap( thisLayer_mc.pic, 0 );
				}// for:  each existing layer
			}// if:  width OR height differ from current bitmaps
			oldTilesize = newData.tileSize;		// remember last-used tileSize for future comparisons
		}// resizeExistingLayers()
		
		
		
		function addNewLayers()
		{
			var newWidth = newData.width * newData.tileSize;
			var newHeight = newData.height * newData.tileSize;
			for(var l=layers_mc_array.length; l<newData.layers.length; l++ ){
				// create a layer
				var newDepth = l * layerDepthOffset;
				var newName = "layer"+l+"_mc";		//->  layer0_mc
				var newLayer_mc = _this.createEmptyMovieClip( newName, newDepth );
				newLayer_mc.pic = new BitmapData( newWidth, newHeight, true, 0 );
				newLayer_mc.attachBitmap( newLayer_mc.pic, 0 );
				layers_mc_array.push( newLayer_mc );
			}// for:  each layer to add
		}// addNewLayers()
		
		
		function loadNewChipsets()
		{
			var chipsetLoad_prom = null;
			var loadAllChipsets_vow = VOW.make();
			// compare old chipset newData to new chipset newData
			// // remember all chipsets that already been loaded
			var allLoadedChipsets = {};
			var chipsetsToLoad = [];
			if(useCache)
			{// if:  re-use previously loaded chipsets
				for(var c=0; c<chipset_array.length; c++)
				{// for:  each existing chipset
					allLoadedChipsets[ chipset_array[c].file ] = chipset_array[c].pic;
				}// for:  each existing chipset
			}// if:  re-use previously loaded chipsets
			// // make a list of new chipsets that haven't already been loaded
			for(var c=0; c<newData.chipsets.length; c++)
			{// for:  each new chipset
				var newChipName = newData.chipsets[c];
				if(allLoadedChipsets[newChipName] == undefined)
				{// if:  this chipset has NOT been loaded yet
					chipsetsToLoad.push( newChipName );
				}// if:  this chipset has NOT been loaded yet
			}// for:  each new chipset
			// // load the remaining chipsets
			if(chipsetsToLoad.length > 0)
			{// if:  there are more chipsets to load
				var load_array = [];
				for(var l=0; l<chipsetsToLoad.length; l++)
				{// for:  each missing chipset
					// load & keep track of each chipset
					var load_promise = loadChipset( chipsetsToLoad[l] );
					load_array.push( load_promise );
				}// for:  each missing chipset
				// wait for all chipsets to finish loading
				currentChipsetLoad.chipsetLoad_prom = chipsetLoad_prom = VOW.every( load_array );		// store a global reference & a local reference, for comparison to check whether a new map-load has interrupted this one
				chipsetLoad_prom.then( checkForAbortedLoad )
				.then( afterAllChipsetsLoad, loadChipsetError );
			}// if:  there are more chipsets to load
			else
			{// if:  no additional chipsets needed
				// arrange chipsets, old and new, into the order specified by the new newData
				arrangeChipsets();
				// report success because no loading is needed
				loadAllChipsets_vow.keep();
			}// if:  no additional chipsets needed
			
			
			function loadChipset( fileName )
			{
				var vow = VOW.make();
				// Load the new image
				var load_promise = loadBitmap( fileName, chipsetLoader_mc );
				load_promise.then( afterLoading, error );
				
				// If successful...
				function afterLoading( newChipset_pic ){
					var chipsetData = {
						file: fileName,
						pic: newChipset_pic
					}
					vow.keep( chipsetData );
				}// afterLoading()
				function error( message ){
					vow.doBreak( {
						type: "fail",
						message: message
					} );
				}// error()
				
				return vow.promise;
			}// loadChipset()
			
			
			
			function checkForAbortedLoad( allNewChipsets ){
				var vow = VOW.make();
				var loadWasInterrupted = false;
				// if:  chipsets were already loading,  and another set of chipsets has started loading in the middle of that
				if( chipsetLoad_prom !== null  &&  chipsetLoad_prom !== currentChipsetLoad.chipsetLoad_prom )		loadWasInterrupted = true;
				
				// if:  this load-sequence has been replaced by another one
				if(loadWasInterrupted === true){
					// abort this old load-sequence
					vow.doBreak({
						type: "abort",
						message: "Another map has started to load.  Aborting the previous load-sequence."
					});
				}// if:  this load-sequence has been replaced by another one
				else
				{// if:  nothing has interrupted this load-sequence
					// pass along the loaded data
					vow.keep( allNewChipsets );
				}// if:  nothing has interrupted this load-sequence
				
				return vow.promise;
			}// checkForAbortedLoad()
			
			
			
			function afterAllChipsetsLoad( allNewChipsets )
			{
				// after all chipsets have loaded
				// add new loaded chipsets to allLoadedChipsets list
				for(var c=0; c<allNewChipsets.length; c++)
				{// for:  each new chipset
					var thisChipset = allNewChipsets[c];
					var newFile = thisChipset.file;
					var newPic = thisChipset.pic;
					allLoadedChipsets[ newFile ] = newPic;
				}// for:  each new chipset
				// arrange chipsets, old and new, into the order specified by the new newData
				arrangeChipsets();
				// report success
				loadAllChipsets_vow.keep();
			}// afterAllChipsetsLoad()
			
			
			function arrangeChipsets()
			{
				// chipset_array  (overall list for displayed map)
				chipset_array.splice(0);		// clear all previous elements
				for(var c=0; c<newData.chipsets.length; c++)
				{// for:  each chipset specified in the new data
					var filePath = newData.chipsets[c];
					var thisChipset = chipset_array[c] = {};
					thisChipset.file = filePath;
					thisChipset.pic = allLoadedChipsets[filePath];
				}// for:  each chipset specified in the new data
			}// arrangeChipsets()
			
			
			function loadChipsetError( err_obj ){
				loadAllChipsets_vow.doBreak( err_obj );
			}// loadChipsetError()
			
			
			return loadAllChipsets_vow.promise;
		}// loadNewChipsets()
		
		
		
		
		function drawAllTiles()
		{
			// for:  each tile on each layer
			for(var l=0; l<newData.layers.length; l++)
			{// for:  each layer
				for(var xx=0; xx<newData.width; xx++)
				{// for:  map width
					for(var yy=0; yy<newData.height; yy++)
					{// for:  map height
						var location = {
							layer: l,
							x: xx,
							y: yy
						}
						var tileData = newData.layers[l][xx][yy];
						updateTile( location, tileData, newData.tileSize );
					}// for:  map height
				}// for:  map width
			}// for:  each layer
		}// drawAllTiles()
		
		
		
		function finished()
		{
			drawMap_vow.keep();
		}// finished()
		
		
		
		function drawMapError( err_obj ){
			drawMap_vow.doBreak( err_obj );
		}// drawMapError()
		
		
		return drawMap_vow.promise;
	}// drawMap()
	
	
	
	////////////////////////////////////////////////////////////////////
	// updateTile()
	
	function updateTile( location, tileData, tileSize )
	{
		// EXTERNAL DEPENDENCIES:
		// chipset_array
		// layers_mc_array
		// Rectangle
		// Point
		
		var l = location.layer;
		var xx = location.x;
		var yy = location.y;
		//var tileData = newData.layers[l][xx][yy];
		var tileSize = tileSize || _this.tileSize;
		
		// copy from chipset -> bitmap layer
		var srcChipset_pic = chipset_array[ tileData.chipset ].pic;
		var destLayer_pic = layers_mc_array[l].pic;
		var xCopy = tileData.x * tileSize;
		var yCopy = tileData.y * tileSize;
		var copy = new Rectangle( xCopy,yCopy, tileSize,tileSize );
		var paste = new Point( xx * tileSize,  yy * tileSize );
		destLayer_pic.copyPixels( srcChipset_pic, copy, paste );
	}// updateTile()
	
	
	
	////////////////////////////////////////////////////////////////////
	// isCompressed()
	
	function isCompressed( data ){
		if( data.isCompressed === true )		return true;
		if( data.collision[0] === undefined )		return true;
		return false;
	}// isCompressed()
	
	
	
	////////////////////////////////////////////////////////////////////
	// compressMapData()
	
	function compressMapData( uncompressedData )
	{
		var dataIsCompressed = isCompressed( compressedData );
		if( dataIsCompressed == false )
		{// if:  data is not compressed
			var newData = {};
			
			// copy root variables
			newData.format = uncompressedData.format;
			newData.isCompressed = true;
			newData.tileSize = uncompressedData.tileSize;
			newData.width = uncompressedData.width;
			newData.height = uncompressedData.height;
			
			
			// copy chipsets
			newData.chipsets = [];
			for(var c=0; c<uncompressedData.chipsets.length; c++){
				newData.chipsets[c] = uncompressedData.chipsets[c];
			}// for:  each chipset
			
			
			// compress tiles
			newData.layers = [];
			for(var l=0; l<uncompressedData.layers.length; l++){
				newData.layers[l] = "";
				var thisLayer = uncompressedData.layers[l];
				for(var yy=0; yy<newData.height; yy++){
					for(var xx=0; xx<newData.width; xx++){
						var thisTile = thisLayer[xx][yy];
						var chipset = thisTile.chipset;
						var xChip = thisTile.x;
						var yChip = thisTile.y;
						var compressedValue = compressTileValue( chipset, xChip, yChip );
						newData.layers[l] += compressedValue;
					}// for:  horz tiles
				}// for:  vert tiles
			}// for:  each layer
			// convert layers
			
			
			// compress collision
			newData.collision = "";
			for(var yy=0; yy<newData.height; yy++){
				for(var xx=0; xx<newData.width; xx++){
					var uncompressedValue = Number( uncompressedData.collision[xx][yy] );
					var compressedValue = uncompressedValue.toString( encodingRadix );
					newData.collision += compressedValue;
				}// for:  horz tiles
			}// for:  vert tiles
			
			
			return newData;
		}// if:  data is not compressed
		else
		{// if:  data is already compressed
			// return the data without modifying it
			return uncompressedData;
		}// if:  data is already compressed
		
		
		
		function compressTileValue( chipset, x, y ){
			var chip_code = Number(chipset).toString( encodingRadix );
			var x_code = Number(x).toString( encodingRadix );
			var y_code = Number(y).toString( encodingRadix );
			var output = chip_code + x_code + y_code;
			return output;
		}// compressTileValue()
	}// compressMapData()
	
	
	
	////////////////////////////////////////////////////////////////////
	// decompressMapData()
	
	function decompressMapData( compressedData )
	{
		var dataIsCompressed = isCompressed( compressedData );
		if( dataIsCompressed == true )
		{// if:  data is compressed
			var newData = {};
			
			// copy root variables
			newData.format = compressedData.format;
			newData.isCompressed = false;
			newData.tileSize = compressedData.tileSize;
			newData.width = compressedData.width;
			newData.height = compressedData.height;
			
			
			// copy chipsets
			newData.chipsets = [];
			for(var c=0; c<compressedData.chipsets.length; c++){
				newData.chipsets[c] = compressedData.chipsets[c];
			}// for:  each chipset
			
			
			// de-compress tiles
			newData.layers = [];
			var readIndex = 0;
			var readLength = 3;
			for(var l=0; l<compressedData.layers.length; l++){
				// read data
				readIndex = 0;
				var layer_str = compressedData.layers[l];
				// write data
				newData.layers[l] = [];
				for(var xx=0; xx<newData.width; xx++){
					newData.layers[l][xx] = [];
					for(var yy=0; yy<newData.height; yy++){
						// read data
						var readFrom = ((compressedData.width *yy) +xx) *readLength;
						// write data
						var thisTile = newData.layers[l][xx][yy] = {};
						thisTile.chipset = decompressValue( layer_str.charAt(readFrom) );
						thisTile.x = decompressValue( layer_str.charAt(readFrom+1) );
						thisTile.y = decompressValue( layer_str.charAt(readFrom+2) );
					}// for:  each layer
				}// for:  each layer
			}// for:  each layer
			// convert layers
			
			
			// de-compress collision
			newData.collision = [];
			var readIndex = 0;
			var readLength = 1;
			for(var xx=0; xx<newData.width; xx++){
				newData.collision[xx] = [];
				for(var yy=0; yy<newData.height; yy++){
					var readFrom = (compressedData.width *yy) +xx;
					// decode that character  (base-36 => base-10 decimal)
					// store as a decimal number in that tile-location
					newData.collision[xx][yy] = parseInt( compressedData.collision.charAt(readFrom), 36 );
				}// height
			}// width
			
			
			return newData;
		}// if:  data is compressed
		else
		{// if:  data is already de-compressed
			// return the received data without modifying it
			return compressedData;
		}// if:  data is already de-compressed
		
		
		
		function decompressValue( encodedValue ){
			if( encodedValue=="0x" ){
				return 33;
			}else{
				return parseInt( encodedValue, encodingRadix );
			}
		}// decompressValue()
	}// decompressMapData()
	
	
	
	////////////////////////////////////////////////////////////////////
	// SCROLL
	
	// Get & Set the map's scroll position		(Use this instead of _x and _y from now on)		(This enables a standard scroll interface regardless of scroll method)
	_this.x = 0;
	_this.y = 0;
	
	
	// use scrollRect instead of _x _y
	_this.cacheAsBitmap = true;
	_this.scrollPos = new flash.geom.Point(0,0);		// Used only by this scroll function
	_this.scrollArea = new flash.geom.Rectangle( 0,0, 320,240 );
	_this.scroll = function( x, y, screenWidth, screenHeight, smoothness)
	{
		// resolve optional parameters
		if(x==undefined && SPRITES.player._x!=undefined)
			var x= SPRITES.player._x;
		if(y==undefined && SPRITES.player._y!=undefined)
			var y= SPRITES.player._y;
		var screenWidth = screenWidth || 320;
		var screenHeight = screenHeight || 240;
		var smoothness = smoothness || 1;
		//
		if(_this.scrollArea.width != screenWidth)		_this.scrollArea.width = screenWidth;
		if(_this.scrollArea.height != screenHeight)		_this.scrollArea.height = screenHeight;
		// calculate view screen center
		var halfWidth = screenWidth / 2;
		var halfHeight = screenHeight / 2;
		// get the map's pixel width
		var mapWidth = layers_mc_array[0].pic.width;
		var mapHeight = layers_mc_array[0].pic.height;
		
		var __x = _this.x;
		var __y = _this.y;
		
		// horz
		if( x < halfWidth)
		{
			// far-left
			var xTarg = 0;
		}
		else if( x > mapWidth-halfWidth)
		{
			// far-right
			var xTarg = -mapWidth+screenWidth;
		}
		else
		{
			// scroll
			var xTarg = -x + halfWidth;
		}
		
		
		// vert
		if( y < halfHeight)
		{
			// top
			var yTarg = 0;
		}
		else if( y > mapHeight-halfHeight)
		{
			// bottom
			var yTarg = -mapHeight+screenHeight;
		}
		else
		{
			// scroll
			var yTarg = -y + halfHeight;
		}
		
		
		// apply target position,  OR center the map if it's too small
		if( mapWidth < screenWidth )
		{// if:  map is smaller than the screen  (horz)
			// center the map
			var xDiff = screenWidth-mapWidth;
			__x = xDiff/2;
		}// if:  map is smaller than the screen  (horz)
		else
		{
			__x += (xTarg-_this.scrollPos.x) * smoothness;
		}
		
		
		if( mapHeight < screenHeight )
		{// if:  map is smaller than the screen  (horz)
			// center the map
			var yDiff = screenHeight-mapHeight;
			__y = yDiff/2;
		}// if:  map is smaller than the screen  (horz)
		else
		{
			__y += (yTarg-_this.scrollPos.y) * smoothness;
		}
		
		
		// round to nearest pixel  (prevent "floating" sprites)
		_this.x = _this.scrollPos.x = Math.floor(__x);
		_this.y = _this.scrollPos.y = Math.floor(__y);
		
		// apply a scrollRect
		_this.scrollArea.x = -_this.scrollPos.x;
		_this.scrollArea.y = -_this.scrollPos.y;
		_this.scrollRect = _this.scrollArea;
	}// scroll()
	
	
	
	////////////////////////////////////////////////////////////////////
	// INTERFACE
	
	_this.drawMap = drawMap;
	_this.updateTile = updateTile;
	_this.isCompressed = isCompressed;
	_this.compressMapData = compressMapData;
	_this.decompressMapData = decompressMapData;
	
	return _this;
}// makeMap()