﻿/**
 * LZW Class
 * Compresses a string to its LZW version
 *
 * @author		Zeh Fernando
 * @version		1.0.0
 * 
 * 17 Oct 06 (1.0.0) - first version
 * 08 Nov 06 (1.1.0) - added dispose()
 */

/*

Part of this code is based on the Python code available at:
http://en.wikipedia.org/wiki/LZW

With help from:
http://marknelson.us/1989/10/01/lzw-data-compression/

*/

/*

	// short, quick strings

	import zeh.compression.LZW
	var myString = "This is a test string";
	var myCompressedString = LZW.compress(myString);

*or*

	// large strings that should be split between frames

	import zeh.compression.LZW
	var myString = "This is a test string";

	var myID = LZW.slowCompress(myString);

	...

	trace ("The string compression is " + (LZW.getSlowCompressFactor(myID) * 100) + "% complete.");

	...

	myCompressedString = LZW.getSlowCompressString(myID);
	
	LZW.dispose(myID); // delete the string and its data

*/

class zeh.compression.LZW {

	private static var _processList:Array;			// List of compression processes

	/**
	 * No constructor.
	 */
	public function LZW() {
		trace ("This is an static Class and should not be instantiated.");
	}

	// ==================================================================================================================================
	// FAST COMPRESSION function --------------------------------------------------------------------------------------------------------

	/**
	 * Quick compression function, to do everything in one shot
	 *
	 * @param		p_str		String		String to be compressed
	 * @return					Object		Compressed string
	 */
	public static function compress (p_str:String): String {
		/*
		// Quick version, that works just the same but doesn't reuse code:

		var ns:String = "";				// New String
		var s:Array = p_str.split("");	// Original text split on an array
		var t:Array = new Array();		// Table for compression codes
		var l:Number = s.length;		// Array length for faster looping
		var is:String = "";				// Current input string
		var ic:String;					// Current input character

		var i:Number;

		// Fills the table with default codes
		for (i = 0; i < 256; i++) t[String.fromCharCode(i)] = i;
		var tl:Number = 256;			// Current table length

		for (i = 0; i <= l; i++) {
			ic = s[i];
			if (!t[is+ic]) {
				// Not on the table, adds
				ns += String.fromCharCode(t[is]);
				t[is+ic] = tl++;
				is = ic;
			} else {
				// Already on the list
				is += ic;
			}
		}
		
		return (ns);
		*/

		var pid:Number = createCompressProcess(p_str);
		iterateCompressProcess(_processList[pid], 0);
		var compressedData:String = getSlowCompressString(pid);
		dispose(pid);
		return compressedData;

	}


	// ==================================================================================================================================
	// SLOW COMPRESSION functions -------------------------------------------------------------------------------------------------------

	/**
	 * Start compressing a string -- should be used on large strings
	 *
	 * @param		p_str		String		String to be compressed
	 * @return					Number		ID (numeric index) of the string process
	 */
	public static function slowCompress (p_str:String):Number {
		return createCompressProcess(p_str);
	}

	/**
	 * Create a new string compression process
	 *
	 * @param		p_str		String		String to be compressed
	 * @return					Number		ID (numeric index) of the string process
	 */
	private static function createCompressProcess(p_str:String): Number {
		var p = {};
		p.method = "compress";			// Method ("compress", "decompress")
		p.ns = "";						// New String
		p.isFinished = false;			// Whether or not has finished
		p.s = p_str.split("");			// Original text split on an array
		p.t = new Array();				// Table for compression codes
		for (var i:Number = 0; i < 256; i++) p.t[String.fromCharCode(i)] = i;
		p.tl = 256;						// Current table length
		p.l = p.s.length;				// Array length for faster looping
		p.is = "";						// Current input string
		p.ic = undefined;				// Current input character
		p.i = 0;						// Iteration position

		if (_processList == undefined) startEngine();
	
		_processList.push(p);

		return _processList.length-1;
	}

	/**
	 * Does one iteration of the compressing process
	 *
	 * @param		p_obj		Object		Process object, with information about the process
	 */
	private static function iterateCompressProcess(p:Object, p_iterations:Number): Void {
		if (isNaN(p_iterations)) p_iterations = 1000; // Number of chars to compress at a time
		if (p_iterations == 0) p_iterations = p.l+1; // Full length
		for (var i:Number = 0; i < p_iterations; i++) {
			if (p.i > p.l) break;
			p.ic = p.s[p.i];
			if (!p.t[p.is+p.ic]) {
				// Not on the table, adds
				p.ns += String.fromCharCode(p.t[p.is]);
				p.t[p.is+p.ic] = p.tl++;
				p.is = p.ic;
			} else {
				// Already on the list
				p.is += p.ic;
			}
			p.i++;
		}
		if (p.i > p.l) endCompressProcess(p);
	}

	/**
	 * Ends one iteration of the compressing process
	 *
	 * @param		p_obj		Object		Process object, with information about the process
	 */
	private static function endCompressProcess(p:Object): Void {
		p.isFinished = true;
		p.s = undefined;
	}

	/**
	 * Returns a value between 0 and 1 indicating how much of the compression has been completed
	 *
	 * @param		p_id		Number		ID of the string process
	 * @return					Number		A number between 0 and 1
	 */
	public static function getSlowCompressFactor(p_id:Number): Number {
		var cp:Object = _processList[p_id];
		if (!cp) return null;
		return cp.i / (cp.l+1);
	}
	
	/**
	 * Returns the compressed string
	 *
	 * @param		p_id		Number		ID of the string process
	 * @return					String		The compressed string
	 */
	public static function getSlowCompressString(p_id:Number): String {
		var cp:Object = _processList[p_id];
		if (!cp || !cp.isFinished) return null;
		return cp.ns;
	}


	// ==================================================================================================================================
	// FAST DECOMPRESSION function ------------------------------------------------------------------------------------------------------

	/**
	 * Quick decompression function, to do everything in one shot
	 *
	 * @param		p_str		String		String to be decompressed
	 * @return					Object		Decompressed string
	 */
	public static function decompress (p_str:String): String {
		/*
		// Quick version, that works just the same but doesn't reuse code:

		var ns:String = "";				// New String
		var s:Array = p_str.split("");	// Original text split on an array
		var t:Array = new Array();		// Table for compression codes
		var l:Number = s.length;		// Array length for faster looping
		var nc:Number;					// New code
		var is:String;					// Input string
		var b:String = "";				// Buffer
		var c:String = "";				// Chain of chars for the table

		var i:Number;

		// The table holds chars this time
		for (i = 0; i < 256; i++) t[i] = String.fromCharCode(i);
		var tl:Number = 256;			// Current table length

		for (i = 0; i < l; i++) {
			nc = s[i].charCodeAt(0); // p_str.charCodeAt(i);
			is = t[nc];
			if (b == "") {
				b = is;
				ns += is;
			} else {
				if (nc < 256) {
					ns += is;
					t[tl++] = b + is;
					b = is;
				} else {
					if (!t[nc]) {
						c = b + b.substr(0, 1);
					} else {
						c = t[nc];
					}
					ns += c;
					t[tl++] = b + c.substr(0, 1);
					b = c;
				}
			}
		}
		
		return (ns);
		*/

		var pid:Number = createDecompressProcess(p_str);
		iterateDecompressProcess(_processList[pid], 0);
		var decompressedData:String = getSlowDecompressString(pid);
		dispose(pid);
		return decompressedData;
	}


	// ==================================================================================================================================
	// SLOW DECOMPRESSION functions -----------------------------------------------------------------------------------------------------

	/**
	 * Start decompressing a string -- should be used on large strings
	 *
	 * @param		p_str		String		String to be decompressed
	 * @return					Number		ID (numeric index) of the string process
	 */
	public static function slowDecompress (p_str:String):Number {
		return createDecompressProcess(p_str);
	}

	/**
	 * Create a new string decompression process
	 *
	 * @param		p_str		String		String to be decompressed
	 * @return					Number		ID (numeric index) of the string process
	 */
	private static function createDecompressProcess(p_str:String): Number {
		var p = {};
		p.method = "decompress";		// Method ("compress", "decompress")
		p.ns = "";						// New String
		p.isFinished = false;			// Whether or not has finished
		p.s = p_str.split("");			// Original text split on an array
		p.t = new Array();				// Table for compression codes
		for (var i:Number = 0; i < 256; i++) p.t[i] = String.fromCharCode(i);
		p.tl = 256;						// Current table length
		p.l = p.s.length;				// Array length for faster looping
		p.nc = undefined;				// New code
		p.is = "";						// Current input string
		p.b = "";						// Buffer
		p.c = "";						// Chain of chars for the table
		p.i = 0;						// Iteration position

		if (_processList == undefined) startEngine();
	
		_processList.push(p);

		return _processList.length-1;
	}

	/**
	 * Does one iteration of the decompressing process
	 *
	 * @param		p_obj		Object		Process object, with information about the process
	 */
	private static function iterateDecompressProcess(p:Object, p_iterations:Number): Void {
		if (isNaN(p_iterations)) p_iterations = 1000; // Number of chars to decompress at a time
		if (p_iterations == 0) p_iterations = p.l; // Full length
		for (var i:Number = 0; i < p_iterations; i++) {
			if (p.i >= p.l) break;
			p.nc = p.s[p.i].charCodeAt(0); // p_str.charCodeAt(i);
			p.is = p.t[p.nc];
			if (p.b == "") {
				p.b = p.is;
				p.ns += p.is;
			} else {
				if (p.nc < 256) {
					p.ns += p.is;
					p.t[p.tl++] = p.b + p.is;
					p.b = p.is;
				} else {
					if (!p.t[p.nc]) {
						p.c = p.b + p.b.substr(0, 1);
					} else {
						p.c = p.t[p.nc];
					}
					p.ns += p.c;
					p.t[p.tl++] = p.b + p.c.substr(0, 1);
					p.b = p.c;
				}
			}
			p.i++;
		}

		if (p.i >= p.l) endDecompressProcess(p);
	}

	/**
	 * Ends one iteration of the decompressing process
	 *
	 * @param		p_obj		Object		Process object, with information about the process
	 */
	private static function endDecompressProcess(p:Object): Void {
		p.isFinished = true;
		p.s = undefined;
	}

	/**
	 * Returns a value between 0 and 1 indicating how much of the compression has been completed
	 *
	 * @param		p_id		Number		ID of the string process
	 * @return					Number		A number between 0 and 1
	 */
	public static function getSlowDecompressFactor(p_id:Number): Number {
		var cp:Object = _processList[p_id];
		if (!cp) return null;
		return cp.i / cp.l;
	}
	
	/**
	 * Returns the decompressed string
	 *
	 * @param		p_id		Number		ID of the string process
	 * @return					String		The decompressed string
	 */
	public static function getSlowDecompressString(p_id:Number): String {
		var cp:Object = _processList[p_id];
		if (!cp || !cp.isFinished) return null;
		return cp.ns;
	}


	// ==================================================================================================================================
	// ENGINE functions -----------------------------------------------------------------------------------------------------------------

	/**
	 * Starts the compression engine for slow compression/decompression
	 */
	private static function startEngine(): Void {
		_processList = new Array();
		_root.createEmptyMovieClip("__LZW_compression__", 1762138);
		_root.__LZW_compression__.onEnterFrame = function() {
			zeh.compression.LZW.tick();
		}
	}

	/**
	 * Stops the compression engine
	 */
	private static function stopEngine(): Void {
		_processList = undefined;
		_root.__LZW_compression__.onEnterFrame = undefined;
		_root.__LZW_compression__.removeMovieClip();
	}

	/**
	 * Disposes of a string data; not stricly needed, but helpful so big strings are deleted
	 *
	 * @param		p_id		Number		ID of the string process
	 */
	public static function dispose(p_id:Number): Void {
		for (var i in _processList[p_id]) _processList[p_id][i] = undefined;
		_processList[p_id] = null;
	}

	/**
	 * One iteration of the compression/decompression engine
	 */
	public static function tick(): Void {
		var hasValidProcesses:Boolean = false;
		for (var i:Number = 0; i < _processList.length; i++) {
			if (_processList[i] != undefined) {
				hasValidProcesses = true;
				if (!_processList[i].isFinished) {
					if (_processList[i].method == "compress") {
						iterateCompressProcess(_processList[i]);
					} else if (_processList[i].method == "decompress") {
						iterateDecompressProcess(_processList[i]);
					}
				}
			}
		}
		if (!hasValidProcesses) stopEngine();
	}

}