<?php namespace ProcessWire;

/**
 * ProcessWire Paginated WireArray
 * 
 * Like WireArray, but with the additional methods and properties needed for WirePaginatable interface.
 * 
 * #pw-summary PaginatedArray is a type of WireArray that supports pagination of items within it. 
 * #pw-body = 
 * Here you will see methods specific to the pagination aspects of this class only. For full details on
 * available methods outside of pagination, please see the `WireArray` class. The most common type of
 * PaginatedArray is a `PageArray`. 
 * #pw-body
 * #pw-summary-manipulation In most cases you will not need these manipulation methods as core API calls already take care of this. 
 *
 * ProcessWire 3.x, Copyright 2023 by Ryan Cramer
 * https://processwire.com
 * 
 * @method string renderPager(array $options = array()) Renders pagination, when MarkupPageArray module installed
 *
 */

class PaginatedArray extends WireArray implements WirePaginatable {

	/**
	 * Total number of items, including those here and others that aren't, but may be here in pagination.
	 *
	 * @var int
	 *
	 */
	protected $numTotal = 0;

	/**
	 * If this WireArray is a partial representation of a larger set, this will contain the max number of items allowed to be
	 * present/loaded in the WireArray at once.
	 *
	 * May vary from count() when on the last page of a result set.
	 * As a result, paging routines should refer to their own itemsPerPage rather than count().
	 * Applicable for paginated result sets. This number is not enforced for adding items to this WireArray.
	 *
	 * @var int
	 *
	 */
	protected $numLimit = 0;

	/**
	 * If this WireArray is a partial representation of a larger set, this will contain the starting result number if previous results preceded it.
	 *
	 * @var int
	 *
	 */
	protected $numStart = 0;

	/**
	 * Set the total number of items, if more than are in the WireArray.
	 * 
	 * #pw-group-manipulation
	 *
	 * @param int $total
	 * @return $this
	 *
	 */
	public function setTotal($total) {
		$this->numTotal = (int) $total;
		return $this;
	}

	/**
	 * Get the total number of items in the WireArray, including all paginations.
	 *
	 * If no limit is used, this returns total number of items currently in the WireArray,
	 * which would be the same as the `WireArray::count()` value. But when a limit is 
	 * used, this number will typically be larger than the count, as it includes all 
	 * items across all paginations, whether currently present or not. 
	 * 
	 * #pw-group-retrieval
	 *
	 * @return int Total number of items across all paginations. 
	 *
	 */
	public function getTotal() {
		$total = $this->numTotal;
		if(!$total) $total = $this->count();
		return $total;
	}

	/**
	 * Set the limit that was used in pagination.
	 * 
	 * #pw-group-manipulation
	 *
	 * @param int $numLimit
	 * @return $this
	 *
	 */
	public function setLimit($numLimit) {
		$this->numLimit = (int) $numLimit; 
		return $this; 
	}

	/**
	 * Get the limit that was used in pagination.
	 *
	 * If no limit was set, then it returns the number of items currently in this WireArray.
	 * 
	 * #pw-group-retrieval
	 *
	 * @return int
	 *
	 */
	public function getLimit() {
		return $this->numLimit; 
	}

	/**
	 * Set the starting offset number to use for pagination.
	 * 
	 * This is typically the current page number (minus 1) multiplied by limit setting. 
	 * 
	 * #pw-group-manipulation
	 *
	 * @param int $numStart
	 * @return $this
	 *
	 */
	public function setStart($numStart) {
		$this->numStart = (int) $numStart; 
		return $this; 
	}

	/**
	 * Get the starting offset number that was used for pagination.
	 * 
	 * #pw-group-retrieval
	 * 
	 * @return int
	 *
	 */
	public function getStart() {
		return $this->numStart; 
	}

	/**
	 * Does this WireArray have more than one pagination?
	 * 
	 * #pw-group-other
	 * 
	 * @return bool
	 * @since 3.0.120
	 * 
	 */
	public function hasPagination() {
		return $this->getTotal() > 0 && $this->count() < $this->getTotal();
	}

	/**
	 * Is there a next pagination containing more items in this PaginatedArray after the current one?
	 * 
	 * #pw-group-other
	 * 
	 * @return bool
	 * @since 3.0.120
	 * 
	 */
	public function hasNextPagination() {
		return $this->getStart() + $this->count() < $this->getTotal();
	}

	/**
	 * Is there a previous pagination before the current one?
	 * 
	 * #pw-group-other
	 * 
	 * @return bool
	 * @since 3.0.120
	 * 
	 */
	public function hasPrevPagination() {
		return $this->getStart() > 0;
	}

	/**
	 * Get a property of the PageArray
	 *
	 * These map to functions from the array and are here for convenience.
	 * Properties include count, total, start, limit, last, first, keys, values,
	 * These can also be accessed by direct reference, i.e. `$items->limit`. 
	 * 
	 * Please see the `WireArray::getProperty()` method for full details on 
	 * non-pagination related properties that can be retrieved through this. 
	 * 
	 * #pw-group-other
	 *
	 * @param string $property Name of property you want to retrieve, can be any of the following:
	 *   - `count` (int): Count of items currently present.
	 *   - `total` (int): Total quantity of items across all paginations.
	 *   - `start` (int): Current start index for pagination.
	 *   - `limit` (int): Current limit used for pagination.
	 *   - `last` (mixed): Last item in the WireArray.
	 *   - `first` (mixed): First item in the WireArray. 
	 * @return mixed Value of requested property. 
	 *
	 */
	public function getProperty($property) {
		static $properties = array(
			// property => method to map to
			'total' => 'getTotal',
			'start' => 'getStart',
			'limit' => 'getLimit',
		);
		if(!in_array($property, $properties)) return parent::getProperty($property);
		$func = $properties[$property];
		return $this->$func();
	}

	/**
	 * Get a "1 to 10 of 50" type of string useful for pagination headlines.
	 * 
	 * This returns a string of `1 to 10 of 30` (items) or `1 of 10` (pages) for example.
	 * 
	 * Since 3.0.108 you can optionally replace either of the arguments with an $options o
	 * array instead, and you can specify any of these options: 
	 * 
	 *  - `label` (string): Label to use for items, same as $label argument (default='').
	 *  - `zeroLabel` (string): Alternate label to use if there are zero items, since 3.0.127 (default='').
	 *  - `usePageNum` (bool): Specify true to show page numbers rather than item numbers (default=false). 
	 *  - `count` (int): Use this count rather than count in this PaginatedArray (default=-1, disabled).
	 *  - `start` (int): Use this start rather than start in this PaginatedArray (default=-1, disabled).
	 *  - `limit` (int): Use this limit rather than limit in this PaginatedArray (default=-1, disabled).
	 *  - `total` (int): Use this total rather than total in this PaginatedArray (default=-1, disabled). 
	 * 
	 * ~~~~~
	 * // Get string like "Items 1 to 25 of 500"
	 * echo $items->getPaginationString('Items');
	 * 
	 * // Get string like "Page 1 of 10"
	 * echo $items->getPaginationString('Page', true);
	 * 
	 * // Get string where you specify all options
	 * echo $items->getPaginationString(array(
	 *   'label' => 'Items',
	 *   'zeroLabel' => 'No items found', // 3.0.127+ only
	 *   'usePageNum' => false,
	 *   'count' => 10, 
	 *   'start' => 0, 
	 *   'limit' => 10, 
	 *   'total' => 100
	 * ));
	 * ~~~~~
	 * 
	 * #pw-group-other
	 * 
	 * @param string|array $label Label to identify item type, i.e. "Items" or "Page", etc. (default=empty).
	 * @param bool|array $usePageNum Specify true to show page numbers rather than item numbers (default=false). 
	 * @return string Formatted string
	 * 
	 */
	public function getPaginationString($label = '', $usePageNum = false) {
		
		$options = array(
			'label' => is_string($label) ? $label : '',
			'zeroLabel' => '',
			'usePageNum' => is_bool($usePageNum) ? $usePageNum : false,
			'count' => -1,
			'start' => -1,
			'limit' => -1, 
			'total' => -1
		);
		
		if(is_array($label)) $options = array_merge($options, $label);
		if(is_array($usePageNum)) $options = array_merge($options, $usePageNum);
		
		$label = $options['label'];
		$usePageNum = $options['usePageNum'];
		
		$count = $options['count'] > -1 ? $options['count'] : $this->count();
		$start = $options['start'] > -1 ? $options['start'] : $this->getStart();
		$limit = $options['limit'] > -1 ? $options['limit'] : $this->getLimit();
		$total = $options['total'] > -1 ? $options['total'] : $this->getTotal();
		
		if($count > $total) $total = $count;
		
		if(empty($total) && !empty($options['zeroLabel'])) {
			
			$str = $options['zeroLabel'];

		} else if($usePageNum) {
			
			$pageNum = $start ? ($start / $limit) + 1 : 1;
			$totalPages = ceil($total / $limit); 
			if(!$totalPages) $pageNum = 0;
			$str = sprintf($this->_('%1$s %2$d of %3$d'), $label, $pageNum, $totalPages); // Page quantity, i.e. Page 1 of 3
			
		} else {

			if($limit && $count > $limit) $count = $limit;
			$end = $start + $count;
			if($end > $total) $total = $end;
			$start++; // make 1 based rather than 0 based...
			if($end == 0) $start = 0; // ...unless there are no items
			$str = sprintf($this->_('%1$s %2$d to %3$d of %4$d'), $label, $start, $end, $total); // Pagination item quantity, i.e. Items 1 to 10 of 50
		}
		
		return trim($str); 
	}


	/**
	 * debugInfo PHP 5.6+ magic method
	 *
	 * @return array
	 *
	 */
	public function __debugInfo() {
		$limit = $this->getLimit();
		$count = $this->count();
		$total = $this->getTotal();
		if($limit || $total > $count) {
			$info = array(
				'count' => $count,
				'total' => $total,
				'start' => $this->getStart(),
				'limit' => $limit, 
				'pager' => $this->getPaginationString(), 
			);
			$info = array_merge($info, parent::__debugInfo());
		} else {
			$info = parent::__debugInfo();
		}
		return $info;
	}
}
