<?php
namespace SabaiApps\Directories\Component\Calendar;

use SabaiApps\Directories\Component\AbstractComponent;
use SabaiApps\Directories\Component\Field;
use SabaiApps\Directories\Component\Form;
use SabaiApps\Directories\Component\View;
use SabaiApps\Directories\Component\System;
use SabaiApps\Directories\Component\Faker;
use SabaiApps\Directories\Context;
use SabaiApps\Directories\Application;

class CalendarComponent extends AbstractComponent implements
	Field\ITypes,
	Field\IWidgets,
	Form\IFields,
	View\IModes,
	System\IScheduledActions,
	Faker\IGenerators,
	Field\IRenderers,
	System\IAdminRouter {

	const VERSION = '2.0.6', PACKAGE = 'directories-pro';

	const OCCURRENCE_STATUS_ACTIVE    = 1;
	const OCCURRENCE_STATUS_CANCELLED = 2;
	const OCCURRENCE_STATUS_PASSED    = 3;

	public static function description() {
		return 'Add calendar features to content items.';
	}

	public function fieldGetTypeNames() {
		return array( 'calendar_slot' );
	}

	public function fieldGetType( $name ) {
		switch ( $name ) {
			case 'calendar_slot':
				return new FieldType\SlotFieldType( $this->_application, $name );
		}
	}

	public function fieldGetWidgetNames() {
		return array( 'calendar_slot' );
	}

	public function fieldGetWidget( $name ) {
		switch ( $name ) {
			case 'calendar_slot':
				return new FieldWidget\SlotFieldWidget( $this->_application, $name );
		}
	}

	public function formGetFieldTypes() {
		return array( 'calendar_slot' );
	}

	public function formGetField( $type ) {
		switch ( $type ) {
			case 'calendar_slot':
				return new FormField\SlotFormField( $this->_application );
		}
	}

	public function viewGetModeNames() {
		return array( 'calendar' );
	}

	public function viewGetMode( $name ) {
		switch ( $name ) {
			case 'calendar':
				return new ViewMode\CalendarViewMode( $this->_application, $name );
		}
	}

	public function onEntityCreateEntitySuccess( $bundle, $entity, $values, $extraArgs ) {
		$this->onEntityUpdateEntitySuccess( $bundle, $entity, null, $values, $extraArgs );
	}

	public function onEntityUpdateEntitySuccess( $bundle, $entity, $oldEntity, $values, $extraArgs ) {
		if ( ! in_array( $entity->getType(), array( 'post', 'user' ) ) ) {
			return;
		}

		if ( $occurrences = $this->_getUpdatedCalendarOccurrences( $entity, $values, $oldEntity ) ) {
			$occ_deleted = $occ_changed = array();
			foreach ( array_keys( $occurrences ) as $slot_field_name ) {
				$current_occs = $this->getModel( 'Occurrence' )
					->bundleName_is( $entity->getBundleName() )
					->entityId_is( $entity->getId() )
					->slotFieldName_is( $slot_field_name )
					->startDate_isOrGreaterThan( time() )
					->status_is( self::OCCURRENCE_STATUS_ACTIVE )
					->fetch();
				foreach ( $current_occs as $occ ) {
					if ( isset( $occurrences[ $slot_field_name ][ $occ->hash ] ) ) {
						$new_slot_data = $occurrences[ $slot_field_name ][ $occ->hash ]['slot_data'] ?? array();
						if ( $occ->slot_data_snapshot !== $new_slot_data ) {
							$occ_data = $occ->data;
							if ( empty( $occ_data ) ) { // update only if no additional data stored
								$occ->slot_data_snapshot = $new_slot_data;
								unset( $occurrences[ $slot_field_name ][ $occ->hash ] );
							} else {
								$occ->status = self::OCCURRENCE_STATUS_CANCELLED;
								$old_hash    = $occ->hash;
								$occ->hash   = 'CHANGED_' . $occ->hash . '_' . time();
								$occ->commit(); // commit now to prevent unique key violation
								$occ_changed[ $old_hash ] = $occ;
							}
						} else {
							unset( $occurrences[ $slot_field_name ][ $occ->hash ] );
						}
					} else {
						$occ->status = self::OCCURRENCE_STATUS_CANCELLED;
						$old_hash    = $occ->hash;
						$occ->hash   = 'DELETED_' . $occ->hash . '_' . time();
						$occ->commit(); // commit now to prevent unique key violation
						$occ_deleted[ $old_hash ] = $occ;
					}
				}
				foreach ( array_keys( $occurrences[ $slot_field_name ] ) as $occ_hash ) {
					$occurrence = $occurrences[ $slot_field_name ][ $occ_hash ];
					if ( $occurrence['start_date'] < time() ) {
						// do not create occurrences in the past
						unset( $occurrences[ $slot_field_name ][ $occ_hash ] );
						continue;
					}

					$occ                     = $this->getModel( 'Occurrence' )->create();
					$occ->hash               = $occ_hash;
					$occ->entity_type        = $entity->getType();
					$occ->bundle_name        = $entity->getBundleName();
					$occ->entity_id          = $entity->getId();
					$occ->slot_field_name    = $slot_field_name;
					$occ->slot_id            = $occurrence['slot_id'];
					$occ->title              = $occurrence['title'];
					$occ->start_date         = $occurrence['start_date'];
					$occ->end_date           = $occurrence['end_date'];
					$occ->all_day            = ! empty( $occurrence['all_day'] );
					$occ->slot_data_snapshot = $occurrence['slot_data'];
					$occ->markNew();
				}
			}
			$this->getModel()->commit();
			if ( ! empty( $occ_changed ) ) {
				$this->_application->Action( 'calendar_occurrences_changed', array( $bundle, $entity, $occ_changed ) );
			}
			if ( ! empty( $occ_deleted ) ) {
				$this->_application->Action( 'calendar_occurrences_deleted', array( $bundle, $entity, $occ_deleted ) );
			}
		}
	}

	protected function _getUpdatedCalendarOccurrences( $entity, $values, $oldEntity = null ) {
		$occurrences = array();
		foreach ( $this->_application->Entity_Field( $entity ) as $field ) {
			if ( $field->getFieldType() !== 'calendar_slot' ) {
				continue;
			}
			// Skip if no slot values and no existing occurrences
			if ( ! isset( $values[ $field->getFieldName() ] ) ) {
				continue;
			}
			if ( empty( $values[ $field->getFieldName() ] )
				&& ( ! isset( $oldEntity ) || ! $oldEntity->getFieldValue( $field->getFieldName() ) )
			) {
				continue;
			}

			$slot_field_name                 = $field->getFieldName();
			$occurrences[ $slot_field_name ] = array();
			if ( ! empty( $values[ $slot_field_name ] ) ) {
				foreach ( $values[ $slot_field_name ] as $slot ) {
					if ( empty( $slot['slot_id'] ) ) {
						continue; // this should not happen
					}

					foreach ( $this->_application->Calendar_Calendar_slotOccurrences( $slot_field_name, $slot['slot_id'], $entity, true ) as $occurrence_hash => $occurrence ) {
						$occurrences[ $slot_field_name ][ $occurrence_hash ] = $occurrence;
					}
				}
			}
		}
		return $occurrences;
	}

	public function systemScheduledActions() {
		if ( ! $this->_application->Calendar_Calendar_slotFields( true ) ) {
			return array();
		}

		return array(
			'calendar_generate_occurrences' => array(
				'recurrence'        => 'daily',
				'title'             => __( 'Generate Calendar Occurrences', 'directories-pro' ),
				'description'       => __( 'Generates calendar occurrences for upcoming events.', 'directories-pro' ),
				'priority'          => 10,
				'immediate_actions' => array(
					'calendar_generate_slot_occurrences' => array(
						'args_count' => 2,
					),
				),
			),
			'calendar_expire_occurrences'   => array(
				'recurrence'  => 'daily',
				'title'       => __( 'Expire Calendar Occurrences', 'directories-pro' ),
				'description' => __( 'Expires calendar occurrences for past events.', 'directories-pro' ),
				'priority'    => 10,
			),
		);
	}

	public function onCalendarExpireOccurrences() {
		$criteria = $this->getModel()->createCriteria( 'Occurrence' )
			->endDate_isSmallerThan( time() )
			->status_is( self::OCCURRENCE_STATUS_ACTIVE );
		$updated  = $this->getModel()->getGateway( 'Occurrence' )->updateByCriteria( $criteria, array( 'occurrence_status' => self::OCCURRENCE_STATUS_PASSED ) );
		$this->_application->logDebug( sprintf( 'Expired %d calendar occurrences.', $updated ) );
	}

	public function onCalendarGenerateSlotOccurrences( $fieldName, $slotId ) {
		$occurrences = $this->_application->Calendar_Calendar_slotOccurrences( $fieldName, $slotId );
		foreach ( $occurrences as $occurrence_hash => $occurrence ) {
			$occ                     = $this->getModel( 'Occurrence' )->create();
			$occ->hash               = $occurrence_hash;
			$occ->entity_type        = $occurrence['entity_type'];
			$occ->bundle_name        = $occurrence['bundle_name'];
			$occ->entity_id          = $occurrence['entity_id'];
			$occ->slot_field_name    = $fieldName;
			$occ->slot_id            = $occurrence['slot_id'];
			$occ->title              = $occurrence['title'];
			$occ->start_date         = $occurrence['start_date'];
			$occ->end_date           = $occurrence['end_date'];
			$occ->all_day            = ! empty( $occurrence['all_day'] );
			$occ->slot_data_snapshot = $occurrence['slot_data'];
			$occ->markNew();
			$this->_application->logDebug(
				sprintf(
					'Generated occurrence %s (%s - %s) for slot ID %s of field %s in entity %s ID %d.',
					$occurrence_hash,
					$this->_application->System_Date_datetime( $occurrence['start_date'] ),
					$this->_application->System_Date_datetime( $occurrence['end_date'] ),
					$slotId,
					$fieldName,
					$occurrence['entity_type'],
					$occurrence['entity_id']
				)
			);
		}
		$this->getModel()->commit();
	}

	public function onCalendarGenerateOccurrences() {
		foreach ( $this->_application->Calendar_Calendar_slotFields( true ) as $field_name => $fields ) {
			$entities = $this->_application->Entity_Query( 'post' )
				->fieldIsNotNull( $field_name, 'rrule' )
				->fetch();
			foreach ( $entities as $entity ) {
				if ( ! $values = $entity->getFieldValue( $field_name ) ) {
					continue;
				}

				foreach ( $values as $slot ) {
					$this->_application->logDebug(
						sprintf(
							'Scheduling generation of occurrences for slot ID %s of field %s in entity %s ID %d.',
							$slot['slot_id'],
							$field_name,
							$entity->getType(),
							$entity->getId()
						)
					);
					$this->_application->getPlatform()->getScheduler()->enqueueAction(
						'calendar_generate_slot_occurrences',
						array(
							'field_name' => $field_name,
							'slot_id'    => $slot['slot_id'],
						)
					);
				}
			}
		}
	}

	public function fakerGetGeneratorNames() {
		return array( 'calendar_slot' );
	}

	public function fakerGetGenerator( $name ) {
		switch ( $name ) {
			case 'calendar_slot':
				return new FakerGenerator\SlotFakerGenerator( $this->_application, $name );
		}
	}

	public function fieldGetRendererNames() {
		return array( 'calendar_datelist' );
	}

	public function fieldGetRenderer( $name ) {
		switch ( $name ) {
			case 'calendar_datelist':
				return new FieldRenderer\DateListFieldRenderer( $this->_application, $name );
		}
	}

	public function systemAdminRoutes() {
		$routes = array();
		foreach ( array_keys( $this->_application->Entity_BundleTypes() ) as $bundle_type ) {
			if ( ! $admin_path = $this->_application->Entity_BundleTypeInfo( $bundle_type, 'admin_path' ) ) {
				continue;
			}

			$routes += array(
				$admin_path . '/calendar/occurrences' => array(
					'controller'      => 'Occurrences',
					'access_callback' => true,
					'title_callback'  => true,
					'callback_path'   => 'occurrences',
				),
			);
		}

		return $routes;
	}

	public function systemOnAccessAdminRoute( Context $context, $path, $accessType, array &$route ) {
		switch ( $path ) {
			case 'occurrences':
				if ( $accessType === Application::ROUTE_ACCESS_LINK ) {
					if ( ( ! $entity_id = $context->getRequest()->asInt( 'entity_id' ) )
						|| ( ! $slot_field_name = $context->getRequest()->asStr( 'slot_field_name' ) )
						|| ( ! $entity = $this->_application->Entity_Entity( $context->bundle->entitytype_name, $entity_id ) )
						|| $context->bundle->name !== $entity->getBundleName()
						|| ( ! $field = $this->_application->Entity_Field( $context->bundle, $slot_field_name ) )
					) {
						$context->setNotFoundError();
						return false;
					}
					// Require edit entity permission
					if ( ! $this->_application->Entity_IsRoutable( $context->bundle, 'edit', $entity ) ) {
						$context->setError();
						return false;
					}
					$context->entity          = $entity;
					$context->slot_field_name = $slot_field_name;
				}
				return true;
		}
	}

	public function systemAdminRouteTitle( Context $context, $path, $titleType, array $route ) {
		switch ( $path ) {
			case 'occurrences':
				$title = $this->_application->Entity_Title( $context->entity )
					. ' - ' . $this->_application->Entity_Field( $context->bundle, $context->slot_field_name )->getFieldLabel()
					. ' - ' . __( 'Calendar Occurrences', 'directories-pro' );
				return $this->_application->Filter(
					'calendar_admin_occurrences_title',
					$title,
					array( $context, $context->bundle, $titleType )
				);
		}
	}
}
