/**
 * base package for fastrack applications
 */
com_starsensortech_fastrack_model = new es_lang.Package(true,
	/*Title*/   'Fastrack Processing',
	/*Docs */   '-- none --',
	/*Package*/ function() {
		// Imports
		var LANG = es_lang;
		var NET = es_net;
		var LOG = es_util_logging;
		var XS = com_pagasg_xxapp_serialize;
		var WU = com_pagasg_www_util;

		var Class = es_lang.Class;
		var Enum = es_lang.Enum;
		var Thread = es_lang.Thread;
		var StringBuilder = LANG.StringBuilder;
		var URLConnection = NET.URLConnection;
		var Container = WU.Container;
		var Objects = WU.Objects;
		var EventManager = WU.EventManager
		var Event = WU.Event;
		var Strings = WU.Strings;
		var QName = XS.QName;

		var extend = Class.extend;
		var implement = Class.implement;
		var enumerate = Enum.enumerate;
		var argsOrArray2Array = Objects.argsOrArray2Array;
		var display = Objects.display;
		var getProperties = Objects.getProperties;
		var testEmpty = Strings.testEmpty;
		var testDefault = Strings.testDefault;

		// ****** Namespaces ******
		var NS = {
			xsi: "http://www.w3.org/2001/XMLSchema-instance",
			pr: "http://xcenetic.org/2009/prototype",
			am: 'http://atms.starsensortech.com/2009/model',
			ar: 'http://atms.starsensortech.com/2009/report',
			ac: 'http://atms.starsensortech.com/2009/control',
			fm: 'http://fastrack.starsensortech.com/2009/model'
		};

		// ****** User ******
		var User = implement({
				username: undefined,
				title: undefined
			},
			function(username, title) {
				if (arguments.length > 0) {
					this.username = username;
					this.title = title;

					this.toString = function() {
						return title + ' (' + username + ')';
					}
				}
			}
		);

		// ****** Contact ******
		var Contact = function(xml) {
			var _eventMgr = new WU.EventManager(WU.Event, this, Contact.Event.items);

			function getLogger() {
				return LOG.Logger.getLogger('com.starsensortech.www.atms.fastrack.Contact');
			}

			this.xml = xml;
		}
		Contact.Event = enumerate([ 'onUpdate' ]);
		Contact.getAddressline = function(contactXml) {
			var str;
			var comma = '';
			var bldr = new StringBuilder();

			if (contactXml) {
				if (str = testEmpty(contactXml.getChild('country'))) {
					bldr.append(str);
					comma = ', ';
				}
				if (str = testEmpty(contactXml.getChild('postal-code'))) {
					if (comma) bldr.insert(0, comma);
					bldr.insert(0, str);
					comma = '  ';
				}
				if (str = testEmpty(contactXml.getChild('region'))) {
					if (comma) bldr.insert(0, comma);
					bldr.insert(0, str);
					comma = ', ';
				}
				if (str = testEmpty(contactXml.getChild('city'))) {
					if (comma) bldr.insert(0, comma);
					bldr.insert(0, str);
					comma = ', ';
				}
				if (str = testEmpty(contactXml.getChild('street1'))) {
					if (comma) bldr.insert(0, comma);
					bldr.insert(0, str);
				}
			}

			return bldr.toString();
		}

		// ****** Account ******
		var Account = function(xml) {
			var _eventMgr = new WU.EventManager(WU.Event, this, Account.Event.items);

			function getLogger() {
				return LOG.Logger.getLogger('com.starsensortech.fastrack.model.Account');
			}

			this.xml = xml;
		}

		// ****** Notifier ******
		var Notifier = function(xml) {
			var _eventMgr = new WU.EventManager(WU.Event, this, Notifier.Event.items);

			function getLogger() {
				return LOG.Logger.getLogger('com.starsensortech.fastrack.model.Notifier');
			}

			this.xml = xml;
		}
		Notifier.Event = enumerate([ 'onUpdate' ]);

		// ****** Region ******
		var Region = function(xml) {
			var _eventMgr = new WU.EventManager(WU.Event, this, Region.Event.items);

			function getLogger() {
				return LOG.Logger.getLogger('com.starsensortech.fastrack.model.Region');
			}

			this.xml = xml;
		}
		Region.Event = enumerate([ 'onUpdate' ]);


		// ****** Hierarchy ******
		var Hierarchy = function(builder) {
			var _relatives = new Container();
			var _serializer = new XS.Serializer();
			var _eventMgr = new WU.EventManager(WU.Event, this, Hierarchy.Event.items);

			function getLogger() {
				return LOG.Logger.getLogger('com.starsensortech.fastrack.model.Hierarchy');
			}
			function getExternalId(xml) {
				return (xml) ? xml.getAttribute('external-id', NS.am) : null;
			}

			this.xml = (builder) ? builder(_serializer.deserialize) : undefined;
			this.externalId = getExternalId(this.xml);
			this.setXml = function(xml) {
				this.xml = xml;
				this.externalId = getExternalId(xml);
			}
			this.setRelatives = function(relationship, externalIds) {
				_relatedAccounts.put(relationship, externalIds || []);
			}
			this.getRelativevs = function(relationship) {
				return _relatives.get(relationship);
			}

		};
		Hierarchy.Event = enumerate([ 'onUpdate' ]);

		// ****** Group ******
		var Group = function(builder) {
			var _selections = {};
			var _eventMgr = new WU.EventManager(WU.Event, this, Group.Event.items);

			// Public
			this.update = function(props) {
				var value;
				var changed = {};

				for (var p in props) {
					if (props.hasOwnProperty(p)) {
						value = props[p];
						if (this[p] != value) {
							this[p] = value;
							changed[p] = value;
						}
					}
				}

				if (changed.length > 0) {
					this.onUpdate.fire(changed);
				}
			};
			this.select = function(selectionType) {
				var type = selectionType || '';
				var selected = _selections[type];

				if (!selected) {
					_selections[type] = true;
					this.onSelect.fire(type);
				}
			};
			this.deselect = function(selectionType) {
				var type = selectionType || '';
				var selected = _selections[type];

				if (selected == undefined || selected) {
					_selections[type] = false;
					this.onDeselect.fire(type);
				}
			};
			this.isSelected = function(type) {
				var selectType = testDefault(type);

				return (_selections[selectType]) ? true : false;
			};
		};
		Group.Event = enumerate([ 'onUpdate', 'onSelect', 'onDeselect' ]);

		/**
		 * An action is an operation that can be applied to an object.
		 */
		var Action = implement({
				name: /*QName*/ undefined,
				uri: /*URI*/ undefined,
				parameters: /*Map*/ undefined,
				test: /*boolean*/ function(/*Obj*/operand) {},
				initTimer: function(/*Timer*/timer) {}
			},
			function(name, uri, parameters, test) {
				if (arguments.length > 0) {
					this.name = name;
					this.uri = (uri) ? uri : name.namespaceUri + '#' + name.localName;
					this.test = test;
					this.parameters = parameters || {};
					this.toString = function() { return uri; }
				}
			}
		);

		/**
		 * Unit container
		 *
		 * Timer states:
		 * >  0: not yet started
		 * >  1: started
		 * >  2: successful completion
		 * > -1: timed out
		 * > -2: canceled
		 */
		var Unit = function(externalId) {
			var _shape;
			var _xml;
			var _refreshOn = 0;
			var _timers = {};
			var _updatedOn = 0;
			var _selections = {};
			var _externalId = externalId;
			var _eventMgr = new WU.EventManager(WU.Event, this, Unit.Event.items);
			var _this = this;

			function getLogger() {
				return LOG.Logger.getLogger('com.starsensortech.fastrack.model.Unit');
			}

			var isSelected = function(name) {
				return (_selections[testDefault(name)]) ? true : false;
			};
			var checkWaitStart = function(type, action) {
				var changed;
				var name = action.name;
				var now = (new Date()).getTime();
				var timer = _timers[name];

				// Moderator instead of timer???
				if (!timer) {
					timer = { types: {}
						, action: action
						, start: now
						, restart: now
						, timeout: 0
						, maxRetries: 0
						, status: 0
						, retries: 0
						, inprocess: false
					};
					_timers[name] = timer;
				} else {
					getLogger().info('Action ' + ((action.name) ? action.name : '[default]') + ' is already in progress for: ' + _externalId);
				}

				if (timer.types.hasOwnProperty(type)) {
					timer = null;
				} else {
					timer.types[type] = type;
				}

				return timer;
			};
			var checkWaitEnd = function(timer) {
				var test;
				var action;
				var clear = false;
				var now = (new Date()).getTime();

				if (timer) {
					action = timer.action;
					test = (action) ? action.test : undefined;

					if (test == undefined || test(_this)) {
						clear = true;
						timer.status = 2;
					} else if (timer.timeout > 0 && now > timer.restart + timer.timeout) {
						//getLogger().info('Retry timer:\r\n' + Objects.display(timer, 0));
						if (timer.retries < timer.maxRetries) {
							timer.restart = timer.restart + timer.timeout;
							timer.retries++;
						} else if (timer.maxRetries >= 0) {
							//getLogger().info('Unit: ' + _externalId + ' timed out, with ' + timer.retries + ' retries');
							timer.status = -1;
							clear = true;
						}
					} else {
						//getLogger().info('Never die?:\r\n' + Objects.display(timer, 1));
					}
				}

				return clear;
			};

			// Public
			this.refreshOn = _refreshOn;
			this.display = _externalId;
			this.externalId = _externalId;
			this.updatedOn = 0;
			this.locatedOn = 0;
			this.reportedOn = 0;
			this.clearUptodate = function() {
				_refreshOn = this.refreshOn = 0;
			};
			this.isUptodate = function() {
				var now = (new Date()).getTime();
				return now < _refreshOn;
			};
			this.checkTimers = function(now) {
				var me = this;
				var timers = {};
				var waitTypes = {};

				getProperties(_timers, function(name, timer) {
					var type;
					var retries = timer.retries;
					var ending = checkWaitEnd(timer);

					if (!ending) {
						timers[name] = timer;
					}

					getProperties(timer.types, function(type) {
						if (!waitTypes[type]) {
							waitTypes[type] = { count: 0, success: true };
						}
						if (!ending) {
							waitTypes[type].count++;
						}
						waitTypes[type].success = waitTypes[type].success && (timer.status == 2);
					});

					if (timer.status == -1) {
						getLogger().info('Unit: ' + _externalId + ' timed out, with ' + timer.retries + ' retries');
						me.onWaitEnd.fire(timer);
					} else if (timer.status == 2) {
						me.onWaitEnd.fire(timer);
					} else if (timer.retries > 0) {
						if (timer.retries > retries && !timer.inprocess) {
							me.onRetry.fire(timer);
						}
					}
				});
				_timers = timers;

				getProperties(waitTypes, function(type, waitType) {
					if (waitType.count == 0) {
						if (waitType.success) {
							_selections[type] = true;
							me.onSelect.fire(type);
						} else {
							delete _selections[type];
							me.onDeselect.fire(type);
						}
					}
				});
			};
			this.getTimers = function(filter) {
				return getProperties(_timers, filter);
			};
			this.getTimer = function(type) {
				return _timers[Strings.testDefault(type)];
			};
			this.setLastReport = function(reportXml) {
				var latitude;
				var longitude;
				var utc;
				var updated = false;

				if (reportXml) {
					utc = reportXml.getChild('utc');
					utc = parseInt(testDefault(utc, '0'));

					if (this.locatedOn < utc) {
						latitude = parseFloat(testEmpty(reportXml.getChild('latitude')));
						longitude = parseFloat(testEmpty(reportXml.getChild('longitude')));

						if (latitude && longitude) {
							this.locatedOn = utc;
							this.locationXml = reportXml;
							this.onNewLocation.fire(reportXml);
							updated = true;
						}

						if (this.reportedOn < utc) {
							this.reportedOn = utc;
							this.reportXml = reportXml;
							this.onNewReport.fire(reportXml);
							updated = true;
						}
					}
				}

				return updated;
			};
			this.setProfile = function(profileXml) {
				var prop;
				var props;

				if (profileXml) {
					this.externalId = _externalId = profileXml.getAttribute('external-ref', NS.am);
					
					props = profileXml.getChildren();
					for (var i = 0; i < props.length; i++) {
						prop = props[i];
						this[prop.localName] = prop.text;
					}
				}
			};
			this.select = function(type, action) {
				var timer;
				var selectType = testDefault(type);
				var selected = _selections[selectType];
				
				// TODO: This is a kludge, by default there should be no action
				if (arguments.length == 1) {
					action = new Unit.Refresh();
				}

				if (action && action.test) {
					if (action.test(this)) {
						_selections[selectType] = true;
						this.onSelect.fire(selectType);
					} else {
						timer = checkWaitStart(selectType, action);
						if (timer != null) {
							action.initTimer(timer);
							if (timer.status == 0) {
								timer.status = 1;
								this.onWaitStart.fire(timer);
							}
						}
					}
				} else if (!selected) {
					_selections[selectType] = true;
					this.onSelect.fire(selectType);
				} else {
					// Do nothing???
				}
			};
			this.deselect = function(type) {
				var timer;
				var timers = {};
				var selectType = testDefault(type);
				var selected = _selections[selectType];

				getProperties(_timers, function(name, timer) {
					getProperties(timer.types, function(type) {
						if (type == selectType) {
							if (timer.state == 1) {
								timer.state = -2;		// canceled
								this.onWaitEnd.fire(timer);
							}
						} else {
							timers[name] = timer;
						}
					});
				});
				_timers = timers;

				if (selected == undefined || selected) {
					delete _selections[selectType];
					this.onDeselect.fire(selectType);
				}
			};
			this.clearSelections = function() {
				var me = this;
				var timers = _timers;
				var selections = _selections;

				_timers = {};
				_selections = {};

				getProperties(timers, function(name, timer) {
					getProperties(timer.types, function(type) {
						if (timer.state == 1) {
							timer.state = -2;		// canceled
							me.onWaitEnd.fire(timer);
						}
					});
				});

				getProperties(selections, function(type) {
					//alert('Deselecting: ' + me.display + ' for: ' + type);
					me.onDeselect.fire(type);
				});

				//alert('cleared out the selections');
			};
			this.isSelected = isSelected;
			this.refresh = function(unitXml, reportXml, locationXml) {
				var externalId;

				if (unitXml) {
					externalId = unitXml.getAttribute('external-id', NS.am);
					if (externalId == _externalId) {
						this.xml = _xml = unitXml;
						this.updatedOn = _updatedOn = new Date().getTime();
						this.refreshOn = _refreshOn = (new Date()).getTime() + Unit.REFRESH_INTERVAL;
						this.display = testDefault(_xml.getAttribute('display', NS.am), this.display);
					} else {
						throw new Error("IllegalArgumentException :: supplied external id: " + this.externalId + " does not match: " + _externalId);
					}
				}
				if (locationXml) {
					this.setLastReport(locationXml);
				}
				if (reportXml) {
					this.setLastReport(reportXml);
				}

				this.onRefresh.fire();
			};
			this.update = function(unitXml) {
				var externalId;
				this.xml = _xml = unitXml;
				this.updatedOn = _updatedOn = new Date().getTime();

				if (_xml) {
					externalId = _xml.getAttribute('external-id', NS.am);
					if (_externalId) {
						if (this.externalId != _externalId) {
							throw new Error("IllegalArgumentException :: supplied external id: " + this.externalId + " does not match: " + _externalId);
						}
					} else {
						this.externalId = _externalId = externalId;
					}
					this.display = _xml.getAttribute('display', NS.am);
				}

				if (unitXml.getChild('report')) {
					this.setLastReport(unitXml.getChild('report'));
				} else if (unitXml.getChild('event-profile')) {
					this.setLastReport(unitXml.getChild('event-profile'));
				}

				_refreshOn = this.refreshOn = (new Date()).getTime() + Unit.REFRESH_INTERVAL;
				this.onUpdate.fire();
			};
		};
		Unit.Event = enumerate([ 'onUpdate', 'onRefresh', 'onWaitStart', 'onRetry', 'onWaitEnd', 'onNewLocation', 'onNewReport', 'onSelect', 'onDeselect' ]);
		Unit.REFRESH_INTERVAL = 600000;				// default refresh interval -- every 10 minutes

		// Unit Actions ...
		Unit.SetStarterDisable = extend(Action,
			function(enabled, callback) {
				var _lastReported = -1;
				var _enabled = (enabled) ? true : false;

				Action.call(this
					, new XS.QName('unit-set-starter-disable', NS.fm)
					, NS.fm + '#unit-set-starter-disable'
					, { enabled: _enabled });

				this.initTimer = function(timer) {
					timer.timeout = 10000;
					timer.maxRetries = 6;
				}

				this.test = function(unit) {
					var ok = false;
					
					if (_lastReported < 0) {
						_lastReported = unit.reportedOn;
					} else {
						ok = (unit.reportedOn > _lastReported);
					}

					if (ok && callback) callback(unit);
					return ok;
				}
			}
		);
		Unit.SetSpeedThreshold = extend(Action,
			function(enabled, upperLimit, lowerLimit, hysteresis, callback) {
				var _lowerLimit;
				var _upperLimit;
				var _parameters;
				var _hysteresis = hysteresis || 0;
				var _lastReported = -1;
				var _enabled = (enabled) ? true : false;

				if (lowerLimit) {
					_lowerLimit = parseInt(lowerLimit)
					if (isNaN(_lowerLimit)) {
						_lowerLimit = 0;
					}
				} else {
					_lowerLimit = 0;
				}
				
				if (upperLimit) {
					_upperLimit = parseInt(upperLimit)
					if (isNaN(_upperLimit)) {
						_upperLimit = 0;
					}
				} else {
					_upperLimit = 0;
				}

				_parameters = { enabled: _enabled };
				if (_upperLimit >= 0) _parameters['upper-limit'] = _upperLimit;
				if (_lowerLimit >= 0) _parameters['lower-limit'] = _lowerLimit;
				if (_hysteresis > 0) _parameters['hysteresis'] = _hysteresis;

				Action.call(this
						, new XS.QName('unit-set-speed-threshold', NS.fm)
						, NS.fm + '#unit-set-speed-threshold'
						, _parameters);

				this.initTimer = function(timer) {
					timer.timeout = 10000;
					timer.maxRetries = 12;
				}
				this.test = function(unit) {
					var ok = false;
					
					if (_lastReported < 0) {
						_lastReported = unit.reportedOn;
					} else {
						ok = unit.reportedOn > _lastReported;
					}

					if (ok && callback) callback(unit);
					return ok;
				}
			}
		);
		Unit.Locate = extend(Action,
			function(callback) {
				var _timer;
				var _lastLocated = -1;
				var _name = new XS.QName('unit-locate', NS.fm);

				Action.call(this
						, _name
						, NS.fm + '#unit-set-speed-threshold');

				this.initTimer = function(timer) {
					timer.timeout = 4000;
					timer.maxRetries = 25;
				}
				this.test = function(unit) {
					var ok = false;
					
					if (_lastLocated < 0) {
						// Initialize
						_lastLocated = unit.locatedOn;
					} else {
						ok = unit.locatedOn > _lastLocated;
					}

					if (ok && callback) callback(unit);
					return ok;
				}
			}
		);
		Unit.Refresh = extend(Action,
			function(callback) {
				Action.call(this, new XS.QName('unit-refresh', NS.fm));

				this.test = function(unit) {
					var ok = unit.isUptodate();
					if (ok && callback) callback(unit);
					return ok;
				}
			}
		);

		// === PACKAGE PUBLIC ===
		this.User = User;
		this.Contact = Contact;
		this.Account = Account;
		this.Region = Region;
		this.Notifier = Notifier;
		this.Action = Action;
		this.Unit = Unit;
		this.Group = Group;
		this.NS = NS;
		// === PACKAGE PUBLIC ===
	}
);




















