<?php

/**
 * Helper for recurrence rules.
 *
 * @author     Time.ly Network Inc.
 * @since      2.0
 *
 * @package    AI1EC
 * @subpackage AI1EC.Recurrence
 */
class Ai1ec_Recurrence_Rule extends Ai1ec_Base {

	/**
	 * Return given recurrence data as text.
	 *
	 * @param  string  $rrule   Recurrence rule
	 *
	 * @return string
	 */
	public function rrule_to_text( $rrule = '' ) {
		$txt = '';
		$rc  = new SG_iCal_Recurrence( new SG_iCal_Line( 'RRULE:' . $rrule ) );
		switch( $rc->getFreq() ) {
			case 'DAILY':
				$this->_get_interval( $txt, 'daily', $rc->getInterval() );
				$this->_ending_sentence( $txt, $rc );
				break;
			case 'WEEKLY':
				$this->_get_interval( $txt, 'weekly', $rc->getInterval() );
				$this->_get_sentence_by( $txt, 'weekly', $rc );
				$this->_ending_sentence( $txt, $rc );
				break;
			case 'MONTHLY':
				$this->_get_interval( $txt, 'monthly', $rc->getInterval() );
				$this->_get_sentence_by( $txt, 'monthly', $rc );
				$this->_ending_sentence( $txt, $rc );
				break;
			case 'YEARLY':
				$this->_get_interval( $txt, 'yearly', $rc->getInterval() );
				$this->_get_sentence_by( $txt, 'yearly', $rc );
				$this->_ending_sentence( $txt, $rc );
				break;
			default:
				$processed = explode( '=', $rrule );
				if (
					isset( $processed[1] ) &&
					in_array(
						strtoupper( $processed[0] ),
						array( 'RDATE', 'EXDATE' )
					)
				) {
					$txt = $this->exdate_to_text( $processed[1] );
				} else {
					$txt = $rrule;
				}
		}
		return $txt;
	}

	/**
	 * Parse a `recurrence rule' into an array that can be used to calculate
	 * recurrence instances.
	 *
	 * @see http://kigkonsult.se/iCalcreator/docs/using.html#EXRULE
	 *
	 * @param string $rule
	 * @return array
	 */
	public function build_recurrence_rules_array( $rule ) {
		$rules     = array();
		$rule_list = explode( ';', $rule );
		foreach ( $rule_list as $single_rule ) {
			if ( false === strpos( $single_rule, '=' ) ) {
				continue;
			}
			list( $key, $val ) = explode( '=', $single_rule );
			$key               = strtoupper( $key );
			switch ( $key ) {
				case 'BYDAY':
					$rules['BYDAY'] = array();
					foreach ( explode( ',', $val ) as $day ) {
						$rule_map = $this->create_byday_array( $day );
						$rules['BYDAY'][] = $rule_map;
						if (
							preg_match( '/FREQ=(MONTH|YEAR)LY/i', $rule ) &&
							1 === count( $rule_map )
						) {
							// monthly/yearly "last" recurrences need day name
							$rules['BYDAY']['DAY'] = substr(
								$rule_map['DAY'],
								-2
							);
						}
					}
					break;

				case 'BYMONTHDAY':
				case 'BYMONTH':
					if ( false === strpos( $val, ',' ) ) {
						$rules[$key] = $val;
					} else {
						$rules[$key] = explode( ',', $val );
					}
					break;

				default:
					$rules[$key] = $val;
			}
		}
		return $rules;
	}

	/**
	 * _merge_exrule method
	 *
	 * Merge RRULE values to EXRULE, to ensure, that it matches the according
	 * repetition values, it is meant to exclude.
	 *
	 * NOTE: one shall ensure, that RRULE values are placed in between EXRULE
	 * keys, so that wording in UI would remain the same after mangling.
	 *
	 * @param string $exrule Value for EXRULE provided by user
	 * @param string $rrule  Value for RRULE provided by user
	 *
	 * @return string Modified value to use for EXRULE
	 */
	public function merge_exrule( $exrule, $rrule ) {
		$list_exrule = explode( ';', $exrule );
		$list_rrule  = explode( ';', $rrule  );
		$map_exrule  = $map_rrule   = array();
		foreach ( $list_rrule as $entry ) {
			if ( empty( $entry ) ) {
				continue;
			}
			list( $key, $value ) = explode( '=', $entry );
			$map_rrule[$key] = $value;
		}
		foreach ( $list_exrule as $entry ) {
			if ( empty( $entry ) ) {
				continue;
			}
			list( $key, $value ) = explode( '=', $entry );
			$map_exrule[$key] = $value;
		}

		$resulting_map = array_merge( $map_rrule, $map_exrule );
		$result_rule   = array();
		foreach ( $resulting_map as $key => $value ) {
			$result_rule[] = $key . '=' . $value;
		}
		$result_rule = implode( ';', $result_rule );
		return $result_rule;
	}

	/**
	 * Return given exception dates as text.
	 *
	 * @param  array   $exception_dates Dates to translate
	 *
	 * @return string
	 */
	public function exdate_to_text( $exception_dates ) {
		$dates_to_add = array();
		foreach ( explode( ',', $exception_dates ) as $_exdate ) {
			$date_format    = $this->_registry->get( 'model.option' )
				->get( 'date_format', 'l, M j, Y' );
			$dates_to_add[] = $this->_registry->get(
				'date.time',
				vsprintf(
					'%04d-%02d-%02d',
					sscanf(
						$_exdate,
						'%04d%02d%02dT%dZ'
					)
				),
				'sys.default'
				)
				->format_i18n( $date_format );
		}
		$dates_to_add = str_replace( ' ', '&nbsp;', $dates_to_add );
		// append dates to the string and return it;
		return implode( '; ', $dates_to_add );
	}

	/**
	 * Filter recurrence / exclusion rule or dates. Avoid throwing exception for old, malformed values.
	 *
	 * @param string $rule Rule or dates value.
	 *
	 * @return string Fixed rule or dates value.
	 */
	public function filter_rule( $rule ) {
		$matches = null;
		if (
			empty( $rule ) ||
			! preg_match('/(T[0-9]+)(ZUNTIL=[0-9Z;T]+)/i', $rule, $matches)
		) {
			return $rule;
		}
		return preg_replace('/(T[0-9]+)(ZUNTIL=[0-9Z;T]+)/i', '$1', $rule);
	}

	/**
	 * when using BYday you need an array of arrays.
	 * This function create valid arrays that keep into account the presence
	 * of a week number beofre the day
	 *
	 * @param string $val
	 * @return array
	 */
	protected function create_byday_array( $val ) {
		$week = substr( $val, 0, 1 );
		if ( is_numeric( $week ) ) {
			return array( $week, 'DAY' => substr( $val, 1 ) );
		}
		return array( 'DAY' => $val );
	}

	/**
	 * _get_sentence_by function
	 *
	 * @internal
	 *
	 * @return void
	 **/
	protected function _get_sentence_by( &$txt, $freq, $rc ) {
		global $wp_locale;

		switch( $freq ) {
			case 'weekly':
				if( $rc->getByDay() ) {
					if( count( $rc->getByDay() ) > 1 ) {
						// if there are more than 3 days
						// use days's abbr
						if( count( $rc->getByDay() ) > 2 ) {
							$_days = '';
							foreach( $rc->getByDay() as $d ) {
								$day = $this->get_weekday_by_id( $d, true );
								$_days .= ' ' . $wp_locale->weekday_abbrev[$wp_locale->weekday[$day]] . ',';
							}
							// remove the last ' and'
							$_days = substr( $_days, 0, -1 );
							$txt .= ' ' . Ai1ec_I18n::_x( 'on', 'Recurrence editor - weekly tab' ) . $_days;
						} else {
							$_days = '';
							foreach( $rc->getByDay() as $d ) {
								$day = $this->get_weekday_by_id( $d, true );
								$_days .= ' ' . $wp_locale->weekday[$day] . ' ' . Ai1ec_I18n::__( 'and' );
							}
							// remove the last ' and'
							$_days = substr( $_days, 0, -4 );
							$txt .= ' ' . Ai1ec_I18n::_x( 'on', 'Recurrence editor - weekly tab' ) . $_days;
						}
					} else {
						$_days = '';
						foreach( $rc->getByDay() as $d ) {
							$day = $this->get_weekday_by_id( $d, true );
							$_days .= ' ' . $wp_locale->weekday[$day];
						}
						$txt .= ' ' . Ai1ec_I18n::_x( 'on', 'Recurrence editor - weekly tab' ) . $_days;
					}
				}
				break;
			case 'monthly':
				if( $rc->getByMonthDay() ) {
					// if there are more than 2 days
					if( count( $rc->getByMonthDay() ) > 2 ) {
						$_days = '';
						foreach( $rc->getByMonthDay() as $m_day ) {
							$_days .= ' ' . $this->_ordinal( $m_day ) . ',';
						}
						$_days = substr( $_days, 0, -1 );
						$txt .= ' ' . Ai1ec_I18n::_x( 'on', 'Recurrence editor - monthly tab' ) . $_days . ' ' . Ai1ec_I18n::__( 'of the month' );
					} else if( count( $rc->getByMonthDay() ) > 1 ) {
						$_days = '';
						foreach( $rc->getByMonthDay() as $m_day ) {
							$_days .= ' ' . $this->_ordinal( $m_day ) . ' ' . Ai1ec_I18n::__( 'and' );
						}
						$_days = substr( $_days, 0, -4 );
						$txt .= ' ' . Ai1ec_I18n::_x( 'on', 'Recurrence editor - monthly tab' ) . $_days . ' ' . Ai1ec_I18n::__( 'of the month' );
					} else {
						$_days = '';
						foreach( $rc->getByMonthDay() as $m_day ) {
							$_days .= ' ' . $this->_ordinal( $m_day );
						}
						$txt .= ' ' . Ai1ec_I18n::_x( 'on', 'Recurrence editor - monthly tab' ) . $_days . ' ' . Ai1ec_I18n::__( 'of the month' );
					}
				} elseif( $rc->getByDay() ) {
					$_days = '';
					$dnum  = '';
					foreach( $rc->getByDay() as $d ) {
						if ( ! preg_match( '|^((-?)\d+)([A-Z]{2})$|', $d, $matches ) ) {
							continue;
						}
						$_dnum  = $matches[1];
						$_day   = $matches[3];
						if ( '-' === $matches[2] ) {
							$dnum = ' ' . Ai1ec_I18n::__( 'last' );
						} else {
							$dnum   = ' ' . $this->_registry->get(
								'date.time',
								strtotime( $_dnum . '-01-1998 12:00:00' )
							)->format_i18n( 'jS' );
						}
						$day    = $this->get_weekday_by_id( $_day, true );
						$_days .= ' ' . $wp_locale->weekday[$day];
					}
					$txt .= ' ' . Ai1ec_I18n::_x( 'on', 'Recurrence editor - monthly tab' ) . $dnum . $_days;
				}
				break;
			case 'yearly':
				if( $rc->getByMonth() ) {
					// if there are more than 2 months
					if( count( $rc->getByMonth() ) > 2  ) {
						$_months = '';
						foreach( $rc->getByMonth() as $_m ) {
							$_m = $_m < 10 ? 0 . $_m : $_m;
							$_months .= ' ' . $wp_locale->month_abbrev[$wp_locale->month[$_m]] . ',';
						}
						$_months = substr( $_months, 0, -1 );
						$txt .= ' ' . Ai1ec_I18n::_x( 'on', 'Recurrence editor - yearly tab' ) . $_months;
					} else if( count( $rc->getByMonth() ) > 1 ) {
						$_months = '';
						foreach( $rc->getByMonth() as $_m ) {
							$_m = $_m < 10 ? 0 . $_m : $_m;
							$_months .= ' ' . $wp_locale->month[$_m] . ' ' . Ai1ec_I18n::__( 'and' );
						}
						$_months = substr( $_months, 0, -4 );
						$txt .= ' ' . Ai1ec_I18n::_x( 'on', 'Recurrence editor - yearly tab' ) . $_months;
					} else {
						$_months = '';
						foreach( $rc->getByMonth() as $_m ) {
							$_m = $_m < 10 ? 0 . $_m : $_m;
							$_months .= ' ' . $wp_locale->month[$_m];
						}
						$txt .= ' ' . Ai1ec_I18n::_x( 'on', 'Recurrence editor - yearly tab' ) . $_months;
					}
				}
				break;
		}
	}

	/**
	 * _ordinal function
	 *
	 * @internal
	 *
	 * @return void
	 **/
	protected function _ordinal( $cdnl ) {
		$locale = explode( '_', get_locale() );

		if( isset( $locale[0] ) && $locale[0] != 'en' )
			return $cdnl;

		$test_c = abs($cdnl) % 10;
		$ext = ( ( abs( $cdnl ) % 100 < 21 && abs( $cdnl ) % 100 > 4 ) ? 'th'
			: ( ( $test_c < 4 ) ? ( $test_c < 3 ) ? ( $test_c < 2 ) ? ( $test_c < 1 )
				? 'th' : 'st' : 'nd' : 'rd' : 'th' ) );
		return $cdnl.$ext;
	}

	/**
	 * Returns the textual representation of the given recurrence frequency and
	 * interval, with result stored in $txt.
	 *
	 * @internal
	 *
	 * @return void
	 */
	protected function _get_interval( &$txt, $freq, $interval ) {
		switch ( $freq ) {
			case 'daily':
				// check if interval is set
				if ( ! $interval || $interval == 1 ) {
					$txt = Ai1ec_I18n::__( 'Daily' );
				} else {
					if ( $interval == 2 ) {
						$txt = Ai1ec_I18n::__( 'Every other day' );
					} else {
						$txt = sprintf(
							Ai1ec_I18n::__( 'Every %d days' ),
							$interval
						);
					}
				}
				break;
			case 'weekly':
				// check if interval is set
				if ( ! $interval || $interval == 1 ) {
					$txt = Ai1ec_I18n::__( 'Weekly' );
				} else {
					if ( $interval == 2 ) {
						$txt = Ai1ec_I18n::__( 'Every other week' );
					} else {
						$txt = sprintf(
							Ai1ec_I18n::__( 'Every %d weeks' ),
							$interval
						);
					}
				}
				break;
			case 'monthly':
				// check if interval is set
				if ( ! $interval || $interval == 1 ) {
					$txt = Ai1ec_I18n::__( 'Monthly' );
				} else {
					if ( $interval == 2 ) {
						$txt = Ai1ec_I18n::__( 'Every other month' );
					} else {
						$txt = sprintf(
							Ai1ec_I18n::__( 'Every %d months' ),
							$interval
						);
					}
				}
				break;
			case 'yearly':
				// check if interval is set
				if ( ! $interval || $interval == 1 ) {
					$txt = Ai1ec_I18n::__( 'Yearly' );
				} else {
					if ( $interval == 2 ) {
						$txt = Ai1ec_I18n::__( 'Every other year' );
					} else {
						$txt = sprintf(
							Ai1ec_I18n::__( 'Every %d years' ),
							$interval
						);
					}
				}
				break;
		}
	}

	/**
	 * get_weekday_by_id function
	 *
	 * Returns weekday name in English
	 *
	 * @param int $day_id Day ID
	 *
	 * @return string
	 **/
	protected function get_weekday_by_id( $day_id, $by_value = false ) {
		return $this->_registry->get( 'view.admin.get-repeat-box' )
		->get_weekday_by_id( $day_id, $by_value );
	}

	/**
	 * _ending_sentence function
	 *
	 * Ends rrule to text sentence
	 *
	 * @internal
	 *
	 * @return void
	 **/
	protected function _ending_sentence( &$txt, &$rc ) {
		if ( $until = $rc->getUntil() ) {
			if ( ! is_int( $until ) ) {
				$until = strtotime( $until );
			}
			$txt .= ' ' . sprintf(
				Ai1ec_I18n::__( 'until %s' ),
				$this->_registry->get(
					'date.time',
					$until
				)->format_i18n(
					$this->_registry->get( 'model.option')->get( 'date_format' )
				)
			);
		} else if ( $count = $rc->getCount() ) {
			$txt .= ' ' . sprintf(
				Ai1ec_I18n::__( 'for %d occurrences' ),
				$count
			);
		} else {
			$txt .= ', ' . Ai1ec_I18n::__( 'forever' );
		}
	}


}