<?php
/*
 * Copyright (c) 2022 LatePoint LLC. All rights reserved.
 */

class OsResourceHelper {

	/**
	 * @param \LatePoint\Misc\BookingRequest $booking_request
	 * @param DateTime $date_from
	 * @param DateTime|null $date_to
	 * @param array $settings
	 *
	 * @return array
	 *
	 *
	 * Returns an array of work periods, grouped by days that were requested in the filter.
	 * example: ['2022-02-24' => [], '2022-02-25' => [], ...]
	 *
	 *  | Agent   | Service | Location | Date          |       Hours       | Weight
	 *  | -----------------------------------------------------------------------------
	 *  | 1       | 1       | 1        | 2022-01-15    |   7:00  - 18:00   |  7
	 *  | 1       | 0       | 1        | 2022-01-15    |   8:00  - 14:00   |  6
	 *  | 1       | 0       | 0        | 2022-01-15    |   8:00  - 14:00   |  5
	 *  | 0       | 0       | 1        | 2022-01-15    |   11:00 - 12:00   |  5
	 *  | 0       | 0       | 0        | 2022-01-15    |   11:00 - 12:00   |  4
	 *  | 1       | 0       | 1        | NULL          |   0:00  - 0:00    |  2
	 *  | 1       | 0       | 0        | NULL          |   9:00  - 12:00   |  1
	 *  | 0       | 0       | 0        | NULL          |   11:00 - 17:00   |  0
	 *
	 */
	public static function get_resources_grouped_by_day( \LatePoint\Misc\BookingRequest $booking_request, DateTime $date_from, ?DateTime $date_to = null, array $settings = [] ): array {
		$defaults = [
			'now'                   => OsTimeHelper::now_datetime_object(),
			'exclude_booking_ids'   => [],
			'accessed_from_backend' => false,
			'consider_cart_items'   => false,
			'timezone_name'         => OsTimeHelper::get_wp_timezone_name(),
		];
		$settings = array_merge( $defaults, $settings );

		$connections = OsConnectorHelper::get_connections_that_satisfy_booking_request( $booking_request, $settings['accessed_from_backend'] );
		$resources   = [];
		foreach ( $connections as $connection ) {
			$resources[] = \LatePoint\Misc\BookingResource::create_from_connection( $connection );
		}
		if ( empty( $date_to ) ) {
			$date_to = clone $date_from;
		}


		// all resource management is done in WP timezone, if requested timezone is different - make sure to include couple days on each end to accommodate for timezone differences, which could be up to 26 hours
		if ( $settings['timezone_name'] != OsTimeHelper::get_wp_timezone_name() ) {
			$date_from->modify( '-2 days' );
			$date_to->modify( '+2 days' );
		}

		$filter = new \LatePoint\Misc\Filter(
			[
				'service_id'  => $booking_request->service_id,
				'connections' => $connections,
				'date_from'   => $date_from->format( 'Y-m-d' ),
				'date_to'     => $date_to->format( 'Y-m-d' ),
			]
		);
		// Pass excluded bookings (e.g. the booking being rescheduled) through to blocked-period generation
		// so daily-booking-limit checks don't count the booking against itself.
		if ( isset( $settings['exclude_booking_ids'] ) ) {
			$filter->exclude_booking_ids = $settings['exclude_booking_ids'];
		}
		$weekday_periods = OsWorkPeriodsHelper::get_work_periods_grouped_by_weekday( $filter );
		$daily_resources = [];
		// loop through the requested days and fill in array with work periods that are applicable to that day

		// Booked periods
		$booked_periods_filter            = new \LatePoint\Misc\Filter();
		$booked_periods_filter->date_from = $date_from->format( 'Y-m-d' );
		$booked_periods_filter->date_to   = $date_to->format( 'Y-m-d' );
		$booked_periods_filter->statuses  = OsBookingHelper::get_timeslot_blocking_statuses();
		if ( $settings['exclude_booking_ids'] ) {
			$booked_periods_filter->exclude_booking_ids = $settings['exclude_booking_ids'];
		}
		if ( $settings['consider_cart_items'] ) {
			$booked_periods_filter->consider_cart_items = true;
		}

		$booked_periods  = OsBookingHelper::get_booked_periods_grouped_by_day( $booked_periods_filter );
		$blocked_periods = OsBookingHelper::get_blocked_periods_grouped_by_day( $filter, $settings['accessed_from_backend'] );


		for ( $day_date = clone $date_from; $day_date->format( 'Y-m-d' ) <= $date_to->format( 'Y-m-d' ); $day_date->modify( '+1 day' ) ) {
			$daily_resources[ $day_date->format( 'Y-m-d' ) ] = [];
			// fill every day with available resources
			foreach ( $resources as $resource ) {
				$last_added_period             = false;
				$available_work_periods_groups = [];
				$group_index                   = 0;
				// loop through available work periods for this week day
				foreach ( $weekday_periods[ $day_date->format( 'N' ) ] as $period ) {
					if ( $period->custom_date && ( $period->custom_date != $day_date->format( 'Y-m-d' ) ) ) {
						continue;
					} // if this period has a custom date set and if it doesn't match the one we search for - skip it
					// only add this work period if agent/location/service match or not set
					if ( ( ! $period->agent_id || $period->agent_id == $resource->agent_id ) && ( ! $period->service_id || $period->service_id == $resource->service_id ) && ( ! $period->location_id || $period->location_id == $resource->location_id ) ) {
						if ( $last_added_period ) {
							// if weight of previously added period is different - break, no need to add any other periods
							if ( $last_added_period->weight != $period->weight ) {
								break;
							}
							if ( ( $last_added_period->service_id != $period->service_id ) || ( $last_added_period->agent_id != $period->agent_id ) || ( $last_added_period->location_id != $period->location_id ) ) {
								// same weight NOT same exact properties, create a new group of work periods, which will later be used to find intersections
								$group_index++;
							}
							if ( ( $period->start_time == 0 ) && ( $period->end_time == 0 ) ) {
								$available_work_periods_groups = [];
								break;
							}
							$available_work_periods_groups[ $group_index ][] = \LatePoint\Misc\TimePeriod::create_from_work_period( $period );
							$last_added_period                               = $period;
						} else {
							if ( ( $period->start_time == 0 ) && ( $period->end_time == 0 ) ) {
								$available_work_periods_groups = [];
								break;
							}
							$available_work_periods_groups[ $group_index ][] = \LatePoint\Misc\TimePeriod::create_from_work_period( $period );
							$last_added_period                               = $period;
						}
					}
				}

				$day_resource       = clone $resource;
				$day_resource->date = $day_date->format( 'Y-m-d' );
				$day_resource->add_available_periods( $available_work_periods_groups );

				/// -----------------------------
				/// LOGIC FOR CALCULATING "BOOKED" PERIODS
				/// -----------------------------
				foreach ( $booked_periods[ $day_date->format( 'Y-m-d' ) ] as $booked_period ) {

					if ( ( $day_resource->service_id == $booked_period->service_id ) && ( $day_resource->location_id == $booked_period->location_id ) && ( $day_resource->agent_id == $booked_period->agent_id ) ) {
						// same service, agent and location is already booked, block no matter what
						$day_resource->add_booked_period( $booked_period );
						continue;
					}

					if ( $day_resource->agent_id == $booked_period->agent_id ) {
						// Same agent
						if ( ( $day_resource->location_id != $booked_period->location_id ) && OsSettingsHelper::is_on( 'one_location_at_time' ) ) {
							// different location is booked, but the same agent, block if "Agents can only be present in one location at a time" is ON
							$day_resource->add_booked_period( $booked_period, true );
							continue;
						}
						if ( ( $day_resource->service_id != $booked_period->service_id ) && ( $day_resource->location_id == $booked_period->location_id ) && ! OsSettingsHelper::is_on( 'multiple_services_at_time' ) ) {
							// Different service, but same location, block, if "One agent can perform different services simultaneously" is OFF
							$day_resource->add_booked_period( $booked_period, true );
						}
					} else {
						// Different agent
						if ( ( $day_resource->location_id == $booked_period->location_id ) && OsSettingsHelper::is_on( 'one_agent_at_location' ) ) {
							// same location, so it doesn't matter who's the agent, block, because location can only be used by a single agent at a time
							// set to max capacity, to block slot even if it still has room
							$day_resource->add_booked_period( $booked_period, true );
						}
					}
				}

				/// -----------------------------
				/// LOGIC FOR CALCULATING "BLOCKED" PERIODS
				/// -----------------------------
				foreach ( $blocked_periods[ $day_date->format( 'Y-m-d' ) ] as $blocked_period ) {
					if ( ! $blocked_period->agent_id || $day_resource->agent_id == $blocked_period->agent_id ) {
						$day_resource->add_blocked_period( $blocked_period );
					}
				}

				$day_resource->build_bookable_slots( $booking_request, $day_resource->get_timeblock_interval() );
				$daily_resources[ $day_date->format( 'Y-m-d' ) ][] = $day_resource;
			}
		}
		$daily_resources = apply_filters( 'latepoint_get_resources_grouped_by_day', $daily_resources, $booking_request, $date_from, $date_to, $settings );

		return $daily_resources;
	}


	/**
	 * @param \LatePoint\Misc\BookingResource[] $resources
	 *
	 * @return array
	 */
	public static function get_ordered_booking_slots_from_resources( array $resources ): array {
		$booking_slots = [];
		foreach ( $resources as $resource ) {
			$booking_slots = array_merge( $booking_slots, $resource->slots );
		}

		usort(
			$booking_slots,
			function ( $first, $second ) {
				return $first->start_time <=> $second->start_time;
			} 
		);

		if ( count( $resources ) > 1 ) {
			$squashed_booking_slots = [];
			$last_added_slot        = false;
			foreach ( $booking_slots as $booking_slot ) {
				if ( $last_added_slot && ( $last_added_slot->start_time == $booking_slot->start_time ) ) {
					if ( $last_added_slot->available_capacity() < $booking_slot->available_capacity() ) {
						$squashed_booking_slots[ count( $squashed_booking_slots ) - 1 ] = $booking_slot;
						$last_added_slot = $booking_slot;
					}
				} else {
					$squashed_booking_slots[] = $booking_slot;
					$last_added_slot          = $booking_slot;
				}
			}
			$booking_slots = $squashed_booking_slots;
		}

		return $booking_slots;
	}

	/**
	 * @param \LatePoint\Misc\BookingResource[]
	 *
	 * @return \LatePoint\Misc\TimePeriod
	 */
	public static function get_work_boundaries_for_resources( $resources ): \LatePoint\Misc\TimePeriod {
		$times = [];
		foreach ( $resources as $resource ) {
			foreach ( $resource->work_time_periods as $work_time_period ) {
				$times[] = $work_time_period->start_time;
				$times[] = $work_time_period->end_time;
			}
			foreach ( $resource->booked_time_periods as $booked_time_period ) {
				if ( $booked_time_period->start_date == $booked_time_period->end_date ) {
					// same day event
					$times[] = $booked_time_period->start_time;
					$times[] = $booked_time_period->end_time;
				} else {
					// event spans mutiple days, expand boundaries to a full day
					$times[] = 0;
					$times[] = 24 * 60 - 1;
				}
			}
		}
		if ( $times ) {
			$boundary_time_period = new \LatePoint\Misc\TimePeriod(
				[
					'start_time' => min( $times ),
					'end_time'   => max( $times ),
				] 
			);
		} else {
			$boundary_time_period = new \LatePoint\Misc\TimePeriod(
				[
					'start_time' => 0,
					'end_time'   => 0,
				] 
			);
		}

		return $boundary_time_period;
	}

	/**
	 * @param array $groups_of_resources
	 *
	 * @return \LatePoint\Misc\TimePeriod
	 */
	public static function get_work_boundaries_for_groups_of_resources( array $groups_of_resources ): \LatePoint\Misc\TimePeriod {
		$times = [];
		foreach ( $groups_of_resources as $resources ) {
			$time_period = self::get_work_boundaries_for_resources( $resources );
			if ( $time_period->start_time || $time_period->end_time ) {
				$times[] = $time_period->start_time;
				$times[] = $time_period->end_time;
			}
		}
		if ( $times ) {
			$boundary_time_period = new \LatePoint\Misc\TimePeriod(
				[
					'start_time' => min( $times ),
					'end_time'   => max( $times ),
				] 
			);
		} else {
			$boundary_time_period = new \LatePoint\Misc\TimePeriod(
				[
					'start_time' => 0,
					'end_time'   => 0,
				] 
			);
		}

		return $boundary_time_period;
	}
}
