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

use SabaiApps\Directories\Component\AbstractComponent;
use SabaiApps\Directories\Component\Field;
use SabaiApps\Directories\Component\Display;
use SabaiApps\Directories\Component\System;
use SabaiApps\Directories\Component\CSV;
use SabaiApps\Directories\Application;
use SabaiApps\Directories\Context;
use SabaiApps\Directories\Exception;
use SabaiApps\Directories\Component\Entity\Model\Bundle;
use SabaiApps\Framework\User\AbstractIdentity;
use SabaiApps\Directories\Exception\IException;

class EntityComponent extends AbstractComponent implements
	Field\ITypes,
	Field\IWidgets,
	Field\IRenderers,
	Field\IFilters,
	Display\IElements,
	Display\ILabels,
	Display\IStatistics,
	System\IMainRouter,
	System\IAdminRouter,
	System\ITools,
	System\IScheduledActions,
	IFeatures,
	CSV\IExporters,
	CSV\IImporters {

	const VERSION         = '2.0.6', PACKAGE = 'directories-pro';
	const FIELD_REALM_ALL = 0, FIELD_REALM_ENTITY_TYPE_DEFAULT = 1, FIELD_REALM_BUNDLE_DEFAULT = 2;

	public static function interfaces() {
		return array( 'WordPressContent\INotifications', 'WordPress\IBlocks' );
	}

	public static function description() {
		return 'Provides general functions and features to manage content.';
	}

	public static function events() {
		return array(
			'entitycreateentity'         => 99,
			'entityupdateentity'         => 99,
			'displayelementrenderfilter' => 15, // after other visibility settings are processed
		);
	}

	public function onCoreComponentsLoaded() {
		$this->_application->setHelper( 'Entity_Storage', array( __CLASS__, 'storageHelper' ) )
			->setHelper( 'Entity_Entity', array( __CLASS__, 'entityHelper' ) )
			->setHelper( 'Entity_Title', array( __CLASS__, 'titleHelper' ) )
			->setHelper( 'Entity_Icon', array( __CLASS__, 'iconHelper' ) )
			->setHelper( 'Entity_Image', array( __CLASS__, 'imageHelper' ) )
			->setHelper( 'Entity_Color', array( __CLASS__, 'colorHelper' ) )
			->setHelper( 'Entity_Status', array( __CLASS__, 'statusHelper' ) )
			->setHelper( 'Entity_Author', array( __CLASS__, 'authorHelper' ) )
			->setHelper( 'Entity_IsAuthor', array( __CLASS__, 'isAuthorHelper' ) )
			->setHelper( 'Entity_HtmlClass', array( __CLASS__, 'htmlClassHelper' ) );
	}

	public static function titleHelper( Application $application, Type\IEntity $entity, $noFilter = false ) {
		$title = strip_tags( $entity->getTitle() );
		if ( ! strlen( $title ) ) {
			$title = $application->System_Text_summarize( $entity->getContent(), 150 );
			if ( ! strlen( $title ) ) {
				$title = __( '(no title)', 'directories-pro' );
			}
		}
		return $noFilter ? $title : $application->Filter( 'entity_title', $title, array( $entity ) );
	}

	public static function imageHelper( Application $application, Type\IEntity $entity, $size = 'medium', $fieldName = null, $returnNoImgUrl = false ) {
		if ( ( ! $bundle = $application->Entity_Bundle( $entity ) )
			|| ( ! $field_name = isset( $fieldName ) ? $fieldName : ( isset( $bundle->info['entity_image'] ) ? $bundle->info['entity_image'] : null ) )
			|| ( ! $value = $entity->getSingleFieldValue( $field_name ) )
			|| ( ! $field_type = $application->Field_Type( $entity->getFieldType( $field_name ), true ) )
			|| ! $field_type instanceof Field\Type\IImage
		) {
			return $returnNoImgUrl ? $application->System_NoImage( 'medium', true, $entity ) : null;
		}

		switch ( $size ) {
			case 'icon':
				return $field_type->fieldImageGetIconUrl( $value );
			case 'icon_lg':
				return $field_type->fieldImageGetIconUrl( $value, 'lg' );
			case 'icon_xl':
				return $field_type->fieldImageGetIconUrl( $value, 'xl' );
			case 'full':
				return $field_type->fieldImageGetFullUrl( $value );
			default:
				return $field_type->fieldImageGetUrl( $value, $size );
		}
	}

	public static function iconHelper( Application $application, Type\IEntity $entity, $fallbackBundleIcon = true, $iconField = null ) {
		if ( $bundle = $application->Entity_Bundle( $entity ) ) {
			if ( ! isset( $iconField )
				&& isset( $bundle->info['entity_icon'] )
			) {
				$iconField = $bundle->info['entity_icon'];
			}
			if ( ! empty( $iconField )
				&& ( $icon = $entity->getSingleFieldValue( $iconField ) )
			) {
				return $icon;
			}
		}

		return $fallbackBundleIcon ? $application->Entity_BundleTypeInfo( $entity->getBundleType(), 'icon' ) : null;
	}

	public static function colorHelper( Application $application, Type\IEntity $entity ) {
		if ( ( $bundle = $application->Entity_Bundle( $entity ) )
			&& ! empty( $bundle->info['entity_color'] )
			&& ( $color = $entity->getSingleFieldValue( $bundle->info['entity_color'] ) )
		) {
			return $color;
		}
	}

	public static function storageHelper( Application $application ) {
		return Storage::getInstance( $application );
	}

	public static function entityHelper( Application $application, $entityType, $entityId, $loadEntityFields = true ) {
		try {
			if ( is_array( $entityId ) ) {
				if ( count( $entityId ) !== 2 ) {
					throw new Exception\InvalidArgumentException();
				}
				// Array with bundle name and entity slug passed
				if ( ! $entity = $application->Entity_Types_impl( $entityType )->entityTypeEntityBySlug( $entityId[0], $entityId[1] ) ) {
					return;
				}
			} elseif ( ! $entity = $application->Entity_Types_impl( $entityType )->entityTypeEntityById( $entityId ) ) {
				return;
			}
			if ( $loadEntityFields ) {
				$application->Entity_Field_load( $entity );
			}
			return $entity;
		} catch ( Exception\IException $e ) {
			$application->logError( $e );
		}
	}

	public static function authorHelper( Application $application, Type\IEntity $entity ) {
		if ( ! $author = $entity->getAuthor() ) {
			if ( $entity->getAuthorId() ) {
				$author = $application->UserIdentity( $entity->getAuthorId() );
			} else {
				$author = $application->getPlatform()->getUserIdentityFetcher()->getAnonymous();
			}
			if ( $entity->getType() === 'post' ) {
				$entity->setAuthor( $application->Filter( 'entity_author', $author, array( $entity ) ) );
			}
		}
		return $author;
	}

	public static function isAuthorHelper( Application $application, Type\IEntity $entity, ?AbstractIdentity $identity = null ) {
		if ( ! isset( $identity ) ) {
			$identity = $application->getUser()->getIdentity();
		}

		if ( ! $identity->isAnonymous() ) {
			$ret = $entity->getAuthorId() === $identity->id;
		} else {
			$ret = false;
		}

		return $application->Filter( 'entity_is_author', $ret, array( $entity, $identity ) );
	}

	public static function htmlClassHelper( Application $application, Type\IEntity $entity, $asArray = false ) {
		$classes = array(
			'drts-entity',
			'drts-entity-' . $entity->getType(),
		);
		if ( $featured = $entity->isFeatured() ) {
			$classes[] = 'drts-entity-featured';
			$classes[] = 'drts-entity-featured-' . $featured;
		}

		$classes = $application->Filter( 'entity_html_classes', $classes, array( $entity ) );

		return $asArray ? $classes : implode( ' ', $classes );
	}

	public static function statusHelper( Application $application, $entityType, $status = 'publish' ) {
		$info = $application->Entity_Types_impl( $entityType )->entityTypeInfo( 'status' );
		if ( is_array( $status ) ) {
			$ret = array();
			foreach ( $status as $_status ) {
				$ret[] = isset( $info[ $_status ] ) ? $info[ $_status ] : $_status;
			}
			return $ret;
		}
		return isset( $info[ $status ] ) ? $info[ $status ] : $status;
	}

	public function onEntityITypesInstalled( AbstractComponent $component ) {
		if ( ! $new_entity_types = $component->entityGetTypeNames() ) {
			return;
		}

		$this->_application->Entity_Admin_createEntityTypes( $component, $new_entity_types );
		$this->_application->getPlatform()->deleteCache( 'entity_types' );
	}

	public function onEntityITypesUninstalled( AbstractComponent $component ) {
		$entity_types = $this->_application->Entity_Types( true );
		if ( ! isset( $entity_types[ $component->getName() ] ) ) {
			return;
		}

		$this->_application->Entity_Admin_deleteEntityTypes( $component, $entity_types[ $component->getName() ] );
		$this->_application->getPlatform()->deleteCache( 'entity_types' );
	}

	public function onEntityITypesUpgraded( AbstractComponent $component ) {
		$entity_types = $this->_application->Entity_Types( true );
		if ( ! $names = $component->entityGetTypeNames() ) {
			if ( isset( $entity_types[ $component->getName() ] ) ) {
				$this->_application->Entity_Admin_deleteEntityTypes( $component, $entity_types[ $component->getName() ] );
				unset( $entity_types[ $component->getName() ] );
			}
		} elseif ( ! isset( $entity_types[ $component->getName() ] ) ) {
				$this->_application->Entity_Admin_createEntityTypes( $component, $names );
				$entity_types[ $component->getName() ] = $names;
		} else {
			$old_entity_types                      = $entity_types[ $component->getName() ];
			$entity_types[ $component->getName() ] = array();
			if ( $new_entity_types = array_diff( $names, $old_entity_types ) ) {
				$this->_application->Entity_Admin_createEntityTypes( $component, $new_entity_types );
				foreach ( $new_entity_types as $new_entity_type ) {
					$entity_types[ $component->getName() ][] = $new_entity_type;
				}
			}
			if ( $current_entity_types = array_intersect( $old_entity_types, $names ) ) {
				$this->_application->Entity_Admin_updateEntityTypes( $component, $current_entity_types );
				foreach ( $current_entity_types as $current_entity_type ) {
					$entity_types[ $component->getName() ][] = $current_entity_type;
				}
			}
			if ( $deleted_entity_types = array_diff( $old_entity_types, $names ) ) {
				$this->_application->Entity_Admin_deleteEntityTypes( $component, $deleted_entity_types );
			}
		}
		$this->_application->getPlatform()->deleteCache( 'entity_types' );
	}

	public function onEntityIBundleTypesUninstalled( AbstractComponent $component ) {
		if ( ! $bundle_types = $component->entityGetBundleTypeNames() ) {
			return;
		}

		if ( $bundles = $this->getModel( 'Bundle' )->type_in( $bundle_types )->fetch()->getArray() ) {
			$this->_application->Entity_Admin_deleteEntityBundles( $bundles );
		}
	}

	public function onEntityIBundleTypesUpgraded( AbstractComponent $component ) {
		if ( $bundle_types = $component->entityGetBundleTypeNames() ) {
			// Should we update existing bundle types on component updates?
		}
	}

	public function createEntityField( Model\Bundle $bundle, $fieldName, array $fieldInfo, $overwrite = false ) {
		$updatedFields = array();
		if ( ! $field = $this->_application->callHelper( 'Entity_Admin_createEntityField', array( $bundle, $fieldName, $fieldInfo, $overwrite, &$updatedFields ) ) ) {
			return;
		}

		$is_new = $field->FieldConfig->id ? false : true;
		$field->FieldConfig->commit();
		$field->commit();

		if ( $is_new ) {
			$this->_application->Entity_Admin_createFieldStorage( array( $field->FieldConfig ) );
		} else {
			// Update field storage if schema has changed
			if ( ! empty( $updatedFields ) ) {
				$this->_application->Entity_Admin_updateFieldStorage( $updatedFields );
			}
		}

		return $field;
	}

	public function createEntityPropertyField( Model\Bundle $bundle, Model\FieldConfig $fieldConfig, array $fieldInfo, $commit = true ) {
		if ( ! $field = $this->_application->Entity_Admin_createEntityPropertyField( $bundle, $fieldConfig, $fieldInfo, true ) ) {
			return;
		}

		if ( $commit ) {
			$this->getModel()->commit();
		}

		return $field;
	}

	/**
	 * Delete entities
	 *
	 * @param type  $entityType
	 * @param array $entities An array of Type\IEntity objects indexed by entity IDs
	 */
	public function deleteEntities( $entityType, array $entities, array $extraArgs = array() ) {
		if ( empty( $entities ) ) {
			return;
		}

		// Load field values from storage so that all field values can be accessed by other components upon delete event
		$this->_application->Entity_Field_load( $entityType, $entities, true, false );

		if ( empty( $extraArgs['fields_only'] ) ) {
			$this->_application->Entity_Types_impl( $entityType )->entityTypeDeleteEntities( $entities );
		}

		// Delete fields
		$entities_by_bundle = $field_values = $bundles_arr = array();
		foreach ( $entities as $entity ) {
			$entities_by_bundle[ $entity->getBundleName() ][ $entity->getId() ] = $entity;
		}
		$bundles = $this->_application->Entity_Bundles_collection( array_keys( $entities_by_bundle ) )->with( 'Fields', 'FieldConfig' );
		foreach ( $bundles as $bundle ) {
			foreach ( $bundle->Fields as $field ) {
				$field_values[ $bundle->name ][ $field->getFieldName() ] = $field->getFieldName();
			}
			$bundles_arr[ $bundle->name ] = $bundle;
		}
		foreach ( $field_values as $bundle_name => $_field_values ) {
			$this->_application->Entity_Storage()->purgeValues( $entityType, array_keys( $entities_by_bundle[ $bundle_name ] ), $_field_values );
			// Clear cached entity fields
			$this->_application->Entity_Field_removeCache( $bundle_name, array_keys( $entities_by_bundle[ $bundle_name ] ) );
		}

		// Notify entities have been deleted
		foreach ( $entities as $entity ) {
			$bundle = $bundles_arr[ $entity->getBundleName() ];
			$this->_invokeEntityEvents( $entityType, $bundle->type, array( $bundle, $entity, array_keys( $entities ), $extraArgs ), 'delete', 'success' );
		}

		foreach ( $entities_by_bundle as $bundle_name => $entities ) {
			$bundle = $bundles_arr[ $bundle_name ];
			$this->_invokeEntityEvents( $entityType, $bundle->type, array( $bundle, $entities, $extraArgs ), 'bulk_delete', 'success' );
		}
	}

	private function _invokeEntityEvents( $entityType, $bundleType, array $params, $prefix, $suffix = '' ) {
		$prefix = 'entity_' . $prefix;
		$suffix = $suffix !== '' ? 'entity_' . $suffix : 'entity';
		$this->_application->Action( $prefix . '_' . $suffix, $params );
		$this->_application->Action( $prefix . '_' . $entityType . '_' . $suffix, $params );
		$this->_application->Action( $prefix . '_' . $entityType . '_' . $bundleType . '_' . $suffix, $params );
	}

	public function deleteFieldStorage( array $fieldConfigs, $force = false ) {
		$fields = array();
		foreach ( $fieldConfigs as $field_config ) {
			$fields[ $field_config->name ] = $field_config;
		}
		$this->_application->Entity_Storage()->delete( $fields, $force );
	}

	public function onFieldTypeDeleted( $fieldType ) {
		$field_configs = array();
		foreach ( $this->getModel( 'FieldConfig' )->type_is( $fieldType->name )->fetch()->with( 'Fields' ) as $field_config ) {
			$field_config->markRemoved();
			$field_configs[ $field_config->name ] = $field_config;
		}
		$this->getModel()->commit();
		$this->deleteFieldStorage( $field_configs );
	}

	public function uninstall( $removeData = false ) {
		if ( $removeData ) {
			// Remove tables created by custom fields

			$fields = array();
			foreach ( $this->getModel( 'FieldConfig' )->fetch() as $field ) {
				if ( $field->property ) {
					continue;
				}

				$fields[] = $field;
			}

			// Remove field tables
			if ( ! empty( $fields ) ) {
				$this->deleteFieldStorage( $fields, true );
			}
		}

		parent::uninstall( $removeData );
	}

	protected function _hasSupportedRenderer( Model\Bundle $bundle, Field\IField $field, array $renderers ) {
		foreach ( array_keys( $renderers ) as $renderer_type ) {
			if ( ( $renderer = $this->_application->Field_Renderers_impl( $renderer_type, true ) )
				&& $renderer->fieldRendererSupports( $bundle, $field )
			) {
				return true;
			}
		}
		return false;
	}

	public function displayGetElementNames( Model\Bundle $bundle ) {
		$ret         = array( 'entity_fieldlist', 'entity_fieldtemplate' );
		$field_types = $this->_application->Field_Types();

		// Elements for "form" display type
		foreach ( array_keys( $field_types ) as $field_type_name ) {
			$field_type = $field_types[ $field_type_name ];
			if ( isset( $field_type['entity_types'] )
				&& ! in_array( $bundle->entitytype_name, $field_type['entity_types'] )
			) {
				// the field type does not support the entity type of the current bundle
				unset( $field_types[ $field_type_name ] );
				continue;
			}

			if ( ! empty( $field_type['widgets'] ) && ! $field_type['admin_only'] ) {
				$ret[] = 'entity_form_' . $field_type_name;
			}
		}

		foreach ( $this->_application->Entity_Field( $bundle->name ) as $field ) {
			if ( ! $field_type = $this->_isFieldRenderable( $field, $field_types ) ) {
				continue;
			}

			if ( $this->_hasSupportedRenderer( $bundle, $field, $field_type['renderers'] ) ) {
				$ret[] = 'entity_field_' . $field->getFieldName();
			}

			if ( $field->getFieldType() === 'entity_reference' ) {
				$field_settings = $field->getFieldSettings();
				if ( ! empty( $field_settings['bundle'] )
					&& ( $ref_bundle = $this->_application->Entity_Bundle( $field_settings['bundle'] ) )
				) {
					foreach ( $this->_application->Entity_Field( $ref_bundle ) as $ref_bundle_field ) {
						if ( ! $this->_isFieldRenderable( $ref_bundle_field, $field_types ) ) {
							continue;
						}

						$ret[] = 'entity_reference_field_' . $field->getFieldName() . '__' . $ref_bundle->name . '__' . $ref_bundle_field->getFieldName();
					}
				}
			}
		}
		if ( ! empty( $bundle->info['parent'] ) ) {
			foreach ( $this->_application->Entity_Field( $bundle->info['parent'] ) as $field ) {
				if ( ! $this->_isFieldRenderable( $field, $field_types ) ) {
					continue;
				}

				$ret[] = 'entity_parent_field_' . $field->getFieldName();
			}
		} elseif ( ! empty( $bundle->info['is_taxonomy'] ) ) {
			if ( ! empty( $bundle->info['is_hierarchical'] ) ) {
				$ret[] = 'entity_child_terms';
			}
		}
		if ( ! empty( $bundle->info['taxonomies'] ) ) {
			foreach ( $bundle->info['taxonomies'] as $taxonomy_bundle_type => $taxonomy_bundle_name ) {
				if ( ! $taxonomy_bundle_fields = $this->_application->Entity_Field( $taxonomy_bundle_name ) ) {
					continue;
				}

				foreach ( $taxonomy_bundle_fields as $field ) {
					if ( ! $this->_isFieldRenderable( $field, $field_types )
						|| $field->getFieldType() === 'entity_title'
					) {
						continue;
					}

					$ret[] = 'entity_tax_field_' . $field->getFieldName() . '__' . $taxonomy_bundle_type;
				}
			}
		}

		return $ret;
	}

	public function displayGetElement( $name ) {
		switch ( $name ) {
			case 'entity_fieldlist':
				return new DisplayElement\FieldListDisplayElement( $this->_application, $name );
			case 'entity_fieldtemplate':
				return new DisplayElement\FieldTemplateDisplayElement( $this->_application, $name );
			case 'entity_child_terms':
				return new DisplayElement\ChildTermsDisplayElement( $this->_application, $name );
			default:
				if ( strpos( $name, 'entity_field_' ) === 0 ) {
					return new DisplayElement\FieldDisplayElement( $this->_application, $name );
				} elseif ( strpos( $name, 'entity_parent_field_' ) === 0 ) {
					return new DisplayElement\ParentFieldDisplayElement( $this->_application, $name );
				} elseif ( strpos( $name, 'entity_reference_field_' ) === 0 ) {
					return new DisplayElement\ReferenceFieldDisplayElement( $this->_application, $name );
				} elseif ( strpos( $name, 'entity_form_' ) === 0 ) {
					return new DisplayElement\FormDisplayElement( $this->_application, $name );
				} elseif ( strpos( $name, 'entity_tax_field_' ) === 0 ) {
					$name = explode( '__', $name );
					return new DisplayElement\TaxonomyFieldDisplayElement( $this->_application, $name[0], $name[1] );
				}
		}
	}

	protected function _isFieldRenderable( Field\IField $field, array $fieldTypes ) {
		if ( ! isset( $fieldTypes[ $field->getFieldType() ] ) ) {
			return;
		}

		$field_type = $fieldTypes[ $field->getFieldType() ];
		if ( empty( $field_type['renderers'] ) ) {
			return;
		}

		return $field_type;
	}

	public function systemAdminRoutes() {
		$routes = array(
			'/_drts/entity/field' => array(
				'controller' => 'FieldInfo',
			),
		);
		foreach ( array_keys( $this->_application->Entity_BundleTypes() ) as $bundle_type ) {
			if ( ( ! $admin_path = $this->_application->Entity_BundleTypeInfo( $bundle_type, 'admin_path' ) )
				|| isset( $routes[ $admin_path ] ) // path added already
			) {
				continue;
			}

			$routes += array(
				$admin_path                => array(
					'controller'      => 'Edit',
					'access_callback' => true,
					'title_callback'  => true,
					'callback_path'   => 'edit',
					'type'            => Application::ROUTE_TAB,
				),
				$admin_path . '/fields'    => array(
					'controller'      => 'Fields',
					'access_callback' => true,
					'title_callback'  => true,
					'callback_path'   => 'fields',
					'type'            => Application::ROUTE_TAB,
					'weight'          => 1,
				),
				$admin_path . '/displays'  => array(
					'controller'      => 'Displays',
					'access_callback' => true,
					'title_callback'  => true,
					'callback_path'   => 'displays',
					'type'            => Application::ROUTE_TAB,
					'weight'          => 10,
				),
				$admin_path . '/add_level' => array(
					'controller'      => 'AddLevel',
					'access_callback' => true,
					'title_callback'  => true,
					'callback_path'   => 'add_level',
				),
			);
		}

		return $routes;
	}

	public function systemOnAccessAdminRoute( Context $context, $path, $accessType, array &$route ) {
		switch ( $path ) {
			case 'edit':
				if ( $accessType === Application::ROUTE_ACCESS_LINK ) {
					if ( ( ! $bundle_name = $context->getRequest()->asStr( 'bundle_name' ) )
						|| ( ! $bundle = $this->_application->Entity_Bundle( $bundle_name ) )
					) {
						return false;
					}

					$context->bundle = $bundle;
				}
				return true;
			case 'fields':
				if ( $context->bundle->isAlias() ) {
					return false;
				}
				return $this->_application->Entity_BundleTypeInfo( $context->bundle->type, 'addable' ) !== false;
			case 'displays':
				if ( $context->bundle->isAlias() ) {
					return false;
				}
				return $this->_application->Entity_Displays( $context->bundle ) ? true : false;
			case 'add_level':
				if ( $accessType === Application::ROUTE_ACCESS_LINK ) {
					if ( ! $this->_application->Entity_BundleTypeInfo( $context->bundle->type, 'levelable' ) ) {
						return false;
					}

					if ( $level = $context->getRequest()->asStr( 'level' ) ) {
						if ( isset( $context->bundle->info['entity_levels'][ $level ] ) ) {
							$context->level      = $level;
							$context->level_data = $context->bundle->info['entity_levels'][ $level ];
						} elseif ( $level === 'default' ) {
							$context->level      = $level;
							$context->level_data = $this->_application->Entity_Levels_defaultLevel( $context->bundle );
						}
					}
				}
				return true;
		}
	}

	public function systemAdminRouteTitle( Context $context, $path, $titleType, array $route ) {
		switch ( $path ) {
			case 'edit':
				if ( $titleType === Application::ROUTE_TITLE_INFO ) {
					$context->clearTabs( true );
					return $context->bundle->getLabel( 'singular' );
				} else {
					return __( 'Edit', 'directories-pro' );
				}
			case 'fields':
				return __( 'Manage Fields', 'directories-pro' );
			case 'displays':
				return __( 'Manage Displays', 'directories-pro' );
		}
	}

	public function systemMainRoutes( $lang = null ) {
		return array(
			'/_drts/entity/:bundle_type'                => array(
				'type'            => Application::ROUTE_CALLBACK,
				'access_callback' => true,
			),
			'/_drts/entity/:bundle_type/query'          => array(
				'controller' => 'QueryEntities',
				'type'       => Application::ROUTE_CALLBACK,
			),
			'/_drts/entity/:bundle_type/query/:bundle'  => array(
				'controller' => 'QueryEntities',
				'type'       => Application::ROUTE_CALLBACK,
			),
			'/_drts/entity/:bundle_type/taxonomy_terms' => array(
				'controller' => 'ListTaxonomyTerms',
				'type'       => Application::ROUTE_CALLBACK,
			),
			'/_drts/entity/:bundle_type/taxonomy_terms/:bundle' => array(
				'controller' => 'ListTaxonomyTerms',
				'type'       => Application::ROUTE_CALLBACK,
			),
		);
	}

	public function systemOnAccessMainRoute( Context $context, $path, $accessType, array &$route ) {
		switch ( $path ) {
			case '/_drts/entity/:bundle_type':
				if ( $accessType === Application::ROUTE_ACCESS_LINK ) {
					if ( ( ! $bundle_type = $context->getRequest()->asStr( 'bundle_type' ) )
						|| ! $this->_application->Entity_BundleTypes( $bundle_type )
					) {
						return false;
					}

					if ( false === $this->_application->Entity_BundleTypeInfo( $bundle_type, 'public' )
						&& ! $this->_application->getUser()->isAdministrator()
					) {
						return false;
					}

					$context->bundle_type = $bundle_type;
				}
				return true;
		}
	}

	public function systemMainRouteTitle( Context $context, $path, $titleType, array $route ) {}

	public function fieldGetTypeNames() {
		return array(
			'entity_id',
			'entity_bundle_name',
			'entity_bundle_type',
			'entity_slug',
			'entity_parent',
			'entity_child_count',
			'entity_reference',
			'entity_activity',
			'entity_title',
			'entity_published',
			'entity_modified',
			'entity_author',
			'entity_terms',
			'entity_term_content_count',
			'entity_term_parent',
			'entity_featured',
			'entity_expires',
			'entity_level',
			'entity_meta',
			'entity_content_count',
		);
	}

	public function fieldGetType( $name ) {
		switch ( $name ) {
			case 'entity_id':
				return new FieldType\IdFieldType( $this->_application, $name );
			case 'entity_slug':
				return new FieldType\SlugFieldType( $this->_application, $name );
			case 'entity_title':
				return new FieldType\TitleFieldType( $this->_application, $name );
			case 'entity_published':
				return new FieldType\PublishedFieldType( $this->_application, $name );
			case 'entity_modified':
				return new FieldType\ModifiedFieldType( $this->_application, $name );
			case 'entity_author':
				return new FieldType\AuthorFieldType( $this->_application, $name );
			case 'entity_parent':
				return new FieldType\ParentFieldType( $this->_application, $name );
			case 'entity_child_count':
				return new FieldType\ChildCountFieldType( $this->_application, $name );
			case 'entity_reference':
				return new FieldType\ReferenceFieldType( $this->_application, $name );
			case 'entity_activity':
				return new FieldType\ActivityFieldType( $this->_application, $name );
			case 'entity_terms':
				return new FieldType\TermsFieldType( $this->_application, $name );
			case 'entity_term_content_count':
				return new FieldType\TermContentCountFieldType( $this->_application, $name );
			case 'entity_term_parent':
				return new FieldType\TermParentFieldType( $this->_application, $name );
			case 'entity_featured':
				return new FieldType\FeaturedFieldType( $this->_application, $name );
			case 'entity_expires':
				return new FieldType\ExpiresFieldType( $this->_application, $name );
			case 'entity_meta':
				return new FieldType\MetaFieldType( $this->_application, $name );
			case 'entity_level':
				return new FieldType\LevelFieldType( $this->_application, $name );
			case 'entity_content_count':
				return new FieldType\ContentCountFieldType( $this->_application, $name );
			default:
				return new FieldType\FieldType( $this->_application, $name );
		}
	}

	public function fieldGetWidgetNames() {
		return array(
			'entity_parent',
			'entity_title',
			'entity_term_select',
			'entity_term_list',
			'entity_term_tagging',
			'entity_term_parent',
			'entity_author',
			'entity_reference',
			'entity_reference_hidden',
			'entity_featured',
			'entity_expires',
			'entity_term_multi_select',
			'entity_level',
			'entity_reference_checkboxes',
			'entity_meta',
		);
	}

	public function fieldGetWidget( $name ) {
		switch ( $name ) {
			case 'entity_parent':
				return new FieldWidget\ParentFieldWidget( $this->_application, $name );
			case 'entity_title':
				return new FieldWidget\TitleFieldWidget( $this->_application, $name );
			case 'entity_term_select':
				return new FieldWidget\TermSelectFieldWidget( $this->_application, $name );
			case 'entity_term_multi_select':
				return new FieldWidget\TermMultiSelectFieldWidget( $this->_application, $name );
			case 'entity_term_list':
				return new FieldWidget\TermListFieldWidget( $this->_application, $name );
			case 'entity_term_tagging':
				return new FieldWidget\TermTaggingFieldWidget( $this->_application, $name );
			case 'entity_term_parent':
				return new FieldWidget\TermParentFieldWidget( $this->_application, $name );
			case 'entity_author':
				return new FieldWidget\AuthorFieldWidget( $this->_application, $name );
			case 'entity_reference':
				return new FieldWidget\ReferenceFieldWidget( $this->_application, $name );
			case 'entity_reference_hidden':
				return new FieldWidget\ReferenceHiddenFieldWidget( $this->_application, $name );
			case 'entity_reference_checkboxes':
				return new FieldWidget\ReferenceCheckboxesFieldWidget( $this->_application, $name );
			case 'entity_featured':
				return new FieldWidget\FeaturedFieldWidget( $this->_application, $name );
			case 'entity_expires':
				return new FieldWidget\ExpiresFieldWidget( $this->_application, $name );
			case 'entity_meta':
				return new FieldWidget\MetaFieldWidget( $this->_application, $name );
			case 'entity_level':
				return new FieldWidget\LevelFieldWidget( $this->_application, $name );
		}
	}

	public function fieldGetRendererNames() {
		return array(
			'entity_id',
			'entity_title',
			'entity_published',
			'entity_modified',
			'entity_author',
			'entity_terms',
			'entity_term_content_count',
			'entity_reference',
			'entity_reference_link',
			'entity_expires',
			'entity_featured_end_date',
		);
	}

	public function fieldGetRenderer( $name ) {
		switch ( $name ) {
			case 'entity_id':
				return new FieldRenderer\IdFieldRenderer( $this->_application, $name );
			case 'entity_title':
				return new FieldRenderer\TitleFieldRenderer( $this->_application, $name );
			case 'entity_published':
				return new FieldRenderer\PublishedFieldRenderer( $this->_application, $name );
			case 'entity_modified':
				return new FieldRenderer\ModifiedFieldRenderer( $this->_application, $name );
			case 'entity_author':
				return new FieldRenderer\AuthorFieldRenderer( $this->_application, $name );
			case 'entity_terms':
				return new FieldRenderer\TermsFieldRenderer( $this->_application, $name );
			case 'entity_term_content_count':
				return new FieldRenderer\TermContentCountFieldRenderer( $this->_application, $name );
			case 'entity_reference':
				return new FieldRenderer\ReferenceFieldRenderer( $this->_application, $name );
			case 'entity_reference_link':
				return new FieldRenderer\ReferenceLinkFieldRenderer( $this->_application, $name );
			case 'entity_expires':
				return new FieldRenderer\ExpiresFieldRenderer( $this->_application, $name );
			case 'entity_featured_end_date':
				return new FieldRenderer\FeaturedEndDateFieldRenderer( $this->_application, $name );
		}
	}

	public function fieldGetFilterNames() {
		return array( 'entity_author' );
	}

	public function fieldGetFilter( $name ) {
		switch ( $name ) {
			case 'entity_author':
				return new FieldFilter\AuthorFieldFilter( $this->_application, $name );
		}
	}

	protected function _onEntityCreateBundlesSuccess( $bundles, $isUpdate = false ) {
		foreach ( $bundles as $bundle ) {
			if ( empty( $bundle->info['is_taxonomy'] ) ) {
				// Add activity field?
				if ( $this->_application->Entity_BundleTypeInfo( $bundle, 'entity_track_activity' ) ) {
					$this->createEntityField(
						$bundle,
						'entity_activity',
						array(
							'type'          => 'entity_activity',
							'settings'      => array(),
							'weight'        => 99,
							'max_num_items' => 1,
						)
					);
				}
				// Add taxonomy terms field if has taxonomies
				if ( ! empty( $bundle->info['taxonomies'] ) ) {
					$term_bundles = array();
					foreach ( $bundle->info['taxonomies'] as $taxonomy_bundle_type => $settings ) {
						if ( ! $term_bundle = $this->_application->Entity_Bundle( $taxonomy_bundle_type, $bundle->group ) ) {
							continue;
						}

						if ( $_term_bundle = $term_bundle->isAlias() ) {
							$term_bundle = $_term_bundle;
						}

						$taxonomy_max_num_items = isset( $settings['max_num_items'] ) ? intval( $settings['max_num_items'] ) : 0;
						$data                   = array( '_bundle_name' => $term_bundle->name );
						if ( false === $this->_application->Entity_BundleTypeInfo( $term_bundle, 'taxonomy_assignable' ) ) {
							$data['_no_ui'] = true;
						}
						if ( ! empty( $settings['data'] ) ) {
							$data += (array) $settings['data'];
						}
						$this->createEntityField(
							$bundle,
							$term_bundle->type,
							array(
								'type'            => 'entity_terms',
								'label'           => isset( $settings['label'] ) ? $settings['label'] : ( $taxonomy_max_num_items === 1 ? $term_bundle->getLabel( 'singular' ) : $term_bundle->getLabel() ),
								'description'     => isset( $settings['description'] ) ? $settings['description'] : '',
								'settings'        => array(),
								'required'        => ! empty( $settings['required'] ),
								'max_num_items'   => $taxonomy_max_num_items,
								'data'            => $data,
								'widget'          => $widget = isset( $settings['widget'] ) ?
									$settings['widget'] :
									( empty( $term_bundle->info['is_hierarchical'] ) ? 'entity_term_tagging' : 'entity_term_select' ),
								'widget_settings' => isset( $settings['widget_settings'] ) ? $settings['widget_settings'] : null,
								'weight'          => isset( $settings['weight'] ) ? $settings['weight'] : 10,
							)
						);
						$term_bundles[ $taxonomy_bundle_type ] = $term_bundle->name;
					}
					$bundle->setInfo( array( 'taxonomies' => $term_bundles ) );
				}
				// Add featured field?
				if ( $this->_application->Entity_BundleTypeInfo( $bundle, 'featurable' ) ) {
					$this->createEntityField(
						$bundle,
						'entity_featured',
						array(
							'type'          => 'entity_featured',
							'max_num_items' => 1,
						)
					);
				}
				// Check if the bundle is a child content type
				if ( ! empty( $bundle->info['parent'] )
					&& ( $parent_bundle = $this->_application->Entity_Bundle( $bundle->info['parent'], $bundle->group ) )
				) {
					$bundle->setInfo( array( 'parent' => $parent_bundle->name ) ); // change from bundle type to name
					$this->createEntityField(
						$parent_bundle,
						'entity_child_count',
						array(
							'type'          => 'entity_child_count',
							'settings'      => array(),
							'weight'        => 99,
							'max_num_items' => 0,
						)
					);
				}
				// Add parent entity field
				if ( $this->_application->Entity_BundleTypeInfo( $bundle, 'parent_field' ) === true ) {
					$label = null;
					if ( ! empty( $bundle->info['parent'] )
						&& ( $parent_bundle = $this->_application->Entity_Bundle( $bundle->info['parent'] ) )
					) {
						$label = $parent_bundle->getLabel( 'singular' );
					}
					$this->createEntityField(
						$bundle,
						'entity_parent',
						array(
							'type'          => 'entity_parent',
							'label'         => $label,
							'settings'      => array(),
							'weight'        => 99,
							'required'      => true,
							'max_num_items' => 1,
						)
					);
				}
				// Add expiration date field?
				if ( $this->_application->Entity_BundleTypeInfo( $bundle, 'expirable' ) ) {
					$this->createEntityField(
						$bundle,
						'entity_expires',
						array(
							'type'          => 'entity_expires',
							'max_num_items' => 1,
						)
					);
				}
				// Add feature level field?
				if ( $this->_application->Entity_BundleTypeInfo( $bundle, 'levelable' ) ) {
					$this->createEntityField(
						$bundle,
						'entity_level',
						array(
							'type'          => 'entity_level',
							'max_num_items' => 1,
						)
					);
				}
			} else {
				// Add term content count field
				$this->createEntityField(
					$bundle,
					'entity_term_content_count',
					array(
						'type'     => 'entity_term_content_count',
						'settings' => array(),
						'weight'   => 99,
					)
				);
			}
			// Add entity meta field
			$this->createEntityField(
				$bundle,
				'entity_meta',
				array(
					'type'     => 'entity_meta',
					'settings' => array(),
				)
			);
		}
	}

	public function onEntityCreateBundlesSuccess( $bundles ) {
		$this->_onEntityCreateBundlesSuccess( $bundles );
	}

	public function onEntityUpdateBundlesSuccess( $bundles ) {
		$this->_onEntityCreateBundlesSuccess( $bundles, true );
	}

	protected function _onEntityCreateBundlesCommitted( array $bundles, array $bundleInfo, $isUpdate = false ) {
		// Create displays and views if any
		foreach ( $bundles as $bundle ) {
			if ( ! empty( $bundleInfo[ $bundle->type ]['displays'] ) ) {
				foreach ( array_keys( $bundleInfo[ $bundle->type ]['displays'] ) as $display_type ) {
					foreach ( array_keys( $bundleInfo[ $bundle->type ]['displays'][ $display_type ] ) as $display_name ) {
						$display = $bundleInfo[ $bundle->type ]['displays'][ $display_type ][ $display_name ];
						$this->_application->Display_Create( $bundle, $display_type, $display_name, $display );
					}
				}
				if ( $isUpdate
					&& ! empty( $bundleInfo[ $bundle->type ]['displays']['form']['default'] )
				) {
					// Force create field elements for the default form display
					$this->_application->Entity_Form_getDisplay( $bundle, 'default', true, true );
				}
			}
			if ( ! empty( $bundleInfo[ $bundle->type ]['views'] ) ) {
				foreach ( array_keys( $bundleInfo[ $bundle->type ]['views'] ) as $view_name ) {
					$view = $bundleInfo[ $bundle->type ]['views'][ $view_name ];
					$this->_application->View_AdminView_add( $bundle, $view_name, $view['mode'], $view['label'], $view['settings'], ! empty( $view['default'] ) );
				}
			}
		}
	}

	public function onEntityCreateBundlesCommitted( array $bundles, array $bundleInfo ) {
		$this->_onEntityCreateBundlesCommitted( $bundles, $bundleInfo );
	}

	public function onEntityUpdateBundlesCommitted( array $bundles, array $bundleInfo ) {
		$this->_onEntityCreateBundlesCommitted( $bundles, $bundleInfo, true );
	}

	public function onEntityCreateEntity( $bundle, &$values ) {
		if ( ! isset( $values['entity_activity'] )
			&& $this->_application->Entity_BundleTypeInfo( $bundle, 'entity_track_activity' )
		) {
			$values['entity_activity'] = array(
				array(
					'active_at'   => ! empty( $values['published'] ) ? $values['published'] : time(),
					'edited_at'   => 0,
					'last_action' => 'new',
				),
			);
		}

		// Maybe populate title/content with value of another field
		$this->_maybePopulateTitle( $bundle, $values );
		$this->_maybePopulateContent( $bundle, $values );

		// Maybe add parent term entries to entity_terms field table
		$this->_maybeAddParentTermValues( $bundle, $values );

		// Set expiration date?
		if ( ! array_key_exists( 'entity_expires', $values )
			&& ! empty( $bundle->info['entity_expire'] )
			&& ! empty( $bundle->info['entity_expire_days'] )
		) {
			$values['entity_expires'] = array(
				array( 'value' => time() + 86400 * $bundle->info['entity_expire_days'] ),
			);
		}
	}

	public function onEntityUpdateEntity( $bundle, $entity, &$values ) {
		// Maybe populate title with value of another field
		$this->_maybePopulateTitle( $bundle, $values, $entity );
		// Maybe populate content with value of another field
		$this->_maybePopulateContent( $bundle, $values, $entity );

		if ( ! isset( $values['entity_activity'] )
			&& $this->_application->Entity_BundleTypeInfo( $bundle, 'entity_track_activity' )
		) {
			if ( isset( $values['content'] ) || isset( $values['title'] ) ) {
				$values['entity_activity'] = array(
					array(
						'active_at'   => time(),
						'edited_at'   => time(),
						'last_action' => 'edit',
					),
				);
			}
		}

		// Maybe add parent term entries to entity_terms field table
		$this->_maybeAddParentTermValues( $bundle, $values );

		// Remove expiration date if payment plan assigned
		if ( ! empty( $bundle->info['entity_expire'] ) ) {
			if ( ! empty( $values['payment_plan'][0] ) ) {
				$values['entity_expires'] = false;
			}
		}
	}

	protected function _maybePopulateTitle( $bundle, &$values, $entity = null ) {
		// Populate title if title is disabled
		if ( empty( $bundle->info['no_title'] ) ) {
			return;
		}

		if ( empty( $bundle->info['title_field'] )
			|| ( ! $title_field = $this->_application->Entity_Field( $bundle, $bundle->info['title_field'] ) )
		) {
			$content         = isset( $values['content'] ) ? $values['content'] : ( isset( $entity ) ? $entity->getContent() : null );
			$values['title'] = strlen( $content ) ? $this->_application->System_Text_summarize( $content, 150 ) : '';
			return;
		}

		$title_field_key = ( $property = $title_field->isPropertyField() ) ? $property : $bundle->info['title_field'];
		if ( isset( $values[ $title_field_key ] )
			&& ( $title_field_type = $this->_application->Field_Type( $title_field->getFieldType() ) )
			&& $title_field_type instanceof Field\Type\ITitle
		) {
			$title_field_values = $values[ $title_field_key ];
			if ( ! is_array( $title_field_values )
				|| ! array_key_exists( 0, $title_field_values )
			) {
				$title_field_values = array( $title_field_values );
			}
			if ( null !== $title = $title_field_type->fieldTitle( $title_field, $title_field_values ) ) {
				$values['title'] = $title;
			}
		}
	}

	protected function _maybePopulateContent( $bundle, &$values, $entity = null ) {
		// Populate content if content is disabled
		if ( empty( $bundle->info['no_content'] ) ) {
			return;
		}

		if ( empty( $bundle->info['content_field'] )
			|| ( ! $content_field = $this->_application->Entity_Field( $bundle, $bundle->info['content_field'] ) )
		) {
			return;
		}

		$content_field_key = ( $property = $content_field->isPropertyField() ) ? $property : $bundle->info['content_field'];
		if ( isset( $values[ $content_field_key ] )
			&& ( $content_field_type = $this->_application->Field_Type( $content_field->getFieldType() ) )
			&& $content_field_type instanceof Field\Type\IContent
		) {
			$content_field_values = $values[ $content_field_key ];
			if ( ! is_array( $content_field_values )
				|| ! array_key_exists( 0, $content_field_values )
			) {
				$content_field_values = array( $content_field_values );
			}
			if ( null !== $content = $content_field_type->fieldContent( $content_field, $content_field_values ) ) {
				$values['content'] = $content;
			}
		}
	}

	public function onEntityAfterCreateEntity( $bundle, $entity, $values, &$extraArgs ) {
		if ( ! empty( $extraArgs['entity_meta'] ) ) {
			foreach ( array_keys( $extraArgs['entity_meta'] ) as $field_name ) {
				if ( $field_name === 'entity_meta' ) {
					foreach ( $extraArgs['entity_meta'][ $field_name ] ?? array() as $meta_key => $meta_value ) {
						$this->_application->getPlatform()->setEntityMeta( $entity->getType(), $entity->getId(), $meta_key, $meta_value );
					}
				} else {
					$this->_application->getPlatform()->setEntityMeta( $entity->getType(), $entity->getId(), $field_name, $extraArgs['entity_meta'][ $field_name ] );
				}
			}
		}
	}

	public function onEntityAfterUpdateEntity( $bundle, $entity, $oldEntity, $values, &$extraArgs ) {
		if ( ! empty( $extraArgs['entity_meta'] ) ) {
			foreach ( array_keys( $extraArgs['entity_meta'] ) as $field_name ) {
				if ( $field_name === 'entity_meta' ) {
					foreach ( array_keys( $extraArgs['entity_meta'][ $field_name ] ?? array() ) as $meta_key ) {
						if ( null === ( $meta_value = $extraArgs['entity_meta'][ $field_name ][ $meta_key ] ) ) {
							$this->_application->getPlatform()->deleteEntityMeta( $entity->getType(), $entity->getId(), $meta_key );
						} else {
							$this->_application->getPlatform()->setEntityMeta( $entity->getType(), $entity->getId(), $meta_key, $meta_value );
						}
					}
				} elseif ( null === ( $meta_value = $extraArgs['entity_meta'][ $field_name ] ) ) {
						$this->_application->getPlatform()->deleteEntityMeta( $entity->getType(), $entity->getId(), $field_name );
				} else {
					$this->_application->getPlatform()->setEntityMeta( $entity->getType(), $entity->getId(), $field_name, $meta_value );
				}
			}
		}
	}

	public function onEntityCreateEntitySuccess( $bundle, $entity, $values, $extraArgs ) {
		if ( $entity->isPublished() ) {
			$this->_application->Action( 'entity_entity_published', array( $entity, null ) );
		}

		if ( ! empty( $bundle->info['is_taxonomy'] ) ) {
			$this->_application->Entity_TaxonomyTerms_clearCache( $bundle->name );
			return;
		}

		// Update parent post
		if ( ! empty( $bundle->info['parent'] )
			&& empty( $extraArgs['entity_skip_update_parent'] )
		) {
			$this->updateParentPostStats( $entity, $entity->getTimestamp(), $entity->isPublished() );
		}

		// Update taxonomy term content count
		if ( ! empty( $bundle->info['taxonomies'] ) ) {
			// Get terms added
			$terms_updated = array();
			foreach ( $bundle->info['taxonomies'] as $taxonomy_name ) {
				if ( ! $taxonomy_bundle = $this->_application->Entity_Bundle( $taxonomy_name ) ) {
					continue;
				}

				$new_terms = $entity->getFieldValue( $taxonomy_bundle->type );
				if ( empty( $new_terms ) ) {
					continue;
				}

				foreach ( $new_terms as $new_term ) {
					$terms_updated[ $taxonomy_bundle->name ][ $new_term->getId() ] = $new_term;
				}
			}
			if ( ! empty( $terms_updated ) ) {
				foreach ( array_keys( $terms_updated ) as $taxonomy ) {
					$this->_application->Entity_UpdateTermContentCount( $taxonomy, $terms_updated[ $taxonomy ], $bundle );
				}
			}
		}

		if ( $reference_field_names = $entity->getFieldNamesByType( 'entity_reference' ) ) {
			foreach ( $reference_field_names as $field_name ) {
				if ( ! isset( $values[ $field_name ] ) ) {
					continue;
				}

				$this->_syncReferenceField( $field_name, $entity );
			}
		}

		if ( $entity->isPublished() ) {
			// Clear facet count cache
			$this->_application->Entity_Facets_clearCache();
		}
	}

	public function onEntityUpdateEntitySuccess( $bundle, $entity, $oldEntity, $values, $extraArgs ) {
		if ( $entity->isPublished() ) {
			if ( $oldEntity->isPending() ) {
				$this->_application->Action( 'entity_entity_published', array( $entity, $oldEntity ) );
			}
		}

		if ( ! empty( $bundle->info['is_taxonomy'] ) ) {
			$this->_application->Entity_TaxonomyTerms_clearCache( $bundle->name );
			return;
		}

		// Update parent post
		if ( ! empty( $bundle->info['parent'] )
			&& empty( $extraArgs['entity_skip_update_parent'] )
			&& ( $this->_application->Entity_ParentEntity( $entity, false ) )
		) {
			$timestamp = $update_children_count = false;
			// Content updated?
			if ( isset( $values['content'] ) || isset( $values['title'] ) ) {
				if ( $this->_application->Entity_BundleTypeInfo( $bundle, 'entity_track_activity' ) ) {
					// Update last activity timestamp of the parent with the last edited timestamp of the updated entity
					$timestamp = $entity->getSingleFieldValue( 'entity_activity', 'edited_at' );
				} else {
					$timestamp = time();
				}
			}
			// Content status changed?
			if ( isset( $values['status'] )
				&& ( $oldEntity->isPublished() || $entity->isPublished() )
			) {
				// The content was published or unpublished
				$update_children_count = true;
			}

			$this->updateParentPostStats( $entity, $timestamp, $update_children_count );
		}

		// Update taxonomy term content count
		if ( ! empty( $bundle->info['taxonomies'] ) ) {
			$is_published_or_unpublished = isset( $values['status'] )
				&& ( $entity->isPublished() || $oldEntity->isPublished() );

			$taxonomy_updated = array();
			foreach ( $bundle->info['taxonomies'] as $taxonomy_name ) {
				if ( ! $taxonomy_bundle = $this->_application->Entity_Bundle( $taxonomy_name ) ) {
					continue;
				}

				if ( $is_published_or_unpublished
					|| isset( $values[ $taxonomy_bundle->type ] )
				) {
					$taxonomy_updated[ $taxonomy_bundle->type ] = $taxonomy_bundle->name;
				}
			}

			if ( ! empty( $taxonomy_updated ) ) {
				$terms_updated = array();
				foreach ( $taxonomy_updated as $taxonomy_type => $taxonomy_name ) {
					// Get terms newly assigned or unassigned
					$current_terms = (array) @$entity->getFieldValue( $taxonomy_type );
					if ( $old_terms = (array) @$oldEntity->getFieldValue( $taxonomy_type ) ) {
						// Exclude terms deleted
						if ( ! empty( $extraArgs['taxonomy_terms_deleted'][ $taxonomy_type ] ) ) {
							foreach ( array_keys( $old_terms ) as $old_term_key ) {
								if ( in_array( $old_terms[ $old_term_key ]->getId(), $extraArgs['taxonomy_terms_deleted'][ $taxonomy_type ] ) ) {
									unset( $old_terms[ $old_term_key ] );
								}
							}
						}
					}
					if ( empty( $current_terms ) && empty( $old_terms ) ) {
						continue;
					}

					foreach ( $current_terms as $current_term ) {
						$terms_updated[ $taxonomy_name ][ $current_term->getId() ] = $current_term;
					}
					if ( $is_published_or_unpublished ) {
						// The content was either published or unpublished. Update all terms.
						foreach ( $old_terms as $old_term ) {
							$terms_updated[ $taxonomy_name ][ $old_term->getId() ] = $old_term;
						}
					} else {
						// Update terms that were newly added or removed
						foreach ( $old_terms as $old_term ) {
							if ( isset( $terms_updated[ $taxonomy_name ][ $old_term->getId() ] ) ) {
								unset( $terms_updated[ $taxonomy_name ][ $old_term->getId() ] );
							} else {
								$terms_updated[ $taxonomy_name ][ $old_term->getId() ] = $old_term;
							}
						}
					}
					if ( empty( $terms_updated[ $taxonomy_name ] ) ) {
						unset( $terms_updated[ $taxonomy_name ] );
					}
				}
				if ( ! empty( $terms_updated ) ) {
					foreach ( array_keys( $terms_updated ) as $taxonomy ) {
						$this->_application->Entity_UpdateTermContentCount( $taxonomy, $terms_updated[ $taxonomy ], $bundle );
					}
				}
			}
		}

		if ( empty( $extraArgs['entity_skip_reference_sync'] ) ) {
			if ( $reference_field_names = $entity->getFieldNamesByType( 'entity_reference' ) ) {
				foreach ( $reference_field_names as $field_name ) {
					if ( ! isset( $values[ $field_name ] ) ) {
						continue; // the field was not updated
					}

					$this->_syncReferenceField( $field_name, $entity, $oldEntity );
				}
			}
		}

		if ( ! ( ! $entity->isPublished() && ! $oldEntity->isPublished() )
		) {
			// Clear facet count cache
			$this->_application->Entity_Facets_clearCache();
		}
	}

	protected function _syncReferenceField( $fieldName, Type\IEntity $entity, ?Type\IEntity $oldEntity = null ) {
		if ( ( ! $field = $this->_application->Entity_Field( $entity, $fieldName ) )
			|| ( ! $field_settings = $field->getFieldSettings() )
			|| empty( $field_settings['bundle'] )
			|| empty( $field_settings['sync'][ $field_settings['bundle'] ] )
			|| ( ! $bundle2sync = $this->_application->Entity_Bundle( $field_settings['bundle'] ) )
			|| ( ! $field2sync = $this->_application->Entity_Field( $bundle2sync, $field_settings['sync'][ $field_settings['bundle'] ] ) )
		) {
			return;
		}

		$ref_entities = $old_ref_entities = array();
		foreach ( (array) $entity->getFieldValue( $fieldName ) as $ref_entity ) {
			$ref_entities[ $ref_entity->getId() ] = $ref_entity;
		}
		if ( isset( $oldEntity ) ) {
			foreach ( (array) $oldEntity->getFieldValue( $fieldName ) as $ref_entity ) {
				$old_ref_entities[ $ref_entity->getId() ] = $ref_entity;
			}
		}

		// Add reference
		if ( $ref_entities_new = array_diff_key( $ref_entities, $old_ref_entities ) ) {
			$this->_application->Entity_Field_load( $bundle2sync->entitytype_name, $ref_entities_new );
			foreach ( $ref_entities_new as $ref_entity ) {
				$ref_entity_ref_ids = array();
				if ( $ref_entity_refs = $ref_entity->getFieldValue( $field2sync->getFieldName() ) ) {
					foreach ( $ref_entity_refs as $ref_entity_ref ) {
						$ref_entity_ref_ids[] = $ref_entity_ref->getId();
					}
				}
				if ( ! in_array( $entity->getId(), $ref_entity_ref_ids ) ) {
					$ref_entity_ref_ids[] = $entity->getId();
					$this->_application->Entity_Save(
						$ref_entity,
						array( $field2sync->getFieldName() => $ref_entity_ref_ids ),
						array( 'entity_skip_reference_sync' => true ) // prevents loop
					);
				}
			}
		}
		// Remove reference
		if ( $ref_entities_removed = array_diff_key( $old_ref_entities, $ref_entities ) ) {
			$this->_application->Entity_Field_load( $bundle2sync->entitytype_name, $ref_entities_removed );
			foreach ( $ref_entities_removed as $ref_entity ) {
				if ( ! $ref_entity_refs = $ref_entity->getFieldValue( $field2sync->getFieldName() ) ) {
					continue;
				}

				$ref_entity_ref_ids = array();
				foreach ( $ref_entity_refs as $ref_entity_ref ) {
					$ref_entity_ref_ids[ $ref_entity_ref->getId() ] = $ref_entity_ref->getId();
				}
				if ( isset( $ref_entity_ref_ids[ $entity->getId() ] ) ) {
					unset( $ref_entity_ref_ids[ $entity->getId() ] );
					$this->_application->Entity_Save(
						$ref_entity,
						array( $field2sync->getFieldName() => $ref_entity_ref_ids ),
						array( 'entity_skip_reference_sync' => true ) // prevents loop
					);
				}
			}
		}
	}

	public function updateParentPostStats( Type\IEntity $entity, $timestamp = false, $updateChildrenCount = false, $isParent = false ) {
		if ( ! $isParent ) {
			if ( ! $parent = $this->_application->Entity_ParentEntity( $entity ) ) {
				return;
			}
		} else {
			// Parent post was passed as the first argument
			$parent = $entity;
		}

		$values = array();

		if ( $timestamp
			&& $this->_application->Entity_BundleTypeInfo( $parent->getBundleType(), 'entity_track_activity' )
		) {
			if ( is_bool( $timestamp ) ) {
				$last_child_entity = $this->_application->Entity_Query( $entity->getType() )
					->fieldIs( 'parent', $parent->getId() )
					->sortByField( 'entity_activity', 'DESC', 'active_at' )
					->fetch();
				if ( $last_child_entity ) {
					$last_child_entity = array_shift( $last_child_entity );
					$timestamp         = $last_child_entity->getSingleFieldValue( 'entity_activity', 'active_at' );
				} else {
					// No last active child entity, then fetch last published
					$last_child_entity = $this->_application->Entity_Query( $entity->getType() )
						->fieldIs( 'parent', $parent->getId() )
						->sortByField( 'published', 'DESC' )
						->fetch();
					if ( $last_child_entity ) {
						$last_child_entity = array_shift( $last_child_entity );
						$timestamp         = $last_child_entity->getTimestamp();
					} else {
						// No last published child entity
						unset( $last_child_entity );
						$timestamp = 0;
					}
				}
			}
			// Update active timestamp of the parent post
			$parent_edited_at = (int) $parent->getSingleFieldValue( 'entity_activity', 'edited_at' );
			if ( $timestamp < $parent_edited_at ) {
				$timestamp      = $parent_edited_at;
				$active_post_id = 0;
			} else {
				$active_post_id = $isParent ? 0 : ( isset( $last_child_entity ) ? $last_child_entity->getId() : $entity->getId() );
			}
			$values = array(
				'entity_activity' => array(
					'active_at'      => $timestamp,
					'edited_at'      => $parent_edited_at,
					'active_post_id' => $active_post_id,
				),
			);
		}

		if ( $updateChildrenCount
			&& ( $child_bundle_types = (array) $this->_application->Entity_BundleTypes_children( $parent->getBundleType(), false ) )
		) {
			$values['entity_child_count'] = array();
			// Count the total number of child posts
			$count = $this->_application->Entity_Query( $parent->getType() )
				->startCriteriaGroup( 'OR' )
				->fieldIs( 'parent', $parent->getId() )
				->startCriteriaGroup()
				->fieldIs( 'entity_parent', $parent->getType(), 'parent_entity_type' )
				->fieldIs( 'entity_parent', $parent->getId(), 'parent_entity_id' )
				->finishCriteriaGroup()
				->finishCriteriaGroup()
				->fieldIs( 'status', $this->_application->Entity_Status( $parent->getType(), 'publish' ) )
				->fieldIsIn( 'bundle_type', $child_bundle_types )
				->groupByField( 'bundle_type' )
				->count();
			if ( ! empty( $count ) ) {
				foreach ( $count as $child_bundle_type => $_count ) {
					if ( ! empty( $_count ) ) {
						$values['entity_child_count'][] = array(
							'value'        => $_count,
							'content_name' => $child_bundle_type,
						);
					}
				}
			}
			if ( empty( $values['entity_child_count'] ) ) {
				// Remove field value
				$values['entity_child_count'] = false;
			}
		}

		if ( ! empty( $values ) ) {
			$this->_application->Entity_Save( $parent, $values );
		}
	}

	protected function _maybeAddParentTermValues( $bundle, &$values ) {
		if ( ! empty( $bundle->info['is_taxonomy'] )
			|| empty( $bundle->info['taxonomies'] )
		) {
			return;
		}

		$entity_type_impl = null;
		foreach ( $bundle->info['taxonomies'] as $taxonomy_bundle_type => $taxonomy ) {
			if ( empty( $values[ $taxonomy_bundle_type ] )
				|| false === $this->_application->Entity_BundleTypeInfo( $taxonomy_bundle_type, 'is_hierarchical' )
			) {
				continue;
			}

			$all_values = array();
			foreach ( array_keys( $values[ $taxonomy_bundle_type ] ) as $i ) {
				if ( empty( $values[ $taxonomy_bundle_type ][ $i ]['value'] ) ) {
					continue;
				}

				$all_values[] = $values[ $taxonomy_bundle_type ][ $i ]['value'];
			}
			if ( ! isset( $entity_type_impl ) ) {
				$entity_type_impl = $this->_application->Entity_Types_impl( $this->_application->Entity_BundleTypeInfo( $taxonomy_bundle_type, 'entity_type' ) );
			}
			$parent_ids = array();
			foreach ( $all_values as $term_id ) {
				try {
					$_parent_ids = $entity_type_impl->entityTypeParentEntityIds( $term_id, $taxonomy );
				} catch ( Exception\RuntimeException $e ) {
					$this->_application->logError( $e );
					continue;
				}
				foreach ( $_parent_ids as $parent_id ) {
					if ( in_array( $parent_id, $all_values )
						|| in_array( $parent_id, $parent_ids )
					) {
						continue;
					}

					$values[ $taxonomy_bundle_type ][] = array(
						'value' => $parent_id,
						'auto'  => true,
					);
					$parent_ids[]                      = $parent_id;
				}
			}
		}
	}

	public function displayGetStatisticNames( Model\Bundle $bundle ) {
		$ret = array();
		if ( empty( $bundle->info['is_taxonomy'] )
			&& empty( $bundle->info['parent'] )
		) {
			foreach ( $this->_application->Entity_BundleTypes_children( $bundle->type ) as $bundle_type ) {
				try {
					$this->_application->Entity_Bundle( $bundle_type, $bundle->group, true );
				} catch ( IException $e ) {
					continue;
				}

				$ret[] = 'entity_child_entity_count_' . $bundle_type;
			}
		}
		return $ret;
	}

	public function displayGetStatistic( $name ) {
		return new DisplayStatistic\ChildEntityCountDisplayStatistic( $this->_application, $name );
	}

	public function displayGetLabelNames( Model\Bundle $bundle ) {
		$ret = array();
		if ( $this->_application->Entity_BundleTypeInfo( $bundle, 'featurable' ) ) {
			$ret[] = 'entity_featured';
		}
		if ( empty( $bundle->info['is_taxonomy'] ) ) {
			$ret[] = 'entity_status';
			$ret[] = 'entity_new';
			$ret[] = 'entity_level';
		}

		return $ret;
	}

	public function displayGetLabel( $name ) {
		switch ( $name ) {
			case 'entity_status':
				return new DisplayLabel\StatusDisplayLabel( $this->_application, $name );
			case 'entity_featured':
				return new DisplayLabel\FeaturedEntityDisplayLabel( $this->_application, $name );
			case 'entity_new':
				return new DisplayLabel\NewEntityDisplayLabel( $this->_application, $name );
			case 'entity_level':
				return new DisplayLabel\LevelDisplayLabel( $this->_application, $name );
		}
	}

	public function wpGetNotificationNames() {
		return array( 'pending' );
	}

	public function wpGetNotification( $name ) {
		return new WordPressNotification\EntityWordPressNotification( $this->_application, $name );
	}

	public function onDisplayRenderFilter( &$ret, $display, $bundle, $var, $options ) {
		if ( $display['type'] !== 'entity'
			|| $display['name'] !== 'detailed'
		) {
			return;
		}

		if ( ! empty( $bundle->info['entity_schemaorg']['type'] ) ) {
			$this->_application->Entity_SchemaOrg( $var, $bundle->info['entity_schemaorg'] );
		}
		if ( ! empty( $bundle->info['entity_opengraph']['type'] ) ) {
			$this->_application->Entity_OpenGraph( $var, $bundle->info['entity_opengraph'] );
		}
	}

	public function onEntityBundleInfoUserKeysFilter( &$keys ) {
		$keys[] = 'entity_image';
		$keys[] = 'entity_icon';
		$keys[] = 'entity_icon_is_image';
		$keys[] = 'entity_color';
		$keys[] = 'entity_title';
		$keys[] = 'entity_expire';
		$keys[] = 'entity_expire_days';
		$keys[] = 'entity_expire_field';
		$keys[] = 'entity_expire_status';
		$keys[] = 'entity_expire_ref';
		$keys[] = 'entity_expire_ref_bundles';
	}

	public function systemGetToolNames() {
		$ret = array( 'entity_refresh_field_cache', 'entity_refresh_term_cache', 'entity_recount' );
		if ( ( $langs = $this->_application->getPlatform()->getLanguages() )
			&& count( $langs ) > 1
		) {
			$ret[] = 'entity_gen_trans';
		}
		return $ret;
	}

	public function systemGetTool( $name ) {
		switch ( $name ) {
			case 'entity_refresh_field_cache':
				return new SystemTool\RefreshFieldCacheSystemTool( $this->_application, $name );
			case 'entity_refresh_term_cache':
				return new SystemTool\RefreshTermCacheSystemTool( $this->_application, $name );
			case 'entity_recount':
				return new SystemTool\RecountSystemTool( $this->_application, $name );
			case 'entity_gen_trans':
				return new SystemTool\GenerateTranslationsSystemTool( $this->_application, $name );
		}
	}

	public function onDisplayElementRenderFilter( &$results, $bundle, array $element, $var ) {
		$previous_checks_passed = empty( $results ) || in_array( true, $results, true );
		if ( ! $previous_checks_passed
			|| ! $var instanceof Type\IEntity
			|| empty( $element['data']['entity_conditions'] )
		) {
			return;
		}

		$results[] = $this->_application->Entity_Field_checkConditions( $element['data']['entity_conditions'], $var );
	}

	public function onDisplayElementSettingsFormFilter( &$form, $bundle, $display, $element, $elementData ) {
		if ( $display->type !== 'entity'
			|| false === $element->displayElementInfo( $bundle, 'conditionable' )
		) {
			return;
		}

		$form['#tabs']['entity_conditions'] = array(
			'#title'  => __( 'Conditions', 'directories-pro' ),
			'#weight' => 40,
		);
		$form['entity_conditions']          = array(
			'#tab' => 'entity_conditions',
		) + $this->_application->Entity_Field_conditionSettingsForm(
			$bundle->name,
			isset( $elementData['entity_conditions'] ) ? $elementData['entity_conditions'] : array(), // settings
			array( 'entity_conditions' ),
			true,
			array(), // exculdeFields
			array() // submitValues
		);
	}

	public function onEntityFormFilter( &$form, $bundle, $entity, &$options ) {
		// Remove warning message for "Exp. Date" field if no payment plan field.
		if ( $this->_application->getPlatform()->isAdmin()
			&& ! isset( $form['payment_plan'] )
		) {
			unset( $form['entity_expires'][0]['warning'] );
		}
	}

	public function onEntityFieldConditionRuleFilter( &$rule, $field, $compare, $value, $name, $type ) {
		$this->_maybeEncodeConditionRuleSlug( $rule, $field );
	}

	public function onViewFilterConditionRuleFilter( &$rule, $field, $settings, $compare, $value, $name ) {
		$this->_maybeEncodeConditionRuleSlug( $rule, $field );
	}

	protected function _maybeEncodeConditionRuleSlug( &$rule, $field ) {
		if ( $field->getFieldType() !== 'entity_terms'
			|| ! in_array( $rule['type'], array( 'value', '!value', 'one' ) )
		) {
			return;
		}

		$rule['value'] = (array) $rule['value'];
		// Slugs need to be URL encoded and lower-cased to match form values
		foreach ( array_keys( $rule['value'] ) as $i ) {
			if ( ! is_numeric( $rule['value'][ $i ] ) ) {
				$rule['value'][ $i ] = strtolower( urlencode( $rule['value'][ $i ] ) );
			}
		}
	}

	public function entityGetFeatureNames() {
		return array( 'entity_fields', 'entity_taxonomy_terms' );
	}

	public function entityGetFeature( $name ) {
		switch ( $name ) {
			case 'entity_fields':
				return new Feature\FieldsFeature( $this->_application, $name );
			case 'entity_taxonomy_terms':
				return new Feature\TaxonomyTermsFeature( $this->_application, $name );
		}
	}

	public function onViewQueryFilter( $query, $bundle, $mode, $name, $settings, $isDefault ) {
		if ( ! empty( $settings['sort']['entity_levels'] )
			&& ! empty( $settings['sort']['entity_level_orders'] )
		) {
			$query->sortByCases( 'entity_level', $settings['sort']['entity_level_orders'] );
		}
	}

	public function onViewFeatureSettingsFormFilter( &$form, $bundle, $settings, $submitValues ) {
		if ( ! $levels = $this->_application->Entity_Levels_options( $bundle, false ) ) {
			return;
		}

		$form['sort']['entity_levels']       = array(
			'#type'          => 'checkbox',
			'#title'         => __( 'Sort items with a non-default feature level first', 'directories-pro' ),
			'#default_value' => ! empty( $settings['sort']['entity_levels'] ),
			'#horizontal'    => true,
			'#weight'        => 19,
		);
		$form['sort']['entity_level_orders'] = array(
			'#type'          => 'sortablecheckboxes',
			'#options'       => $levels,
			'#default_value' => isset( $settings['sort']['entity_level_orders'] ) ? $settings['sort']['entity_level_orders'] : array( current( array_keys( $levels ) ) ),
			'#title'         => __( 'Feature level sort order', 'directories-pro' ),
			'#horizontal'    => true,
			'#weight'        => 19,
			'#states'        => array(
				'visible' => array(
					'input[name="sort[entity_levels]"]' => array(
						'type'  => 'checked',
						'value' => true,
					),
				),
			),
		);
	}

	public function wordpressGetBlockNames() {
		return array( 'entity' );
	}

	public function wordpressGetBlock( $name ) {
		return new WordPressBlock\EntityWordPressBlock( $this->_application, $name );
	}

	public function csvGetExporterNames() {
		return array(
			'entity_id',
			'entity_slug',
			'entity_published',
			'entity_author',
			'entity_title',
			'entity_parent',
			'entity_reference',
			'entity_featured',
			'entity_activity',
			'entity_child_count',
			'entity_term_parent',
			'entity_terms',
			'entity_term_content_count',
		);
	}

	public function csvGetExporter( $name ) {
		switch ( $name ) {
			case 'entity_reference':
				return new CSVExporter\ReferenceCSVExporter( $this->_application, $name );
			default:
				return new CSVExporter\EntityCSVExporter( $this->_application, $name );
		}
	}

	public function csvGetImporterNames() {
		return array(
			'entity_id',
			'entity_slug',
			'entity_published',
			'entity_author',
			'entity_title',
			'entity_parent',
			'entity_reference',
			'entity_featured',
			'entity_activity',
			'entity_child_count',
			'entity_term_parent',
			'entity_terms',
			'entity_term_content_count',
		);
	}

	public function csvGetImporter( $name ) {
		switch ( $name ) {
			case 'entity_terms':
				return new CSVImporter\TermsCSVImporter( $this->_application, $name );
			case 'entity_reference':
				return new CSVImporter\ReferenceCSVImporter( $this->_application, $name );
			default:
				return new CSVImporter\EntityCSVImporter( $this->_application, $name );
		}
	}

	public function systemScheduledActions() {
		return array(
			'entity_unfeature_entities' => array(
				'title'       => __( 'Unfeature Entities', 'directories-pro' ),
				'description' => __( 'Unfeatures entities whose featured period has expired.', 'directories-pro' ),
				'recurrence'  => 'daily',
			),
			'entity_expire_entities'    => array(
				'title'       => __( 'Expire Entities', 'directories-pro' ),
				'description' => __( 'Expires entities whose expiration date has passed.', 'directories-pro' ),
				'recurrence'  => 'daily',
			),
		);
	}

	public function onEntityUnfeatureEntities() {
		foreach ( $this->_application->Entity_BundleTypes_byFeatures( array( 'featurable' ) ) as $entity_type => $bundle_types ) {
			$entities = $this->_application->Entity_Query( $entity_type )
				->fieldIsIn( 'bundle_type', $bundle_types )
				->fieldIsOrSmallerThan( 'entity_featured', time(), 'expires_at' )
				->fieldIsGreaterThan( 'entity_featured', 0, 'expires_at' ) // exclude those that never expire
				->fetch();
			foreach ( array_keys( $entities ) as $entity_id ) {
				$this->_application->Entity_Save( $entities[ $entity_id ], array( 'entity_featured' => false ) );
				$logs['success'][] = sprintf( __( 'Unfeatured item: %s', 'directories-pro' ), $entities[ $entity_id ]->getTitle() );
			}
			$this->_application->Action( 'entity_entities_unfeatured', array( $entity_type, $entities, &$logs ) );
		}
	}

	public function onEntityExpireEntities() {
		// Expire entities
		if ( ! $this->_application->Entity_Storage()->getFieldSchemaType( 'entity_expires' ) ) {
			return;
		}

		foreach ( $this->_application->Entity_BundleTypes_byFeatures( array( 'expirable' ) ) as $entity_type => $bundle_types ) {
			foreach ( $this->_application->Entity_Bundles_byType( $bundle_types ) as $bundle ) {
				if ( empty( $bundle->info['entity_expire'] ) ) {
					continue;
				}

				$expiration_field = 'entity_expires';
				if ( $custom_expiry_date_field = $this->hasCustomExpiryDateField( $bundle ) ) {
					$expiration_field = $custom_expiry_date_field->getFieldName();
				}
				$query = $this->_application->Entity_Query( $entity_type, $bundle->name )
					->fieldIs( 'status', $this->_application->Entity_Status( $bundle->entitytype_name, 'publish' ) )
					->fieldIsOrSmallerThan( $expiration_field, time() );
				if ( ! empty( $bundle->info['payment_enable'] )
					&& $this->_application->isComponentLoaded( 'Payment' )
				) {
					$query->fieldIsNull( 'payment_plan', 'plan_id' );
				}
				if ( ! $entities = $query->fetch() ) {
					continue;
				}

				$values                   = array();
				$status                   = $this->_application->Filter( 'entity_expired_status', isset( $bundle->info['entity_expire_status'] ) ? $bundle->info['entity_expire_status'] : 'trash', array( $bundle ) );
				$values['entity_expires'] = false;
				if ( is_array( $status ) ) {
					$values['status'] = $status[0];
					if ( isset( $status[1] ) ) {
						if ( $status[1] === true ) {
							if ( ! empty( $bundle->info['entity_expire_days'] ) ) {
								$values['entity_expires'] = time() + 86400 * $bundle->info['entity_expire_days'];
							}
						} elseif ( $status[1] !== false ) {
							if ( ( is_int( $status[1] ) && $status[1] > time() )
									|| ( $status[1] = $this->_application->System_Date_strToTime( $status[1] ) )
								) {
								$values['entity_expires'] = $status[1];
							}
						}
					}
				} else {
					$values['status'] = $this->_application->Entity_Status( $bundle->entitytype_name, $status );
				}
				if ( $expiration_field !== 'entity_expires' ) {
					$values[ $expiration_field ] = $values['entity_expires'];
				}
				foreach ( array_keys( $entities ) as $entity_id ) {
					$this->_application->Entity_Save( $entities[ $entity_id ], $values );
					$logs['success'][] = sprintf(
						__( 'Expired: %1$s (%2$s)', 'directories-pro' ),
						$entities[ $entity_id ]->getTitle(),
						$bundle->getGroupLabel() . ' - ' . $bundle->getLabel( 'singular' )
					);
				}

				// Expire referenced/referencing entities?
				if ( ! empty( $bundle->info['entity_expire_ref'] )
					&& ! empty( $bundle->info['entity_expire_ref_bundles'] )
				) {
					foreach ( $bundle->info['entity_expire_ref_bundles'] as $bundle_ref ) {
						if ( strpos( $bundle_ref, ',' ) ) {
							// Get referenced entities
							list($ref_bundle_name, $ref_field_name) = explode( ',', $bundle_ref );
							$ref_bundle                             = $this->_application->Entity_Bundle( $ref_bundle_name, '', true );
							$ref_entities                           = $this->_application->Entity_Query( $ref_bundle->entitytype_name )
								->fieldIs( 'bundle_name', $ref_bundle->name )
								->fieldIsIn( $ref_field_name, array_keys( $entities ) )
								->fetch();
						} else {
							// Get referencing entities
							$ref_field_name = $bundle_ref;
							$ref_entity_ids = array();
							foreach ( array_keys( $entities ) as $entity_id ) {
								foreach ( $entities[ $entity_id ]->getFieldValue( $ref_field_name ) as $ref_entity_id ) {
									$ref_entity_ids[] = is_object( $ref_entity_id ) ? $ref_entity_id->getId() : $ref_entity_id;
								}
							}
							if ( empty( $ref_entity_ids ) ) {
								continue;
							}

							if ( ! $ref_field = $this->_application->Entity_Field( $bundle, $bundle_ref ) ) {
								continue;
							}

							$ref_field_settings = $ref_field->getFieldSettings();
							if ( empty( $ref_field_settings['bundle'] ) ) {
								continue;
							}

							$ref_bundle   = $this->_application->Entity_Bundle( $ref_field_settings['bundle'], '', true );
							$ref_entities = $this->_application->Entity_Query( $ref_bundle->entitytype_name )
								->fieldIs( 'bundle_name', $ref_bundle->name )
								->fieldIsIn( 'id', $ref_entity_ids )
								->fetch();
						}
						if ( ! empty( $ref_entities ) ) {
							foreach ( array_keys( $ref_entities ) as $entity_id ) {
								$this->_application->Entity_Save( $ref_entities[ $entity_id ], $values );
								$logs['success'][] = sprintf(
									__( 'Expired: %1$s (%2$s)', 'directories-pro' ),
									$ref_entities[ $entity_id ]->getTitle(),
									$ref_bundle->getGroupLabel() . ' - ' . $ref_bundle->getLabel( 'singular' )
								);
							}
						}
					}
				}

				// Notify
				$this->_application->Action( 'entity_entities_expired', array( $bundle, $entities, $values, &$logs ) );
			}
		}
	}

	public function hasCustomExpiryDateField( Bundle $bundle ) {
		if ( ! empty( $bundle->info['entity_expire'] ) ) {
			if ( ! empty( $bundle->info['entity_expire_field'] ) ) {
				if ( $field = $this->_application->Entity_Field( $bundle, $bundle->info['entity_expire_field'] ) ) {
					return $field;
				}
				$this->_application->logError( sprintf( __( 'Expiry date field %s does not exist.', 'directories-pro' ), $bundle->info['entity_expire_field'] ) );
			}
		}
		return false;
	}
}
