/**
 * Class: ICalendar() - parser object.
 */
function ICalendar() {
	this.data = null; // Holds the iCalendar input data.
	this.calendar = new vCalendar(); // The VCALENDAR object.
	this.eventCount = -1; // Tracks the number of events in the calendar.
	this.lastKey = null;	// Reference to last proccessed key (property).
}

// Class methods
ICalendar.prototype = {
	
	/**
	 * Prepares and sets the data for the parser.  
	 */     
	prepareData: function(data) {
		// Fix for malformed Mozilla VCALENDAR syntax.
		this.data = data.replace(/[\r\n]{1,} ([:;])/g, '$1');
		// Make array of all the lines.
		this.data = this.data.split(/\r?\n/);                
		// Is it really a VCALENDAR?
		if(this.data[0].indexOf('BEGIN:VCALENDAR') == -1) {
			throw('invalidCalendarException');
		}
		return this.data;
	},
	
	/**
	 * Method that does the actual parsing.
	 */   	
      parse: function() {
      	this.calendar = new vCalendar();
        // Loop through all lines and analyze them.
      	for(var i=0; i<this.data.length; i++) {
			var line = this.data[i];
			if(line!=""){				
  			 // Get possible key/value for line.
  				var values = this.returnKeyValue(line);
  				key = values[0];
  				value = values[1];
				
  				switch(line) {	
  					// It's a new event.
  					case 'BEGIN:VEVENT':
  						this.eventCount++;
  						type = 'VEVENT';
  						break;
  					// It's a calendar property.
  					case 'BEGIN:VCALENDAR':
  					case 'BEGIN:DAYLIGHT':
  					case 'BEGIN:VTIMEZONE':
  					case 'BEGIN:STANDARD':
  						type = value;
  						break;
  					// It's the end of the calendar property or event.
  					case 'END:VEVENT':
  					case 'END:VCALENDAR':
  					case 'END:DAYLIGHT':
  					case 'END:VTIMEZONE': 
  					case 'END:STANDARD':
  						type = 'VCALENDAR';
  						break;
  					// Add data to the calendar or event.
  					default:
  						this.addToCalendar(type, key, value);
  						break;
  				}	
			
			}    			
						
        } 
		
      },
      
	/**
	 * Adds data to the calendar object from the parser.                  
	 */     
	addToCalendar: function(type, key, value) {    
		// Make a new event if we are not proccessing a current one and type is VEVENT.
		if(type == 'VEVENT') {        
			try {
				var event = this.calendar.getEventAtIndex(this.eventCount);
			} catch(e) {
				var event = new vEvent();
				this.calendar.addEvent(event);
			}
		}    
		// If no key, add the current value to currently proccessing property's value.
		if (key == false) {
			key = this.lastKey;
			var oldValue;
			switch(type) {
				case 'VEVENT':
				oldValue = this.calendar.getEventAtIndex(this.eventCount).getProperty(key); 			 
				value = oldValue+this.trimStart(value);
				break;
			}
		}
		// Convert calendar date properties to javascript date.
		if ((key == 'DTSTAMP') || (key == 'LAST-MODIFIED') || (key == 'CREATED')) {
			value = this.toDate(value);
		} 
		// Convert event date properties to own detailed mapping.
		if (key.indexOf('DTSTART') > -1 || key.indexOf('DTEND') > -1) {
			var dateArray = this.toDateProperties(key,value);
			key = dateArray[0];
			value = dateArray[1];
		}
		// Parse any rules for item.
		if (key == 'RRULE' ) {
			value = this.makeRuleProperties(value);
		}
		// Add the data.    
		switch(type) {
			// It's an event. Add property and value to event.
			case 'VEVENT': 
				this.calendar.getEventAtIndex(this.eventCount).setProperty(key,value);
				break;
			// It's a calendar's property. 
			default:
				this.calendar.setProperty(key,value);
				break;
		}
		// Reference last proccessed key.
		this.lastKey = key;
	},
	
	/**
	 * Make rule property map. 
	 */     
	makeRuleProperties: function(value) {      
		var ruledata = value.split(';');
		var rule = new PropertyMap();
		for(var i=0;i<ruledata.length;i++){
			var data = ruledata[i].split('=');
			rule.put(data[0], data[1]);
		}
//		for(var r in ruledata) {		
//			var data = ruledata[r].split('=');
//			rule.put(data[0], data[1]);
//		}
		return rule;
	},
	
	/**
	 * Parse a VEVENT type date to a own property map object. 
	 */     
	toDateProperties: function(key,value) {
	
		var dtProperty = new PropertyMap();
		dtProperty.put('TZID', 'Undefined'); // Default in case we don't find any timezone data.
		
		// Convert time to JS-date and make property.
		dtProperty.put('JSDATE', this.toDate(value));
		
		// Get date info from key value.
		var dtInfo = key.split(';');
		
		key = dtInfo[0]; // Shorten the key to read DTSTART or DTEND not "DTSTART;TZID=Europe/Oslo".
		dtProperty.put(key, value);
		
		if(typeof(dtInfo[1]) != 'undefined') { // Timezone is specified.
			// Get timezone.
			var tzInfo = dtInfo[1].split('=');
			var timezoneValue = tzInfo[1];
			dtProperty.put('TZID', timezoneValue);
			return new Array(key,dtProperty);          
		} else {
			// Try get the calendar default TZ.
			try { dtProperty.put('TZID', this.calendar.getProperty('TZID')); } catch(e) {}
			try { dtProperty.put('TZID', this.calendar.getProperty('X-WR-TIMEZONE')); } catch(e) {}            
			return new Array(key,dtProperty);
		}
	},
	
	/**
	 * Convert a iCal type timestamp to Javascript date.  
	 */     
	toDate: function(dateString) {
		dateString = dateString.replace('T', '');
		dateString = dateString.replace('Z', '');
		var pattern_datetime = /^(19\d\d|20\d\d)(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])([01][0-9]|2[0-3])([012345][0-9])([012345][0-9])/; 
		var pattern_date = /^(19\d\d|20\d\d)(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])/; 
		
		try {
			var dtArr = pattern_datetime.exec(dateString);
			var dArr = pattern_date.exec(dateString);
			var calDate = new Date();
			if (dtArr) {				
				var months = (dtArr[2].substr(0,1) == '0') ? dtArr[2].substr(1,1) : dtArr[2];
				months = parseInt(months)-1;
				var days = (dtArr[3].substr(0,1) == '0') ? dtArr[3].substr(1,1) : dtArr[3];
				days = parseInt(days);
				
				var hours = (dtArr[4].substr(0,1) == '0') ? dtArr[4].substr(1,1) : dtArr[4];
				hours = hours == '' ? '0' : hours;
				hours = parseInt(hours);
				
				var minutes = (dtArr[5].substr(0,1) == '0') ? dtArr[5].substr(1,1) : dtArr[5];
				minutes = minutes == '' ? '0' : minutes;
				minutes = parseInt(minutes);
				
				var seconds = (dtArr[6].substr(0,1) == '0') ? dtArr[6].substr(1,1) : dtArr[6];
				seconds = seconds == '' ? '0' : seconds;
				seconds = parseInt(seconds);
				
				var calDate = new Date(dtArr[1], months, days, hours, minutes, seconds);
			} else if (dArr) {
				var months = (dArr[2].substr(0,1) == '0') ? dArr[2].substr(1,1) : dArr[2];
				months = parseInt(months)-1;
				var days = (dArr[3].substr(0,1) == '0') ? dArr[3].substr(1,1) : dArr[3];
				days = parseInt(days);
				
				var calDate = new Date(dArr[1], months, days);
			} else {
				alert(1)
				throw('invalidDateException');
			}
		} catch(e) {
			throw('invalidDateException');
		}
		return calDate;
	},
	
	/**
	 * Returns a possible value/key-set of a calendar data line.     
	 */     
	returnKeyValue: function(line) {    
		// Regex for VCALENDAR syntax. Match letters in uppercase in the beginning
		// of the line followed by VCALENDAR-type operator and value.
		var pattern = /^([A-Z]+[^:]+)[:]([\w\W]*)/;
		var matches = pattern.exec(line);
		if(matches) {
			return matches.splice(1,2);
		}
		// No key found, just return value.
		return new Array(false,line);
	},
	
	/**
	 * Trims the beginning of string one whitespace character.        
	 */     
	trimStart: function(str) {
		str=str.replace(/^\s{0,1}(.*)/, '$1');
		return str;
	},

	/**
	 * Get the calendar object for the reader.  	 
	 */   	
	getCalendar: function() {
		return this.calendar;
	},

	/**
	 * Sorts the calendar events by time desc.   
	 *
	 */
	sort: function(){
		this.calendar.sort();
	}     
}

/**
 * Class: vCalendar() - calendar object.
 */
function vCalendar() {
	this.vEvents = new Array();
	this.properties = new PropertyMap();
}

// Class methods
vCalendar.prototype = {

	/**
	 * Gets the event array.   
	 */     
	getEvents: function() {
		return this.vEvents;
	},
	
	/**
	 * Gets the properties hashmap.   
	 */     
	getProperties: function() {
		return this.properties;
	},

	/**
	 * Get the number of events.
	 */        
	getNrOfEvents: function() {
		return this.vEvents.length;
	},
	
	/**
	 * Sorts the array of events by time desc.   
	 *
	 */
	sort: function(){
		this.vEvents = this.vEvents.sort(this.sortByDate);
	},

	/**
	 * Get list of available properties for the calendar.   
	 */
	getPropertyNames: function() {
		return this.properties.keys();
	},
	
	/**
	 * Get an event at a given index.             
	 */
	getEventAtIndex: function(index) {
		var evt = this.vEvents[index];
		if(typeof(evt) == 'undefined') {
			throw('eventNotFoundException');
		}
		return this.vEvents[index];
	},
	
	/**
	 * Get value of a given property.       
	 */
	getProperty: function(property) {  
		try {
			return this.properties.get(property);      
		} catch(e) {
			throw(e); 
		}
	},
	
	/**
	 * Adds a vEvent object to the event array.   
	 */
	addEvent: function(vEvent) {
		this.vEvents.push(vEvent);
	},
	
	/**
	 * Set a property to the calendar.            
	 */
	setProperty: function(property, value) {
		if(typeof(property) == 'string' && property != null && property != '') {      
			this.properties.put(property,value);
		} else {
			throw('invalidKeyNameException');
		}
	},

	/**
	 * Sorting method for the events.
	 *
	 */
	sortByDate: function(a, b) {
		var x = a.getStartDate();
		var y = b.getStartDate();
		return ((x < y) ? -1 : ((x > y) ? 1 : 0));
	}
}
 
/**
 * Class: vEvent()  - event object.
 */              
function vEvent() {
	this.properties = new PropertyMap();
}

// Class methods
vEvent.prototype = {
	/**
	 * Get start time for event.       
	 */
	getStartDate: function() {
		var dt = this.getProperty('DTSTART');
		return dt.get('JSDATE');
	},
	
	/**
	 * Get end time for event.          
	 */
	getEndDate: function() {
		var dt = this.getProperty('DTEND');
		return dt.get('JSDATE');
	},
	
	/**
	 * Get timezone for event.              
	 */
	getTimeZone: function() {
		var dt = this.getProperty('DTSTART');
		return dt.get('TZID');
	},
	
	/**
	 * Get rules for event.            
	 */
	getRuleProperties: function() {
		var r;
		try {
			var r = this.getProperty('RRULE');
		} catch(e) {
			r = new PropertyMap();
		}
		return r;
	},
	
	/**
	 * Get a property by name.       
	 */     
	getProperty: function(property) {  
		try {
			return this.removeSlashes(this.properties.get(property));      
		} catch(e) {
			throw(e); 
		}
	},
	
	/**
	 * Sets a property with given name and value.            
	 */
	setProperty: function(property, value) {
		if(typeof(property) == 'string' && property != null && property != '') {
			this.properties.put(property, value);
		} else {
			throw('invalidKeyNameException');
		}
	},
	
	/**
	 * Get property with given key in HTML-format.     
	 */
	getHtmlValue: function(property) {
		prop = this.removeSlashes(this.properties.get(property));
		if(typeof(prop) == 'string') {
			prop = prop.replace('\n','<br/>', 'g');
			return prop; 
		} else {
			return prop;
		}                    
	},
	/**
	 * Get categories property
	 */
	getXCategories: function() {
		var keys = this.properties.keys();
		var xcategories = [];
		for (var i = 0; i < keys.length; i++) {
			var key = keys[i];
			var cat_matches = key.match(/^CATEGORIES;(.*)$/);
			if (cat_matches) {
				var xnames = cat_matches[1].split(";");
				for (var j = 0; j < xnames.length; j++) {
					var matches = xnames[j].match(/^(x|X\-[0-9a-zA-z_\-]+)\=(.*)/);
					if (matches) {
						xcategories[matches[1]] = matches[2];
					}
				}
			}
		}
		return xcategories;
	},

	/**
	 * Get a list of property- names for this event.     
	 */     
	getPropertyNames: function() {
		return this.properties.keys();
	},
	
	/**
	 * Removes slashes from a string     
	 */  
	removeSlashes: function(str) {
		if(typeof(str) == 'string') {
			str = str.replace('\\n','\n', 'g');
			str = str.replace('\\,','\,', 'g');
			str = str.replace('\\;','\;', 'g');
		}    
		return str;
	}  
}

/**
 * Class: PropertyMap() - property hashmap object for vCal and vEvent.
 */      
function PropertyMap()  {   
	this.size = 0;   
	this.properties = new Object();
}

// Class methods
PropertyMap.prototype = {
	/**
	 * Add or update property.
	 */            
	put: function(key, value) {   
		if(!this.containsKey(key)) {   
			this.size++ ;   
		}   
		this.properties[key] = value;   
	},
	
	/**
	 * Get property with given key.
	 */            
	get: function(key) {
		if(this.containsKey(key)) {
			return this.properties[key];
		} else {
			throw('invalidPropertyException');
		}   
	},
	
	/**
	 * Alias for get method to keep consistancy in syntax in regard to the other classes.
	 */            
	getProperty: function(key) {
		try {
			return this.get(key);
		} catch(e) {
			throw(e);
		}
	},
	
	/**
	 * Remove property with key.
	 */
	remove: function(key) {   
		if( this.containsKey(key) && (delete this.properties[key])) {   
			size--;
		} 
	},
	
	/**
	 * Check if a property exists.
	 */
	containsKey: function(key) {   
		return (key in this.properties);   
	},  
	  
	/**
	 * Check if a value exists.    
	 */
	containsValue: function(value) {   
		for(var prop in this.properties) {   
			if(this.properties[prop] == value) {   
				return true; 
			}   
		}   
		return false;   
	},   
	
	/**
	 * Get all the values.   
	 */
	values: function () {   
		var values = new Array();   
		for(var prop in this.properties) {   
			values.push(this.properties[prop]);   
		}   
		return values;   
	},
	
	/**
	 * Get all the keys.  
	 */
	keys: function () {   
		var keys = new Array();   
		for(var prop in this.properties) {   
			keys.push(prop);   
		}   
		return keys;   
	},

	/**
	 * Get the size of map.
	 */
	size: function () {   
		return this.size;   
	},  
	 
	/**
	 * Clears all properties.
	 */
	clear: function () {   
		this.size = 0;   
		this.properties = new Object();   
	},
	
	/**
	 * Gives a string representation of this propertymap.
	 */         
	toString: function() {
		var str = '';
		for(var prop in this.properties) {   
			str += prop+'='+this.get(prop)+', ';   
		}        
		return '{ '+str.substring(0,(str.length-2))+' }'; 
	}
}   

