<?php
/**
 * Date functions
 *
 * @package RosarioSIS
 * @subpackage functions
 */


/**
 * Get the Date of the day
 * Database ISO format, ready for SQL
 *
 * @example "SELECT SCHOOL_DATE
 *               FROM attendance_calendar
 *               WHERE SCHOOL_DATE<'" . DBDate() . "'";
 *
 * @example DBDate() > $res['DATE']
 *
 * @return string Date of the day
 */
function DBDate()
{
	// ISO, eg. 2015-07-10.
	return date( 'Y-m-d' );
}


/**
 * Localized & preferred date
 * Accepts Oracle or Postgres date
 *
 * @since 3.8 CSS add .proper-date class.
 * @since 7.1 Export (Excel) date to YYYY-MM-DD format (ISO).
 * @since 7.1 Select Date Format: Add Preferences( 'DATE' ).
 * @since 9.0 Fix PHP8.1 deprecated strftime() use strftime_compat() instead
 * @since 12.1 Fix regression since 7.1 short month name, eg.: "Sep"
 *
 * @see Preferences
 *
 * @param  string $date   Date.
 * @param  string $length long|short Month name length (optional).
 *
 * @return string Formatted & localized date or empty string if invalid format
 */
function ProperDate( $date, $length = 'long' )
{
	if ( empty( $date )
		|| ! VerifyDate( $date ) )
	{
		return '';
	}

	$date_exploded = ExplodeDate( (string) $date );

	$comment = '<!-- ' . $date . ' -->';

	// Export (Excel) date to MM/DD/YYYY format.
	if ( isset( $_REQUEST['LO_save'] )
		&& Preferences( 'E_DATE' ) === 'MM/DD/YYYY' )
	{
		return $comment .
			$date_exploded['month'] . '/' .
			$date_exploded['day'] . '/' .
			$date_exploded['year'];
	}

	// @since 7.1 Export (Excel) date to YYYY-MM-DD format (ISO).
	if ( isset( $_REQUEST['LO_save'] )
		&& Preferences( 'E_DATE' ) === 'YYYY-MM-DD' )
	{
		return $comment .
			$date_exploded['year'] . '-' .
			$date_exploded['month'] . '-' .
			$date_exploded['day'];
	}

	$pref_date = Preferences( 'DATE' );

	if ( $length === 'short' )
	{
		// Short month name, eg.: "Sep".
		$pref_date = str_replace( '%B', '%b', $pref_date );
	}

	// Display localized date with strftime().
	// CSS add .proper-date class.
	// @since 9.0 Fix PHP8.1 deprecated strftime() use strftime_compat() instead
	return $comment .
		'<span class="proper-date">' . strftime_compat(
			$pref_date,
			$date_exploded['year'] . '-' . $date_exploded['month'] . '-' . $date_exploded['day']
		) . '</span>';
}


/**
 * Localized & preferred Date & Time
 * "2015-09-21 16:35:42" => "September 21 2015 04:35:42 PM"
 *
 * If 'short' length & day = today, only time is returned.
 *
 * @since 2.9
 *
 * @example ProperDateTime( $value, 'short' );
 *
 * @uses ProperDate()
 *
 * @param string $datetime Datetime / Timestamp.
 * @param string $length   long|short (optional). Defaults to 'long'.
 */
function ProperDateTime( $datetime, $length = 'long' )
{
	try
	{
		// @since 9.0 Fix PHP8.1 deprecated strftime() use strftime_compat() instead
		$locale_time = strftime_compat( '%X', $datetime );
	}
	catch ( \Exception $e )
	{
		$local_time = '12:00:00';
	}

	// Remove trailing seconds :00.
	$locale_time = mb_substr( $locale_time, -3 ) === ':00' ? mb_substr( $locale_time, 0, -3 ) : $locale_time;

	// 0x202f is hex for "narrow no-break space" unicode character.
	$locale_time = str_replace( [ ':00 ', ':00 ' ], [ ' ', ' ' ], $locale_time );

	$date = mb_substr( $datetime, 0, 10 );

	// Add raw Datetime inside HTML comment. Used for sorting in ListOutput()
	$comment = '<!-- ' . $datetime . ' -->';

	if ( $length === 'short'
		&& DBDate() === $date )
	{
		// Today: only time!
		return $comment . $locale_time;
	}

	return $comment . ProperDate( $date, $length ) . ' ' . $locale_time;
}


/**
 * Localized Time
 * AM/PM: "16:35:42" => "04:35:42 PM"
 * 24H: "22:30:00" => "22:30"
 * Datetime: "2024-02-29 16:35:42" => "04:35:42 PM"
 *
 * @since 11.5
 *
 * @example ProperTime( $value );
 * @example DBGet( "SELECT ON_TIME FROM meetings", [ 'ON_TIME' => 'ProperTime' ] );
 *
 * @param string $time   Time or datetime.
 * @param string $column DB column (optional). When using this function as DBGet() callback.
 *
 * @return string Localized Time
 */
function ProperTime( $time, $column = '' )
{
	if ( ! $time )
	{
		return $time;
	}

	try
	{
		// Preferred time based on locale.
		$locale_time = strftime_compat( '%X', strtotime( $time ) );
	}
	catch ( \Exception $e )
	{
		return $time;
	}

	// Add raw time inside HTML comment (only if different from locale time). Used for sorting in ListOutput()
	$comment = $locale_time === $time ? '' : '<!-- ' . $time . ' -->';

	// Strip seconds :00.
	$locale_time = mb_substr( $locale_time, -3 ) === ':00' ? mb_substr( $locale_time, 0, -3 ) : $locale_time;

	// Strip seconds when AM/PM :00.
	// 0x202f is hex for "narrow no-break space" unicode character.
	$locale_time = str_replace( [ ':00 ', ':00 ' ], [ ' ', ' ' ], $locale_time );

	return $comment . $locale_time;
}


/**
 * Verify date
 *
 * Accepts 3 dates formats (Oracle & Postgres)
 *
 * @param  string $date date to verify.
 *
 * @return boolean true if valid date, else false
 */
function VerifyDate( $date )
{
	$date_exploded = ExplodeDate( (string) $date );

	if ( ! $date_exploded['month'] )
	{
		return false;
	}

	return checkdate(
		(int) $date_exploded['month'],
		(int) $date_exploded['day'],
		(int) $date_exploded['year']
	);
}


/**
 * Generate date pull-downs
 *
 * Send PrepareDate a date as the selected date
 * to have returned a date selection series of pull-down menus
 *
 * 3 date formats are accepted (Oracle & Postgres)
 *
 * For the default to be Not Specified,
 * send a date of 00-000-00 or send nothing
 *
 * @since 7.2 Order Day, Month & Year inputs depending on User date preference.
 *
 * @global array   $_ROSARIO Sets $_ROSARIO['PrepareDate']
 *
 * @param  string  $date      Date to prepare.
 * @param  string  $name_attr select inputs name attribute suffix (optional).
 * @param  boolean $allow_na  Allow N/A, defaults to true (optional).
 * @param  array   $options   Keys: Y|M|D|C|short|submit|required (optional).
 *
 * @return string  ProperDate (PDF) or date selection series of pull-down menus with an optional JS calendar
 */
function PrepareDate( $date, $name_attr = '', $allow_na = true, $options = [] )
{
	global $_ROSARIO;

	// PDF printing, display text date.
	if ( isset( $_REQUEST['_ROSARIO_PDF'] ) )
	{
		return ProperDate( (string) $date );
	}

	$return = $extraY = $extraM = $extraD = '';

	$defaults = [
		'Y' => false, // Year.
		'M' => false, // Month.
		'D' => false, // Day.
		'C' => false, // JS Calendar.
		'short' => false, // Short month.
		'submit' => false, // Submit onchange.
		'required' => false, // Required fields.
	];

	if ( ! VerifyDate( $date ) )
	{
		// Fix PHP fatal error when not a date
		$date = '';
	}

	$options = array_replace_recursive( $defaults, (array) $options );

	/**
	 * If none of the Y|M|D|C options are true
	 * set them all to true.
	 */
	if ( ! $options['Y']
		&& ! $options['M']
		&& ! $options['D']
		&& ! $options['C'] )
	{
		$options['Y'] = $options['M'] = $options['D'] = $options['C'] = true;
	}

	// Short month select input.
	if ( $options['short'] )
	{
		$extraM = ' style="max-width: 65px;"';
		$extraY = ' style="max-width: 65px;"';
	}

	// Submit on date change.
	if ( $options['submit'] )
	{
		$URL_args = [ 'month' . $name_attr, 'day' . $name_attr, 'year' . $name_attr ];

		$date_onchange_href = PreparePHP_SELF(
			$_REQUEST,
			$URL_args
		);

		// Create date onchange link
		// @since 12.5 CSP remove unsafe-inline Javascript
		$e = ' class="onchange-ajax-link onchange-date-submit" data-link="' . $date_onchange_href . '" data-name="' . AttrEscape( $name_attr ) . '"';

		$extraM .= $e;

		$extraD .= $e;

		$extraY .= $e;
	}

	$_ROSARIO['PrepareDate'] = isset( $_ROSARIO['PrepareDate'] ) ?
		++$_ROSARIO['PrepareDate'] :
		1;

	// Required fields.
	if ( $options['required'] )
	{
		$extraM .= ' required';

		$extraD .= ' required';

		$extraY .= ' required';
	}

	$date_exploded = ExplodeDate( (string) $date );

	$return .= '<!-- ' . implode( '', $date_exploded ) . ' -->';

	$return_m = $return_d = $return_y = '';

	// MONTH  ---------------.
	if ( $options['M'] )
	{
		$return_m .= '<select name="month' . $name_attr . '" id="monthSelect' . $_ROSARIO['PrepareDate'] . '"' .
			$extraM . ' autocomplete="off">';

		if ( $allow_na )
		{
			if ( $date_exploded['month'] < 1 )
			{
				$return_m .= '<option value="" selected>' . _( 'N/A' );
			}
			else
				$return_m .= '<option value="">' . _( 'N/A' );
		}

		$months_locale = [
			'01' => _( 'January' ),
			'02' => _( 'February' ),
			'03' => _( 'March' ),
			'04' => _( 'April' ),
			'05' => _( 'May' ),
			'06' => _( 'June' ),
			'07' => _( 'July' ),
			'08' => _( 'August' ),
			'09' => _( 'September' ),
			'10' => _( 'October' ),
			'11' => _( 'November' ),
			'12' => _( 'December' ),
		];

		foreach ( $months_locale as $key => $name )
		{
			$return_m .= '<option value="' . AttrEscape( $key ) . '"' . ( $date_exploded['month'] == $key ? ' selected' : '' ) . '>' . $name;
		}

		$return_m .= '</select>';

		$return_m .= '<label for="monthSelect' . $_ROSARIO['PrepareDate'] . '" class="a11y-hidden">' .
			_( 'Month' ) . '</label>';
	}

	// DAY  ---------------.
	if ( $options['D'] )
	{
		$return_d .= '<select name="day' . $name_attr . '" id="daySelect' . $_ROSARIO['PrepareDate'] . '"' .
			$extraD . ' autocomplete="off">';

		if ( $allow_na )
		{
			if ( $date_exploded['day'] < 1 )
			{
				$return_d .= '<option value="" selected>' . _( 'N/A' );
			}
			else
				$return_d .= '<option value="">' . _( 'N/A' );
		}

		for ( $i = 1; $i <= 31; $i++ )
		{
			$print = $i;

			if ( $i < 10 )
				$print = '0' . $i;

			$return_d .= '<option value="' . AttrEscape( $print ) . '"' . ( $date_exploded['day'] == $print ? ' selected' : '' ) . '>' . $i;
		}

		$return_d .= '</select>';

		$return_d .= '<label for="daySelect' . $_ROSARIO['PrepareDate'] . '" class="a11y-hidden">' .
			_( 'Day' ) . '</label>';
	}

	// YEAR  ---------------.
	if ( $options['Y'] )
	{
		if ( $date_exploded['year'] < 1 )
		{
			// Show 80 previous years.
			$begin = date( 'Y' ) - 80;
			$end = date( 'Y' ) + 5;
		}
		else
		{
			// Show 20 previous years.
			$begin = $date_exploded['year'] - 20;
			$end = $date_exploded['year'] + 20;
		}

		$return_y .= '<select name="year' . $name_attr . '" id="yearSelect' . $_ROSARIO['PrepareDate'] . '"' .
			$extraY . ' autocomplete="off">';

		if ( $allow_na )
		{
			$return_y .= $date_exploded['year'] < 1 ?
					'<option value="" selected>' . _( 'N/A' ) :
					'<option value="">' . _( 'N/A' );
		}

		for ( $i = $begin; $i <= $end; $i++ )
		{
			$return_y .= '<option value="' . AttrEscape( $i ) . '"' . ( $date_exploded['year'] == $i ?' selected' : '' ) . '>' . $i;
		}

		$return_y .= '</select>';

		$return_y .= '<label for="yearSelect' . $_ROSARIO['PrepareDate'] . '" class="a11y-hidden">' .
			_( 'Year' ) . '</label>';
	}

	// @since 7.2 Order Day, Month & Year inputs depending on User date preference.
	$preferred_date_first = mb_substr( Preferences( 'DATE' ), 0, 2 );

	if ( $preferred_date_first === '%Y' )
	{
		// Year, Month, Day.
		$return .= $return_y . $return_m . $return_d;
	}
	elseif ( $preferred_date_first === '%d' )
	{
		// Day, Month, Year.
		$return .= $return_d . $return_m . $return_y;
	}
	else
	{
		// Month, Day, Year.
		$return .= $return_m . $return_d . $return_y;
	}

	// CALENDAR  ---------------.
	if ( $options['C'] )
	{
		$return .= '<img src="assets/themes/' . Preferences( 'THEME' ) . '/btn/calendar.png" title="' . AttrEscape( _( 'Open calendar' ) ) . '" class="button cal" alt="' . AttrEscape( _( 'Open calendar' ) ) . '" id="trigger' . $_ROSARIO['PrepareDate'] . '">';
	}

	// NOBR on date input.
	$return = '<span class="nobr">' . $return . '</span>';

	return $return;
}


/**
 * Explode a ISO or Oracle date
 *
 * @since 11.6 Use strtotime()
 * @link https://www.php.net/strtotime
 *
 * @param  string $date Postgres or Oracle date.
 *
 * @return array  [ 'year' => '4_digits_year', 'month' => 'numeric_month', 'day' => 'day' ]
 */
function ExplodeDate( $date )
{
	$timestamp = strtotime( $date );

	if ( empty( $timestamp ) )
	{
		return [ 'year' => '', 'month' => '', 'day' => '' ];
	}

	return [
		'year' => date( 'Y', $timestamp ),
		'month' => date( 'm', $timestamp ),
		'day' => date( 'd', $timestamp ),
	];
}

/**
 * Get date requested by User
 * Returns an empty string if date is malformed / incomplete
 * Returns a corrected date
 * if day does not exist in month,
 * for example, 2015-02-31 will return 2015-02-28
 *
 * @since 2.9
 * @since 4.5 Recursive function: use request index and default value.
 *
 * @example RequestedDate( $year, $month, $day );
 * @example $date_end = RequestedDate( 'end', DBDate() );
 * @example $date = RequestedDate( 'date', DBDate(), 'set' );
 *
 * @param  string $year_or_request_index Requested year.
 * @param  string $month_or_default      Requested month or default date.
 * @param  string $day_or_mode           Requested day or mode: [empty]|add|set.
 *
 * @return string Empty string if malformed/incomplete date or date or default.
 */
function RequestedDate( $year_or_request_index, $month_or_default, $day_or_mode = '' )
{
	if ( $day_or_mode === ''
		|| $day_or_mode === 'add'
		|| $day_or_mode === 'set' )
	{
		$request_index = $year_or_request_index;

		$default = strlen( (string) $month_or_default ) > 7 && VerifyDate( $month_or_default ) ? $month_or_default : '';

		$mode = $day_or_mode;

		if (  isset( $_REQUEST['day_' . $request_index ] )
			&& isset( $_REQUEST['month_' . $request_index ] )
			&& isset( $_REQUEST['year_' . $request_index ] ) )
		{
			$requested_date = RequestedDate(
				$_REQUEST['year_' . $request_index ],
				$_REQUEST['month_' . $request_index ],
				$_REQUEST['day_' . $request_index ]
			);

			if ( ! empty( $requested_date ) )
			{
				return $requested_date;
			}
		}

		if ( $mode === 'set'
			&& $default )
		{
			$exploded_date = ExplodeDate( (string) $default );

			$_REQUEST['year_' . $request_index ] = $exploded_date['year'];
			$_REQUEST['month_' . $request_index ] = $exploded_date['month'];
			$_REQUEST['day_' . $request_index ] = $exploded_date['day'];
		}

		if ( $mode === 'add' )
		{
			$_REQUEST[ $request_index ] = $default;
		}

		return $default;
	}

	$year = (int) $year_or_request_index;

	$month = str_pad( (int) $month_or_default, 2, '0', STR_PAD_LEFT );

	$day = str_pad( (int) $day_or_mode, 2, '0', STR_PAD_LEFT );

	$date = $year . '-' . $month . '-' . $day;

	/**
	 * Verify first this is an ISO date: YYYY-MM-DD
	 * Day between 1 and 31
	 * Month between 1 and 12
	 * Year between 1000 and 9999
	 */
	if ( mb_strlen( $date ) !== 10
		|| $day < 1
		|| $day > 31
		|| $month > 12
		|| $month < 1
		|| $year < 1000
		|| $year > 9999 )
	{
		return '';
	}

	// Correct date if day does not exist in month.
	while ( ! checkdate( $month, $day, $year )
		&& $day > 0 )
	{
		$day--;

		$date = $year . '-' . $month . '-' . $day;
	}

	return $date;
}


/**
 * Get dates requested by User
 *
 * @uses RequestedDate() function
 * Recursive function
 *
 * @since 2.9
 *
 * @example RequestedDates( $_REQUEST['year_tables'], $_REQUEST['month_tables'], $_REQUEST['day_tables'] );
 *
 * @param  array $year_array  Requested years.
 * @param  array $month_array Requested months.
 * @param  array $day_array   Requested days.
 *
 * @return array Requested dates, or empty if no dates found or malformed/incomplete dates
 */
function RequestedDates( $year_array, $month_array, $day_array )
{
	$return = [];

	foreach ( (array) $month_array as $field_name => $month )
	{
		if ( $month === false
			|| ! isset( $year_array[ $field_name ] )
			|| ! isset( $day_array[ $field_name ] ) )
		{
			// Fix PHP Notice: Array to string conversion when month set to false.
			continue;
		}

		if ( ! is_array( $month ) )
		{
			$return[ $field_name ] = RequestedDate(
				$year_array[ $field_name ],
				$month,
				$day_array[ $field_name ]
			);

			continue;
		}

		$dates = RequestedDates( $year_array[ $field_name ], $month, $day_array[ $field_name ] );

		if ( ! empty( $dates ) )
		{
			$return[ $field_name ] = $dates;
		}
	}

	return $return;
}


/**
 * Add dates requested by User
 * to the $_REQUEST (and $_POST) array index specified.
 *
 * @since 3.8
 *
 * @example AddRequestedDates( 'values', 'post' );
 *
 * @uses RequestedDates() function
 *
 * @param string $request_index $_REQUEST array index where we add requested dates.
 * @param string $add_to_post   Add to $_POST array too. Defaults to '' (optional).
 */
function AddRequestedDates( $request_index, $add_to_post = '' )
{
	if ( ! $request_index
		|| ! isset(
			$_REQUEST[ 'day_' . $request_index ],
			$_REQUEST[ 'month_' . $request_index ],
			$_REQUEST[ 'year_' . $request_index ]
		) )
	{
		return;
	}

	$requested_dates = RequestedDates(
		$_REQUEST[ 'year_' . $request_index ],
		$_REQUEST[ 'month_' . $request_index ],
		$_REQUEST[ 'day_' . $request_index ]
	);

	if ( isset( $_REQUEST[ $request_index ] ) && is_array( $_REQUEST[ $request_index ] ) )
	{
		$_REQUEST[ $request_index ] = array_replace_recursive(
			$_REQUEST[ $request_index ],
			$requested_dates
		);

		if ( $add_to_post === 'post' )
		{
			$_POST[ $request_index ] = array_replace_recursive(
				isset( $_POST[ $request_index ] ) ? $_POST[ $request_index ] : [],
				$requested_dates
			);
		}

		return;
	}

	$_REQUEST[ $request_index ] = $requested_dates;

	if ( $add_to_post === 'post' )
	{
		$_POST[ $request_index ] = $requested_dates;
	}
}
