<?php
/**
 * Attribute component.
 *
 * @package HivePress\Components
 */

namespace HivePress\Components;

use HivePress\Helpers as hp;

// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;

/**
 * Handles model attributes.
 */
final class Attribute extends Component {

	/**
	 * Model parameters.
	 *
	 * @var array
	 */
	protected $models = [];

	/**
	 * Model attributes.
	 *
	 * @var array
	 */
	protected $attributes = [];

	/**
	 * Class constructor.
	 *
	 * @param array $args Component arguments.
	 */
	public function __construct( $args = [] ) {
		$args = hp\merge_arrays(
			[
				'models' => [
					'listing' => [],
					'vendor'  => [],
					'user'    => [],
				],
			],
			$args
		);

		parent::__construct( $args );
	}

	/**
	 * Bootstraps component properties.
	 */
	protected function boot() {

		// Register models.
		add_action( 'hivepress/v1/setup', [ $this, 'register_models' ] );

		// Register post types.
		add_filter( 'hivepress/v1/post_types', [ $this, 'register_post_types' ], 1 );

		// Register taxonomies.
		add_filter( 'hivepress/v1/taxonomies', [ $this, 'register_taxonomies' ], 1 );

		// Register attributes.
		add_action( 'init', [ $this, 'register_attributes' ], 100 );

		// Import attribute.
		add_filter( 'wxr_importer.pre_process.term', [ $this, 'import_attribute' ] );

		// Add settings.
		add_filter( 'hivepress/v1/settings', [ $this, 'add_settings' ] );

		// Manage meta boxes.
		add_filter( 'hivepress/v1/meta_boxes', [ $this, 'add_meta_boxes' ], 1 );
		add_action( 'add_meta_boxes', [ $this, 'remove_meta_boxes' ], 100 );

		// Redirect archive page.
		add_action( 'template_redirect', [ $this, 'redirect_archive_page' ] );

		if ( ! is_admin() ) {

			// Set search query.
			add_action( 'pre_get_posts', [ $this, 'set_search_query' ] );

			// Disable Jetpack search.
			add_filter( 'jetpack_search_should_handle_query', [ $this, 'disable_jetpack_search' ], 10, 2 );
		}
	}

	/**
	 * Gets attribute name.
	 *
	 * @param string $slug Attribute slug.
	 * @param string $prefix Attribute prefix.
	 * @return string
	 */
	public function get_attribute_name( $slug, $prefix = '' ) {
		if ( $prefix ) {
			$prefix .= '_';
		}

		return substr( hp\sanitize_key( urldecode( $slug ) ), 0, 32 - strlen( hp\prefix( $prefix ) ) );
	}

	/**
	 * Gets attributes.
	 *
	 * @param string $model Model name.
	 * @param array  $category_ids Category IDs.
	 * @return array
	 */
	public function get_attributes( $model, $category_ids = null ) {
		$attributes = hp\get_array_value( $this->attributes, $model, [] );

		if ( ! is_null( $category_ids ) ) {
			$attributes = array_filter(
				$attributes,
				function( $attribute ) use ( $category_ids ) {
					return empty( $attribute['categories'] ) || array_intersect( (array) $category_ids, $attribute['categories'] );
				}
			);
		}

		return $attributes;
	}

	/**
	 * Gets attribute fields.
	 *
	 * @param string $model Model name.
	 * @param array  $values Attribute values.
	 * @param string $context Field context.
	 * @return array
	 */
	protected function get_attribute_fields( $model, $values, $context = 'search' ) {
		$attribute_fields = [];

		// Get category IDs.
		if ( 'search' === $context ) {
			$category_ids = isset( $values['_category'] ) ? [ absint( $values['_category'] ) ] : [];
		} else {
			$category_ids = array_map( 'absint', (array) hp\get_array_value( $values, 'categories' ) );
		}

		// Get attributes.
		$attributes = $this->get_attributes( $model, $category_ids );

		foreach ( $attributes as $attribute_name => $attribute ) {

			// Check attribute.
			if ( ( 'search' === $context && ! $attribute['searchable'] && ! $attribute['filterable'] ) || ( 'edit' === $context && ! $attribute['related'] ) ) {
				continue;
			}

			// Get field arguments.
			$field_args = $attribute[ $context . '_field' ];

			if ( isset( $field_args['options'] ) && ! isset( $field_args['_external'] ) ) {
				$field_args['name'] = hp\prefix( $model . '_' . $attribute_name );
			} else {
				$field_args['name'] = hp\prefix( $attribute_name );
			}

			// Create field.
			$field = hp\create_class_instance( '\HivePress\Fields\\' . $field_args['type'], [ $field_args ] );

			if ( $field && $field::get_meta( 'filterable' ) ) {

				// Set field value.
				$field->set_value( hp\get_array_value( $values, $attribute_name ) );

				if ( $field->validate() ) {

					// Check range values.
					if ( 'number_range' === $field::get_meta( 'name' ) && ! array_diff( (array) $field->get_value(), $this->get_range_values( $model, $attribute_name ) ) ) {
						continue;
					}

					// Add field.
					$attribute_fields[ $attribute_name ] = $field;
				}
			}
		}

		return $attribute_fields;
	}

	/**
	 * Gets model names.
	 *
	 * @param string $type Model type.
	 * @return array
	 */
	public function get_models( $type = null ) {
		$models = array_keys( $this->models );

		if ( 'post' === $type ) {
			$models = array_filter(
				$models,
				function( $model ) {
					return 'user' !== $model;
				}
			);
		}

		return $models;
	}

	/**
	 * Gets category model name.
	 *
	 * @param string $model Model name.
	 * @return string
	 */
	protected function get_category_model( $model ) {
		return ( isset( $this->models[ $model ]['category_model'] ) ? $this->models[ $model ]['category_model'] : $model ) . '_category';
	}

	/**
	 * Checks if category model required.
	 *
	 * @param string $model Model name.
	 * @return bool
	 */
	protected function requires_category_model( $model ) {

		// Get category model.
		$category_model = $this->get_category_model( $model );

		// Set query arguments.
		$args = [
			'taxonomy'   => hp\prefix( $category_model ),
			'number'     => 1,
			'fields'     => 'ids',
			'hide_empty' => false,
		];

		// Check taxonomy.
		if ( ! taxonomy_exists( $args['taxonomy'] ) ) {
			return false;
		}

		// Get category IDs.
		$category_ids = hivepress()->cache->get_cache( $args, 'models/' . $category_model );

		if ( is_null( $category_ids ) ) {
			$category_ids = get_terms( $args );

			if ( is_array( $category_ids ) ) {
				hivepress()->cache->set_cache( $args, 'models/' . $category_model, $category_ids );
			}
		}

		return (bool) $category_ids;
	}

	/**
	 * Gets category model IDs.
	 *
	 * @param string $model Model name.
	 * @param object $object Model object.
	 * @return array
	 */
	protected function get_category_ids( $model, $object = null ) {

		// Check model.
		if ( 'user' === $model ) {
			return;
		}

		// Check object.
		if ( ! $object ) {
			return;
		}

		if ( is_object( $object ) && ! $object->get_id() ) {

			// @todo fix for cases when category model is set.
			return $object->get_categories__id();
		}

		// Get object ID.
		$id = is_object( $object ) ? $object->get_id() : $object;

		if ( isset( $this->models[ $model ]['category_model'] ) ) {

			// @todo remove temporary solution, check model fields instead.
			$id = absint( get_post_field( 'post_parent', $id ) );
		}

		if ( ! $id ) {
			return;
		}

		// Get category IDs.
		$category_ids = wp_get_post_terms( $id, hp\prefix( $this->get_category_model( $model ) ), [ 'fields' => 'ids' ] );

		return $category_ids;
	}

	/**
	 * Gets current category ID.
	 *
	 * @param string $model Model name.
	 * @param bool   $validate Validate ID?
	 * @return mixed
	 */
	public function get_category_id( $model, $validate = false ) {

		// Get taxonomy.
		$taxonomy = hp\prefix( $this->get_category_model( $model ) );

		// Get category ID.
		$category_id = null;

		if ( isset( $_GET['_category'] ) ) {
			$category_id = absint( $_GET['_category'] );

			if ( $validate && $category_id && ! term_exists( $category_id, $taxonomy ) ) {
				$category_id = null;
			}
		} elseif ( is_tax( $taxonomy ) ) {
			$category_id = get_queried_object_id();
		}

		return $category_id;
	}

	/**
	 * Gets current term ID.
	 *
	 * @param string $model Model name.
	 * @return mixed
	 */
	protected function get_term_id( $model ) {
		$term_id = null;

		if ( is_tax() ) {
			$term = get_queried_object();

			if ( $term && strpos( $term->taxonomy, hp\prefix( $model . '_' ) ) === 0 ) {
				$term_id = get_queried_object_id();
			}
		}

		return $term_id;
	}

	/**
	 * Gets query arguments.
	 *
	 * @param string $model Model name.
	 * @param array  $values Attribute values.
	 * @return array
	 */
	public function get_query_args( $model, $values = [] ) {

		// Set default arguments.
		$query_args = [
			'meta_query' => [],
			'tax_query'  => [],
		];

		// Get attribute fields.
		if ( is_array( $model ) ) {

			// @todo replace temporary fix.
			$attribute_fields = $model;
		} else {
			$attribute_fields = $this->get_attribute_fields( $model, $values );
		}

		// Set attribute filters.
		foreach ( $attribute_fields as $attribute_name => $field ) {
			if ( $field->get_arg( '_parent' ) ) {

				// Get parent field.
				$parent_field = hp\get_array_value( $attribute_fields, $field->get_arg( '_parent' ) );

				if ( $parent_field ) {

					// Set parent value.
					$field->set_parent_value( $parent_field->get_value() );

					// Update field filter.
					$field->update_filter();
				}
			}

			// Get field filter.
			$field_filter = $field->get_filter();

			if ( $field_filter ) {
				if ( ! is_null( $field->get_arg( 'options' ) ) && ! $field->get_arg( '_external' ) ) {

					// Set taxonomy filter.
					$field_filter = array_combine(
						array_map(
							function( $param ) {
								return hp\get_array_value(
									[
										'name'  => 'taxonomy',
										'value' => 'terms',
									],
									$param,
									$param
								);
							},
							array_keys( $field_filter )
						),
						$field_filter
					);

					unset( $field_filter['type'] );

					// Add taxonomy clause.
					$query_args['tax_query'][ $attribute_name ] = $field_filter;
				} else {

					// Set meta filter.
					$field_filter = array_combine(
						array_map(
							function( $param ) {
								return hp\get_array_value(
									[
										'name'     => 'key',
										'operator' => 'compare',
									],
									$param,
									$param
								);
							},
							array_keys( $field_filter )
						),
						$field_filter
					);

					// Add meta clause.
					$query_args['meta_query'][ $attribute_name ] = $field_filter;
				}
			}
		}

		return $query_args;
	}

	/**
	 * Registers models.
	 */
	public function register_models() {

		/**
		 * Filters the attribute-enabled models. If you add a new model name to the filtered array, HivePress will register all the required callbacks for handling the model attributes (e.g. custom fields, search filters, sorting options).
		 *
		 * @hook hivepress/v1/components/attribute/models
		 * @param {array} $models Model names.
		 * @return {array} Model names.
		 */
		$this->models = apply_filters( 'hivepress/v1/components/attribute/models', $this->models );

		// Convert for compatibility.
		foreach ( $this->models as $index => $model ) {
			if ( is_string( $model ) ) {
				unset( $this->models[ $index ] );

				$this->models[ $model ] = [];
			}
		}

		foreach ( $this->get_models() as $model ) {

			// Set defaults.
			$this->models[ $model ]['searchable'] = true;

			// @todo check post type config instead.
			if ( ! in_array( $model, [ 'listing', 'vendor', 'request' ] ) ) {
				$this->models[ $model ]['searchable'] = false;
			}

			// Add block settings.
			add_filter( 'hivepress/v1/blocks/' . $model . 's/meta', [ $this, 'add_block_settings' ], 100 );

			// Add field settings.
			add_filter( 'hivepress/v1/meta_boxes/' . $model . '_attribute_edit', [ $this, 'add_field_settings' ], 100 );
			add_filter( 'hivepress/v1/meta_boxes/' . $model . '_attribute_search', [ $this, 'add_field_settings' ], 100 );
			add_filter( 'hivepress/v1/meta_boxes/' . $model . '_attribute_display', [ $this, 'add_field_settings' ], 100 );

			// Add model fields.
			add_filter( 'hivepress/v1/models/' . $model . '/fields', [ $this, 'add_model_fields' ], 100, 2 );

			// Add edit fields.
			add_filter( 'hivepress/v1/forms/' . $model . '_update', [ $this, 'add_edit_fields' ], 100, 2 );

			// Add admin fields.
			add_filter( 'hivepress/v1/meta_boxes/' . $model . '_attributes', [ $this, 'add_admin_fields' ], 100 );

			if ( 'user' === $model ) {

				// Add register fields.
				add_filter( 'hivepress/v1/forms/user_register', [ $this, 'add_register_fields' ], 100 );
			} else {

				// Update attribute.
				add_action( 'save_post_hp_' . $model . '_attribute', [ $this, 'update_attribute' ] );

				// Update model snippet.
				add_action( 'hivepress/v1/models/' . $model . '/create', [ $this, 'update_model_snippet' ], 100, 2 );
				add_action( 'hivepress/v1/models/' . $model . '/update', [ $this, 'update_model_snippet' ], 100, 2 );

				// Add submit fields.
				add_filter( 'hivepress/v1/forms/' . $model . '_submit', [ $this, 'add_submit_fields' ], 100, 2 );

				// Add search fields.
				add_filter( 'hivepress/v1/forms/' . $model . '_search', [ $this, 'add_search_fields' ], 100, 2 );
				add_filter( 'hivepress/v1/forms/' . $model . '_filter', [ $this, 'add_search_fields' ], 100, 2 );
				add_filter( 'hivepress/v1/forms/' . $model . '_sort', [ $this, 'add_search_fields' ], 100, 2 );

				// Add sort options.
				add_filter( 'hivepress/v1/forms/' . $model . '_sort', [ $this, 'add_sort_options' ], 100, 2 );

				// Add category options.
				add_filter( 'hivepress/v1/forms/' . $model . '_filter', [ $this, 'add_category_options' ], 100, 2 );

				// Set category value.
				add_filter( 'hivepress/v1/forms/' . $model . '_search', [ $this, 'set_category_value' ], 100, 2 );
				add_filter( 'hivepress/v1/forms/' . $model . '_filter', [ $this, 'set_category_value' ], 100, 2 );
				add_filter( 'hivepress/v1/forms/' . $model . '_sort', [ $this, 'set_category_value' ], 100, 2 );

				// Set range values.
				add_filter( 'hivepress/v1/forms/' . $model . '_filter', [ $this, 'set_range_values' ], 100, 2 );

				// Set related query.
				add_action( 'hivepress/v1/models/' . $model . '/relate', [ $this, 'set_related_query' ], 100, 2 );
			}
		}
	}

	/**
	 * Registers post types.
	 *
	 * @param array $post_types Post types.
	 * @return array
	 */
	public function register_post_types( $post_types ) {
		foreach ( $this->get_models() as $model ) {
			$post_types[ $model . '_attribute' ] = [
				'public'       => false,
				'show_ui'      => true,
				'show_in_menu' => 'user' === $model ? 'users.php' : 'edit.php?post_type=' . hp\prefix( $model ),
				'supports'     => [ 'title', 'page-attributes' ],

				'labels'       => [
					'name'               => hivepress()->translator->get_string( 'attributes' ),
					'singular_name'      => hivepress()->translator->get_string( 'attribute' ),
					'add_new'            => hivepress()->translator->get_string( 'add_new_attribute' ),
					'add_new_item'       => hivepress()->translator->get_string( 'add_attribute' ),
					'edit_item'          => hivepress()->translator->get_string( 'edit_attribute' ),
					'new_item'           => hivepress()->translator->get_string( 'add_attribute' ),
					'all_items'          => hivepress()->translator->get_string( 'attributes' ),
					'search_items'       => hivepress()->translator->get_string( 'search_attributes' ),
					'not_found'          => hivepress()->translator->get_string( 'no_attributes_found' ),
					'not_found_in_trash' => hivepress()->translator->get_string( 'no_attributes_found' ),
				],
			];
		}

		return $post_types;
	}

	/**
	 * Registers taxonomies.
	 *
	 * @param array $taxonomies Taxonomies.
	 * @return array
	 */
	public function register_taxonomies( $taxonomies ) {
		foreach ( $this->get_models( 'post' ) as $model ) {
			$taxonomy = $this->get_category_model( $model );

			if ( isset( $taxonomies[ $taxonomy ] ) ) {
				$taxonomies[ $taxonomy ] = hp\merge_arrays(
					$taxonomies[ $taxonomy ],
					[
						'post_type'          => [ $model . '_attribute' ],
						'show_in_quick_edit' => false,
					]
				);
			}
		}

		return $taxonomies;
	}

	/**
	 * Registers attributes.
	 */
	public function register_attributes() {

		// Set field contexts.
		$field_contexts = [ 'edit', 'search' ];

		foreach ( $this->get_models() as $model ) {

			// Get category model.
			$category_model = $this->get_category_model( $model );

			// Set query arguments.
			$query_args = [
				'post_type'        => hp\prefix( $model . '_attribute' ),
				'post_status'      => 'publish',
				'posts_per_page'   => -1,
				'orderby'          => 'menu_order',
				'order'            => 'ASC',
				'suppress_filters' => ! hivepress()->translator->is_multilingual(),
			];

			// Get cache group.
			$cache_group = hivepress()->model->get_cache_group( 'post', hp\prefix( $model . '_attribute' ) );

			// Get cached attributes.
			$attributes = hivepress()->cache->get_cache( array_merge( $query_args, [ 'format' => 'attributes' ] ), $cache_group );

			if ( is_null( $attributes ) ) {
				$attributes = [];

				// Get attribute objects.
				$attribute_objects = get_posts( $query_args );

				foreach ( $attribute_objects as $attribute_object ) {

					// Set defaults.
					$attribute_args = [
						'id'             => $attribute_object->ID,
						'label'          => $attribute_object->post_title,
						'display_areas'  => array_filter( (array) $attribute_object->hp_display_areas ),
						'display_format' => (string) $attribute_object->hp_display_format,
						'public'         => (bool) $attribute_object->hp_public,
						'editable'       => (bool) $attribute_object->hp_editable,
						'synced'         => (bool) $attribute_object->hp_synced,
						'moderated'      => (bool) $attribute_object->hp_moderated,
						'indexable'      => (bool) $attribute_object->hp_indexable,
						'searchable'     => (bool) $attribute_object->hp_searchable,
						'filterable'     => (bool) $attribute_object->hp_filterable,
						'sortable'       => (bool) $attribute_object->hp_sortable,
						'related'        => (bool) $attribute_object->hp_related,
						'categories'     => [],
						'edit_field'     => [],
						'search_field'   => [],
					];

					// Set icon.
					$icon = $attribute_object->hp_icon;

					if ( $icon ) {
						$icon = '<i class="hp-icon fas fa-fw fa-' . esc_attr( $icon ) . '"></i>';
					}

					$attribute_args['display_format'] = str_replace( '%icon%', $icon, $attribute_args['display_format'] );

					// Set categories.
					if ( taxonomy_exists( hp\prefix( $category_model ) ) ) {
						$attribute_args['categories'] = wp_get_post_terms( $attribute_object->ID, hp\prefix( $category_model ), [ 'fields' => 'ids' ] );
					}

					// Get fields.
					foreach ( $field_contexts as $field_context ) {

						// Set defaults.
						$field_args = [
							'label'  => $attribute_args['label'],
							'type'   => 'text',
							'_order' => 100 + absint( $attribute_object->menu_order ),
						];

						// Get field type.
						$field_type = sanitize_key( get_post_meta( $attribute_object->ID, hp\prefix( $field_context . '_field_type' ), true ) );

						if ( $field_type ) {

							// Get field settings.
							$field_settings = hp\call_class_method( '\HivePress\Fields\\' . $field_type, 'get_meta', [ 'settings' ] );

							if ( $field_settings ) {

								// Set field type.
								$field_args['type'] = $field_type;

								// @todo replace temporary fix.
								$field_settings['description'] = hp\create_class_instance(
									'\HivePress\Fields\Textarea',
									[
										[
											'max_length' => 2048,
											'html'       => true,
										],
									]
								);

								// Set field settings.
								// @todo remove array filtering.
								foreach ( array_filter( $field_settings ) as $settings_field_name => $settings_field ) {

									// Set field value.
									$settings_field->set_value( get_post_meta( $attribute_object->ID, hp\prefix( $field_context . '_field_' . $settings_field_name ), true ) );

									// Get field value.
									if ( $settings_field->validate() ) {
										$field_args[ $settings_field_name ] = $settings_field->get_value();
									}
								}
							}
						}

						// Add field.
						$attribute_args[ $field_context . '_field' ] = $field_args;
					}

					// Get attribute name.
					$attribute_name = $this->get_attribute_name( $attribute_object->post_name );

					if ( array_key_exists( 'options', $attribute_args['edit_field'] ) && ! isset( $attribute_args['edit_field']['_external'] ) ) {
						$attribute_name = $this->get_attribute_name( $attribute_object->post_name, $model );

						// Set field options.
						foreach ( $field_contexts as $field_context ) {
							$attribute_args[ $field_context . '_field' ]['options']     = 'terms';
							$attribute_args[ $field_context . '_field' ]['option_args'] = [ 'taxonomy' => hp\prefix( $model . '_' . $attribute_name ) ];
						}
					}

					// Add attribute.
					$attributes[ $attribute_name ] = $attribute_args;
				}

				// Cache attributes.
				if ( count( $attributes ) <= 100 ) {
					hivepress()->cache->set_cache( array_merge( $query_args, [ 'format' => 'attributes' ] ), $cache_group, $attributes );
				}
			}

			// Register taxonomies.
			foreach ( $attributes as $attribute_name => $attribute_args ) {
				if ( isset( $attribute_args['edit_field']['options'] ) && ! isset( $attribute_args['edit_field']['_external'] ) ) {
					$taxonomy_name = hp\prefix( $model . '_' . $attribute_name );
					$taxonomy_type = hp\prefix( $model );

					// Set taxonomy arguments.
					$taxonomy_args = [
						'hierarchical'       => true,
						'public'             => false,
						'show_ui'            => true,
						'meta_box_cb'        => false,
						'show_in_quick_edit' => false,
						'show_in_menu'       => false,
						'rewrite'            => false,

						'labels'             => [
							'name'          => $attribute_args['label'],
							'singular_name' => $attribute_args['label'],
							'add_new_item'  => esc_html__( 'Add Option', 'hivepress' ),
							'edit_item'     => esc_html__( 'Edit Option', 'hivepress' ),
							'update_item'   => esc_html__( 'Update Option', 'hivepress' ),
							'parent_item'   => esc_html__( 'Parent Option', 'hivepress' ),
							'search_items'  => esc_html__( 'Search Options', 'hivepress' ),
							'not_found'     => esc_html__( 'No options found.', 'hivepress' ),
						],
					];

					if ( 'user' === $model ) {
						$taxonomy_type = hp\prefix( 'vendor' );
					} elseif ( hp\get_array_value( $attribute_args, 'public' ) ) {
						$taxonomy_args['public'] = true;

						$taxonomy_args['rewrite'] = [
							'slug' => hp\sanitize_slug( $model . '_' . $attribute_name ),
						];
					}

					if ( ! taxonomy_exists( $taxonomy_name ) ) {

						// Register taxonomy.
						register_taxonomy( $taxonomy_name, $taxonomy_type, $taxonomy_args );

						// Set term arguments.
						$term_args = [
							'taxonomy'   => $taxonomy_name,
							'fields'     => 'count',
							'hide_empty' => false,
						];

						// Get cache group.
						$cache_group = hivepress()->model->get_cache_group( 'term', $taxonomy_name );

						// Get term count.
						$term_count = hivepress()->cache->get_cache( $term_args, $cache_group );

						if ( is_null( $term_count ) ) {
							$term_count = get_terms( $term_args );

							hivepress()->cache->set_cache( $term_args, $cache_group, $term_count );
						}

						if ( $term_count > 100 ) {
							foreach ( $field_contexts as $field_context ) {

								// Set field source.
								$attributes[ $attribute_name ][ $field_context . '_field' ]['source'] = hivepress()->router->get_url( 'attribute_options_resource', [ 'attribute_id' => $attribute_args['id'] ] );
							}
						}
					}
				}
			}

			/**
			 * Filters model attributes. By adding a new attribute to the filtered array, you can add a new field to the model forms and meta boxes, enable the search filter and a sorting option for it. The dynamic part of the hook refers to the model name (e.g. `listing`, `vendor`).
			 *
			 * @hook hivepress/v1/models/{model_name}/attributes
			 * @param {array} $attributes Attribute configurations.
			 * @return {array} Attribute configurations.
			 */
			$attributes = apply_filters( 'hivepress/v1/models/' . $model . '/attributes', $attributes );

			// Set categories.
			foreach ( $attributes as $attribute_name => $attribute_args ) {
				$taxonomy_name = hp\prefix( $category_model );

				if ( ! taxonomy_exists( $taxonomy_name ) ) {
					continue;
				}

				$category_ids = hp\get_array_value( $attribute_args, 'categories' );

				if ( ! $category_ids ) {
					continue;
				}

				foreach ( $category_ids as $category_id ) {

					// @todo cache category IDs.
					$category_ids = array_merge( $category_ids, get_term_children( $category_id, $taxonomy_name ) );
				}

				$attributes[ $attribute_name ]['categories'] = array_unique( $category_ids );
			}

			// Set attributes.
			$this->attributes[ $model ] = array_map(
				function( $args ) {
					if ( ! isset( $args['label'] ) && isset( $args['edit_field']['label'] ) ) {
						$args['label'] = $args['edit_field']['label'];
					}

					return array_merge(
						[
							'id'             => null,
							'label'          => '',
							'display_areas'  => [],
							'display_format' => '%value%',
							'protected'      => false,
							'editable'       => false,
							'synced'         => false,
							'moderated'      => false,
							'indexable'      => false,
							'searchable'     => false,
							'filterable'     => false,
							'sortable'       => false,
							'related'        => false,
							'categories'     => [],
							'edit_field'     => [],
							'search_field'   => [],
						],
						$args
					);
				},
				$attributes
			);
		}
	}

	/**
	 * Imports attribute.
	 *
	 * @param array $term Term arguments.
	 * @return array
	 */
	public function import_attribute( $term ) {
		if ( strpos( $term['taxonomy'], 'hp_' ) === 0 && ! taxonomy_exists( $term['taxonomy'] ) ) {
			register_taxonomy( $term['taxonomy'], hp\prefix( $this->get_models( 'post' ) ) );
		}

		return $term;
	}

	/**
	 * Updates attribute.
	 *
	 * @param int $attribute_id Attribute ID.
	 */
	public function update_attribute( $attribute_id ) {

		// Check permissions.
		if ( ! current_user_can( 'edit_post', $attribute_id ) ) {
			return;
		}

		// Check action.
		if ( hp\get_array_value( $_POST, 'action' ) !== 'editpost' ) {
			return;
		}

		// Check autosave.
		if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
			return;
		}

		// Check post ID.
		if ( get_the_ID() !== $attribute_id ) {
			return;
		}

		// Refresh permalinks.
		hivepress()->router->flush_rewrite_rules();
	}

	/**
	 * Adds block settings.
	 *
	 * @param array $meta Block meta.
	 * @return array
	 */
	public function add_block_settings( $meta ) {

		// Get attributes.
		$attributes = $this->get_attributes( substr( $meta['name'], 0, -1 ), [] );

		// Add settings.
		foreach ( $attributes as $attribute_name => $attribute_args ) {

			// @todo remove type check when supported.
			if ( ( $attribute_args['searchable'] || $attribute_args['filterable'] ) && ! isset( $meta['settings'][ $attribute_name ] ) && in_array( $attribute_args['search_field']['type'], [ 'text', 'number', 'select', 'checkbox' ] ) ) {
				$meta['settings'][ $attribute_name ] = $attribute_args['search_field'];
			}
		}

		return $meta;
	}

	/**
	 * Adds field settings.
	 *
	 * @param array $meta_box Meta box arguments.
	 * @return array
	 */
	public function add_field_settings( $meta_box ) {

		// Get model.
		$model = $meta_box['model'];

		// Get field context.
		$field_context = hp\get_last_array_value( explode( '_', $meta_box['name'] ) );

		// Get field prefix.
		$field_prefix = 'display' === $field_context ? 'edit' : $field_context;

		// Get field type.
		$field_type = sanitize_key( get_post_meta( get_the_ID(), hp\prefix( $field_prefix . '_field_type' ), true ) );

		if ( $field_type ) {

			// Get field settings.
			$field_settings = hp\call_class_method( '\HivePress\Fields\\' . $field_type, 'get_meta', [ 'settings' ] );

			// Add field settings.
			if ( $field_settings ) {
				foreach ( $field_settings as $field_name => $field ) {
					if ( ( ! $field->get_arg( '_context' ) && 'display' !== $field_context ) || $field->get_arg( '_context' ) === $field_context ) {

						// Get field arguments.
						$field_args = $field->get_args();

						// Set field arguments.
						if ( 'options' === $field_name ) {
							if ( get_post_status() === 'publish' ) {
								$field_args = array_merge(
									$field_args,
									[
										'caption'      => esc_html__( 'Edit Options', 'hivepress' ),
										'type'         => 'button',
										'display_type' => 'button',

										'attributes'   => [
											'data-component' => 'link',
											'data-url' => esc_url(
												admin_url(
													'edit-tags.php?' . http_build_query(
														[
															'taxonomy' => hp\prefix( $model . '_' . $this->get_attribute_name( get_post_field( 'post_name' ), $model ) ),
															'post_type' => hp\prefix( 'user' === $model ? 'vendor' : $model ),
														]
													)
												)
											),
										],
									]
								);
							} else {
								$field_args = array_merge(
									$field_args,
									[
										'disabled'     => true,
										'display_type' => 'hidden',
									]
								);
							}
						}

						if ( 'required' !== $field_name ) {
							$field_args['_order'] = hp\get_array_value( $field_args, '_order', 10 ) + 100;
						}

						// Add field.
						$meta_box['fields'][ $field_prefix . '_field_' . $field_name ] = $field_args;
					}
				}

				if ( 'edit' === $field_context ) {

					// @todo replace temporary fix.
					$meta_box['fields'][ $field_prefix . '_field_description' ] = [
						'label'      => hivepress()->translator->get_string( 'description' ),
						'type'       => 'textarea',
						'max_length' => 2048,
						'html'       => true,
						'_order'     => 120,
					];
				} elseif ( 'search' === $field_context && in_array( $field_type, [ 'select', 'number', 'date', 'date_range' ], true ) ) {
					$meta_box['fields']['searchable'] = [
						'label'   => esc_html_x( 'Searchable', 'attribute', 'hivepress' ),
						'caption' => esc_html__( 'Display in the search form', 'hivepress' ),
						'type'    => 'checkbox',
						'_order'  => 5,
					];
				} elseif ( 'display' === $field_context && isset( $field_settings['options'] ) && 'user' !== $model ) {
					$meta_box['fields']['public'] = [
						'label'       => esc_html__( 'Pages', 'hivepress' ),
						'caption'     => esc_html__( 'Create a page for each option', 'hivepress' ),
						'description' => esc_html__( 'Check this option to enable a category-like page for each attribute option.', 'hivepress' ),
						'type'        => 'checkbox',
						'_order'      => 1,
					];
				}
			}
		}

		if ( 'edit' === $field_context ) {

			// Get field name.
			$field_name = get_post_field( 'post_name' );

			if ( ! $field_name || preg_match( '/^[a-z]{1}[a-z0-9_-]*$/', $field_name ) ) {

				// Set field arguments.
				$field_args = [
					'label'       => esc_html__( 'Field Name', 'hivepress' ),
					'description' => esc_html__( 'Set the field name used for storing the attribute values.', 'hivepress' ) . ' ' . esc_html__( 'Use lowercase letters, numbers, and underscores only.', 'hivepress' ),
					'type'        => 'text',
					'pattern'     => '[a-z]{1}[a-z0-9_]*',
					'max_length'  => 64,
					'required'    => true,
					'_alias'      => 'post_name',
					'_order'      => 99,
				];

				if ( get_post_status() === 'publish' ) {
					$field_args['readonly'] = true;
				}

				// Add field.
				$meta_box['fields'][ $field_prefix . '_field_name' ] = $field_args;
			}

			// @todo replace temporary fix.
			if ( 'attachment_upload' === $field_type ) {
				unset( $meta_box['fields']['moderated'] );
			}
		}

		return $meta_box;
	}

	/**
	 * Adds admin fields.
	 *
	 * @param array $meta_box Meta box arguments.
	 * @return array
	 */
	public function add_admin_fields( $meta_box ) {

		// Get model.
		$model = $meta_box['model'];

		// Get category IDs.
		$category_ids = $this->get_category_ids( $model, get_the_ID() );

		// Add fields.
		foreach ( $this->get_attributes( $model, $category_ids ) as $attribute_name => $attribute ) {
			if ( ! $attribute['protected'] && ! isset( $meta_box['fields'][ $attribute_name ] ) ) {
				$meta_box['fields'][ $attribute_name ] = $attribute['edit_field'];
			}
		}

		return $meta_box;
	}

	/**
	 * Adds model fields.
	 *
	 * @param array  $fields Model fields.
	 * @param object $object Model object.
	 * @return array
	 */
	public function add_model_fields( $fields, $object ) {

		// Get model.
		$model = $object::_get_meta( 'name' );

		// Get category model.
		$category_model = $this->get_category_model( $model );

		// Get category IDs.
		$category_ids = null;

		if ( 'user' !== $model ) {
			$category_ids = null;

			if ( $object->get_id() ) {
				$category_ids = hivepress()->cache->get_post_cache( $object->get_id(), [ 'fields' => 'ids' ], 'models/' . $category_model );
			}

			if ( is_null( $category_ids ) ) {
				$category_ids = $this->get_category_ids( $model, $object );

				if ( $object->get_id() && is_array( $category_ids ) && count( $category_ids ) <= 100 ) {
					hivepress()->cache->set_post_cache( $object->get_id(), [ 'fields' => 'ids' ], 'models/' . $category_model, $category_ids );
				}
			}

			// Make category required.
			if ( isset( $fields['categories'] ) && $this->requires_category_model( $model ) ) {
				$fields['categories']['required'] = true;
			}
		}

		// Get attributes.
		$attributes = $this->get_attributes( $model, $category_ids );

		foreach ( $attributes as $attribute_name => $attribute ) {
			if ( ! isset( $fields[ $attribute_name ] ) ) {

				// Get field arguments.
				$field_args = array_merge(
					$attribute['edit_field'],
					[
						'display_template' => $attribute['display_format'],
						'_display_areas'   => $attribute['display_areas'],
					]
				);

				// Set field context.
				if ( $attribute['display_areas'] ) {
					$field_args['context'][ $model ] = $object;
				}

				// Set required flag.
				if ( ! $attribute['editable'] ) {
					$field_args['required'] = false;
				}

				// Set indexable flag.
				if ( $attribute['indexable'] ) {
					$field_args['_indexable'] = true;
				}

				// Set field relation.
				if ( ! isset( $field_args['options'] ) || 'user' === $model ) {
					$field_args['_external'] = true;
				} elseif ( ! isset( $field_args['_external'] ) ) {
					$field_args = array_merge(
						$field_args,
						[
							'_model'    => $category_model,
							'_alias'    => hp\prefix( $model . '_' . $attribute_name ),
							'_relation' => 'many_to_many',
						]
					);
				}

				// Add field.
				$fields[ $attribute_name ] = $field_args;
			}
		}

		// Add snippet field.
		if ( 'user' !== $model ) {
			$fields['snippet'] = [
				'type'       => 'textarea',
				'max_length' => 10240,
				'html'       => true,
				'_alias'     => 'post_excerpt',
			];
		}

		return $fields;
	}

	/**
	 * Updates model search snippet.
	 *
	 * @param int    $model_id Model ID.
	 * @param object $model Model object.
	 */
	public function update_model_snippet( $model_id, $model ) {

		// Remove action.
		remove_action( 'hivepress/v1/models/' . $model::_get_meta( 'name' ) . '/update', [ $this, 'update_model_snippet' ], 100 );

		// Get snippet.
		$snippet = '';

		foreach ( $model->_get_fields() as $field ) {
			if ( $field->get_arg( '_indexable' ) && ! is_null( $field->get_value() ) ) {
				if ( $field->get_label() ) {
					$snippet .= $field->get_label() . ': ';
				}

				$snippet .= $field->get_display_value() . '; ';
			}
		}

		if ( $model::_get_meta( 'name' ) === 'listing' && $model->get_vendor__id() ) {
			$snippet .= hivepress()->translator->get_string( 'vendor' ) . ': ' . $model->get_vendor__name() . '; ';
		}

		$snippet = ltrim( rtrim( $snippet, '; ' ) . '.', '.' );

		// Update snippet.
		if ( $model->get_snippet() !== $snippet ) {
			$model->set_snippet( $snippet )->save_snippet();
		}
	}

	/**
	 * Adds register fields.
	 *
	 * @param array $form Form arguments.
	 * @return array
	 */
	public function add_register_fields( $form ) {
		foreach ( $this->get_attributes( 'user' ) as $attribute_name => $attribute ) {

			// Get required flag.
			$required = hp\get_array_value( $attribute['edit_field'], 'required' );

			if ( $attribute['editable'] && $required && 'attachment_upload' !== $attribute['edit_field']['type'] && ! isset( $form['fields'][ $attribute_name ] ) ) {

				// Add field.
				$form['fields'][ $attribute_name ] = $attribute['edit_field'];
			}
		}

		return $form;
	}

	/**
	 * Adds edit fields.
	 *
	 * @param array  $form_args Form arguments.
	 * @param object $form Form object.
	 * @return array
	 */
	public function add_edit_fields( $form_args, $form ) {

		// Get model.
		$model = $form::get_meta( 'model' );

		// Get category IDs.
		$category_ids = $this->get_category_ids( $model, $form->get_model() );

		// Get attributes.
		$attributes = $this->get_attributes( $model, $category_ids );

		foreach ( $attributes as $attribute_name => $attribute ) {
			if ( $attribute['editable'] && ! isset( $form_args['fields'][ $attribute_name ] ) ) {

				// Get field arguments.
				$field_args = $attribute['edit_field'];

				if ( $attribute['moderated'] && $model . '_update' === $form::get_meta( 'name' ) ) {
					$field_args = hp\merge_arrays(
						$field_args,
						[
							'statuses'   => [ 'moderated' => esc_html_x( 'requires review', 'field', 'hivepress' ) ],
							'_moderated' => true,
						]
					);
				}

				// Add field.
				$form_args['fields'][ $attribute_name ] = $field_args;
			}
		}

		return $form_args;
	}

	/**
	 * Adds submit fields.
	 *
	 * @param array  $form_args Form arguments.
	 * @param object $form Form object.
	 * @return array
	 */
	public function add_submit_fields( $form_args, $form ) {
		if ( $this->requires_category_model( $form::get_meta( 'model' ) ) ) {
			$form_args['fields']['categories'] = [
				'multiple'   => false,
				'required'   => true,
				'_order'     => 5,

				'attributes' => [
					'data-multistep' => 'true',
					'data-render'    => hivepress()->router->get_url( 'form_resource', [ 'form_name' => $form::get_meta( 'name' ) ] ),
				],
			];
		}

		return $form_args;
	}

	/**
	 * Adds search fields.
	 *
	 * @param array  $form_args Form arguments.
	 * @param object $form Form object.
	 * @return array
	 */
	public function add_search_fields( $form_args, $form ) {

		// Get form context.
		$form_context = hp\get_last_array_value( explode( '_', $form::get_meta( 'name' ) ) );

		// Get model.
		$model = $form::get_meta( 'model' );

		// Get category ID.
		$category_id = $this->get_category_id( $model );

		if ( ! empty( $form_args['values']['_category'] ) ) {
			$category_id = absint( $form_args['values']['_category'] );
		}

		// Get attributes.
		$attributes = $this->get_attributes( $model, (array) $category_id );

		foreach ( $attributes as $attribute_name => $attribute ) {
			if ( ! isset( $form_args['fields'][ $attribute_name ] ) && ( ( ( $attribute['searchable'] || $attribute['filterable'] ) && in_array( $form_context, [ 'sort', 'filter' ], true ) ) || ( $attribute['searchable'] && 'search' === $form_context ) ) ) {

				// Get field arguments.
				$field_args = hp\merge_arrays(
					$attribute['search_field'],
					[
						'statuses' => [ 'optional' => null ],
					]
				);

				if ( 'sort' === $form_context || ( ! $attribute['filterable'] && 'filter' === $form_context ) ) {
					$field_args['display_type'] = 'hidden';
				}

				// Add field.
				$form_args['fields'][ $attribute_name ] = $field_args;
			}
		}

		// Set default fields.
		$default_fields = (array) get_option( hp\prefix( $model . '_search_fields' ), [ 'keyword' ] );

		if ( ! in_array( 'keyword', $default_fields, true ) && 'search' === $form_context ) {
			$form_args['fields']['s']['display_type'] = 'hidden';
		}

		if ( in_array( 'category', $default_fields, true ) ) {
			if ( 'search' === $form_context ) {
				$form_args['fields']['_category']['display_type'] = 'select';
				$form_args['fields']['_category']['_order']       = 30;
			} elseif ( 'filter' === $form_context ) {
				$form_args['fields']['_category']['display_type'] = 'hidden';
			}
		}

		return $form_args;
	}

	/**
	 * Adds sort options.
	 *
	 * @param array  $form_args Form arguments.
	 * @param object $form Form object.
	 * @return array
	 */
	public function add_sort_options( $form_args, $form ) {

		// Get model.
		$model = $form::get_meta( 'model' );

		// Get category ID.
		$category_id = $this->get_category_id( $model );

		// Get attributes.
		$attributes = $this->get_attributes( $model, (array) $category_id );

		// Add attribute options.
		$options = [];

		foreach ( $attributes as $attribute_name => $attribute ) {
			if ( $attribute['sortable'] ) {

				// Get sort order.
				$order = hp\call_class_method( '\HivePress\Fields\\' . $attribute['edit_field']['type'], 'get_meta', [ 'sortable' ] );

				if ( $order ) {

					// Get option label.
					$label = $attribute['edit_field']['label'];

					// Add option.
					if ( is_bool( $order ) ) {
						$options[ $attribute_name . '__asc' ]  = sprintf( '%s &uarr;', $label );
						$options[ $attribute_name . '__desc' ] = sprintf( '%s &darr;', $label );
					} else {
						$options[ $attribute_name . '__' . strtolower( $order ) ] = $label;
					}
				}
			}
		}

		// Set options.
		$form_args['fields']['_sort']['options'] = array_merge( $form_args['fields']['_sort']['options'], $options );

		// Set default option.
		$default = get_option( hp\prefix( $model . '_default_order' ) );

		if ( $default ) {
			$form_args['fields']['_sort']['default'] = $default;
		}

		return $form_args;
	}

	/**
	 * Adds category options.
	 *
	 * @param array  $form_args Form arguments.
	 * @param object $form Form object.
	 * @return array
	 */
	public function add_category_options( $form_args, $form ) {

		// Get model.
		$model = $form::get_meta( 'model' );

		// Get category model.
		$category_model = $this->get_category_model( $model );

		// Check category option.
		if ( ! taxonomy_exists( hp\prefix( $category_model ) ) || in_array( 'category', (array) get_option( hp\prefix( $model . '_search_fields' ) ), true ) ) {
			return $form_args;
		}

		// Get category ID.
		$category_id = $this->get_category_id( $model );

		// Set query arguments.
		$query_args = [
			'taxonomy'   => hp\prefix( $category_model ),
			'parent'     => $category_id,
			'fields'     => 'ids',
			'hide_empty' => false,
		];

		// Get cached options.
		$options = hivepress()->cache->get_cache(
			array_merge(
				$query_args,
				[
					'fields' => 'names',
					'format' => 'tree',
				]
			),
			'models/' . $category_model
		);

		if ( is_null( $options ) ) {
			$options = [];

			// Get category IDs.
			$category_ids = get_terms( $query_args );

			if ( $category_id ) {
				$category_ids = array_merge( $category_ids, [ $category_id ], get_ancestors( $category_id, hp\prefix( $category_model ), 'taxonomy' ) );
			}

			if ( $category_ids ) {

				// Set custom order.
				if ( get_terms(
					array_merge(
						$query_args,
						[
							'number'     => 1,
							'meta_query' => [
								[
									'key'     => 'hp_sort_order',
									'value'   => 0,
									'compare' => '>',
									'type'    => 'NUMERIC',
								],
							],
						]
					)
				) ) {

					// Get categories.
					$categories = get_terms(
						array_merge(
							$query_args,
							[
								'parent'   => '',
								'fields'   => 'all',
								'include'  => $category_ids,
								'meta_key' => 'hp_sort_order',
								'orderby'  => 'meta_value_num',
							]
						)
					);
				} else {

					// Get categories.
					$categories = get_terms(
						array_merge(
							$query_args,
							[
								'parent'  => '',
								'fields'  => 'all',
								'include' => $category_ids,
								'orderby' => 'name',
							]
						)
					);
				}

				// Add options.
				$options[0] = [
					'label'  => hivepress()->translator->get_string( 'all_categories' ),
					'parent' => null,
				];

				foreach ( $categories as $category ) {
					$options[ $category->term_id ] = [
						'label'  => $category->name,
						'parent' => $category->parent,
					];
				}
			}

			// Cache options.
			if ( count( $options ) <= 1000 ) {
				hivepress()->cache->set_cache(
					array_merge(
						$query_args,
						[
							'fields' => 'names',
							'format' => 'tree',
						]
					),
					'models/' . $category_model,
					$options
				);
			}
		}

		if ( $options ) {

			// Set options.
			$form_args['fields']['_category']['options'] = $options;
		} else {

			// Remove field.
			unset( $form_args['fields']['_category'] );
		}

		return $form_args;
	}

	/**
	 * Sets category field value.
	 *
	 * @param array  $form_args Form arguments.
	 * @param object $form Form object.
	 * @return array
	 */
	public function set_category_value( $form_args, $form ) {

		// Get model.
		$model = $form::get_meta( 'model' );

		// Set category ID.
		if ( isset( $form_args['fields']['_category'] ) ) {
			$form_args['fields']['_category']['default'] = 0;
		}

		// Get term ID.
		$term_id = $this->get_term_id( $model );

		if ( $term_id ) {

			// Get field name.
			$field_name = substr( get_queried_object()->taxonomy, strlen( hp\prefix( $model . '_' ) ) );

			if ( 'category' === $field_name ) {
				$field_name = '_' . $field_name;
			}

			// Set field value.
			if ( isset( $form_args['fields'][ $field_name ] ) ) {
				$form_args['fields'][ $field_name ]['default'] = $term_id;
			}
		}

		return $form_args;
	}

	/**
	 * Gets number range field values.
	 *
	 * @param string $model Model name.
	 * @param string $field Field name.
	 * @return array
	 */
	protected function get_range_values( $model, $field ) {

		// Set query arguments.
		$query_args = [
			'post_type'        => hp\prefix( $model ),
			'post_status'      => 'publish',
			'meta_key'         => hp\prefix( $field ),
			'orderby'          => 'meta_value_num',
			'posts_per_page'   => 1,
			'fields'           => 'ids',
			'suppress_filters' => ! hivepress()->translator->is_multilingual(),
		];

		// Get cached range.
		$range = hivepress()->cache->get_cache(
			array_merge(
				$query_args,
				[
					'fields' => 'meta_values',
					'format' => 'range',
				]
			),
			'models/' . $model
		);

		if ( is_null( $range ) ) {

			// Get range.
			$range = [
				floor(
					floatval(
						get_post_meta(
							hp\get_first_array_value( get_posts( array_merge( $query_args, [ 'order' => 'ASC' ] ) ) ),
							hp\prefix( $field ),
							true
						)
					)
				),
				ceil(
					floatval(
						get_post_meta(
							hp\get_first_array_value( get_posts( array_merge( $query_args, [ 'order' => 'DESC' ] ) ) ),
							hp\prefix( $field ),
							true
						)
					)
				),
			];

			// Cache range.
			hivepress()->cache->set_cache(
				array_merge(
					$query_args,
					[
						'fields' => 'meta_values',
						'format' => 'range',
					]
				),
				'models/' . $model,
				$range
			);
		}

		return $range;
	}

	/**
	 * Sets number range field values.
	 *
	 * @param array  $form_args Form arguments.
	 * @param object $form Form object.
	 * @return array
	 */
	public function set_range_values( $form_args, $form ) {

		// Get model.
		$model = $form::get_meta( 'model' );

		// Filter fields.
		foreach ( $form_args['fields'] as $field_name => $field_args ) {
			if ( 'number_range' === $field_args['type'] ) {

				// Get range values.
				$range = $this->get_range_values( $model, $field_name );

				// Set range values.
				if ( hp\get_first_array_value( $range ) !== hp\get_last_array_value( $range ) ) {
					$form_args['fields'][ $field_name ]['min_value'] = hp\get_first_array_value( $range );
					$form_args['fields'][ $field_name ]['max_value'] = hp\get_last_array_value( $range );
				}
			}
		}

		return $form_args;
	}

	/**
	 * Adds settings.
	 *
	 * @param array $settings Settings configuration.
	 * @return array
	 */
	public function add_settings( $settings ) {
		foreach ( $this->models as $model_name => $model_args ) {

			// Check model.
			if ( ! $model_args['searchable'] ) {
				continue;
			}

			// Create sort form.
			$sort_form = hp\create_class_instance( '\HivePress\Forms\\' . $model_name . '_sort' );

			if ( ! $sort_form ) {
				continue;
			}

			// Get sort field.
			$sort_field = hp\get_array_value( $sort_form->get_fields(), '_sort' );

			if ( ! $sort_field ) {
				continue;
			}

			// Get sort options.
			$sort_options = $sort_field->get_arg( 'options' );

			if ( ! $sort_options ) {
				continue;
			}

			// Add field.
			if ( isset( $settings[ $model_name . 's' ]['sections']['search'] ) ) {
				$settings[ $model_name . 's' ]['sections']['search']['fields'][ $model_name . '_default_order' ] = [
					'label'   => hivepress()->translator->get_string( 'default_sorting' ),
					'type'    => 'select',
					'options' => $sort_options,
					'_order'  => 20,
				];
			}
		}

		return $settings;
	}

	/**
	 * Adds meta boxes.
	 *
	 * @param array $meta_boxes Meta box arguments.
	 * @return array
	 */
	public function add_meta_boxes( $meta_boxes ) {

		// Set defaults.
		$meta_box_args = [
			'attributes'        => [
				'title'  => esc_html__( 'Attributes', 'hivepress' ),
				'fields' => [],
			],

			'attribute_edit'    => [
				'title'  => hivepress()->translator->get_string( 'editing' ),

				'fields' => [
					'editable'        => [
						'label'   => esc_html_x( 'Editable', 'attribute', 'hivepress' ),
						'caption' => esc_html__( 'Allow front-end editing', 'hivepress' ),
						'type'    => 'checkbox',
						'_order'  => 1,
					],

					'edit_field_type' => [
						'label'       => esc_html__( 'Field Type', 'hivepress' ),
						'type'        => 'select',
						'options'     => 'fields',
						'option_args' => [ 'editable' => true ],
						'required'    => true,
						'_order'      => 100,
					],
				],
			],

			'attribute_search'  => [
				'title'  => hivepress()->translator->get_string( 'search_noun' ),

				'fields' => [
					'filterable'        => [
						'label'   => esc_html_x( 'Filterable', 'attribute', 'hivepress' ),
						'caption' => esc_html__( 'Display in the filter form', 'hivepress' ),
						'type'    => 'checkbox',
						'_order'  => 10,
					],

					'indexable'         => [
						'label'   => esc_html_x( 'Indexable', 'attribute', 'hivepress' ),
						'caption' => esc_html__( 'Include in keyword search', 'hivepress' ),
						'type'    => 'checkbox',
						'_order'  => 15,
					],

					'sortable'          => [
						'label'   => esc_html_x( 'Sortable', 'attribute', 'hivepress' ),
						'caption' => esc_html__( 'Display as a sorting option', 'hivepress' ),
						'type'    => 'checkbox',
						'_order'  => 20,
					],

					'search_field_type' => [
						'label'       => esc_html__( 'Field Type', 'hivepress' ),
						'type'        => 'select',
						'options'     => 'fields',
						'option_args' => [ 'filterable' => true ],
						'_order'      => 100,
					],
				],
			],

			'attribute_display' => [
				'title'  => hivepress()->translator->get_string( 'display_noun' ),

				'fields' => [
					'display_areas'  => [
						'label'       => esc_html__( 'Areas', 'hivepress' ),
						'description' => esc_html__( 'Choose the template areas where you want to display this attribute.', 'hivepress' ),
						'type'        => 'select',
						'multiple'    => true,
						'_order'      => 10,

						'options'     => [
							'view_block_primary'   => esc_html__( 'Block', 'hivepress' ) . ' ' . sprintf( '(%s)', esc_html_x( 'primary', 'area', 'hivepress' ) ),
							'view_block_secondary' => esc_html__( 'Block', 'hivepress' ) . ' ' . sprintf( '(%s)', esc_html_x( 'secondary', 'area', 'hivepress' ) ),
							'view_block_ternary'   => esc_html__( 'Block', 'hivepress' ) . ' ' . sprintf( '(%s)', esc_html_x( 'ternary', 'area', 'hivepress' ) ),
							'view_page_primary'    => esc_html__( 'Page', 'hivepress' ) . ' ' . sprintf( '(%s)', esc_html_x( 'primary', 'area', 'hivepress' ) ),
							'view_page_secondary'  => esc_html__( 'Page', 'hivepress' ) . ' ' . sprintf( '(%s)', esc_html_x( 'secondary', 'area', 'hivepress' ) ),
							'view_page_ternary'    => esc_html__( 'Page', 'hivepress' ) . ' ' . sprintf( '(%s)', esc_html_x( 'ternary', 'area', 'hivepress' ) ),
						],
					],

					'icon'           => [
						'label'       => esc_html__( 'Icon', 'hivepress' ),
						'description' => esc_html__( 'Choose an icon for this attribute to include in the display format with the %icon% token.', 'hivepress' ),
						'type'        => 'select',
						'options'     => 'icons',
						'_parent'     => 'display_areas[]',
						'_order'      => 20,
					],

					'display_format' => [
						'label'       => esc_html__( 'Format', 'hivepress' ),
						'description' => esc_html__( 'Set the attribute display format.', 'hivepress' ) . ' ' . sprintf( hivepress()->translator->get_string( 'these_tokens_are_available' ), '%label%, %icon%, %value%, %parent_value%' ),
						'type'        => 'textarea',
						'max_length'  => 2048,
						'default'     => '%value%',
						'html'        => true,
						'_parent'     => 'display_areas[]',
						'_order'      => 100,
					],
				],
			],

			'option_settings'   => [
				'screen' => [],

				'fields' => [
					'icon'       => [
						'label'       => esc_html__( 'Icon', 'hivepress' ),
						'description' => esc_html__( 'Choose an icon to include in the display format with the %icon% token.', 'hivepress' ),
						'type'        => 'select',
						'options'     => 'icons',
						'_order'      => 10,
					],

					'sort_order' => [
						'label'     => esc_html_x( 'Order', 'sort priority', 'hivepress' ),
						'type'      => 'number',
						'min_value' => 0,
						'default'   => 0,
						'required'  => true,
						'_order'    => 20,
					],
				],
			],
		];

		// Add meta boxes.
		foreach ( $this->get_models() as $model ) {
			foreach ( $meta_box_args as $meta_box_name => $meta_box ) {
				if ( strpos( $meta_box_name, 'attribute' ) === 0 ) {

					// Skip adding meta box.
					if ( 'attribute_search' === $meta_box_name && ! $this->models[ $model ]['searchable'] ) {
						continue;
					}

					// Set screen and model.
					$meta_box['model'] = $model;

					if ( 'attributes' === $meta_box_name ) {
						$meta_box['screen'] = $model;

						if ( ! isset( $this->models[ $model ]['category_model'] ) && $this->requires_category_model( $model ) ) {
							$meta_box['fields']['categories'] = [
								'label'       => hivepress()->translator->get_string( 'category' ),
								'type'        => 'select',
								'options'     => 'terms',
								'option_args' => [ 'taxonomy' => hp\prefix( $model . '_category' ) ],
								'required'    => true,
								'_order'      => 1,

								'attributes'  => [
									'data-multistep' => 'true',
									'data-render'    => hivepress()->router->get_url( 'meta_box_resource', [ 'meta_box_name' => $model . '_' . $meta_box_name ] ),
								],
							];
						}
					} else {
						$meta_box['screen'] = $model . '_attribute';

						foreach ( [ 'edit', 'search' ] as $field_context ) {
							if ( isset( $meta_box['fields'][ $field_context . '_field_type' ] ) ) {
								$meta_box['fields'][ $field_context . '_field_type' ]['attributes'] = [
									'data-render' => hivepress()->router->get_url( 'meta_box_resource', [ 'meta_box_name' => $model . '_' . $meta_box_name ] ),
								];
							}
						}
					}

					// @todo replace temporary fix.
					if ( 'listing' === $model ) {
						if ( 'attribute_edit' === $meta_box_name ) {
							$meta_box['fields']['synced'] = [
								'label'       => esc_html_x( 'Synced', 'attribute', 'hivepress' ),
								'caption'     => esc_html__( 'Sync with the vendor field', 'hivepress' ),
								'description' => esc_html__( 'Check this option to sync the value with the vendor field of the same name.', 'hivepress' ),
								'type'        => 'checkbox',
								'_order'      => 20,
							];

							$meta_box['fields']['moderated'] = [
								'label'   => esc_html_x( 'Moderated', 'attribute', 'hivepress' ),
								'caption' => esc_html__( 'Manually approve changes', 'hivepress' ),
								'type'    => 'checkbox',
								'_parent' => 'editable',
								'_order'  => 30,
							];
						} elseif ( 'attribute_display' === $meta_box_name ) {
							$meta_box['fields']['related'] = [
								'label'       => esc_html_x( 'Related', 'attribute', 'hivepress' ),
								'caption'     => esc_html__( 'Include in related criteria', 'hivepress' ),
								'description' => esc_html__( 'Check this option if you want this attribute to determine related listings.', 'hivepress' ),
								'type'        => 'checkbox',
								'_order'      => 5,
							];
						}
					}
				} elseif ( 'option_settings' === $meta_box_name ) {
					foreach ( $this->attributes[ $model ] as $attribute_name => $attribute ) {
						if ( isset( $attribute['edit_field']['options'] ) && ! isset( $attribute['edit_field']['_external'] ) ) {

							// Get screen.
							$screen = $model . '_' . $attribute_name;

							// Add screen.
							if ( ! post_type_exists( hp\prefix( $screen ) ) ) {
								$meta_box['screen'][] = $screen;
							}
						}
					}
				}

				// Add meta box.
				$meta_boxes[ $model . '_' . $meta_box_name ] = $meta_box;
			}
		}

		return $meta_boxes;
	}

	/**
	 * Removes meta boxes.
	 */
	public function remove_meta_boxes() {
		global $pagenow;

		if ( in_array( $pagenow, [ 'post.php', 'post-new.php' ], true ) ) {

			// Get post type.
			$post_type = get_post_type();

			if ( in_array( $post_type, hp\prefix( $this->get_models( 'post' ) ), true ) ) {

				// Get model.
				$model = hp\unprefix( $post_type );

				// Get category IDs.
				$category_ids = $this->get_category_ids( $model, get_the_ID() );

				// Get attributes.
				$attributes = $this->get_attributes( $model, $category_ids );

				// Remove meta boxes.
				remove_meta_box( 'commentsdiv', $post_type, 'normal' );
				remove_meta_box( hp\prefix( $model . '_categorydiv' ), $post_type, 'side' );

				foreach ( $this->attributes[ $model ] as $attribute_name => $attribute ) {
					if ( ! isset( $attributes[ $attribute_name ] ) && isset( $attribute['edit_field']['options'] ) && ! isset( $attribute['edit_field']['_external'] ) ) {
						remove_meta_box( hp\prefix( $model . '_' . $attribute_name . 'div' ), $post_type, 'side' );
					}
				}
			}
		}
	}

	/**
	 * Redirects archive page.
	 */
	public function redirect_archive_page() {

		// Check page.
		if ( ! is_post_type_archive( hp\prefix( $this->get_models( 'post' ) ) ) || is_search() || is_feed() ) {
			return;
		}

		// Get model.
		$model = hp\unprefix( get_post_type() );

		// Get page ID.
		$page_id = absint( get_option( hp\prefix( 'page_' . $model . 's' ) ) );

		if ( ! $page_id ) {
			return;
		}

		// Get page slug.
		$page_slug = get_post_field( 'post_name', $page_id );

		if ( hp\get_array_value( get_queried_object()->rewrite, 'slug' ) === $page_slug ) {
			return;
		}

		// Redirect page.
		wp_safe_redirect( get_permalink( $page_id ) );

		exit;
	}

	/**
	 * Sets related query.
	 *
	 * @param object $query Query object.
	 * @param object $model Model object.
	 */
	public function set_related_query( $query, $model ) {

		// Exclude ID.
		$query->filter( [ 'id__not_in' => [ $model->get_id() ] ] );

		// Set categories.
		if ( $model->get_categories__id() && in_array( 'category', (array) get_option( 'hp_' . $model::_get_meta( 'name' ) . '_related_criteria', [ 'category' ] ) ) ) {
			$query->filter( [ 'categories__in' => $model->get_categories__id() ] );
		}

		// Get attribute fields.
		$fields = $this->get_attribute_fields( $model::_get_meta( 'name' ), $model->serialize(), 'edit' );

		if ( $fields ) {

			// Set query arguments.
			$query->set_args( $this->get_query_args( $fields ) );
		}
	}

	/**
	 * Sets search query.
	 *
	 * @param WP_Query $query Search query.
	 */
	public function set_search_query( $query ) {

		// Check query.
		if ( ! $query->is_main_query() && ! $query->get( 'hp_main' ) ) {
			return;
		}

		// Get model.
		$model = null;

		foreach ( $this->get_models( 'post' ) as $model_name ) {
			if ( is_post_type_archive( hp\prefix( $model_name ) ) || $this->get_term_id( $model_name ) ) {
				$model = $model_name;

				break;
			}
		}

		if ( empty( $model ) ) {
			return;
		}

		if ( $query->is_main_query() ) {

			// Set post type.
			$query->set( 'post_type', hp\prefix( $model ) );

			// Set status.
			$query->set( 'post_status', 'publish' );

			// Paginate results.
			$query->set( 'posts_per_page', absint( get_option( hp\prefix( $model . 's_per_page' ) ) ) );
		}

		// Get meta and taxonomy queries.
		$meta_query = array_filter( (array) $query->get( 'meta_query' ) );
		$tax_query  = array_filter( (array) $query->get( 'tax_query' ) );

		// Get category ID.
		$category_id = null;

		if ( $query->is_main_query() ) {
			$category_id = $this->get_category_id( $model );

			if ( $category_id ) {

				// Set category ID.
				$tax_query[] = [
					[
						'taxonomy' => hp\prefix( $this->get_category_model( $model ) ),
						'terms'    => $category_id,
					],
				];
			} else {

				// Set term ID.
				$term_id = $this->get_term_id( $model );

				if ( $term_id ) {
					$tax_query[] = [
						[
							'taxonomy' => get_queried_object()->taxonomy,
							'terms'    => $term_id,
						],
					];
				}
			}
		}

		// Get attributes.
		$attributes = $this->get_attributes( $model, (array) $category_id );

		// Sort results.
		if ( $query->is_main_query() || $query->get( 'hp_archive' ) ) {

			// Create sort form.
			$sort_form = hp\create_class_instance( '\HivePress\Forms\\' . $model . '_sort' );

			if ( $sort_form ) {

				// Set form values.
				$sort_form->set_values( $_GET, true );

				if ( $sort_form->validate() ) {

					// Get sort parameter.
					$sort_param = $sort_form->get_value( '_sort' );

					if ( 'title' === $sort_param ) {

						// Set sort order.
						$query->set( 'orderby', 'title' );
						$query->set( 'order', 'ASC' );
					} else {

						// Get sort order.
						$sort_order = 'ASC';

						if ( strpos( $sort_param, '__' ) ) {
							list($sort_param, $sort_order) = explode( '__', $sort_param );
						}

						// Get sort attribute.
						$sort_attribute = hp\get_array_value( $attributes, $sort_param );

						if ( $sort_attribute && $sort_attribute['sortable'] ) {

							// Get sort field.
							$sort_field = hp\create_class_instance( '\HivePress\Fields\\' . $sort_attribute['edit_field']['type'], [ $sort_attribute['edit_field'] ] );

							if ( $sort_field && $sort_field::get_meta( 'sortable' ) ) {

								// Update field filter.
								$sort_field->update_filter( true );

								// Set sort filter.
								$sort_filter = [
									'key'  => hp\prefix( $sort_param ),
									'type' => hp\get_array_value( $sort_field->get_filter(), 'type' ),
								];

								// Add meta clause.
								$meta_query[] = [
									'relation' => 'OR',

									$sort_param . '__order' => array_merge(
										$sort_filter,
										[
											'compare' => 'NOT EXISTS',
										]
									),

									array_merge(
										$sort_filter,
										[
											'compare' => 'EXISTS',
										]
									),
								];

								// Set sort order.
								$query->set( 'orderby', $sort_param . '__order' );
								$query->set( 'order', strtoupper( $sort_order ) );
							}
						}
					}
				}
			}
		}

		// Filter results.
		if ( $query->is_search() ) {

			// Get attribute fields.
			$attribute_fields = $this->get_attribute_fields( $model, $_GET );

			// Get query arguments.
			$query_args = $this->get_query_args( $attribute_fields );

			$meta_query = array_merge( $meta_query, $query_args['meta_query'] );
			$tax_query  = array_merge( $tax_query, $query_args['tax_query'] );
		}

		// Set meta and taxonomy queries.
		$query->set( 'meta_query', $meta_query );
		$query->set( 'tax_query', $tax_query );

		/**
		 * Fires when models are being queried (e.g. on the archive, category, or search pages). The dynamic part of the hook refers to the model name (e.g. `listing`, `vendor`).
		 *
		 * @hook hivepress/v1/models/{model_name}/query
		 * @param {WP_Query} $query Query object.
		 */
		do_action( 'hivepress/v1/models/' . $model . '/query', $query );

		if ( $query->is_search() ) {

			/**
			 * Fires when models are being searched. The dynamic part of the hook refers to the model name (e.g. `listing`, `vendor`).
			 *
			 * @hook hivepress/v1/models/{model_name}/search
			 * @param {WP_Query} $query Query object.
			 * @param {array} $fields Search fields.
			 */
			do_action( 'hivepress/v1/models/' . $model . '/search', $query, $attribute_fields );
		}

		if ( $query->is_main_query() ) {

			// Get featured results.
			$featured_count = absint( get_option( hp\prefix( $model . 's_featured_per_page' ) ) );

			if ( $featured_count ) {

				// Get featured IDs.
				$featured_ids = get_posts(
					[
						'post_type'        => hp\prefix( $model ),
						'post_status'      => 'publish',
						's'                => $query->get( 's' ),
						'tax_query'        => $query->get( 'tax_query' ),
						'meta_query'       => $query->get( 'meta_query' ),
						'meta_key'         => 'hp_featured',
						'posts_per_page'   => $featured_count,
						'orderby'          => 'rand',
						'fields'           => 'ids',
						'suppress_filters' => false,
					]
				);

				if ( $featured_ids ) {

					// Exclude featured IDs.
					$query->set( 'post__not_in', $featured_ids );

					// Set request context.
					hivepress()->request->set_context( 'featured_ids', $featured_ids );
				}
			}
		}
	}

	/**
	 * Disables Jetpack search.
	 *
	 * @param mixed    $enabled Is search enabled?
	 * @param WP_Query $query Search query.
	 * @return bool
	 */
	public function disable_jetpack_search( $enabled, $query ) {
		if ( $query->is_main_query() && $query->is_search() && strpos( $query->get( 'post_type' ), 'hp_' ) === 0 ) {
			$enabled = false;
		}

		return $enabled;
	}
}
