/**
 * Copyright 2007 Tim Down.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
 
/**
 * simpledateformat.js
 *
 * A faithful JavaScript implementation of Java's SimpleDateFormat's format
 * method. All pattern layouts present in the Java implementation are
 * implemented here except for z, the text version of the date's time zone.
 *
 * Thanks to Ash Searle (http://hexmen.com/blog/) for his fix to my
 * misinterpretation of pattern letters h and k.
 * 
 * See the official Sun documentation for the Java version:
 * http://java.sun.com/j2se/1.5.0/docs/api/java/text/SimpleDateFormat.html
 *
 * Author: Tim Down <tim@timdown.co.uk>
 * Last modified: 6/2/2007
 * Website: http://www.timdown.co.uk/code/simpledateformat.php
 */
 
/* ------------------------------------------------------------------------- */

var SimpleDateFormat;

(function() {
	function isUndefined(obj) {
		return typeof obj == "undefined";
	}

	var regex = /('[^']*')|(G+|y+|M+|w+|W+|D+|d+|F+|E+|a+|H+|k+|K+|h+|m+|s+|S+|Z+)|([a-zA-Z]+)|([^a-zA-Z']+)/;
	var monthNames = ["January", "February", "March", "April", "May", "June",
		"July", "August", "September", "October", "November", "December"];
	var dayNames = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
	var TEXT2 = 0, TEXT3 = 1, NUMBER = 2, YEAR = 3, MONTH = 4, TIMEZONE = 5;
	var types = {
		G : TEXT2,
		y : YEAR,
		M : MONTH,
		w : NUMBER,
		W : NUMBER,
		D : NUMBER,
		d : NUMBER,
		F : NUMBER,
		E : TEXT3,
		a : TEXT2,
		H : NUMBER,
		k : NUMBER,
		K : NUMBER,
		h : NUMBER,
		m : NUMBER,
		s : NUMBER,
		S : NUMBER,
		Z : TIMEZONE
	};
	var ONE_DAY = 24 * 60 * 60 * 1000;
	var ONE_WEEK = 7 * ONE_DAY;
	var DEFAULT_MINIMAL_DAYS_IN_FIRST_WEEK = 1;

	var newDateAtMidnight = function(year, month, day) {
		var d = new Date(year, month, day, 0, 0, 0);
		d.setMilliseconds(0);
		return d;
	}

	Date.prototype.getDifference = function(date) {
		return this.getTime() - date.getTime();
	};

	Date.prototype.isBefore = function(d) {
		return this.getTime() < d.getTime();
	};

	Date.prototype.getUTCTime = function() {
		return Date.UTC(this.getFullYear(), this.getMonth(), this.getDate(), this.getHours(), this.getMinutes(),
				this.getSeconds(), this.getMilliseconds());
	};

	Date.prototype.getTimeSince = function(d) {
		return this.getUTCTime() - d.getUTCTime();
	};

	Date.prototype.getPreviousSunday = function() {
		// Using midday avoids any possibility of DST messing things up
		var midday = new Date(this.getFullYear(), this.getMonth(), this.getDate(), 12, 0, 0);
		var previousSunday = new Date(midday.getTime() - this.getDay() * ONE_DAY);
		return newDateAtMidnight(previousSunday.getFullYear(), previousSunday.getMonth(),
				previousSunday.getDate());
	}

	Date.prototype.getWeekInYear = function(minimalDaysInFirstWeek) {
		if (isUndefined(this.minimalDaysInFirstWeek)) {
			minimalDaysInFirstWeek = DEFAULT_MINIMAL_DAYS_IN_FIRST_WEEK;
		}
		var previousSunday = this.getPreviousSunday();
		var startOfYear = newDateAtMidnight(this.getFullYear(), 0, 1);
		var numberOfSundays = previousSunday.isBefore(startOfYear) ?
			0 : 1 + Math.floor(previousSunday.getTimeSince(startOfYear) / ONE_WEEK);
		var numberOfDaysInFirstWeek =  7 - startOfYear.getDay();
		var weekInYear = numberOfSundays;
		if (numberOfDaysInFirstWeek < minimalDaysInFirstWeek) {
			weekInYear--;
		}
		return weekInYear;
	};

	Date.prototype.getWeekInMonth = function(minimalDaysInFirstWeek) {
		if (isUndefined(this.minimalDaysInFirstWeek)) {
			minimalDaysInFirstWeek = DEFAULT_MINIMAL_DAYS_IN_FIRST_WEEK;
		}
		var previousSunday = this.getPreviousSunday();
		var startOfMonth = newDateAtMidnight(this.getFullYear(), this.getMonth(), 1);
		var numberOfSundays = previousSunday.isBefore(startOfMonth) ?
			0 : 1 + Math.floor((previousSunday.getTimeSince(startOfMonth)) / ONE_WEEK);
		var numberOfDaysInFirstWeek =  7 - startOfMonth.getDay();
		var weekInMonth = numberOfSundays;
		if (numberOfDaysInFirstWeek >= minimalDaysInFirstWeek) {
			weekInMonth++;
		}
		return weekInMonth;
	};

	Date.prototype.getDayInYear = function() {
		var startOfYear = newDateAtMidnight(this.getFullYear(), 0, 1);
		return 1 + Math.floor(this.getTimeSince(startOfYear) / ONE_DAY);
	};

	/* ----------------------------------------------------------------- */

	SimpleDateFormat = function(formatString) {
		this.formatString = formatString;
	};

	/**
	 * Sets the minimum number of days in a week in order for that week to
	 * be considered as belonging to a particular month or year
	 */
	SimpleDateFormat.prototype.setMinimalDaysInFirstWeek = function(days) {
		this.minimalDaysInFirstWeek = days;
	};

	SimpleDateFormat.prototype.getMinimalDaysInFirstWeek = function(days) {
		return isUndefined(this.minimalDaysInFirstWeek)	?
			DEFAULT_MINIMAL_DAYS_IN_FIRST_WEEK : this.minimalDaysInFirstWeek;
	};

	SimpleDateFormat.prototype.parse = function(timeStamp) {
		/*
		 * This method/function is incomplete, is not coded for variable length patterns
		 * only coded for static length patterns
		 * Static length pattern:
		 * 	yyyy-MM-dd HH:mm:ss.SSS
		 * Dynamic length pattern:
		 * 	M/d/yyyy H:mm:ss.S
		 * 		The example above is dynamic length because:
		 * 			M could be 1 or 2 characters
		 * 			d could be 1 or 2 characters
		 * 			H could be 1 or 2 characters
		 * 			S could be 1, 2 or 3 characters
		 * 		M could be 0 or 0,1
		 * 		d could be 2 or 3 or 2,3 or 3,4
		 * 		y could be 4,7 or 5,8 or 6,9
		 * 		...
		 */
		var date = new Date();
		var mill = Date.parse(timeStamp);
		if(isNaN(mill)) {
			var year;
			var month;
			var day;
			var hour;
			var minute;
			var second;
			var millisecond;
			var ampm24;
			var a1,a2,D1,D2,d1,d2,E1,E2,F1,F2,G1,G2,H1,H2,h1,h2,K1,K2,
				k1,k2,M1,M2,m1,m2,S1,S2,s1,s2,W1,W2,w1,w2,y1,y2,Z1,Z2 = null;
			var skip = false;
			var pattern = this.formatString;
			var pIndex = 0;
			var pChar;
			for(pIndex = 0; pIndex < pattern.length; pIndex++) {
				pChar = pattern.charAt(pIndex);
				if(skip && pChar == "'") {
					skip = false;
					if(pIndex+1 < pattern.length)
						pattern = pattern.substring(0,pIndex) + pattern.substring(pIndex+1);
					else
						pattern = pattern.substring(0,pIndex);
					pIndex--;
				} else if(pChar == "'") {
					if(pIndex+1 < pattern.length)
						pattern = pattern.substring(0,pIndex) + pattern.substring(pIndex+1);
					else
						pattern = pattern.substring(0,pIndex);
					pIndex--;
					skip = true;
				} else if(skip) {
					pattern = pattern.substring(0,pIndex) + "-" + pattern.substring(pIndex + 1); 
				} else {
					switch(pChar) {
						case "a":
							if(a1 == null) a1 = pIndex;
							a2 = pIndex;
							break;
						case "D":
							if(D1 == null) D1 = pIndex;
							D2 = pIndex;
							break;
						case "d":
							if(d1 == null) d1 = pIndex;
							d2 = pIndex;
							break;
						case "E":
							if(E1 == null) E1 = pIndex;
							E2 = pIndex;
							break;
						case "F":
							if(F1 == null) F1 = pIndex;
							F2 = pIndex;
							break;
						case "G":
							if(G1 == null) G1 = pIndex;
							G2 = pIndex;
							break;
						case "H":
							if(H1 == null) H1 = pIndex;
							H2 = pIndex;
							break;
						case "h":
							if(h1 == null) h1 = pIndex;
							h2 = pIndex;
							break;
						case "K":
							if(K1 == null) K1 = pIndex;
							K2 = pIndex;
							break;
						case "k":
							if(k1 == null) k1 = pIndex;
							k2 = pIndex;
							break;
						case "M":
							if(M1 == null) M1 = pIndex;
							M2 = pIndex;
							break;
						case "m":
							if(m1 == null) m1 = pIndex;
							m2 = pIndex;
							break;
						case "S":
							if(S1 == null) S1 = pIndex;
							S2 = pIndex;
							break;
						case "s":
							if(s1 == null) s1 = pIndex;
							s2 = pIndex;
							break;
						case "W":
							if(W1 == null) W1 = pIndex;
							W2 = pIndex;
							break;
						case "w":
							if(w1 == null) w1 = pIndex;
							w2 = pIndex;
							break;
						case "y":
							if(y1 == null) y1 = pIndex;
							y2 = pIndex;
							break;
						case "Z":
							if(Z1 == null) Z1 = pIndex;
							Z2 = pIndex;
							break;
					}
					//date.
					if(y1 != null) {
						year = timeStamp.substring(y1,y2+1);
					}
					
					if(M1 != null) {
						month = timeStamp.substring(M1,M2+1);
					}
					
					if(d1 != null) {
						day = timeStamp.substring(d1,d2+1);
					}
					
					if(H1 != null) {
						hour = timeStamp.substring(H1,H2+1);
						ampm24 = "24";
					}
					
					if(h1 != null && H1 == null) {
						hour = timeStamp.substring(h1,h2+1);
					}
					
					if(a1 != null) {
						ampm24 = timeStamp.substring(a1,a2+1);
					}
					
					if(m1 != null) {
						minute = timeStamp.substring(m1,m2+1);
					}
					
					if(s1 != null) {
						second = timeStamp.substring(s1,s2+1);
					}
					
					if(S1 != null) {
						millisecond = timeStamp.substring(S1,S2+1);
					}
					
					if(y1 != null && M1 != null && d1 != null) {
						month = parseInt(month,10);
						month--;
						date.setFullYear(year,month,day);
					}
					
					if(m1 != null) {
						date.setMinutes(minute);
					}
					
					if(s1 != null) {
						date.setSeconds(second);
					}
					
					if(S1 != null) {
						date.setMilliseconds(millisecond);
					}
					
					if(H1 != null) {
						date.setHours(hour);
					}
					
					if(h1 != null && H1 == null && ampm24 != null) {
						if(ampm24.charAt(0).toLowerCase() == "a") {
							date.setHours(hour);
						} else {
							date.setHours(parseInt(hour,10) + 12);
						}
					}
				}
			} 
		} else {
			date.setTime(mill);
		}
		return date;
		
	};
	
	SimpleDateFormat.prototype.format = function(date) {
		var formattedString = "";
		var result;

		var padWithZeroes = function(str, len) {
			while (str.length < len) {
				str = "0" + str;
			}
			return str;
		};

		var formatText = function(data, numberOfLetters, minLength) {
			return (numberOfLetters >= 4) ? data : data.substr(0, Math.max(minLength, numberOfLetters));
		};

		var formatNumber = function(data, numberOfLetters) {
			var dataString = "" + data;
			// Pad with 0s as necessary
			return padWithZeroes(dataString, numberOfLetters);
		};

		var searchString = this.formatString;
		while ((result = regex.exec(searchString))) {
			var matchedString = result[0];
			var quotedString = result[1];
			var patternLetters = result[2];
			var otherLetters = result[3];
			var otherCharacters = result[4];

			// If the pattern matched is quoted string, output the text between the quotes
			if (quotedString) {
				if (quotedString == "''") {
					formattedString += "'";
				} else {
					formattedString += quotedString.substring(1, quotedString.length - 1);
				}
			} else if (otherLetters) {
				// Swallow non-pattern letters by doing nothing here
			} else if (otherCharacters) {
				// Simply output other characters
				formattedString += otherCharacters;
			} else if (patternLetters) {
				// Replace pattern letters
				var patternLetter = patternLetters.charAt(0);
				var numberOfLetters = patternLetters.length;
				var rawData = "";
				switch (patternLetter) {
					case "G":
						rawData = "AD";
						break;
					case "y":
						rawData = date.getFullYear();
						break;
					case "M":
						rawData = date.getMonth();
						break;
					case "w":
						rawData = date.getWeekInYear(this.getMinimalDaysInFirstWeek());
						break;
					case "W":
						rawData = date.getWeekInMonth(this.getMinimalDaysInFirstWeek());
						break;
					case "D":
						rawData = date.getDayInYear();
						break;
					case "d":
						rawData = date.getDate();
						break;
					case "F":
						rawData = 1 + Math.floor((date.getDate() - 1) / 7);
						break;
					case "E":
						rawData = dayNames[date.getDay()];
						break;
					case "a":
						rawData = (date.getHours() >= 12) ? "PM" : "AM";
						break;
					case "H":
						rawData = date.getHours();
						break;
					case "k":
						rawData = date.getHours() || 24;
						break;
					case "K":
						rawData = date.getHours() % 12;
						break;
					case "h":
						rawData = (date.getHours() % 12) || 12;
						break;
					case "m":
						rawData = date.getMinutes();
						break;
					case "s":
						rawData = date.getSeconds();
						break;
					case "S":
						rawData = date.getMilliseconds();
						break;
					case "Z":
						rawData = date.getTimezoneOffset(); // This is returns the number of minutes since GMT was this time.
						break;
				}
				// Format the raw data depending on the type
				switch (types[patternLetter]) {
					case TEXT2:
						formattedString += formatText(rawData, numberOfLetters, 2);
						break;
					case TEXT3:
						formattedString += formatText(rawData, numberOfLetters, 3);
						break;
					case NUMBER:
						formattedString += formatNumber(rawData, numberOfLetters);
						break;
					case YEAR:
						if (numberOfLetters <= 3) {
							// Output a 2-digit year
							var dataString = "" + rawData;
							formattedString += dataString.substr(2, 2);
						} else {
							formattedString += formatNumber(rawData, numberOfLetters);
						}
						break;
					case MONTH:
						if (numberOfLetters >= 3) {
							formattedString += formatText(monthNames[rawData], numberOfLetters, numberOfLetters);
						} else {
							// NB. Months returned by getMonth are zero-based
							formattedString += formatNumber(rawData + 1, numberOfLetters);
						}
						break;
					case TIMEZONE:
						var isPositive = (rawData > 0);
						// The following line looks like a mistake but isn't
						// because of the way getTimezoneOffset measures.
						var prefix = isPositive ? "-" : "+";
						var absData = Math.abs(rawData);

						// Hours
						var hours = "" + Math.floor(absData / 60);
						hours = padWithZeroes(hours, 2);
						// Minutes
						var minutes = "" + (absData % 60);
						minutes = padWithZeroes(minutes, 2);

						formattedString += prefix + hours + minutes;
						break;
				}
			}
			searchString = searchString.substr(result.index + result[0].length);
		}
		return formattedString;
	};
})();

