/*
 * Copyright (c)  2006-2007 Maskat Project.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/**
 * XMLHttpRequest のプーリング機構を持つ通信マネージャです。
 *
 * 送信キューにコンテキスト (maskat.comm.HttpContext のインスタンス) を追加
 * することにより通信がスケジューリングされ、通信処理の各段階でコンテキスト
 * のコールバック関数が実行されます。
 */
maskat.lang.Class.declare("maskat.comm.CommunicationManager", {
	
	_static: {
		/**
		 * このクラスの唯一のインスタンスを返します。(Singleton パターン)
		 *
		 * @return このクラスの唯一のインスタンス
		 */
		getInstance: function(param) {
			var self = arguments.callee;
			if (!self.instance) {
				var sendInterval = maskat.app.getProperty("maskat.comm.sendInterval");
				var timeoutInterval = maskat.app.getProperty("maskat.comm.timeoutInterval");
				var poolSize = maskat.app.getProperty("maskat.comm.poolSize");				
				self.instance = new this(sendInterval, timeoutInterval, poolSize);
			}
			return self.instance;
		}
	},
	
	/**
	 * コンストラクタ
	 *
	 * @param sendInterval 送信キューを処理する間隔 (ms)
	 * @param timeoutInterval HTTP 応答待ちのタイムアウトを確認する間隔 (ms)
	 * @param poolSize XMLHttpRequest のプールサイズ
	 */
	initialize: function(sendInterval, timeoutInterval, poolSize){
		this.queue = [];
		this.waitings = [];
		this.completed = [];
		this.locked = false;

		/* HTTP 要求送信スレッドの生成 */		
		this.senderThread = new maskat.lang.Thread(
			this,
			this.processRequest,
			null,
			sendInterval || 100);

		/* タイムアウト確認スレッドの生成 */		
		this.timeoutThread = new maskat.lang.Thread(
			this,
			this.checkTimeout,
			null,
			timeoutInterval || 500);

		/* XMLHttpRequest プールの生成 */		
		this.initPool(poolSize || 2);
	},
	
	/**
	 * XMLHttpRequest をプールから取得します。
	 * プールがすべて利用されている場合には新しいインスタンスを生成します。
	 *
	 * @return XMLHttpRequest のインスタンス
	 */
	getXMLHttpRequest: function() {
		if (this.pool.length) {
			return this.pool.shift();
		}
		return maskat.util.CrossBrowser.createXMLHttpRequest();
	},

	/**
	 * XMLHttpRequest のプールをパラメータで指定されたサイズで初期化します。
	 */
	initPool: function(size) {
		this.poolSize = size;
		this.pool = [];
		for (var i = 0; i < size; i++) {
			this.pool.push(maskat.util.CrossBrowser.createXMLHttpRequest());
		}
	},
	
	/**
	 * XMLHttpRequest のプールサイズをパラメータで指定されているサイズまで
	 * 縮小します。
	 */
	shrinkPool: function() {
		for (var i = this.poolSize; i < this.pool.length; i++) {
			delete this.pool[i];
		}
		this.pool.length = this.poolSize;
	},
	
	/**
	 * HTTP コンテキストを送信キューに追加し、通信をスケジュールします。
	 *
	 * @param context 送信コンテキスト
	 */
	enqueue: function(context){
		this.queue.push(context);
		try {
			context.setStatus(maskat.comm.HttpContext.READY);
		} catch (e) {
			this.queue.shift();
			throw e;
		}
		/*
		 * 送信キューが空の場合は送信スレッドは停止状態しているため、
		 * スレッドを開始する
		 */
		if (this.queue.length == 1) {
			this.senderThread.start();
		}
	},

	/**
	 * 送信待ちキューからコンテキストを取得し、HTTP 要求を実行します。
	 *
	 * このメソッドは HTTP 要求送信スレッドから、パラメータで指定された
	 * インターバルで起動されます。
	 */
	processRequest: function() {
		/* 他のコンテキストが同期通信中の場合はロック開放まで待機 */
		if (this.locked) {
			return;
		}

		/* 同期通信の場合はロックを取得 */
		var context = this.queue.shift();
		this.locked = !context.async;

		/* 送信キューが空になった場合、送信スレッドを停止する */
		if (!this.queue.length) {
			this.senderThread.stop();
		}

		/* XMLHttpRequest にパラメータとコールバック関数を設定 */
		try {
			var xhr = this.getXMLHttpRequest();
			var self = this;
			xhr.open(context.method, context.url, context.async);
			xhr.onreadystatechange = function(){
				self.processResponse(context);
			};
		} catch (e) {
			this.finishProcess(context, e);
			return;
		}
		
		/*
		 * リクエストヘッダの設定:
		 * 
		 * XHR をプールから再利用した場合でも、open() メソッドを実行すると
		 * 以前に設定されていたリクエストヘッダはクリアされる
		 */
		var headers = context.requestHeaders;
		if (headers) {
			for (var n in headers) {
				xhr.setRequestHeader(n, headers[n]);
			}
		}

		/*
		 * 応答待ちリストが空の場合、タイムアウト監視スレッドは停止状態なので
		 * スレッドを開始する
		 */
		if (!this.waitings.length) {
			this.timeoutThread.start();
		}

		/* HTTP 要求を送信し、コンテキスト応答待ちリストに追加する */
		this.waitings.push(context);
		context.xhr = xhr;
		context.sentAt = (new Date()).getTime();
		
		try {
			context.setStatus(maskat.comm.HttpContext.WAIT);
		} catch (e) {
			/*
			 * openされたXHRはsend完了もしくはabortされるまで占有されます。
			 * FireFoxではopen後のabortで発生するreadyStateはcomplete(4)
			 */
			try {
				context.xhr.onreadystatechange = null;
			} catch (ex) {
				context.xhr.onreadystatechange = function() {};
			}
			context.xhr.abort();
			this.finishProcess(context, e);
			return;
		}
		xhr.send(context.requestMessage);

		/*
		 * Firefox の同期通信は送信処理が終わるまで send 関数から復帰せず、
		 * onreadystatechange 関数を呼び出さない
		 */
		if (navigator.appName == "Netscape" && !context.async) {
			if (context.status == maskat.comm.HttpContext.WAIT) {
				this.processResponse(context);
			}
		}
	},
	
	/**
	 * レスポンス処理
	 * サーバとのレスポンス処理を行うためのコールバック関数
	 *
	 * @param xhr XMLHttpRequest
	 * @param context 送信コンテキスト
	 */
	processResponse: function(context) {
		var xhr = context.xhr;
		/*
		 * FireBugを利用している場合、処理済リクエストのonreadystatechange
		 * が再度呼び出される。context.xhrは既に削除されており存在しない。
		 */
		if (!xhr || context.status != maskat.comm.HttpContext.WAIT) {
			return;
		}
		try {
			if (xhr.readyState == 4) {
				if (xhr.status == 200) {
					maskat.util.CrossBrowser.checkParseError(xhr);

					context.responseMessage = xhr.responseText;
					context.responseXML = xhr.responseXML;
					context.responseHeaders = this.parseResponseHeaders(
						xhr.getAllResponseHeaders());
					
					/*
					 * ロック中 (同期通信中) に非同期処理が完了した場合には
					 * 同期通信の完了を待つ
					 */
					if (context.async && this.locked) {
						this.completed.push(context);
					} else {
						context.setStatus(maskat.comm.HttpContext.COMPLETE);
					}
				} else if (xhr.status != 0) {
					maskat.util.CrossBrowser.checkHttpError(xhr);
				}
				this.finishProcess(context);
			}
		} catch (e) {
			var error = e;
			if (!(e instanceof maskat.lang.Error)) {
				error = maskat.util.CrossBrowser.getHttpError(xhr);
				if (!error) {
					error = e;
				}
			}
			this.finishProcess(context, error);
		}
	},
	
	/**
	 * レスポンスヘッダ文字列を解析し、ヘッダ名と値のペアを要素とする配列を
	 * 返します。
	 *
	 * @param headers すべてのレスポンスヘッダを含んだ文字列
	 *     "name1:value1\nname2:value2\n..."
	 *
	 * @return レスポンスヘッダの解析済み配列
	 *     {
	 *         name1: "value1",
	 *         name2: "value2",
	 *         ...
	 *     }
	 */
	parseResponseHeaders: function(headers) {
		if (!headers) {
			return null;
		}

		var result = {};
		var lines = headers.split(/\r?\n/);
		var pair;
		for (var i = 0; i < lines.length; i++) {
			if (lines[i].length > 1) {
				/* レスポンスヘッダ行を名前／値ペアに分割する */
				pair = lines[i].split(/\s*:\s*/);
				if (result[pair[0]]) {
					result[pair[0]] = result[pair[0]] + " " +
						pair[1].replace(/, /g, " ");
				} else {
					result[pair[0]] = pair[1].replace(/, /g, " ");
				}
			}
		}
		return result;
	},
	
	/**
	 * 送信プロセス終了処理
	 *
	 * @param context 通信が終了したコンテキスト
	 * @param error 発生したエラー（正常時はundefined)
	 */
	finishProcess: function(context, error) {
		/*
		 * エラー発生によるfinishProcess呼び出し
		 * 処理中断 (InterruptedError)による呼び出しはステータス変更を行わない
		 */
		if (error && !(error instanceof maskat.lang.InterruptedError)) {
			context.error = error;
			try {
				context.setStatus(maskat.comm.HttpContext.ERROR);
			} catch (ex) {
				context.error.cause = ex;
			}
		}
		/* XMLHttpRequest をプールに戻す */
		if (context.xhr) {
			this.pool.push(context.xhr);
			delete context.xhr;
		}
		/* 応答待ちリストからコンテキストを削除 */
		for (var i = 0; i < this.waitings.length; i++) {
			if (context === this.waitings[i]) {
				this.waitings.splice(i, 1);
				break;
			}
		}
		
		/* 応答待ちリストが空の場合、タイムアウト監視スレッドを停止 */
		if (!this.waitings.length) {
			this.waitings.length = 0;
			this.timeoutThread.stop();
			this.shrinkPool();
		}

		/*
		 * 同期通信が終了した場合、ロックフラグの解除を待っている非同期通信の
		 * 結果をリリースする
		 */
		if (!context.async) {
			for (var i = 0; i < this.completed.length; i++) {
				try {
					this.completed[i].setStatus(maskat.comm.HttpContext.COMPLETE);
				} catch (e) {
					this.completed[i].error = e;
					try {
						this.completed[i].setStatus(maskat.comm.HttpContext.ERROR);
					} catch (ex) {
						this.completed[i].error.cause = ex;
					}
				}
			}
			this.completed.length = 0;
			this.locked = false;
		}
	},
	
	/**
	 * HTTP 応答待ちのキューを走査し、タイムアウト時間が経過している場合に
	 * タイムアウト処理を発生させます。
	 *
	 * このメソッドはタイムアウト処理スレッドから、パラメータで指定された
	 * インターバルで起動されます。
	 */
	checkTimeout: function() {
		var context;
		for (var i = 0; i < this.waitings.length; i++) {
			context = this.waitings[i];
			if (context.isTimeout()) {
				try {
					context.setStatus(maskat.comm.HttpContext.TIMEOUT);
				} catch (e) {
					context.error = e;
				}
				/*
				 * abortを実行するとonreadystatechange関数をIEはstatus=0で呼び出します。
				 * FireFoxではNS_ERROR_NOT_AVAILABLEエラーとして呼び出されるのでonready
				 * statechange関数を無効にしてabortを呼んでいます。
				 */
				try {
					context.xhr.onreadystatechange = null;
				} catch (ex) {
					context.xhr.onreadystatechange = function() {};
				}
				context.xhr.abort();
				this.finishProcess(context);
			}
		}
	}
});

