<?php

namespace Planaday\Utility\DataGrouping;

use ArrayAccess;
use Countable;
use Iterator;
use Traversable;

/**
 * @template T
 * @implements Iterator<int, T>
 * @implements ArrayAccess<int, T>
 * @implements LazyFetchModifiable<int, T>
 */
class PagedResponse implements ArrayAccess, Iterator, Countable, LazyFetchModifiable {
	protected int $currentPosition = 0;
	protected int $fetchPosition = 0;
	protected int $totalPossibleEntries = 0;
	protected int $currentEntryCount = 0;
	protected int $pageSize = 0;
	protected mixed $loadValuesCallable = null;

	/** @var ArrayAccess<int, callable(T):(T|null)>&Traversable<int, callable(T):(T|null)>&Countable */
	protected ArrayAccess&Traversable&Countable $fetchActions;

	/**
	 * @param ArrayAccess<int, T>&Traversable<int, T>&Countable $pageEntries
	 */
	public function __construct(
		protected ArrayAccess&Traversable&Countable $pageEntries = new Collection()
	) {
		$this->fetchPosition = count( $pageEntries );
		$this->pageSize      = count( $pageEntries );
		$this->totalPossibleEntries = count( $pageEntries );
		$this->currentEntryCount = count( $pageEntries );
		$this->fetchActions = new Collection();
	}

	public function setPageSize(int $pageSize):self {
		$this->pageSize = $pageSize;
		return $this;
	}

	public function setFetchPosition( int $fetchPosition ): self {
		$this->fetchPosition = $fetchPosition;

		return $this;
	}

	public function setTotalPossibleEntries( int $totalPossibleEntries ): self {
		$this->totalPossibleEntries = $totalPossibleEntries;

		return $this;
	}

	/**
	 * @param callable(int $offset, int $limit):(T[]|null) $loadValuesCallable
	 *
	 * @return $this
	 */
	public function setLoadValuesCallable( callable $loadValuesCallable ): self {
		$this->loadValuesCallable = $loadValuesCallable;

		return $this;
	}

	/**
	 * @param callable(T):(T|null) $fetchAction
	 *
	 * @return $this
	 */
	public function addFetchAction( callable $fetchAction, bool $applyRetroactively = false): self {
		$this->fetchActions[] = $fetchAction;

		if ( $applyRetroactively ) {
			foreach ( $this->pageEntries as $key => $value ) {
				$newValue = $fetchAction( $value );

				if ( $newValue === null ) {
					unset( $this->pageEntries[$key] );
					continue;
				}

				$this->pageEntries[$key] = $newValue;
			}
		}
		return $this;
	}

	/**
	 * @param int $index
	 *
	 * @return T|null
	 */
	public function getIndex(int $index): mixed {
		return $this->pageEntries[ $index ] ?? null;
	}

	/**
	 * @return T|null
	 */
	public function current(): mixed {
		$this->valid();

		return $this->pageEntries[$this->currentPosition] ?? null;
	}

	public function next(): void {
		++$this->currentPosition;
	}

	public function key(): int {
		return $this->currentPosition;
	}

	public function valid(): bool {
		if (isset($this->pageEntries[$this->currentPosition])) return true;

		while ($this->canLoadNextValues() && !isset($this->pageEntries[$this->currentPosition])) {
			$this->loadNextValues();
		}

		return isset($this->pageEntries[$this->currentPosition]);
	}

	public function rewind(): void {
		$this->currentPosition = 0;
	}

	public function totalCount():int {
		return $this->totalPossibleEntries;
	}

	public function offsetExists( mixed $offset ): bool {
		if (isset($this->pageEntries[$offset])) return true;

		if ( $offset < 0 || $offset > $this->totalPossibleEntries) return false;

		while ($this->canLoadNextValues() && !isset($this->pageEntries[$offset])) {
			$this->loadNextValues();
		}

		return isset($this->pageEntries[$offset]);
	}

	public function offsetGet( mixed $offset ): mixed {
		$this->offsetExists($offset);
		return $this->pageEntries[$offset] ?? null;
	}

	public function offsetSet( mixed $offset, mixed $value ): void {
		$this->pageEntries[$offset] = $value;
	}

	public function offsetUnset( mixed $offset ): void {
		unset( $this->pageEntries[$offset] );
	}

	public function count(): int {
		return count( $this->pageEntries );
	}

	public function getLoadedValues(): ArrayAccess&Traversable&Countable {
		return $this->pageEntries;
	}

	protected function canLoadNextValues(): bool {
		return !($this->fetchPosition >= $this->totalPossibleEntries || $this->loadValuesCallable === null);
	}

	protected function loadNextValues(): void {
		if ( ! $this->canLoadNextValues() ) {
			return;
		}

		foreach ( ($this->loadValuesCallable)($this->fetchPosition, $this->pageSize) as $value ) {
			$value = $this->filterValue( $value );

			if ( $value !== null ) {
				$this->pageEntries->append($value);
			}
		}

		$this->fetchPosition += $this->pageSize;
		$this->currentEntryCount = count( $this->pageEntries );
	}

	/**
	 * @param T $value
	 *
	 * @return T|null
	 */
	protected function filterValue($value): mixed {
		foreach ( $this->fetchActions as $fetchAction ) {
			$value = $fetchAction( $value );

			if ( $value === null ) {
				return null;
			}
		}

		return $value;
	}
}