package Packer
{
	import flash.display.MovieClip;
	import flash.display.Stage;
	import flash.geom.*;
	import flash.display.*;
	import flash.utils.ByteArray;
	import flash.utils.getQualifiedClassName;
	import flash.net.FileReference;
	import flash.utils.Endian;
	import flash.events.MouseEvent;
	import flash.events.KeyboardEvent;
	import flash.utils.*;
	
	import djarts.core.CacheManager;
	import com.adobe.serialization.json.JSON;
	import flash.filters.BitmapFilter;
	import flash.filters.BlurFilter;
	import flash.filters.ColorMatrixFilter;
	import fl.motion.AdjustColor;
	
	import djarts.core.*;
	import djarts.display.*;

	public class GraphicPacker
	{
		protected var clips:Array = new Array();
		protected var clipSizing:Array = new Array();
		protected var fonts:Array = new Array();
		protected var fontCopies:Array = new Array();
		protected var sheetWidth = 1024;
		protected var sheetHeight = 1024;
		protected var stage;
		
		protected var additionalData = {};
		public var regionsOutput = new MovieClip();
		
		public function Init(sheetWidth, sheetHeight, stage)
		{
			this.sheetWidth = sheetWidth;
			this.sheetHeight = sheetHeight;
			this.stage = stage;
		}

		protected var doneCallback = null;
		protected var callbackParam;
		public function ExportDefs(graphicDefs:Array, callback, callbackParam)
		{
			this.doneCallback = callback;
			this.callbackParam = callbackParam;
			var clipCollection:GraphicDefClipCollection = new GraphicDefClipCollection(graphicDefs, GraphicDefsLoaded);
		}

		public function ExportClips(clips:Array)
		{
			this.clips = clips;
			var sheetSize:Point = this.DetermineMinimumSheetSizeForClips();
			this.sheetWidth = sheetSize.x;
			this.sheetHeight = sheetSize.y;
			return this.Export(false);
		}
		
		public function GraphicDefsLoaded(clips:Array)
		{
			this.clips = clips;
			var sheetSize:Point = this.DetermineMinimumSheetSizeForClips();
			this.sheetWidth = sheetSize.x;
			this.sheetHeight = sheetSize.y;
			var output:ByteArray = this.Export(false);
			this.doneCallback(output, callbackParam);
		}
		
		public function AddClip(clip:MovieClip, scale=1, sizing:Point = null)
		{
			clip.scaleX = clip.scaleX * scale;
			clip.scaleY = clip.scaleY * scale;
			clips.push(clip);
			clipSizing.push(sizing);
		}
		
		public function AddBitmapFont(font:BitmapFont)
		{
			fonts.push(font);
		}
		
		public function AddBitmapFontCopy(font:BitmapFont, toName:String)
		{
			fontCopies.push({from:font,to:toName});
		}
		
		protected var sheet = 0;
		public function ExportFrom(clip, scale=1, save = true)
		{
			
			for (var i = 0; i < clip.numChildren; i++)
			{
				if (clip.getChildAt(i) is MovieClip)
				{
					AddClip(clip.getChildAt(i), scale);
				}
			}
			return Export(save);
		}
		
		public static const CACHESEQUENCES = 0;
		public static const PAD = 2;
		
		public function GetSheets(){return sheets;}
		
		public var sheets:Array = new Array();
		protected var sprites:Array = new Array();
		protected var spriteOriginalClips:Array = new Array();
		public function Export(save = true)
		{
			additionalData = {};
			sheets = new Array();
			sprites = new Array();
			spriteOriginalClips = new Array();
			
			var areas:Array = new Array();
			for (var i in clips)
			{
				areas[i] = clips[i].width * clips[i].height;
			}
			
			var sorted:Array = areas.sort(Array.RETURNINDEXEDARRAY | Array.NUMERIC | Array.DESCENDING);
			
			sheets.push(new SpriteSheet(sheetWidth, sheetHeight));
			for (var i = 0; i < clips.length; i++)
			{
				var sprite:TextureSprite = new TextureSprite(clips[sorted[i]], CACHESEQUENCES);
				CacheClip(clips[sorted[i]], sprite);
				sprites.push(sprite);
				spriteOriginalClips.push(clips[sorted[i]]);
				_strace("cached clip "+i+" of "+clips.length);
			}
			
			var fontSpritesByName = {};
			for (var i in fonts)
			{
				var sprite:TextureSprite = new TextureSprite(fonts[i], CACHESEQUENCES);
				CopyBitmapFontToSingleSheet(fonts[i], sprite);
				sprites.push(sprite);
				spriteOriginalClips.push(fonts[i]);
				additionalData[fonts[i].Name] = fonts[i].GetAdditionalData();
				fontSpritesByName[fonts[i].Name] = sprite;
			}
			
			for (var i in fontCopies)
			{
				var f:BitmapFontCopy = new BitmapFontCopy(fontCopies[i].to, fontCopies[i].from.Size, fontCopies[i].from.Bold, fontCopies[i].from);
				sprites.push(fontSpritesByName[fontCopies[i].from.Name]);
				spriteOriginalClips.push(f);
				additionalData[f.Name] = f.GetAdditionalData();
			}
			
			
			var output:ByteArray = new ByteArray();
			output.endian = Endian.LITTLE_ENDIAN;
			
			output.writeUnsignedInt(sheets.length);
			for (var i = 0; i < sheets.length; i++)
			{
				var s:SpriteSheet = sheets[i];
				output.writeUnsignedInt(s.width);
				output.writeUnsignedInt(s.height);
				var png:ByteArray = PNGExportHelper.GetExportPNG(s.GetBitmap());
				output.writeUnsignedInt(png.length);
				output.writeBytes(png);
			}
			
			for (var i = 0; i < sprites.length; i++)
			{
				var orig = spriteOriginalClips[i];
				if (orig is BitmapFont && orig.AsBackupOnly)
				{
					sprites.splice(i, 1);
					spriteOriginalClips.splice(i, 1);
					i--;
				}
			}			
			
			output.writeUnsignedInt(sprites.length);
			for (var i = 0; i < sprites.length; i++)
			{
				var tex:TextureSprite = sprites[i];
				var orig = spriteOriginalClips[i];
				var clipName = "clip";
				if (orig is BitmapFont || orig is BitmapFontCopy)
				{
					clipName = orig.Name;
				} else {
					clipName = getQualifiedClassName(orig);
					if (clipName == "flash.display::MovieClip")
					{
						clipName = orig.name;
					}
				}
				_strace("Exporting "+clipName);
				output.writeUnsignedInt(clipName.length);
				output.writeMultiByte(clipName, "us-ascii");
				output.writeUnsignedInt(tex.NumSequences());
				for (var j = 0; j < tex.NumSequences(); j++)
				{
					var info:SequenceInfo = tex.GetSequenceInfo(j);
					output.writeUnsignedInt(info.Length);
					_strace("With "+info.Length+" Frames");
					for (var k = 0; k < info.Length; k++)
					{
						output.writeUnsignedInt(sheets.indexOf(info.FrameTextures[k]));
						var location:Rectangle = info.FrameNodes[k].Rect;
						output.writeFloat(location.x + PAD);
						output.writeFloat(location.y + PAD);
						output.writeFloat(location.width - (PAD * 2));
						output.writeFloat(location.height - (PAD * 2));
						var offset:Point = info.FrameOffsets[k];
						output.writeFloat(offset.x - PAD);
						output.writeFloat(offset.y - PAD);
						
						regionsOutput.graphics.beginFill(0x00CCFF, .2);
						regionsOutput.graphics.lineStyle(1, 0x00CCFF);
						regionsOutput.graphics.drawRect(location.x + PAD, location.y + PAD, location.width - (PAD * 2), location.height - (PAD * 2));
						regionsOutput.graphics.endFill();
					}
				}
			}
			
			var additionalDataString = com.adobe.serialization.json.JSON.encode(additionalData);
			output.writeUnsignedInt(additionalDataString.length);
			output.writeMultiByte(additionalDataString, "us-ascii");
			
			if (save)
			{
				var fr:FileReference = new FileReference();
				fr.save(output, "data.pak");
			}
			_strace("done. num sheets: " + sheets.length);
			
			return output;
		}
		

		public function DetermineMinimumSheetSizeForClips():Point
		{

			/* TODO - FOR LARGE SIZED CHUNKS BETTER HANDLING FOR FINDING MINIMUM SIZE! */
			
			sheets = new Array();
			sprites = new Array();
			
			var areas:Array = new Array();
			for (var i in clips)
			{
				areas[i] = clips[i].width * clips[i].height;
			}
			var sorted:Array = areas.sort(Array.RETURNINDEXEDARRAY | Array.NUMERIC | Array.DESCENDING);
			
			sheets.push(new SpriteSheet(2048, 2048));
			for (var i = 0; i < clips.length; i++)
			{
				var sprite:TextureSprite = new TextureSprite(clips[sorted[i]], CACHESEQUENCES);
				CacheClip(clips[sorted[i]], sprite);
				sprites.push(sprite);
			}
			
			var allUsedRects:Array = new Array();
			var nodesToVisit:Array = new Array();
			for (var i = 0; i < sheets.length; i++)
			{
				var sheet:SpriteSheet = sheets[i];
				var nodes = sheet.GetAllocator().GetLeafNodes();
				for (var n = 0; n < nodes.length; n++)
				{
					allUsedRects.push(nodes[n].Rect);
				}
			}
			
			var sizeFound = false;
			var sheetSizeX = 64;
			var sheetSizeY = 64;
			while(!sizeFound)
			{
				var sheet:SpriteSheet = new SpriteSheet(sheetSizeX, sheetSizeY);				
				sizeFound = true;
				for(var i = 0; i < allUsedRects.length; i++)
				{
					var node:RectangleNode = sheet.GetAllocator().Insert(allUsedRects[i].width, allUsedRects[i].height);
					if (node == null)
					{
						sizeFound = false;
						break;
					}
				}
				if (sizeFound == false)
				{
					if (sheetSizeX == sheetSizeY)
					{
						sheetSizeX *= 2;
					}
					else					
					{
						sheetSizeY *= 2;
					}
				}
			}
			return new Point(sheetSizeX, sheetSizeY);
		}
		
		
		protected function CopyBitmapFontToSingleSheet(font:BitmapFont, sprite:TextureSprite)
		{
			sprite.SetSequence(0, font.CodepointData.length);
			
			// info to go into the sprite
			var offsets:Array = new Array();
			var bounds:Array = new Array();			
			var nodes:Array = new Array();
			var localSheets:Array = new Array();
			
			// track the nodes we have locally created, if we can't fit
			// everything on 1 sheet we will remove them
			var createdNodes:Array = new Array();			
			
			var createdNewSheet = false;
			
			var useSheets:Array = new Array();
			for (var s = 0; s < this.sheets.length; s++)
			{
				useSheets.push(s);
			}
			
			for (var sheetIndex = 0; sheetIndex < useSheets.length; sheetIndex++)
			{
				var s = useSheets[sheetIndex];
				var sheet:SpriteSheet = sheets[s];
				
				if (!sheet.sheetFull)
				{
					var ok = true;
					
					for (var i = 0; i < font.CodepointData.length; i++)
					{
						var node:RectangleNode = null;
						
						var data:FontCodepointData = font.CodepointData[i];
						if (font.AsBackupOnly && data.References == 0)
						{
							// do nothing
						}
						else if (data.FromOtherFont)
						{
							// this is a copy node, from a backup font
							node = data.FromFont.CodepointData[data.FromFontIndex].DestinationNode;
						}
						else
						{
							node = sheet.GetAllocator().Insert(data.Data.width, data.Data.height);
							if (node == null)
							{
								// failed to fit the entire font on a sheet, so let's clear
								// everything and start again
								ok = false;
								
								for (var j in createdNodes)
								{
									sheet.GetBitmap().fillRect(createdNodes[j].Rect, 0x0);
								}
								
								sheet.GetAllocator().RemoveNodes(createdNodes, false);
								nodes = new Array();
								createdNodes = new Array();
								
								break;
							}
							createdNodes.push(node);
						}
						
						nodes.push(node);
					}
					if (ok)
					{
						// the nodes array is completed (length == font.CodepointData.length)
						// now we have to setup the sheet indices, bounds, offsets, etc...
						for (var i = 0; i < font.CodepointData.length; i++)
						{
							var data:FontCodepointData = font.CodepointData[i];
							if (font.AsBackupOnly && data.References == 0)
							{
								// do nothing
								offsets.push(new Point());
								bounds.push(new Rectangle());
								localSheets.push(-1);
							}
							else
							{
								var node:RectangleNode = nodes[i];
								
								if (!data.FromOtherFont)
								{
									sheet.GetBitmap().copyPixels(data.Data, data.Data.rect, new Point(node.Rect.x, node.Rect.y), null, null, true);
								}
								
								offsets.push(data.Offset);
								bounds.push(node.Rect.clone());
								
								localSheets.push(node.Host.HostTexture);
								
								data.DestinationNode = node;
							}
						}
						break;
					}
					else					
					{
						if (s == sheets.length - 1)
						{
							if (createdNewSheet)
							{
								// couldn't fit the font on 1 sheet
								CopyBitmapFontToSheets(font, sprite);
								return;
							}
							else
							{
								createdNewSheet = true;
								var newSheet:SpriteSheet = new SpriteSheet(sheetWidth, sheetHeight);
								newSheet.SheetIndex = sheets.length;
								useSheets.push(sheets.length);
								sheets.push(newSheet);
							}
						}
					}
				}
			}
			
			for (var j = 0; j < nodes.length; j++)
			{
				sprite.AddFrame(0, j, offsets[j], bounds[j], localSheets[j], nodes[j], null);
			}

			sprite.Loaded = true;
		}
		
		protected function CopyBitmapFontToSheets(font:BitmapFont, sprite:TextureSprite)
		{
			sprite.SetSequence(0, font.CodepointData.length);
			
			// info to go into the sprite
			var offsets:Array = new Array();
			var bounds:Array = new Array();
			var nodes:Array = new Array();
			var localSheets:Array = new Array();
			
			var i = 0;
			
			var useSheets:Array = new Array();
			for (var s = 0; s < this.sheets.length; s++)
			{
				useSheets.push(s);
			}
			var sheetIndex = useSheets.length - 1;
			
			var searchBackwards = true;
			
			var done = false;
			
			while (!done)
			{
				var s = useSheets[sheetIndex];
				var sheet:SpriteSheet = sheets[s];
				
				if (!sheet.sheetFull)
				{
					var count = 0;
					var spaceAvailable = true;
					
					while (spaceAvailable && i < font.CodepointData.length)
					{
						var data:FontCodepointData = font.CodepointData[i];
						
						var node:RectangleNode = null;
						var offset:Point = new Point();
						
						if (font.AsBackupOnly && data.References == 0)
						{
							// do nothing, this sprite won't be used ever
						}
						else if (data.FromOtherFont)
						{
							var copyData:FontCodepointData = data.FromFont.CodepointData[data.FromFontIndex];
							node = copyData.DestinationNode;
							offset = copyData.Offset;
						}
						else
						{
							node = sheet.GetAllocator().Insert(data.Data.width, data.Data.height);
							if (node == null)
							{
								spaceAvailable = false;
								break;
							}
								
							offset = data.Offset;

							sheet.GetBitmap().copyPixels(data.Data, data.Data.rect, new Point(node.Rect.x, node.Rect.y), null, null, true);
						}
						
						nodes.push(node);
						offsets.push(offset);
						bounds.push(node.Rect.clone());
						localSheets.push(node.Host.HostTexture);
						
						data.DestinationNode = node;
						
						i++;
						count++;
					}
					/*
					if (count > 0)
					{
						for (var q = 0; q < count; q++)
						{
							localSheets.push(sheet);
						}
					}
					*/

					if (i < font.CodepointData.length)
					{
						if (searchBackwards)
						{
							sheetIndex--;
							if (sheetIndex < 0)
							{
								searchBackwards = false;
								sheetIndex = sheets.length - 1;
							}
						}
						if (!searchBackwards)
						{
							var newSheet:SpriteSheet = new SpriteSheet(sheetWidth, sheetHeight);
							newSheet.SheetIndex = sheets.length;
							useSheets.push(sheets.length);
							sheets.push(newSheet);
							sheetIndex++;
						}
					}
					else
					{
						done = true;
					}
				}
			}
			
			for (var j = 0; j < offsets.length; j++)
			{
				sprite.AddFrame(0, j, offsets[j], bounds[j], localSheets[j], nodes[j], null);
			}

			sprite.Loaded = true;
		}
		
		public static var UseFlatHeuristic = false;
		private function AnalyzeClipType(clip:MovieClip):MovieClip
		{
			var isFlat = false;
			
			if (clip.totalFrames > 1 && UseFlatHeuristic)
			{
				var framesWithMultiChildren = 0;
				var totalChildren = 0;
				var childrenWithSingleFrame = 0;
				for (var i = 0; i < clip.totalFrames; i++)
				{
					clip.gotoAndStop(i);
					var children = clip.numChildren;
					totalChildren += children;
					if (children > 1) framesWithMultiChildren++;
					
					for (var j = 0; j < clip.numChildren; j++)
					{
						var child = clip.getChildAt(j);
						if (!(child is MovieClip) || child.totalFrames == 1)
						{
							childrenWithSingleFrame++;
						}
					}
				}
				if (framesWithMultiChildren / clip.totalFrames > 0.75)
				{
					if (childrenWithSingleFrame / totalChildren > 0.75)
					{
						isFlat = true;
					}
				}
			}
			
			if (isFlat)
			{
				var ret:MovieClip = new MovieClip();
				ret.addChild(clip);
				return ret;
			}
			else				
			{
				return clip;
			}
		}
		
		protected function CacheClip(clip:MovieClip, sprite:TextureSprite)
		{
			clip = AnalyzeClipType(clip);
			
			var totalFrames = clip.totalFrames;
			
			var startSeq = 1;
			var endSeq = totalFrames;

			for (var i = startSeq; i <= endSeq; i++)
			{
				var endFrame = 1;
				
				RecursivelyStop(clip, i);

				
				for (var j = 0; j < clip.numChildren; j++)
				{
					var child = clip.getChildAt(j);
					if (child is MovieClip)
					{
						if (child.totalFrames > endFrame) endFrame = child.totalFrames;
					}
				}

				var offsets:Array = new Array();
				var bounds:Array = new Array();
				var nodes:Array = new Array();
				var localSheets:Array = new Array();
				var completed = false;
				var startFrame = 1;

				sprite.SetSequence(i - 1, endFrame);
				
				var origStart = startFrame;
				
				var useSheets:Array = new Array();
				for (var s = 0; s < this.sheets.length; s++)
				{
					useSheets.push(s);
				}
				
				for (var sheetIndex = 0; sheetIndex < useSheets.length; sheetIndex++)
				{
					var s = useSheets[sheetIndex];
					var sheet:SpriteSheet = sheets[s];
					//if (this.ForceSpriteSheet) sheet = this.ForceSpriteSheet;
					if (!sheet.sheetFull)
					{
						var count = 0;
						count = CacheFrames(clip, offsets, bounds, sheet, nodes, startFrame, endFrame);
						if (count > 0)
						{
							for (var q = 0; q < count; q++)
							{
								localSheets.push(sheet);
							}
						}
						
						startFrame += count;
						
						if (startFrame <= endFrame && s == sheets.length - 1)
						{
							var newSheet:SpriteSheet = new SpriteSheet(sheetWidth, sheetHeight);
							newSheet.SheetIndex = sheets.length;
							useSheets.push(sheets.length);
							sheets.push(newSheet);
							
						}
					}
				}
				
				if (localSheets.length < endFrame - startFrame + 1)
				{
					_strace("GraphicPacker: Problem caching");
				}
				
				for (var j = 0; j < offsets.length; j++)
				{
					sprite.AddFrame(i - 1, j + origStart - 1, offsets[j], bounds[j], localSheets[j], nodes[j], null);
				}
			}
			
			sprite.Loaded = true;
		}
		
		protected function CacheFrames(clip:MovieClip, offsets:Array, bounds:Array, sheet:SpriteSheet, nodes:Array, startFrame = 1, lastFrame = -1)
		{
			
			if (lastFrame == -1) lastFrame = clip.totalFrames;

			for (var i = startFrame; i <= lastFrame; i++)
			{
				
				SubclipsGotoFrame(clip, i, {}, clip);
				RecursivelyStop(clip);
				
				var currentMatrix = clip.transform.matrix.clone();
				
				var flippedX = currentMatrix.a < 0;
				var flippedY = currentMatrix.d < 0;
				var m:Matrix = new Matrix(currentMatrix.a, 0, 0, currentMatrix.d);
				
				var r:Rectangle = clip.getBounds(clip);
				r = CacheManager.Instance().GetAccurateBounds(clip, 2048);
				
				var usedCacheBounds = false;
				
				if (clip.getChildByName("__cache_bounds__"))
				{
					usedCacheBounds = true;
					r = clip.getChildByName("__cache_bounds__").getRect(clip);
					clip.mask = clip.getChildByName("__cache_bounds__");
				}
				
				r.x *= Math.abs(m.a);
				r.y *= Math.abs(m.d);
				r.width *= Math.abs(m.a);
				r.height *= Math.abs(m.d);
				r.width += PAD * 2.0;
				r.height += PAD * 2.0;
				var w = Math.ceil(r.width);
				var h = Math.ceil(r.height);
						
				
				var node:RectangleNode = sheet.GetAllocator().Insert(w, h);
				if (node == null)
				{
					return i - startFrame;
				}
				
				nodes.push(node);
				
				var target:Point = new Point(node.Rect.x, node.Rect.y);
				//flippedX = false;
				var offsetX = flippedX ? Math.ceil(r.right - PAD * 2.0) : Math.ceil(-r.left);
				var offsetY = flippedY ? Math.ceil(r.bottom - PAD * 2.0) : Math.ceil(-r.top);
				
				m.translate(offsetX + target.x + PAD, offsetY + target.y + PAD);
				
				var offset:Point = new Point(offsetX + PAD, offsetY + PAD);
				
				RenderBitmap(sheet, clip, m, new ColorTransform(1.0, 1.0, 1.0, clip.alpha));				
				
				offsets.push(offset);
				bounds.push(node.Rect.clone());
				
				if (usedCacheBounds)
				{
					clip.mask = null;
				}
				
			}
			
			return lastFrame - startFrame + 1;
		}
		
		protected function RenderBitmap(sheet:SpriteSheet, clip:MovieClip, m:Matrix, ct:ColorTransform)
		{
			sheet.GetBitmap().draw(clip, m, ct);
		}
		
		protected function SubclipsGotoFrame(clip:MovieClip, i, force, rootClip, exceptClip = null)
		{
						
			for (var j = 0; j < clip.numChildren; j++)
			{
				var child = clip.getChildAt(j);
				if (child is MovieClip && child != exceptClip)
				{
					if (i != null) child.gotoAndStop(((i - 1) % child.totalFrames) + 1);
					SubclipsGotoFrame(child, i, force, rootClip);
				}
			}
		}
		protected function RecursivelyStop(clip, frame = null)
		{
			if (clip is MovieClip)
			{
				if (frame == null)
				{
					clip.stop();
				} else {
					clip.gotoAndStop(frame);
				}
			}
			if (clip is DisplayObjectContainer)
			{
				for (var i = 0; i < clip.numChildren; i++)
				{				
					RecursivelyStop(clip.getChildAt(i));
				}
			}
		}
		
		
	}
}