<?php
namespace SabaiApps\Framework\Model;

use SabaiApps\Framework\Criteria;

/**
 * AbstractEntity class for representing entities in the model.
 * Provides methods for managing entity variables, relationships, and lifecycle states.
 */
abstract class AbstractEntity {

	/**
	 * @var string Name of the entity.
	 */
	protected $_name;

	/**
	 * @var Model Model instance associated with the entity.
	 */
	protected $_model;

	/**
	 * @var array Variables representing the entity's data.
	 */
	protected $_vars = array();

	/**
	 * @var array Cached objects related to the entity.
	 */
	protected $_objects = array();

	/**
	 * @var string Temporary ID for the entity.
	 */
	private $_tempId = false;

	/**
	 * @var array Entities to be assigned to this entity during commit.
	 */
	private $_entitiesToAssign = array();

	/**
	 * @var array Entities that should be assigned to this entity during commit.
	 */
	private $_entitiesToBeAssigned = array();

	/**
	 * 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;
	}

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

	/**
	 * Gets the model instance associated with the entity.
	 *
	 * @return Model
	 */
	public function getModel() {
		return $this->_model;
	}

	/**
	 * Shortcut method for getting the related entity repository object.
	 *
	 * @return AbstractEntityRepository
	 */
	protected function _getRepository() {
		return $this->_model->getRepository( $this->getName() );
	}

	/**
	 * Sets a temporary ID for the entity.
	 *
	 * @param string $value Temporary ID value.
	 * @return $this
	 */
	public function setTempId( $value ) {
		$this->_tempId = $value;

		return $this;
	}

	/**
	 * Gets the temporary ID of the entity.
	 *
	 * @return string
	 */
	public function getTempId() {
		return $this->_tempId;
	}

	/**
	 * Initializes variables using row data fetched from the database.
	 *
	 * @param array $arr Row data.
	 * @return $this
	 */
	public function initVars( array $arr ) {
		foreach ( array_keys( $arr ) as $name ) {
			$this->_initVar( $name, $arr[ $name ] );
		}

		return $this;
	}

	/**
	 * Abstract method for initializing a variable.
	 *
	 * @param string $name Variable name.
	 * @param mixed  $value Variable value.
	 */
	abstract protected function _initVar( $name, $value );

	/**
	 * Gets the value of a variable.
	 *
	 * @param string $key Variable name.
	 * @return mixed
	 */
	public function get( $key ) {
		return $this->__get( $key );
	}

	/**
	 * Abstract method for getting a variable value.
	 *
	 * @param string $name Variable name.
	 * @return mixed
	 */
	abstract public function __get( $name );

	/**
	 * Sets the value of a variable.
	 *
	 * @param string $key Variable name.
	 * @param mixed  $value Variable value.
	 * @return $this
	 */
	public function set( $key, $value ) {
		$this->__set( $key, $value );

		return $this;
	}

	/**
	 * Abstract method for setting a variable value.
	 *
	 * @param string $name Variable name.
	 * @param mixed  $value Variable value.
	 */
	abstract public function __set( $name, $value );

	/**
	 * Gets all variables of the entity.
	 *
	 * @return array
	 */
	public function getVars() {
		return $this->_vars;
	}

	/**
	 * Sets a variable value and optionally marks the entity as dirty.
	 *
	 * @param string $name Variable name.
	 * @param mixed  $value Variable value.
	 * @param bool   $markDirty Whether to mark the entity as dirty (default: true).
	 */
	protected function _setVar( $name, $value, $markDirty = true ) {
		$this->_vars[ $name ] = $value;
		if ( $markDirty ) {
			$this->markDirty();
		}
	}

	/**
	 * Sets an object related to this entity.
	 *
	 * @param string $name Object name.
	 * @param mixed  $object An object or null.
	 * @return $this
	 */
	public function assignObject( $name, $object = null ) {
		$this->_objects[ $name ] = $object;

		return $this;
	}

	/**
	 * Gets an object related to this entity.
	 *
	 * @param string $name Object name.
	 * @return object|null
	 */
	public function fetchObject( $name ) {
		return isset( $this->_objects[ $name ] ) ? $this->_objects[ $name ] : null;
	}

	/**
	 * Clears a cached object related to this entity.
	 *
	 * @param string $name Object name.
	 * @return $this
	 */
	public function clearObject( $name ) {
		unset( $this->_objects[ $name ] );

		return $this;
	}

	/**
	 * Marks the entity as new.
	 *
	 * @return $this
	 */
	public function markNew() {
		$this->_model->registerNew( $this );

		return $this;
	}

	/**
	 * Marks the entity as dirty (modified).
	 *
	 * @return $this
	 */
	public function markDirty() {
		$this->_model->registerDirty( $this );

		return $this;
	}

	/**
	 * Marks the entity as removed.
	 *
	 * @return $this
	 */
	public function markRemoved() {
		$this->_model->registerRemoved( $this );
		$this->_entitiesToBeAssigned = array(); // Prevent assignments during commit.

		return $this;
	}

	/**
	 * Caches the entity in the model.
	 *
	 * @return $this
	 */
	public function cache() {
		$this->_model->cacheEntity( $this );

		return $this;
	}

	/**
	 * @param string $entityName
	 * @param string $foreignKey
	 * @return AbstractEntity
	 */
	protected function _fetchEntity( $entityName, $foreignKey ) {
		if ( ! array_key_exists( $entityName, $this->_objects ) ) {
			if ( $id = $this->$foreignKey ) {
				$this->_objects[ $entityName ] = $this->_model->getRepository( $entityName )->fetchById( $id );
			} else {
				$this->_objects[ $entityName ] = null;
			}
		}

		return $this->_objects[ $entityName ];
	}

	/**
	 * @param AbstractEntity $entity
	 * @param string         $foreignKey
	 */
	protected function _assignEntity( AbstractEntity $entity, $foreignKey, $markDirty = true ) {
		$entity_name = $entity->getName();
		if ( ! $id = $entity->id ) {
			if ( ! $temp_id = $entity->getTempId() ) {
				$entity->markNew();
				$temp_id = $entity->getTempId();
			}
			$entity->addEntityToAssign( $this );
			$this->_entitiesToBeAssigned[ $entity_name ][ $foreignKey ] = $temp_id;
		} elseif ( $this->_vars[ $foreignKey ] != $id ) {
			if ( $temp_id = $entity->getTempId() ) {
				// temp id is set, meaning that the entity is being assigned on commit
				// check if we are really allowed to assgin this entity
				if ( ! isset( $this->_entitiesToBeAssigned[ $entity_name ][ $foreignKey ] )
					|| $this->_entitiesToBeAssigned[ $entity_name ][ $foreignKey ] != $temp_id
				) {
					return;
				}

				$markDirty = false; // do not change state during the commit process
			}

				// Assign entity
				$this->_setVar( $foreignKey, $id, $markDirty );
				unset( $this->_entitiesToBeAssigned[ $entity_name ][ $foreignKey ] );
		}
		$this->_objects[ $entity_name ] = $entity;
	}

	/**
	 * @param string $entityName
	 * @param string $id
	 * @param string $foreignKey
	 * @param bool   $markDirty
	 */
	protected function _assignEntityById( $entityName, $id, $foreignKey, $markDirty = true ) {
		$this->_setVar( $foreignKey, $id, $markDirty );
		unset( $this->_entitiesToBeAssigned[ $entityName ][ $foreignKey ] );
	}

	public function fetchEntitiesToBeAssigned() {
		return $this->_entitiesToBeAssigned;
	}

	/**
	 * @param string $entityName
	 * @param string $objectName
	 * @return RowsetEntityCollection
	 */
	protected function _fetchEntities( $entityName, $objectName = null ) {
		if ( ! isset( $objectName ) ) {
			$method = 'fetchBy' . $this->getName();
			return $this->_model->getRepository( $entityName )->$method( $this->id );
		}

		if ( ! isset( $this->_objects[ $objectName ] ) ) {
			$method                        = 'fetchBy' . $this->getName();
			$this->_objects[ $objectName ] = $this->_model->getRepository( $entityName )->$method( $this->id );
		}

		return $this->_objects[ $objectName ];
	}

	/**
	 * @param string $targetPrimaryKey
	 * @param string $entityName
	 * @param string $id
	 * @return int
	 */
	protected function _removeEntityById( $targetPrimaryKey, $entityName, $id ) {
		$method   = 'fetchBy' . $this->getName() . 'AndCriteria';
		$criteria = new Criteria\IsCriteria( $targetPrimaryKey, $id );
		$entities = $this->_model->getRepository( $entityName )->$method( $this->id, $criteria );
		foreach ( $entities as $entity ) {
			$entity->{$this->getName()} = null;
		}
	}

	/**
	 * @param string $entityName
	 * @return int
	 */
	protected function _removeEntities( $entityName ) {
		$entities = $this->_fetchEntities( $entityName );
		foreach ( $entities as $entity ) {
			$entity->{$this->getName()} = null;
		}
	}

	/**
	 * @param string $entityName
	 * @return AbstractEntity
	 */
	protected function _createEntity( $entityName ) {
		$entity                     = $this->_model->create( $entityName );
		$entity->{$this->getName()} = $this;

		return $entity;
	}

	/**
	 * @param string         $linkEntityName
	 * @param AbstractEntity $entity
	 * @return object AbstractEntity
	 */
	protected function _linkEntity( $linkEntityName, AbstractEntity $entity ) {
		$link                       = $this->_model->create( $linkEntityName )->markNew();
		$link->{$this->getName()}   = $this;
		$link->{$entity->getName()} = $entity;

		return $link;
	}

	/**
	 * @param string $linkEntityName
	 * @param string $linkTargetKey
	 * @param string $id
	 * @return object AbstractEntity
	 */
	protected function _linkEntityById( $linkEntityName, $linkTargetKey, $id ) {
		$link                     = $this->_model->create( $linkEntityName )->markNew();
		$link->{$this->getName()} = $this;
		$link->$linkTargetKey     = $id;

		return $link;
	}

	/**
	 * @param string $linkEntityName
	 * @param string $linkSelfKey
	 * @param string $linkTargetKey
	 * @param string $id
	 */
	protected function _unlinkEntityById( $linkEntityName, $linkSelfKey, $linkTargetKey, $id ) {
		if ( ! $id = intval( $id ) ) {
			return;
		}

		$criteria = new Criteria\CompositeCriteria();
		$criteria->addAnd( new Criteria\IsCriteria( $linkSelfKey, $this->id ) )
			->addAnd( new Criteria\IsCriteria( $linkTargetKey, $id ) );
		foreach ( $this->_model->getRepository( $linkEntityName )->fetchByCriteria( $criteria ) as $link ) {
			$link->markRemoved();
		}
	}

	/**
	 * @param string $linkEntityName
	 */
	protected function _unlinkEntities( $linkEntityName ) {
		$method = 'fetchBy' . $this->getName();
		foreach ( $this->_model->getRepository( $linkEntityName )->$method( $this->id ) as $link ) {
			$link->markRemoved();
		}
	}

	/**
	 * @param AbstractEntity $entity
	 */
	public function addEntityToAssign( AbstractEntity $entity, $assignMethod = null ) {
		if ( isset( $assignMethod ) ) {
			$this->_entitiesToAssign[ $assignMethod ][] = $entity;
		} else {
			$this->_entitiesToAssign[] = $entity;
		}

		return $this;
	}

	/**
	 */
	public function clearEntitiesToAssign() {
		$this->_entitiesToAssign = array();

		return $this;
	}

	/**
	 * @return array
	 */
	public function fetchEntitiesToAssign() {
		return $this->_entitiesToAssign;
	}

	/**
	 * Commits the changes made to this entity. You must call markNew() or markRemoved()
	 * prior to this method to insert or delete this entity.
	 */
	public function commit() {
		$this->_model->commitOne( $this );

		return $this;
	}

	/**
	 * A callback method called just before committing this entity
	 */
	public function onCommit( $type = 'new' ) {
	}

	/**
	 * Reloads vars from the repository
	 *
	 * @return bool
	 */
	public function reload() {
		$this->_model->clearEntityCache( $this->_name, $this->id ); // make sure not using cache
		$this->_vars    = $this->_getRepository()->fetchById( $this->id )->getVars();
		$this->_objects = array();

		return $this;
	}

	public function with() {
		$args       = func_get_args();
		$collection = call_user_func_array(
			array( $this->_model->getRepository( $this->_name )->createCollection( array( $this ) ), 'with' ),
			$args
		);
		return $collection->getFirst();
	}
}
