<?php
namespace SabaiApps\Framework\Model;

use SabaiApps\Framework\Criteria;
use SabaiApps\Framework\DB\AbstractRowset;

/**
 * AbstractEntityRepository class for managing entity repositories.
 * Provides methods for fetching, counting, deleting, and paginating entities,
 * as well as building criteria for queries.
 */
abstract class AbstractEntityRepository {

	/**
	 * @var string Name of the entity.
	 */
	protected $_name;
	/**
	 * @var string Prefix for entity fields.
	 */
	private $_fieldPrefix;
	/**
	 * @var Model Model instance.
	 */
	protected $_model;
	/**
	 * @var array Criteria object for building queries.
	 */
	private $_criteria;

	/**
	 * Constructor
	 *
	 * @param string $name Name of the entity.
	 * @param Model  $model Model instance.
	 */
	protected function __construct( $name, Model $model ) {
		$this->_name        = $name;
		$this->_model       = $model;
		$this->_fieldPrefix = strtolower( $name ) . '_';
	}

	/**
	 * Gets the name of the entity.
	 *
	 * @return string
	 */
	public function getName() {
		return $this->_name;
	}

	/**
	 * Creates a new entity instance.
	 *
	 * @return AbstractEntity
	 */
	public function create() {
		return $this->_model->create( $this->_name );
	}

	/**
	 * Creates a criteria element and returns self for chainability.
	 *
	 * @return AbstractEntityRepository
	 */
	public function criteria() {
		$this->_criteria = $this->_model->createCriteria( $this->_name );
		return $this;
	}

	/**
	 * Calls a method defined in the criteria element and returns self for chainability.
	 *
	 * @return AbstractEntityRepository
	 */
	public function __call( $method, $args ) {
		if ( ! isset( $this->_criteria ) ) {
			$this->_criteria = $this->_model->createCriteria( $this->_name );
		}
		$this->_criteria = call_user_func_array( array( $this->_criteria, $method ), $args );

		return $this;
	}

	/**
	 * Fetches an entity by its ID.
	 *
	 * @param int  $id Entity ID.
	 * @param bool $returnCollection Whether to return a collection.
	 * @param bool $useCache Whether to use the cache.
	 * @return AbstractEntity|AbstractEntityCollection
	 */
	public function fetchById( $id, $returnCollection = false, $useCache = true ) {
		if ( $useCache
			&& ( $entity = $this->_model->isEntityCached( $this->_name, $id ) )
		) {
			return $returnCollection ? $this->createCollection( array( $entity ) ) : $entity;
		}

		$collection = $this->_getCollection( $this->_model->getGateway( $this->_name )->selectById( $id ) );
		if ( ! $returnCollection ) {
			return $collection->getFirst();
		}
		$collection->rewind();
		return $collection;
	}

	/**
	 * Fetches entities by their IDs.
	 *
	 * @param array $ids Array of entity IDs.
	 * @return AbstractEntityCollection
	 */
	public function fetchByIds( $ids ) {
		return $this->_getCollection( $this->_model->getGateway( $this->_name )->selectByIds( $ids ) );
	}

	/**
	 * Fetches entities by criteria.
	 *
	 * @param Criteria\AbstractCriteria $criteria Criteria object.
	 * @param int                       $limit Limit of entities.
	 * @param int                       $offset Offset for fetching.
	 * @param mixed                     $sort Sort field(s).
	 * @param mixed                     $order Sort order(s).
	 * @return RowsetEntityCollection
	 */
	public function fetchByCriteria( Criteria\AbstractCriteria $criteria, $limit = 0, $offset = 0, $sort = null, $order = null ) {
		return $this->_getCollection(
			$this->_model->getGateway( $this->getName() )
				->selectByCriteria( $criteria, array(), $limit, $offset, array_map( array( $this, '_prefixSort' ), (array) $sort ), (array) $order )
		);
	}

	/**
	 * Fetches entities using the current criteria or an empty criteria.
	 *
	 * @param int   $limit Limit of entities.
	 * @param int   $offset Offset for fetching.
	 * @param mixed $sort Sort field(s).
	 * @param mixed $order Sort order(s).
	 * @return RowsetEntityCollection
	 */
	public function fetch( $limit = 0, $offset = 0, $sort = null, $order = null ) {
		$criteria = ! isset( $this->_criteria ) ? new Criteria\EmptyCriteria() : $this->_criteria;
		unset( $this->_criteria );
		return $this->fetchByCriteria( $criteria, $limit, $offset, $sort, $order );
	}

	/**
	 * Fetches a single entity using the current criteria or an empty criteria.
	 *
	 * @param mixed $sort Sort field(s).
	 * @param mixed $order Sort order(s).
	 * @param int   $offset Offset for fetching.
	 * @return RowsetEntityCollection|false
	 */
	public function fetchOne( $sort = null, $order = null, $offset = 0 ) {
		return $this->fetch( 1, $offset, $sort, $order )->getNext();
	}

	/**
	 * Counts entities by criteria.
	 *
	 * @param Criteria\AbstractCriteria $criteria Criteria object.
	 * @return mixed Integer if no grouping, array otherwise.
	 */
	public function countByCriteria( Criteria\AbstractCriteria $criteria ) {
		return $this->_model->getGateway( $this->getName() )->countByCriteria( $criteria );
	}

	/**
	 * Counts entities using the current criteria or an empty criteria.
	 *
	 * @return int
	 */
	public function count() {
		$criteria = ! isset( $this->_criteria ) ? new Criteria\EmptyCriteria() : $this->_criteria;
		unset( $this->_criteria );
		return $this->countByCriteria( $criteria );
	}

	/**
	 * Deletes entities using the current criteria or an empty criteria.
	 *
	 * @return int Number of deleted entities.
	 */
	public function delete() {
		$criteria = ! isset( $this->_criteria ) ? new Criteria\EmptyCriteria() : $this->_criteria;
		unset( $this->_criteria );
		return $this->_model->getGateway( $this->getName() )->deleteByCriteria( $criteria );
	}

	/**
	 * Returns a paginator for entities by criteria.
	 *
	 * @param Criteria\AbstractCriteria $criteria Criteria object.
	 * @param int                       $perpage Number of entities per page.
	 * @param mixed                     $sort Sort field(s).
	 * @param mixed                     $order Sort order(s).
	 * @param int                       $limit Maximum number of entities.
	 * @return Paginator\CriteriaPaginator
	 */
	public function paginateByCriteria( Criteria\AbstractCriteria $criteria, $perpage = 10, $sort = null, $order = null, $limit = 0 ) {
		return new Paginator\CriteriaPaginator( $this, $criteria, $perpage, $sort, $order, $limit );
	}

	/**
	 * Returns a paginator for entities using the current criteria or an empty criteria.
	 *
	 * @param int   $perpage Number of entities per page.
	 * @param mixed $sort Sort field(s).
	 * @param mixed $order Sort order(s).
	 * @param int   $limit Maximum number of entities.
	 * @return Paginator\CriteriaPaginator
	 */
	public function paginate( $perpage = 10, $sort = null, $order = null, $limit = 0 ) {
		$criteria = ! isset( $this->_criteria ) ? new Criteria\EmptyCriteria() : $this->_criteria;
		unset( $this->_criteria );
		return $this->paginateByCriteria( $criteria, $perpage, $sort, $order, $limit );
	}

	/**
	 * Helper method for paginating entities by foreign key relationship.
	 *
	 * @param string $entityName Name of the related entity.
	 * @param string $id Related entity ID.
	 * @param int    $perpage Number of entities per page.
	 * @param mixed  $sort Sort field(s).
	 * @param mixed  $order Sort order(s).
	 * @param int    $limit Maximum number of entities.
	 * @return Paginator\EntityPaginator|Paginator\EntityCriteriaPaginator
	 */
	protected function _paginateByEntity( $entityName, $id, $perpage = 10, $sort = null, $order = null, $limit = 0 ) {
		if ( $criteria = @$this->_criteria ) {
			unset( $this->_criteria );
			return $this->_paginateByEntityAndCriteria( $entityName, $id, $criteria, $perpage, $sort, $order, $limit );
		}

		return new Paginator\EntityPaginator( $this, $entityName, $id, $perpage, $sort, $order, $limit );
	}

	/**
	 * Helper method for paginating entities by entity ID and criteria.
	 *
	 * @param string                    $entityName Name of the related entity.
	 * @param string                    $id Related entity ID.
	 * @param Criteria\AbstractCriteria $criteria Criteria object.
	 * @param int                       $perpage Number of entities per page.
	 * @param mixed                     $sort Sort field(s).
	 * @param mixed                     $order Sort order(s).
	 * @param int                       $limit Maximum number of entities.
	 * @return Paginator\EntityCriteriaPaginator
	 */
	protected function _paginateByEntityAndCriteria( $entityName, $id, Criteria\AbstractCriteria $criteria, $perpage = 10, $sort = null, $order = null, $limit = 0 ) {
		return new Paginator\EntityCriteriaPaginator( $this, $entityName, $id, $criteria, $perpage, $sort, $order, $limit );
	}

	/**
	 * Helper method for fetching entities by foreign key relationship.
	 *
	 * @param string $foreignKey Foreign key field.
	 * @param string $id Foreign key value.
	 * @param int    $limit Limit of entities.
	 * @param int    $offset Offset for fetching.
	 * @param mixed  $sort Sort field(s).
	 * @param mixed  $order Sort order(s).
	 * @return RowsetEntityCollection
	 */
	protected function _fetchByForeign( $foreignKey, $id, $limit = 0, $offset = 0, $sort = null, $order = null ) {
		if ( $criteria = @$this->_criteria ) {
			unset( $this->_criteria );
			return $this->_fetchByForeignAndCriteria( $foreignKey, $id, $criteria, $limit, $offset, $sort, $order );
		}

		$criteria = is_array( $id ) ? new Criteria\InCriteria( $foreignKey, $id ) : new Criteria\IsCriteria( $foreignKey, $id );
		return $this->fetchByCriteria( $criteria, $limit, $offset, $sort, $order );
	}

	/**
	 * Helper method for counting entities by foreign key relationship.
	 *
	 * @param string $foreignKey Foreign key field.
	 * @param string $id Foreign key value.
	 * @return int
	 */
	protected function _countByForeign( $foreignKey, $id ) {
		if ( $criteria = @$this->_criteria ) {
			unset( $this->_criteria );
			return $this->_countByForeignAndCriteria( $foreignKey, $id, $criteria );
		}

		$criteria = is_array( $id ) ? new Criteria\InCriteria( $foreignKey, $id ) : new Criteria\IsCriteria( $foreignKey, $id );
		return $this->countByCriteria( $criteria );
	}

	/**
	 * Helper method for fetching entities by foreign key relationship and criteria.
	 *
	 * @param string                    $foreignKey Foreign key field.
	 * @param string                    $id Foreign key value.
	 * @param Criteria\AbstractCriteria $criteria Criteria object.
	 * @param int                       $limit Limit of entities.
	 * @param int                       $offset Offset for fetching.
	 * @param mixed                     $sort Sort field(s).
	 * @param mixed                     $order Sort order(s).
	 * @return RowsetEntityCollection
	 */
	protected function _fetchByForeignAndCriteria( $foreignKey, $id, Criteria\AbstractCriteria $criteria, $limit = 0, $offset = 0, $sort = null, $order = null ) {
		$criterion = new Criteria\CompositeCriteria( array( $criteria ) );
		if ( is_array( $id ) ) {
			$criterion->addAnd( new Criteria\InCriteria( $foreignKey, $id ) );
		} else {
			$criterion->addAnd( new Criteria\IsCriteria( $foreignKey, $id ) );
		}

		return $this->fetchByCriteria( $criterion, $limit, $offset, $sort, $order );
	}

	/**
	 * Helper method for counting entities by foreign key relationship and criteria.
	 *
	 * @param string                    $foreignKey Foreign key field.
	 * @param string                    $id Foreign key value.
	 * @param Criteria\AbstractCriteria $criteria Criteria object.
	 * @return int
	 */
	protected function _countByForeignAndCriteria( $foreignKey, $id, Criteria\AbstractCriteria $criteria ) {
		$criterion = new Criteria\CompositeCriteria( array( $criteria ) );
		if ( is_array( $id ) ) {
			$criterion->addAnd( new Criteria\InCriteria( $foreignKey, $id ) );
		} else {
			$criterion->addAnd( new Criteria\IsCriteria( $foreignKey, $id ) );
		}

		return $this->countByCriteria( $criterion );
	}

	/**
	 * Helper method for fetching entities by association table relationship.
	 *
	 * @param string $selfTable Name of the self table.
	 * @param string $assocEntity Name of the association entity.
	 * @param string $assocTargetKey Target key in the association entity.
	 * @param string $id Target ID.
	 * @param int    $limit Limit of entities.
	 * @param int    $offset Offset for fetching.
	 * @param mixed  $sort Sort field(s).
	 * @param mixed  $order Sort order(s).
	 * @return RowsetEntityCollection
	 */
	protected function _fetchByAssoc( $selfTable, $assocEntity, $assocTargetKey, $id, $limit = 0, $offset = 0, $sort = null, $order = null ) {
		if ( $criteria = @$this->_criteria ) {
			unset( $this->_criteria );
			return $this->_fetchByAssocAndCriteria( $selfTable, $assocEntity, $assocTargetKey, $id, $criteria, $limit, $offset, $sort, $order );
		}

		$criteria = is_array( $id ) ? new Criteria\InCriteria( $assocTargetKey, $id ) : new Criteria\IsCriteria( $assocTargetKey, $id );
		$fields   = array( 'DISTINCT ' . $selfTable . '.*' );

		return $this->_getCollection(
			$this->_model->getGateway( $assocEntity )
				->selectByCriteria( $criteria, $fields, $limit, $offset, array_map( array( $this, '_prefixSort' ), (array) $sort ), $order )
		);
	}

	/**
	 * Helper method for counting entities by association table relationship.
	 *
	 * @param string $selfTableId ID field in the self table.
	 * @param string $assocEntity Name of the association entity.
	 * @param string $assocTargetKey Target key in the association entity.
	 * @param string $id Target ID.
	 * @return int
	 */
	protected function _countByAssoc( $selfTableId, $assocEntity, $assocTargetKey, $id ) {
		if ( $criteria = @$this->_criteria ) {
			unset( $this->_criteria );
			return $this->_countByAssocAndCriteria( $selfTableId, $assocEntity, $assocTargetKey, $id, $criteria );
		}

		$criteria = is_array( $id ) ? new Criteria\InCriteria( $assocTargetKey, $id ) : new Criteria\IsCriteria( $assocTargetKey, $id );

		return $this->_model->getGateway( $assocEntity )->selectByCriteria( $criteria, array( 'COUNT(DISTINCT ' . $selfTableId . ')' ) )->fetchSingle();
	}

	/**
	 * Helper method for fetching entities by association table relationship and criteria.
	 *
	 * @param string                    $selfTable Name of the self table.
	 * @param string                    $assocEntity Name of the association entity.
	 * @param string                    $assocTargetKey Target key in the association entity.
	 * @param string                    $id Target ID.
	 * @param Criteria\AbstractCriteria $criteria Criteria object.
	 * @param int                       $limit Limit of entities.
	 * @param int                       $offset Offset for fetching.
	 * @param mixed                     $sort Sort field(s).
	 * @param mixed                     $order Sort order(s).
	 * @return RowsetEntityCollection
	 */
	protected function _fetchByAssocAndCriteria( $selfTable, $assocEntity, $assocTargetKey, $id, Criteria\AbstractCriteria $criteria, $limit = 0, $offset = 0, $sort = null, $order = null ) {
		$criterion = new Criteria\CompositeCriteria( array( $criteria ) );
		if ( is_array( $id ) ) {
			$criterion->addAnd( new Criteria\InCriteria( $assocTargetKey, $id ) );
		} else {
			$criterion->addAnd( new Criteria\IsCriteria( $assocTargetKey, $id ) );
		}
		$fields = array( 'DISTINCT ' . $selfTable . '.*' );

		return $this->_getCollection(
			$this->_model->getGateway( $assocEntity )
				->selectByCriteria( $criterion, $fields, $limit, $offset, array_map( array( $this, '_prefixSort' ), (array) $sort ), (array) $order )
		);
	}

	/**
	 * Helper method for counting entities by association table relationship and criteria.
	 *
	 * @param string                    $selfTableId ID field in the self table.
	 * @param string                    $assocEntity Name of the association entity.
	 * @param string                    $assocTargetKey Target key in the association entity.
	 * @param string                    $id Target ID.
	 * @param Criteria\AbstractCriteria $criteria Criteria object.
	 * @return int
	 */
	protected function _countByAssocAndCriteria( $selfTableId, $assocEntity, $assocTargetKey, $id, Criteria\AbstractCriteria $criteria ) {
		$criterion = new Criteria\CompositeCriteria( array( $criteria ) );
		if ( is_array( $id ) ) {
			$criterion->addAnd( new Criteria\InCriteria( $assocTargetKey, $id ) );
		} else {
			$criterion->addAnd( new Criteria\IsCriteria( $assocTargetKey, $id ) );
		}

		return $this->_model->getGateway( $assocEntity )->selectByCriteria( $criterion, array( 'COUNT(DISTINCT ' . $selfTableId . ')' ) )->fetchSingle();
	}

	/**
	 * Prefixes the requested sort value to get the actual field name.
	 *
	 * @param string $sort Sort field.
	 * @return array
	 */
	private function _prefixSort( $sort ) {
		return $this->_fieldPrefix . $sort;
	}

	/**
	 * Turns a rowset object into an entity collection object.
	 *
	 * @param AbstractRowset $rs Rowset object.
	 * @return Model\EntityCollection
	 */
	protected function _getCollection( AbstractRowset $rs ) {
		return $this->_getCollectionByRowset( $rs );
	}

	/**
	 * Abstract method to create a collection from a rowset.
	 *
	 * @param AbstractRowset $rs Rowset object.
	 * @return Model\EntityCollection
	 */
	abstract protected function _getCollectionByRowset( AbstractRowset $rs );

	/**
	 * Abstract method to create a collection from an array of entities.
	 *
	 * @param array $entities Array of entities.
	 * @return Model\EntityCollection
	 */
	abstract public function createCollection( array $entities = array() );
}
