<?php

class ET_Theme_Builder_Request {
	/**
	 * Type constants.
	 */
	const TYPE_FRONT_PAGE        = 'front_page';
	const TYPE_404               = '404';
	const TYPE_SEARCH            = 'search';
	const TYPE_SINGULAR          = 'singular';
	const TYPE_POST_TYPE_ARCHIVE = 'archive';
	const TYPE_TERM              = 'term';
	const TYPE_AUTHOR            = 'author';
	const TYPE_DATE              = 'date';

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

	/**
	 * Requested object subtype.
	 *
	 * @var string
	 */
	protected $subtype = '';

	/**
	 * Requested object id.
	 *
	 * @var integer
	 */
	protected $id = 0;

	/**
	 * Create a request object based on the current request.
	 *
	 * @since 4.0
	 *
	 * @return ET_Theme_Builder_Request|null
	 */
	public static function from_current() {
		$is_extra_layout_home = 'layout' === get_option( 'show_on_front' ) && is_home();

		if ( $is_extra_layout_home || is_front_page() ) {
			return new self( self::TYPE_FRONT_PAGE, '', get_queried_object_id() );
		}

		if ( is_404() ) {
			return new self( self::TYPE_404, '', 0 );
		}

		if ( is_search() ) {
			return new self( self::TYPE_SEARCH, '', 0 );
		}

		$id             = get_queried_object_id();
		$object         = get_queried_object();
		$page_for_posts = (int) get_option( 'page_for_posts' );
		$is_blog_page   = 0 !== $page_for_posts && is_page( $page_for_posts );

		if ( is_singular() ) {
			return new self( self::TYPE_SINGULAR, get_post_type( $id ), $id );
		}

		if ( $is_blog_page || is_home() ) {
			return new self( self::TYPE_POST_TYPE_ARCHIVE, 'post', $id );
		}

		if ( is_category() || is_tag() || is_tax() ) {
			return new self( self::TYPE_TERM, $object->taxonomy, $id );
		}

		if ( is_post_type_archive() ) {
			return new self( self::TYPE_POST_TYPE_ARCHIVE, $object->name, $id );
		}

		if ( is_author() ) {
			return new self( self::TYPE_AUTHOR, '', $id );
		}

		if ( is_date() ) {
			return new self( self::TYPE_DATE, '', 0 );
		}

		return null;
	}

	/**
	 * Create a request object based on a post id.
	 *
	 * @since 4.0
	 *
	 * @param integer $post_id
	 *
	 * @return ET_Theme_Builder_Request
	 */
	public static function from_post( $post_id ) {
		if ( (int) get_option( 'page_on_front' ) === $post_id ) {
			return new self( self::TYPE_FRONT_PAGE, '', $post_id );
		}

		if ( (int) get_option( 'page_for_posts' ) === $post_id ) {
			return new self( self::TYPE_POST_TYPE_ARCHIVE, 'post', $post_id );
		}

		return new self( self::TYPE_SINGULAR, get_post_type( $post_id ), $post_id );
	}

	/**
	 * Constructor.
	 *
	 * @since 4.0
	 *
	 * @param string $type
	 * @param string $subtype
	 * @param integer $id
	 */
	public function __construct( $type, $subtype, $id ) {
		$this->type    = $type;
		$this->subtype = $subtype;
		$this->id      = $id;
	}

	/**
	 * Get the requested object type.
	 *
	 * @since 4.0
	 *
	 * @return string
	 */
	public function get_type() {
		return $this->type;
	}

	/**
	 * Get the requested object subtype.
	 *
	 * @since 4.0
	 *
	 * @return string
	 */
	public function get_subtype() {
		return $this->subtype;
	}

	/**
	 * Get the requested object id.
	 *
	 * @since 4.0
	 *
	 * @return string
	 */
	public function get_id() {
		return $this->id;
	}

	/**
	 * Get the top ancestor of a setting based on its id. Takes the setting itself
	 * if it has no ancestors.
	 * Returns an empty array if the setting is not found.
	 *
	 * @since 4.0
	 *
	 * @param array $flat_settings
	 * @param string $setting_id
	 *
	 * @return array
	 */
	protected function _get_template_setting_ancestor( $flat_settings, $setting_id ) {
		$id = $setting_id;

		if ( ! isset( $flat_settings[ $id ] ) ) {
			// If the setting is not found, check if a valid parent exists.
			$parent_id = explode( ET_THEME_BUILDER_SETTING_SEPARATOR, $id );
			array_pop( $parent_id );
			$parent_id[] = '';
			$parent_id   = implode( ET_THEME_BUILDER_SETTING_SEPARATOR, $parent_id );
			$id          = $parent_id;
		}

		if ( ! isset( $flat_settings[ $id ] ) ) {
			// The setting is still not found - bail.
			return array();
		}

		return $flat_settings[ $id ];
	}

	/**
	 * Get $a or $b depending on which template setting has a higher priority.
	 * Handles cases such as category settings with equal priority but in a ancestor-child relationship.
	 * Returns an empty string if neither setting is found.
	 *
	 * @since 4.0
	 *
	 * @param array $flat_settings
	 * @param string $a
	 * @param string $b
	 *
	 * @return string
	 */
	protected function _get_higher_priority_template_setting( $flat_settings, $a, $b ) {
		$map        = array_flip( array_keys( $flat_settings ) );
		$a_ancestor = $this->_get_template_setting_ancestor( $flat_settings, $a );
		$b_ancestor = $this->_get_template_setting_ancestor( $flat_settings, $b );
		$a_found    = ! empty( $a_ancestor );
		$b_found    = ! empty( $b_ancestor );

		if ( ! $a_found || ! $b_found ) {
			if ( $a_found ) {
				return $a;
			}

			if ( $b_found ) {
				return $b;
			}

			return '';
		}

		if ( $a_ancestor['priority'] !== $b_ancestor['priority'] ) {
			// Priorities are not equal - use a simple comparison.
			return $a_ancestor['priority'] >= $b_ancestor['priority'] ? $a : $b;
		}

		if ( $a_ancestor['id'] !== $b_ancestor['id'] ) {
			// Equal priorities, but the ancestors are not the same - use the order in $flat_settings
			// so we have a deterministic result even if $a and $b are swapped.
			return $map[ $a_ancestor['id'] ] <= $map[ $b_ancestor['id'] ] ? $a : $b;
		}

		// Equal priorities, same ancestor.
		$ancestor  = $a_ancestor;
		$a_pieces  = explode( ET_THEME_BUILDER_SETTING_SEPARATOR, $a );
		$b_pieces  = explode( ET_THEME_BUILDER_SETTING_SEPARATOR, $b );
		$separator = preg_quote( ET_THEME_BUILDER_SETTING_SEPARATOR, '/' );

		// Hierarchical post types are a special case by spec since we have to take hierarchy into account.
		// Test if the ancestor matches "singular:post_type:<post_type>:children:id:".
		$id_pieces  = array( 'singular', 'post_type', '[^' . $separator . ']+', 'children', 'id', '' );
		$term_regex = '/^' . implode( $separator, $id_pieces ) . '$/';

		if ( preg_match( $term_regex, $ancestor['id'] ) && is_post_type_hierarchical( $a_pieces[2] ) ) {
			$a_post_id = (int) $a_pieces[5];
			$b_post_id = (int) $b_pieces[5];

			$a_post_ancestors = get_post_ancestors( $a_post_id );
			$b_post_ancestors = get_post_ancestors( $b_post_id );

			if ( in_array( $a_post_id, $b_post_ancestors, true ) ) {
				// $b is a child of $a so it should take priority.
				return $b;
			}

			if ( in_array( $b_post_id, $a_post_ancestors, true ) ) {
				// $a is a child of $b so it should take priority.
				return $a;
			}

			// neither $a nor $b is an ancestor to the other - continue the comparisons.
		}

		// Term archive listings are a special case by spec since we have to take hierarchy into account.
		// Test if the ancestor matches "archive:taxonomy:<taxonomy>:term:id:".
		$id_pieces  = array( 'archive', 'taxonomy', '[^' . $separator . ']+', 'term', 'id', '' );
		$term_regex = '/^' . implode( $separator, $id_pieces ) . '$/';

		if ( preg_match( $term_regex, $ancestor['id'] ) && is_taxonomy_hierarchical( $a_pieces[2] ) ) {
			$a_term_id = $a_pieces[5];
			$b_term_id = $b_pieces[5];

			if ( term_is_ancestor_of( $a_term_id, $b_term_id, $a_pieces[2] ) ) {
				// $b is a child of $a so it should take priority.
				return $b;
			}

			if ( term_is_ancestor_of( $b_term_id, $a_term_id, $a_pieces[2] ) ) {
				// $a is a child of $b so it should take priority.
				return $a;
			}

			// neither $a nor $b is an ancestor to the other - continue the comparisons.
		}

		// Find the first difference in the settings and compare it.
		// The difference should be representing an id or a slug.
		foreach ( $a_pieces as $index => $a_piece ) {
			$b_piece = $b_pieces[ $index ];

			if ( $b_piece === $a_piece ) {
				continue;
			}

			if ( is_numeric( $a_piece ) ) {
				$prioritized = (float) $a_piece <= (float) $b_piece ? $a : $b;
			} else {
				$prioritized = strcmp( $a, $b ) <= 0 ? $a : $b;
			}

			/**
			 * Filters the higher prioritized setting in a given pair that
			 * has equal built-in priority.
			 *
			 * @since 4.2
			 *
			 * @param string $prioritized_setting
			 * @param string $setting_a
			 * @param string $setting_b
			 * @param ET_Theme_Builder_Request $request
			 */
			return apply_filters( 'et_theme_builder_prioritized_template_setting', $prioritized, $a, $b, $this );
		}

		// We should only reach this point if $a and $b are equal so it doesn't
		// matter which we return.
		return $a;
	}

	/**
	 * Check if this request fulfills a template setting.
	 *
	 * @since 4.0
	 *
	 * @param array $flat_settings
	 * @param string $setting_id
	 *
	 * @return boolean
	 */
	protected function _fulfills_template_setting( $flat_settings, $setting_id ) {
		$ancestor  = $this->_get_template_setting_ancestor( $flat_settings, $setting_id );
		$fulfilled = false;

		if ( ! empty( $ancestor ) && isset( $ancestor['validate'] ) && is_callable( $ancestor['validate'] ) ) {
			// @phpcs:ignore Generic.PHP.ForbiddenFunctions.Found
			$fulfilled = call_user_func(
				$ancestor['validate'],
				$this->get_type(),
				$this->get_subtype(),
				$this->get_id(),
				explode( ET_THEME_BUILDER_SETTING_SEPARATOR, $setting_id )
			);
		}

		return $fulfilled;
	}

	/**
	 * Reduce callback for self::get_template() to get the highest priority template from all applicable ones.
	 *
	 * @since 4.0
	 *
	 * @param array $carry
	 * @param array $applicable_template
	 *
	 * @return array
	 */
	public function reduce_get_template( $carry, $applicable_template ) {
		global $__et_theme_builder_request_flat_settings;

		if ( empty( $carry ) ) {
			return $applicable_template;
		}

		$higher = $this->_get_higher_priority_template_setting(
			$__et_theme_builder_request_flat_settings,
			$carry['top_setting_id'],
			$applicable_template['top_setting_id']
		);

		return $carry['top_setting_id'] !== $higher ? $applicable_template : $carry;
	}

	/**
	 * Get the highest-priority template that should be applied for this request, if any.
	 *
	 * @since 4.0
	 *
	 * @param array $templates
	 * @param array $flat_settings
	 *
	 * @return array
	 */
	public function get_template( $templates, $flat_settings ) {
		// Use a global variable to pass data to the reduce callback as we support PHP 5.2.
		global $__et_theme_builder_request_flat_settings;

		$applicable_templates = array();

		foreach ( $templates as $template ) {
			if ( ! $template['enabled'] ) {
				continue;
			}

			foreach ( $template['exclude_from'] as $setting_id ) {
				if ( $this->_fulfills_template_setting( $flat_settings, $setting_id ) ) {
					// The setting is explicitly excluded - bail from testing the template any further.
					continue 2;
				}
			}

			$highest_priority = '';

			foreach ( $template['use_on'] as $setting_id ) {
				if ( $this->_fulfills_template_setting( $flat_settings, $setting_id ) ) {
					$highest_priority = $this->_get_higher_priority_template_setting( $flat_settings, $highest_priority, $setting_id );
				}
			}

			if ( '' !== $highest_priority ) {
				$applicable_templates[] = array(
					'template'       => $template,
					'top_setting_id' => $highest_priority,
				);
			}
		}

		$__et_theme_builder_request_flat_settings = $flat_settings;
		$applicable_template = array_reduce( $applicable_templates, array( $this, 'reduce_get_template' ), array() );
		$__et_theme_builder_request_flat_settings = array();

		if ( ! empty( $applicable_template ) ) {
			// Found the highest priority applicable template - return it.
			return $applicable_template['template'];
		}

		$default_templates = et_()->array_pick( $templates, array( 'default' => true ) );

		if ( ! empty( $default_templates ) ) {
			$default_template = $default_templates[0];

			if ( $default_template['enabled'] ) {
				// Return the first default template. We don't expect there to be multiple ones but
				// it is technically possible with direct database edits, for example.
				return $default_template;
			}
		}

		// No templates found at all - probably never used the Theme Builder.
		return array();
	}
}
