<?php
/**
  * Ahead WebSoft Technologies Pvt Ltd.
  * @copyright  Copyright 2006-2026 Ahead WebSoft Technologies Pvt Ltd.
  * @license    https://www.socialengine.com/policies
*/
abstract class Core_Model_Item_Abstract extends Engine_Db_Table_Row implements Core_Model_Item_Interface
{
    /**
     * @var string The module name of this model (say that 12 times fast)
     */
    protected $_moduleName;

    /**
     * The unique identifier of this instance
     *
     * @var integer|mixed
     */
    protected $_identity;

    /**
     * The resource type, i.e. user, group, etc
     *
     * @var string
     */
    protected $_type;

    /**
     * The short resource type (last class suffix)
     *
     * @var string
     */
    protected $_shortType;

    /**
     * For mixin objects to have local storage
     *
     * @var stdClass
     */
    protected $_store;

    /**
     * List of columns that, when changed, will cause the search indexer to update
     *
     * @var array
     */
    protected $_searchTriggers = array('search', 'title', 'description', 'body');

    /**
     * List of columns that, when changed, will cause the modified_date column to
     * be updated
     *
     * @var array
     */
    protected $_modifiedTriggers = array('title', 'description', 'body',
        'photo_id', 'file_id', 'category_id');

    /**
     * Disable internal hooks?
     * @var boolean
     */
    protected $_disableHooks = false;

    /**
     * Abstract constructor
     *
     * @param mixed $identity
     */
    public function __construct(array $config)
    {
        parent::__construct($config);

        // Get identity
        $primary = $this->getTable()->info(Zend_Db_Table_Abstract::PRIMARY);
        if( count($primary) !== 1 ) {
            throw new Core_Model_Item_Exception(sprintf('Item tables must have only a single primary column, given: %s', join(', ', $primary)));
        }
        $prop = array_shift($primary);
        if( !isset($this->$prop) ) {
            //throw new Core_Model_Item_Exception(sprintf('Primary column "%s" not defined', $prop));
        } else if( isset($this->$prop) ) {
            $this->_identity = $this->$prop;
        }

        // Get store
        $this->_store = new stdClass();

        // Backwards compatibility
        if( isset($this->_searchColumns) && is_array($this->_searchColumns) ) {
            $this->_searchTriggers = $this->_searchColumns;
            unset($this->_searchColumns);
        }
    }

    /**
     * Magic caller
     *
     * @param string $method
     * @param array $arguments
     */
    public function __call($method, array $args)
    {
        throw new Core_Model_Item_Exception(sprintf('Unknown method %s in class %s', $method, get_class($this)));
    }

    /**
     * Get the module this model belongs to
     *
     * @return string The module name of this model
     */
    public function getModuleName()
    {
        if( empty($this->_moduleName) )
        {
            $class = get_class($this);
            if (preg_match('/^([a-z][a-z0-9]*)_/i', $class, $matches)) {
                $prefix = $matches[1];
            } else {
                $prefix = $class;
            }
            $this->_moduleName = $prefix;
        }
        return $this->_moduleName;
    }

    /**
     * Gets the resource type of the current object.
     * User_Model_User -> user
     * Album_Model_Photo -> album_photo
     *
     * @return string The type identifier (i.e. user, group, etc)
     */
    public function getType($inflect = false)
    {
        if( null === $this->_type )
        {
            $this->_type = Engine_Api::classToType(get_class($this), $this->getModuleName());
        }

        if( $inflect )
        {
            return str_replace(' ', '', ucwords(str_replace('_', ' ', $this->_type)));
        }

        return $this->_type;
    }

    /**
     * Get a short type (used for id column prefixes)
     * User_Model_User -> user
     * Album_Model_Photo -> photo
     *
     * @param boolean $inflect
     * @return string
     */
    public function getShortType($inflect = false)
    {
        if( null === $this->_shortType )
        {
            $this->_shortType = ltrim(strrchr(strtolower(get_class($this)), '_'), '_');
        }

        if( $inflect )
        {
            return str_replace(' ', '', ucwords(str_replace('_', ' ', $this->_shortType)));
        }

        return $this->_shortType;
    }

    /**
     * Gets the numeric unique identifier for this object
     *
     * @return integer|mixed
     */
    public function getIdentity()
    {
        return (int) $this->_identity;
    }

    /**
     * Gets a globally unique identitfier
     *
     * @param bool $asArray Return guid as an array of length two
     * @return string|array The guid
     */
    public function getGuid($asArray = false)
    {
        if( $asArray )
        {
            return array($this->getType(), $this->getIdentity());
        }

        else
        {
            return sprintf('%s_%d', $this->getType(), $this->getIdentity());
        }
    }

    /**
     * Gets an absolute URL to this resource
     *
     * @return string The URL
     */
    public function getHref()
    {
        return null;
        //throw new Core_Model_Item_Exception('getHref must be defined in child classes');
    }

    /**
     * Gets the title of the item. This would be a name for users
     *
     * @return string The title
     */
    public function getTitle()
    {
        if( isset($this->title) )
        {
            return $this->title;
        }
        return null;
    }

    /**
     * Gets a url slug for this item, based on it's title
     *
     * @return string The slug
     */
    public function getSlug($str = null, $maxstrlen = 64)
    {
        if( null === $str ) {
            $str = $this->getTitle();
        }

        return Engine_String::slug($str, $maxstrlen);
    }

    /**
     * Gets the description of the item. This might be about me for users (todo
     *
     * @return string The description
     */
    public function getDescription()
    {
        if( isset($this->description) )
        {
            return Engine_Api::_()->core()->smileyToEmoticons($this->description);
        }
        return '';
    }

    /**
     * Gets keywords for this item, should be overridden
     *
     * @return string
     */
    public function getKeywords()
    {
        if( isset($this->keywords) )
        {
            return $this->keywords;
        }
        return '';
    }

    /**
     * Gets rich HTML content (i.e. video object src) for feed and other things
     *
     * @return string|null
     */
    public function getRichContent()
    {
        return null;
    }

    /**
     * Get the date this item was created
     *
     * @return integer The creation date
     */
    public function getCreationDate()
    {
        return $this->creation_date;
    }

    /**
     * Get the date this item was last modifier
     *
     * @return integer The last modified date
     */
    public function getModificationDate()
    {
        return $this->modified_date;
    }

    /**
     * Gets an item that defines the authorization permissions, usually the item
     * itself
     *
     * @return Core_Model_Item_Abstract
     */
    public function getAuthorizationItem()
    {
        return $this;
    }

    /**
     * Gets a url to the current photo representing this item. Return null if none
     * set
     *
     * @param string The photo type (null -> main, thumb, icon, etc);
     * @return string The photo url
     */
    public function getPhotoUrl($type = null)
    {
        if( empty($this->photo_id) ) {
            return null;
        }

        $file = Engine_Api::_()->getItemTable('storage_file')->getFile($this->photo_id, $type);
        if( !$file ) {
            return null;
        }

        return $file->map();
    }

    /**
     * Checks if this item is searchable
     *
     * @return bool
     */
    public function isSearchable()
    {
        return ( (!isset($this->search) || $this->search) && !empty($this->_searchTriggers) && is_array($this->_searchTriggers) );
    }

    /**
     * Get data to be indexed for search, but not displayed to the user
     *
     * @return string
     */
    public function getHiddenSearchData()
    {
        return '';
    }

    /**
     * Get a generic media type. Values:
     * audio, image, video, news, blog
     *
     * @return string
     */
    public function getMediaType()
    {
        $type = $this->getType();
        if( strpos($type, 'photo') !== false ) {
            return 'image';
        } else if( strpos($type, 'video') !== false ) {
            return 'video';
        } else if( strpos($type, 'blog') !== false ) {
            return 'blog';
        } else if( strpos($type, 'link') !== false ) {
            return 'link';
        } else if( strpos($type, 'poll') !== false ) {
            return 'poll';
        } else if( strpos($type, 'event') !== false ) {
            return 'event';
        } else if( strpos($type, 'group') !== false ) {
            return 'group';
        } else if( strpos($type, 'forum') !== false ) {
            return 'forum';
        } else if( strpos($type, 'activity_action') !== false ) {
            return 'post';
        } else {
            return 'item';
        }
    }

    /**
     * Can this item own various types of content?
     * Examples:
     *   user
     *   event
     *   group
     *
     * @return boolean
     */
    public function isContentParent()
    {
        return !empty($this->_isContentParent);
    }



    // Meta

    /**
     * Gets the primary table model associated with this class.
     *
     * @return Zend_Db_Table_Abstract
     */
    public function getTable()
    {
        return $this->_getTable();
    }

    /**
     *
     * @return Zend_Db_Table_Abstract
     */
    protected function _getTable()
    {
        if( null === $this->_table )
        {
            $this->_table = Engine_Api::_()->getItemTable($this->getType());
        }
        return $this->_table;
    }

    /**
     * Sets the primary table model associated with this class
     *
     * @param Zend_Db_Table_Abstract $table
     * @return Core_Model_Item_Abstract
     */
    public function setTable(Zend_Db_Table_Abstract $table = null)
    {
        $this->_table = $table;
        return $this;
    }




    // Internal hooks

    /**
     * Disable hooks. Sometimes required to prevent infinite loops in hooks.
     *
     * @param bool $flag
     * @return self
     */
    public function disableHooks($flag = true)
    {
        $this->_disableHooks = (bool) $flag;
        return $this;
    }

    /**
     * Pre-insert hook. If overridden, should be called at end of function.
     *
     * @return void
     */
    protected function _insert()
    {
        if( $this->_disableHooks ) return;

        parent::_insert();

        if( isset($this->creation_date) ) {
            $this->creation_date = date('Y-m-d H:i:s');
        }

        // Should updated be initialized on creation or be left null?
        if( isset($this->modified_date) ) {
            $this->modified_date = date('Y-m-d H:i:s');
        }

        Engine_Hooks_Dispatcher::getInstance()
            ->callEvent('on'.$this->getType(true).'CreateBefore', $this);
        Engine_Hooks_Dispatcher::getInstance()
            ->callEvent('onItemCreateBefore', $this);
    }

    /**
     * Post-insert hook. If overridden, should be called at end of function.
     *
     * @return void
     */
    protected function _postInsert()
    {
        if( $this->_disableHooks ) return;

        parent::_postInsert();

        $prop = $this->getShortType() . '_id';
        $this->_identity = $this->$prop;

        Engine_Hooks_Dispatcher::getInstance()
            ->callEvent('on'.$this->getType(true).'CreateAfter', $this);
        Engine_Hooks_Dispatcher::getInstance()
            ->callEvent('onItemCreateAfter', $this);

        // Search indexer
        if( $this->isSearchable() &&
            is_array($this->_searchTriggers) /* We don't need to check on insert &&
        engine_count(array_intersect_key((array)@$this->_modifiedFields, array_flip($this->_searchTriggers))) > 0 */ ) {
            // Index
            Engine_Api::_()->getApi('search', 'core')->index($this);
        }
    }

    /**
     * Pre-update hook. If overridden, should be called at end of function.
     *
     * @return void
     */
    protected function _update()
    {
        if( $this->_disableHooks ) return;

        Engine_Hooks_Dispatcher::getInstance()
            ->callEvent('on'.$this->getType(true).'UpdateBefore', $this);
        Engine_Hooks_Dispatcher::getInstance()
            ->callEvent('onItemUpdateBefore', $this);

        // Update modified
        if( is_array($this->_modifiedTriggers) &&
            isset($this->modified_date) &&
            empty($this->_modifiedFields['modified_date']) && // Prevents modified_date from being overwritten here
            engine_count(array_intersect_key((array)@$this->_modifiedFields, array_flip($this->_modifiedTriggers))) > 0 ) {
            $this->modified_date = date('Y-m-d H:i:s'); //new Zend_Db_Expr('NOW()');
        }

        parent::_update();
    }

    /**
     * Post-insert hook. If overridden, should be called at end of function.
     *
     * @return void
     */
    protected function _postUpdate()
    {
        if( $this->_disableHooks ) return;

        parent::_postUpdate();

        Engine_Hooks_Dispatcher::getInstance()
            ->callEvent('on'.$this->getType(true).'UpdateAfter', $this);
        Engine_Hooks_Dispatcher::getInstance()
            ->callEvent('onItemUpdateAfter', $this);

        // Search indexer
        if( !$this->isSearchable() ) {
            // De-index
            Engine_Api::_()->getApi('search', 'core')->unindex($this);
        } else if( is_array($this->_searchTriggers) &&
            engine_count(array_intersect_key((array)@$this->_modifiedFields, array_flip($this->_searchTriggers))) > 0 ) {
            // Re-index
            Engine_Api::_()->getApi('search', 'core')->index($this);
        }
    }

    /**
     * Pre-delete hook. If overridden, should be called at end of function.
     *
     * @return void
     */
    protected function _delete()
    {
        if( $this->_disableHooks ) return;

        parent::_delete();

        Engine_Hooks_Dispatcher::getInstance()
            ->callEvent('on'.$this->getType(true).'DeleteBefore', $this);
        Engine_Hooks_Dispatcher::getInstance()
            ->callEvent('onItemDeleteBefore', $this);

        // Unindex from search
        Engine_Api::_()->getApi('search', 'core')->unindex($this);
    }

    /**
     * Post-insert hook. If overridden, should be called at end of function.
     *
     * @return void
     */
    protected function _postDelete()
    {
        if( $this->_disableHooks ) return;

        parent::_postDelete();

        Engine_Hooks_Dispatcher::getInstance()
            ->callEvent('on'.$this->getType(true).'DeleteAfter', array(
                'type' => $this->getType(),
                'identity' => $this->getIdentity()
            ));
        Engine_Hooks_Dispatcher::getInstance()
            ->callEvent('onItemDeleteAfter', array(
                'type' => $this->getType(),
                'identity' => $this->getIdentity()
            ));
    }




    // Misc

    /**
     * Returns the local storage object
     *
     * @return stdClass
     */
    public function store()
    {
        return $this->_store;
    }


    // Ownership/Auth

    public function authorization($adapter = null)
    {
        $object = Engine_Api::_()->getApi('core', 'authorization');
        if( null !== $adapter && $object->$adapter ) {
            $object = $object->$adapter;
        }
        return new Engine_ProxyObject($this, $object);
    }

    /**
     * Checks if the passed item has the same guid as the object
     *
     * @param Core_Model_Item_Abstract $item
     * @return bool
     */
    public function isSelf(Core_Model_Item_Abstract $item)
    {
        return ( $item->getGuid() === $this->getGuid() );
    }

    /**
     * Get the parent of this item. The parent is an item this belongs to.
     *
     * @param string (OPTIONAL) $itemType
     * @return Core_Model_Item_Abstract
     */
    public function getParent($recurseType = null)
    {
        if( empty($recurseType) ) $recurseType = null;

        // Parent and owner are same
        if( !empty($this->_parent_is_owner) ) {
            return $this->getOwner($recurseType);
        }

        // Just return self for users
        if( $this->getType() === 'user' ) {
            if( null === $recurseType || $recurseType === 'user' ) {
                return $this;
            } else {
                throw new Core_Model_Item_Exception('Cannot request parent of user of type other than user');
            }
        }

        // Get parent type
        $type = null;
        if( !empty($this->_parent_type) ) { // Local definition
            $type = $this->_parent_type;
        } else if( !empty($this->parent_type) ) { // Db definition
            $type = $this->parent_type;
        } else if( !empty($this->resource_type) ) {
            $type = $this->resource_type;
        }

        if( null === $type || !Engine_Api::_()->hasItemType($type) ) {
            throw new Core_Model_Item_Exception('Unable to determine parent type or parent type doesn\'t exist');
        }

        // Get parent id
        $id = null;
        if( !empty($this->parent_id) ) {
            $id = $this->parent_id;
        } else if( !empty($this->resource_id) ) {
            $id = $this->resource_id;
        } else {
            $short_type = Engine_Api::typeToShort($type, Engine_Api::_()->getItemModule($type));
            $prop = $short_type . '_id';
            if( !empty($this->$prop) ) {
                $id = $this->$prop;
            }
        }

        if( null === $id || !(($parent = Engine_Api::_()->getItem($type, $id)) instanceof Core_Model_Item_Abstract) ||
            !$parent->getIdentity() ) {
            throw new Core_Model_Item_Exception('Parent item missing');
        }

        if( null !== $recurseType && $parent->getType() != $recurseType ) {
            $newParent = $parent->getParent($recurseType);
            if( $newParent->isSelf($parent) ) {
                throw new Core_Model_Item_Exception('Infinite recursion detected in getOwner()');
            }
            return $newParent;
        }

        return $parent;
    }

    public function getOwner($recurseType = null)
    {
        if( empty($recurseType) ) $recurseType = null;

        // Just return self for users
        if( $this->getType() === 'user' ) {
            if( null === $recurseType || $recurseType === 'user' ) {
                return $this;
            } else {
                throw new Core_Model_Item_Exception('Cannot request owner of user of type other than user');
            }
        }

        // Get owner type
        $type = null;
        if( !empty($this->_owner_type) ) { // Local definition
            $type = $this->_owner_type;
        } else if( !empty($this->owner_type) ) { // Db definition
            $type = $this->owner_type;
        } else {
            $type = 'user';
        }

        if( null === $type ) {
            throw new Core_Model_Item_Exception('No owner type defined and not overriden');
        }
        if( !Engine_Api::_()->hasItemType($type) ) {
            throw new Core_Model_Item_Exception('Unknown owner type: '.$type);
        }

        // Get parent id
        $id = null;
        if(isset($this->owner_id) && !empty($this->owner_id) ) {
          $id = $this->owner_id;
        } else if(isset($this->user_id) && !empty($this->user_id)) {
          $id = $this->user_id;
        } else {
          $short_type = Engine_Api::typeToShort($type, Engine_Api::_()->getItemModule($type));
          $prop = $short_type . '_id';
          if( !empty($this->$prop) ) {
              $id = $this->$prop;
          }
        }

        if( null === $id ) {
            //throw new Core_Model_Item_Exception('No owner id defined');
        }
        if( !(($owner = Engine_Api::_()->getItem($type, $id)) instanceof Core_Model_Item_Abstract) ||
            !$owner->getIdentity() ) {
            //throw new Core_Model_Item_Exception('Owner missing');

            //instead of throwing exception return and empty user object, the user model should handle it gracefully
            return Engine_Api::_()->getItem($type, $id);
        }

        if( null !== $recurseType && $owner->getType() != $recurseType ) {
            $newOwner = $owner->getOwner($recurseType);
            if( $newOwner->isSelf($owner) ) {
                throw new Core_Model_Item_Exception('Infinite recursion detected in getOwner()');
            }
            return $newOwner;
        }

        return $owner;
    }

    /**
     * Checks if passed object is the owner. All items default to owning themselves
     *
     * @param Core_Model_Item_Abstract $owner The object to check for ownership
     * @return bool
     */
    public function isOwner($owner)
    {
        if( $this->isSelf($owner) ) {
            return true;
        }

        $tmp = $this->getOwner() ;
        return $tmp instanceof Core_Model_Item_Abstract
            ? $tmp->isSelf($owner)
            : false;
    }

    public function getChildren($type, $params = array())
    {
        if( !Engine_Api::_()->hasItemType($type) || empty($this->_children_types) || !engine_in_array($type, $this->_children_types) ) {
            throw new Core_Model_Item_Exception(sprintf('Specified child type doesn\'t exist or not registered as a child type of this item: %s', $type));
        }

        $childTable = Engine_Api::_()->getItemTable($type);
        $childSelect = $this->getChildrenSelect($type, $params);
        return $childTable->fetchAll($childSelect);
    }

    public function getChildrenSelect($type, $params = array())
    {
        if( !Engine_Api::_()->hasItemType($type) || empty($this->_children_types) || !engine_in_array($type, $this->_children_types) ) {
            throw new Core_Model_Item_Exception(sprintf('Specified child type doesn\'t exist or not registered as a child type of this item: %s', $type));
        }

        $childTable = Engine_Api::_()->getItemTable($type);
        $method = 'getChildrenSelectOf'.$this->getType(true);

        if( !method_exists($childTable, $method) ) {
            throw new Core_Model_Item_Exception('Child table doesn\'t support retrieval by parent');
        }

        $childSelect = $childTable->$method($this, $params);;
        if( !($childSelect instanceof Zend_Db_Select) ) {
            throw new Core_Model_Item_Exception('Child table did not return a select object');
        }

        // Throw in some automatic stuff
        if( !empty($params['order']) ) {
            $childSelect->order($params['order']);
        }
        if( !empty($params['limit']) || !empty($params['offset']) ) {
            $childSelect->limit(@$params['limit'], @$params['offset']);
        }

        return $childSelect;
    }



    // Data type convertors

    /**
     * Experimetnal string accessor. Returns an html string representation of the
     * object
     *
     * @return string
     */
    public function toString($attribs = array())
    {
        $href = $this->getHref();
        $title = $this->getTitle();
        $view = Zend_Registry::isRegistered('Zend_View') ? Zend_Registry::get('Zend_View') : null;

        if( !$href ) {
            return $title;
        } else if( !$view ) {
            return '<a href="'.$href.'">'.$title.'</a>';
        } else {
            return $view->htmlLink($href, $title, $attribs);
        }
    }

    /**
     * Magic Method for {self::toString()}
     *
     * @return string
     */
    public function __toString()
    {
        try {
            return $this->toString();
        } catch( Exception $e ) {
            Zend_Registry::get('Zend_Log')->log($e, Zend_Log::ERR);
            return '';
        }
    }

    /**
     * Gets an array of data about the object that is safe for sending to untrusted
     * sources (i.e. doesn't contain any passwords, keys, or private settings or
     * information)
     *
     * @return array
     */
    public function toRemoteArray()
    {
        $arr = array(
            'identity' => $this->getIdentity(),
            'type' => $this->getType(),
            'title' => $this->getTitle(),
            'description' => $this->getDescription(),
            'keywords' => $this->getKeywords(),
            'href' => $this->getHref(),
            'photo' => $this->getPhotoUrl(),
        );
        if( isset($this->creation_date) ) {
            $arr['creation_date'] = $this->creation_date;
        }
        if( isset($this->modified_date) ) {
            $arr['modified_date'] = $this->modified_date;
        }
        return $arr;
    }
}
