<?php
/**
 * JEvents Component for Joomla! 3.x
 *
 * @version     $Id: iCalRRule.php 3467 2012-04-03 09:36:16Z geraintedwards $
 * @package     JEvents
 * @copyright   Copyright (C) 2008-2020 GWESystems Ltd, 2006-2008 JEvents Project Group
 * @license     GNU/GPLv2, see http://www.gnu.org/licenses/gpl-2.0.html
 * @link        http://www.jevents.net
 */

// no direct access
defined('_JEXEC') or die('Restricted access');

use Joomla\CMS\Factory;
use Joomla\String\StringHelper;

class iCalRRule extends Joomla\CMS\Table\Table
{

	/** @var int Primary key */
	var $rr_id = null;

	/**
	 * This holds the raw data as an array
	 *
	 * @var array
	 */
	var $data;
	var $freq;

	// array of exception dates
	var $_exdate = array();

	/**
	 * Null Constructor
	 */
	function __construct(&$db)
	{

		parent::__construct('#__jevents_rrule', 'rr_id', $db);
	}

	/**
	 * Pseudo Constructor
	 *
	 * @param iCal Entry parsed from ICS file as an array $ice
	 *
	 * @return n/a
	 */
	public static function iCalRRuleFromData($rrule)
	{

		$db   = Factory::getDbo();
		$temp = new iCalRRule($db);

		$temp->data = $rrule;
		$temp->freq = $temp->data['FREQ'];

		// Should really test count
		$temp->processField("count", 999);
		$temp->processField("interval", 1);
		//interval ios a mysql reserved word
		$temp->rinterval = $temp->interval;
		$temp->_interval = $temp->interval;
		unset($temp->interval);
		if ($temp->freq == "none")
		{
			$temp->processField("until", "");
		}
		else
		{
			// cap indefinate repeats if count is blank as well as until
			if (array_key_exists("COUNT", $temp->data))
			{
				$temp->processField("until", 0);
			}
			else
			{
				$cfg = JEVConfig::getInstance();
				$temp->processField("until", JevDate::mktime(23, 59, 59, 12, 12, JEVHelper::getMaxYear()));
				$temp->processField("count", 9999);
			}
		}
		$temp->processField("untilraw", "");
		$temp->processField("bysecond", "");
		$temp->processField("byminute", "");
		$temp->processField("byhour", "");
		$temp->processField("byday", "");
		$temp->processField("bymonthday", "");
		$temp->processField("byyearday", "");
		$temp->processField("byweekno", "");
		$temp->processField("bymonth", "");
		$temp->processField("bysetpos", "");
		$temp->processField("irregulardates", "");
		$temp->irregulardates = json_encode($temp->irregulardates);
		$temp->processField("wkst", "");

		return $temp;
	}

	public function processField($field, $default)
	{

		$this->$field = array_key_exists(strtoupper($field), $this->data) ? $this->data[strtoupper($field)] : $default;
	}

	public function store($updateNulls = false)
	{

		return parent::store($updateNulls);
	}

	/**
	 * Pseudo Constructor
	 *
	 * @param iCal Entry parsed from ICS file as an array $ice
	 *
	 * @return n/a
	 */
	public function iCalRRuleFromDB($icalrowAsArray)
	{

		$db   = Factory::getDbo();
		$temp = new iCalRRule($db);

		$temp->data = $icalrowAsArray;

		// Should really test count
		$temp->processField2("freq", "YEARLY");
		$temp->processField2("count", 999);
		$temp->processField2("rinterval", 1);
		//interval ios a mysql reserved word
		$temp->_interval = $temp->rinterval;
		if ($temp->freq == "none")
		{
			$temp->processField2("until", "");
		}
		else
		{
			$cfg = JEVConfig::getInstance();
			// cap indefinate repeats if count is blank as well as until
			if (array_key_exists("COUNT", $temp->data))
			{
				$temp->processField2("until", "");
			}
			else
			{
				$temp->processField2("until", JevDate::mktime(23, 59, 59, 12, 12, JEVHelper::getMaxYear()));
			}
		}
		$temp->processField2("untilraw", "");
		$temp->processField2("bysecond", "");
		$temp->processField2("byminute", "");
		$temp->processField2("byhour", "");
		$temp->processField2("byday", "");
		$temp->processField2("bymonthday", "");
		$temp->processField2("byyearday", "");
		$temp->processField2("byweekno", "");
		$temp->processField2("bymonth", "");
		$temp->processField2("bysetpos", "");
		$temp->processField2("irregulardates", "");
		$temp->processField2("wkst", "");

		return $temp;
	}

	public function processField2($field, $default)
	{

		$this->$field = array_key_exists(strtolower($field), $this->data) ? $this->data[strtolower($field)] : $default;
	}

	/**
	 * Generates repetition from vevent & rrule data from scratch
	 * The result can then be saved to the database
	 */
	public function getRepetitions($dtstart, $dtend, $duration, $recreate = false, $exdate = array())
	{

		// put exdate into somewhere that I can get from _makerepeat
		$this->_exdate = $exdate;
		// TODO  "getRepetitions doesnt yet deal with Short months and 31st or leap years/week 53<br/>";
		if ($dtend == 0 && $duration > 0)
		{
			$dtend = $dtstart + $duration;
		}
		if (!$recreate && isset($this->_repetitions)) return $this->_repetitions;
		$this->_repetitions = array();
		if (!isset($this->eventid))
		{
			echo "no eventid set in generateRepetitions<br/>";

			return $this->_repetitions;
		}

		if ($this->count == 1 && $this->freq != "IRREGULAR")
		{
			//echo "count=1 returing<br/>";
			$this->_makeRepeat($dtstart, $dtend);

			return $this->_repetitions;
		}
		//list ($h,$min,$s,$d,$m,$y) = explode(":",JevDate::strftime("%H:%M:%S:%d:%m:%Y",$end));

		list ($startHour, $startMin, $startSecond, $startDay, $startMonth, $startYear, $startWD)
			= explode(":", JevDate::strftime("0%H:0%M:0%S:%d:%m:%Y:%w", $dtstart));
		//echo "$startHour,$startMin,$startSecond,$startDay,$startMonth,$startYear,$startWD,$dtstart<br/>";
		$dtstartMidnight = JevDate::mktime(0, 0, 0, $startMonth, $startDay, $startYear);
		list ($endHour, $endMin, $endSecond, $endDay, $endMonth, $endYear, $endWD) = explode(":", JevDate::strftime("0%H:0%M:0%S:%d:%m:%Y:%w", $dtend));
		$duration = $dtend - $dtstart;
		// duration in days (work in the middle part of the day in case the clocks change and make the end time a couple of hours later too)
		$jevStart     = JevDate::mktime(12, 0, 0, $startMonth, $startDay, $startYear);
		$jevstart     = new JevDate($jevStart);
		$jevEnd       = JevDate::mktime(15, 0, 0, $endMonth, $endDay, $endYear);
		$jevend       = new JevDate($jevEnd);
		$durationdays = $jevstart->diff($jevend)->days;
		static $weekdayMap = array("SU" => 0, "MO" => 1, "TU" => 2, "WE" => 3, "TH" => 4, "FR" => 5, "SA" => 6);
		static $weekdayReverseMap = array("SU", "MO", "TU", "WE", "TH", "FR", "SA");
		static $dailySecs = 86400;
		static $weeklySecs = 604800;
		// TODO implement byyearday
		// TODO do full leap year audit e.g. YEARLY repeats
		//echo "freq = ".$this->freq."<br/>";
		switch ($this->freq)
		{
			case "YEARLY":
				// TODO the code doesn't yet deal with multiple bymonths
				if ($this->bymonth == "") $this->bymonth = $startMonth;
				//if ($this->byday=="") $this->byday=$weekdayReverseMap[$startWD];

				// If I have byday and bymonthday then the two considions must be met
				$weekdays = array();
				if ($this->byday != "")
				{
					foreach (explode(",", str_replace(" ", "", $this->byday)) as $bday)
					{
						if (array_key_exists($bday, $weekdayMap))
						{
							$weekdays[] = $weekdayMap[$bday];
						}
					}
				}

				if ($this->byyearday != "")
				{
					echo "byyearday <br/>";
					$days = explode(",", str_replace(" ", "", $this->byyearday));

					$start            = $dtstart;
					$end              = $dtend;
					$countRepeats     = 0;
					$currentYearStart = JevDate::mktime(0, 0, 0, 1, 1, $startYear);
					// do the current month first
					while ($countRepeats < $this->count && !$this->_afterUntil($currentYearStart))
					{
						$currentYear     = JevDate::strftime("%Y", $currentYearStart);
						$currentYearDays = date("L", $currentYearStart) ? 366 : 365;
						foreach ($days as $day)
						{
							if ($countRepeats >= $this->count || $this->_afterUntil($start)) return $this->_repetitions;

							// TODO I am assuming all + or all -ve
							$details = array();
							preg_match("/(\+|-?)(\d*)/", $day, $details);
							list($temp, $plusminus, $daynumber) = $details;
							if (StringHelper::strlen($plusminus) == 0) $plusminus = "+";

							// do not go over year end
							if ($daynumber > $currentYearDays) continue;
							if ($plusminus == "+")
							{
								$targetStart = JevDate::mktime($startHour, $startMin, $startSecond, 12, 31, $currentYear - 1);
								$targetStart = JevDate::strtotime("+$daynumber days", $targetStart);
							}
							else
							{
								$targetStart = JevDate::mktime($startHour, $startMin, $startSecond, 1, 1, $currentYear + 1);
								$targetStart = JevDate::strtotime("-$daynumber days", $targetStart);
							}
							// TODO - Fix situation where summer time starts or ends for all day event here!!!
							$targetEnd = $targetStart + $duration;

							if ($countRepeats >= $this->count)
							{
								return $this->_repetitions;
							}
							if ($targetStart >= $dtstartMidnight && !$this->_afterUntil($targetStart))
							{
								// double check for byday constraints
								if ($this->byday != "")
								{
									if (!in_array(JevDate::strftime("%w", $targetStart), $weekdays))
									{
										continue;
									}
								}
								$countRepeats += $this->_makeRepeat($targetStart, $targetEnd);
							}
						}
						// now ago to the start of next year
						$maxyear = (PHP_INT_SIZE === 8) ? 2999 : 2037;
						if ($currentYear + $this->rinterval > $maxyear) return $this->_repetitions;
						$currentYearStart = JevDate::mktime(0, 0, 0, 1, 1, $currentYear + $this->rinterval);
					}
				}
				// assume for now that its an anniversary of the start month only!
				// TODO relzs this assumption !!
				else if ($this->bymonthday != "")
				{
					echo "bymonthday" . $this->bymonthday . " <br/>";
					$days = explode(",", str_replace(" ", "", $this->bymonthday));


					$start        = $dtstart;
					$end          = $dtend;
					$countRepeats = 0;

					$currentMonthStart = JevDate::mktime(0, 0, 0, $startMonth, 1, $startYear);
					// do the current month first
					while ($countRepeats < $this->count && !$this->_afterUntil($currentMonthStart))
					{
						list ($currentMonth, $currentYear) = explode(":", JevDate::strftime("%m:%Y", $currentMonthStart));
						$currentMonthDays = date("t", $currentMonthStart);
						foreach ($days as $day)
						{
							if ($countRepeats >= $this->count || $this->_afterUntil($start)) return $this->_repetitions;

							// Assume no negative bymonthday values
							// TODO relax this assumption

							// do not go over month end
							if ($day > $currentMonthDays) continue;

							$targetStart = JevDate::mktime($startHour, $startMin, $startSecond, $currentMonth, $day, $currentYear);
							$targetEnd   = $targetStart + $duration;
							if ($countRepeats >= $this->count)
							{
								return $this->_repetitions;
							}
							if ($targetStart >= $dtstartMidnight && !$this->_afterUntil($targetStart))
							{
								// double check for byday constraints
								if ($this->byday != "")
								{
									if (!in_array(JevDate::strftime("%w", $targetStart), $weekdays))
									{
										continue;
									}
								}
								$countRepeats += $this->_makeRepeat($targetStart, $targetEnd);
							}
						}
						// now ago to the start of next month
						if ($currentYear + $this->rinterval > 2099) return $this->_repetitions;
						$currentMonthStart = JevDate::mktime(0, 0, 0, $currentMonth, 1, $currentYear + $this->rinterval);
					}

				}
				// see ical RFC 2445 page 42
				/*
   Each BYDAY value can also be preceded by a positive (+n) or negative
   (-n) integer. If present, this indicates the nth occurrence of the
   specific day within the MONTHLY or YEARLY RRULE. For example, within
   a MONTHLY rule, +1MO (or simply 1MO) represents the first Monday
   within the month, whereas -1MO represents the last Monday of the
   month. If an integer modifier is not present, it means all days of
   this type within the specified frequency. For example, within a
   MONTHLY rule, MO represents all Mondays within the month.
				 */

				// annual repeats of the start date - TODO check this
				else if ($this->byday == "")
				{
					$start        = $dtstart;
					$end          = $dtend;
					$countRepeats = 0;
					while ($countRepeats < $this->count && !$this->_afterUntil($start))
					{
						$countRepeats += $this->_makeRepeat($start, $end);

						$currentYear = JevDate::strftime("%Y", $start);
						list ($h, $min, $s, $d, $m, $y) = explode(":", JevDate::strftime("%H:%M:%S:%d:%m:%Y", $start));
						$maxyear = (PHP_INT_SIZE === 8) ? 2999 : 2037;
						if (($currentYear + $this->rinterval) >= $maxyear) break;
						$start = JevDate::strtotime("+" . $this->rinterval . " years", $start);
						$end   = JevDate::strtotime("+" . $this->rinterval . " years", $end);
					}
				}
				else
				{
					$days = explode(",", str_replace(" ", "", $this->byday));
					// duplicate where necessary
					$extradays = array();
					foreach ($days as $day)
					{
						if (strpos($day, "+") === false && strpos($day, "-") === false)
						{
							for ($i = 2; $i <= 52; $i++)
							{
								$extradays[] = "+" . $i . $day;
							}
						}
					}
					$days = array_merge($days, $extradays);

					$start        = $dtstart;
					$end          = $dtend;
					$countRepeats = 0;
					$currentYear = JevDate::strftime("%Y", $start);

					// do the current month first
					while ($countRepeats < $this->count && !$this->_afterUntil($start))
					{
						$currentMonth = JevDate::strftime("%m", $start);
						foreach ($days as $day)
						{
							if ($countRepeats >= $this->count || $this->_afterUntil($start))
							{
								return $this->_repetitions;
							}

							$details = array();
							if (strpos($day, "+") === false && strpos($day, "-") === false)
							{
								$day = "+1" . $day;
							}
							preg_match("/(\+|-?)(\d+)(.{2})/", $day, $details);
							if (count($details) != 4)
							{
								echo "<br/><br/><b>PROBLEMS with $day</b><br/><br/>";

								return $this->_repetitions;
							}
							else
							{
								list($temp, $plusminus, $weeknumber, $dayname) = $details;
								if (StringHelper::strlen($plusminus) == 0) $plusminus = "+";
								if (StringHelper::strlen($weeknumber) == 0) $weeknumber = 1;

								// always check for dtstart (nothing is allowed earlier)
								if ($plusminus == "-")
								{
									//echo "count back $weeknumber weeks on $dayname<br/>";
									list ($startDay, $startMonth, $startYear, $startWD) = explode(":", JevDate::strftime("%d:%m:%Y:%w", $start));
									$startLast  = date("t", $start);
									$monthEnd   = JevDate::mktime(0, 0, 0, $startMonth, $startLast, $startYear);
									$meWD       = JevDate::strftime("%w", $monthEnd);
									$adjustment = $startLast - (7 + $meWD - $weekdayMap[$dayname]) % 7;

									$targetstartDay = $adjustment - ($weeknumber - 1) * 7;
									$targetendDay   = $targetstartDay + $endDay - $startDay;
									list ($h, $min, $s, $d, $m, $y) = explode(":", JevDate::strftime("%H:%M:%S:%d:%m:%Y", $start));

									$testStart = JevDate::mktime($h, $min, $s, $m, $targetstartDay, $y);
									if ($currentMonth == JevDate::strftime("%m", $testStart))
									{

										$targetStart = $testStart;
										// WE can't just add the duration since if summer time starts/ends within the event length then the end and possibly the date could be wrong
										$targetEnd = $targetStart + $duration;
										$targetEnd = JevDate::mktime($endHour, $endMin, $endSecond, $currentMonth, $targetstartDay + $durationdays, $currentYear);

										if ($countRepeats >= $this->count)
										{
											return $this->_repetitions;
										}
										if ($targetStart >= $dtstartMidnight && !$this->_afterUntil($targetStart))
										{
											$countRepeats += $this->_makeRepeat($targetStart, $targetEnd);
										}
									}
								}
								else
								{
									//echo "count forward $weeknumber weeks on $dayname<br/>";
									list ($startDay, $startMonth, $startYear, $startWD) = explode(":", JevDate::strftime("%d:%m:%Y:%w", $start));
									$monthStart = JevDate::mktime(0, 0, 0, $startMonth, 1, $startYear);
									$msWD       = JevDate::strftime("%w", $monthStart);
									if (!isset($weekdayMap[$dayname]))
									{
										$x = 1;
									}
									$adjustment = 1 + (7 + $weekdayMap[$dayname] - $msWD) % 7;

									$targetstartDay = $adjustment + ($weeknumber - 1) * 7;
									$targetendDay   = $targetstartDay + $endDay - $startDay;
									list ($h, $min, $s, $d, $m, $y) = explode(":", JevDate::strftime("%H:%M:%S:%d:%m:%Y", $start));

									$testStart = JevDate::mktime($h, $min, $s, $m, $targetstartDay, $y);
									if ($currentMonth == JevDate::strftime("%m", $testStart))
									{
										$targetStart = $testStart;
										// WE can't just add the duration since if summer time starts/ends within the event length then the end and possibly the date could be wrong
										$targetEnd = $targetStart + $duration;
										$targetEnd = JevDate::mktime($endHour, $endMin, $endSecond, $currentMonth, $targetstartDay + $durationdays, $currentYear);
										if ($countRepeats >= $this->count)
										{
											return $this->_repetitions;
										}
										if ($targetStart >= $dtstartMidnight && !$this->_afterUntil($targetStart))
										{
											$countRepeats += $this->_makeRepeat($targetStart, $targetEnd);
										}
									}
								}
							}
						}
						// now ago to the start of the next month
						$start = $targetStart;
						$end   = $targetEnd;
						list ($h, $min, $s, $d, $m, $y) = explode(":", JevDate::strftime("%H:%M:%S:%d:%m:%Y", $start));
						if (($y + $this->rinterval + $m / 12) > 2099) return $this->_repetitions;
						$start = JevDate::mktime($h, $min, $s, $m, 1, $y + $this->rinterval);
						$end   = $start + $duration;
					}

				}

				return $this->_repetitions;
				break;
			case "MONTHLY":
				if ($this->bymonthday == "" && $this->byday == "")
				{
					$this->bymonthday = $startDay;
				}
				if ($this->bymonthday != "")
				{
					echo "bymonthday" . $this->bymonthday . " <br/>";
					// if not byday then by monthday
					$days = explode(",", str_replace(" ", "", $this->bymonthday));
					// If I have byday and bymonthday then the two considions must be met
					$weekdays = array();
					if ($this->byday != "")
					{
						foreach (explode(",", str_replace(" ", "", $this->byday)) as $bday)
						{
							$weekdays[] = $weekdayMap[$bday];
						}
					}

					$start             = $dtstart;
					$end               = $dtend;
					$countRepeats      = 0;
					$currentMonthStart = JevDate::mktime(0, 0, 0, $startMonth, 1, $startYear);

					// do the current month first
					while ($countRepeats < $this->count && !$this->_afterUntil($currentMonthStart))
					{
						//echo $countRepeats ." ".$this->count." ".$currentMonthStart."<br/>";
						list ($currentMonth, $currentYear) = explode(":", JevDate::strftime("%m:%Y", $currentMonthStart));
						$currentMonthDays = date("t", $currentMonthStart);
						foreach ($days as $day)
						{
							if ($countRepeats >= $this->count || $this->_afterUntil($start)) return $this->_repetitions;

							$details = array();
							preg_match("/(\+|-?)(\d+)/", $day, $details);
							if (count($details) != 3)
							{
								echo "<br/><br/><b>PROBLEMS with $day</b><br/><br/>";

								return $this->_repetitions;
							}
							else
							{
								list($temp, $plusminus, $daynumber) = $details;
								if (StringHelper::strlen($plusminus) == 0) $plusminus = "+";
								if (StringHelper::strlen($daynumber) == 0) $daynumber = $startDay;

								// always check for dtstart (nothing is allowed earlier)
								if ($plusminus == "-")
								{
									// must not go before start of month etc.
									if ($daynumber > $currentMonthDays) continue;

									echo "I need to check negative bymonth days <br/>";
									$targetStart = JevDate::mktime($startHour, $startMin, $startSecond, $currentMonth, $currentMonthDays + 1 - $daynumber, $currentYear);
									$targetEnd   = $targetStart + $duration;
									if ($countRepeats >= $this->count)
									{
										return $this->_repetitions;
									}
									if ($targetStart >= $dtstartMidnight && !$this->_afterUntil($targetStart))
									{
										$countRepeats += $this->_makeRepeat($targetStart, $targetEnd);
									}
								}
								else
								{
									//echo "$daynumber $currentMonthDays bd=".$this->byday." <br/>";
									// must not go over end month etc.
									if ($daynumber > $currentMonthDays) continue;
									//echo "$startHour,$startMin,$startSecond,$currentMonth,$daynumber,$currentYear<br/>";
									$targetStart = JevDate::mktime($startHour, $startMin, $startSecond, $currentMonth, $daynumber, $currentYear);
									$targetEnd   = $targetStart + $duration;
									// WE can't just add the duration since if summer time starts/ends within the event length then the end and possibly the date could be wrong
									$targetEnd = JevDate::mktime($endHour, $endMin, $endSecond, $currentMonth, $daynumber + $durationdays, $currentYear);
									//echo "$targetStart $targetEnd $dtstartMidnight<br/>";
									if ($countRepeats >= $this->count)
									{
										return $this->_repetitions;
									}
									if ($targetStart >= $dtstartMidnight && !$this->_afterUntil($targetStart))
									{
										// double check for byday constraints
										if ($this->byday != "")
										{
											if (!in_array(JevDate::strftime("%w", $targetStart), $weekdays))
											{
												continue;
											}
										}
										$countRepeats += $this->_makeRepeat($targetStart, $targetEnd);
										//echo "countrepeats = $countRepeats<br/>";
									}
								}
							}
						}
						// now ago to the start of next month
						if (($currentYear + ($currentMonth + $this->rinterval) / 12) > 2099) return $this->_repetitions;
						$currentMonthStart = JevDate::mktime(0, 0, 0, $currentMonth + $this->rinterval, 1, $currentYear);
					}

				}
				// This is byday
				else
				{
					$days = explode(",", str_replace(" ", "", $this->byday));
					// TODO I should also iterate over week number if this is used
					//$weeknumbers = explode(",",str_replace(" ", "", $this->byweekno));

					if ($this->bysetpos != "")
					{
						$newdays      = array();
						$setpositions = explode(",", str_replace(" ", "", $this->bysetpos));
						foreach ($setpositions as $setposition)
						{
							foreach ($days as $day)
							{
								if (strpos($setposition, "+") === false && strpos($setposition, "-") === false)
								{
									$setposition = "+" . $setposition;
								}
								$newdays[] = $setposition . $day;
							}
						}
						$days        = $newdays;
						$this->byday = implode(",", $days);
					}

					$start             = $dtstart;
					$end               = $dtend;
					$countRepeats      = 0;
					$currentMonthStart = JevDate::mktime(0, 0, 0, $startMonth, 1, $startYear);

					// do the current month first
					while ($countRepeats < $this->count && !$this->_afterUntil($currentMonthStart))
					{
						list ($currentMonth, $currentYear, $currentMonthStartWD) = explode(":", JevDate::strftime("%m:%Y:%w", $currentMonthStart));
						$currentMonthDays = date("t", $currentMonthStart);
						$this->sortByDays($days, $currentMonthStart, $dtstart);

						foreach ($days as $day)
						{
							if ($countRepeats >= $this->count || $this->_afterUntil($start))
							{
								return $this->_repetitions;
							}

							$details = array();
							preg_match("/(\+|-?)(\d?)(.+)/", $day, $details);
							if (count($details) != 4)
							{
								echo "<br/><br/><b>PROBLEMS with $day</b><br/><br/>";

								return $this->_repetitions;
							}
							else
							{
								list($temp, $plusminus, $weeknumber, $dayname) = $details;
								if (StringHelper::strlen($plusminus) == 0) $plusminus = "+";
								if (StringHelper::strlen($weeknumber) == 0) $weeknumber = 1;

								$multiplier = $plusminus == "+" ? 1 : -1;
								// always check for dtstart (nothing is allowed earlier)
								if ($plusminus == "-")
								{
									//echo "count back $weeknumber weeks on $dayname<br/>";
									$startLast         = date("t", $currentMonthStart);
									$currentMonthEndWD = ($startLast - 1 + $currentMonthStartWD) % 7;

									$adjustment = $startLast - (7 + $currentMonthEndWD - $weekdayMap[$dayname]) % 7;

									$targetstartDay = $adjustment - ($weeknumber - 1) * 7;
								}
								else
								{
									//echo "count forward $weeknumber weeks on $dayname<br/>";
									$adjustment = 1 + (7 + $weekdayMap[$dayname] - $currentMonthStartWD) % 7;

									$targetstartDay = $adjustment + ($weeknumber - 1) * 7;

								}
								$targetStart = JevDate::mktime($startHour, $startMin, $startSecond, $currentMonth, $targetstartDay, $currentYear);

								if ($currentMonth == JevDate::strftime("%m", $targetStart))
								{
									// WE can't just add the duration since if summer time starts/ends within the event length then the end and possibly the date could be wrong
									$targetEnd = $targetStart + $duration;
									$targetEnd = JevDate::mktime($endHour, $endMin, $endSecond, $currentMonth, $targetstartDay + $durationdays, $currentYear);
									if ($countRepeats >= $this->count)
									{
										return $this->_repetitions;
									}
									if ($targetStart >= $dtstartMidnight && !$this->_afterUntil($targetStart))
									{
										$countRepeats += $this->_makeRepeat($targetStart, $targetEnd);
									}
								}

							}
						}
						// now go to the start of next month
						if (($currentYear + ($currentMonth + $this->rinterval) / 12) > 2099) return $this->_repetitions;
						$currentMonthStart = JevDate::mktime(0, 0, 0, $currentMonth + $this->rinterval, 1, $currentYear);
					}
				}

				return $this->_repetitions;

				break;
			case "WEEKLY":
				$days           = explode(",", str_replace(" ", "", $this->byday));
				$start          = $dtstart;
				$end            = $dtend;
				$countRepeats   = 0;
				$currentWeekDay = JevDate::strftime("%w", $start);
				// Go to the zero day of the first week (even if this is in the past)
				// this will be the base from which we count the weeks and weekdays
				$currentWeekStart = JevDate::strtotime("-$currentWeekDay days", $start);

				// no BYDAY specified
				if ($this->byday == "")
				{
					$daynames    = array("SU", "MO", "TU", "WE", "TH", "FR", "SA", "SU");
					$this->byday = "+" . $daynames[$currentWeekDay];
					$days        = array($this->byday);
				}
				while ($countRepeats < $this->count && !$this->_afterUntil($currentWeekStart))
				{
					list ($currentDay, $currentMonth, $currentYear) = explode(":", JevDate::strftime("%d:%m:%Y", $currentWeekStart));

					foreach ($days as $day)
					{
						if ($countRepeats >= $this->count || $this->_afterUntil($start))
						{
							return $this->_repetitions;
						}
						$details = array();
						preg_match("/(\+|-?)(\d?)(.+)/", $day, $details);
						if (count($details) != 4)
						{
							continue;
							echo "<br/><br/><b>PROBLEMS with $day</b><br/><br/>";

							return $this->_repetitions;
						}
						else
						{
							list($temp, $plusminus, $daynumber, $dayname) = $details;
							if (StringHelper::strlen($plusminus) == 0) $plusminus = "+";
							// this is not relevant for weekly recurrence ?!?!?
							//if (StringHelper::strlen($daynumber)==0) $daynumber=1;
							$multiplier = $plusminus == "+" ? 1 : -1;
							if ($plusminus == "-")
							{
								// TODO find out if I ever have this situation?
								// It would seem meaningless
							}
							else
							{
								//echo "count forward $daynumber days on $dayname<br/>";
								$targetstartDay = $currentDay + $weekdayMap[$dayname];
							}

							$targetStart = JevDate::mktime($startHour, $startMin, $startSecond, $currentMonth, $targetstartDay, $currentYear);

							// WE can't just add the duration since if summer time starts/ends within the event length then the end and possibly the date could be wrong
							$targetEnd = $targetStart + $duration;
							$targetEnd = JevDate::mktime($endHour, $endMin, $endSecond, $currentMonth, $targetstartDay + $durationdays, $currentYear);
							if ($countRepeats >= $this->count)
							{
								return $this->_repetitions;
							}
							if ($targetStart >= $dtstartMidnight && !$this->_afterUntil($targetStart))
							{
								$countRepeats += $this->_makeRepeat($targetStart, $targetEnd);
							}

						}
					}

					// now go to the start of next week
					if ($currentYear + ($currentMonth / 12) > 2099) return $this->_repetitions;
					$currentWeekStart = JevDate::strtotime("+" . ($this->rinterval) . " weeks", $currentWeekStart);

				}

				return $this->_repetitions;
				break;
			case "DAILY":
				$start        = $dtstart;
				$end          = $dtend;
				$countRepeats = 0;

				$startYear = JevDate::strftime("%Y", $start);
				while ($startYear < 2027 && $countRepeats < $this->count && !$this->_afterUntil($start))
				{
					//while ($startYear<5027 && $countRepeats < $this->count && !$this->_afterUntil($start)) {
					$countRepeats += $this->_makeRepeat($start, $end);
					$start        = JevDate::strtotime("+" . $this->rinterval . " days", $start);
					$end          = JevDate::strtotime("+" . $this->rinterval . " days", $end);
					$startYear    = JevDate::strftime("%Y", $start);
				}

				return $this->_repetitions;
				break;
			case "IRREGULAR":
				$processedDates = array();
				// current date is ALWAYS a repeat
				$processedDates[] = $dtstart;
				$this->_makeRepeat($dtstart, $dtend);
				if (is_string($this->irregulardates) && $this->irregulardates != "")
				{
					$this->irregulardates = @json_decode(str_replace("'", '"', trim($this->irregulardates, '"')));
					if (is_array($this->_irregulardates))
					{
						array_walk($this->irregulardates, function (& $item, $index) {

							if (!is_int($item))
							{
								$item = JevDate::strtotime($item . " 00:00:00");
							}
						});
					}
				}
				if (!is_array($this->irregulardates))
				{
					$this->irregulardates = array();
				}
				sort($this->irregulardates);
				foreach ($this->irregulardates as $irregulardate)
				{
					// avoid duplicate values
					if (in_array($irregulardate, $processedDates))
					{
						//continue;
					}
					$processedDates[] = $irregulardate;
					// find the start and end times of the initial event
					$irregulardate += ($dtstart - $dtstartMidnight);
					$this->_makeRepeat($irregulardate, $irregulardate + $duration);
				}

				return $this->_repetitions;
				break;
			default:
				echo "UNKNOWN TYPE<br/>";

				return $this->_repetitions;
				break;
		}
	}

	/**
	 * Creates a repeat if not an exception date returns 1 anyway
	 *
	 * @param unknown_type $start
	 * @param unknown_type $end
	 *
	 * @return unknown
	 */
	public function _makeRepeat($start, $end)
	{

		if (!isset($this->_repetitions)) $this->_repetitions = array();
		$db              = Factory::getDbo();
		$repeat          = new iCalRepetition($db);
		$repeat->eventid = $this->eventid;
		// TODO CHECK THIS logic
		$repeat->startrepeat = JevDate::strftime('%Y-%m-%d %H:%M:%S', $start);
		// iCal for whole day uses 00:00:00 on the next day JEvents uses 23:59:59 on the same day
		list ($h, $m, $s) = explode(":", JevDate::strftime("%H:%M:%S", $end));
		if (($h + $m + $s) == 0)
		{
			//			$repeat->endrepeat = JevDate::strftime('%Y-%m-%d 23:59:59',($end-86400));
			$duration          = $end - $start;
			$repeat->endrepeat = JevDate::strftime('%Y-%m-%d %H:%M:%S', $start + $duration - 1);
			//$repeat->endrepeat = JevDate::strftime('%Y-%m-%d 23:59:59',$end);
		}
		else
		{
			$repeat->endrepeat = JevDate::strftime('%Y-%m-%d %H:%M:%S', $end);
		}

		$repeat->duplicatecheck = md5($repeat->eventid . $start);

		// Double check its not in the list of exception dates
		foreach ($this->_exdate as $exdate)
		{
			// compare based on YYYYMMDD since exceptions are(normally) whole day, "EXDATE;VALUE=DATE"
			if (JevDate::strftime('%Y%m%d', $exdate) == JevDate::strftime('%Y%m%d', $start))
			{
				// return 0;
				// exceptions count
				return 1;
			}
		}

		// if we are saving a set of irregular repeats initially they have the same time so the duplicatecheck fails
		// so spoof this for repeat sother than the first
		if (in_array($repeat, $this->_repetitions))
		{
			static $duplicates = 1;
			$repeat->duplicatecheck = md5($repeat->eventid . $start . " " . $duplicates);
			$duplicates++;
		}
		$this->_repetitions[] = $repeat;

		return 1;
	}

	public function _afterUntil($testDate)
	{

		if (StringHelper::strlen($this->until) == 0 || $this->until === 0) return false;
		if (!isset($this->_untilMidnight))
		{
			list ($d, $m, $y) = explode(":", JevDate::strftime("%d:%m:%Y", $this->until));
			$this->_untilMidnight = JevDate::mktime(23, 59, 59, $m, $d, $y);
		}
		if (StringHelper::strlen($this->until) > 0 && $testDate > intval($this->_untilMidnight))
		{
			return true;
		}
		else return false;
	}

	/**
	 * Technically this is very complicated
	 * see http://www.w3.org/2002/12/cal/rfc2445 ad search for "BYxxx rule parts modify the recurrence in some manner"
	 *
	 * Priority to 'analysis' should therefore be (??)
	 *
	 * FREQ=YEARLY
	 * BYMONTH
	 * BYWEEKNO
	 * BYYEARDAY
	 * FREQ=MONTHLY ??
	 * BYMONTHDAY
	 * FREQ=WEEKLY ??
	 * BYDAY
	 * FREQ=DAILY ??
	 * BYHOUR, BYMINUTE, BYSECOND
	 * BYSETPOS
	 *
	 * INTERVAL always applies to FREQ
	 *
	 * So if I go over step in freq units adding the BYMONTH to YEARLY etc.
	 * then restricting DAILY with BYMONTH (do an excessive loop in this situation and test if the rules hols
	 * until I get some better logic)
	 */

	/**
	 * sort the by days string for negative values so that we start at the beginning of the month/start date
	 *
	 * @param unknown_type $days
	 * @param unknown_type $currentMonthStart
	 * @param unknown_type $dtstart
	 */
	public function sortByDays(&$days, $currentMonthStart, $dtstart)
	{

		if (count($days) == 0) return;
		// only sort negative values
		if (strpos($days[0], "-") === false) return;

		static $weekdayMap = array("SU" => 0, "MO" => 1, "TU" => 2, "WE" => 3, "TH" => 4, "FR" => 5, "SA" => 6);
		static $weekdayReverseMap = array("SU", "MO", "TU", "WE", "TH", "FR", "SA");

		list ($currentMonth, $currentYear) = explode(":", JevDate::strftime("%m:%Y", $currentMonthStart));
		list ($startMonth, $startYear) = explode(":", JevDate::strftime("%m:%Y", $dtstart));
		if ($startMonth == $currentMonth && $startYear == $currentYear)
		{
			$startdate = $dtstart;
		}
		else
		{
			$startdate = $currentMonthStart;
		}
		$startWD = JevDate::strftime("%w", $startdate);

		$sorteddays = array();
		// start from week -6 and go forward (overkill I know)
		for ($w = -6; $w < 0; $w++)
		{
			// now loop over the week starting at the appropriate day fo the week
			for ($i = 0; $i < 7; $i++)
			{
				$wd    = ($startWD + $i) % 7;
				$check = strval($w) . $weekdayReverseMap[$wd];
				if (in_array($check, $days)) $sorteddays[] = $check;
			}
		}
		$days = $sorteddays;

	}

	public function dumpData()
	{

		echo "Freq : $this->freq <br/>";
		echo "Interval : " . $this->data['INTERVAL'] . "<br/>";
		switch ($this->freq)
		{
			case "YEARLY":
				echo "By Month : " . $this->data['BYMONTH'] . "<br/>";
				break;
			case "MONTHLY":
				echo "By Day : " . $this->data['BYDAY'] . "<br/>";
				$days = explode(",", str_replace(" ", "", $this->data['BYDAY']));
				foreach ($days as $day)
				{
					$details = array();
					preg_match("/(\+|-?)(\d?)(.+)/", $day, $details);
					if (count($details) != 4) echo "<br/><br/><b>PROBLEMS with $day</b><br/><br/>";
					else
					{
						if (StringHelper::strlen($details[1]) == 0) $details[1] = "+";
						echo "Event repeat details<br/>";
						if ($details[1] == "-") echo "count back $details[2] weeks on $details[3]<br/>";
						else echo "count forward $details[2] weeks on $details[3]<br/>";

						// Note if no number given then EVERY specified day!!
					}
				}
				break;
			case "WEEKLY":
				echo "By Day : " . $this->data['BYDAY'] . "<br/>";
				$days = explode(",", str_replace(" ", "", $this->data['BYDAY']));
				foreach ($days as $day)
				{
					$details = array();
					preg_match("/(\+|-?)(\d?)(.+)/", $day, $details);
					if (count($details) != 4) echo "<br/><br/><b>PROBLEMS with $day</b><br/><br/>";
					else
					{
						if (StringHelper::strlen($details[1]) == 0) $details[1] = "+";
						echo "Event repeat details<br/>";
						if ($details[1] == "-") echo "count back $details[2] weeks on $details[3]<br/>";
						else echo "count forward $details[2] weeks on $details[3]<br/>";

						// Note if no number given then EVERY specified day!!
					}
				}
				break;
			default:
				echo "UNKNOWN TYPE<br/>";
				break;
		}

		// Doesnt yet deal with INTERVAL, UNTIL or COUNT
		print_r($this->data);
		echo "<hr/>";
	}

	public function eventInPeriod($startDate, $endDate, $start, $end)
	{

		// stupid verison to start that scans through EVERY single day!!!
		$checkDate = $startDate;
		while ($checkDate <= $endDate)
		{
			if ($this->checkDate($checkDate, $start, $end)) return true;
			$checkDate += 24 * 60 * 60;
		}

		return false;
	}

	public function checkDate($test, $start, $end)
	{

		if ($test >= $start && $test <= $end) return true;
		else return false;
	}

	public function isDuplicate()
	{

		$sql = "SELECT rr_id from #__jevents_rrule as rr WHERE rr.eventid = '" . $this->eventid . "'";
		$this->_db->setQuery($sql);
		$matches = $this->_db->loadObjectList();
		if (count($matches) > 0 && isset($matches[0]->rr_id))
		{
			return $matches[0]->rr_id;
		}

		return false;
	}
}
