<?php

namespace Pods;

use Closure;
use Exception;
use Pods\Data\Conditional_Logic;
use Pods\Whatsit\Field;
use Pods\Whatsit\Group;
use Pods\Whatsit\Object_Field;
use Pods\Whatsit\Store;

/**
 * Whatsit abstract class.
 *
 * Why did we name this "Whatsit"? Because we wanted "Object" but that's a reserved PHP keyword,
 * and we couldn't think of a better name. We said "let's just use Whatsit for now" and then we
 * never changed it. Enjoy!
 *
 * @method string      get_object_type()
 * @method string|null get_object_storage_type()
 * @method string|null get_name()
 * @method string|null get_id()
 * @method string|null get_parent()
 * @method string|null get_group()
 * @method string|null get_type()
 * @method string|null get_parent_identifier()
 * @method string|null get_parent_object_type()
 * @method string|null get_parent_object_storage_type()
 * @method string|null get_parent_name()
 * @method string|null get_parent_id()
 * @method string|null get_parent_label()
 * @method string|null get_parent_description()
 * @method string|null get_parent_type()
 * @method string|null get_group_identifier()
 * @method string|null get_group_object_type()
 * @method string|null get_group_object_storage_type()
 * @method string|null get_group_name()
 * @method string|null get_group_id()
 * @method string|null get_group_label()
 * @method string|null get_group_description()
 * @method string|null get_group_type()
 *
 * @since 2.8.0
 */
abstract class Whatsit implements \ArrayAccess, \JsonSerializable, \Iterator {

	/**
	 * @var int
	 */
	private $position = 0;

	/**
	 * @var string
	 */
	protected static $type = 'object';

	/**
	 * @var array
	 * @noinspection PropertyCanBeStaticInspection
	 */
	protected $args = [
		'object_type'         => '',
		'object_storage_type' => 'collection',
		'name'                => '',
		'id'                  => '',
		'parent'              => '',
		'group'               => '',
		'label'               => '',
		'description'         => '',
	];

	/**
	 * @var Field[]|null
	 */
	protected $_fields;

	/**
	 * @var Object_Field[]|null
	 */
	protected $_object_fields;

	/**
	 * @var Group[]|null
	 */
	protected $_groups;

	/**
	 * @var array|null
	 */
	protected $_table_info;

	/**
	 * Whatsit constructor.
	 *
	 * @param array     $args        {
	 *                               Object arguments.
	 *
	 * @type string     $name        Object name.
	 * @type string|int $id          Object ID.
	 * @type string     $label       Object label.
	 * @type string     $description Object description.
	 * @type string|int $parent      Object parent name or ID.
	 * @type string|int $group       Object group name or ID.
	 * }
	 * @todo Define storage per Whatsit.
	 *
	 */
	public function __construct( array $args = [] ) {
		$this->args['object_type'] = static::$type;

		// Setup the object.
		$this->setup( $args );
	}

	/**
	 * Setup object from a serialized string.
	 *
	 * @param string  $serialized Serialized representation of the object.
	 * @param boolean $to_args    Return as arguments array.
	 *
	 * @return Whatsit|array|null
	 */
	public static function from_serialized( $serialized, $to_args = false ) {
		$object = maybe_unserialize( $serialized );

		if ( $object instanceof self ) {
			if ( $to_args ) {
				return $object->get_args();
			}

			return $object;
		}

		if ( is_array( $object ) ) {
			$called_class = get_called_class();

			/** @var Whatsit $object */
			$object = new $called_class( $object );

			if ( $to_args ) {
				return $object->get_args();
			}

			return $object;
		}

		return null;
	}

	/**
	 * Setup object from a JSON string.
	 *
	 * @param string  $json    JSON representation of the object.
	 * @param boolean $to_args Return as arguments array.
	 *
	 * @return Whatsit|array|null
	 */
	public static function from_json( $json, $to_args = false ) {
		/** @noinspection PhpUsageOfSilenceOperatorInspection */
		$args = @json_decode( $json, true );

		if ( is_array( $args ) ) {
			if ( ! empty( $args['id'] ) ) {
				// Check if we already have an object registered and available.
				$store = Store::get_instance();

				// Attempt to get from storage directly.
				$object = $store->get_object_from_storage( isset( $args['object_storage_type'] ) ? $args['object_storage_type'] : null, $args['id'] );

				if ( $object ) {
					if ( $to_args ) {
						return $object->get_args();
					}

					return $object;
				}
			}

			$called_class = get_called_class();

			/** @var Whatsit $object */
			$object = new $called_class( $args );

			if ( $to_args ) {
				return $object->get_args();
			}

			return $object;
		}//end if

		return null;
	}

	/**
	 * Setup object from an array configuration.
	 *
	 * @param array   $array   Array configuration.
	 * @param boolean $to_args Return as arguments array.
	 *
	 * @return Whatsit|array|null
	 */
	public static function from_array( array $array, $to_args = false ) {
		if ( ! empty( $array['id'] ) ) {
			// Check if we already have an object registered and available.
			$store = Store::get_instance();

			// Attempt to get from storage directly.
			$object = $store->get_object_from_storage( isset( $args['object_storage_type'] ) ? $args['object_storage_type'] : null, $array['id'] );

			if ( $object ) {
				if ( $to_args ) {
					return $object->get_args();
				}

				return $object;
			}
		}

		$called_class = get_called_class();

		/** @var Whatsit $object */
		$object = new $called_class( $array );

		if ( $to_args ) {
			return $object->get_args();
		}

		return $object;
	}

	/**
	 * Override var_dump data that is used for debugging with.
	 *
	 * @return array Data for debugging with.
	 */
	public function __debugInfo(): array {
		return [
			'args' => $this->args,
		];
	}

	/**
	 * Override __set_state handling to limit it to passing in only $args.
	 *
	 * @return self Object with state set.
	 */
	#[\ReturnTypeWillChange]
	public static function __set_state( $data ) {
		$args = [];

		if ( ! empty( $data['args'] ) ) {
			$args = $data['args'];
		}

		return new static( $args );
	}

	/**
	 * On serialization of this object, only include _args.
	 *
	 * @return array List of properties to serialize.
	 */
	public function __sleep(): array {
		// @todo If DB based config, return only name, id, parent, group
		// @todo Maybe set up a variable with the custom array and implement Serializable::serialize/unserialize
		/*
		$this->args = array(
			'object_type' => $this->args['object_type'],
			'name'        => $this->args['name'],
			'id'          => $this->args['id'],
			'parent'      => $this->args['parent'],
			'group'       => $this->args['group'],
		);
		*/

		// Handle closures that cannot be serialized.
		if ( isset( $this->args['data'] ) && $this->args['data'] instanceof Closure ) {
			$this->args['data'] = $this->args['data']();
		}

		return [
			'args',
		];
	}

	/**
	 * On unserialization of this object, setup the object.
	 */
	public function __wakeup(): void {
		// Setup the object.
		$this->setup();
	}

	/**
	 * Handle JSON encoding for object.
	 *
	 * @return array Object arguments.
	 */
	#[\ReturnTypeWillChange]
	public function jsonSerialize() {
		$args = $this->get_args();

		// Handle closures that cannot be serialized.
		if ( isset( $args['data'] ) && $args['data'] instanceof Closure ) {
			$args['data'] = $args['data']();
		}

		return $args;
	}

	/**
	 * {@inheritdoc}
	 */
	public function getArrayCopy(): array {
		return array_values( $this->args );
	}

	/**
	 * {@inheritdoc}
	 */
	public function rewind(): void {
		$this->position = 0;
	}

	/**
	 * {@inheritdoc}
	 */
	#[\ReturnTypeWillChange]
	public function current() {
		$args = $this->getArrayCopy();

		return $args[ $this->position ];
	}

	/**
	 * {@inheritdoc}
	 */
	#[\ReturnTypeWillChange]
	public function key() {
		return $this->position;
	}

	/**
	 * {@inheritdoc}
	 */
	public function next(): void {
		$this->position ++;
	}

	/**
	 * {@inheritdoc}
	 */
	public function valid(): bool {
		$args = $this->getArrayCopy();

		return isset( $args[ $this->position ] );
	}

	/**
	 * On cast to string, return object identifier.
	 *
	 * @return string Object identifier.
	 */
	public function __toString(): string {
		return $this->get_identifier();
	}

	/**
	 * Check if offset exists.
	 *
	 * @param mixed $offset Offset name.
	 *
	 * @return bool Whether the offset exists.
	 */
	public function offsetExists( $offset ): bool {
		return $this->__isset( $offset );
	}

	/**
	 * Get offset value.
	 *
	 * @param mixed $offset Offset name.
	 *
	 * @return mixed|null Offset value, or null if not set.
	 */
	#[\ReturnTypeWillChange]
	public function &offsetGet( $offset ) {
		// We fake the pass by reference to avoid PHP errors for backcompat.
		$value = $this->__get( $offset );

		return $value;
	}

	/**
	 * Set offset value.
	 *
	 * @param mixed $offset Offset name.
	 * @param mixed $value  Offset value.
	 */
	public function offsetSet( $offset, $value ): void {
		if ( null === $offset ) {
			// Do not allow $object[] additions.
			return;
		}

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

	/**
	 * Unset offset value.
	 *
	 * @param mixed $offset Offset name.
	 */
	public function offsetUnset( $offset ): void {
		$this->__unset( $offset );
	}

	/**
	 * Check if offset exists.
	 *
	 * @param mixed $name Offset name.
	 *
	 * @return bool Whether the offset exists.
	 */
	public function __isset( $name ): bool {
		if ( is_int( $name ) ) {
			$args = $this->getArrayCopy();

			return isset( $args[ $name ] );
		}

		$special_args = [
			'fields'        => 'get_fields',
			'object_fields' => 'get_object_fields',
			'groups'        => 'get_groups',
			'table_info'    => 'get_table_info',
			'options'       => 'get_args',
		];

		if ( isset( $special_args[ $name ] ) ) {
			return true;
		}

		$value = $this->get_arg( $name, null );

		return ( null !== $value );
	}

	/**
	 * Get offset value.
	 *
	 * @param mixed $name Offset name.
	 *
	 * @return mixed|null Offset value, or null if not set.
	 */
	#[\ReturnTypeWillChange]
	public function __get( $name ) {
		if ( is_int( $name ) ) {
			$args = $this->getArrayCopy();

			return isset( $args[ $name ] );
		}

		return $this->get_arg( $name );
	}

	/**
	 * Set offset value.
	 *
	 * @param mixed $name Offset name.
	 * @param mixed $value  Offset value.
	 */
	public function __set( $name, $value ): void {
		$this->set_arg( $name, $value );
	}

	/**
	 * Unset offset value.
	 *
	 * @param mixed $name Offset name.
	 */
	public function __unset( $name ): void {
		$this->set_arg( $name, null );
	}

	/**
	 * Setup object.
	 *
	 * @param array     $args        {
	 *                               Object arguments.
	 *
	 * @type string     $name        Object name.
	 * @type string|int $id          Object ID.
	 * @type string     $label       Object label.
	 * @type string     $description Object description.
	 * @type string|int $parent      Object parent name or ID.
	 * @type string|int $group       Object group name or ID.
	 * }
	 */
	public function setup( array $args = [] ) {
		if ( empty( $args ) ) {
			$args = $this->get_args();
		}

		$this->_fields        = null;
		$this->_object_fields = null;
		$this->_groups        = null;
		$this->_table_info    = null;

		$defaults = [
			'object_type'         => $this->get_arg( 'object_type' ),
			'object_storage_type' => $this->get_arg( 'object_storage_type', 'collection' ),
			'name'                => '',
			'id'                  => '',
			'parent'              => '',
			'group'               => '',
			'label'               => '',
			'description'         => '',
		];

		$args = array_merge( $defaults, $args );

		// Reset arguments.
		$this->args = $defaults;

		foreach ( $args as $arg => $value ) {
			$this->set_arg( $arg, $value );
		}

		/**
		 * Allow further adjustments after a Whatsit object is set up.
		 *
		 * @since 2.9.8
		 *
		 * @param Whatsit $object      Whatsit object.
		 * @param string  $object_type The Whatsit object type.
		 */
		do_action( 'pods_whatsit_setup', $this, static::$type );

		// Make a hook friendly name.
		$class_hook = static::$type;

		/**
		 * Allow further adjustments after a Whatsit object is set up for a specific object class.
		 *
		 * Example hook names:
		 * - pods_whatsit_setup_pod
		 * - pods_whatsit_setup_group
		 * - pods_whatsit_setup_object-field
		 * - pods_whatsit_setup_field
		 * - pods_whatsit_setup_template
		 * - pods_whatsit_setup_page
		 *
		 * @since 2.9.8
		 *
		 * @param Whatsit $object      Whatsit object.
		 * @param string  $object_type The Whatsit object type.
		 */
		do_action( "pods_whatsit_setup_{$class_hook}", $this, static::$type );

		$content_type_for_is_types_only = ( $this->args['parent'] ? $this->args['parent'] : $this->args['name'] );

		// If the type is a Pod or Group and types-only mode is enabled, force the groups/fields to be empty.
		if (
			(
				'pod' === static::$type
				|| 'group' === static::$type
			)
			&& pods_is_types_only( false, $content_type_for_is_types_only )
		) {
			$this->_groups = [];
			$this->_fields = [];
		}
	}

	/**
	 * Set the arguments for the object.
	 *
	 * @param array|Whatsit $args    List of object arguments to set.
	 * @param bool          $replace Whether to replace the argument if it was already set.
	 *
	 * @return self The object.
	 */
	public function set_args( $args, $replace = true ) {
		if ( $args instanceof self ) {
			$args = $args->get_args();
		}

		// Invalid arguments received.
		if ( ! is_array( $args ) ) {
			return $this;
		}

		// Set up the options if they were provided.
		if ( isset( $args['options'] ) ) {
			$args = array_merge( $args['options'], $args );

			unset( $args['options'] );
		}

		foreach ( $args as $arg => $value ) {
			// Skip arguments if we are not replacing them but they are already set.
			if (
				! $replace
				&& ! empty( $this->args[ $arg ] )
				&& '' !== $this->args[ $arg ]
			) {
				continue;
			}

			$this->set_arg( $arg, $value );
		}

		return $this;
	}

	/**
	 * Get object argument value.
	 *
	 * @param string     $arg     Argument name.
	 * @param mixed|null $default Default to use if not set.
	 * @param bool       $strict  Whether to check only normal arguments and not special arguments.
	 * @param bool       $raw     Whether only get the raw value.
	 *
	 * @return null|mixed Argument value, or null if not set.
	 */
	public function get_arg( $arg, $default = null, $strict = false, $raw = false ) {
		$arg = (string) $arg;

		if ( $raw ) {
			return $this->args[ $arg ] ?? $default;
		}

		$special_args = [
			'identifier'        => 'get_identifier',
			'label'             => 'get_label',
			'description'       => 'get_description',
			'fields'            => 'get_fields',
			'object_fields'     => 'get_object_fields',
			'all_fields'        => 'get_all_fields',
			'groups'            => 'get_groups',
			'table_info'        => 'get_table_info',
			'options'           => 'get_args',
			'conditional_logic' => 'get_conditional_logic',
		];

		if ( isset( $special_args[ $arg ] ) ) {
			return $this->{$special_args[ $arg ]}();
		}

		// Enforce lowercase 'id' argument usage.
		if ( 'ID' === $arg ) {
			$arg = 'id';
		}

		$is_set = isset( $this->args[ $arg ] );

		if ( ! $is_set && ! $strict ) {
			if ( 'internal' === $arg ) {
				return $default;
			}

			$table_info_fields = [
				'object_name',
				'object_hierarchical',
				'table',
				'meta_table',
				'pod_table',
				'field_id',
				'field_index',
				'field_slug',
				'field_type',
				'field_parent',
				'field_parent_select',
				'meta_field_id',
				'meta_field_index',
				'meta_field_value',
				'pod_field_id',
				'pod_field_index',
				'pod_field_slug',
				'pod_field_parent',
				'join',
				'where',
				'where_default',
				'orderby',
				'recurse',
			];

			if ( in_array( $arg, $table_info_fields, true ) ) {
				$table_info = $this->get_table_info();

				if ( isset( $table_info[ $arg ] ) ) {
					return $table_info[ $arg ];
				}
			}
		}//end if

		$value = $is_set ? $this->args[ $arg ] : $default;

		/**
		 * Allow filtering the object arguments / options.
		 *
		 * @since 2.8.4
		 *
		 * @param mixed   $value  The object argument value.
		 * @param string  $name   The argument name.
		 * @param Whatsit $object The object.
		 */
		return apply_filters( 'pods_whatsit_get_arg', $value, $arg, $this );
	}

	/**
	 * Set object argument.
	 *
	 * @param string $arg   Argument name.
	 * @param mixed  $value Argument value.
	 */
	public function set_arg( $arg, $value ) {
		$arg = (string) $arg;

		$reserved = [
			'object_type',
			'object_storage_type',
			'fields',
			'object_fields',
			'groups',
			'table_info',
			'name',
			'id',
			'parent',
			'group',
			'label',
			'description',
		];

		$read_only = [
			'object_type',
			'fields',
			'object_fields',
			'groups',
			'table_info',
		];

		if ( 'options' === $arg && is_array( $value ) ) {
			foreach ( $value as $real_arg => $real_value ) {
				$this->set_arg( $real_arg, $real_value );
			}

			return;
		}

		if ( in_array( $arg, $reserved, true ) ) {
			if ( in_array( $arg, $read_only, true ) ) {
				return;
			}

			if ( is_string( $value ) ) {
				$value = trim( $value );
			}

			$empty_values = [
				null,
				0,
				'0',
			];

			if ( in_array( $value, $empty_values, true ) ) {
				$value = '';
			}
		}

		$this->args[ $arg ] = $value;
	}

	/**
	 * Override table info when it needs to be set, for fields for example.
	 *
	 * @since 2.8.0
	 *
	 * @param array $table_info The table information to be referenced by this object.
	 */
	public function set_table_info( $table_info ) {
		$this->_table_info = $table_info;
	}

	/**
	 * Check whether the object is valid.
	 *
	 * @return bool Whether the object is valid.
	 */
	public function is_valid() {
		if ( $this->get_name() ) {
			return true;
		}

		return false;
	}

	/**
	 * Get object identifier from arguments.
	 *
	 * @param array $args Object arguments.
	 *
	 * @return string|null Object identifier or if invalid object.
	 */
	public static function get_identifier_from_args( array $args ) {
		if ( empty( $args['object_type'] ) ) {
			return null;
		}

		$parts = [
			$args['object_type'],
		];

		if ( isset( $args['parent'] ) && 0 < strlen( $args['parent'] ) ) {
			$parts[] = $args['parent'];
		}

		if ( isset( $args['name'] ) && 0 < strlen( $args['name'] ) ) {
			$parts[] = $args['name'];
		}

		return implode( '/', $parts );
	}

	/**
	 * Get object identifier.
	 *
	 * @return string Object identifier.
	 */
	public function get_identifier() {
		return self::get_identifier_from_args( $this->get_args() );
	}

	/**
	 * Get object label.
	 *
	 * @return string Object label.
	 */
	public function get_label() {
		$label = '';

		if ( isset( $this->args['label'] ) ) {
			$label = $this->args['label'];
		}

		/**
		 * Allow filtering the object label.
		 *
		 * @since 2.8.0
		 *
		 * @param string  $label  The object label.
		 * @param Whatsit $object The object.
		 */
		return apply_filters( 'pods_whatsit_get_label', $label, $this );
	}

	/**
	 * Get object description.
	 *
	 * @return string Object description.
	 */
	public function get_description() {
		$description = '';

		if ( isset( $this->args['description'] ) ) {
			$description = $this->args['description'];
		}

		/**
		 * Allow filtering the object description.
		 *
		 * @since 2.8.0
		 *
		 * @param string  $description The object description.
		 * @param Whatsit $object      The object.
		 */
		return apply_filters( 'pods_whatsit_get_description', $description, $this );
	}

	/**
	 * Get list of object arguments.
	 *
	 * @return array List of object arguments.
	 */
	public function get_args() {
		$this->maybe_migrate_dependency();

		if ( isset( $this->args['conditional_logic'] ) && is_string( $this->args['conditional_logic'] ) ) {
			$this->args['conditional_logic'] = json_decode( $this->args['conditional_logic'], true );

			if ( empty( $this->args['conditional_logic'] ) || ! is_array( $this->args['conditional_logic'] ) ) {
				unset( $this->args['conditional_logic'] );
			}
		}

		/**
		 * Allow filtering the object arguments.
		 *
		 * @since 2.8.4
		 *
		 * @param array   $args   The object arguments.
		 * @param Whatsit $object The object.
		 */
		return apply_filters( 'pods_whatsit_get_args', $this->args, $this );
	}

	/**
	 * Get list of clean object arguments.
	 *
	 * @return array List of clean object arguments.
	 */
	public function get_clean_args() {
		$args = $this->args;

		$excluded_args = [
			'object_type',
			'object_storage_type',
			'parent',
			'group',
		];

		foreach ( $excluded_args as $excluded_arg ) {
			if ( isset( $args[ $excluded_arg ] ) ) {
				unset( $args[ $excluded_arg ] );
			}
		}

		return $args;
	}

	/**
	 * Get object parent.
	 *
	 * @return Whatsit|null Object parent, or null if not set.
	 */
	public function get_parent_object() {
		$parent = $this->get_parent();

		if ( $parent ) {
			$store = Store::get_instance();

			// Attempt to get from storage directly.
			$parent = $store->get_object_from_storage( $this->get_object_storage_type(), $parent );
		}

		return $parent;
	}

	/**
	 * Get object group.
	 *
	 * @return Whatsit|null Object group, or null if not set.
	 */
	public function get_group_object() {
		$group = $this->get_group();

		if ( $group ) {
			$store = Store::get_instance();

			// Attempt to get from storage directly.
			$group = $store->get_object_from_storage( $this->get_object_storage_type(), $group );

			if ( $group ) {
				$this->set_arg( 'group', $group->get_identifier() );
			}
		}

		return $group;
	}

	/**
	 * Maybe get the list of objects or determine if they need to be loaded.
	 *
	 * @since 2.9.10
	 *
	 * @param array $objects The list of objects or their identifiers.
	 * @param array $args    The list of arguments to filter by.
	 *
	 * @return Whatsit[]|null The list of objects or null if they need to be loaded separately.
	 */
	protected function maybe_get_objects_by_identifier( array $objects, $args ) {
		$api = pods_api();

		$object_collection = Store::get_instance();

		$storage_type = ! empty( $args['object_storage_type'] ) ? $args['object_storage_type'] : $api->get_default_object_storage_type();

		/** @var \Pods\Whatsit\Storage\Post_Type $storage_object */
		$storage_object = $object_collection->get_storage_object( $storage_type );

		$parent = $this;

		// Check if we have at least the object field.
		if ( ! empty( $objects ) ) {
			$first_object = reset( $objects );

			// Check if this is an identifier.
			if ( is_string( $first_object ) ) {
				// We likely don't have any of these objects so just fetch them together normally as that's quicker.
				return null;
			}
		}

		$found_identifier = false;

		// Build any objects from identifiers that are needed.
		$objects = array_map(
			static function( $identifier ) use ( $storage_object, $parent, &$found_identifier ) {
				if ( $identifier instanceof Whatsit ) {
					return $identifier;
				}

				$found_identifier = true;

				return $storage_object->get_by_identifier( $identifier, $parent );
			},
			$objects
		);

		if ( ! $found_identifier ) {
			return $objects;
		}

		return pods_objects_keyed_by_name( $objects );
	}

	/**
	 * Fetch field from object with no traversal support.
	 *
	 * @param string $field_name    Field name.
	 * @param bool   $load_all      Whether to load all fields when getting this field.
	 * @param bool   $check_aliases Whether to check aliases if field not found.
	 *
	 * @return Field|null Field object, or null if object not found.
	 */
	public function fetch_field( $field_name, $load_all = true, $check_aliases = true ) {
		$get_fields_args = [];

		if ( ! $load_all ) {
			$get_fields_args = [
				'name' => $field_name,
			];
		}

		$fields = $this->get_fields( $get_fields_args );

		$field = null;

		if ( isset( $fields[ $field_name ] ) ) {
			$field = $fields[ $field_name ];
		} else {
			$object_fields = $this->get_object_fields();

			if ( isset( $object_fields[ $field_name ] ) ) {
				$field = $object_fields[ $field_name ];
			} elseif ( $check_aliases ) {
				foreach ( $fields as $the_field ) {
					if ( ! empty( $the_field['alias'] ) && in_array( $field_name, $the_field['alias'], true ) ) {
						$field = $the_field;

						break;
					}
				}

				if ( ! $field && ! empty( $object_fields ) ) {
					foreach ( $object_fields as $the_field ) {
						if ( ! empty( $the_field['alias'] ) && in_array( $field_name, $the_field['alias'], true ) ) {
							$field = $the_field;

							break;
						}
					}
				}
			}
		}

		if ( ! $field instanceof Field ) {
			return null;
		}

		return $field;
	}

	/**
	 * Get field from object with traversal support.
	 *
	 * @param string      $field_name    Field name.
	 * @param null|string $arg           Argument name.
	 * @param bool        $load_all      Whether to load all fields when getting this field.
	 * @param bool        $check_aliases Whether to check aliases if field not found.
	 *
	 * @return Field|mixed|null Field object, argument value, or null if object not found.
	 */
	public function get_field( $field_name, $arg = null, $load_all = true, $check_aliases = true ) {
		$fields_to_traverse = explode( '.', $field_name );
		$fields_to_traverse = array_filter( $fields_to_traverse );

		$total_fields_to_traverse = count( $fields_to_traverse );

		$field = null;

		/** @var Whatsit $whatsit */
		$whatsit = $this;

		for ( $f = 0; $f < $total_fields_to_traverse; $f ++ ) {
			$field_to_traverse = $fields_to_traverse[ $f ];

			$field = $whatsit->fetch_field( $field_to_traverse, $load_all, $check_aliases );

			// Check if there are more fields to traverse.
			if ( ( $f + 1 ) === $total_fields_to_traverse ) {
				break;
			}

			// Check if the field is traversable.
			if ( ! $field instanceof Field ) {
				$field = null;

				break;
			}

			// Fill in the next object data.
			$whatsit = $field->get_related_object();

			// Check if the related object exists.
			if ( ! $whatsit instanceof Whatsit ) {
				$field = null;

				break;
			}
		}

		if ( ! $field instanceof Field ) {
			return null;
		}

		if ( null !== $arg ) {
			return $field->get_arg( $arg );
		}

		return $field;
	}

	/**
	 * Get fields for object.
	 *
	 * @param array $args List of arguments to filter by.
	 *
	 * @return Field[] List of field objects.
	 */
	public function get_fields( array $args = [] ) {
		if ( [] === $this->_fields ) {
			return [];
		}

		$api = pods_api();

		$has_custom_args = ! empty( $args );

		if ( null !== $this->_fields && ! $has_custom_args ) {
			$objects = $this->maybe_get_objects_by_identifier( $this->_fields, $args );

			if ( is_array( $objects ) ) {
				$this->_fields = pods_clone_objects( $objects );

				/** @var Field[] $objects */
				return $objects;
			}
		}

		$filtered_args = [
			'parent'            => $this->get_id(),
			'parent_id'         => $this->get_id(),
			'parent_name'       => $this->get_name(),
			'parent_identifier' => $this->get_identifier(),
		];

		if ( empty( $filtered_args['parent_id'] ) ) {
			$filtered_args['bypass_post_type_find'] = true;
		}

		$filtered_args = array_filter( $filtered_args );

		$args = array_merge( [
			'orderby' => 'menu_order title',
			'order'   => 'ASC',
		], $filtered_args, $args );

		try {
			if ( empty( $args['parent_identifier'] ) ) {
				throw new Exception( 'Invalid parent identifier for field lookup by object' );
			}

			if ( ! empty( $args['object_type'] ) ) {
				$objects = $api->_load_objects( $args );
			} else {
				$objects = $api->load_fields( $args );
			}
		} catch ( Exception $exception ) {
			pods_debug_log( $exception );

			$objects = [];
		}

		if ( ! $has_custom_args ) {
			$this->_fields = pods_clone_objects( $objects );
		}

		return $objects;
	}

	/**
	 * Get object fields for object.
	 *
	 * @return Object_Field[] List of object field objects.
	 */
	public function get_object_fields() {
		return [];
	}

	/**
	 * Get all fields for object.
	 *
	 * @return Field[] List of field objects.
	 */
	public function get_all_fields() {
		return array_merge( $this->get_fields(), $this->get_object_fields() );
	}

	/**
	 * Determine whether the object has fields.
	 *
	 * @param array $args List of arguments to filter by.
	 *
	 * @return bool Whether the object has fields.
	 */
	public function has_fields( array $args = [] ) {
		$count = $this->count_fields( $args );

		return 0 < $count;
	}

	/**
	 * Count the number of fields the object has.
	 *
	 * @param array $args List of arguments to filter by.
	 *
	 * @return int The number of fields the object has.
	 */
	public function count_fields( array $args = [] ) {
		if ( [] === $this->_fields ) {
			return 0;
		}

		$has_custom_args = ! empty( $args );

		if ( null !== $this->_fields && ! $has_custom_args ) {
			return count( $this->_fields );
		}

		$filtered_args = [
			'parent'            => $this->get_id(),
			'parent_id'         => $this->get_id(),
			'parent_name'       => $this->get_name(),
			'parent_identifier' => $this->get_identifier(),
		];

		if ( empty( $filtered_args['parent_id'] ) ) {
			$filtered_args['bypass_post_type_find'] = true;
		}

		$filtered_args = array_filter( $filtered_args );

		$args = array_merge( $filtered_args, $args );

		// Enforce argument.
		$args['count'] = true;

		try {
			$api = pods_api();

			if ( ! empty( $args['object_type'] ) ) {
				$total_objects = $api->_load_objects( $args );
			} else {
				$total_objects = $api->load_fields( $args );
			}
		} catch ( Exception $exception ) {
			pods_debug_log( $exception );

			$total_objects = 0;
		}

		return $total_objects;
	}

	/**
	 * Determine whether the object has object fields.
	 *
	 * @return bool Whether the object has object fields.
	 */
	public function has_object_fields() {
		$count = $this->count_object_fields();

		return 0 < $count;
	}

	/**
	 * Count the number of object fields the object has.
	 *
	 * @return int The number of object fields the object has.
	 */
	public function count_object_fields() {
		return 0;
	}

	/**
	 * Get groups for object.
	 *
	 * @param array $args List of arguments to filter by.
	 *
	 * @return Group[] List of group objects.
	 */
	public function get_groups( array $args = [] ) {
		if ( [] === $this->_groups ) {
			return [];
		}

		$api = pods_api();

		$has_custom_args = ! empty( $args );

		if ( null !== $this->_groups && ! $has_custom_args ) {
			$objects = $this->maybe_get_objects_by_identifier( $this->_groups, $args );

			if ( is_array( $objects ) ) {
				$this->_groups = $objects;

				/** @var Group[] $objects */
				return $objects;
			}
		}

		$filtered_args = [
			'parent'            => $this->get_id(),
			'parent_id'         => $this->get_id(),
			'parent_name'       => $this->get_name(),
			'parent_identifier' => $this->get_identifier(),
		];

		if ( empty( $filtered_args['parent_id'] ) ) {
			$filtered_args['bypass_post_type_find'] = true;
		}

		$filtered_args = array_filter( $filtered_args );

		$args = array_merge( [
			'orderby' => 'menu_order title',
			'order'   => 'ASC',
		], $filtered_args, $args );

		try {
			if ( ! empty( $args['object_type'] ) ) {
				$objects = $api->_load_objects( $args );
			} else {
				$objects = $api->load_groups( $args );
			}
		} catch ( Exception $exception ) {
			pods_debug_log( $exception );

			$objects = [];
		}

		if ( ! $has_custom_args ) {
			$this->_groups = wp_list_pluck( $objects, 'identifier' );
		}

		return $objects;
	}

	/**
	 * Determine whether the object has groups.
	 *
	 * @param array $args List of arguments to filter by.
	 *
	 * @return bool Whether the object has groups.
	 */
	public function has_groups( array $args = [] ) {
		$count = $this->count_groups( $args );

		return 0 < $count;
	}

	/**
	 * Count the number of groups the object has.
	 *
	 * @param array $args List of arguments to filter by.
	 *
	 * @return int The number of groups the object has.
	 */
	public function count_groups( array $args = [] ) {
		if ( [] === $this->_groups ) {
			return 0;
		}

		$has_custom_args = ! empty( $args );

		if ( null !== $this->_groups && ! $has_custom_args ) {
			return $this->_groups;
		}

		$filtered_args = [
			'parent'            => $this->get_id(),
			'parent_id'         => $this->get_id(),
			'parent_name'       => $this->get_name(),
			'parent_identifier' => $this->get_identifier(),
		];

		if ( empty( $filtered_args['parent_id'] ) ) {
			$filtered_args['bypass_post_type_find'] = true;
		}

		$filtered_args = array_filter( $filtered_args );

		$args = array_merge( $filtered_args, $args );

		// Enforce argument.
		$args['count'] = true;

		try {
			$api = pods_api();

			if ( ! empty( $args['object_type'] ) ) {
				$total_objects = $api->_load_objects( $args );
			} else {
				$total_objects = $api->load_groups( $args );
			}
		} catch ( Exception $exception ) {
			pods_debug_log( $exception );

			$total_objects = 0;
		}

		return $total_objects;
	}

	/**
	 * Get table information for object.
	 *
	 * @return array Table information for object.
	 */
	public function get_table_info() {
		if ( null !== $this->_table_info ) {
			return $this->_table_info;
		}

		return [];
	}

	/**
	 * Get table name for object.
	 *
	 * @return string|null Table name for object or null if not set.
	 */
	public function get_table_name() {
		$table_info = $this->get_table_info();

		if ( ! empty( $table_info['table'] ) ) {
			return $table_info['table'];
		}

		return null;
	}

	/**
	 * Get the pod table name for object.
	 *
	 * @since 2.8.9
	 *
	 * @return string|null The Pod table name for object or null if not set.
	 */
	public function get_pod_table_name() {
		$table_info = $this->get_table_info();

		if ( ! empty( $table_info['pod_table'] ) ) {
			return $table_info['pod_table'];
		}

		return null;
	}

	/**
	 * Get the object storage type label.
	 *
	 * @since 2.8.24
	 *
	 * @return string|null
	 */
	public function get_object_storage_type_label() {
		$object_storage_type = $this->get_arg( 'object_storage_type', 'collection' );

		if ( ! $object_storage_type ) {
			return null;
		}

		$object_collection = Store::get_instance();

		$storage_type_obj = $object_collection->get_storage_object( $object_storage_type );

		if ( ! $storage_type_obj ) {
			return null;
		}

		return $storage_type_obj->get_label();
	}

	/**
	 * Determine whether conditional logic is enabled.
	 *
	 * @since 3.0
	 *
	 * @return bool Whether conditional logic is enabled.
	 */
	public function is_conditional_logic_enabled(): bool {
		return (
			filter_var( $this->get_arg( 'enable_conditional_logic', false ), FILTER_VALIDATE_BOOLEAN )
			|| $this->maybe_migrate_dependency()
		);
	}

	/**
	 * Get the conditional logic.
	 *
	 * @since 3.0
	 *
	 * @return array|null The conditional logic or null if not set.
	 */
	public function get_conditional_logic(): ?Conditional_Logic {
		if ( ! $this->is_conditional_logic_enabled() ) {
			return null;
		}

		if ( empty( $this->args['conditional_logic'] ) && ! $this->maybe_migrate_dependency() ) {
			return null;
		}

		return Conditional_Logic::maybe_setup_from_object( $this );
	}

	/**
	 * Get the conditional logic configuration array.
	 *
	 * @since 3.0
	 *
	 * @return array|null The conditional logic configuration array or null if not set.
	 */
	public function get_conditional_logic_config(): ?array {
		$conditional_logic = $this->args['conditional_logic'];

		if ( is_string( $conditional_logic ) ) {
			$conditional_logic = 0 === strpos( $conditional_logic, '{' ) ? json_decode( $conditional_logic, true ) : null;

			if ( empty( $conditional_logic ) || ! is_array( $conditional_logic ) ) {
				unset( $this->args['conditional_logic'] );

				return null;
			}
		}

		if ( empty( $conditional_logic['rules'] ) ) {
			return null;
		}

		if ( empty( $conditional_logic['action'] ) ) {
			$conditional_logic['action'] = 'show';
		}

		if ( empty( $conditional_logic['logic'] ) ) {
			$conditional_logic['logic'] = 'any';
		}

		return $conditional_logic;
	}

	/**
	 * Determine whether the conditional logic allows this object to be visible.
	 *
	 * @since 3.0
	 *
	 * @return bool Whether the conditional logic allows this object to be visible.
	 */
	public function is_visible( array $values ): bool {
		$conditional_logic = $this->get_conditional_logic();

		if ( ! $conditional_logic ) {
			return true;
		}

		return $conditional_logic->is_field_visible( $values );
	}

	/**
	 * Maybe migrate dependency logic into the newer conditional logic.
	 *
	 * @return bool Whether the migration was done.
	 */
	public function maybe_migrate_dependency(): bool {
		if ( ! empty( $this->args['conditional_logic'] ) ) {
			return false;
		}

		if (
			! isset( $this->args['depends-on'] )
			&& ! isset( $this->args['depends-on-any'] )
			&& ! isset( $this->args['depends-on-multi'] )
			&& ! isset( $this->args['excludes-on'] )
			&& ! isset( $this->args['wildcard-on'] )
		) {
			return false;
		}

		$conditional_logic_object = Conditional_Logic::maybe_setup_from_old_syntax( $this );

		if ( ! $conditional_logic_object ) {
			return false;
		}

		$this->args['enable_conditional_logic'] = true;
		$this->args['conditional_logic']        = $conditional_logic_object->to_array();

		return true;
	}

	/**
	 * Get the full data from the object.
	 *
	 * @param array $args List of arguments.
	 *
	 * @return array Full data from the object.
	 */
	public function export( array $args = [] ) {
		$defaults = [
			'include_groups'        => true,
			'include_group_fields'  => true,
			'include_fields'        => true,
			'include_field_data'    => false,
			'include_object_fields' => false,
			'include_table_info'    => false,
			'build_default_group'   => false,
			'assoc_keys'            => false,
			'bypass_cache'          => false,
			'ref_id'                => null,
		];

		$args = array_merge( $defaults, $args );

		$data = $this->get_args();

		if ( $args['include_groups'] ) {
			$data['groups'] = $this->get_export_for_items(
				$this->get_groups( [
					'bypass_cache' => $args['bypass_cache'],
					'ref_id'       => $args['ref_id'],
				] ),
				[
					'include_groups'     => false,
					'include_fields'     => $args['include_group_fields'],
					'include_field_data' => $args['include_field_data'],
					'assoc_keys'         => $args['assoc_keys'],
					'bypass_cache'       => $args['bypass_cache'],
					'ref_id'             => $args['ref_id'],
				]
			);

			// If there are no groups, see if we need to build the default one.
			if ( $args['build_default_group'] && empty( $data['groups'] ) ) {
				$fields = [];

				if ( $args['include_group_fields'] ) {
					$fields = $this->get_args_for_items(
						$this->get_fields( [
							'bypass_cache' => $args['bypass_cache'],
							'ref_id'       => $args['ref_id'],
						] ),
						[
							'include_field_data' => $args['include_field_data'],
						]
					);

					if ( ! $args['assoc_keys'] ) {
						$fields = array_values( $fields );
					}
				}

				/**
				 * Filter the title of the Pods Metabox used in the post editor.
				 *
				 * @since unknown
				 *
				 * @param string  $title  The title to use, default is 'More Fields'.
				 * @param Whatsit $pod    Current Pods Object.
				 * @param array   $fields Array of fields that will go in the metabox.
				 * @param string  $type   The type of Pod.
				 * @param string  $name   Name of the Pod.
				 */
				$group_title = apply_filters( 'pods_meta_default_box_title', __( 'More Fields', 'pods' ), $this, $fields, $this->get_type(), $this->get_name() );

				$group_name = sanitize_key( pods_js_name( sanitize_title( $group_title ) ) );

				$data['groups'][ $group_name ] = [
					'name'   => $group_name,
					'label'  => $group_title,
					'fields' => $fields,
				];
			}

			if ( ! $args['assoc_keys'] ) {
				$data['groups'] = array_values( $data['groups'] );
			}
		}

		if ( $args['include_fields'] ) {
			$data['fields'] = $this->get_args_for_items(
				$this->get_fields( [
					'bypass_cache' => $args['bypass_cache'],
					'ref_id'       => $args['ref_id'],
				] ),
				[
					'include_field_data' => $args['include_field_data'],
				]
			);

			if ( ! $args['assoc_keys'] ) {
				$data['fields'] = array_values( $data['fields'] );
			}
		}

		if ( $args['include_object_fields'] ) {
			$data['object_fields'] = $this->get_args_for_items( $this->get_object_fields() );

			if ( ! $args['assoc_keys'] ) {
				$data['object_fields'] = array_values( $data['object_fields'] );
			}
		}

		if ( $args['include_table_info'] ) {
			$data['table_info'] = $this->get_table_info();
		}

		return $data;
	}

	/**
	 * Get args for items in an array.
	 *
	 * @since 2.8.0
	 *
	 * @param Whatsit[] $items List of items.
	 * @param array     $args  List of arguments to customize what is returned.
	 *
	 * @return array List of item args.
	 */
	protected function get_args_for_items( array $items, array $args = [] ) {
		return array_map( static function ( $object ) use ( $args ) {
			/** @var Whatsit $object */
			$item_args = $object->get_args();

			// Include related field data if needed.
			if ( ! empty( $args['include_field_data'] ) ) {
				/** @var Whatsit\Field $object */
				$related_data = $object->get_related_object_data();

				if ( is_array( $related_data ) ) {
					$item_args['data'] = $related_data;
				}
			}

			return $item_args;
		}, $items );
	}

	/**
	 * Get export for items in an array.
	 *
	 * @since 2.8.0
	 *
	 * @param Whatsit[] $items List of items.
	 * @param array     $args  List of export arguments.
	 *
	 * @return array List of item exports.
	 */
	protected function get_export_for_items( array $items, array $args = [] ) {
		return array_map( static function ( $object ) use ( $args ) {
			/** @var Whatsit $object */
			return $object->export( $args );
		}, $items );
	}

	/**
	 * Call magic methods.
	 *
	 * @param string $name      Method name.
	 * @param array  $arguments Method arguments.
	 *
	 * @return mixed|null
	 */
	#[\ReturnTypeWillChange]
	public function __call( $name, $arguments ) {
		$object = null;
		$method = null;

		if ( 0 === strpos( $name, 'get_parent_' ) ) {
			// Handle parent method calls.
			$object = $this->get_parent_object();

			$method = explode( 'get_parent_', $name );
			$method = 'get_' . $method[1];
		} elseif ( 0 === strpos( $name, 'get_group_' ) ) {
			// Handle group method calls.
			$object = $this->get_group_object();

			$method = explode( 'get_group_', $name );
			$method = 'get_' . $method[1];
		}

		if ( $method ) {
			if ( ! $object ) {
				return null;
			}

			return call_user_func_array( [ $object, $method ], $arguments );
		}

		// Handle arg method calls.
		if ( 0 === strpos( $name, 'get_' ) ) {
			$arg = explode( 'get_', $name );
			$arg = $arg[1];

			$supported_args = [
				'object_type',
				'object_storage_type',
				'name',
				'id',
				'parent',
				'group',
				'label',
				'description',
				'type',
			];

			$value = $this->get_arg( $arg );

			if ( ! empty( $value ) && in_array( $arg, $supported_args, true ) ) {
				return $value;
			}

			return null;
		}//end if

		return null;
	}

	/**
	 * Flush object of cached child data.
	 */
	public function flush() {
		$this->_fields        = null;
		$this->_groups        = null;
		$this->_object_fields = null;
		$this->_table_info    = null;
	}

}
