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

use SabaiApps\Directories\Application;
use SabaiApps\Directories\Component\AbstractComponent;
use SabaiApps\Directories\Component\Entity\EntityComponent;
use SabaiApps\Directories\Component\Entity\Model\Bundle;
use SabaiApps\Directories\Component\Entity\Model\FieldConfig;
use SabaiApps\Directories\Component\Entity\Model\Field;
use SabaiApps\Directories\Exception;

class AdminHelper {

	public function createEntityTypes( Application $application, AbstractComponent $component, array $entityTypes ) {
		$model        = $application->getModel( null, 'Entity' );
		$entity_types = array();
		foreach ( $entityTypes as $name ) {
			$info = $component->entityGetType( $name )->entityTypeInfo();
			// Create entity type specific fields if any
			if ( ! empty( $info['properties'] ) ) {
				foreach ( $info['properties'] as $property_name => $property_info ) {
					if ( empty( $property_info ) ) {
						continue;
					}

					$this->_createEntityPropertyFieldConfig( $application, $name, $property_name, $property_info );
				}
			}
			$entity_types[] = $name;
		}
		$model->commit();

		$application->Action( 'entity_create_entity_types_success', array( $entity_types ) );
	}

	public function updateEntityTypes( Application $application, AbstractComponent $component, $entityTypes ) {
		$new_fields_by_entity_type = array();
		foreach ( $entityTypes as $entity_type ) {
			$info = $component->entityGetType( $entity_type )->entityTypeInfo();
			// Update property fields
			$current_fields = $application->getModel( 'FieldConfig', 'Entity' )
				->entitytypeName_is( $entity_type )
				->property_isNot( '' )
				->fetch();
			if ( ! empty( $info['properties'] ) ) {
				// Check for re-named properties
				$was = array();
				foreach ( $info['properties'] as $property => $property_info ) {
					if ( isset( $property_info['was'] ) ) {
						$was[ $property_info['was'] ] = $property;
					}
				}
				$fields_already_installed = array();
				foreach ( $current_fields as $current_field ) {
					if ( empty( $info['properties'][ $current_field->property ] ) ) {
						if ( isset( $was[ $current_field->property ] ) ) {
							// Rename
							$current_field->property = $was[ $current_field->property ];
							$current_field->name     = $entity_type . '_' . $current_field->property;
						} else {
							$current_field->markRemoved();
							continue;
						}
					}
					if ( isset( $info['properties'][ $current_field->property ]['settings'] ) ) {
						$current_field->settings = (array) $current_field->settings + (array) $info['properties'][ $current_field->property ]['settings'];
					}
					if ( $current_field->type !== $info['properties'][ $current_field->property ]['type'] ) {
						$current_field->type = $info['properties'][ $current_field->property ]['type'];
					}
					$fields_already_installed[] = $current_field->property;
				}
				// Create newly added fields
				foreach ( array_diff( array_keys( $info['properties'] ), $fields_already_installed ) as $property_name ) {
					if ( empty( $info['properties'][ $property_name ] ) ) {
						continue;
					}

					if ( $new_field = $this->_createEntityPropertyFieldConfig( $application, $entity_type, $property_name, $info['properties'][ $property_name ] ) ) {
						$new_fields_by_entity_type[ $entity_type ][ $new_field->name ] = array( $new_field, $info['properties'][ $property_name ] );
					}
				}
			} else {
				foreach ( $current_fields as $current_field ) {
					$current_field->markRemoved();
				}
			}
		}
		$application->getModel( null, 'Entity' )->commit();

		$application->Action( 'entity_update_entity_types_success', array( $entityTypes ) );

		// Add new entity type fields to current active bundles
		if ( ! empty( $new_fields_by_entity_type ) ) {
			$application->Field_Types( false ); // reload field types
			foreach ( $new_fields_by_entity_type as $entity_type => $new_entity_type_fields ) {
				foreach ( $application->getModel( 'Bundle', 'Entity' )->entitytypeName_is( $entity_type )->fetch() as $bundle ) {
					foreach ( $new_entity_type_fields as $field_name => $field_data ) {
						$this->createEntityPropertyField( $application, $bundle, $field_data[0], $field_data[1] );
					}
				}
			}
			$application->getModel( null, 'Entity' )->commit();
		}
	}

	public function deleteEntityTypes( Application $application, AbstractComponent $component, array $entityTypes ) {
		foreach ( $entityTypes as $entity_type ) {
			if ( $delete_bundles = $application->getModel( 'Bundle', 'Entity' )->entitytypeName_is( $entity_type )->fetch()->getArray() ) {
				$this->_deleteEntityBundles( $application, $delete_bundles );
			}
			$application->Entity_Field_cleanCache();
		}
		$application->getModel( null, 'Entity' )->commit();
		$application->Action( 'entity_delete_entity_types_success', array( $entityTypes ) );
	}

	private function _createEntityPropertyFieldConfig( Application $application, $entityType, $propertyName, array $propertyInfo ) {
		if ( isset( $propertyInfo['field_name'] ) ) {
			// Not a real property but an alias to a field

			if ( ! isset( $propertyInfo['settings'] ) ) {
				return;
			}

			$field_config = $this->_createEntityFieldConfig(
				$application,
				$propertyInfo['field_name'],
				array(
					'type'     => $propertyInfo['type'],
					'settings' => $propertyInfo['settings'],
				),
				$entityType,
				EntityComponent::FIELD_REALM_ENTITY_TYPE_DEFAULT
			);

			$this->createFieldStorage( $application, array( $field_config ) );
		} else {
			$field_config = $application->getModel( null, 'Entity' )
				->create( 'FieldConfig' )
				->markNew()
				->set( 'name', $entityType . '_' . $propertyName )
				->set( 'type', $propertyInfo['type'] )
				->set( 'system', EntityComponent::FIELD_REALM_ENTITY_TYPE_DEFAULT )
				->set( 'entitytype_name', $entityType )
				->set( 'settings', isset( $propertyInfo['settings'] ) ? (array) $propertyInfo['settings'] : array() );
		}
		$field_config->set( 'property', $propertyName );

		return $field_config;
	}

	public function createEntityBundles( Application $application, array $bundles, $group = '' ) {
		return $this->_createEntityBundles(
			$application,
			$application->Filter( 'entity_bundles_info', $bundles, array( $group ) ),
			$group
		);
	}

	protected function _createEntityBundles( Application $application, array $bundles, $group, ?array &$newFields = null ) {
		if ( ! isset( $newFields ) ) {
			$newFields = array();
		}
		$created_bundles = array();
		foreach ( array_keys( $bundles ) as $bundle_type ) {
			foreach ( array( 'fields', 'displays', 'views' ) as $key ) {
				if ( empty( $bundles[ $bundle_type ][ $key ] ) ) {
					continue;
				}

				try {
					$bundles[ $bundle_type ][ $key ] = $this->_maybeGetSettingsFromFile( $bundles[ $bundle_type ][ $key ] );
				} catch ( Exception\RuntimeException $e ) {
					$application->logError( 'Skipping ' . $key . ' for bundle ' . $bundle_type . ': ' . $e );
					unset( $bundles[ $bundle_type ][ $key ] );
				}
			}
			$bundles[ $bundle_type ] = $application->Filter( 'entity_bundle_info', $bundles[ $bundle_type ], array( $group ) );
			if ( $bundle = $this->_createEntityBundle( $application, $bundle_type, $bundles[ $bundle_type ], $group, $newFields ) ) {
				$created_bundles[ $bundle->name ] = $bundle;
				BundleHelper::add( $bundle );
			}
		}

		if ( ! empty( $created_bundles ) ) {
			// Clear bundles cache
			$application->getPlatform()->deleteCache( 'entity_bundle_types' );

			// Update fields
			if ( ! empty( $newFields ) ) {
				$this->createFieldStorage( $application, $newFields );
			}

			$application->Action( 'entity_create_bundles_success', array( $created_bundles, $bundles ) );

			$application->getModel( null, 'Entity' )->commit();

			$application->Action( 'entity_create_bundles_committed', array( $created_bundles, $bundles ) );

			// Remove cache from Entity_Bundle helper
			foreach ( array_keys( $created_bundles ) as $bundle_name ) {
				BundleHelper::remove( $bundle_name );
			}
		}

		return $created_bundles;
	}

	public function updateEntityBundles( Application $application, array $bundles, $group = '' ) {
		$bundles           = $application->Filter( 'entity_bundles_info', $bundles, array( $group ) );
		$bundles_to_update = $bundles_info_to_update = $bundles_to_delete = array();
		foreach ( $application->getModel( 'Bundle', 'Entity' )->group_is( $group )->fetch() as $bundle ) {
			if ( isset( $bundles[ $bundle->type ] ) ) {
				$bundles_to_update[ $bundle->name ]      = $bundle;
				$bundles_info_to_update[ $bundle->type ] = $bundles[ $bundle->type ];
				unset( $bundles[ $bundle->type ] );
			} else {
				$bundles_to_delete[ $bundle->name ] = $bundle;
			}
		}
		// Delete old bundles
		if ( ! empty( $bundles_to_delete ) ) {
			$this->_deleteEntityBundles( $application, $bundles_to_delete );
		}
		// Create new bundles
		if ( ! empty( $bundles ) ) {
			$this->_createEntityBundles( $application, $bundles, $group );
		}
		// Update current bundles
		if ( ! empty( $bundles_to_update ) ) {
			$this->_updateEntityBundles( $application, $bundles_to_update, $bundles_info_to_update );
		}
	}

	protected function _updateEntityBundles( Application $application, array $bundles, array $bundleInfo ) {
		$newFields       = $updatedFields = array();
		$current_bundles = array();
		foreach ( $bundles as $bundle ) {
			foreach ( array( 'fields', 'displays', 'views' ) as $key ) {
				if ( empty( $bundleInfo[ $bundle->type ][ $key ] ) ) {
					continue;
				}

				try {
					$bundleInfo[ $bundle->type ][ $key ] = $this->_maybeGetSettingsFromFile( $bundleInfo[ $bundle->type ][ $key ] );
				} catch ( Exception\RuntimeException $e ) {
					$application->logError( 'Skipping ' . $key . ' for bundle ' . $bundle->type . ': ' . $e );
					unset( $bundleInfo[ $bundle->type ][ $key ] );
				}
			}
			$bundleInfo[ $bundle->type ]      = $application->Filter( 'entity_bundle_info', $bundleInfo[ $bundle->type ], array( $bundle->group ) );
			$current_bundles[ $bundle->name ] = $this->_updateEntityBundle( $application, $bundle, $bundleInfo[ $bundle->type ], $newFields, $updatedFields );
			BundleHelper::add( $current_bundles[ $bundle->name ] );
		}

		// Clear bundles cache
		$application->getPlatform()->deleteCache( 'entity_bundle_types' );

		// Update fields
		if ( ! empty( $newFields ) ) {
			$this->createFieldStorage( $application, $newFields );
		}
		if ( ! empty( $updatedFields ) ) {
			$this->updateFieldStorage( $application, $updatedFields );
		}

		$application->Action( 'entity_update_bundles_success', array( $current_bundles, $bundleInfo ) );

		$application->getModel( null, 'Entity' )->commit();

		$application->Action( 'entity_update_bundles_committed', array( $current_bundles, $bundleInfo ) );

		// Remove cache from Entity_Bundle helper
		foreach ( $current_bundles as $bundle ) {
			BundleHelper::remove( $bundle->name );

			// Also remove sort options cache
			$application->Entity_Sorts_clearCache( $bundle->name );
		}
	}

	public function deleteEntityBundles( Application $application, ?array $bundles = null, $group = '', $deleteContent = false ) {
		if ( empty( $bundles ) ) {
			if ( ! strlen( $group ) ) {
				return;
			}
			$bundles = array();
			foreach ( $application->getModel( 'Bundle', 'Entity' )->group_is( $group )->fetch() as $bundle ) {
				$bundles[ $bundle->name ] = $bundle;
			}
		}
		if ( ! empty( $bundles ) ) {
			$this->_deleteEntityBundles( $application, $bundles, $deleteContent );
		}
	}

	protected function _deleteEntityBundles( Application $application, array $bundles, $deleteContent = false ) {
		$removed_field_names = $field_configs = array();
		foreach ( $bundles as $bundle ) {
			// Cache bundle info for 7 days just in case
			$application->getPlatform()->setCache( $bundle->info, 'entity_deleted_bundle_info_' . $bundle->name, 604800 );

			$bundle->markRemoved();
			foreach ( $bundle->Fields->with( 'FieldConfig' ) as $field ) {
				// No need to call markRemoved for fields since they are removed when bundle is deleted

				if ( ! $field->isPropertyField() ) {
					// Save removed field name for later use
					$removed_field_names[] = $field->getFieldName();

					$field_configs[ $field->FieldConfig->name ] = $field->FieldConfig;
				}
			}
		}

		// Clear bundles cache
		$application->getPlatform()->deleteCache( 'entity_bundle_types' );

		if ( $deleteContent ) {
			// Delete field data of deleted bundles
			$application->Entity_Storage()->purgeValuesByBundle( array_keys( $bundles ), $removed_field_names );
		}

		$application->Action( 'entity_delete_bundles_success', array( $bundles, $deleteContent ) );

		$application->getModel( null, 'Entity' )->commit();

		// Delete field configs if no fields
		foreach ( $field_configs as $field_config ) {
			if ( ! count( $field_config->Fields ) ) {
				$field_config->markRemoved()->commit();
				$application->getComponent( 'Entity' )->deleteFieldStorage( array( $field_config ) );
			}
		}

		$application->Action( 'entity_delete_bundles_committed', array( $bundles, $deleteContent ) );
	}

	private function _createEntityBundle( Application $application, $bundleType, array $info, $group, array &$newFields ) {
		if ( ! $bundle_type_info = $application->Entity_BundleTypeInfo( $bundleType, null, false ) ) {
			$application->logError( 'Could not fetch info for bundle type: ' . $bundleType );
			return;
		}

		if ( isset( $info['name'] ) ) {
			$bundle_name = $info['name'];
		} elseif ( isset( $info['suffix'] ) ) {
			if ( $bundle_type_info['entity_type'] === 'post' ) {
				if ( strlen( $info['suffix'] ) > 7 ) {
					return; // WP limits post type name to 20 chars
				}
			} elseif ( $bundle_type_info['entity_type'] === 'term' ) {
				if ( strlen( $info['suffix'] ) > 19 ) {
					return; // WP limits taxonomy name to 32 chars
				}
			} elseif ( strlen( $info['suffix'] ) > 27 ) {
				return; // Bundle table limits bundle name to 40 chars

			}
			$bundle_name = $group . '_' . $info['suffix'];
		} else {
			$application->logError( 'No bundle name specified for bundle: ' . $bundleType . ' ' . serialize( $info ) );
			return;
		}

		// Get the model
		$model = $application->getModel( null, 'Entity' );

		// Make sure bundle with same name does not exist
		if ( $model->Bundle->name_is( $bundle_name )->count() ) {
			return;
		}

		// Restore previous bundle info if any
		if ( $_info = $application->getPlatform()->getCache( 'entity_deleted_bundle_info_' . $bundle_name ) ) {
			$info = $_info + $info;
		}

		$bundle = $model->create( 'Bundle' )
			->markNew()
			->set( 'entitytype_name', $bundle_type_info['entity_type'] )
			->set( 'name', $bundle_name )
			->set( 'type', $bundleType )
			->set( 'component', $bundle_type_info['component'] === 'Fieldset' ? 'Fieldset' : 'Directory' )
			->set( 'group', $group )
			->setInfo( $info );

		// Create entity type property fields
		$this->_assignEntityPropertyFields( $application, $bundle, $info );

		// Add extra fields associated with the bundle if any
		if ( ! empty( $info['fields'] )
			&& is_array( $info['fields'] )
		) {
			foreach ( array_keys( $info['fields'] ) as $field_name ) {
				$field_info = $info['fields'][ $field_name ];
				if ( ! isset( $field_info['realm'] ) ) {
					$field_info['realm'] = strpos( $field_name, 'field_' ) === 0 ? EntityComponent::FIELD_REALM_ALL : EntityComponent::FIELD_REALM_BUNDLE_DEFAULT;
				}
				if ( $field = $this->createEntityField( $application, $bundle, $field_name, $field_info ) ) {
					if ( ! $field->FieldConfig->id ) {
						$newFields[ $field->getFieldName() ] = $field->FieldConfig;
					}
				}
			}
		}

		$application->getModel( null, 'Entity' )->commit();

		return $bundle->reload();
	}

	protected function _maybeGetSettingsFromFile( $settings ) {
		if ( is_string( $settings ) ) {
			if ( ! file_exists( $settings )
				|| ( ! $settings = include $settings )
			) {
				throw new Exception\RuntimeException( 'Invalid file.' );
			}
		}
		if ( ! is_array( $settings ) ) {
			throw new Exception\RuntimeException( 'Invalid settings.' );
		}
		return $settings;
	}

	private function _updateEntityBundle( Application $application, Bundle $bundle, array $info, array &$newFields, array &$updatedFields ) {
		$bundle->setInfo( $info, false );
		// Update entity type property fields
		$this->_assignEntityPropertyFields( $application, $bundle, $info );
		// Fetch bundle specific fields
		$current_bundle_fields = $application->getModel( 'Field', 'Entity' )
			->bundleName_is( $bundle->name )
			->fetch()
			->with( 'FieldConfig' );
		if ( ! empty( $info['fields'] )
			&& is_array( $info['fields'] )
		) {
			$_current_bundle_fields = array();
			foreach ( $current_bundle_fields as $current_field ) {
				if ( $current_field->FieldConfig->system !== EntityComponent::FIELD_REALM_BUNDLE_DEFAULT ) {
					continue;
				}

				$field_name = $current_field->fieldconfig_name;
				if ( ! isset( $info['fields'][ $field_name ] ) ) {
					$current_field->markRemoved();

					continue;
				}
				$field_info = $info['fields'][ $field_name ];
				if ( ! isset( $field_info['realm'] ) ) {
					$field_info['realm'] = strpos( $field_name, 'field_' ) === 0 ? EntityComponent::FIELD_REALM_ALL : EntityComponent::FIELD_REALM_BUNDLE_DEFAULT;
				}
				// Create or update the field.
				$this->createEntityField( $application, $bundle, $current_field->FieldConfig, $field_info, false, $updatedFields );
				$_current_bundle_fields[] = $field_name;
			}
			// Create or update other fields
			foreach ( array_diff( array_keys( $info['fields'] ), $_current_bundle_fields ) as $field_name ) {
				$field_info = $info['fields'][ $field_name ];
				if ( ! isset( $field_info['realm'] ) ) {
					$field_info['realm'] = strpos( $field_name, 'field_' ) === 0 ? EntityComponent::FIELD_REALM_ALL : EntityComponent::FIELD_REALM_BUNDLE_DEFAULT;
				}
				// Create if the field does not exist and it is a bundle specific field. If the field exists, update.
				if ( $field = $this->createEntityField( $application, $bundle, $field_name, $field_info, false, $updatedFields, $field_info['realm'] !== EntityComponent::FIELD_REALM_ALL ) ) {
					if ( ! $field->FieldConfig->id ) {
						$newFields[ $field->FieldConfig->name ] = $field->FieldConfig;
					}
				}
			}
		} elseif ( isset( $info['fields'] )
				&& $info['fields'] !== false
			) {

			foreach ( $current_bundle_fields as $current_field ) {
				if ( $current_field->FieldConfig->system !== EntityComponent::FIELD_REALM_BUNDLE_DEFAULT ) {
					continue;
				}

				$current_field->markRemoved();
			}
		}

		$application->getModel( null, 'Entity' )->commit();

		return $bundle->reload();
	}

	public function createEntityPropertyField( Application $application, Bundle $bundle, FieldConfig $fieldConfig, array $fieldInfo, $overwrite = false ) {
		if ( ! $field_type_info = $this->_isValidFieldInfo( $application, $bundle, $fieldInfo ) ) {
			return;
		}

		// Fetch field
		$field = null;
		foreach ( $fieldConfig->Fields as $_field ) {
			if ( $_field->bundle_name === $bundle->name ) {
				$field = $_field;
				break;
			}
		}
		if ( ! $field ) {
			// Create field
			$field = $bundle->createField()->markNew()->set( 'FieldConfig', $fieldConfig );
		}

		$fieldInfo['max_num_items'] = 1;
		return $this->_updateEntityField( $application, $field, $fieldInfo, $field_type_info, $overwrite );
	}

	public function createEntityField( Application $application, Bundle $bundle, $fieldName, array $fieldInfo, $overwrite = false, array &$updatedFields = array(), $create = true ) {
		if ( ! $field_type_info = $this->_isValidFieldInfo( $application, $bundle, $fieldInfo ) ) {
			return;
		}

		if ( ! $field_config = $this->_createEntityFieldConfig( $application, $fieldName, $fieldInfo, $bundle, null, $overwrite, $create, $updatedFields ) ) {
			return;
		}

		if ( $field_config->id ) {
			foreach ( $field_config->Fields as $_field ) {
				if ( $_field->bundle_name === $bundle->name ) {
					$field = $_field;
					break;
				}
			}
		}
		if ( ! isset( $field ) ) {
			if ( ! $create ) {
				return;
			}

			$field = $bundle->createField()->markNew();
		}
		$field->FieldConfig = $field_config;

		return $this->_updateEntityField( $application, $field, $fieldInfo, $field_type_info, $overwrite );
	}

	private function _isValidFieldInfo( Application $application, Bundle $bundle, array $fieldInfo ) {
		if ( ! isset( $fieldInfo['type'] ) ) {
			return false;
		}

		$field_types = $application->Field_Types();
		if ( ! $field_type_info = @$field_types[ $fieldInfo['type'] ] ) {
			return false;
		}

		if ( isset( $field_type_info['entity_types'] )
			&& ! in_array( $bundle->entitytype_name, $field_type_info['entity_types'] )
		) {
			// the field type does not support the entity type of the bundle
			return false;
		}

		return $field_type_info;
	}

	private function _updateEntityField( Application $application, Field $field, array $fieldInfo, array $fieldTypeInfo, $overwrite ) {
		$admin_only = ( true === $application->Field_Type( $fieldInfo['type'] )->fieldTypeInfo( 'admin_only' ) );

		// Set custom field data
		if ( ! empty( $fieldInfo['data'] ) ) {
			foreach ( array_keys( $fieldInfo['data'] ) as $data_k ) {
				if ( strpos( $data_k, '_' ) === 0 ) { // custom field data name must start with underscore
					$field->setFieldData( $data_k, $fieldInfo['data'][ $data_k ] );
				} elseif ( ! isset( $fieldInfo[ $data_k ] ) ) {
						$fieldInfo[ $data_k ] = $fieldInfo['data'][ $data_k ];
				}
			}
		}
		if ( $overwrite
			|| ! $field->id
			|| $admin_only
		) {
			$widget = isset( $fieldInfo['widget'] ) ? $fieldInfo['widget'] : $fieldTypeInfo['default_widget'];
			if ( ! isset( $fieldInfo['max_num_items'] ) ) {
				$fieldInfo['max_num_items'] = $widget && $application->Field_Widgets_impl( $widget )->fieldWidgetInfo( 'accept_multiple' ) ? 0 : 1;
			}
			if ( ! isset( $fieldInfo['widget_settings'] ) ) {
				$fieldInfo['widget_settings'] = $widget && ( $defaults = $application->Field_Widgets_impl( $widget )->fieldWidgetInfo( 'default_settings' ) ) ? $defaults : array();
			}
			$hide_label = false;
			if ( isset( $fieldInfo['label'] ) ) {
				if ( is_array( $fieldInfo['label'] ) ) {
					$label      = $fieldInfo['label'][0];
					$hide_label = ! empty( $fieldInfo['label'][1] );
				} else {
					$label = $fieldInfo['label'];
				}
			} else {
				$label = $fieldTypeInfo['label'];
			}
			$field->setFieldLabel( $label, $hide_label )
				->setFieldDescription( $description = isset( $fieldInfo['description'] ) ? $fieldInfo['description'] : '' )
				->setFieldDefaultValue( @$fieldInfo['default_value'] )
				->setFieldRequired( ! empty( $fieldInfo['required'] ), ! empty( $fieldInfo['admin_unrequired'] ) )
				->setFieldDisabled( ! empty( $fieldInfo['disabled'] ) )
				->setFieldWeight( (int) @$fieldInfo['weight'] )
				->setFieldMaxNumItems( $fieldInfo['max_num_items'] )
				->setFieldWidget( $widget )
				->setFieldWidgetSettings( $fieldInfo['widget_settings'] )
				->setFieldConditions( empty( $fieldInfo['conditions'] ) ? array() : $fieldInfo['conditions'] );
			// Register label/description for translation
			$application->getPlatform()->registerString( $label, $field->bundle_name . '_' . $field->getFieldName() . '_label', 'entity_field' )
				->registerString( $description, $field->bundle_name . '_' . $field->getFieldName() . '_description', 'entity_field' );
		} else {
			// Set field widget if there isn't any set
			if ( ! $field->getFieldWidget()
				&& ( $widget = isset( $fieldInfo['widget'] ) ? $fieldInfo['widget'] : $fieldTypeInfo['default_widget'] )
			) {
				if ( ! isset( $fieldInfo['widget_settings'] ) ) {
					$widget_settings = ( $defaults = $application->Field_Widgets_impl( $widget )->fieldWidgetInfo( 'default_settings' ) ) ? $defaults : array();
				} else {
					$widget_settings = $fieldInfo['widget_settings'];
				}
				$field->setFieldWidget( $widget )->setFieldWidgetSettings( $widget_settings );
				if ( ! $application->Field_Widgets_impl( $widget )->fieldWidgetInfo( 'accept_multiple' ) ) {
					$field->setFieldMaxNumItems( 1 );
				}
			}
		}

		return $field;
	}

	protected function _createEntityFieldConfig( Application $application, $fieldName, array $fieldInfo, $bundleOrEntityType, $realm = null, $overwrite = false, $create = true, ?array &$updatedFields = null ) {
		$field_type_impl = $application->Field_Type( $fieldInfo['type'] );
		$field_settings  = isset( $fieldInfo['settings'] ) ? $fieldInfo['settings'] : array();
		$field_settings += (array) $field_type_impl->fieldTypeInfo( 'default_settings' );
		if ( $bundleOrEntityType instanceof Bundle ) { // @todo should we allow passing entity type name as well?
			$field_settings = $field_type_impl->fieldTypeInitialSettings( $fieldName, $bundleOrEntityType, $field_settings );
		}
		if ( ! is_object( $fieldName ) ) {
			$fieldName = strtolower( trim( $fieldName ) );
			if ( strlen( $fieldName ) === 0 ) {
				return;
			}

			if ( ! $field_config = $application->getModel( 'FieldConfig', 'Entity' )->name_is( $fieldName )->fetchOne() ) {
				if ( ! $create ) {
					return;
				}

				if ( ! isset( $realm ) ) {
					$realm = isset( $fieldInfo['realm'] ) ? $fieldInfo['realm'] : EntityComponent::FIELD_REALM_ALL;
				}
				$field_config = $application->getModel( null, 'Entity' )
					->create( 'FieldConfig' )
					->markNew()
					->set( 'name', $fieldName )
					->set( 'system', $realm )
					->set( 'settings', $field_settings );
				if ( $realm !== EntityComponent::FIELD_REALM_ALL
					&& isset( $bundleOrEntityType )
				) {
					if ( $bundleOrEntityType instanceof Bundle ) {
						$field_config->set( 'bundle_type', $bundleOrEntityType->type )->set( 'entitytype_name', $bundleOrEntityType->entitytype_name );
					} else {
						$field_config->set( 'entitytype_name', $bundleOrEntityType );
					}
				}
			} else {
				$is_update = true;
			}
		} else {
			$is_update    = true;
			$field_config = $fieldName;
		}
		$field_schema = (array) $field_type_impl->fieldTypeSchema( $field_config->settings );
		if ( ! empty( $is_update ) ) {
			if ( $overwrite || $field_type_impl->fieldTypeInfo( 'admin_only' ) ) {
				$field_config->settings = $field_settings;
			} else {
				$field_config->settings += $field_settings;
			}
			// if ($field_config->schema !== $field_schema) {
			// Notify that field schema has changed
			if ( isset( $updatedFields ) ) {
				$updatedFields[ $field_config->name ] = $field_config;
			}
			// }
		}
		$field_config->schema      = $field_schema;
		$field_config->type        = $fieldInfo['type'];
		$field_config->schema_type = $field_type_impl->fieldTypeSchemaType( $field_config->type, $field_config->name );

		return $field_config;
	}

	private function _assignEntityPropertyFields( Application $application, Bundle $bundle, array $info ) {
		$property_fields = $application->getModel( 'FieldConfig', 'Entity' )->entitytypeName_is( $bundle->entitytype_name )->bundleType_is( '' )->fetch()->with( 'Fields' );
		if ( count( $property_fields ) ) {
			$entity_type_info = $application->Entity_Types_impl( $bundle->entitytype_name, false )->entityTypeInfo();
			foreach ( $property_fields as $property_field ) {
				if ( ! isset( $entity_type_info['properties'][ $property_field->property ] ) ) {
					// Delete stale data
					$property_field->markRemoved()->commit();
					continue;
				}
				$property_field_settings = $entity_type_info['properties'][ $property_field->property ];
				// Each bundle can set custom field settings but not overwrite the default
				if ( ! empty( $info['properties'][ $property_field->property ] ) ) {
					$property_field_settings += $info['properties'][ $property_field->property ];
					// Except for label/weight which can be overwritten
					foreach ( array( 'label', 'weight' ) as $property_key ) {
						if ( isset( $info['properties'][ $property_field->property ][ $property_key ] ) ) {
							$property_field_settings[ $property_key ] = $info['properties'][ $property_field->property ][ $property_key ];
						}
					}
				}
				$this->createEntityPropertyField( $application, $bundle, $property_field, $property_field_settings );
			}
		}
	}

	public function createFieldStorage( Application $application, array $fieldConfigs ) {
		$fields = array();
		foreach ( $fieldConfigs as $field_config ) {
			$fields[ $field_config->name ] = $field_config;
		}
		$application->Entity_Storage()->create( $fields );
	}

	public function updateFieldStorage( Application $application, array $fieldConfigs ) {
		$fields = array();
		foreach ( $fieldConfigs as $field_config ) {
			$fields[ $field_config->name ] = $field_config;
		}
		$application->Entity_Storage()->update( $fields );
	}
}
