/*
	$Id: transport.js 70 2006-12-12 19:42:11Z oleg $
	$Rev: 70 $
	$Author: oleg $
	$Date: 2006-12-12 22:42:11 +0300 $
*/
/*

TODO
		* Start & Finalize handlers
		* Allow to call active handlers while state!=4? No, use handlers.interaction instead.

		* buildQuery - more to do
		
		* More complex cookies merging on transparent session		
		* Escaping cookie values & keys
		* Transparent mode doesn't work in IE55
		* Rich cookie format support (based on RFC2109, RFC2965)

		* More complex escaping for query and components?
		* Determine encoding routine by the Resource type
		
		* Default (overrideable) settings for access methods

		* multipart posts
		* using function as header content: for defaults, for user defined, for cyclic execution

		* discardDefaults : no cookies grabbing, no default handlers, no other bahaviours
		
		* overrideMimeType support

		* Avoid memory leaks


API
		* handle ([event, ]handler)

		* constructor (url [, headers, caching?, transparent?, escaping?])
		* say (method[, source, headers, caching?])
		* reset (void)

SNIPPETS
		Headers packing:
		
			Function evaluated first
			Null value removes header property
			Content-type overrides all installed Mime
			Charset or Type are only update associated property
			Cookie: if defined, overwrite harvested; don't send if null; else send harvested on 'transparent'


		Attaching handlers:

			text		:= xhr.responseText
			headers 	:= (object) response headers
			code		:= (int) http connection status code
			watcher		:= (function) < code
			handler		:= (function) < text, headers, code, this < xhr

			handle (handler) // success
			handle (event, handler)
			handle ({
				event: handler,
				...
			})

	*/


var IO = function (Url, Headers, Caching, Transparent) {	// < //////////////////////////////////////////////////////

	/* library version */

	function ver () {
		return ('$Rev: 70 $').match(/(\d+)/)[0];
	}


	/* returns xhr object */

	function getTransport () {
		if (window.XMLHttpRequest)
			return new XMLHttpRequest();
		else
			if (this.ver)
				return new ActiveXObject(this.ver);
		else
			if (window.ActiveXObject) {
				var avail = [
					'Msxml2.XMLHTTP.5.0',
					'Msxml2.XMLHTTP.4.0',
					'MSXML2.XMLHTTP.3.0',
					'MSXML2.XMLHTTP',
					'Microsoft.XMLHTTP'
				];
				for (var i = 0; i < avail.length; i++)
					try {
						this.ver = avail[i];
						return new ActiveXObject(this.ver);
					}
					catch (e) {}

				this.ver = null;
			}
		throw new Error('XMLHttp object not available');
	}


	/* simple randomizer */

	function rand () {
		return (new Date).valueOf()+Math.random();
	}


	/* discard default headers for the instance */

   	function discardDefaults () {
   		default_headers = {};

		content_type.type = null;
  		content_type.options = {};

		return;
   	}


	/* matches if a given string is valid mimetype definition */

	function isMimetype (S) {
		return S.match(/^(text|image|audio|video|application|multipart|message|(x-\w([.+-]?\w)*))\/\w([.+-]?\w)*$/gi);
	}


	/* return current date in RFC1123 format */

	function now () {
		var D = new Date();
		return (
			new Date(
				Date.parse( D.toUTCString() ) - 60000 * D.getTimezoneOffset()
			)
		).toUTCString();
	}


	/* deep copy routine for objects */

	function copyObject (A) {
		if (null == A || 'object' != typeof A)
			return A;

		var res = A instanceof Array ? [] : {};

		for (var i in A)
			res[i] = 'object' == typeof A[i] ? copyObject(A[i]) : A[i];

		return res;
	}


	/* wrapper for data std escaping */

	function componentEncode (S) {
	    var a = S.split(' ');
    	for (var i = 0; i < a.length; i++)
	    	a[i] = encodeURIComponent(a[i]);
	
		return a.join('+');
	}
	
	
	/* wrapper for data std unescaping */

	function componentDecode (S) {
		return decodeURIComponent(S).split('+').join(' ');
	}


	/* concat url & query string in case of get (nb: cut off fragment) */

	function appendQuery (Path, query) {
		var path;
		return (path = Path.split('#')[0]) + (-1 == path.indexOf('?') ? '?' : path.match(/[?&]$/g) ? '' : '&') + query;
	}


	/* serialize data */

    function buildQuery (Input) {
        var r = new Array();

		var O = Input instanceof Function ? Input() : Input;

		if (O instanceof Array)
			r = O;

		else if (null === O)
			return null;

		else if ('object' != typeof O)
			r.push(componentEncode(O));
		
		else for (var prop in O)
			r.push(componentEncode(prop) + '=' + componentEncode(O[prop]));

        return r.join('&');
    }

	
	/* expands target Key with alias */

	function expandName (Key) {
		var key = Key.toLowerCase();
		return aliases[key] ? aliases[key] : key;
	}


	/* serialize various types of header content to string *//* i remember something strange about it */

	function expandContent (V, active) {
		var t = typeof V;

		if (null == V)
			return V;

  		if ('function' == t && ('undefined' == typeof active || active))
  			return V();

		if (V instanceof Array)
			return V.join(', ');

		if ('object' != t)
			return V;

		var a = [];
		for (var i in V)
			a.push(i + '=' + V[i]);

		return a.join('; ');
	}


	/* pack Content-Type object to the string */

	function packContentType (O) {
		var content = expandContent(O.options);
		return O.type + (content.length ? '; '+content : '');
	}


	/* enscaping *//* unpack string to the Content-Type object */

	function unpackContentType (S) {
		var mime = {type: null, options: {}};

		if ('string' != typeof S)
			return mime;

		var options = S.replace(/([^\\]);/g, '$1\n').split(/\s*\n\s*/g);

		if (!options)
			return mime;

		var option, pair;

		while ((option = options.shift())) {
			pair = option.match(/^([\w+-.]*)=(.*)$/i);

			if (pair)
	 			mime.options[ pair[1].toLowerCase() ] = pair[2];
			else
				if (isMimetype(option))
					mime.type = option;
		}
		return mime;
	}


	/* enscaping *//* forms an object from cookie string */

	function cookieObject (S) {
		var option, pair, key, value, alone;
		var cookie = {
			name	 :null,
			value	 :null,
			expires	 :null,
			domain	 :null,
			path	 :null,
			httponly :false,
			secure	 :false
		};

		var options = S.replace(/,(\s|\t)*$/g, '').split(/\s*;\s*/g);

		while ((option = options.shift()))

			if (option.length) {
				pair = option.match(/^([^=]*)(?:=(.*))?$/);

				if (pair) {
					key	  = pair[1].toLowerCase();
					alone = 'undefined' == typeof pair[2];

					if ('undefined' != typeof cookie[key]) {
						cookie[key]  = alone ? true : pair[2];
					}
					else if (!alone) {
						cookie.name  = componentDecode(pair[1]);
						cookie.value = componentDecode(pair[2]);
					}
				}
			}

		return cookie;
	}


	/* packs cookie-object to the string */

	function packCookie (O) {
		var cookie = [];

		for (var i in O) {
			if ('name' == i || 'value' == i || null === O[i] || false === O[i])
				continue;

			cookie.push(i + (true !== typeof O[i] ? '=' + O[i] : ''));
		}

		cookie.unshift( componentEncode(O.name) + '=' + componentEncode(O.value) );
		return cookie.join('; ');
	}


	/* parse string to an array of cookie-objects */

	function unpackCookie (S) {
		var cookies = [];

		if ('string' != typeof S)
			return cookies;

		var matches = S.match(/([^\s=]+)(?:=)/g);

		if (!matches)
			return cookies;

		var prev, name;
		var known = ' httponly=, domain=, expires=, path=, secure=';

		while ((name = matches.shift()))

	 		if (-1 == known.indexOf(name.toLowerCase())) {
 				if (prev)
 					cookies.push( cookieObject(S.substring(S.indexOf(prev), S.indexOf(name))));

 				if (!matches.length)
 					cookies.push( cookieObject(S.substring(S.indexOf(name))) );
	 			else
 					prev = name;
			}

		return cookies;
	}


	/* build hash from the source object */

	function hashHeaders (O) {
		var hash = {};
		var A = O instanceof Array;

		if (A || 'object' == typeof O) {
			var P;
			for (var i in O)
				if (A) {
					if ('string' == typeof O[i] && (P = O[i].match(/^((?:[!-]?\w[.-]?)*\w)\s*:\s*(.*)$/i)) )
						hash[ P[1] ] = P[2];
				}
				else if (i.match(/^[!-]?\w([.+-]?\w)*\s*$/i))
					hash[i] = O[i];
		}
		return hash;
	}


	/* mounts input (headers) to the objects (container for common entities & mime for mimetype related) */

	function mountHeaders (Input, Container, Mime, Active) {

		var pair, name, value;
		var active = 'undefined' == typeof Active || Active;

		var O = Input instanceof Function ? Input() : Input;

		if ('string' == typeof O) {

			if (isMimetype(O))
				Mime.type = O;
			else
				Mime.options.charset = O;
		}
		else if (O instanceof Object) {

			var hash = hashHeaders(O);
			for (var i in hash) {

				value	= expandContent(hash[i], active);
				name	= i.toLowerCase();

				if ('charset' == name)
					Mime.options.charset = value;

				else if ('type' == name) {
					if (!active || isMimetype(value)) {
						Mime.type = value;
					}
				}
				else if ('content-type' == expandName(i))
					Mime = active ? unpackContentType(value) : value;

				else
					Container[ expandName(i) ] = value;
			}
		}
	}


	/* todo: rename *//* parses raw header's data to the object */

	function unpackHeaders (S) {

		var header, pair, name, flag, cname = 'set-cookie';
		var headers = S.split(/[\r\n]+/g);

		var H = {}, C = [];

		while ((header = headers.shift())) {

			pair = header.match(/^([\w._-]+):\s*(.*)$/);

			if (pair) {
				name = pair[1].toLowerCase();
				flag = cname == name;

				if (!flag)
					H[name] = pair[2];
			}

			if (flag)
				C.push(pair ? pair[2] : header);
		}

		if (C.length) {
		    var i, c = [], u = unpackCookie(C.join(', '));

		    while ((i = u.shift()))
		    	c.push( packCookie(i) )

			H[cname] = c.join(',\n');
		}
		return H;
	}


	/* merge with defaults & packs user input to the headers object. */

	function packHeaders (Input, Active) {

		var headers = copyObject(default_headers);
		var mime	= copyObject(content_type);
		var o		= {};

		mountHeaders(Input, headers, mime, Active);

		if (mime.type)
			headers['content-type'] = packContentType(mime)

		for (var i in headers)
			o[i] = expandContent(headers[i], Active);

		return o;
	}

	
	/* attaches a handler to the event/status code */

	function handle (A, B) {
		var code, a = typeof A;

		if ('function' == typeof B) {

			if ('function' == a)
				custom_handlers.push({watcher: A, handler: B})

			else if ('number' == typeof (code = 1*A) && code < 599 && code > 100 && code == Math.floor(code))
				handlers.custom[code] = B;

			else if ('string' == a && 'undefined' != handler[A])
				handlers[A] = B;

			else throw new Error('Invalid custom response code indentifier/handler');

		}
		else if ('function' == a)
			handlers.success = A;

		else if ('object' == a)
			for (var i in A)
				handle(i, A[i]);

		else throw new Error('Invalid handler');
	}


	/* apply handler to the connection instance */

	function callHandler (Handler, O, Headers) {
		Handler.apply(O, [O.responseText, Headers, O.status]);
	}


	/*****************************************/





	function say (Method, Resource, Headers) {

		if ('string' != typeof Method)
			throw new TypeError('Invalid connection method');

		var method	= Method.toLowerCase();
		var conn	= getTransport();

		function onStateChange () {
			if (3 == conn.readyState && handlers.interaction)
				callHandler(handlers.interaction, conn);

			else if (4 == conn.readyState) {

			    var headers = unpackHeaders( conn.getAllResponseHeaders() );

			    
			    /* check if we're in transparent mode and setup retrieved cookies if true */

				if (settings.transparent && headers['set-cookie']) {
				    var cookies = headers['set-cookie'].split(',\n');

					for (var i = 0; i < cookies.length; i++)
						document.cookie = cookies[i];
				}

				
				/* handle connection instance */

				if (null != conn.status && handlers.custom[conn.status])
					callHandler(handlers.custom[conn.status], conn, headers);

				else if (custom_handlers.length)
					for (var i = 0; i < custom_handlers.length; i++ ) {
					    var custom = custom_handlers[i];
						if (custom.watcher(conn.status)) {
							callHandler(custom.handler, conn, headers);
							break;
						}
					}
				else if (conn.status > 399 && handlers.failure)
					callHandler(handlers.failure, conn, headers);

				else if (handlers.success)
					callHandler(handlers.success, conn, headers);

			}
		}


		var headers = copyObject(default_headers);
		var mime	= copyObject(content_type);
		var data	= buildQuery(Resource);
		var url		= settings.caching ? settings.url : appendQuery(settings.url, 'spar.io.seed='+rand());

		mountHeaders(Headers, headers, mime, true);

		/*
			 mime hacks
		*/



   		if ('post' == method) {
   			if (null === data)
   				data = '';

   			mime.type = 'application/x-www-form-urlencoded';
   			headers['content-length'] = data.length;
   		}
   		else if ('get' == method) {
   			url = appendQuery(url, data)
   			data = null
   		}

		
		if (mime.type) {
			headers['content-type'] = packContentType(mime)
		}

		
		
		headers['X-Transport'] = 'spar.io.transport '+ver();

		if (!settings.caching)
			headers['if-modified-since'] = 'Mon, 04 May 1996 12:17:34 GMT';

		if (mime.options.charset)
			headers['accept-charset'] = mime.options.charset;

		if (conn.overrideMimeType)
			headers.connection = 'close';
		
		conn.open(method, url, settings.async);

		conn.onreadystatechange = onStateChange;

		
		for (var i in headers) {
			conn.setRequestHeader(i, expandContent(headers[i], true));
		}

		conn.send(data);
	}





	/* aliases for shortened or partial key-naming */

	var aliases = {
		agent		: 'user-agent',
		type		: 'content-type'
	};


	/* common headers */

	var default_headers = {
		'referer'	: document.location.href,
		'date'		: now,
		'accept'	: 'text/json, text/javascript, text/html, application/xml, text/xml, */*'
	};


  	/* default mime type */

  	var content_type = {
  		type		: 'text/plain',
  		options 	: {
  			charset : 'windows-1251'
  		}
  	};

	
	/* handlers for connection state */

	var handlers = {
		interaction : null,
		success		: null,
		failure		: null,
		custom 		: []
	};


	/* custom status code flexible handlers */

	var custom_handlers = [];


	/* CONSTRUCTOR */
	
	if ('string' != typeof Url)
		throw new TypeError('url is not a string');

	
	/* connection instance setting */

	var settings = {
		url			: Url,
		async		: true,
		user		: null,
		password	: null,
		transparent	: Transparent,
		caching		: Caching
	};



	mountHeaders(Headers, default_headers, content_type);


	/* public interface */

	this.ver = ver;
	this.discardDefaults = discardDefaults;

	this.handle = handle;
	this.say	= say;
	this.handleSuccess = handle;
}

