<?php

namespace Limb_Chatbot\Includes\Services;

use Countable;
use IteratorAggregate;
use ArrayIterator;
use JsonSerializable;

/**
 * A versatile collection class for managing arrays of items with utility methods.
 *
 * Implements Countable, IteratorAggregate, and JsonSerializable interfaces.
 *
 * @since 1.0.0
 */
class Collection implements Countable, IteratorAggregate, JsonSerializable {

	/**
	 * Array of items stored in the collection.
	 *
	 * @var array
	 * @since 1.0.0
	 */
	public array $items;

	/**
	 * Total count of items, can be set separately.
	 *
	 * @var null|int
	 * @since 1.0.0
	 */
	public ?int $total;

	/**
	 * Additional included properties, accessible in JSON serialization.
	 *
	 * @var array|null
	 * @since 1.0.0
	 */
	protected $included;

	/**
	 * Constructor.
	 *
	 * @param  array  $items  Initial items for the collection.
	 *
	 * @since 1.0.0
	 *
	 */
	public function __construct( array $items = [] ) {
		$this->items = $items;
		$this->set_total( count( $items ) );
	}

	/**
	 * Set the total number of items.
	 *
	 * @param  null|int  $count  Total item count.
	 *
	 * @since 1.0.0
	 *
	 */
	public function set_total( ?int $count ): void {
		$this->total = $count;
	}

	/**
	 * Get the total number of items.
	 *
	 * @return int Total item count.
	 * @since 1.0.0
	 *
	 */
	public function total(): ?int {
		return $this->total;
	}

	/**
	 * Get the first item or null if empty.
	 *
	 * @return mixed|null First item or null.
	 * @since 1.0.0
	 *
	 */
	public function first() {
		return ! $this->is_empty() ? $this->items[0] : null;
	}

	/**
	 * Check if the collection is empty.
	 *
	 * @return bool True if empty, false otherwise.
	 * @since 1.0.0
	 *
	 */
	public function is_empty(): bool {
		return empty( $this->items );
	}

	/**
	 * Get the last item or null if empty.
	 *
	 * @return mixed|null Last item or null.
	 * @since 1.0.0
	 *
	 */
	public function last() {
		return ! $this->is_empty() ? $this->items[ count( $this->items ) - 1 ] : null;
	}

	/**
	 * Get the number of items in the collection.
	 *
	 * @return int Count of items.
	 * @since 1.0.0
	 *
	 */
	public function count(): int {
		return count( $this->items );
	}

	/**
	 * Get all items as an array.
	 *
	 * @return array Items array.
	 * @since 1.0.0
	 *
	 */
	public function get(): array {
		return $this->items;
	}

	/**
	 * Iterate over items, applying a callback to each.
	 *
	 * @param  callable  $callback  Function to apply to each item.
	 *
	 * @return self Returns the collection for chaining.
	 * @since 1.0.0
	 *
	 */
	public function each( callable $callback ): self {
		foreach ( $this->items as $item ) {
			$callback( $item );
		}

		return $this;
	}

	/**
	 * Transform items using a callback, returning a new collection.
	 *
	 * @param  callable  $callback  Transformation function.
	 *
	 * @return static New transformed collection.
	 * @since 1.0.0
	 *
	 */
	public function map( callable $callback ): self {
		return new static( array_map( $callback, $this->items ) );
	}

	/**
	 * Filter items using a callback, returning a new collection.
	 *
	 * @param  callable  $callback  Filter function.
	 *
	 * @return static New filtered collection.
	 * @since 1.0.0
	 *
	 */
	public function filter( callable $callback ): self {
		return new static( array_values( array_filter( $this->items, $callback ) ) );
	}

	/**
	 * Reduce the items to a single value.
	 *
	 * @param  callable  $callback  Reduction function.
	 * @param  mixed  $initial  Initial value.
	 *
	 * @return mixed Reduced value.
	 * @since 1.0.0
	 *
	 */
	public function reduce( callable $callback, $initial ) {
		return array_reduce( $this->items, $callback, $initial );
	}

	/**
	 * Get an iterator for the items.
	 *
	 * @return ArrayIterator Iterator over items.
	 * @since 1.0.0
	 *
	 */
	public function getIterator(): ArrayIterator {
		return new ArrayIterator( $this->items );
	}

	/**
	 * Check if collection contains a given value.
	 *
	 * @param  mixed  $value  Value to check.
	 *
	 * @return bool True if contains, false otherwise.
	 * @since 1.0.0
	 *
	 */
	public function contains( $value ): bool {
		return in_array( $value, $this->items, true );
	}

	/**
	 * Get unique items as a new collection.
	 *
	 * @return static Collection with unique items.
	 * @since 1.0.0
	 *
	 */
	public function unique(): self {
		return new static( array_values( array_unique( $this->items, SORT_REGULAR ) ) );
	}

	/**
	 * Merge current items with an array of items.
	 *
	 * @param  array  $items  Items to merge.
	 *
	 * @return static New merged collection.
	 * @since 1.0.0
	 *
	 */
	public function merge( array $items ): self {
		return new static( array_merge( $this->items, $items ) );
	}

	/**
	 * Add an item to the collection optionally with a key.
	 *
	 * @param  mixed  $item  Item to add.
	 * @param  string  $key  Optional key.
	 *
	 * @return self Current collection.
	 * @since 1.0.0
	 *
	 */
	public function push_item( $item, $key = '' ): self {
		if ( $key ) {
			$this->items[ $key ] = $item;
		} else {
			$this->items[] = $item;
		}
		$this->set_total( count( $this->items ) );

		return $this;
	}

	/**
	 * Add a property to the included metadata.
	 *
	 * @param  string  $key  Property key.
	 * @param  mixed  $property  Property value.
	 *
	 * @return self Current collection.
	 * @since 1.0.0
	 *
	 */
	public function push_property( $key, $property ) {
		$this->included[ $key ] = $property;

		return $this;
	}

	/**
	 * Remove and return the first item from the collection.
	 *
	 * @return mixed|null Removed item or null if empty.
	 * @since 1.0.0
	 *
	 */
	public function shift() {
		if ( $this->is_empty() ) {
			return null;
		}
		$item = array_shift( $this->items );
		$this->set_total( count( $this->items ) );

		return $item;
	}

	/**
	 * Extract values of a given key from items.
	 *
	 * @param  string  $key  Key to pluck.
	 *
	 * @return array Array of values for the key.
	 * @since 1.0.0
	 *
	 */
	public function pluck( string $key ): array {
		$results = [];
		if ( ! empty( $this->items ) ) {
			foreach ( $this->items as $item ) {
				if ( is_array( $item ) && array_key_exists( $key, $item ) ) {
					$results[] = $item[ $key ];
				} elseif ( is_object( $item ) && isset( $item->$key ) ) {
					$results[] = $item->$key;
				}
			}
		}

		return $results;
	}

	/**
	 * Specify data for JSON serialization.
	 *
	 * @return array Data ready for json_encode.
	 * @since 1.0.0
	 *
	 */
	#[\ReturnTypeWillChange]
	public function jsonSerialize() {
		$array = [
			'items' => $this->items,
			'total' => $this->total,
		];
		if ( ! empty( $this->included ) ) {
			foreach ( $this->included as $key => $property ) {
				$array[ $key ] = $this->included[ $key ];
			}
		}

		return $array;
	}

	/**
	 * Push new items to collection
	 *
	 * @param  array  $array_merge
	 *
	 * @return void
	 * @since 1.0.0
	 */
	public function push_items( array $array_merge ) {
		$this->items = array_merge( $this->items, $array_merge );
	}

	/**
	 * Remove property from included array
	 *
	 * @param  string  $string The item which should be removed
	 *
	 * @return void
	 * @since 1.0.0
	 */
	public function remove_property( string $string ) {
		if ( ! empty( $this->included[ $string ] ) ) {
			unset( $this->included[ $string ] );
		}
	}

	/**
	 * Remove an item from the collection by key or value.
	 *
	 * @param  mixed  $identifier  The key or value to remove.
	 * @param  bool   $by_value    Whether to remove by value instead of key.
	 *
	 * @return self Current collection.
	 * @since 1.0.0
	 */
	public function remove_item( $identifier, bool $by_value = false ): self {
		if ( $this->is_empty() ) {
			return $this;
		}

		if ( $by_value ) {
			$this->items = array_values(
				array_filter( $this->items, static function ( $item ) use ( $identifier ) {
					return $item !== $identifier;
				} )
			);
		} elseif ( array_key_exists( $identifier, $this->items ) ) {
			unset( $this->items[ $identifier ] );
		}

		$this->set_total( count( $this->items ) );

		return $this;
	}

	public function get_property( string $string ) {
		return ! empty( $this->included[ $string ] ) ? $this->included[ $string ] : null;
	}
}
