<?php
/**
 * Class WC_Appointments_Availability
 *
 * @package WooCommerce/Appointments
 */

/**
 * Class WC_Appointments_Availability
 */
class WC_Appointments_Availability extends WC_Data implements ArrayAccess, WC_Appointments_Data_Object_Interface {

	public const DATA_STORE          = 'appointments-availability';
	public const SECONDS_IN_A_MINUTE = 60;

	/**
	 * Scope constants for availability rules.
	 */
	public const SCOPE_GLOBAL = 'global';
	public const SCOPE_PRODUCT = 'product';
	public const SCOPE_STAFF = 'staff';

	/**
	 * Kind constants for availability rules (used in kind field).
	 */
	public const KIND_GLOBAL = 'availability#global';
	public const KIND_PRODUCT = 'availability#product';
	public const KIND_STAFF = 'availability#staff';

	/**
	 * This is the name of this object type.
	 *
	 * @var string
	 */
	protected $object_type = 'appointments_availability';
	protected $cache_group = 'appointments-availability';

	/**
	 * Data array.
	 *
	 * @var array
	 */
	protected $data = [
		'kind'                  => '',
		'kind_id'               => '',
		'event_id'              => '',
		'parent_event_id'       => '',
		'title'                 => '',
		'range_type'            => 'custom',
		'from_date'             => '',
		'to_date'               => '',
		'from_range'            => '',
		'to_range'              => '',
		'appointable'           => 'yes',
		'priority'              => 10,
		'qty'                   => '',
		'ordering'              => 0,
		'date_created'          => '',
		'date_modified'         => '',
		'rrule'                 => '',
	];

	/**
	 * Constructor.
	 *
	 * @param int|object|array $id Id.
	 *
	 * @throws Exception When validation fails.
	 */
	public function __construct( $id = 0 ) {
		parent::__construct( $id );

		if ( is_numeric( $id ) && 0 < $id ) {
			$this->set_id( $id );
		} elseif ( $id instanceof self ) {
			$this->set_id( $id->get_id() );
		} else {
			$this->set_object_read( true );
		}

		$this->data_store = WC_Data_Store::load( self::DATA_STORE );

		if ( $this->get_id() > 0 ) {
			$this->data_store->read( $this );
		}
	}

	/**
	 * Get created date.
	 *
	 * @param  string $context What the value is for.
	 *                          Valid values are 'view' and 'edit'.
	 *
	 * @return WC_DateTime|null Object if the date is set or null if there is no date.
	 */
	public function get_date_created( $context = 'view' ): ?WC_DateTime {
		return $this->get_prop( 'date_created', $context );
	}

	/**
	 * Get modified date.
	 *
	 * @param  string $context What the value is for.
	 *                          Valid values are 'view' and 'edit'.
	 *
	 * @return WC_DateTime|null Object if the date is set or null if there is no date.
	 */
	public function get_date_modified( $context = 'view' ): ?WC_DateTime {
		return $this->get_prop( 'date_modified', $context );
	}

	/**
	 * Get title.
	 *
	 * @param  string $context What the value is for.
	 *                          Valid values are 'view' and 'edit'.
	 *
	 * @return string
	 */
	public function get_title( $context = 'view' ): string {
		return $this->get_prop( 'title', $context );
	}

	/**
	 * Get kind (rule type).
	 *
	 * @param  string $context What the value is for.
	 *                          Valid values are 'view' and 'edit'.
	 *
	 * @return string
	 */
	public function get_kind( $context = 'view' ): string {
		return $this->get_prop( 'kind', $context );
	}

	/**
	 * Get kind id (rule type id).
	 *
	 * @param  string $context What the value is for.
	 *                          Valid values are 'view' and 'edit'.
	 *
	 * @return string
	 */
	public function get_kind_id( $context = 'view' ): string {
		return $this->get_prop( 'kind_id', $context );
	}

	/**
	 * Get event id (gcal event id).
	 *
	 * @param  string $context What the value is for.
	 *                          Valid values are 'view' and 'edit'.
	 *
	 * @return string
	 */
	public function get_event_id( $context = 'view' ): string {
		return $this->get_prop( 'event_id', $context );
	}

	/**
	 * Get parent event id (for modified instances of recurring events).
	 *
	 * @param  string $context What the value is for.
	 *                          Valid values are 'view' and 'edit'.
	 *
	 * @return string
	 */
	public function get_parent_event_id( $context = 'view' ): string {
		return $this->get_prop( 'parent_event_id', $context );
	}

	/**
	 * Get Range Type.
	 *
	 * @param  string $context What the value is for.
	 *                          Valid values are 'view' and 'edit'.
	 *
	 * @return string
	 */
	public function get_range_type( $context = 'view' ): string {
		return $this->get_prop( 'range_type', $context );
	}

	/**
	 * Get From Date
	 *
	 * @param  string $context What the value is for.
	 *                          Valid values are 'view' and 'edit'.
	 *
	 * @return string
	 */
	public function get_from_date( $context = 'view' ): string {
		return $this->get_prop( 'from_date', $context );
	}

	/**
	 * Get to Date.
	 *
	 * @param  string $context What the value is for.
	 *                          Valid values are 'view' and 'edit'.
	 *
	 * @return string
	 */
	public function get_to_date( $context = 'view' ): string {
		return $this->get_prop( 'to_date', $context );
	}

	/**
	 * Get From Range.
	 *
	 * @param  string $context What the value is for.
	 *                          Valid values are 'view' and 'edit'.
	 *
	 * @return string
	 */
	public function get_from_range( $context = 'view' ): string {
		return $this->get_prop( 'from_range', $context );
	}

	/**
	 * Get To Range.
	 *
	 * @param  string $context What the value is for.
	 *                          Valid values are 'view' and 'edit'.
	 *
	 * @return string
	 */
	public function get_to_range( $context = 'view' ): string {
		return $this->get_prop( 'to_range', $context );
	}

	/**
	 * Get Bookable. 'yes' or 'no'.
	 *
	 * @param  string $context What the value is for.
	 *                          Valid values are 'view' and 'edit'.
	 *
	 * @return string
	 */
	public function get_appointable( $context = 'view' ): string {
		return $this->get_prop( 'appointable', $context );
	}

	/**
	 * Get Priority.
	 *
	 * @param  string $context What the value is for.
	 *                          Valid values are 'view' and 'edit'.
	 *
	 * @return string
	 */
	public function get_priority( $context = 'view' ): string {
		return $this->get_prop( 'priority', $context );
	}

	/**
	 * Get Quantity.
	 *
	 * @param  string $context What the value is for.
	 *                          Valid values are 'view' and 'edit'.
	 *
	 * @return string
	 */
	public function get_qty( $context = 'view' ): string {
		return $this->get_prop( 'qty', $context );
	}

	/**
	 * Get RRULE.
	 *
	 * @param  string $context What the value is for.
	 *                          Valid values are 'view' and 'edit'.
	 *
	 * @return string
	 */
	public function get_rrule( $context = 'view' ): string {
		return $this->get_prop( 'rrule', $context );
	}

	/**
	 * Get availability rules.
	 *
	 * Retrieves the specific availability rules defined for this availability object.
	 *
	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
	 * @return array           List of availability rules.
	 */
	public function get_availability_rules( $context = 'view' ) {
		return array();
	}

	/**
	 * Get Ordering.
	 *
	 * @param  string $context What the value is for.
	 *                          Valid values are 'view' and 'edit'.
	 *
	 * @return string
	 */
	public function get_ordering( $context = 'view' ): string {
		return $this->get_prop( 'ordering', $context );
	}

	/**
	 * Get whether or not availability rule applies to full days.
	 *
	 * @return bool True if availability rule affects full day.
	 */
	public function is_all_day(): bool {
		// 'custom' type is a date range, so they will always be all day.
		return ( 'custom' === $this->get_range_type() );
	}

	/**
	 * Get whether or not it's a store availability.
	 *
	 * @return bool True if it's a store availability.
	 */
	public function is_store_availability(): bool {
		return ( 'store_availability' === $this->get_range_type() );
	}

	/**
	 * Get start and end times for global availability rule.
	 *
	 * @param int $date Timestamp of beginning of the day to check.
	 *
	 * @return null|array {
	 *   int $start  Timestamp of start time.
	 *   int $end    Timestamp of end time.
	 */
	public function get_time_range_for_date( $date ): ?array {
		$rule_array = WC_Product_Appointment_Rule_Manager::process_availability_rules( [ $this ], 'global', false );
		$rule       = $rule_array[0] ?? '';
		if ( ! empty( $rule ) ) {
			$minute_data = WC_Product_Appointment_Rule_Manager::get_rule_minute_range( $rule, $date );
			$minutes     = $minute_data['minutes'];

			#print '<pre>'; print_r( $minute_data ); print '</pre>';

			if ( ( false === $minute_data['is_appointable'] ) && count( $minutes ) > 0 ) {
				$start_minute = $minutes[0];
				$end_minute   = end( $minutes ) + 1; #add minute to round up.

				return [
					'start' => $date + $start_minute * self::SECONDS_IN_A_MINUTE,
					'end'   => $date + $end_minute * self::SECONDS_IN_A_MINUTE,
				];
			}
		}
		return null;
	}

	/**
	 * Helper method that returns formatted date.
	 *
	 * @param int    $date_ts Timestamp to be formatted to date.
	 * @param string $date_format Format string for date.
	 * @param string $time_format Format string for time.
	 *
	 * @return string Date formatted via date_i18n
	 */
	public function get_formatted_date( $date_ts = null, $date_format= null, $time_format = null ): string {
		if ( is_null( $date_format ) ) {
			$date_format = wc_appointments_date_format();
		}
		if ( is_null( $time_format ) ) {
			$time_format = ', ' . wc_appointments_time_format();
		}

		if ( $this->is_all_day() ) {
			return date_i18n( $date_format, $date_ts );
		}
        return date_i18n( $date_format . $time_format, $date_ts );
	}

	/**
	 * For multi-day events, check if the timestamp occurs after 00:00.
	 *
	 * @param int $start_ts Timestamp.
	 *
	 * @return bool True if date starts during the day.
	 */
	public function date_starts_today( $start_ts ): bool {
		if ( 'custom:daterange' !== $this->get_range_type() ) {
			return true;
		}
		return '00:00' !== date_i18n( 'H:i', $start_ts );
	}

	/**
	 * Set kind (rule type).
	 *
	 * @param string $kind Rule type name.
	 *
	 * @return WC_Appointments_Availability
	 */
	public function set_kind( $kind ): self {
		$this->set_prop( 'kind', $kind );

		return $this;
	}

	/**
	 * Set kind id (rule type id).
	 *
	 * @param string $kind_id Rule type id.
	 *
	 * @return WC_Appointments_Availability
	 */
	public function set_kind_id( $kind_id ): self {
		$this->set_prop( 'kind_id', $kind_id );

		return $this;
	}

	/**
	 * Set event id (gcal event id).
	 *
	 * @param string $event_id Rule type id.
	 *
	 * @return WC_Appointments_Availability
	 */
	public function set_event_id( $event_id ): self {
		$this->set_prop( 'event_id', $event_id );

		return $this;
	}

	/**
	 * Set parent event id (for modified instances of recurring events).
	 *
	 * @param string $parent_event_id Parent recurring event ID.
	 *
	 * @return WC_Appointments_Availability
	 */
	public function set_parent_event_id( $parent_event_id ): self {
		$this->set_prop( 'parent_event_id', $parent_event_id );

		return $this;
	}

	/**
	 * Set Title
	 *
	 * @param string $title Title.
	 *
	 * @return WC_Appointments_Availability
	 */
	public function set_title( $title ): self {
		$this->set_prop( 'title', $title );

		return $this;
	}

	/**
	 * Set Range Type.
	 *
	 * @param string $range_type Range Type.
	 *
	 * @return WC_Appointments_Availability
	 */
	public function set_range_type( $range_type ): self {
		$this->set_prop( 'range_type', $range_type );

		return $this;
	}

	/**
	 * Set From Date
	 *
	 * @param string $from_date From Date.
	 *
	 * @return WC_Appointments_Availability
	 */
	public function set_from_date( $from_date ): self {
		$this->set_prop( 'from_date', $from_date );

		return $this;
	}

	/**
	 * Set To Date.
	 *
	 * @param string $to_date To Date.
	 *
	 * @return WC_Appointments_Availability
	 */
	public function set_to_date( $to_date ): self {
		$this->set_prop( 'to_date', $to_date );

		return $this;
	}

	/**
	 * Set From Range.
	 *
	 * @param string $from_range From Range.
	 *
	 * @return WC_Appointments_Availability
	 */
	public function set_from_range( $from_range ): self {
		$this->set_prop( 'from_range', $from_range );

		return $this;
	}

	/**
	 * Set To Range.
	 *
	 * @param string $to_range To Range.
	 *
	 * @return WC_Appointments_Availability
	 */
	public function set_to_range( $to_range ): self {
		$this->set_prop( 'to_range', $to_range );

		return $this;
	}

	/**
	 * Set Bookable.
	 *
	 * @param string $appointable Bookable.
	 *
	 * @return WC_Appointments_Availability
	 */
	public function set_appointable( $appointable ): self {
		$this->set_prop( 'appointable', $appointable );

		return $this;
	}

	/**
	 * Set Priority.
	 *
	 * @param string $priority Priority.
	 *
	 * @return WC_Appointments_Availability
	 */
	public function set_priority( $priority ): self {
		$this->set_prop( 'priority', (int) $priority );

		return $this;
	}

	/**
	 * Set Quantity.
	 *
	 * @param string $qty Priority.
	 *
	 * @return WC_Appointments_Availability
	 */
	public function set_qty( $qty ): self {
		$this->set_prop( 'qty', (int) $qty );

		return $this;
	}

	/**
	 * Set Ordering.
	 *
	 * @param string $ordering Ordering.
	 *
	 * @return WC_Appointments_Availability
	 */
	public function set_ordering( $ordering ): self {
		$this->set_prop( 'ordering', (int) $ordering );

		return $this;
	}

	/**
	 * Set RRULE.
	 *
	 * @param string $rrule RRULE.
	 *
	 * @return WC_Appointments_Availability
	 */
	public function set_rrule( $rrule ): self {
		$this->set_prop( 'rrule', $rrule );

		return $this;
	}

	/**
	 * Set webhook created date.
	 *
	 * @since 3.2.0
	 *
	 * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime.
	 *                                  If the DateTime string has no timezone or offset,
	 *                                  WordPress site timezone will be assumed.
	 *                                  Null if their is no date.
	 *
	 * @return WC_Appointments_Availability
	 */
	public function set_date_created( $date = null ): self {
		$this->set_date_prop( 'date_created', $date );

		return $this;
	}

	/**
	 * Set webhook modified date.
	 *
	 * @since 3.2.0
	 *
	 * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime.
	 *                                  If the DateTime string has no timezone or offset,
	 *                                  WordPress site timezone will be assumed.
	 *                                  Null if their is no date.
	 *
	 * @return WC_Appointments_Availability
	 */
	public function set_date_modified( $date = null ): self {
		$this->set_date_prop( 'date_modified', $date );

		return $this;
	}

	/**
	 * Check if an availability is in the past.
	 *
	 * @return bool
	 */
	public function has_past(): bool {
        $now_gmt        = current_time( 'timestamp', true );
        $grace_threshold = strtotime( '-30 days', $now_gmt );

        $to_ts = null;

        if ( 'time:range' === $this->get_range_type() ) {
            $to_date = $this->get_to_date();
            if ( ! empty( $to_date ) ) {
                $to_ts = strtotime( $to_date . ' 23:59:59 UTC' );
            }
        } elseif ( 'custom:daterange' === $this->get_range_type() ) {
            $to_date = $this->get_to_date();
            if ( ! empty( $to_date ) ) {
                $to_ts = strtotime( $to_date . ' 23:59:59 UTC' );
            }
        } elseif ( 'custom' === $this->get_range_type() ) {
            $to_range = $this->get_to_range();
            if ( ! empty( $to_range ) ) {
                $to_ts = strtotime( $to_range . ' UTC' );
            }
        }

        $retval = ( $to_ts && ( $to_ts < $grace_threshold ) );

        return apply_filters( 'woocommerce_appointments_availability_has_past', $retval, $this );
	}

	/**
	 * Check of offset exists.
	 *
	 * @param mixed $offset Offset.
	 *
	 * @return bool
	 */
	#[\ReturnTypeWillChange]
	public function offsetExists( $offset ) {
		$offset = $this->update_bc_offset( $offset );

		if ( 'id' === $offset ) {
			return true;
		}

		return array_key_exists( $offset, $this->data );
	}

	/**
	 * Get prop based on offset.
	 *
	 * @param mixed $offset Offset.
	 *
	 * @return mixed
	 */
	#[\ReturnTypeWillChange]
	public function offsetGet( $offset ) {
		$offset = $this->update_bc_offset( $offset );

		if ( 'id' === $offset ) {
			return $this->get_id();
		}

		return $this->get_prop( $offset );
	}

	/**
	 * Set offset.
	 *
	 * @param mixed $offset Offset.
	 * @param mixed $value Value.
	 */
	#[\ReturnTypeWillChange]
	public function offsetSet( $offset, $value ): void {
		$offset = $this->update_bc_offset( $offset );

		if ( 'id' === $offset ) {
			$this->set_id();
			return;
		}

		$this->set_prop( $offset, $value );
	}

	/**
	 * Unset Offset.
	 *
	 * @param mixed $offset Offset.
	 */
	#[\ReturnTypeWillChange]
	public function offsetUnset( $offset ): void {
		$offset = $this->update_bc_offset( $offset );

		if ( 'id' === $offset ) {
			$this->set_id( 0 );
			return;
		}

		$this->set_prop( $offset, null );
	}

	/**
	 * Convert offset to it's new name.
	 *
	 * @param mixed $offset Offset.
	 *
	 * @return string
	 */
	#[\ReturnTypeWillChange]
	private function update_bc_offset( $offset ) {
		if ( 'to' === $offset ) {
			$offset = 'to_range';
		} elseif ( 'from' === $offset ) {
			$offset = 'from_range';
		} elseif ( 'type' === $offset ) {
			$offset = 'range_type';
		} elseif ( 'ID' === $offset ) {
			$offset = 'id';
		}
		return $offset;
	}
}
