var CustomXsl = [];
function AddCustomXsl(url) {
	CustomXsl.push(url);
}

var SKIP_MYSELF=true; // Magic constant for RefreshTabOrder and focusFirst
// Focus state
var FS_FOCUSED=true; // window manager is focused
var FS_NOTFOCUSED=false; // window manager is not focused

function ApplyInnerHTML(node, sHTML) {
	if(Prototype.Browser.IE) { // workaround innerHTML bug
		var child;
		while (child = node.firstChild) {
			node.removeChild(child);
		}
	}
	node.innerHTML = sHTML;
}

function ApplyTreeOrString(node, oSrc) {
	if (typeof(oSrc.documentElement) != 'undefined') { // document passed
		var srcNode = oSrc.documentElement;
		node.update(); // clean target node
		if (!srcNode) return; // nothing to insert
		if (srcNode.localName == 'result') { // handle transformiix:result
			while (srcNode.childNodes.length) {
				// node will be unlinked from source document, so we will always append first child
				node.appendChild(srcNode.firstChild);
			}
		}
		else if (srcNode.nodeName == 'HTML') {
			var n;
			for (var i = 0; i < srcNode.childNodes.length; i++) {
				n = srcNode.childNodes[i];
				if (n.nodeName == 'BODY') break;
				n = null;
			}

			if (!n) return;
			node.innerHTML=n.innerHTML;
		}
		else {
			// transformation result is a single node
			node.appendChild(ImportNode(node.ownerDocument, srcNode, true));
		}
	}
	else {
		ApplyInnerHTML(node, oSrc); // string
	}
}

// AWindow class
var AWindow = Class.create({
	initialize: function(oParams) {
		this.sParent_ = (oParams.sParent || ""); // parent.this()
		this.sLocalID_ = (oParams.sID || ""); // local name (in XML)
		this.sID_ = (oParams.sHTMLID || ""); // global name (in HTML)
		this.sendAllControls = oParams.SendAllControls;
		this.sInstanceID_ = (oParams.sInstanceID || ""); // identification
		this.sAction_ = (oParams.sInstanceID ? "Refresh" : "");

		// where to insert content (control ID)
		this.sControlID_ = (oParams.sControlID) ? oParams.sControlID : "";

		// Collection of controls
		this.oControls_ = {};
		this.aControls_ = [];

		this.oHandlers_ = {};

		// Collection of children instanceIDs (not used now)
		this.oChildren = {};

		// Collection of label cells
		this.oLabelCells = {};

		//if(!sParent) this.nTabBase=1;
		//else this.nTabBase=-1;

		// widget contents initialization
		this.sValue = oParams.sValue;

		if (this.sParent_) {
			// bug was here (oParentWin was not initialized in some cases) // mvel@
			this.oParentWin = GWindowManager.GetWindow(this.sParent_);
		}

		// data
		if (!oParams.sParams) oParams.sParams = "";
		if (oParams.HTMLControls) { // Server-side XSLT
			this.sXMLSource_ = oParams.sURL;
			this.sParams_ = "";
			this.NonXSLWindowInit(oParams.HTMLControls);
			return;
		}
		else if (oParams.sURL) {
			// we have real window
			var paramIdx=oParams.sURL.indexOf('?');
			if(paramIdx<0) {
				this.sXMLSource_=oParams.sURL;
				this.sParams_=oParams.sParams;
			}
			else {
				this.sXMLSource_=oParams.sURL.substr(0, paramIdx);
				this.sParams_=oParams.sURL.substr(paramIdx+1)+oParams.sParams;
			}
			this.oXML_ = null;
		} else {
			// XML was manually generated
			this.sParams_ = ""; // ignore all params
			this.sXMLSource_ = ""; // !!!
			if (!oParams.oXML) {
				ThrowError("AWindow::initialize: Neither URL nor XML object passed. ");
			}
			this.oXML_ = oParams.oXML;
		}

		if (this.sXMLSource_) {
			var url = this.sXMLSource_;
			if (url.indexOf('/srv/') == 0 && this.sInstanceID_) url = "/act/dynamic/";

			if (this.sAction_ || this.sInstanceID_) url += '/' + this.sAction_;
			if (this.sInstanceID_) url += '/' + this.sInstanceID_;

			new AjaxRequest("POST", url, this.sParams_, {
				onSuccess: this.LoadingCallback.bind(this),
				onException: this.ExceptionCallback.bind(this)
			});
		} else {
			this.Load();
		}
	},
	NonXSLWindowInit: function(controls) {
		for (var i = 0; i < controls.length; ++i) {
			var c = $(controls[i].id);
			this.oControls_[controls[i].id] = { node_: c };
			this.aControls_.push(c);
		}
		var n = $(this.sID_);
		n.JSControl = this;
		n.writeAttribute('instanceid', this.sInstanceID_);
		n.writeAttribute('localid', this.sLocalID_);
		n.writeAttribute('eleclass', 'wnd');

		GWindowManager.registerWindow(this.InstanceID(), this);
		if(this.oParentWin) this.oParentWin.oChildren[this.InstanceID()]=true;

		for (var i = 0; i < controls.length; ++i) {
			var id = controls[i].id;
			var n = $(id);
			if (!n) continue;

			var dh = n.readAttribute('defaulthandler');
			if (dh) AControl.AddDefaultHandler(dh, id);

			var sHandlers = n.getAttribute("handle");
			if(sHandlers) {
				var aHandlers = sHandlers.split(' ');
				if (!this.oControls_[id]) this.oControls_[id] = new Object();

				var oCtrlInfo = this.oControls_[id];
				for (var j = 0; j < aHandlers.length; ++j) {
					var sEventName = aHandlers[j];
					if (!sEventName) continue;
					// put into control's map
					if (!oCtrlInfo.oHandlers_) oCtrlInfo.oHandlers_ = new Object();
					oCtrlInfo.oHandlers_[sEventName] = ET_REGULAR;
					// and into global map (to register handlers
					this.oHandlers_[sEventName] = true;
				}
			}
			var n = [];
			for (a in controls[i].attributes) {
				n.push({name: a, value: controls[i].attributes[a]});
			}
			this.SetAttributes(id, {attributes: n});
		}
		this.InitDefaultSubscribers();

		for (var i = 0; i < controls.length; ++i) { //TODO: this can probably be reused
			var id = controls[i].id;
			var n = $(id);
			if (!n) continue;

			var dependsOn = n.readAttribute("dependsOn");
			if (dependsOn && dependsOn != "") {
				var d = dependsOn.split(" ");
				for (var j = 0; j < d.length; ++j) {
					d[j] = this.ControlHTMLID(d[j]);
				}

				this.Subscribe("INPUT", this.CheckDependencies.bind(this, id, d));
				this.Subscribe("CHANGE", this.CheckDependencies.bind(this, id, d));

				this.CheckDependencies(id, d)
			}
		}
	},
	InitDefaultSubscribers: function() {
		//LogL("CHANGE Subscribed on: ID="+this.ID());
		this.oHandlers_["CHANGE"]=true;
		for (var sKey in this.oHandlers_) {
			//LogL("GENERIC Subscribed on: "+sKey+", ID="+this.ID());
			this.Subscribe(sKey, this.ServerEventHandler, this);
		}
		this.Subscribe("CHANGE", this.OnControlChange, this);
		this.Subscribe("WM_INIT", this.OnControlChange, this);
		this.Subscribe("WM_RESTORE", this.OnRestore, this);
		this.Subscribe("INPUT", this.OnControlInput, this);

	},
	LoadingCallback: function(oAjaxRequest) {
		this.oXML_ = oAjaxRequest.XML();
		this.Load();
	},
	Load: function() {
		if (!this.oXML_.documentElement) {
			this.SetError("AWindow::Load(): XML loading failed: No document data! ");
			return;
		}
		this.sLocalID_ = this.oXML_.documentElement.getAttribute("id");
		this.sTitle_ = this.oXML_.documentElement.getAttribute("title");
		this.sInstanceID_ = this.oXML_.documentElement.getAttribute("this");

		GWindowManager.registerWindow(this.InstanceID(), this);
		if(this.oParentWin) this.oParentWin.oChildren[this.InstanceID()]=true;


		this.ProcessHTMLID();
//		alert(this.oXML_.xml);

		var oTransformDoc = GWindowManager.Transform(this.oXML_, true);
		//LogL("CHANGE Subscribed on: ID="+this.ID());

		this.InitDefaultSubscribers();

		var ctlId = this.sControlID_;
		if (!this.sParent_) ctlId = GWindowManager.ID();

		var node = $(ctlId);
		if (!$(node)) return; // window already died...

		if(node.JSControl && typeof(node.JSControl.Die)=="function") node.JSControl.Die();
		ApplyTreeOrString(node, oTransformDoc);
		this.HTML=node.select('div')[0];
		this.HTML.JSControl=this;
		node.JSControl=this;

		if (this.sParent_) this.registerInParentResizer();

		try {
			if(typeof(oTransformDoc)=="string") oTransformDoc.evalScripts(); // another IE bug workaroung
			else node.innerHTML.evalScripts();
		} catch (err) {
			alert("Exception during script evaluation! Exception name is '"+err.name+"', message is '"+err.message+"'. ");
		}

		var oRoot = this.XML().documentElement;
		//var aElements = AXML.SelectNodes(oRoot, "/ai:wnd/ai:lay//ai:ele", AINS);
		var aElements = AXML.SelectNodes(oRoot, "/ai:wnd/ai:ele|/ai:wnd/ai:ele/ai:ele", AINS);
		if (!aElements) {
			alert("DEBUG: Bad window! No elements found! XML:\r\n"+oRoot.xml);
			return;
		}

		for (var i = 0; i < aElements.length; ++i) {
			var oNode = aElements[i];
			var sID = oNode.getAttribute("id");
// 			oNode=AXML.SelectNode(oRoot, '/ai:wnd/ai:ele[@id="'+sID+'"]', AINS);
			var sHTMLID = oNode.getAttribute("htmlid");
			if (!sHTMLID) continue; // element is not referenced in layout

			var dependsOn = oNode.getAttribute("dependsOn");
			if (dependsOn && dependsOn != "") {
				var d = dependsOn.split(" ");
				for (var j = 0; j < d.length; ++j) {
					d[j] = this.ControlHTMLID(d[j]);
				}

				this.Subscribe("INPUT", this.CheckDependencies.bind(this, sHTMLID, d));
				this.Subscribe("CHANGE", this.CheckDependencies.bind(this, sHTMLID, d));

			}

			var el = $(sHTMLID);
			if (el) {
				// this will not work on nested elements (!!!) ControlHTMLID gives wrong ID for nested elements
				var sControlID = this.ControlHTMLID(sID);
				if (!this.oControls_[sControlID]) this.oControls_[sControlID] = new Object();
				//LogL("Node registered: "+sControlID);
				this.oControls_[sControlID].node_ = el;
			}

			var sClassName = oNode.getAttribute("class");
			if (sClassName == "widget") {
				var sNewSrc = oNode.getAttribute("src");
				var sValue = oNode.getAttribute("value");
				if (!sValue || oNode.getAttribute("update-value")!='yes') {
					/*if (sValue) {
						LogE("Undefining VALUE, was '"+sValue+"'");
						LogL("WindowID=$$"+this.ID()+"$$");
						LogL("Control ID=$$"+oNode.getAttribute("id")+"$$");
						LogL("Control XML="+oNode.xml);
					}*/
					sValue=undefined;
				}
				//if (sValue) LogE("LoadingCallback: "+sValue);
				if (sNewSrc) {
					GWindowManager.CreateWindow({
						sURL: sNewSrc,
						sParent: this.sInstanceID_,
						sControlID: sHTMLID,
						sValue: sValue
					});
				}
			}
			else {
				this.SetAttributes(sHTMLID, oNode);
			}
		}

		this.UpdateLayoutElements(oRoot);

		this.InitChangesTracking(oRoot);

		// filling an array for further tabstop setting
		var aLay = AXML.SelectNodes(oRoot, "/ai:wnd/ai:lay//ai:ele", AINS);
		for (var i = 0; i < aLay.length; ++i) {
			var sID = aLay[i].getAttribute("id");
			var sControlID = this.ControlHTMLID(sID);
			if(!this.oControls_[sControlID]) continue;
			var el=this.oControls_[sControlID].node_;
			if(!el) continue;
			el.position_ = this.aControls_.length;
			el.nTabBase = 0;
			this.aControls_.push(el);
		}
		if(this.oParentWin) { // Setup tab order of the window
			if (this.IsPopup) { // experimental
				GWindowManager.StoreActiveElement();
				GWindowManager.ClearTabOrder(FS_NOTFOCUSED);
				this.SetTabOrder(0);
			} else {
				this.oParentWin.RefreshTabOrder(this.sControlID_);
			}
			this.focusFirst(this.HTML, SKIP_MYSELF);
		}
		// emit creation event
		new AEvent("WM_CREATE", { wid: this.LocalID()}, this.ID());
		for (var cid in this.oControls_) {
			// TODO: verify this (last commented condition)
			if (!this.oControls_[cid] || !this.oControls_[cid].node_/* || !this.oControls_[cid].node_.JSControl*/) continue;
			new AEvent("INPUT", {}, this.oControls_[cid].node_.JSControl);
		}
		// initialize window as widget
		if (this.sValue) {
			var oValue=URLRestoreV(this.sValue);
			this.SetValue(oValue);
			this.sValue=undefined;
		}
		this.ProcessEvents(oRoot);
		// We cannot post WM_INIT from window itself, because it will be captured by 'this' window
		// but we can use widget control of the parent window as event emitter // mvel
		var widgetNode=$(this.HTML.parentNode);
		if (widgetNode && widgetNode.JSControl==this) {
			new AEvent("WM_INIT", {}, widgetNode.getAttribute('id'));
			//LogL("Widget WM_INIT: $$"+this.ID()+"$$");
		}
	},
	InitChangesTracking: function(oRoot) {
		var sTrackChanges=oRoot.getAttribute("track-changes") || '';
		if (sTrackChanges=="no") return;
		if (sTrackChanges.substr(0,1)=="'") {
			// extract control id (= action name for default handler)
			this.trackChangesAction=sTrackChanges.substr(1,sTrackChanges.length-2);
		}
		var aLabels=AXML.SelectNodes(oRoot, "/ai:wnd/ai:lay//ai:cell[@labelfor]", AINS);
		for(var i=0; i<aLabels.length; ++i) {
			var sID = aLabels[i].getAttribute("id");
			var sLF = aLabels[i].getAttribute("labelfor");
			var sHLF=this.ID()+'_'+sLF;
			var eleNode=$(sHLF);
			if( !eleNode || !eleNode.JSControl ||
				typeof(eleNode.JSControl.Value)!="function" ||
				eleNode.getAttribute("track-changes")=="no" ) continue;
			this.oLabelCells[sHLF]={
				'oNode': $(this.ID()+'_cell-'+sID),
				'bModified': false,
				'sValue': URLSerialize(eleNode.JSControl.Value())
			};
		}
		this.UpdateLayoutElements(oRoot);
	},
	// activates window
	Focus: function() {
		if (GWindowManager.Focused()) {
			//LogL("WM is already in focus. ");
			return;
		}
		GWindowManager.ClearTabOrder(FS_FOCUSED);
		GWindowManager.SetTabOrder(0);
		this.focusFirst(this.HTML, SKIP_MYSELF);
		//LogL("Window $$"+this.ID()+"$$ activated");
	},

	focusFirst: function(element, bSkipMyself) {
		if (!element || element.nodeType != Node.ELEMENT_NODE) return false;
		/*LogL(
			"try to focus to $$"+element.nodeName+"$$"+
			" id=$$"+element.getAttribute('id')+"$$"+
			" tabindex=$$"+element.tabindex+"$$"+
			" bSkipMyself=$$"+bSkipMyself+"$$");*/
		if (!bSkipMyself && element.tabIndex >= 0 && element.style.display != 'none') {
			if (typeof element.activate == "function") {
				element.activate();
				return true;
			} else if (element.focus) {
				element.focus();
				//LogE("Focused to id=$$"+element.getAttribute('id')+"$$, with tabIndex="+element.tabIndex);
				return true;
			}
		}
		//LogL("Not focusing, processing children");
		for (var child = element.firstChild; child; child = child.nextSibling){
			if (this.focusFirst(child)) return true;
		}
		return false;
	},

	SetAttributes: function(sHTMLID, oNode) {
//	LogL("AControl.SetAttributes: "+sHTMLID);
		var htmlNode=$(sHTMLID);
		if(!htmlNode || !htmlNode.JSControl) return;
		var JSControl=htmlNode.JSControl;

		for (var j = 0; j < oNode.attributes.length; ++j) {
			var oAttr = oNode.attributes[j];
			if (oAttr.name == "value" && JSControl.SetValue) {
				JSControl.SetValue(URLRestoreV(oAttr.value));
			}
			else {
				if (JSControl.SetAttribute && oAttr.name!="id" && oAttr.name!="class" && oAttr.name!="type") {
					JSControl.SetAttribute(oAttr.name, oAttr.value);
				}
			}
		}
	},

	RegisterNewControl: function(oControl, parentNode) {
		// now used only for tree and popups registering
		var el=oControl.parentNode || parentNode;
		if(!el) return;
		var id=oControl.ID();
		if (!this.oControls_[id]) this.oControls_[id]=new Object();
		this.oControls_[id].node_=el;
		el.position_ = this.aControls_.length;
		if (parentNode) {
			var len=this.aControls_.length;
			if (len) {
				var tabIndex=this.aControls_.last().nTabBase;
				if (typeof(tabIndex)=='number') el.nTabBase=tabIndex+1;
				else el.nTabBase=0;
			}
		} else {
			el.nTabBase = 0;
		}
		this.aControls_.push(el);
	},
	registerInParentResizer: function() {
		if (!this.sParent_) return; // window has no parent
		//LogL("registerInParentResizer $$"+this.ID()+"$$, parent's InstanceID="+this.sParent_);
		var oParentWin;
		var elSelf = $(this.ID());
		if (elSelf) oParentWin = AControl.GetParentWindow( $(elSelf.parentNode) );
		// we'll fail here for popups
		if (!oParentWin) {
			oParentWin = GWindowManager.GetWindow(this.sParent_);
		}
		if (oParentWin) {
			oParentWin.RegisterResizeableChild(this.ID());
			oParentWin.registerInParentResizer(); // force chain
		} else {
			LogL("Cannot get parent window for "+this.ID());
		}
	},
	ProcessEvent: function(oEvtNode, oRoot) {
		var sType=oEvtNode.getAttribute("type");
		var oParams=new Object();
		var sArgs=oEvtNode.getAttribute("args");
		if(sArgs) {
			var aParts = sArgs.split(" ");
			for(var j = 0; j < aParts.length; ++j) {
				var p = aParts[j];
				var n = p.indexOf("=");
				if (n < 1) continue;

				var sKey = p.substring(0, n);
				var sQVal=p.substring(n+1);
				if (sQVal.substr(0,1)!="'") {
					alert("Event processing error: argument named '"+sKey+
						"' is not defined, please check '"+this.LocalID()+"' window description. ");
					continue;
				}
				var sVal=sQVal.substr(1, sQVal.length-2);
				oParams[sKey]=URLRestore('Value:' + sVal + "&&");
			}
		}
		var sEmitter=oEvtNode.getAttribute("emitter");
		if (!sEmitter) sEmitter=this.ID();
		var oData = {"args": oParams, "xml": oRoot};
		for(var a=0; a<oEvtNode.attributes.length; ++a) {
			var attr=oEvtNode.attributes[a];
			if(attr.name=="args" | attr.name=="type") continue;
			oData[attr.name]=attr.value;
		}

		new AEvent(sType, oData, sEmitter);
	},
	ProcessEvents: function(oRoot) {
		var aEvents = AXML.SelectNodes(oRoot, "/ai:wnd/ai:e/ai:evt[@type!='WE_APP_MESSAGE']", AINS);
		if (aEvents) {
			for(var i=0; i<aEvents.length; ++i) {
				var oEvtNode=aEvents[i];
				this.ProcessEvent(oEvtNode, oRoot);
			}
		}
		aEvents = AXML.SelectNodes(oRoot, "/ai:wnd/ai:e/ai:evt[@type='WE_APP_MESSAGE']", AINS);
		if (aEvents) {
			for(var i=0; i<aEvents.length; ++i) {
				var oEvtNode=aEvents[i];
				this.ProcessEvent(oEvtNode, oRoot);
			}
		}
	},

	ServerEventHandler: function(oEvent) {
		var sEventName = oEvent.Name();
		//LogL("AWindow::ServerEventHandler: "+sEventName+", emitter="+oEvent.EmitterID());
		var sControlID = oEvent.EmitterID();
		var oCtrlInfo = this.oControls_[sControlID];
		if (!oCtrlInfo) {
			// it can be event from widget
			var win=$(sControlID).JSControl || null;
			if (!win) return; // something is a miss here
			oCtrlInfo=this.oControls_[win.sControlID_];
			if (!oCtrlInfo) return;
		}
		if (!oCtrlInfo.oHandlers_) {
			// control can have no registered server handlers
			//LogL("Control "+sControlID+" has no server handler for event "+sEventName);
			return;
		}
		var nEventType = oCtrlInfo.oHandlers_[sEventName];
		if (!nEventType) {
			//LogL("Control "+sControlID+" has no server handler for event "+sEventName);
			return;
		}

		if (this.windowBusy) {
			if (nEventType & ET_IMPORTANT) { // this event handler is important, we should process in unconditionally
				//LogE("This is an important event handler");
			} else {
				oEvent.Stop();
				return;
			}
		}
		this.Busy();
		oEvent.SetType(nEventType);
		oEvent.Stop();

		this.SubscribeOnValue(this.CallServerEvent.bind(this, oEvent));
	},
	SubscribeOnValue: function(fCallback) {
		this.nControlsToWait=1;
		this.fOnReadyCallback=fCallback;
		for (var sKey in this.oControls_) {
			// enum all controls
			var oControl = this.oControls_[sKey];
			if (!oControl.bChanged_ || !oControl.node_) continue;
			var node=oControl.node_;
			if (!node.JSControl) continue;
			var jsControl=node.JSControl;
			if (!jsControl.ID || !$(jsControl.ID())) continue;
			if (!jsControl.SubscribeOnValue) continue;
			++this.nControlsToWait;
			jsControl.SubscribeOnValue(this.ValueIsReady.bind(this));
		}
		this.ValueIsReady();
	},

	ValueIsReady: function(oEvent) {
		if( --this.nControlsToWait<=0 ) {
			this.fOnReadyCallback();
			delete this.fOnReadyCallback;
		}
	},

	CallServerEvent: function(oEvent) {
		var sControlID=oEvent.EmitterID();
		var elpControl = $(sControlID);
		var oData = this.CollectChangedData();
		var sPEventName;
		if (elpControl.readAttribute('eleclass')=='wnd') { // it's an event from widget
			var win=$(sControlID).JSControl || null;
			if (!win) {
				LogE("Cannot make server event call passed from widget "+sControlID);
				return;
			}
			elpControl=$(win.sControlID_); // switch to window' widget
		}
		var sLocalName = elpControl.readAttribute('localid');
		if (oEvent.Type() & ET_DEFAULT) sPEventName=sLocalName;
		else sPEventName = sLocalName+"_"+oEvent.Name();
		var oED=oEvent.Data();
		if (oED && typeof(oED["args"]) != 'undefined') oData["__event"]=oED["args"];
		var sData = URLSerializeVS(oData);

		var timeout=elpControl.readAttribute('timeout');
		if(timeout==undefined) timeout=30; // default timeout for method calls
		this.Update(sPEventName, sData, timeout);
	},

	SetTabOrder: function(nTabBase) {
		//LogE("AWindow:SetTabOrder("+nTabBase+")");
		var nTabEnd=nTabBase;

		for(var i=0; i<this.aControls_.length; ++i) {
			var oCtrl=this.aControls_[i];
			oCtrl.nTabBase=nTabEnd;
			var childWin=Element.down(oCtrl, 'DIV[instanceid]');
			if (childWin && childWin.JSControl) {
				nTabEnd=childWin.JSControl.SetTabOrder(nTabEnd);
				continue;
			}
			// non-window control
			if(oCtrl.JSControl && oCtrl.JSControl.SetTabOrder) { // Kosher control
				nTabEnd=oCtrl.JSControl.SetTabOrder(nTabEnd);
			}
			else if(oCtrl.nodeName=='INPUT') { // simple HTML control
				oCtrl.writeAttribute('tabindex', nTabEnd++);
			}
		}
		return nTabEnd;
	},

	RefreshTabOrder: function(sControlID, bSkipMyself) {
		//LogE("Refreshing tab order: $$"+sControlID+"$$");
		var oCtrlInfo=this.oControls_[sControlID];
		if (!oCtrlInfo) return; // control not found!
		var ctrl=oCtrlInfo.node_;
		var nTabEnd=ctrl.nTabBase;
		if(nTabEnd<=0) {
			if(!this.oParentWin) nTabEnd=1;
			else return;
		}
		var startIndex=ctrl.position_;
		if(bSkipMyself) ++startIndex;

		for(var i=startIndex; i<this.aControls_.length; ++i) {
			var oCtrl=this.aControls_[i];
			oCtrl.nTabBase=nTabEnd;

			if( oCtrl.select &&
				  oCtrl.select('DIV[instanceid]') &&
					oCtrl.select('DIV[instanceid]').length>0 ) { // child window
				var child=oCtrl.select('DIV[instanceid]')[0].JSControl;
				oCtrl.nTabBase=nTabEnd;
				nTabEnd=child.SetTabOrder(nTabEnd);
				continue;
			}

			if(oCtrl.JSControl && oCtrl.JSControl.SetTabOrder) nTabEnd=oCtrl.JSControl.SetTabOrder(nTabEnd);
		}
		if(this.oParentWin) this.oParentWin.RefreshTabOrder(this.sControlID_, SKIP_MYSELF);
	},
	Busy: function() {
		if(this.HTML) this.HTML.addClassName('WidgetBusy');
		this.windowBusy=true;
	},
	Free: function() {
		if(this.HTML) this.HTML.removeClassName('WidgetBusy');
		this.windowBusy=false;
	},
	Refresh: function(sTimeout) {
		var sURL = this.sXMLSource_;
		if (sURL.indexOf("/srv/") == 0) sURL = "/act/dynamic/"

		sURL += "/Refresh/"+this.sInstanceID_;
		new AjaxRequest("POST", sURL, "", {
			onSuccess: this.UpdatingCallback.bind(this),
			onException: this.ExceptionCallback.bind(this),
			'timeout': parseInt(sTimeout, 10)
		});
	},

	Update: function(sEventName, sData, sTimeout) {
		var sURL = this.sXMLSource_;
		if (sURL.indexOf("/srv/") == 0) sURL = "/act/dynamic/"

		// TODO: if window not loaded, we cannot update it...
		sURL += "/"+sEventName+"/"+this.sInstanceID_;
		new AjaxRequest("POST", sURL, sData, {
			onSuccess: this.UpdatingCallback.bind(this),
			onException: this.ExceptionCallback.bind(this),
			timeout: parseInt(sTimeout, 10)
		});
	},
	ExceptionCallback: function(oAjaxRequest) {
		this.Free();
		if (window.GLoader && GLoader.CheckFatalError(oAjaxRequest.ErrorCode()) ) return;

//		var oXmlDomUpdate = oAjaxRequest.XML();
//		if(!oXmlDomUpdate) alert("Error! Wrong Ajax response. ");

		alert("" + oAjaxRequest.Error());
	},
	UpdatingCallback: function(oAjaxRequest) { try {
// 	alert(oAjaxRequest.oTransport_.responseText)
		this.Free();
		var oXmlDomUpdate = oAjaxRequest.XML();
		var oDstRoot = (this.oXML_ ? this.oXML_.documentElement : null);

		// merge element attributes
		var oSrcRoot = oXmlDomUpdate.documentElement;
		this.ProcessLocalID(oSrcRoot);

		var sWindowInstanceID = oSrcRoot.getAttribute("this");
		//LogL("Update: this="+sWindowInstanceID);

		this.ProcessEvents(oSrcRoot);

//		LogL("Updating window "+this.ID());

		this.rememberPrevValues();
		var aElements = AXML.SelectNodes(oSrcRoot, "//ai:ele[not(ancestor::ai:lay)]", AINS);
		if (!aElements) {
			alert("DEBUG: Bad window! No elements found! XML:\r\n"+oSrcRoot.xml);
			return;
		}

		for (var i = 0; i < aElements.length; ++i) {
			var oSrcNode = aElements[i];
			var sID = oSrcNode.getAttribute("id");
//			LogL("node found: "+sID);

			var oDstNode;
			if (oDstRoot && !(oDstNode = AXML.SelectNode(oDstRoot, "//ai:ele[not(ancestor::ai:lay)][@id='"+sID+"']", AINS))) {
				//LogL("Unknown element received (was not described in elements section): " + sID);
				return;
			}

			var sHTMLID = (oDstNode ? oDstNode : oSrcNode).getAttribute("htmlid");
			if (!sHTMLID) continue; // element is not referenced in layout
//			LogL("node suited: "+sID);

			var sClassName = oSrcNode.getAttribute("class");
			if (sClassName == "widget") {
				var sNewSrc = oSrcNode.getAttribute("src");
				var eW=$(sHTMLID);
				var sOldSrc="";
				if(eW && eW.JSControl) {
					sOldSrc=eW.JSControl.XMLSource();
					if(eW.JSControl.Params()) sOldSrc+='?'+eW.JSControl.Params();
				}
				if (sNewSrc!=sOldSrc) {
					if (sNewSrc) {
						GWindowManager.CreateWindow({
							sURL: sNewSrc,
							sParent: this.InstanceID(),
							sControlID: sHTMLID,
							bCheckChanges: true
						});
					} else {
						// TODO: destroy inner window
					}
				} else { // src was not changed, only set attributes
					if (oSrcNode.getAttribute("update-value")=='yes') {
						this.SetAttributes(sHTMLID, oSrcNode);
					}
				}
			} else { // regular control
				this.SetAttributes(sHTMLID, oSrcNode);
			}
		}
		this.updateModificationStatus();
		this.UpdateLayoutElements(oSrcRoot);

		for(var cid in this.oControls_) {
			var oNode=this.oControls_[cid].node_;
			if(!oNode) continue;
			var sHTMLID=oNode.getAttribute("htmlid");
			if(!sHTMLID) continue;

			var dependsOn=oNode.getAttribute("dependsOn");
			if(dependsOn && dependsOn!="") {
				var d=dependsOn.split(" ");
				for(var j=0; j<d.length; ++j) {
					d[j]=this.ControlHTMLID(d[j]);
				}
				this.CheckDependencies(sHTMLID, d);
			}
		}
		// ------------------------------------ Exception handling block
		} catch (exc) {
			alert("AWindow:UpdatingCallback: Exception: "+exc);
		}
	},

	UpdateLayoutElements: function(xml) {
		var els = AXML.SelectNodes(xml, "/ai:wnd/ai:lay//*[name() != 'ele'][@id][@visibility or @style]", AINS);
		for (var i = 0; i < els.length; ++i) {
			var e = els[i];
			var id = e.getAttribute('id');

			var tag = e.nodeName.replace(/[^:]+:/, '');
			var node;
			if (tag == "grid") {
				node = $("gridFrame_" + id + "_" + this.InstanceID());
			}
			else {
				var uid = tag + "-" + id;
				node = $( this.ControlHTMLID(uid) );
			}

			if (!node) continue;
			var st = e.getAttribute('style');
			if (st !== null) {
				node.writeAttribute('style', st);
			}

			var visibility = e.getAttribute('visibility');
			if(!visibility) continue;
			if(visibility != 'no') node.show();
			else node.hide();
		}
	},

	CollectChangedData: function() {
		var oData = new Object();
		for (var sKey in this.oControls_) {
			// enum all controls
			var oControl = this.oControls_[sKey];
			if (oControl.bChanged_ || this.sendAllControls) {
				if (!oControl.node_) {
					LogE("CollectChangedData: Control node not found: " + sKey);
					continue;
				}
				var JSControl=oControl.node_.JSControl;
				if(!JSControl || typeof(JSControl.Value) != 'function') continue;
				oData[oControl.node_.readAttribute('localid')] = JSControl.Value();
			}
		}
		// TODO: clear controls change state
		//this.oControls_ = new Object();
		return oData;
	},
	Value: function() {
		var res={};
		for(var k in this.oControls_) {
			var node=this.oControls_[k].node_;
			if(node && node.JSControl && typeof(node.JSControl.Value)=='function') {
				var locid=node.readAttribute('localid');
				if(locid) res[locid]=node.JSControl.Value();
			}
		}
		return res;
	},
	SetValue: function(oValue) {
		for(var k in this.oControls_) {
			var node=this.oControls_[k].node_;
			if (!node) continue; // strange control...
			var jsControl=node.JSControl;
			if (!jsControl) continue; // non-kosher control
			if (typeof(jsControl.SetValue)!='function') continue; // cannot perform SetValue for this control
			var locid=node.readAttribute('localid');
			if (!locid) continue; // cannot get local ID, strange control
			if (typeof(oValue[locid]) == 'undefined') continue; // no value for this control

			jsControl.SetValue(oValue[locid]);
			//new AEvent("CHANGE", {}, jsControl);
		}
	},

	OnRestore: function(oEvent) {
//		oEvent.Stop();
		this.registerInParentResizer();
	},
	Die: function() {
		for(var ch in this.oChildren) {
			GWindowManager.GetWindow(ch).Die();
		}
		if(this.oParentWin) delete this.oParentWin.oChildren[this.InstanceID()];
		GWindowManager.DeleteWindow(this.sInstanceID_);
	},
	hasModifiedChildren: function() {
		for(var ch in this.oChildren) {
			var chWin=GWindowManager.GetWindow(ch);
			if (chWin.IsModified) return ch;
			var modCh=chWin.hasModifiedChildren();
			if (modCh) return modCh;
		}
		return '';
	},

	ProcessLocalID: function(oRoot) {
		var aElements = AXML.SelectNodes(oRoot, "/ai:wnd/ai:ele", AINS);
		for (var i = 0; i < aElements.length; ++i) {
			var oNode = aElements[i];
			if (typeof(oNode.getAttribute) == 'undefined') continue;
			var sLocalID = oNode.getAttribute("id");
			var sID = this.sID_+'_'+sLocalID;
			oNode.setAttribute("htmlid", sID);
			this.SetChildLocalID(sID, sLocalID, oRoot);
		}
	},

	SetChildLocalID: function(sPrefix, sParentLocalID, oRoot) {
		var aElements = AXML.SelectNodes(oRoot, "//ai:ele[@htmlid='"+sPrefix+"']/ai:ele", AINS);
		for (var i = 0; i < aElements.length; ++i) {
			var oNode = aElements[i];
			if (typeof oNode.getAttribute == 'undefined') continue;
			var sLocalID = oNode.getAttribute("id");
			var sID = sPrefix+'_'+sLocalID;
			sLocalID=sParentLocalID+"."+sLocalID;
			oNode.setAttribute("htmlid", sID);
			oNode.setAttribute("id", sLocalID);
			this.SetChildLocalID(sID, sLocalID, oRoot);
		}
	},

	processHandlers: function(oNode, sID) {
		var sHandlers=oNode.getAttribute("handle");
		if (!sHandlers) return;
		var aHandlers=sHandlers.split(' ');
		if (!this.oControls_[sID]) this.oControls_[sID]=new Object();
		var oCtrlInfo=this.oControls_[sID];
		for (var j=0; j<aHandlers.length; ++j) {
			var sEventName=aHandlers[j];
			if (!sEventName) continue;
			var flags=ET_REGULAR;
			if (sEventName.substr(sEventName.length-1,1)=='*') {
				sEventName=sEventName.substr(0,sEventName.length-1);
				flags |= ET_IMPORTANT;
			}
			// put into control's map
			if (!oCtrlInfo.oHandlers_) oCtrlInfo.oHandlers_=new Object();
			oCtrlInfo.oHandlers_[sEventName]=flags;
			// and into global map (to register handlers
			this.oHandlers_[sEventName]=true;
		}
	},

	ProcessHTMLID: function() {
		var sGlobalPrefix = this.sControlID_;
		if (sGlobalPrefix) sGlobalPrefix += '_';
		sGlobalPrefix += this.LocalID();
		// add htmlid to window itself
		this.oXML_.documentElement.setAttribute("htmlid", sGlobalPrefix);
		this.sID_ = sGlobalPrefix;
		sGlobalPrefix += '_';
		this.oHandlers_ = new Object();

		var oRoot = this.oXML_.documentElement;

		var aElements = AXML.SelectNodes(oRoot, "/ai:wnd/ai:ele|/ai:wnd/ai:ele/ai:ele", AINS);
		for (var i = 0; i < aElements.length; ++i) {
			var oNode = aElements[i];
			if (typeof(oNode.getAttribute) == 'undefined') continue;
			var sLocalID = oNode.getAttribute("id");
			var sID = sGlobalPrefix+sLocalID;
			oNode.setAttribute("htmlid", sID);
			this.processHandlers(oNode, sID);
			this.SetChildHTMLID(sID, sLocalID, oRoot);
		}

		var aElements = AXML.SelectNodes(oRoot, "/ai:wnd/ai:lay//*[name() != 'ele']", AINS);
		for (var i = 0; i < aElements.length; ++i) {
			var oNode = aElements[i];
			if (typeof(oNode.getAttribute) == 'undefined') continue;
			if (oNode.localName == 't') continue;

			var sLocalID = oNode.getAttribute("id");
			var sID = sGlobalPrefix + (oNode.baseName || oNode.localName) + "-" + sLocalID;
			oNode.setAttribute("htmlid", sID);
		}
	},
	SetChildHTMLID: function(sPrefix, sParentLocalID, oRoot) {
		var aElements = AXML.SelectNodes(oRoot, "//ai:ele[@htmlid='"+sPrefix+"']/ai:ele", AINS);
		for (var i = 0; i < aElements.length; ++i) {
			var oNode = aElements[i];
			if (typeof oNode.getAttribute == 'undefined') continue;
			var sLocalID = oNode.getAttribute("id");
			var sID = sPrefix+'_'+sLocalID;
			sLocalID=sParentLocalID+"."+sLocalID;
			oNode.setAttribute("htmlid", sID);
			oNode.setAttribute("id", sLocalID);
			this.processHandlers(oNode);
			this.SetChildHTMLID(sID, sLocalID, oRoot);
		}
	},
	// Adds default handlers to some controls
	AddDefaultHandler: function(sEventName, sControlID) {
		//LogL('AddDefaultHandler for '+sControlID+", window.this="+this.InstanceID());
		var oCtrlInfo = this.oControls_[sControlID];
		if (!oCtrlInfo) {
			this.oControls_[sControlID] = new Object();
			oCtrlInfo = this.oControls_[sControlID];
		}
		if (!oCtrlInfo.oHandlers_) oCtrlInfo.oHandlers_ = new Object();
		if (typeof(oCtrlInfo.oHandlers_[sEventName])=='undefined') oCtrlInfo.oHandlers_[sEventName]=ET_NONE;
		oCtrlInfo.oHandlers_[sEventName] |= ET_DEFAULT; // default handler
		//LogL("ADH after addition===");
		//showObject(this.oControls_[sControlID].oHandlers_);
		//showObject(this.oControls_);

		// if this event type was not already
		if (!this.oHandlers_[sEventName]) {
			// add to subscription list
			//LogL("GENERIC Subscribed on: "+sEventName+", ID="+this.ID());
			this.Subscribe(sEventName, this.ServerEventHandler, this);
			this.oHandlers_[sEventName] = true;
		}
	},

	CheckDependencies: function(dependentId, dependencies, oEvent) {
		var sControlID = (oEvent ? oEvent.EmitterID() : null);
		var i;
		for (i = 0; i < dependencies.length; ++i) {
			if (dependencies[i] == sControlID || !sControlID) break;
		}
		if (i == dependencies.length) return;

		var controls = "";
		var enabled = true;
		for (i = 0; i < dependencies.length; ++i) {
			var c = $(dependencies[i]);
			var v = (c ? c.JSControl.Value() : null);
			if (c && (!v || v.IsNull && v.IsNull() || typeof v.IsIncorrect == "function" && v.IsIncorrect())) {
				enabled = false;
				controls += ", " + c.readAttribute("title");
			}
		}

		var c = $(dependentId);
		if (!c) return;

		var f = controls.substr(2);
		c.JSControl.SetAttribute("visibility", (enabled ? "yes" : "vo"));
		c.JSControl.SetAttribute("title", enabled ? "" :
			"Пожалуйста, заполните обязательные поля: " + f + "");
	},

	rememberPrevValues: function() {
		for(var sControlID in this.oLabelCells) {
			var lc=this.oLabelCells[sControlID];
			if($(sControlID)) lc.sPrevValue=URLSerialize($(sControlID).JSControl.Value());
		}
	},
	updateModificationStatus: function() {
		this.IsModified=false;
		for(var sControlID in this.oLabelCells) {
			if(!$(sControlID)) continue;
			var lc=this.oLabelCells[sControlID];
			var newVal=URLSerialize($(sControlID).JSControl.Value());
			if(!lc.bModified) {
				lc.sValue=newVal;
				continue;
			}
			if(lc.sPrevValue!=newVal) {
				lc.sValue=newVal;
				lc.bModified=false;
				lc.oNode.removeClassName("Modified");
			}
			else this.IsModified=true;
			delete lc.sPrevValue;
		}
	},
	syncModificationStatus: function() {
		for(var sControlID in this.oLabelCells) {
			var lc=this.oLabelCells[sControlID];
			if(lc.bModified) {
				this.IsModified=true;
				return;
			}
		}
		this.IsModified=false;
	},
	OnControlInput: function(oEvent) {
		if(oEvent.Name() == "INPUT") oEvent.Stop();
		var sControlID=oEvent.EmitterID();

		if( (oEvent.Data() && oEvent.Data().bMutable) ||
		    AControl.GetParentWindowID(sControlID)!=this.ID() ) return;

		var lc=this.oLabelCells[sControlID];
		if(lc) {
			if(lc.sValue==URLSerialize($(sControlID).JSControl.Value())) {
				lc.oNode.removeClassName("Modified");
				lc.bModified=false;
				this.syncModificationStatus();
			}
			else {
				lc.oNode.addClassName("Modified");
				lc.bModified=true;
				this.IsModified=true;
			}
		}
	},
	OnControlChange: function(oEvent) {
//		LogE("AWindow[$$"+this.ID()+"$$]::OnControlChange: $$"+oEvent.Name()+"$$, emitter=$$"+oEvent.EmitterID()+"$$");
		var sControlID=oEvent.EmitterID();
		this.OnControlInput(oEvent);

		if(this.oControls_[sControlID]) {
			this.oControls_[sControlID].bChanged_ = true;
			this.oControls_[sControlID].node_ = $(sControlID);
		}
		else {
			var found=false;
			for(var k in this.oControls_) {
				if(sControlID.search(k+'_')==0) {
					found=true;
					this.oControls_[k].bChanged_ = true;
					break;
				}
			}
			if(!found) {
				this.oControls_[sControlID] = new Object();
				this.oControls_[sControlID].bChanged_ = true;
				this.oControls_[sControlID].node_ = $(sControlID);
			}
		}
		if(oEvent.Name() == "WM_INIT") oEvent.Stop();

		// TODO: clear changed state when posting to server
	},
	Subscribe: function(sEventName, fCallback, oThisObject) {
//		LogL("Window "+this.LocalID()+" has subscribed to event "+sEventName);
		sEventName = String(sEventName).toUpperCase();
		if (!this.oEventListeners_) this.oEventListeners_ = new Object();

		if (!this.oEventListeners_[sEventName]) {
			this.oEventListeners_[sEventName] = new Array();
		}
		this.oEventListeners_[sEventName].unshift(new AEventHandler(fCallback, oThisObject));
//		LogL("Event handler list length = "+this.oEventListeners_[sEventName].length);
//		for (var i = 0; i < this.oEventListeners_[sEventName].length; ++i) {
//			LogL(" registered callback for "+sEventName+", i="+i+", this="+this.InstanceID());
//		}
	},
	Unsubscribe: function(sEventName, fCallback, oThisObject) {
		sEventName = String(sEventName).toUpperCase();
		if (!this.oEventListeners_) this.oEventListeners_ = new Object();

		if (!this.oEventListeners_[sEventName]) {
			LogE("Event type '"+sEventName+"' is not registered");
			return;
		}

//		LogL("Unregistering handler for '"+sEventName+"'");
		var oHandler = new AEventHandler(fCallback, oThisObject);

		var bDeleted = false;
		for (var i = 0; i < this.oEventListeners_[sEventName].length; ++i) {
			if (this.oEventListeners_[sEventName][i].Compare(oHandler)) {
				this.oEventListeners_[sEventName].splice(i, 1);

//				LogL("Handler found and deleted! Size = " + this.oEventListeners_[sEventName].length);
				bDeleted = true;
				break;
			}
		}
		if (!bDeleted) LogL("WARNING: Handler not found!");
	},
	SaveChanges: function() {
		if (!this.trackChangesAction) return false;
		/*
			Here we assume that window with automated changes tracking
			has SimpleButton or another control with 'DoClick' function that
			performs click and raises event.
		*/
		var rName=this.ControlHTMLID(this.trackChangesAction);
		var ele=$(rName);
		if (!ele) {
			alert("Tracking-changes control "+rName+" was not found in window "+ this.ID());
			return false;
		}
		if (!ele.JSControl) {
			alert("Tracking-changes control "+rName+" is not a kosher control. ");
			return false;
		}
		var jsControl=ele.JSControl;
		if (typeof(jsControl.DoClick) != 'function') {
			alert("Cannot execute CLICK on tracking-changes control "+rName+". ");
			return false;
		}
		jsControl.DoClick();
		return true;
	},
	UpdateControls: function(sRets, oValues) {
		var aChanged=[];
		var aRets = sRets.split(' ');
		for (var i = 0; i < aRets.length; ++i) {
			var x = aRets[i].indexOf('=');
			var rName = (x >= 0 ? aRets[i].substr(0, x) : aRets[i]);
			var rVal = (x >= 0 ? aRets[i].substr(x + 1) : aRets[i]);
			// TODO: rewrite this (bad func used)
			rName=this.ControlHTMLID(rName);
			if (oValues[rVal] === undefined) continue;
			if (!this.oControls_[rName]) {
				LogE("Unknown control: " + rName);
				continue;
			}
			var jsControl=this.oControls_[rName].node_.JSControl;
			jsControl.SetValue(oValues[rVal]);
			// we should change state before emitting events
			// to ensure that any server action will pass all changed controls' values
			this.oControls_[rName].bChanged_=true;
			this.oControls_[rName].node_=$(rName);
			aChanged.push(jsControl);
		}
		// we should first set all values and only then emit events
		for (var i=0; i < aChanged.length; ++i) {
			new AEvent("CHANGE", {}, aChanged[i]);
		}
	},
	RegisterResizeableChild: function(sControlID) {
		if (!this.oResizeableChildren) this.oResizeableChildren = new Object();
		this.oResizeableChildren[sControlID] = true;
		//LogL("Win[$$"+this.ID()+"$$]: Resizeable child registered: "+sControlID);
	},
	OnResize: function(evt) {
		if (!this.HTML) {
			alert('OnResize: Window HTML is not initialized');
			return;
		}
		var eG = AControl.GetBoundingFrame(this.HTML);
		// TODO: remove this popup spike
		if (!eG) eG = this.HTML.up('DIV.popupContents'); // no grid? try popup frame
		var nFullHeight = (eG ? eG.offsetHeight : document.body.clientHeight);

		// resize window grids
		//LogE("OnResize: Win[$$"+this.ID()+"$$]: Resizing window grids...");
		this.ResizeGrids(nFullHeight);
		// Resize children
		if (!this.oResizeableChildren) {
			//LogL("OnResize: No children to resize.");
			return;
		}
		for (var sK in this.oResizeableChildren) {
			var elChild = $(sK);
			if (elChild === null) {
				delete this.oResizeableChildren[sK];
				//LogE("OnResize: Child $$"+sK+"$$ was garbage-collected.");
				continue;
			}
			var jsControl = elChild.JSControl;
			if (jsControl) {
				if (typeof(jsControl.OnResize) == 'function') {
					//LogL("OnResize: Win[$$"+this.ID()+"$$]: Resizing JSControl "+jsControl.ID());
					jsControl.OnResize(evt);
				} else {
					LogE("Warning: Non-kosher control subscribed! No OnResize method! "+jsControl.ID());
				}
			}
			else {
				//LogE("Child "+sK+" does not have JSControl!");
			}
		}
	},
	ResizeGrids: function(nFullHeight, eRoot) {
		//LogE("Win[$$"+this.ID()+"$$]: ResizeGrids...");
		var nHeight = nFullHeight;
		//LogL("ResizeGrids: FullHeight="+nFullHeight);

		//if (!eRoot) this.nRecurseLevel = 0; else this.nRecurseLevel++;
		//LogE("Win[$$"+this.ID()+"$$]: ResizeGrids: Entering level "+this.nRecurseLevel+", nFullHeight="+nFullHeight);
		elWF = (eRoot ? eRoot : this.HTML.parentNode); // window' div or other frame
		//LogL("ROOT ID="+elWF.getAttribute('id'));

		//elWF.setStyle({"height": nHeight+'px'}); // TODO: implement setting window size itself
		var eBF;
		var eBFInner;
		var nInnerFramesHeight = 0;
		var nBFInnerHeight = 0;
		var aFrames=elWF.select('DIV[type="gridFrame"]');
		for(var i=0; i<aFrames.length; ++i) {
			//LogL(" ");
			var eF = aFrames[i];
			if (i == 0) {
				eBF = AControl.GetBoundingFrame(eF); // only once
				if (eBF) {
					// detect inner container
					eBFInner = eBF.firstDescendant(); // TODO: very bad algo (why SCRIPT only?)
					while (eBFInner.nodeName == 'SCRIPT') {
						//LogE("Script tag skipped!");
						eBFInner = eBFInner.next(); // skip script tags
					}
					nBFInnerHeight = eBFInner.offsetHeight; // scrollHeight was here (bad in some cases)
					//LogL("nBFInnerHeight set to "+nBFInnerHeight);
				}
			}
			// since this moment, bounding frame is defined or absent (main window)
			if (AControl.GetBoundingFrame(eF) != eBF) {
				//LogL("C0 ["+i+"]: Current frame is NOT an immediate child! $$"+eF.getAttribute('id')+"$$");
				continue;
			}

			var eW = AControl.GetParentWindowElement(eF); // get frame's window
			//LogL("C1. Frame: "+eF.readAttribute('id')+ ": h="+eF.offsetHeight);
			//LogL("C2. Got window for frame: "+eW.readAttribute('id'));
			if (this.ID() != eW.readAttribute('id')) {
				//LogE("C3. This is not our window, skipping");
				continue; // not our window
			}

			if (eBF) {
				nInnerFramesHeight += eF.offsetHeight; // scrollHeight was here (bad in some cases)
				//LogL("C4. ["+i+"]: nInnerFramesHeight incremented by "+eF.scrollHeight);
			} else {
				//LogE("C4. Bounding frame not found.");
			}

			//LogL("IH="+eF.innerHTML);
			var sHL = eF.readAttribute('height-limit');
			if (sHL == 'stretch') {
				//LogE("C5. This is a STRETCH frame, skipping");
				continue;
			}
			//LogL("C5. This is a HL-type frame");
			var nHL=parseInt(sHL);
			if (nHL) {
				//LogE("C6. HL is SET, skipping");
				continue;
			}
			//LogL("C6. HL is not set");
			//LogL("C7: Added "+eF.readAttribute('id')+ ": h="+eF.offsetHeight);
			nHeight -= eF.offsetHeight;
		}
		// after all calculations
		//LogL("Floating height totally decremented by "+(nBFInnerHeight-nInnerFramesHeight));
		nHeight -= (nBFInnerHeight-nInnerFramesHeight);

		//LogL("ResizeGrids: float Height="+nHeight+", fullHeight="+nFullHeight);
		//LogL("----------------------------------------------------------------");

		for(var i=0; i<aFrames.length; ++i) {
			var eF = aFrames[i];
			//LogL(" ");
			if (AControl.GetBoundingFrame(eF) != eBF) {
				//LogL("S0 ["+i+"]: Current frame is NOT an immediate child! $$"+eF.getAttribute('id')+"$$");
				continue;
			}

			var eW = AControl.GetParentWindowElement(eF); // get frame's window
			//LogL("S1. Frame: "+eF.readAttribute('id')+ ": h="+eF.offsetHeight);
			//LogL("S2. Got window for frame: "+eW.readAttribute('id'));
			if (this.ID() != eW.readAttribute('id')) {
				//LogE("S3. This is not our window, skipping");
				continue; // not our window
			}

			var nHL=parseInt(eF.readAttribute('height-limit'));
			if (!nHL) {
				//LogE("S3. This is not an HL frame, skipping");
				continue;
			}
			var iHeight=Math.floor(nHeight*nHL/100.0);
			//LogL("S3. This is a HL frame, SETTING STYLE! "+iHeight);
			eF.setStyle({"height": iHeight+'px'});
			//LogL("IH="+eF.innerHTML);

			this.ResizeGrids(iHeight, eF); // recurse into subframes

		}
		//LogL("ResizeGrids finished with level "+this.nRecurseLevel);
		//this.nRecurseLevel--;
	},

	// various getters/setters
	InstanceID: function() { return this.sInstanceID_; },
	LocalID: function() { return this.sLocalID_; },
	ID: function() { return this.sID_; }, // ID in HTML
	Title: function() { return this.sTitle_; },
	// obtain child control HTML ID
	ControlHTMLID: function(sID) { return this.sID_ + '_' + sID.replace('.', '_'); },
	Control: function(sID) { return this.oControls_[this.ControlHTMLID(sID)].node_; },
	// obtain parent window instance ID
	Parent: function() { return this.sParent_; },
	XML: function() { return this.oXML_; },
	XMLSource: function() { return this.sXMLSource_; },
	Params: function() { return this.sParams_; }
});

// static event poster
AWindow.postEvent = function(oEvent) {
	var sControlID = oEvent.EmitterID();

	var sCurrentWindowID = AControl.GetParentWindowInstanceID(sControlID);
	var sEventName = oEvent.Name();
	//showObject(GWindowManager.GetWindow(sCurrentWindowID).oEventListeners_);
//	if (sEventName!='INPUT')
//	LogL("AWindow.postEvent: Name=$$"+sEventName+"$$, Emitter=$$"+sControlID+"$$, Parent=$$"+sCurrentWindowID+"$$");
	while (sCurrentWindowID) {
		var oWindow = GWindowManager.GetWindow(sCurrentWindowID);
//		LogL("postEvent():  Event propogated to "+oWindow.ID());
		if (oWindow.oEventListeners_[sEventName]) {
			// there are some handlers
			for (var i = 0; i < oWindow.oEventListeners_[sEventName].length; ++i) {
				// call function
				var oHandler = oWindow.oEventListeners_[sEventName][i];
				if (!oHandler) {
//					LogL("Handler was deleted!");
					continue;
				}
				var fCallback = oHandler.Callback();
				var fCallbackBound = fCallback.bind(oHandler.ThisObject());
				fCallbackBound(oEvent);
//				LogL("AWindow.postEvent: callback done for "+sEventName+", i="+i);
				if (!oEvent.Propogation()) {
//					if (sEventName!='INPUT')
//					LogE("Event $$"+sEventName+"$$ was stopped in handler.");
					// event propogation was stopped
					return;
				}
			}
		}
		// get parent window
		if(oWindow.Parent()==sCurrentWindowID) sCurrentWindowID="";
        else sCurrentWindowID = oWindow.Parent();
	}
	GDocumentInstance.FireEvent(oEvent);
}

var AWindowManager = Class.create({
	initialize: function() {
		this.oWindows_ = Object();
		Event.observe(window, 'resize', this.OnResize.bind(this));
	},
	ID: function() { return 'WindowManager'; },
	Start: function(sEntryPoint) {
		var elWM=new Element('div', { id: this.ID() });
		$(document.body).update();
		document.body.appendChild(elWM);
		if(!sEntryPoint) { // less hardcode
			alert('AWindowManager::Start: No entry point defined. ');
			return;
		}
		this.CreateWindow({sURL: sEntryPoint});
	},

	Focus: function() {
		var eWM=$('wMain');
		if (eWM && eWM.JSControl) eWM.JSControl.Focus();
	},

	StoreActiveElement: function() {
		this.activeElement=document.activeElement ? document.activeElement : null;
	},
	ActiveElement: function() { return this.activeElement; },
	Focused: function() { return this.bFocused; },
	SetTabOrder: function(nTabBase) {
		var eWM=$('wMain');
		if (eWM && eWM.JSControl) eWM.JSControl.SetTabOrder(nTabBase);
		this.bFocused=true;
	},

	ClearTabOrder: function(bFocusedState) {
		var aTabStops=$(document.body).select('*[tabIndex]');
		if (!aTabStops) return;
		for (var i=0; i<aTabStops.length; ++i) {
			aTabStops[i].writeAttribute('tabindex', -1);
		}
		this.bFocused=bFocusedState;
		//LogE("All tabs clearing done, bFocused="+this.bFocused);
	},

	OnResize: function(evt) {
		var eWM=$('wMain');
		if (eWM && eWM.JSControl) eWM.JSControl.OnResize(); // TODO: rewrite in more kosher way
	},

	ObserveResize: function(sElementID) {
		//LogL("WindowManager::ObserveResize $$"+sElementID+"$$.");
		var oWindow = AControl.GetParentWindow(sElementID);
		if (oWindow) oWindow.RegisterResizeableChild(sElementID);
	},

	LoadXSL: function() {
		var prefix = document.location.protocol + '//' +
			document.location.host + SkinPrefix;
		try {
			var t = new AXSLTemplate(SkinPrefix + "main.xsl");
			t.FixXSLInclude(prefix, CustomXsl);
			t.Compile();
			this.oXSLDoc_ = t;
		}
		catch (e) {
			var errText;
			for(i in e) {
				errText+=(i+": "+e[i]+";\n\t");
			}
			alert("Failed to load \"" + SkinPrefix + "main.xsl\":\n\t"+errText);

			LogE("Failed to load \"" + SkinPrefix + "main.xsl\": "+errText);
			return null;
		}

	},
	// retrieve window by window instance ID (this)
	GetWindow: function(sThis) {
		if (!this.oWindows_[sThis]) ThrowError("GetWindow: Window '"+sThis+"' not registered. ");
		return this.oWindows_[sThis];
	},
	// delete window by instance ID (this)
	DeleteWindow: function(sThis) {
		LogL("Deleting window "+sThis);
		if (!this.oWindows_[sThis]) {
			alert("DEBUG: AWindowManager::DeleteWindow(): Window with InstanceID = '"+sThis+"' not found. ");
		}
		// TODO: kill children
		// this.oWindows_[sThis].DeleteChildren();
		delete this.oWindows_[sThis]; // anything else? unregister handlers, etc???
	},
	CheckChanges: function(oParams) {
		if (!oParams.bCheckChanges || !oParams.sParent) return true; // don't care about changes
		var parent = GWindowManager.GetWindow(oParams.sParent);
		var modCh=parent.hasModifiedChildren();
		if (!modCh) return true; // nothing changed
		var modWin=this.GetWindow(modCh);
		if (!modWin) return true; // should not happen
		if (modWin.SaveChanges()) return true; // don't ask when auto-save is turned on
		return (confirm("Страница содержит несохранённые данные. \nВы уверены, что хотите покинуть страницу? ") ? true : false);
	},
	/**
	  * Creates new window. oParams can contain following fields:
	  * @sURL: URL of XML window source
	  *  or
	  * @oXML: XML object containing source
	  * @sParent: Parent window instance ID
	  * @sControlID: Control identifier to embed in (or anchor to it)
	  * @sParams: Window initialization parameters
	  **/
	CreateWindow: function(oParams) {
		if (!this.CheckChanges(oParams)) return;
		return new AWindow(oParams);
	},
	/**
	  * Creates new window. oParams can contain following fields:
	  * @sURL: URL of XML window source
	  *  or
	  * @oXML: XML object containing source
	  * @sParent: Parent window instance ID
	  * @sControlID: Control identifier to embed in (or anchor to it)
	  * @sInstanceID: Window instance ID (it is a refreshing of an existing window)
	  * @sParams: Window initialization parameters
	  **/
	RefreshWindow: function(oParams) {
		if (!this.CheckChanges(oParams)) return;
		return new AWindow(oParams);
	},
	registerWindow: function(sInstanceID, oWindow) {
		this.oWindows_[sInstanceID] = oWindow;
	},
	// TODO: eliminate second parameter, it's a spike
	Transform: function (xXMLDoc, bReturnTree) {
		var xXMLRoot = xXMLDoc.documentElement;
		try {
			var xsl = this.XSL();
			if (!xsl) return;
//			LogL((new XMLSerializer()).serializeToString(xXMLDoc));

			if (bReturnTree) return AXML.TransformDOMTree(xXMLDoc, xsl);
			else return AXML.TransformDOM(xXMLDoc, xsl);
		} catch (err) {
			alert("AWindow::Transform(): XSL transformation failed! " + err.description + ", "+err.message);
			return "";
		}
	},
	SetAddress: function(src, args) {
		if (args && args.length > 0) args = '?' + args;
		while (args && args.length > 0 && args[args.length - 1] == '&') {
			args = args.substr(0, args.length - 1);
		}
		if (Prototype.Browser.IE) {
			loc = window.location;
			if (loc.protocol.length+loc.host.length+loc.pathname.length+src.length+args.length+3>350) {
				// maximum buffer length is 2083 chars (non-unicode) for IE
				args="";
			}
		}
		var upd = function() {
			window.location.hash = '#' + src + args;
		};
		upd.defer();
	},
	XSL: function() {
		if (!this.oXSLDoc_) this.LoadXSL();
		if (!this.oXSLDoc_) return;

		return this.oXSLDoc_;
	},
	OnUnload: function(evt) {
		for(var win in this.oWindows_) {
			if(this.oWindows_[win].IsModified) return "Страница содержит несохранённые данные. ";
		}
	}
});

// Document-wide DOM event manager
var DocumentEventManager = Class.create({
	initialize: function() {
		this.oSubscribers = {
			'KeyUp': [],
			'KeyDown': [],
			'KeyPressDown': []
		};
		$(document).observe('keyup', this.processEvent.bind(this, 'KeyUp'));
		$(document).observe('keydown', this.processEvent.bind(this, 'KeyDown'));
		BindKeyDown(document, this.processEvent.bind(this, 'KeyPressDown'));
	},
	processEvent: function(sType, event) {
		var eKD = GetKeyCode(event);
		var evt = Event.extend(event || window.event);
		var aSubscribers=this.oSubscribers[sType];
		// find nearest subscriber related to
		var emitter=evt.element();
		var newSubscribers=[];
		for (var i=0; i<aSubscribers.length; ++i) {
			// typically this array will be rather short (1 or 0 elements),
			// so loop won't hang everything
			var subs=aSubscribers[i];
			var elControl=$(subs);
			if (!elControl || !elControl.JSControl) continue;
			newSubscribers.push(subs);
		}
		this.oSubscribers[sType]=newSubscribers;
		var s=this.findSubscriber(emitter, newSubscribers);
		if (!s) return;
		var jsControl=s.JSControl;
		var fHandler=jsControl['On'+sType].bind(jsControl);
		if (fHandler(evt)) {
			StopEvent(evt);
			//LogL("Key handling was stopped");
		}
	},
	Subscribe: function(sType, sControlID) {
		if (!this.oSubscribers[sType]) {
			alert("DocumentEventManager: Event type "+sType+" is unknown, please verify your code. ");
			return;
		}
		var aSubscribers=this.oSubscribers[sType];
		for (var i=0; i<aSubscribers.length; ++i) {
			var subs=aSubscribers[i];
			if (subs==sControlID) return; // do not subscribe twice
		}
		aSubscribers.push(sControlID);
	},
	isLeftSibling: function(left, right) {
		if (!right && !left) return true;
		if (!left) return false; // right is defined
		if (!right) return true; // left is defined
		if (left==right) return true;
		var n=left;
		for (var n=left; n; n=n.nextSibling) if (n==right) return true;
		return false;
	},
	findSubscriber: function(emitter, aSubscribers) {
		var emAnc=[];
		for (var e=emitter; e; e=e.parentNode) emAnc.unshift(e);
		
		// fill paths for each subscriber
		var aPaths=[];
		for (var i=0;i<aSubscribers.length; ++i) {
			var aPath=[];
			for (var e=$(aSubscribers[i]); e; e=e.parentNode) aPath.unshift(e);
			aPaths[i]=aPath;
		}
		//LogE("========================================================");
		var lastMatchLevel=-1;
		var lastMatchIndex=-1;
		for (var i=0;i<emAnc.length;++i) {
			var n=emAnc[i];
			//LogE("processing node "+n.nodeName);
			var matchIndex=-1;
			//if (n.getAttribute) LogL("n.id="+n.getAttribute('id'));
			for (var si=0; si<aPaths.length; ++si) {
				var aPath=aPaths[si];
				if (aPath.length<=i) {
					//LogE("path ["+si+"] is less than index "+i);
					continue; // something strange
				}
				//LogL("path ["+si+"]: "+aPath[i].nodeName);
				//if (aPath[i].getAttribute) LogL("id="+aPath[i].getAttribute('id'));
				if (aPath[i]==n) {
					matchIndex=i; // set match index
					lastMatchIndex=si;
					//LogE("Set for ["+si+"] match index="+i);
				}
			}
			if (matchIndex>=0) {
				//LogL("set lastMatchLevel=$$"+i+"$$");
				lastMatchLevel=i;
			} else {
				//LogE("PATHS differ at "+i);
				break;
			}
		}
		//LogE("lastMatchIndex="+lastMatchIndex+", lastMatchLevel="+lastMatchLevel);
		if (lastMatchIndex<0) return null; // no luck today

		//LogE("Resolving path conflict...");
		// conflict solving
		var n=emAnc[lastMatchLevel]; // last node when at least one path crossed the emitter path
		var d=lastMatchLevel;
		var aNewPaths=[];
		for (;;) {
			// exclude all paths that are not crossing last match node
			//if (n.getAttribute) LogL("n.id="+n.getAttribute('id'));
			for (var si=0; si<aPaths.length; ++si) {
				var aPath=aPaths[si];
				if (aPath.length<=d) {
					//LogE("path ["+si+"] is less than current depth $$"+d+"$$");
					continue; // exclude this path
				}
				//LogL("path ["+si+"]: "+aPath[d].nodeName);
				//if (aPath[d].getAttribute) LogL("id="+aPath[d].getAttribute('id'));
				
				if (aPath[d]==n) {
					aNewPaths.push(aPath);
				}
			}
			if (!aNewPaths.length) {
				// TODO: fix this
				//LogE("No way to go!!!");
			}
			++d; // step to n' children
			var min=null;
			for (var si=0; si<aNewPaths.length; ++si) {
				var aPath=aNewPaths[si];
				if (aPath.length<=d) {
					//LogE("path ["+si+"] is less than current depth $$"+d+"$$");
					continue; // skip this path
				}
				if (this.isLeftSibling(aPath[d], min)) {
					//LogE("switching to found new min in path ["+si+"]...");
					min=aPath[d];
					minPathIndex=si;
				}
			}
			if (min) {
				n=min; // change current node
				//if (n.getAttribute) LogL("changed min: n.id="+n.getAttribute('id'));
			} else {
				//LogE("no min found!!!");
				break;
			}
		}
		return n;
	}
}); // DocumentEventManager

var GDocumentEventManager=new DocumentEventManager();

var DocumentInstance = Class.create({
	oEventListeners_: {},
	initialize: function(){},

	Subscribe: function(sEventName, fCallback, oThisObject) {
		sEventName = String(sEventName).toUpperCase();
		if (typeof(this.oEventListeners_[sEventName]) == "undefined") this.oEventListeners_[sEventName] = new Array();
		this.oEventListeners_[sEventName].unshift(new AEventHandler(fCallback, oThisObject));
	},

	Unsubscribe: function(sEventName, fCallback, oThisObject) {
		sEventName = String(sEventName).toUpperCase();
		if (!this.oEventListeners_[sEventName]) {
			LogE("Event type '"+sEventName+"' is not registered");
			return;
		}

		var oHandler = new AEventHandler(fCallback, oThisObject);

		var bDeleted = false;
		for (var i = 0; i < this.oEventListeners_[sEventName].length; ++i) {
			if (this.oEventListeners_[sEventName][i].Compare(oHandler)) {
				this.oEventListeners_[sEventName].splice(i, 1);
				bDeleted = true;
				break;
			}
		}
		if (!bDeleted) LogL("WARNING: Handler not found!");
	},

	FireEvent: function(oEvent) {
		var sEventName=oEvent.Name();
		if (typeof(this.oEventListeners_[sEventName])=="undefined") return;
		var aListeners=this.oEventListeners_[sEventName];
		for (var i=0; i < aListeners.length; ++i) {
			var oHandler=aListeners[i];
			var fCallback=oHandler.Callback();
			var fCallbackBound=fCallback.bind(oHandler.ThisObject());
			fCallbackBound(oEvent);
		}
	}
});

var LabeledGrid = Class.create({
initialize: function(htmlid) {
	this.id = htmlid;
	this.frame = $(htmlid);
	this.collapse = $(htmlid + '_collapse');
	this.contents = $(htmlid + '_contents');
	this.label = $(htmlid + '_label');

	this.collapsed = false;
	if (this.frame && this.collapse && this.frame.readAttribute('collapsed')) {
		this.OnCollapse();
	}

	if (this.collapse) this.collapse.observe('click', this.OnCollapse.bind(this));
},
OnCollapse: function(evt) {
	if (!this.collapsed) {
		this.contents.hide();
		this.collapse.addClassName('LabeledGridExpand');
		this.label.addClassName('LabeledGridCollapsed');
		this.collapsed = true;
	}
	else {
		this.contents.show();
		this.collapse.removeClassName('LabeledGridExpand');
		this.label.removeClassName('LabeledGridCollapsed');
		this.collapsed = false;
	}
	if (evt) evt.stop();
}
});

////////////////////////////////////////////////////////
/// Global Definitions
////////////////////////////////////////////////////////

var GWindowManager = new AWindowManager();
var GDocumentInstance = new DocumentInstance();
window.onbeforeunload = GWindowManager.OnUnload.bind(GWindowManager);


