argos.require("utils");
argos.check("services");

argos.services.datetime = new (function() {
	// Creates a datetime object for tracking time (initialised by servertime or local now()).
	// Includes related and unrelated time/date utility functions.

	var _time = this;
	var _interval = 1000;
	var _now = null;

	var _helpers = {
		getType : argos.utils.getType
	}

	// Check if (DateTime)d matches current dd/mm/yyyy.
	function _isToday(d) {
		return _sameAsCurrentYear(d) && _sameAsCurrentMonth(d) && _sameAsCurrentDate(d);
	}

	// Check if (DateTime)d matches yesterday.
	function _isYesterday(d) {
		var today = _now.getDate();
		var date = _helpers.getType(d) == "Date" ? d.getDate() : d;
		return _sameAsCurrentYear(d) && _sameAsCurrentMonth(d) && (date == today - 1 || today == 1 && date == _lastDayOfMonth(_now.getMonth() - 1));
	}

	// Check if (DateTime)d matches tomorrow.
	function _isTomorrow(d) {
		var today = _now.getDate();
		var date = _helpers.getType(d) == "Date" ? d.getDate() : d;
		return _sameAsCurrentYear(d) && _sameAsCurrentMonth(d) && (date == today + 1 || date == 1 && today == _lastDayOfMonth(_now.getMonth())); 
	}

	// Compare current date to passed date object or (int)date. 
	function _sameAsCurrentDate(d) {
		var date = _helpers.getType(d) == "Date" ? d.getDate() : Number(d);
		return date == _now.getDate();
	}		

	// Compare current month to passed date object or (int)month.
	function _sameAsCurrentMonth(d) {
		var month = _helpers.getType(d) == "Date" ? d.getMonth() : Number(d);
		return month == _now.getMonth();
	}

	// Compare current year to passed date object or (int)year.
	function _sameAsCurrentYear(d) {
		var year = _helpers.getType(d) == "Date" ? d.getFullYear() : Number(d);
		return year == _now.getFullYear();
	}

	// Get the last day of month from passed date object or (int) month;
	function _lastDayOfMonth(d) {
		var month = _helpers.getType(d) == "Date" ? d.getMonth() : Number(d);
		var day;

		switch(month) {
			case  4 : // fall through.
			case  6 : // fall through.
			case 10 : // fall through.
			case 11 : day = 30; break;
			case  2 : day = _isLeapYear() ? 29 : 28; break;
			default : // assume it's one of the other months.
					// This also helps when going into the next or previous
					// year, where _now.getMonth() +/- 1, might be 13 or 0.
					day = 31;
		} 
		return day;			  
	}

	// Take a number and convert it into (String)day of the week (e.g. Date.getDay() return 0 to 6)
	// (int)limit allows truncation of returned string. 
	function _dayAsString(day, limit) {
		var str;
		switch(day) {
			case 6: str = "Saturday"; break;
			case 5: str = "Friday";	break;
			case 4: str = "Thursday"; break;
			case 3: str = "Wednesday"; break;
			case 2: str = "Tuesday"; break;
			case 1: str = "Monday"; break;
			default: str = "Sunday";	
		} 
		return arguments.length > 1 ? str.substring(0, limit) : str; 
	}

	// Take a number and convert it into (String)month (e.g. Date.getMonth() return 0 to 11)
	// (int)limit allows truncation of returned string. 
	function _monthAsString(month, limit) {
		var str;
		switch(Number(month)) {
			case  1 : str = "February"; break;
			case  2 : str = "March"; break;
			case  3 : str = "April"; break;
			case  4 : str = "May"; break;
			case  5 : str = "June"; break;
			case  6 : str = "July"; break;
			case  7 : str = "August"; break;
			case  8 : str = "September"; break;
			case  9 : str = "October"; break;
			case 10 : str = "November"; break;
			case 11 : str = "December"; break;
			default : str = "January";
		}
		return arguments.length > 1 ? str.substring(0, limit) : str;
	}

	function _yyyymmdd(date) {
		// return integer value created from extracted yyyymmdd @date value.
		var year = String(date.getFullYear());
		var month = date.getMonth() + 1;
		var day = date.getDate();
		month = month < 10 ? "0" + String(month) : String(month);
		day = day < 10 ? "0" + String(day) : String(day);
		return Number(year + month + day);
	}

	function _hhmmss(hours, minutes, seconds) {
		// return integer value created from extracted @hours and @minutes concatenated as String.
		hours = String(hours);
		minutes = minutes < 10 ? "0" + String(minutes) : String(minutes);
		seconds = seconds < 10 ? "0" + String(seconds) : String(seconds); 
		return Number(hours + minutes + seconds);
	}
 
	function _from12To24Hour(str) {
		// Takes str as 1am / 1:30pm / 12:34am etc. and returns 01:00 / 13:30 / 00:34 etc.
		var splitStr = str.toLowerCase().replace(/(\d{1,2}):(\d{2})(\w{2})/, "$1,$2,$3").split(",");
		var hours = splitStr.length > 0 ? splitStr[0] : "";
		var minutes = splitStr.length > 1 ? splitStr[1] : "";
		var ampm = splitStr.length > 2 ? splitStr[2] : "";
		if(ampm == "am") { 
			hours = Number(hours) < 10 ? "0" + hours : hours;
			if(hours == "12") hours = "00";
		}
		else { 
			hours = Number(hours) == 12 ? hours : String(Number(hours) + 12);
		}
		minutes = minutes < 10 ? "0" + minutes : minutes;
		return hours + ":" + minutes;
	}

	function _from24To12Hour(str) {
		// Takes str as 00:34 / 13:34 etc. and returns 12:34am / 1:34pm etc.
		var splitStr = str.split(":");
		var hours = splitStr.length > 0 ? splitStr[0] : "";
		var minutes = splitStr.length > 1 ? splitStr[1] : "";
		var ampm;
		if(Number(hours) < 12) { 
			hours = Number(hours) == 0 ? "12" : hours.replace(/^0(\d)/, "$1");
			ampm = "am";
		}
		else { 
			hours = Number(hours) > 12 ? String(Number(hours) - 12) : "12";
			ampm = "pm";
		}
		minutes = minutes < 10 ? "0" + minutes : minutes;
		return hours + ":" + minutes + ampm;
	}

	function _pastPresentFuture(dt, what, dtr) {
		// @dt can be Date obj or just an integer value for comparison against @what.
		// Optional @dtr provides a range.
		//     e.g. dt=13:15, dtr=13:50.
		//     e.g. dt=(yyyymmdd)19841201, dtr=(yyyymmdd)19850131.
		// Proving a range means present is between the two.
		// When providing a range you need to get the lower and higher order correct.
		var states = {
			past : "past",
			present : "present",
			future : "future"
		}

		var status = states.present; // set as default.

		var d = _helpers.getType(dt) == "Date" ? {	
			day : dt.getDate(),
			month : dt.getMonth(),
			year : dt.getFullYear(),
			seconds : dt.getSeconds(),
			minute : dt.getMinutes(),
			hour : dt.getHours(),
			time : _hhmmss(dt.getHours(), dt.getMinutes(), dt.getSeconds()), 
			date : _yyyymmdd(dt),
			full : dt.getTime()
			} : (
			arguments.length > 0 ? {
			// received string so just set all as the same to make things easy. 
			day : Number(dt),
			month : Number(dt),
			year : Number(dt),
			seconds : Number(dt),
			minute : Number(dt),
			hour : Number(dt),
			time : Number(dt.replace(":","")), // needs to be 24hr format
			date : Number(dt),
			full : Number(dt)
		} : null);

		var dr = arguments.length > 2 && _helpers.getType(dtr) == "Date" ? {	
			day : dtr.getDate(),
			month : dtr.getMonth(),
			year : dtr.getFullYear(),
			seconds : dtr.getSeconds(),
			minute : dtr.getMinutes(),
			hour : dtr.getHours(),
			time : _hhmmss(dtr.getHours(), dtr.getMinutes(), dtr.getSeconds()), 
			date : _yyyymmdd(dtr),
			full : dtr.getTime()
			} : (
			arguments.length > 2 ? {
			// received string so just set all as the same to make things easy. 
			day : Number(dtr),
			month : Number(dtr),
			year : Number(dtr),
			seconds : Number(dtr),
			minute : Number(dtr),
			hour : Number(dtr),
			time : Number(dtr.replace(":","")), // needs to be 24hr format
			date : Number(dtr),
			full : Number(dtr)
		} : null); 

		var n = (arguments.length > 0 ? {
			day : _now.getDate(),
			month : _now.getMonth(),
			year : _now.getFullYear(),
			seconds : _now.getSeconds(),
			minute : _now.getMinutes(),
			hour : _now.getHours(),
			time : _hhmmss(_now.getHours(), _now.getMinutes(), _now.getSeconds()),
			date : _yyyymmdd(_now),
			full : _now.getTime()
		} : null);

		if(d && n) {
			if(d[what] < n[what]) status = states.past; 
			if(d[what] > n[what]) status = states.future;
		}

		if(dr) {
			// Do extra check for present because we have a range.
			if(arguments.length > 2 && (n[what] >= d[what] && n[what] < dr[what])) status = states.present;
		}		

		return d && n ? status : states; // Allows retrieval of available states to avoid hard-coding elsewhere.
	}

	function _differenceBetweenNowAndWhen(when) {
		// Returns the difference between current (_now) time and @when Date() value.
		return when.getTime() - _now.getTime();
	}

	function _substituteNoon(ampm) {
		// Take a 12-hour formatted string and substitute 12pm or 12:00pm times for "12:00 noon".
		return ampm.replace(/(12:00|12)[\w]*pm/, "$1 noon");  
	}


	// Public.
	this.now = function() { return _now; };
	this.isToday = _isToday;
	this.isYesterday = _isYesterday;
	this.isTomorrow = _isTomorrow;
	this.sameAsCurrentYear = _sameAsCurrentYear;
	this.sameAsCurrentMonth = _sameAsCurrentMonth;
	this.sameAsCurrentDate = _sameAsCurrentDate;
	this.lastDayOfMonth = _lastDayOfMonth;
	this.dayAsString = _dayAsString;
	this.monthAsString = _monthAsString;
	this.yyyymmdd = _yyyymmdd;
	this.hhmmss = _hhmmss; 
	this.from12To24Hour = _from12To24Hour;
	this.from24To12Hour = _from24To12Hour;
	this.pastPresentFuture = _pastPresentFuture;
	this.differenceBetweenNowAndWhen = _differenceBetweenNowAndWhen;
	this.substituteNoon = _substituteNoon;

	this.init = function(initialiser) {
		// Initialises the time...
		// initialiser = (String) url for valid timestamp/datestring or,
		// initialiser = (int) valid timestamp.
		var interval = _interval;
		var time = new Object();

		if(_now) {
			// Do not reinitialise time, just fake a response using existing _now.
			time.status = 200;
			time.responseText = _now.getTime();
		}
		else {
			if(isNaN(Number(initialiser))) {
				// Get the server time.
				time = $.ajax({
					url : initialiser,
					async : false,
					cache : false
				})
			}
			else {
				// Fake ajax because initialiser should be timestamp.
				time.status = 200;
				time.responseText = Number(initialiser);
			}
	
			// ...and keep it updated.
			setInterval(function() {
				_now.setTime(_now.getTime() + _interval);
			}, interval);

		}

		// Set the object global/private variable _now.
		_now = time.status != 200 ? new Date() : new Date(time.responseText);
	}


});


// Add an event that fires a passed function at a given time, with repeat option.
// when = DateTime object (or either "next" shortcut, @hour or @minute)
// what = function
// repeat = interval in milliseconds. 
argos.services.cron = new (function() {
	var _register = {};
	var _helpers = {
		dt : argos.services.datetime,
		now : argos.services.datetime.now,
		getType : argos.utils.getType,
		unique : argos.utils.generateUniqueStr
	}

	this.add = function(when, what, options) {
		return new Cron(when, what, options);
	}

	this.remove = function(id) {
		// remove Cron;
		_register[id].stop();
	}

	this.clear = function() {
		for(var timer in _register) {
			_register[timer].stop();
		}
	}

	this.queue = function() {
		return _register;
	}

	function _generateEventTime(when) {
		// Give it a time you want an event to run. Returns (milliseconds)now + milliseconds to @when
		var whenType = _helpers.getType(when);
		var time = 1000;
		switch(whenType) {
			case "String" :
				// pass it a shortcut, e.g. @hour (see _next)
				time = _next(when); 
				break;
			case "Number" : 
				// pass it number of milliseconds
				time = when;
				break;
			case "Date" :
				// pass it a date/time in future
				time = _helpers.dt.differenceBetweenNowAndWhen(when);
				break; 
			default : 
				// Do nothing and let it return default 1000;
		} 
		return time; 
	}

	function _next(param) {
		// Allows you to specify cron.when value as "next hour", or "next minute".
		var next = argos.services.datetime.now();
		switch(param) {
			case "@hour" : 
				next.setHours(next.getHours() + 1);
			case "@minute" :
				next.setMinutes(next.getMinutes() + 1);
		}
		return next;
	}

	// CLASSES
	this.Cron = Cron;
	function Cron(when, what, options) {
		// (Date) @when, (Function) @what, (milliseconds as Integer) @repeat.
		var _cron = this;
		var _now = _helpers.now().getTime();
		var _type = _helpers.getType(when);
		var _when = _generateEventTime(when);
		var _timeout = null;
		var _interval = null;
		var _id = "cron_" + _helpers.unique();
		var _options = options ? options : {};

		if(_when > 0) {
			_timeout = window.setTimeout(function() {
				//console.warn("Running Cron... \nid: ", _id, "\nwhat: ", what.name, "\nwhen: ", when, "\narguments: ", _options["arguments"], "\ndescription: ", _cron.description);
				if(_options.arguments && _options.arguments.length > 0) {
					what.apply(this, _options.arguments);
				}
				else {
					what();
				}
				if(_options.repeat) {
					_interval = window.setInterval(what, _options.repeat);
				}

				// Update some details about the Cron.
				_cron.details["activated"] = true;
				_cron.details["repeat"] = _options.repeat ? _options.repeat : false;

			}, _when);

			_register[_id] = this;
		}
		else {
			// Commented out to fail silently.
			// alert("Cannot schedule an event to run in the past.");
			//console.warn("Running Cron... \nid: ", _id, "\nwhat: ", what, "\nwhen: ", when, "\narguments: ", _options["arguments"], "\ndescription: ", _cron.description);
		}

		this.stop = function() {
			window.clearInterval(_interval);
			window.clearTimeout(_timeout);
			delete _register[_id];	
		}

		this.id = _id;
		this.description = _options.description ? _options.description : "";
		this.details = {
			created : _now,
			time : when,
			activated : false,
			repeat : false
		}

//		this.what = what;
	}

});


argos.services.unique = new (function() {
	var _self = this;
	var _lists = new Array();
	var _rules = new Array();
	var _tag = "uniqueValue"; 
	
	// Initialise script (_init)
	$(document).ready(function() {
	
		// Add some defaults that we know will be used.
		_self.rule("products", ".product", function(el) { return el.find(".number").text(); });

	});

	
	// CLASSES
	function Rule(name, elements, value) {
		// @name = String (e.g. "product"), type of component.
		// @elements = String (e.g. ".product .number"), where to find unique value.
		// @value = Function (e.g. text()). Function is always passed the current target element (e.g. current product).
		// See above pre-defined rule, for modern product markup, as an example of usage.
		this.name = name;
		this.elements = elements;
		this.value = value;
	}

	function List(context, rule) {
		// Return list of unique values from $els or return empty Array.
		var List = this;
		var $context = typeof(context) == "string" ? $(context) : context;
		var r = typeof(rule) == "string" ? _rules[rule] : rule;
		$(r.elements, $context).each(function() {
			var uv = this[_tag];
			// Only create if unique doesn't already exist.
			if(!uv) {
				uv = new Value(this, r.value($(this)));
			}
			if(uv.length > 0) {
				List.push(uv);
			}
		});
		
		// Public
		this.context = context;
		this.content = r.name;
		this.rule = r;
	}
	
	List.prototype = new Array();
	
	List.prototype.contains = function(element) {
		// @element should be DOM node.
		var el = element.jquery ? element.get(0) : element;
		return this.join().indexOf(el[_tag]) >= 0;
	}
	
	List.prototype.add = function(o) {
		// Add an item to the list (probably because we've just created it).
		// Check to see if o is jQuery object, else String(), default assume it's DOM node
		var value = o.jquery ? o.get(0)[_tag] : typeof(o) == "String" ? o : o[_tag];
		if(value && value != "") {
			this.push(value);
		}
	}
	
	List.prototype.remove = function(o) {
		// Remove an item from the list (probably because we've deleted it).
		var index = -1;
		var howMany = arguments.length > 1 ? arguments[1] : 1; // Intended use singular, but providing option for multiple.
		var value;
		if(o && !isNaN(o)) {
			index = Number(o);
		}
		else {
			if(typeof o != "string") {
				// Assume jQuery element or DOM node
				value = o.jquery ? o.get(0)[_tag] : o[_tag];
				//console.log("this... ", this);
				for(var i=0; i<this.length; ++i) {
					//console.log("value = %s, this[%d].toString() = %s", value, i, this[i].toString());
					if(this[i].toString() == value) {
						index = i;
						//console.log("index: ", index);
						break;
					}
				}
			}
			
		}
		return index >= 0 ? this.splice(index, howMany) : [];
	}
	
	List.prototype.destroy = function() {
		// Remove the list altogether.
		console.log("TODO: Inside List.delete (this): ", this);
		// TODO: Complete functionality.
		return null;
	}
	
	function Value(element, unique) {
		// Probably a smarter way of doing this...
		var a = new String(unique);
		a.ref = element;
		element[_tag] = a;
		return a;
	}

	
	// PUBLIC	
	this.classes = {};
	this.classes.List = List;
	this.tag = _tag;

	this.rule = function(name, element, value) {
		// @name = String
		// @element = jQuery Selector
		// @value = Function
		if(!_rules[name] && arguments.length > 2) {
			_rules[name] = new Rule(name, element, value);
		}
		return _rules[name];
	}
	
	this.value = function(target, value) {
		var target = target.jquery ? target.get(0) : target;
		var val = target[_tag];
		if(arguments.length > 1 && target && value.length > 0 && !val) {
			// Create only if doesn't exist.
			val = new Value(target, value);
		}
		return val;
	}
	
	this.rules = function(rule) { return arguments.length > 0 ? _rules[rule] : _rules; }
	
});


argos.services.data = new (function() {

	this.getXmlSynchronously = function(url, cache) {
		var get = $.ajax({
			cache : cache ? cache : true,
			url : url,
			dataType : "xml",
			async : false
		});

		return get.status != 200 ? "<failed />" : get.responseText;
	}

	this.getHtmlSynchronously = function(url, cache) {
		var get = $.ajax({
			cache : cache ? cache : true,
			url : url,
			dataType : "html",
			async : false
		});

		return get.status != 200 ? "<failed />" : get.responseText;
	}

});


