<?php
/**
 * Availability service: works out which time slots are open on a date.
 *
 * Shared by the time-slot mode now, and by the date-range mode later. Given a
 * calendar, a date, and the field rules, it returns the open times with anything
 * already on the calendar removed.
 *
 * @package DrezSync
 */

defined( 'ABSPATH' ) || exit;

/**
 * Computes open time slots for a date.
 */
class DrezSync_Availability {

	/**
	 * Google Calendar connection.
	 *
	 * @var DrezSync_Connection
	 */
	private $connection;

	/**
	 * Constructor.
	 *
	 * @param DrezSync_Connection $connection Connection service.
	 */
	public function __construct( DrezSync_Connection $connection ) {
		$this->connection = $connection;
	}

	/**
	 * Open times for a date, as an array of 'H:i' strings.
	 *
	 * @param string $calendar_id Google Calendar ID.
	 * @param string $date        Date as Y-m-d.
	 * @param array  $rules       days, start, end, slot.
	 * @return array
	 */
	public function get_times( $calendar_id, $date, $rules ) {
		$times = array();

		if ( empty( $calendar_id ) || ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date ) ) {
			return $times;
		}

		$tz = wp_timezone();

		try {
			$day = new DateTime( $date . ' 00:00:00', $tz );
		} catch ( \Exception $e ) {
			return $times;
		}

		$weekday   = (int) $day->format( 'w' ); // 0 = Sunday ... 6 = Saturday.
		$open_days = array_map( 'intval', (array) ( isset( $rules['days'] ) ? $rules['days'] : array( 1, 2, 3, 4, 5 ) ) );
		if ( ! in_array( $weekday, $open_days, true ) ) {
			return $times;
		}

		$slot     = max( 5, (int) ( isset( $rules['slot'] ) ? $rules['slot'] : 30 ) );
		$capacity = max( 1, (int) ( isset( $rules['capacity'] ) ? $rules['capacity'] : 1 ) );
		$start = ( isset( $rules['start'] ) && preg_match( '/^\d{1,2}:\d{2}$/', $rules['start'] ) ) ? $rules['start'] : '09:00';
		$end   = ( isset( $rules['end'] ) && preg_match( '/^\d{1,2}:\d{2}$/', $rules['end'] ) ) ? $rules['end'] : '17:00';

		try {
			$window_start = new DateTime( $date . ' ' . $start, $tz );
			$window_end   = new DateTime( $date . ' ' . $end, $tz );
		} catch ( \Exception $e ) {
			return $times;
		}

		if ( $window_end <= $window_start ) {
			return $times;
		}

		$busy = $this->get_busy( $calendar_id, $window_start, $window_end, $tz );
		$now  = new DateTime( 'now', $tz );

		$cursor = clone $window_start;
		$guard  = 0;
		while ( $cursor < $window_end && $guard < 300 ) {
			$guard++;
			$slot_end = clone $cursor;
			$slot_end->modify( '+' . $slot . ' minutes' );
			if ( $slot_end > $window_end ) {
				break;
			}
			if ( $cursor > $now && $this->count_overlaps( $cursor, $slot_end, $busy ) < $capacity ) {
				$times[] = $cursor->format( 'H:i' );
			}
			$cursor->modify( '+' . $slot . ' minutes' );
		}

		return $times;
	}


	/**
	 * Busy intervals from the calendar within the window.
	 *
	 * @param string       $calendar_id  Calendar ID.
	 * @param DateTime     $window_start Window start.
	 * @param DateTime     $window_end   Window end.
	 * @param DateTimeZone $tz           Site timezone.
	 * @return array Array of array( DateTime $start, DateTime $end ).
	 */
	private function get_busy( $calendar_id, $window_start, $window_end, $tz ) {
		$busy   = array();
		$client = $this->connection->get_client();
		if ( ! $client || ! class_exists( '\Google\Service\Calendar' ) ) {
			return $busy;
		}

		try {
			$service = new \Google\Service\Calendar( $client );
			$events  = $service->events->listEvents(
				$calendar_id,
				array(
					'timeMin'      => $window_start->format( 'c' ),
					'timeMax'      => $window_end->format( 'c' ),
					'singleEvents' => true,
					'orderBy'      => 'startTime',
					'maxResults'   => 250,
				)
			);

			foreach ( $events->getItems() as $event ) {
				$s = $event->getStart();
				$e = $event->getEnd();
				if ( ! $s || ! $e ) {
					continue;
				}
				$s_dt   = $s->getDateTime() ? new DateTime( $s->getDateTime() ) : new DateTime( $s->getDate(), $tz );
				$e_dt   = $e->getDateTime() ? new DateTime( $e->getDateTime() ) : new DateTime( $e->getDate(), $tz );
				$busy[] = array( $s_dt, $e_dt );
			}
		} catch ( \Exception $e ) {
			return $busy;
		}

		return $busy;
	}

	/**
	 * How many busy intervals overlap a slot (its current booking count).
	 *
	 * @param DateTime $slot_start Slot start.
	 * @param DateTime $slot_end   Slot end.
	 * @param array    $busy       Busy intervals.
	 * @return int
	 */
	private function count_overlaps( $slot_start, $slot_end, $busy ) {
		$count = 0;
		foreach ( $busy as $interval ) {
			if ( $slot_start < $interval[1] && $slot_end > $interval[0] ) {
				$count++;
			}
		}
		return $count;
	}
}
