//<meta http-equiv="Content-Type" content="text/html; charset=windows-1251">

/*	xloader, v1.0.001 (c) Дм.Григорьев, 2004-11-13.
	Объект xloader управляет подгрузкой HTML через скрытые iframe. Может параллельно использовать произвольное количество frame/iframe.
	Одним запросом можно синхронно загрузить несколько файлов (подзапросов) - например, как разные столбцы дерева ответов в RSDN.
	Загружаемый файл должен вызвать в конце загрузки: parent.xloader.frame_loaded(url, content). (Если объект xloader расположен не в 
	родительском по отношению к фрейму документе, вместо parent должен быть указан правильный путь к объекту xloader).
	Интерфейсные методы принимают параметрами ссылки на объекты вместо id, чтобы не привязываться к какому-либо фрейму.
	FRAME НЕ ОТЛАЖЕНО, ТОЛЬКО IFRAME!!! МОДУЛЬ НЕ ИСПОЛЬЗУЕТ ВНЕШНИЙ КОД (правда, для этого два стандартных array-метода реализованы внутри). */

xloader = {
// private 

	requests: new Array(),		// запросы {id:string/int/null, subrequests:[object], counter:int, callback:function(request,code)/null, tag:any}
	subrequests: new Array(),	// подзапросы {request:object, index:int, page:object, target:element/null, tag:any}
	pages: new Array(),			// страницы {url:string, content:string/null, subrequests:[object], frame:object/null}
	frames: new Array(),		// фреймы {frame:element, page:object/null}

// public

	base_url: null,				// база для преобразований относительных URL (просто префикс). ОБЯЗАТЕЛЬНО ЕСЛИ В ЗАПРОСАХ ЕСТЬ ОТНОСИТЕЛЬНЫЕ URL.
	debug_callback: debug_xloader,		// ссылка на функцию с прототипом debug(module_name, msg, severity); module_name: "xloader"; severity: 0=info, 1=warning, 2=error
	debug_severity: 0,			// серьезность выводимых сообщений (0=все, 1=warnings & errors, 2=errors).

	// Регистрация frame/iframe для использования объектом xloader. При ошибке возвращает false; при повторной регистрации возвращает true.
	register_frame: function(e) {
		if (!e && (e.tagName != 'FRAME') && (e.tagName != 'IFRAME')) {
			this.debug("register_frame(): bad param", 2);
			return false;
		}
		for (var i = 0;  i < this.frames.length;  i++)
			if (this.frames[i].frame == e) {
				this.debug("register_frame(): already registered", 1);
				return true;
			}
		this.frames[this.frames.length] = {frame:e, page:null};   // IE5.5 не держит Array.push().
		this.schedule();
		this.debug("register_frame(): success");
		return true;
	},

	// Запуск нового запроса на загрузку. Формат запроса: 
	// param = {id:string/int/null, subrequests: [{url:string, target:element/null, tag:any}], callback:function(request,code)/null, tag:any};
	// Функция callback вызывается в следующих случаях:
	// - code=0 - ошибка или отмена запроса (void)
	// - code=1 - все данные загружены в кеш, загружать в target? (boolean)
	// - code=2 - все данные загружены в target (void)
	// Если callback не задан, после загрузки всех данных в кеш выполняется безусловная одновременная загрузка в target. 
	// Если в подзапросе target=null, задача обработки загруженных данных полностью ложится на callback. 
	// ВНИМАНИЕ!!! Если все запрошенные страницы уже лежат в кеше, запрос отдается немедленно, возможна рекурсия! Это происходит перед самым 
	// возвратом из метода, поэтому глюков от рекурсии внутри данного модуля не будет, но возможны глюки в пользовательских скриптах.
	// ВНИМАНИЕ!!! Относительные URL приводятся к абсолютным, с использованием заданной базы.
	new_file_request: function(request) {
		var i, r, r0 = request;
		// Проверяем корректность запроса (кроме банальных ошибок несоответствия типов).
		this.debug("<hr>");
		if (!r0.subrequests.length) {
			this.debug("new_request(): empty request", 2);
			return false;
		}
		if (r0.id) {
			for (i = 0;  i < this.requests.length;  i++)
				if (this.requests[i].id == r0.id) {
					this.debug("new_request(): duplicate request id", 2);
					return false;
				}
		}
		for (i = 0;  i < r0.subrequests.length;  i++) {
			var t = r0.subrequests[i].target;
			for (var j = 0;  t && (j < this.subrequests.length);  j++)
				if (this.subrequests[j].target == t) {
					this.debug("new_request(): duplicate target", 2);
					return false;
				}
		}
		// Копируем себе данные запроса.
		r = {id:r0.id, subrequests:new Array(), counter:0, callback:r0.callback, tag:r0.tag};
		this.requests[this.requests.length] = r;
		for (i = 0;  i < r0.subrequests.length;  i++) {
			var sr0 = r0.subrequests[i];
			var p = this.get_page_for_url(this.absurl(sr0.url), true);
			var sr = {request:r, index:i, page:p, target:sr0.target, tag:sr0.tag};
			r.subrequests[r.subrequests.length] = sr;
			p.subrequests[p.subrequests.length] = sr;
			this.subrequests[this.subrequests.length] = sr;
			if (!p.content)
				r.counter++;
		}
		// Если все запрошенные данные уже в кеше, отдаем их. Иначе вызываем планировщик.
	
			this.debug("new_request(): scheduling...");
			this.schedule();
		
		return true;
	},

	new_request: function(request) {
		var i, r, r0 = request;
		// Проверяем корректность запроса (кроме банальных ошибок несоответствия типов).
		this.debug("<hr>");
		if (!r0.subrequests.length) {
			this.debug("new_request(): empty request", 2);
			return false;
		}
		if (r0.id) {
			for (i = 0;  i < this.requests.length;  i++)
				if (this.requests[i].id == r0.id) {
					this.debug("new_request(): duplicate request id"+this.requests[i].id, 2);
					return false;
				}
		}
		for (i = 0;  i < r0.subrequests.length;  i++) {
			var t = r0.subrequests[i].target;
			for (var j = 0;  t && (j < this.subrequests.length);  j++)
				if (this.subrequests[j].target == t) {
					this.debug("new_request(): duplicate target", 2);
					return false;
				}
		}
		// Копируем себе данные запроса.
		r = {id:r0.id, subrequests:new Array(), counter:0, callback:r0.callback, tag:r0.tag};
		this.requests[this.requests.length] = r;
		for (i = 0;  i < r0.subrequests.length;  i++) {
			var sr0 = r0.subrequests[i];
			var p = this.get_page_for_url(this.absurl(sr0.url), true);
			var sr = {request:r, index:i, page:p, target:sr0.target, tag:sr0.tag};
			r.subrequests[r.subrequests.length] = sr;
			p.subrequests[p.subrequests.length] = sr;
			this.subrequests[this.subrequests.length] = sr;
			if (!p.content)
				r.counter++;
		}
		// Если все запрошенные данные уже в кеше, отдаем их. Иначе вызываем планировщик.
		if (!r.counter) {
			this.debug("new_request(): already loaded, finalizing...");
			this.finalize_completed_requests();
		}
		else {
			this.debug("new_request(): scheduling...");
			this.schedule();
		}
		return true;
	},

	// Выдача reload на загружаемые в данный момент подзапросы указанного запроса. Уже загруженное не трогает.
	reload_request: function(id) {
		var r = this.get_request_by_id(id);
		if (!r) {
			this.debug("reload_request(): bad id", 1);
			return;
		}
		// Формируем массив pa активных объектов page по данному запросу.
		var pa = new Array(), p, i;
		for(i = 0;  i < r.subrequests.length;  i++) {
			p = r.subrequests[i].page;
			if (p.frame)
				pa[pa.length] = p;
		}
		// Сканируем массив pa, с проверкой на повторения.
		for(i = 0;  i < pa.length;  i++) {
			p = pa[i];
			var f = true, j;
			for (j = 0;  j < i;  j++)
				if (pa[j] == p) {
					f = false;
					break;
				}
			if (f) {
				this.debug("reload_request(): reloading frame idx=" + this.arrfind(this.frames, p.frame) + ", url=" + p.url);
				p.frame.frame.src = p.url;
			}
		}
	},

	// Отмена запроса.
	cancel_request: function(id) {
		var r = this.get_request_by_id(id);
		if (!r) {
			this.debug("cancel_request(): bad id", 1);
			return;
		}
		if (r.callback)
			r.callback(this.create_callback_param(r), 0);
		this.remove_request(r, true);	// this may fail, but status not returned
		this.schedule();
		return;
	},

	// Вызывается из фрейма, завершившего загрузку. Обрабатывает полностью завершенные запросы и вызывает schedule().
	frame_loaded: function(url, content) {
		this.debug("frame_loaded() started, url=" + url);
		var p = this.get_page_for_url(url), hascompleted = false, i;
		if (!p) {
			this.debug("frame_loaded(): page not found", 2);
			return false;
		}
		if (p.frame) {
			p.frame.page = null;
			p.frame = null;
		}
		else this.debug("frame_loaded(): page.frame reference is null", 2);
		p.content = "" + content;
		for (i = 0;  i < p.subrequests.length;  i++) {
			var r = p.subrequests[i].request;
			if (r.counter > 0)
				r.counter--;
			else this.debug("frame_loaded(): request.counter already zero", 2);
			if (r.counter == 0) {
				hascompleted = true;
				this.debug("frame_loaded(): request completed, id=" + (r.id ? r.id : "(null)"));
			}
		}
		if (hascompleted)
			this.finalize_completed_requests();
		this.schedule();
		return true;
	},

	debug_dump: function() {
		/*requests: new Array(),		// запросы {id:string/null, subrequests:[object], counter:int, callback:function(request,code)/null, tag:any}
		subrequests: new Array(),	// подзапросы {request:object, index:int, page:object, target:element/null, tag:any}
		pages: new Array(),			// страницы {url:string, content:string/null, subrequests:[object], frame:object/null}
		frames: new Array(),		// фреймы {frame:element, page:object/null}*/
	
		var s = "<b>debug_dump():</b><br/>", i,j, x;
		s += "BASE_URL: " + this.base_url + "<br/>";
		s += "REQUESTS<br/>";
		for (i = 0;  i < this.requests.length;  i++) {
			x = this.requests[i];
			s += "[" + i + "]: id=" + (x.id ? x.id : "(null)") + ", subrequests=[";
			for (j = 0;  j < x.subrequests.length;  j++) {
				if (j > 0)
					s += ",";
				s += this.arrfind(this.subrequests, x.subrequests[j]);
			}
			s += "], counter=" + x.counter + ", tag=" + (x.tag ? x.tag : "(null)") + "<br/>";
		}
		s += "SUBREQUESTS<br/>";
		for (i = 0;  i < this.subrequests.length;  i++) {
			x = this.subrequests[i];
			s += "[" + i + "]: request=" + this.arrfind(this.requests, x.request) + ", index=" + x.index + ", page=" +
				(x.page ? this.arrfind(this.pages, x.page) : "(null)") + ", target" + (x.target ? (x.target.id ? ".id=" + x.target.id : "=" + x.target) : "=(null)") + ", tag=" + (x.tag ? x.tag : "(null)") + "<br/>";
		}
		s += "PAGES<br/>";
		for (i = 0;  i < this.pages.length;  i++) {
			x = this.pages[i];
			s += "[" + i + "]: subrequests=[";
			for (j = 0;  j < x.subrequests.length;  j++) {
				if (j > 0)
					s += ",";
				s += this.arrfind(this.subrequests, x.subrequests[j]);
			}
			s += "], frame=" + this.arrfind(this.frames, x.frame) + ", content=" + (x.content ? "(yes)" : "(no)") + ", url=" + x.url + "<br/>";
		}
		s += "FRAMES<br/>";
		for (i = 0;  i < this.frames.length;  i++) {
			x = this.frames[i];
			s += "[" + i + "]: frame" + (x.frame ? (x.frame.id ? ".id=" + x.frame.id : "=" + x.frame) : "=(null)") + ", page=" + this.arrfind(this.pages, x.page) + "<br/>";
		}
		this.debug(s + "<br/>");
	},

// private

	// Вывод отладочного сообщения.
	debug: function(msg, severity) {
		if (!severity)
			severity = 0;
		if (this.debug_callback && severity >= this.debug_severity)
			this.debug_callback("xloader", msg, severity);
	},

	// Возвращает позицию элемента в массиве или -1. Сделана здесь, чтобы не использовать внешние модули.
	arrfind: function(a, x) {
		for (var i = 0;  i < a.length;  i++)
			if (a[i] == x)
				return i;
		return -1;
	},
	
	// Удаляет элемент из массива. Сделана здесь, чтобы не использовать внешние модули.
	arrremove: function(a, i) {
		if (i >= 0 && i < a.length) {
			for (var j = i+1;  j < a.length;  j++)
				a[j-1] = a[j];
			a.length--;
		}
	},
	
	// Приводит относительный URL к абсолютному по заданной базе.
	absurl: function(url) {
		if (/^(http:|https:)\/\//.test(url))
			return url;
		if (typeof(this.base_url) != 'string') {
			this.debug("absurl(): base_url is undefined", 2);
			return url;
		}
		return this.base_url + url;
	},
	
	get_request_by_id: function(id) {
		if (!id) {
			this.debug("get_request_by_id(): bad param", 2);
			return null;
		}
		for (var i = 0;  i < this.requests.length;  i++)
			if (this.requests[i].id == id)
				return this.requests[i];
		return null;
	},
	
	// Находит (или создает, если параметр create=true) элемент массива this.pages[] по заданному URL.
	get_page_for_url: function(url, create) {
		for (var i = 0;  i < this.pages.length;  i++)
			if (this.pages[i].url == url)
				return this.pages[i];
		if (!create)
			return null;
		var p = {url:url, content:null, subrequests:new Array(), frame:null};
		this.pages[this.pages.length] = p;
		return p;
	},

	// Генерирует параметр для вызова callback(). Используется методами: finalize_completed_requests().
	// Формат параметра request для callback(): {id, subrequests: [{url, content, target, tag}], callback, tag}
	// То есть, к формату запроса добавляется subrequests.content. Поле callback нужно для дальнейшей работы вызывающих методов данного модуля.
	create_callback_param: function(r) {
		var p = {id:r.id, subrequests:new Array(), callback:r.callback, tag:r.tag};
		for (var j = 0;  j < r.subrequests.length;  j++) {
			var sr = r.subrequests[j];
			p.subrequests[p.subrequests.length] = {url:sr.page.url, content:sr.page.content, target:sr.target, tag:sr.tag};
		}
		return p;
	},
	
	// Удаляет запрос из внутренних данных. Используется методами: finalize_completed_requests, cancel_request().
	// Если задан параметр cancel, останавливает запрос.
	remove_request: function(r, cancel) {
		this.debug("remove_request() started, request id=" + (r.id ? r.id : "(null)"));
		var idx = this.arrfind(this.requests, r);
		if (idx >= 0)
			this.arrremove(this.requests, idx);
		else
			this.debug("remove_request(): not found in this.requests", 2);
		for (var i = r.subrequests.length - 1;  i >= 0;  i--) {
			var sr = r.subrequests[i], p = sr.page;
			idx = this.arrfind(this.subrequests, sr);
			if (idx >= 0)
				this.arrremove(this.subrequests, idx);
			else
				this.debug("remove_request(): subrequest no. " + i + " not found in this.subrequests", 2);
			idx = this.arrfind(p.subrequests, sr);
			if (idx >= 0)
				this.arrremove(p.subrequests, idx);
			else
				this.debug("remove_request(): subrequest no. " + i + " not found in p.subrequests", 2);
			if (!p.subrequests.length) {
				if (p.frame) {
					if (cancel) {
						p.frame.frame.src = "about:blank";
						p.frame.page = null;
						p.frame = null;
					}
					else
						this.debug("remove_request(): page without subrequests is linked to frame, url=" + p.url, 2);
				}
				if (!p.frame) {
					idx = this.arrfind(this.pages, p);
					if (idx >= 0)
						this.arrremove(this.pages, idx);
					else
						this.debug("remove_request(): page not found in this.pages, url=" + p.url, 2);
				}
			}
		}
		this.debug("remove_request() finished");
	},

	// Проверяет на наличие завершенных запросов (у которых this.requests[].counter=0), отдает их и удаляет. Возвращает количество удаленных запросов (0 - нет таких). 
	// Во избежание глюков из-за рекурсии в данном модуле, сначала копирует данные во временный массив структур-параметров для callback() и удаляет запросы из очереди.
	// Затем вызывает callback(request,code=1), затем в подзапросах копирует subrequests[].content в subrequests[].target.innerHTML, затем вызывает callback(code=2).
	finalize_completed_requests: function() {
		this.debug("finalize_completed_requests() started");
		var a = new Array(), i, result = 0;
		for (i = this.requests.length - 1;  i >= 0;  i--) {   // сканируем с конца, из-за удаления
			var r = this.requests[i];
			if (r.counter <= 0) {
				result++;
				a[a.length] = this.create_callback_param(r);
				this.debug("finalize_completed_requests(): removing request...");
				this.remove_request(r);
			}
		}
		this.debug("finalize_completed_requests(): giving back " + result + " request(s)...");
		for (i = a.length - 1;  i >= 0;  i--) {
			var r = a[i];
			if (!r.callback || r.callback(r, 1)) {
				for (var j = 0;  j < r.subrequests.length;  j++) {
					var sr = r.subrequests[j];
					if (sr.content && sr.target)
						sr.target.innerHTML = sr.content;
				}
				if (r.callback)
					r.callback(r, 2);
			}
		}
		this.debug("finalize_completed_requests() finished");
		return result;
	},


	// Раскидывает ожидающие очереди запросы по свободным фреймам. Не грузит URL, которые в данный момент уже грузятся или загружены.
	schedule: function() {
		this.debug("schedule() started");
		var fi, f, pi, p;
		for (fi = 0;  fi < this.frames.length;  fi++) {
			f = this.frames[fi];
			if (!f.page) {
				// Ищем задание для фрейма f
				this.debug("found");
				for (pi = 0;  pi < this.pages.length;  pi++) {
					p = this.pages[pi];
					if (!p.content && !p.frame) {
						this.debug("schedule(): running frame index=" + fi + ", url=" + p.url);
						p.frame = f;
						f.page = p;
						f.frame.src = p.url;
						break;
					}
				}
				//if (!f.page)
				//	f.frame.src = "about:blank";
			}
		}
		this.debug("schedule() finished");
	}
	
	
};
