/*!
 * ICS_PARSER-object v0.2
 * Copyright 2010, Kai Pfeiffer
 * http://www.gasthauspfeiffer.de/
 * licensed under creative-commons license(by).
 * Http://creativecommons.org/licenses/by/3.0
 *
 * Date: So 25. Apr 22:04:02 CET 2010
 */


//########################################################################################
// public methods:
// this.formatDate: 
// this.getMonthLength: line 136
// this.nextDate: line 152
// this.getEvents: line 177
// this.Event: line 177
// this.parseICalendar: line 375
//########################################################################################

function ICS_PARSER(){
  this.cal		= {};								// object ordered by date with link to event
  this.calInfo		= [];								// array which stores informations for each calendar
  this.events		= {};								// object to store information for each event
  this.period		= {};								// object stores links to events which last longer than one day
  this.rrule		= [];								// object stores links to events which have a reccuring-clause
  this.tzo		= 0;								// TimeZoneOffset adjust JS-Calendars timezone
  this.sto		= 0;								// daylightsavings "1" = transform ; "0" = ignore

  this.aC  		= new Number(0);						// actual calendar-index
  this.cM		= new Number(0);						// CurrentMonth	 Number with the current Month Format YYYYMM
  this.mE		= {};								// monthEvents	 Object indexed by a Value of currentMonth with the according tempEvents
  this.tE		= {};								// tempEvents	 Object for Events in actual Month with a RRULE
  this.eA		= {};

  this.testObject	= {};
  this.cache		= 1;
    this.wDs		= ["SU","MO","TU","WE","TH","FR","SA"];  
  // regexes to retrieve Data from ics-event:
  var dDs		= /((\d{4})(\d{2})(\d{2}))T{0,1}(\d{0,2})(\d{0,2})(\d{0,2})/;	// dateDetails: regular expression to retrieve Year, Month,Day etc. from calendarentry
  
    // this hash contains regular-expressions to retrieve the values of the searches properties
    // please keep in mind, that it ist important to bracket the property-name (e.g. "(DESCRIPTION)"), 
    // the attributes of the property (in this case only "(:)" and
    // the value (in this case "(.*)"

  this.regexes	= [
	  /(DTSTART)(.*):(\d{8}T{0,1}\d{0,6})/,
	  /(DTEND)(.*):(\d{8}T{0,1}\d{0,6})/,
	  /(SUMMARY)(:)(.*)/,
	  /(DESCRIPTION)(:)(.*)/,
	  /(RRULE)(:)(.*)/,
	  /(UID)(:)([\w\@\.\/\-]*)/
  ];

  this.infoRegs = [
		 /(X-WR-CALNAME).*:([\w\@\.\/\ \-]*)/,
		 /(X-WR-TIMEZONE).*:([\w\@\.\/\ \-]*)/,
		 /(X-WR-CALDESC).*:([\w\@\.\/\ \-]*)/
		 ]


  // regex UID has to conform to the following conditions (not done yet)
  //
  // Dawson & Stenerson          Standards Track                    [Page 45]
  
  // RFC 2445                       iCalendar                   November 1998
  //    Property Name: UID
  
  //    Purpose: This property defines the persistent, globally unique
  //    identifier for the calendar component.
  
  //    Value Type: TEXT

  //   text       = *(TSAFE-CHAR / ":" / DQUOTE / ESCAPED-CHAR)
  //     ; Folded according to description above

  //      ESCAPED-CHAR = "\\" / "\;" / "\," / "\N" / "\n")
  //         ; \\ encodes \, \N or \n encodes newline
  //         ; \; encodes ;, \, encodes ,
  
  //      TSAFE-CHAR = %x20-21 / %x23-2B / %x2D-39 / %x3C-5B
  //                   %x5D-7E / NON-US-ASCII
  //         ; Any character except CTLs not needed by the current
  //         ; character set, DQUOTE, ";", ":", "\", ","

  //      Note: Certain other character sets may require modification of the
  //      above definitions, but this is beyond the scope of this document.


  
  // the regular-expressions of the rrules-Array comply with rfc 2445 from 09/1998
  this.rrules	= [
		   /(UNTIL)=(\d{8}T{0,1}\d{0,6})/,
		   /(COUNT)=(\d*)/,
		   /(FREQ)=(\w*)/,
		   /(INTERVAL)=(\d*)/,
		   /(BYSECOND)=([\,\d]*)/,
		   /(BYMINUTE)=([\,\d]*)/,
		   /(BYHOUR)=([\,\d]*)/,
		   /(BYDAY)=([\+\-\,\w]*)/,
		   /(BYMONTHDAY)=([\,\-\+\d]*)/,
		   /(BYYEARDAY)=([\,\-\+\d]*)/,
		   /(BYWEEKNO)=([\,\-\+\d]*)/,
		   /(BYMONTH)=([\,\d]*)/,
		   /(BYSETPOS)=([\-\+]{0,1}\d{1,3})/,
		   /(WKST)=(\w{2})/
		   ];
  
  
  // functions to process dates
  //###########################################################################################
  // formatDate:  formats a date according to the php-format-descriptions
  // parameters:  format -> String with php-like format description
  //		  iDate  -> Date in iCal Format (yyyymmddThhmmssZ)
  //###########################################################################################

  this.formatDate = function(format,iDate){
    var indexes = {"i":6,"H":5,"h":5,"s":7,"Y":2,"y":2,"m":3,"M":3,"n":3,"d":4,"j":4};	// the index of the according values
    var dateDetails = iDate.match(dDs);							// extract detail-information
    var error  = 0;
    var result = "";
    if(iDate.length >= 8){								// if date-entry is correct
      for(var y=0;y<format.length;y++){							// parse format-string
	if(indexes[format[y]]){								// if char is indexed
	  result += dateDetails[indexes[format[y]]];					// append entry to result
	}
	else if(format[y] == '\\'){							// if escape caracter appears
	  y++;										// go to next character
	  result += format[y];								// and append char
	  }
	else{
	  result += format[y];								// else append char
	}
      }
    }
    return result;
  }


  //###########################################################################################
  // getMonthLength:	formats a date according to the behavior of Javascript
  // parameters:	month  -> the month to explore
  //			year   -> the year to explore
  // 			format -> 1 if correct month, 0 if retrieved month from js-funktion
  //###########################################################################################

  this.getMonthLength = function (month,year,format){
    format = format == 1 || 0;
    month -= format;
    return(new Date(year,month+1,0).getDate());
  }


  //###########################################################################################
  // nextDate:	calculates the next date of the next day
  // parameter:	param the actual date
  //###########################################################################################
  
  this.nextDate = function(param){
    var year = Math.floor(param / 10000);
    var month = Math.floor(param % 10000 / 100);
    var day = param % 100;
    if(day < this.getMonthLength(month,year,1)){
      return((year*10000)+(month*100)+(++day));
    }
    else{
      if(month < 12){
	return((year*10000)+(++month*100)+1);
      }
      else{
	return((++year*10000)+101);
      }
    }
  }


  //###########################################################################################
  // getEvents:		retrieve the date_entries of the committed date
  // parameters:	date   -> the year to explore or complete date in format ('yyyymmdd')
  // 			month  -> the month to explore
  // 			day  -> the day to explore
  //			format -> 1 if correct month, 0 if retrieved month from js-funktion
  //###########################################################################################
  
  this.getEvents = function(date,month,day,format){
    format = format == 1 || 0;								// var to correct different month-value
    month  = month >= 0 && month + 1-format;						// set month to correct format
    if (date < 10000000){								// if var date contains only the day
      date = date * 10000 + month * 100 + day;						// create var date withe the format yyyymmdd
    }
    
      var wDs = this.wDs;

    // LOCAL FUNCTIONS:
    //#########################################################################################
    // addEntry:	adds a new entry in the through "index" specified position
    // parameters:	obj   => the hash which should receive the data	
    //			index => index where data should be inserted
    //			uid  => data to insert
    //#########################################################################################

    var addEntry = function(obj,index,uid){
      if(typeof(obj[index]) == "object"){						// tests, if the through "index" marked part of the object "obj" is an object
	obj[index].push(uid);								// yes, we can push the data to the existing array
      }
      else{
	obj[index] = new Array(uid);							// no, we must create a new Array
      }
    }
   
    //#########################################################################################
    // inList:		checks if "item" ist indexed in "obj"	
    // parameters:	obj  => the hash we look for the data
    //			item => the data we search for
    //######################################################################################### 

    var inList = function(obj,item){
      var mL	= obj.split(",");							// split "obj" into its entries
      var erg,i	= 0;									// initialize vars "erg" & "i" with 0
      while(!erg &&i<mL.length){							// parse each entry
	erg = mL[i]*1 == item*1;							// true, if values are equal
	i++;										// next index
      }
      return erg;
    }

    //#########################################################################################
    // checkList:	checks the list of weekdays, if the committed day is present
    // parameters:	obj => weekday, or list of weekdays
    //			aY  => actual year
    //			aM  => actual month
    //			i   => avtual date
    //			m   => actual monthlength
    //#########################################################################################
    
    var checkList = function(obj,aY,aM,i,m){
      var mL = obj.split(",");
      var erg = "";
      
      entry = wDs[new Date(aY,aM-1,i).getDay()];					// split "obj" into its entries
      fM = Math.floor((i-1)/7)+1;							// x-th week in month 
      lM = Math.floor((i-1-m)/7);							// x-th last week

      for(var i=0;i<mL.length;i++){							// parse each entry of "obj"
	var wD = mL[i].match(/([\+\-]{0,1}\d{0,2})(\w*)/);				// extract weekno and weekday
	var target = (wD[1]) == "" || (wD[1]*1==fM) || (wD[1]*1==lM);			// entry matches weeknumber true/false
	erg = erg || wD[2] == entry && target && mL[i];					// if weekday correct and weekno correct
      }
      return erg;	  
    }

    //#########################################################################################
    //			
    //			Main part of the function getEvents
    //			
    //#########################################################################################
     
    if (this.cM != Math.floor(date/100) || !this.cache){						// if current month(cM) mismatches month of date 
      this.cM = Math.floor(date/100);							// set current month
      if(typeof(this.mE[this.cM]) != "object"){						// if ical for current month isn't parsed
	var tE		= {};								// create hash for temorary events
	var tempCal	= this;								// create placeholder for this-object
	      
	// check each entry of the period object, 
	// if the entry matches the current month

	  for(var index in this.period){						// parse each entry in the period-array
	      var maincontent = this.period[index];					// get the content
	      for(var index in maincontent){						// parse Array content
		  var content = maincontent[index];
		  if(index < (tempCal.cM*100+100)){					// if startdate is befor or in the actual month
		      var tempDate = tempCal.events[content]["from"];			// get startdate of the event
		      while(tempDate <= tempCal.events[content]["until"]){		// while the end of the event isn't reached
			  addEntry(tE,tempDate,tempCal.events[content]["UID"]);		// add UID to the hash for temporary events
			  tempDate = tempCal.nextDate(tempDate);			// get the next date
		      }
		  }
	      };									// end of inner for-in-statement
	  };										// end of outer for-in-statement

	for(var i=0;i<this.rrule.length;i++){						// parse every entry in the hash for recurring events
	    var rB = {};								// hash, which stores the RRULE statement of the entry
	    var event = tempCal.events[this.rrule[i]];					// get i-th element of rrule-array
	    var entry = event["RRULE"];							// extract rrule-property
	    for(var index in this.rrules){						// parse all regexes for rrules
		var content = this.rrules[index];
		erg = entry.match(content);						// if regex matched
		if(erg){
		    rB[erg[1]] = erg[2];						// store rrule in rB-hash
		}
	    };


	  var dO		= event["DTSTART"].match(dDs);				// apply regex "dDs"
	  if(dO){									// if event matches regex
	    var dM	= rB["BYMONTH"] || dO[3];					// extract details
	    var dD	= rB["BYDAY"] || dO[4];
	    var dH	= rB["BYHOUR"] || dO[5];
	    var dI	= rB["BYMINUTE"] || dO[6];
	    var dS	= rB["BYSECOND"] || dO[7];
	  }
	  var freq	= rB["FREQ"]||"";
	  var interval	= rB["INTERVAL"]||1;
	  var until	= rB["UNTIL"]||"";
	  var count	= rB["COUNT"]||"";
	  var mD	= rB["BYMONTHDAY"];
	  var yD	= rB["BYYEARDAY"];
	  var wN	= rB["BYWEEKNO"];
	  
	  var aY	= Math.floor(tempCal.cM/100);
	  var aM	= tempCal.cM%100;
	  
	  switch (rB["FREQ"]){								// which kind of "FREQ" has this element
	  case "YEARLY":{								// yearly-event
	    if((inList(dM,aM))&&							// if events freq-month is the actual month
	       !((aY-event["from"].match(dDs)[2])%interval)){				// interval matches this year
		var tempDate = new String(dO[4]).match(/\D/)&&("Tag berechnen")|| 
		    aY*10000+aM*100+dO[4]*1;						// calculate aktual day
		addEntry(tE,tempDate,event["UID"]);					// add Entry to temporary event array
	    }
	    break;
	  }
	  case "MONTHLY":{								// will be parsed by weekly
	    
	  }
	  case "WEEKLY":{

	      for(var y=1;y<=this.getMonthLength(aM,aY,1);y++){				// parse each day of the month
		  if(checkList(dD,aY,aM,y,this.getMonthLength(aM,aY,1))){		// if weekday matches

		      if(interval == 1){
			  addEntry(tE,tempCal.cM*100+y,event["UID"]);			// add entry in temporary-event-array
		      }
		      else{
			  var tempDate= new Date(					// create Date-Object from the current FREQ-entry
			      this.formatDate("y",event["DTSTART"]),
			      this.formatDate("m",event["DTSTART"])-1,
			      this.formatDate("d",event["DTSTART"]));
			  var actDate= new Date(					//  create Date-Object from the actual date
			      Math.floor(date/10000),
			      tempCal.cM%100-1,
			      y);
			  var tempWk =	Date.parse(tempDate) - 				// calculate the first Weekday of the event 
					tempDate.getDay()*3600*24000;
			  var actWk  = 	Date.parse(actDate) -  				// calculate the first Weekday of the actual day
					actDate.getDay()*3600*24000;
			  var diff   = 	actWk-tempWk-((actDate.getTimezoneOffset()-
						       tempDate.getTimezoneOffset())*
						      60000);				// adjust summertimeoffsets
			      var weekOK = ((diff) % (24*7*3600*1000*interval) == 0);	// variable to check if the week coorespondents to the interval
			      var intCnt = Math.floor((diff) / 
						      (24*7*3600*1000*interval))	// variable to count the elapsed intervals

			  if(diff >= 0 &&						// difference is positive, the FREQ could match
			     weekOK &&							// the week is correct
			     ((until >= new String(date)) ||				// the FREQ rule has not endet yet Important: String-comparison!
			      (intCnt <rB["COUNT"])
			     )){
			      addEntry(tE,tempCal.cM*100+y,event["UID"]);		// add entry in temporary-event-array
			  }
		      }
		  }
	      }
	      break;	   
	  }
	  case "DAILY":{}								// DAILY-events 
	  case "HOURLY":{}								// HOURLY-events
	  case "MINUTELY":{}								// MINUTELY-events
	  case "SECONDLY":{}								// SECONDLY-events
	  }
	}
	if (this.cache){
	  this.mE[this.cM] = tE;							// add tE-array to hash mE(events of the month) with index 'yyyymm'
	}
      }
      // end of parsing ical for selected month, the result will be cached
      //##################################################################################
      
      this.tE = this.cache && this.mE[this.cM] || tE;					// retrieve events from the cache for the selected month
    }

    var ergArray = [];
    if(typeof(this.tE[date]) == "object"){						// if temporary events are available
      ergArray = ergArray.concat(this.tE[date]);					// append them to "ergArray"
    }
    if(typeof(this.cal[date]) == "object"){						// if events are in calendar
      ergArray = ergArray.concat(this.cal[date]);					// append them to "ergArray"
    }

    this.eA = ergArray;									// copy local "ergArray" tzo object-variable
    return (this.eA.length);								// return count of events
  }
  
  

  //###########################################################################################
  // getEvent:		returns a hash with the information of the event with the 
  //			submitted id
  // parameters:	id   -> String with the id of the desired event
  //###########################################################################################
  
  this.getEvent = function(id){
    return this.events[id];
    }


  //###########################################################################################
  // parseICalendar:	extracts the data from an ical-file
  // parameters:	data   -> String with the hole ical-textfile
  //###########################################################################################
  
  this.parseICalendar = function(data,calIndex){
    var regex = /BEGIN:VTIMEZONE.*END:VTIMEZONE/;
    var cC = {};;									// create an empty hash
      for(var index in this.infoRegs){		       		    			// execute all REGEXES for ical-details
	  var content = this.infoRegs[index];
	  erg = data.match(content);
	
	  if(erg){									// if successful
	      cC[erg[1]] = erg[2];							// store detail in entry with according indexname
	     }
      };

    this.aC = calIndex;									// set calendar-index
    this.calInfo[this.aC] = cC;								// store calendarinfo in array
    data = data.replace(/\n/g," xLFx ");						// replaces carriage-return with a funny string
    data = data.replace(/\s/g,"  ");							// replaces all kind of space-chars with whitespace
    
    var eventExpr = new RegExp("BEGIN:VEVENT.*?END:VEVENT","g");			// REGEX for hole event
    var events = data.match(eventExpr);							// create Array with all events

    var tempCal = this;
      for(var index in events){								// parse each array entry
	  var content = events[index];
	  tempCal.parseICalendarEntry(content.replace(/ xLFx /g,"\n"));			// replaces funny string with a carriage-return before parsing
      };
  }


  //###########################################################################################
  // parseICalendarEntry:	extracts the data from an ical-event
  // parameters:		entry   -> String with the hole event-text
  //###########################################################################################
  
  this.parseICalendarEntry = function(entry){
    var eB = {};;									// create an empty hash
    eB["cI"] = this.aC;									// remember calendar-index

      for(var index in this.regexes){		       		       			// execute all REGEXES for ical-details
	  var content  = this.regexes[index];
	  erg = entry.match(content);
	  if(erg){									// if successful
	      eB[erg[1]+"_info"] = erg[2];						// store infos in entry with according indexname
	      eB[erg[1]] = erg[3];							// store detail in entry with according indexname
	     }
      };

    //##################################################################################
    // function adjustTimeZone:	the CALENDAR-Object expects ical-files with UTC-timezone
    //				this function adjusts the time to local timezone;
    //##################################################################################

    function adjustTimeZone(param,tzo,sto,start){
      if(param.length > 8){								// if date in param contains timeinformation
	var details = param.match(dDs);							// extract details
	var mydate = new Date(details[2],						// create date-object: year
			      details[3]-1,						// month
			      details[4],						// day
			      details[5],						// hours
			      details[6],						// minutes
			      details[7]);						// seconds
	var datesecs = Date.parse(mydate);						// get UTC-Time in Milliseconds
	if(tzo && sto){									// if timezoneoffset is set and adjust daylight-savings 
	  tzo = mydate.getTimezoneOffset();					      	// get timezoneoffset of the date
	}
	var newDate = new Date(Date.parse(mydate)-tzo*60000);				// add tzo multiplied with 60000 Milliseconds
	var time = new String(newDate.getHours()*10000+
			      newDate.getMinutes()*100+
			      newDate.getSeconds());					// create new datestring
	var date = new String(newDate.getFullYear()*10000+
			      (newDate.getMonth()+1)*100 + 
			      newDate.getDate());
	time = time.length == 6&&time||"0"+time;
	if((time*1) == 0 && !start){
	  newDate = new Date(Date.parse(newDate)-1);
	  time = "240000";
	}
	else if((time*1) == 0){
	  time = "000000";
	}
	param = (newDate.getFullYear()*10000+
		 (newDate.getMonth()+1)*100 + 
		 newDate.getDate())+"T"+time+"Z";					// create new date-time value
      }
      return param;
    }

      //##################################################################################
      // TODO:
      // it would be great, to parse the VTIMEZONE-properties of the ics-file and
      // process the DT*-values withe the gained information
      // The three workaround-lines below avoid the adjustment of TZID-specified time-values
      // from UTC.

      var tzidReg = /TZID/;								// workaround to detect TZID-information
      eB["DTSTART"] = eB["DTSTART"].length > 8 && 
	  (!(tzidReg.exec(eB["DTSTART_info"])) &&					// workaround to avoid adjustment of TZID-specified values
	  adjustTimeZone(eB["DTSTART"],this.tzo,this.sto,1) || 
	   eB["DTSTART"]) ||
	  eB["DTSTART"]+"T000000";
	  
	  
/*      eB["DTEND"] = eB["DTEND"].length > 8 && 
	  (!(tzidReg.exec(eB["DTEND_info"])) &&						// workaround to avoid adjustment of TZID-specified values
	  adjustTimeZone(eB["DTEND"],this.tzo,this.sto,0) || 
	   eB["DTEND"])||
	  eB["DTSTART"].match(dDs)[1]+"T240000";					// ugly workaround for correct all-day-event
*/
    var startDate = eB["DTSTART"].match(dDs)[1];

    eB["from"] = startDate;
    //    var enddetails = eB["DTEND"].match(dDs)
    var endDate = ''; //eB["DTEND"].match(dDs)[1];
    eB["until"] = endDate;
    this.events[eB["UID"]] = eB; 							// store event in uid-indexed hash
    if(typeof(eB["RRULE"]) == "string"){						// if we have a recurring date
	if(eB["RRULE"].match(/WEEKLY/) && !eB["RRULE"].match(/BYDAY/)){			// if no BYDAY is specified
	    var dateDetails = eB["DTSTART"].match(dDs);
	    eB["RRULE"] = eB["RRULE"]+";BYDAY="+this.wDs[new Date(dateDetails[2],
								  dateDetails[3]-1,
								  dateDetails[4])	// retrieve weekday from the startdate
							 .getDay()];			// and appent entry "BYDAY" to the "RRULE"
	}
      this.rrule.push(eB["UID"]);							// we add the UID to the rrule array
    }
    else if ((eB["DTSTART"].length > 8 || 
	 this.nextDate(startDate) < endDate) && 	
	startDate < endDate){ 								// if the date lasts over more than one day

      if(typeof(this.period[startDate]) != "object"){					// is there an array for the actual date?
	this.period[startDate] = new Array(eB["UID"]);					// no, we'll create it
      }
      else{
	this.period[startDate].push(eB["UID"]);						// yes, we append this entry
      }
    }
    else{
      if(typeof(this.cal[startDate]) != "object"){					// is there an array for the actual day?
	this.cal[startDate] = new Array(eB["UID"]);					// no, we'll create it
	}
      else{
	this.cal[startDate].push(eB["UID"]);						// yes, we append this entry
      }
    }
  }
}

