<?php if (!defined('VB_ENTRY')) die('Access denied.');
/*======================================================================*\
|| #################################################################### ||
|| # vBulletin 4.2.5
|| # ---------------------------------------------------------------- # ||
|| # Copyright 2000-2017 vBulletin Solutions Inc. All Rights Reserved. ||
|| # This file may not be redistributed in whole or significant part. # ||
|| # ---------------- VBULLETIN IS NOT FREE SOFTWARE ---------------- # ||
|| #        www.vbulletin.com | www.vbulletin.com/license.html        # ||
|| #################################################################### ||
\*======================================================================*/

/**
 * Collection
 * Fetches a collection of items with the given criteria.
 *
 * Usually, if an itemid is specified for a collection then it is an array of all of
 * the itemids that should be fetched.
 *
 * Note that the INFO_ constants should use those of the child items and only add
 * more if they are applicable.
 *
 * Note: As a standard, collections should not add conditions to the query if the item
 * ids are already known; unless some property has been set to force this.
 *
 * Note: If the collection supports pagination it should set $can_paginate to true,
 * and should include SQL_CALC_FOUND_ROWS in it's fields.  It should also limit the
 * results by $start and $quantity.
 *
 * @package vBulletin
 * @author vBulletin Development Team
 * @version $Revision: 92140 $
 * @since $Date: 2016-12-30 20:26:15 -0800 (Fri, 30 Dec 2016) $
 * @copyright vBulletin Solutions Inc.
 */
abstract class vB_Collection extends vB_Model implements ArrayAccess, Iterator
{
	/*Item==========================================================================*/

	/**
	 * The package identifier of the child items.
	 *
	 * @var string
	 */
	protected $item_package = 'vB';

	/**
	 * The class identifier of the child items.
	 *
	 * @var string
	 */
	protected $item_class = 'Item';

	/**
	 * Collections mostly don't require a itemid.
	 *
	 * @var bool
	 */
	protected $allow_no_itemid = true;

	/**
	 * Whether this collection type supports pagination.
	 *
	 * @var bool
	 */
	protected $can_paginate = false;

	/**
	 * Whether to calculate pagination.
	 *
	 * @var bool
	 */
	protected $paginate = false;

	/**
	 * The page to display if paginated.
	 *
	 * @var int
	 */
	protected $page = 1;

	/**
	 * The amount of items to fetch.
	 * If 0, all items will be fetched.
	 *
	 * @var int
	 */
	protected $quantity = 20;

	/**
	 * The resolved start result index in the collection
	 *
	 * @var int
	 */
	protected $start;

	/**
	 * The resolved end result index in the collection
	 *
	 * @var int
	 */
	protected $end;



	/*Sort&Order====================================================================*/

	/**
	 * If this is true, the collection will be fetched in descending order.
	 *
	 * @var bool
	 */
	protected $descending;

	/**
	 * Field to sort by.
	 * @todo allow multiple
	 *
	 * @var mixed
	 */
	protected $sortfield;



	/*Result========================================================================*/

	/**
	 * Cached result
	 *
	 * @var array
	 */
	protected $collection = array();

	/**
	 * Field name of the primary key.
	 * This is required so that the collection can refer to a specific item when
	 * setting info.
	 *
	 * @var string
	 */
	protected $primary_key = 'itemid';

	/**
	 * The resolved total size of the collection.
	 *
	 * @var integer
	 */
	protected $total;

	/**
	 * Reference to the first item of the resolved collection.
	 *
	 * @var array
	 */
	protected $firstitem;

	/**
	 * Reference to the last item of the resolved collection.
	 *
	 * @var array
	 */
	protected $lastitem;



	/*Hooks=========================================================================*/

	/**
	 * Hook id for changing the sort column and order.
	 *
	 * @var string
	 */
	protected $sort_hook;



	/*Initialisation================================================================*/

	/**
	 * Constructs the Model.
	 * The id passed will usually be the primary key of the model data in the
	 * database but as this is model specific it can be interpreted in other ways.
	 *
	 * @param mixed $itemid					- The id of the item
	 * @param int $load_flags				- Any required info prenotification
	 */
	public function __construct($itemid = false, $load_flags = false)
	{
		if (!$this->item_package OR !$this->item_class)
		{
			throw (new vB_Exception_Model('No item type defined for collection \'' . get_class($this) . '\''));
		}

		parent::__construct($itemid, $load_flags);
	}



	/*Criteria======================================================================*/

	/**
	 * Fetches the collection array.
	 *
	 * @return array vb_Item
	 */
	public function getCollection()
	{
		$this->Load();

		return $this->collection;
	}


	/**
	 * Sets an existing array of items as the collection
	 *
	 * @param $items							- Array of itemid => item
	 * @param $load_flags						- INFO already loaded for the items
	 */
	public function setCollection(array $items, $load_flags)
	{
		foreach ($items AS $item)
		{
			if (!$this->validCollectionItem($item))
			{
				throw (new vB_Exception_Model('Trying to add an item of the wrong type (\'' . get_class($item) . '\' to a collection (\'' . get_class($this) . '\')'));
			}
		}

		$this->itemid = array_keys($items);
		$this->collection = $items;
		$this->removeFilters();
		$this->loaded_info = $load_flags;
	}


	/**
	 * Checks if an item of a valid type to be in the collection.
	 *
	 * @param $item
	 * @return bool
	 */
	protected function validCollectionItem($item)
	{
		if (!($item instanceof vB_Item))
		{
			return false;
		}

		return true;
	}


	/**
	 * Sets the item ids for the collection.
	 * The item id's are arbitrary but must be understood by the item classes that
	 * the collection creates.
	 *
	 * Child classes can extend this for validation.
	 *
	 * @param mixed $itemids
	 */
	public function setItemId($itemid)
	{
		if (!$itemid)
		{
			return;
		}

		$itemid = (array)$itemid;

		foreach($itemid AS &$id)
		{
			$id = intval($id);
		}

		if ($this->itemid !== $itemid)
		{
			$this->itemid = $itemid;
			$this->removeFilters();
		}
	}


	/**
	 * Adds a single itemid to the collection itemid.
	 *
	 * @param $itemid
	 */
	public function addItemId($itemid)
	{
		if (!$itemid)
		{
			return;
		}

		$itemid = (array)$itemid;

		if ($this->itemid)
		{
			$itemid = array_merge($this->itemid, $itemid);
		}

		$this->setItemId($itemid);
	}


	/**
	 * Removes any filters.
	 */
	public function removeFilters(){}



	/*PaginationCriteria============================================================*/

	/**
	 * Set the page to fetch,
	 *
	 * @param int $page
	 */
	public function paginatePage($page)
	{
		if (!$this->can_paginate)
		{
			throw (new vB_Exception_Model('Setting page for collection \'' . get_class($this) . '\' but the collection does not support pagination'));
		}

		$page = intval($page);
		$this->paginate = true;

		if ($this->page != $page)
		{
			$this->page = $page;
			$this->reset();
		}
	}


	/**
	 * Sets the maximum amount of results to fetch.
	 *
	 * @param int $quantity
	 */
	public function paginateQuantity($quantity)
	{
		if (!$this->can_paginate)
		{
			throw (new vB_Exception_Model('Setting quantity for collection \'' . get_class($this) . '\' but the collection does not support pagination'));
		}

		$this->paginate = true;

		if ($this->quantity != $quantity)
		{
			$this->quantity = $quantity;
			$this->Reset();
		}
	}


	/**
	 * Enables or disables pagination.
	 *
	 * @param bool $paginate
	 */
	public function paginate($paginate = true)
	{
		if (!$this->can_paginate)
		{
			throw (new vB_Exception_Model('Setting pagination for collection \'' . get_class($this) . '\' but the collection does not support pagination'));
		}

		$this->paginate = $paginate;
	}


	/**
	 * Convenience method for setting all pagination criteria with a single call.
	 *
	 * @param int $page							- The page offset to use
	 * @param int $quantity						- The amount of items to show per page
	 */
	public function paginateCriteria($page, $quantity)
	{
		$this->paginatePage($page);
		$this->paginateQuantity($quantity);
	}



	/*LoadInfo======================================================================*/

	/**
	 * Populates the model info.
	 *
	 * @param int $info_flags					- Additional info to load
	 */
	protected function Load($info_flags = false)
	{
		// Resolve the start index to fetch
		if ($this->paginate AND !$this->start)
		{
			// actual start index
			$this->start = $this->paginate ? (max(($this->page - 1), 0) * $this->quantity) : 0;
		}

		return parent::Load($info_flags);
	}


	/**
	 * Builds or updates the collection from a db result.
	 * If child classes need to apply loaded info to items that are not part of the
	 * item model properties then they will have to extend or override this method.
	 *
	 * @param resource $result					- The result resource of the query
	 * @param int $load_query					- The query that the result is from
	 * @return bool								- Success
	 */
	protected function applyLoad($result, $load_query)
	{
		// Calculate the newly loaded info from required info and the loaded query
		$loaded = ($this->required_info & $this->query_info[$load_query]);

		if (self::QUERY_BASIC == $load_query)
		{
			// resolve total
			if ($this->paginate)
			{
				$sql = "SELECT FOUND_ROWS() AS qty";
				$record = $this->important ? vB::$db->query_first($sql) : vB::$db->query_first_slave($sql);
				$this->total = $record['qty'];
			}
			else
			{
				list($this->total) = vB::$db->num_rows($result);
			}
		}

		// Build collection, get the first item and flag ignored messages
		while ($iteminfo = vB::$db->fetch_array($result))
		{
			// create the collection on first load
			if ($this->requireLoad(self::INFO_BASIC))
			{
				// create the item and set the info
				if ($item = $this->createItem($iteminfo, $this->required_info))
				{
					$this->lastitem = $this->collection[$iteminfo[$this->primary_key]] = $item;

					if (!$this->firstitem)
					{
						$this->firstitem = $this->lastitem;
					}
				}
			}
			else
			{
				// set the info on existing items
				$this->setInfo($iteminfo, $loaded);
			}
		}

		// Set itemid based on resolved collection
		if (self::QUERY_BASIC == $load_query)
		{
			$this->itemid = array_keys($this->collection);
			$this->removeFilters();

			// check page is valid
			if ($this->paginate)
			{
				if ($this->start >= $this->total)
				{
					$this->page = ceil($this->total / $this->quantity);
				}
			}
			else
			{
				$this->page = 1;
			}

			// calculate end
			$this->end = $this->paginate ? (min(($this->start) + $this->quantity, $this->total)) : $this->total;

			// nudge start to user friendly range (1+)
			$this->start++;
		}

		// Mark info as loaded
		$this->loaded_info |= $loaded;

		return (!($loaded & self::INFO_BASIC) OR sizeof($this->collection));
	}


	/**
	 * Return value for no result from a query.
	 * This allows child items to set related properties or populate defaults.
	 *
	 * @param $load_query						- The query that returned no result
	 * @return bool								- Whether the model is valid
	 */
	protected function noResult($load_query)
	{
		if ((self::QUERY_BASIC == $load_query) AND $this->paginate)
		{
			list($this->total) = vB::$db->query_first("SELECT FOUND_ROWS()", DBARRAY_NUM);
		}

		return false;
	}


	/**
	 * Creates an item to add to the collection.
	 *
	 * @param array mixed $iteminfo				- The known properties of the new item
	 * @return vB_Item							- The created item
	 */
	protected function createItem($iteminfo, $load_flags = false)
	{
		if (!isset($iteminfo[$this->primary_key]))
		{
			throw (new vB_Exception_Model('No primary key property value in iteminfo for vB_Collection::createItem()'));
		}

		$item_class = $this->item_package . '_Item_' . $this->item_class;
		$item = new $item_class($iteminfo[$this->primary_key]);
		$item->setInfo($iteminfo, $load_flags);

		if ($item->isValid())
		{
			return $item;
		}

		return false;
	}


	/**
	 * Sets info on a single item.
	 * If items are not indexed with $this->primary_key then the child class will
	 * have to override this method to ensure the info is assigned to the correct
	 * item.
	 *
	 * Note that the collection must be loaded before iteminfo can be set.  The
	 * collection doesn't support adding items on the fly yet (until needed).
	 *
	 * @param array mixed $iteminfo				- Property => Value
	 */
	public function setInfo($iteminfo, $load_flags = false)
	{
		if (!isset($iteminfo[$this->primary_key]))
		{
			throw (new vB_Exception_Model('No primary key property value in iteminfo for vB_Collection::setInfo()'));
		}

		// If the item exists, set the info
		if (isset($this->collection[$iteminfo[$this->primary_key]]))
		{
			$itemid = $iteminfo[$this->primary_key];

			// ensure we don't set the primary key
			unset($iteminfo[$this->primary_key]);

			$this->collection[$itemid]->setInfo($iteminfo, $load_flags);
		}
		else
		{
			throw (new vB_Exception_Model('Setting collection item iteminfo for an item that is not in the collection: \'' . htmlspecialchars_uni($iteminfo[$this->primary_key]) . '\''));
		}
	}


	/**
	 * Only fetches the total size of the collection.
	 *
	 * @return int
	 */
	public function getTotal()
	{
		$this->Load();

		return $this->total;
	}


	/**
	 * Fetches the first item of the collection.
	 *
	 * @return vB_Item							- The first item
	 */
	public function getFirstItem()
	{
		$this->Load();

		return ($this->firstitem ? $this->firstitem : false);
	}


	/**
	 * Fetches the last item of the collection.
	 *
	 * @return vB_Item							- The last item
	 */
	public function getLastItem()
	{
		$this->Load();

		return ($this->lastitem ? $this->lastitem : false);
	}


	/**
	 * Unsets the fetched collection.
	 * Child classes may want to extend this to reset any other data.
	 */
	public function reset($reset_item = false)
	{
		$this->loaded_info = 0;
		$this->collection = array();
		unset($this->total, $this->firstitem, $this->lastitem);

		if ($reset_item)
		{
			unset($this->itemid);
		}
	}



	/*SQL===========================================================================*/

	/**
	 * Sets the hook for modifying the sort fields and order.
	 *
	 * @param string $name						- The name of the hook to use
	 */
	public function setSortHook($name)
	{
		$this->sort_hook = $name;
	}



	/*Sort&Order====================================================================*/

	/**
	 * Sets the order to ASC or DESC.
	 *
	 * @param bool $descending
	 */
	public function orderDescending($descending = true)
	{
		if ($this->descending != $descending)
		{
			$this->descending = $descending;
			$this->Reset();
		}
	}


	/**
	 * Sets the sort field.
	 * Child classes should validate the field and prefix.* the appropriate table alias.
	 *
	 * @param string $field						- The client id of the field to sort by
	 */
	public function orderSortField($field)
	{
		$this->orderSortFieldHook($field);
	}


	/**
	 * Allows hooks to evaluate sort field.
	 *
	 * @param string $field						- The client id of the field to sort by
	 */
	protected function orderSortFieldHook($field)
	{
		$resolved_table = false;
		$resolved_field = false;

		($hook = vBulletinHook::fetch_hook($this->sort_field_hook)) ? eval($hook) : false;

		if ($resolved_table AND $resolved_field)
		{
			$sortfield = $resolved_table . '.' . $resolved_field;

			if ($sortfield != $this->sortfield)
			{
				$this->sortfield = $sortfield;
				$this->Reset();
			}
		}
	}



	/*PaginationResults=============================================================*/

	/**
	 * Fetches the start index of the collection.
	 *
	 * @return int
	 */
	public function getStart()
	{
		$this->Load();

		return $this->start;
	}


	/**
	 * Fetches the end index of the collection.
	 *
	 * @return int
	 */
	public function getEnd()
	{
		$this->Load();

		return $this->end;
	}


	/**
	 * Fetches how many items were fetched.
	 *
	 * @return int
	 */
	public function getShown()
	{
		$this->Load();

		return sizeof($this->collection);
	}


	/**
	 * Fetches the counts of the fetched collection.
	 *
	 * @return array							- Assoc array of count information
	 */
	public function getCounts()
	{
		if (! $this->isValid())
		{
			return false;
		}

		return array(
				'start' => $this->getStart(),
				'end' => $this->getEnd(),
				'shown' => $this->getShown(),
				'total' => $this->getTotal(),
				'page' => $this->getPageNumber());
	}


	/**
	 * Gets the resolved results page
	 *
	 * @return int
	 */
	public function getPageNumber()
	{
		$this->Load();

		return $this->page;
	}


	/**
	 * Gets the set per page quantity
	 *
	 * @return int
	 */
	public function getQuantity()
	{
		return $this->quantity;
	}



	/*Iterator======================================================================*/

	/**
	 * Returns current item.
	 *
	 * @return vB_Item
	 */
	public function current()
	{
		// Ensure collection is loaded
		$this->Load();
		return current($this->collection);
	}


	/**
	 * Returns key of current element.
	 *
	 * @return string | int
	 */
	public function key()
	{
		return key($this->collection);
	}


	/**
	 * Advances pointer to next element and returns it's value.
	 *
	 * @return vB_Item
	 */
	public function next()
	{
		return next($this->collection);
	}


	/**
	 * Returns pointer to the beginning.
	 */
	public function rewind()
	{
		reset($this->collection);
	}


	/**
	 * Checks if there is a current element.
	 *
	 * @return bool
	 */
	public function valid()
	{
		return $this->Load() AND (bool)current($this->collection);
	}



	/*ArrayAccess===================================================================*/

	/**
	 * Checks that a key exists.
	 *
	 * @param mixed $offset
	 * @return bool
	 */
	public function offsetExists($offset)
	{
		if (!$this->loaded_info)
		{
			$this->Load();
		}

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


	/**
	 * Gets the value for the given key if it exists.
	 * If it doesn't exist then the same warning is triggered that php gives as
	 * standard.
	 *
	 * @param mixed $offset
	 * @return mixed
	 */
	public function offsetGet($offset)
	{
		if (!$this->loaded_info)
		{
			$this->Load();
		}

		if (!isset($this->collection[$offset]))
		{
			trigger_error('Undefined index: ' . $offset, E_USER_WARNING);
		}
		else
		{
			return $this->collection[$offset];
		}
	}


	/**
	 * Sets the value for the given key.
	 *
	 * @param mixed $offset
	 * @param mixed $value
	 */
	public function offsetSet($offset, $value)
	{
		throw (new vB_Exception_Model('Cannot set vB_Collection items directly'));
	}


	/**
	 * Unsets the element for the given key
	 *
	 * @param mixed $offset
	 */
	public function offsetUnset($offset)
	{
		throw (new vB_Exception_Model('Cannot unset vB_Collection items directly'));
	}
}

/*======================================================================*\
|| ####################################################################
|| # $Revision: 92140 $
|| # $Date: 2016-12-30 20:26:15 -0800 (Fri, 30 Dec 2016) $
|| ####################################################################
\*======================================================================*/
