<?php
declare(strict_types=1);

namespace WP_AUX\Entities\PostsQuery;

use WP_AUX\Enum\PostQueryTypeEnum;
use WP_AUX\Services\PostsQueryService;

/**
 * Manages the retrieval and querying, and provide for modification of Wordpress post queries in the child theme
 * based on specified parameters such as taxonomies, post types, and limits.
 *
 * The post query entity can be treated as an array of posts. In this way, existing posts queries
 * can be replaced with configured post query entities without updating
 * downstream uses of these queries.
 *
 * @since 1.0.0
 */
class PostsQueryEntity implements \Iterator, \Countable
{
  protected PostQueryTypeEnum $type = PostQueryTypeEnum::DEFAULT;
  protected ?array $taxonomies = [];
  protected ?array $postTypes = [];
  protected int $limit = 3;
  protected array $manuallySelectedPosts = [];

  protected int $position = 0;
  protected ?array $posts = null;

  public function __construct() {}

  /**
   * @since 1.0.0
   */
  public function getType(): PostQueryTypeEnum
  {
    return $this->type;
  }

  /**
   * Set the type of the post query
   *
   * Filters and actions are associated with the query type and provided in the PostQueryTypeEnum
   *
   * @since 1.0.0
   * @see PostQueryTypeEnum
   */
  public function setType(PostQueryTypeEnum $type): self
  {
    $this->type = $type;
    return $this;
  }

  /**
   * @since 1.0.0
   */
  public function getTaxonomies(): ?array
  {
    return $this->taxonomies;
  }

  /**
   * Define the taxonomies to match results against the current post, or specific values
   * in defined taxonomies
   *
   * @since 1.0.0
   * @see PostsQueryService::taxonomiesQueryArgs()
   */
  public function setTaxonomies(?array $taxonomies): self
  {
    $this->taxonomies = $taxonomies;
    return $this;
  }

  /**
   * @since 1.0.0
   */
  public function getPostTypes(): ?array
  {
    return $this->postTypes;
  }

  /**
   * Set post types for the query
   * @param string[] $postTypes Post type slugs
   * @since 1.0.0
   */
  public function setPostTypes(?array $postTypes): self
  {
    $this->postTypes = $postTypes;
    return $this;
  }

  /**
   * @since 1.0.0
   */
  public function getLimit(): int
  {
    return $this->limit;
  }

  /**
   * Set the maximum number of posts to return
   * @since 1.0.0
   */
  public function setLimit(int $limit): self
  {
    $this->limit = $limit;
    return $this;
  }

  /**
   * @return \WP_Post[]
   * @since 1.0.0
   */
  public function getManuallySelectedPosts(): array
  {
    return $this->manuallySelectedPosts;
  }

  /**
   * @param \WP_Post[] $manuallySelectedPosts
   * @since 1.0.0
   */
  public function setManuallySelectedPosts(array $manuallySelectedPosts): self
  {
    $this->manuallySelectedPosts = $manuallySelectedPosts;
    return $this;
  }

  /**
   * Prepares the base arguments for a posts query, including limits, ordering, exclusions,
   * post types, and taxonomy filters.
   *
   * @return array
   * @since 1.0.0
   */
  protected function baseArgs(): array
  {
    $post = get_post();

    $queryArgs = array(
      'numberposts' => $this->limit,
      'post_status' => 'publish',
      'post__not_in' => [$post->ID],
      'orderby' => 'date',
      'order' => 'DESC',
    );

    if (!empty($this->postTypes)) {
      $queryArgs['post_type'] = $this->postTypes;
    }

    $queryArgs['tax_query'] = PostsQueryService::taxonomiesQueryArgs($this->taxonomies);

    return $queryArgs;
  }

  /**
   * Retrieves posts based on the specified criteria, including manually selected posts
   * and query arguments. Configures the query parameters, applies filters, and merges
   * manually selected posts with the queried results.
   *
   * This method can be called directly to get the posts, and it is also used to get the posts for
   * accessing the entity as an array. No matter how this method is used, it sets the posts property of the entity and resets the position to 0.
   *
   * @return \WP_Post
   * @since 1.0.0
   */
  public function getPosts(): array
  {
    $limit = $this->limit;

    $manuallySelectedPosts = $this->manuallySelectedPosts;
    if ($manuallySelectedPosts) {
      array_splice($manuallySelectedPosts, $limit);
    }

    $queryArgs = $this->baseArgs();

    $argsFilter = $this->type->argumentsFilter();
    if ($argsFilter) {
      $queryArgs = apply_filters($argsFilter, $queryArgs);
    }

    if (!empty($manuallySelectedPosts)) {
      $limit -= count($manuallySelectedPosts);

      $queryArgs['post__not_in'] = array_merge(
        (array) @$queryArgs['post__not_in'],
        wp_list_pluck($manuallySelectedPosts, 'ID')
      );

      $queryArgs['numberposts'] = $limit;
    }

    if ($limit) {
      $posts = get_posts($queryArgs);
    } else {
      $posts = [];
    }

    if (!empty($manuallySelectedPosts)) {
      $posts = array_merge($manuallySelectedPosts, $posts);
    }

    $this->posts = PostsQueryService::expandRelatedPosts(
      $posts,
      $limit,
      $queryArgs
    );
    $this->position = 0;

    return $this->posts;
  }

  /**
   * Reset the loop to the beginning by setting the position pointer to 0
   *
   * Method must be provided for classes implementing <code>\Iterator</code>
   *
   * @since 1.0.0
   */
  public function rewind(): void
  {
    $this->position = 0;
  }

  /**
   * Get the current post in the loop
   *
   * Method must be provided for classes implementing <code>\Iterator</code>
   *
   * @since 1.0.0
   */
  public function current(): ?\WP_Post
  {
    return !empty($this->posts[$this->position]) ? $this->posts[$this->position] : null;
  }

  /**
   * Get the current position in the loop
   *
   * Method must be provided for classes implementing <code>\Iterator</code>
   *
   * @since 1.0.0
   */
  public function key(): int
  {
    return $this->position;
  }

  /**
   * Advance the loop one position
   *
   * Method must be provided for classes implementing <code>\Iterator</code>
   *
   * @since 1.0.0
   */
  public function next(): void {
    ++$this->position;
  }

  /**
   * Check if there is a post at the current position
   *
   * Method must be provided for classes implementing <code>\Iterator</code>
   *
   * @since 1.0.0
   */
  public function valid(): bool {
    if (is_null($this->posts)) {
      $this->getPosts();
    }

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

  /**
   * Get number of posts in the loop
   *
   * Method must be provided for classes implementing <code>\Countable</code>
   *
   * @since 1.0.0
   */
  public function count(): int
  {
    if (is_null($this->posts)) {
      $this->getPosts();
    }

    return count($this->posts);
  }

  /**
   * Create and initialize a query with provided taxonomies, post types, and limit parameters.
   *
   * @since 1.0.0
   * @see PostsQueryService::taxonomiesQueryArgs()
   */
  public static function create(
    array $taxonomies,
    ?array $postTypes = null,
    int $limit = 3
  ): static
  {
    $postsQuery = (new static())
      ->setTaxonomies($taxonomies)
      ->setPostTypes($postTypes)
      ->setLimit($limit);

    return $postsQuery;
  }
}
