package
{
	import flash.display.*;
	import flash.net.*;
	import flash.events.*;
	import flash.desktop.*;
	import flash.errors.*;
	import flash.system.*;
	import flash.utils.*;
	import by.blooddy.crypto.*;

	public class Exporter extends MovieClip
	{
		public static var Instance:Exporter = null;
		
		public static var DefaultPort:int = 7890;
		private var driver:Socket = null;
		private var debug:Boolean = false;
		public function Exporter()
		{
			Instance = this;
			
			try
			{				
				NativeApplication.nativeApplication.addEventListener("invoke", ApplicationInvoked);
			}
			catch (e)
			{
				// not an air runtime app
				ConnectDriver(DefaultPort);	// try default port
			}
		}
		
		public function ApplicationInvoked(e)
		{
			if (e.arguments.length == 1)
			{
				ConnectDriver(parseInt(e.arguments[0]));
			}
			else
			{
				ConnectDriver(DefaultPort);
			}
		}
		
		private var connected:Boolean = false;
		private var lastPort:int;
		private function ConnectDriver(port:int, firstTime:Boolean = true)
		{
			this.lastPort = port;
			if (firstTime)
			{
				Trace("Attempting to connect to driver");
			}
			try
			{
				var s:Socket = new Socket();
				s.addEventListener(IOErrorEvent.IO_ERROR, DriverError);
				s.addEventListener(Event.CONNECT, DriverConnected);
				s.addEventListener(Event.CLOSE, DriverClosed);
				s.connect("localhost", port);
			}
			catch (e)
			{
				Trace("Error establishing wrapper connection");
			}
		}
		
		private function DriverClosed(e)
		{
			connected = false;
			if (debug)
			{
				ConnectDriver(lastPort);
			}
			else
			{
				Quit();
			}
		}
		
		private function DriverError(e)
		{
			if (!debug)
			{
				Trace("  Failed to connect");
			}
			if (debug && !connected)
			{
				setTimeout(ConnectDriver, 500, lastPort, false);
			}
		}
		
		private function DriverConnected(e)
		{
			connected = true;
			Trace("  Connected");
			driver = e.target as Socket;
			driver.addEventListener(ProgressEvent.SOCKET_DATA, DriverRecv);
		}
		
		private function Quit()
		{
			fscommand("quit");
			try
			{
				NativeApplication.nativeApplication.exit();
			} catch (e)
			{
				
			}
		}

		private var jobQueue:Array = new Array();
		private var activeJob:Job = null;
		private function QueueJob(jobParams)
		{				
			if (!jobParams.hasOwnProperty("id"))
			{
				Print("Failure: job details didn't contain job id");
				CheckNewJob();
				return;
			}
			var id:int = parseInt(jobParams.id);
			
			if (!jobParams.hasOwnProperty("graphics") || getQualifiedClassName(jobParams.graphics) != "Array")
			{
				Print("Failure: job " + id + " details didn't contain graphics array");
				JobFailed(id);
				return;
			}
			var graphicsDetails:Array = jobParams.graphics;

			delete jobParams.command;
			delete jobParams.id;
			delete jobParams.graphics;
			
			var job:Job = new Job(id, graphicsDetails);
			jobQueue.push(job);
			CheckNewJob();
		}
		
		private function JobFailed(id:int)
		{
			DriverCommand("done", {id:id});
			CheckNewJob();
		}
		
		private function CheckNewJob()
		{
			if (activeJob == null && jobQueue.length > 0)
			{
				activeJob = jobQueue.shift();
				activeJob.Go(JobComplete);
			}
		}
		
		private function JobComplete()
		{
			Trace("Done: " + activeJob.GetID());
			
			var data = activeJob.GetData();
			var resources:Array = activeJob.GetResources().map(function(r:ByteArray, i, arr) { return Base64.encode(r); });
			DriverCommand("done", {id:activeJob.GetID(), data:data, resources:resources});
			
			activeJob = null;
			CheckNewJob();
		}
		
		private function DispatchCommand(cmdData)
		{
			try
			{
				if (cmdData.hasOwnProperty("command"))
				{
					var cmd = "" + cmdData.command;
					//Trace("cmd: " + cmd);
					switch (cmd)
					{
						case "trace":
							Trace(cmdData.string);
						break;
						case "exit":
							Quit();
						break;
						case "job":
							QueueJob(cmdData);
						break;
						default:
							Trace("Recv unknown: " + JSON.stringify(cmdData));
						break;
					}
				}
			} catch (e)
			{
				Print(e.getStackTrace());
			}
		}
		
		private var buffer:ByteArray = new ByteArray();
		private var swpBuffer:ByteArray = new ByteArray();
		private function DriverRecv(e)
		{
			var oldPos = buffer.position;
			//Trace("recv");
			try
			{
				driver.readBytes(buffer, buffer.length, driver.bytesAvailable);
				//Trace(buffer.length - buffer.position);			
				var len:uint = buffer.readUnsignedInt();
				var msg = buffer.readUTFBytes(len);

				buffer.readBytes(swpBuffer);
				buffer.clear();
				
				var tmp = buffer;
				buffer = swpBuffer;
				swpBuffer = tmp;
				
				var cmdData = JSON.parse(msg);
				DispatchCommand(cmdData);
			}
			catch (eof:EOFError)
			{
				buffer.position = oldPos;
				//Trace("Waiting for more data... " + driver.bytesAvailable + " " + (buffer.length - buffer.position));
				// wait for more data
			}
			catch (err:IOError)
			{
				Trace("Driver IO error");
			}
			catch (err)
			{
				Trace("Problem reading from driver:\n" + err.toString());
			}
		}
		
		private function DriverCommand(cmd:String, data = null)
		{
			var obj = {command:cmd};
			if (data != null)
			{
				for (var k in data)
				{
					obj[k] = data[k];
				}
			}
			var str:String = JSON.stringify(obj);
			DriverSend(str);
		}
		
		private function DriverSend(str:String)
		{
			if (driver != null)
			{
				driver.writeUnsignedInt(uint(str.length));
				driver.writeUTFBytes(str);
				driver.flush();
			}
		}
		
		public function Trace(str)
		{
			str = "" + str;
			traceText.x = 2;

			trace(str);
			traceText.width = stage.stageWidth - 4;
			traceText.height = stage.stageHeight + 100;			
			if (traceText.text == "")
			{
				traceText.text = str;
			}
			else
			{
				traceText.appendText("\n" + str);
			}
			while (traceText.textHeight > traceText.height)
			{
				var split = traceText.text.indexOf("\n");
				if (split == -1)
				{
					traceText.height = traceText.textHeight + 5;
					break;
				}
				traceText.text = traceText.text.substr(split + 1);
			}
			traceText.y = stage.stageHeight - traceText.textHeight - 2;
		}
		
		public function Print(str:String, localTrace:Boolean = true)
		{
			if (localTrace || driver == null)
			{
				Trace(str);
			}
			else
			{
				trace(str);
			}
			if (driver != null)
			{
				DriverCommand("print", {string:str + "\n"});
			}
		}
	}
}