<?php
/**
 * The parent class for all Elgg Entities.
 *
 * An ElggEntity is one of the basic data models in Elgg.  It is the primary
 * means of storing and retrieving data from the database.  An ElggEntity
 * represents one row of the entities table.
 *
 * The ElggEntity class handles CRUD operations for the entities table.
 * ElggEntity should always be extended by another class to handle CRUD
 * operations on the type-specific table.
 *
 * ElggEntity uses magic methods for get and set, so any property that isn't
 * declared will be assumed to be metadata and written to the database
 * as metadata on the object.  All children classes must declare which
 * properties are columns of the type table or they will be assumed
 * to be metadata.  See ElggObject::initialise_entities() for examples.
 *
 * Core supports 4 types of entities: ElggObject, ElggUser, ElggGroup, and
 * ElggSite.
 *
 * @tip Most plugin authors will want to extend the ElggObject class
 * instead of this class.
 *
 * @package    Elgg.Core
 * @subpackage DataModel.Entities
 *
 * @property string $type           object, user, group, or site (read-only after save)
 * @property string $subtype        Further clarifies the nature of the entity (read-only after save)
 * @property int    $guid           The unique identifier for this entity (read only)
 * @property int    $owner_guid     The GUID of the creator of this entity
 * @property int    $container_guid The GUID of the entity containing this entity
 * @property int    $site_guid      The GUID of the website this entity is associated with
 * @property int    $access_id      Specifies the visibility level of this entity
 * @property int    $time_created   A UNIX timestamp of when the entity was created (read-only, set on first save)
 * @property int    $time_updated   A UNIX timestamp of when the entity was last updated (automatically updated on save)
 * @property-read string $enabled
 */
abstract class ElggEntity extends ElggData implements
	Notable,    // Calendar interface
	Locatable,  // Geocoding interface
	Importable // Allow import of data
{

	/**
	 * If set, overrides the value of getURL()
	 */
	protected $url_override;

	/**
	 * Icon override, overrides the value of getIcon().
	 */
	protected $icon_override;

	/**
	 * Holds metadata until entity is saved.  Once the entity is saved,
	 * metadata are written immediately to the database.
	 */
	protected $temp_metadata = array();

	/**
	 * Holds annotations until entity is saved.  Once the entity is saved,
	 * annotations are written immediately to the database.
	 */
	protected $temp_annotations = array();

	/**
	 * Holds private settings until entity is saved. Once the entity is saved,
	 * private settings are written immediately to the database.
	 */
	protected $temp_private_settings = array();

	/**
	 * Volatile data structure for this object, allows for storage of data
	 * in-memory that isn't sync'd back to the metadata table.
	 */
	protected $volatile = array();

	/**
	 * Initialize the attributes array.
	 *
	 * This is vital to distinguish between metadata and base parameters.
	 *
	 * @return void
	 */
	protected function initializeAttributes() {
		parent::initializeAttributes();

		$this->attributes['guid'] = NULL;
		$this->attributes['type'] = NULL;
		$this->attributes['subtype'] = NULL;

		$this->attributes['owner_guid'] = elgg_get_logged_in_user_guid();
		$this->attributes['container_guid'] = elgg_get_logged_in_user_guid();

		$this->attributes['site_guid'] = NULL;
		$this->attributes['access_id'] = ACCESS_PRIVATE;
		$this->attributes['time_created'] = NULL;
		$this->attributes['time_updated'] = NULL;
		$this->attributes['last_action'] = NULL;
		$this->attributes['enabled'] = "yes";

		// There now follows a bit of a hack
		/* Problem: To speed things up, some objects are split over several tables,
		 * this means that it requires n number of database reads to fully populate
		 * an entity. This causes problems for caching and create events
		 * since it is not possible to tell whether a subclassed entity is complete.
		 *
		 * Solution: We have two counters, one 'tables_split' which tells whatever is
		 * interested how many tables are going to need to be searched in order to fully
		 * populate this object, and 'tables_loaded' which is how many have been
		 * loaded thus far.
		 *
		 * If the two are the same then this object is complete.
		 *
		 * Use: isFullyLoaded() to check
		 */
		$this->attributes['tables_split'] = 1;
		$this->attributes['tables_loaded'] = 0;
	}

	/**
	 * Clone an entity
	 *
	 * Resets the guid so that the entity can be saved as a distinct entity from
	 * the original. Creation time will be set when this new entity is saved.
	 * The owner and container guids come from the original entity. The clone
	 * method copies metadata but does not copy annotations or private settings.
	 *
	 * @note metadata will have its owner and access id set when the entity is saved
	 * and it will be the same as that of the entity.
	 *
	 * @return void
	 */
	public function __clone() {
		$orig_entity = get_entity($this->guid);
		if (!$orig_entity) {
			elgg_log("Failed to clone entity with GUID $this->guid", "ERROR");
			return;
		}

		$metadata_array = elgg_get_metadata(array(
			'guid' => $this->guid,
			'limit' => 0
		));

		$this->attributes['guid'] = "";

		$this->attributes['subtype'] = $orig_entity->getSubtype();

		// copy metadata over to new entity - slightly convoluted due to
		// handling of metadata arrays
		if (is_array($metadata_array)) {
			// create list of metadata names
			$metadata_names = array();
			foreach ($metadata_array as $metadata) {
				$metadata_names[] = $metadata['name'];
			}
			// arrays are stored with multiple enties per name
			$metadata_names = array_unique($metadata_names);

			// move the metadata over
			foreach ($metadata_names as $name) {
				$this->set($name, $orig_entity->$name);
			}
		}
	}

	/**
	 * Return the value of a property.
	 *
	 * If $name is defined in $this->attributes that value is returned, otherwise it will
	 * pull from the entity's metadata.
	 *
	 * Q: Why are we not using __get overload here?
	 * A: Because overload operators cause problems during subclassing, so we put the code here and
	 * create overloads in subclasses.
	 *
	 * @todo What problems are these?
	 *
	 * @warning Subtype is returned as an id rather than the subtype string. Use getSubtype()
	 * to get the subtype string.
	 *
	 * @param string $name Name
	 *
	 * @return mixed Returns the value of a given value, or null.
	 */
	public function get($name) {
		// See if its in our base attributes
		if (array_key_exists($name, $this->attributes)) {
			return $this->attributes[$name];
		}

		// No, so see if its in the meta data for this entity
		$meta = $this->getMetaData($name);

		// getMetaData returns NULL if $name is not found
		return $meta;
	}

	/**
	 * Sets the value of a property.
	 *
	 * If $name is defined in $this->attributes that value is set, otherwise it is
	 * saved as metadata.
	 *
	 * @warning Metadata set this way will inherit the entity's owner and access ID. If you want
	 * to set metadata with a different owner, use create_metadata().
	 *
	 * @warning It is important that your class populates $this->attributes with keys
	 * for all base attributes, anything not in their gets set as METADATA.
	 *
	 * Q: Why are we not using __set overload here?
	 * A: Because overload operators cause problems during subclassing, so we put the code here and
	 * create overloads in subclasses.
	 *
	 * @todo What problems?
	 *
	 * @param string $name  Name
	 * @param mixed  $value Value
	 *
	 * @return bool
	 */
	public function set($name, $value) {
		if (array_key_exists($name, $this->attributes)) {
			// Certain properties should not be manually changed!
			switch ($name) {
				case 'guid':
				case 'time_updated':
				case 'last_action':
					return FALSE;
					break;
				default:
					$this->attributes[$name] = $value;
					break;
			}
		} else {
			return $this->setMetaData($name, $value);
		}

		return TRUE;
	}

	/**
	 * Return the value of a piece of metadata.
	 *
	 * @param string $name Name
	 *
	 * @return mixed The value, or NULL if not found.
	 */
	public function getMetaData($name) {
		$guid = $this->getGUID();

		if (! $guid) {
			if (isset($this->temp_metadata[$name])) {
				// md is returned as an array only if more than 1 entry
				if (count($this->temp_metadata[$name]) == 1) {
					return $this->temp_metadata[$name][0];
				} else {
					return $this->temp_metadata[$name];
				}
			} else {
				return null;
			}
		}

		// upon first cache miss, just load/cache all the metadata and retry.
		// if this works, the rest of this function may not be needed!
		$cache = elgg_get_metadata_cache();
		if ($cache->isKnown($guid, $name)) {
			return $cache->load($guid, $name);
		} else {
			$cache->populateFromEntities(array($guid));
			// in case ignore_access was on, we have to check again...
			if ($cache->isKnown($guid, $name)) {
				return $cache->load($guid, $name);
			}
		}

		$md = elgg_get_metadata(array(
			'guid' => $guid,
			'metadata_name' => $name,
			'limit' => 0,
		));

		$value = null;

		if ($md && !is_array($md)) {
			$value = $md->value;
		} elseif (count($md) == 1) {
			$value = $md[0]->value;
		} else if ($md && is_array($md)) {
			$value = metadata_array_to_values($md);
		}

		$cache->save($guid, $name, $value);

		return $value;
	}

	/**
	 * Unset a property from metadata or attribute.
	 *
	 * @warning If you use this to unset an attribute, you must save the object!
	 *
	 * @param string $name The name of the attribute or metadata.
	 *
	 * @return void
	 */
	function __unset($name) {
		if (array_key_exists($name, $this->attributes)) {
			$this->attributes[$name] = "";
		} else {
			$this->deleteMetadata($name);
		}
	}

	/**
	 * Set a piece of metadata.
	 *
	 * Plugin authors should use the magic methods or create_metadata().
	 *
	 * @warning The metadata will inherit the parent entity's owner and access ID.
	 * If you want to write metadata with a different owner, use create_metadata().
	 *
	 * @access private
	 *
	 * @param string $name       Name of the metadata
	 * @param mixed  $value      Value of the metadata (doesn't support assoc arrays)
	 * @param string $value_type Types supported: integer and string. Will auto-identify if not set
	 * @param bool   $multiple   Allow multiple values for a single name (doesn't support assoc arrays)
	 *
	 * @return bool
	 */
	public function setMetaData($name, $value, $value_type = null, $multiple = false) {

		// normalize value to an array that we will loop over
		// remove indexes if value already an array.
		if (is_array($value)) {
			$value = array_values($value);
		} else {
			$value = array($value);
		}

		// saved entity. persist md to db.
		if ($this->guid) {
			// if overwriting, delete first.
			if (!$multiple) {
				$options = array(
					'guid' => $this->getGUID(),
					'metadata_name' => $name,
					'limit' => 0
				);
				// @todo in 1.9 make this return false if can't add metadata
				// https://github.com/elgg/elgg/issues/4520
				//
				// need to remove access restrictions right now to delete
				// because this is the expected behavior
				$ia = elgg_set_ignore_access(true);
				if (false === elgg_delete_metadata($options)) {
					return false;
				}
				elgg_set_ignore_access($ia);
			}

			// add new md
			$result = true;
			foreach ($value as $value_tmp) {
				// at this point $value should be appended because it was cleared above if needed.
				$md_id = create_metadata($this->getGUID(), $name, $value_tmp, $value_type,
						$this->getOwnerGUID(), $this->getAccessId(), true);
				if (!$md_id) {
					return false;
				}
			}

			return $result;
		} else {
			// unsaved entity. store in temp array
			// returning single entries instead of an array of 1 element is decided in
			// getMetaData(), just like pulling from the db.
			//
			// if overwrite, delete first
			if (!$multiple || !isset($this->temp_metadata[$name])) {
				$this->temp_metadata[$name] = array();
			}

			// add new md
			$this->temp_metadata[$name] = array_merge($this->temp_metadata[$name], $value);
			return true;
		}
	}

	/**
	 * Deletes all metadata on this object (metadata.entity_guid = $this->guid).
	 * If you pass a name, only metadata matching that name will be deleted.
	 *
	 * @warning Calling this with no $name will clear all metadata on the entity.
	 *
	 * @param null|string $name The name of the metadata to remove.
	 * @return bool
	 * @since 1.8
	 */
	public function deleteMetadata($name = null) {

		if (!$this->guid) {
			return false;
		}

		$options = array(
			'guid' => $this->guid,
			'limit' => 0
		);
		if ($name) {
			$options['metadata_name'] = $name;
		}

		return elgg_delete_metadata($options);
	}

	/**
	 * Deletes all metadata owned by this object (metadata.owner_guid = $this->guid).
	 * If you pass a name, only metadata matching that name will be deleted.
	 *
	 * @param null|string $name The name of metadata to delete.
	 * @return bool
	 * @since 1.8
	 */
	public function deleteOwnedMetadata($name = null) {
		// access is turned off for this because they might
		// no longer have access to an entity they created metadata on.
		$ia = elgg_set_ignore_access(true);
		$options = array(
			'metadata_owner_guid' => $this->guid,
			'limit' => 0
		);
		if ($name) {
			$options['metadata_name'] = $name;
		}

		$r = elgg_delete_metadata($options);
		elgg_set_ignore_access($ia);
		return $r;
	}

	/**
	 * Remove metadata
	 *
	 * @warning Calling this with no or empty arguments will clear all metadata on the entity.
	 *
	 * @param string $name The name of the metadata to clear
	 * @return mixed bool
	 * @deprecated 1.8 Use deleteMetadata()
	 */
	public function clearMetaData($name = '') {
		elgg_deprecated_notice('ElggEntity->clearMetadata() is deprecated by ->deleteMetadata()', 1.8);
		return $this->deleteMetadata($name);
	}

	/**
	 * Disables metadata for this entity, optionally based on name.
	 *
	 * @param string $name An options name of metadata to disable.
	 * @return bool
	 * @since 1.8
	 */
	public function disableMetadata($name = '') {
		$options = array(
			'guid' => $this->guid,
			'limit' => 0
		);
		if ($name) {
			$options['metadata_name'] = $name;
		}

		return elgg_disable_metadata($options);
	}

	/**
	 * Enables metadata for this entity, optionally based on name.
	 *
	 * @warning Before calling this, you must use {@link access_show_hidden_entities()}
	 *
	 * @param string $name An options name of metadata to enable.
	 * @return bool
	 * @since 1.8
	 */
	public function enableMetadata($name = '') {
		$options = array(
			'guid' => $this->guid,
			'limit' => 0
		);
		if ($name) {
			$options['metadata_name'] = $name;
		}

		return elgg_enable_metadata($options);
	}

	/**
	 * Get a piece of volatile (non-persisted) data on this entity.
	 *
	 * @param string $name The name of the volatile data
	 *
	 * @return mixed The value or NULL if not found.
	 */
	public function getVolatileData($name) {
		if (!is_array($this->volatile)) {
			$this->volatile = array();
		}

		if (array_key_exists($name, $this->volatile)) {
			return $this->volatile[$name];
		} else {
			return NULL;
		}
	}

	/**
	 * Set a piece of volatile (non-persisted) data on this entity
	 *
	 * @param string $name  Name
	 * @param mixed  $value Value
	 *
	 * @return void
	 */
	public function setVolatileData($name, $value) {
		if (!is_array($this->volatile)) {
			$this->volatile = array();
		}

		$this->volatile[$name] = $value;
	}

	/**
	 * Remove all relationships to and from this entity.
	 *
	 * @return true
	 * @todo This should actually return if it worked.
	 * @see ElggEntity::addRelationship()
	 * @see ElggEntity::removeRelationship()
	 */
	public function deleteRelationships() {
		remove_entity_relationships($this->getGUID());
		remove_entity_relationships($this->getGUID(), "", true);
		return true;
	}

	/**
	 * Remove all relationships to and from this entity.
	 *
	 * @return bool
	 * @see ElggEntity::addRelationship()
	 * @see ElggEntity::removeRelationship()
	 * @deprecated 1.8 Use ->deleteRelationship()
	 */
	public function clearRelationships() {
		elgg_deprecated_notice('ElggEntity->clearRelationships() is deprecated by ->deleteRelationships()', 1.8);
		return $this->deleteRelationships();
	}

	/**
	 * Add a relationship between this an another entity.
	 *
	 * @tip Read the relationship like "$guid is a $relationship of this entity."
	 *
	 * @param int    $guid         Entity to link to.
	 * @param string $relationship The type of relationship.
	 *
	 * @return bool
	 * @see ElggEntity::removeRelationship()
	 * @see ElggEntity::clearRelationships()
	 */
	public function addRelationship($guid, $relationship) {
		return add_entity_relationship($this->getGUID(), $relationship, $guid);
	}

	/**
	 * Remove a relationship
	 *
	 * @param int $guid         GUID of the entity to make a relationship with
	 * @param str $relationship Name of relationship
	 *
	 * @return bool
	 * @see ElggEntity::addRelationship()
	 * @see ElggEntity::clearRelationships()
	 */
	public function removeRelationship($guid, $relationship) {
		return remove_entity_relationship($this->getGUID(), $relationship, $guid);
	}

	/**
	 * Adds a private setting to this entity.
	 *
	 * Private settings are similar to metadata but will not
	 * be searched and there are fewer helper functions for them.
	 *
	 * @param string $name  Name of private setting
	 * @param mixed  $value Value of private setting
	 *
	 * @return bool
	 */
	function setPrivateSetting($name, $value) {
		if ((int) $this->guid > 0) {
			return set_private_setting($this->getGUID(), $name, $value);
		} else {
			$this->temp_private_settings[$name] = $value;
			return true;
		}
	}

	/**
	 * Returns a private setting value
	 *
	 * @param string $name Name of the private setting
	 *
	 * @return mixed
	 */
	function getPrivateSetting($name) {
		if ((int) ($this->guid) > 0) {
			return get_private_setting($this->getGUID(), $name);
		} else {
			if (isset($this->temp_private_settings[$name])) {
				return $this->temp_private_settings[$name];
			}
		}
		return null;
	}

	/**
	 * Removes private setting
	 *
	 * @param string $name Name of the private setting
	 *
	 * @return bool
	 */
	function removePrivateSetting($name) {
		return remove_private_setting($this->getGUID(), $name);
	}

	/**
	 * Deletes all annotations on this object (annotations.entity_guid = $this->guid).
	 * If you pass a name, only annotations matching that name will be deleted.
	 *
	 * @warning Calling this with no or empty arguments will clear all annotations on the entity.
	 *
	 * @param null|string $name The annotations name to remove.
	 * @return bool
	 * @since 1.8
	 */
	public function deleteAnnotations($name = null) {
		$options = array(
			'guid' => $this->guid,
			'limit' => 0
		);
		if ($name) {
			$options['annotation_name'] = $name;
		}

		return elgg_delete_annotations($options);
	}

	/**
	 * Deletes all annotations owned by this object (annotations.owner_guid = $this->guid).
	 * If you pass a name, only annotations matching that name will be deleted.
	 *
	 * @param null|string $name The name of annotations to delete.
	 * @return bool
	 * @since 1.8
	 */
	public function deleteOwnedAnnotations($name = null) {
		// access is turned off for this because they might
		// no longer have access to an entity they created annotations on.
		$ia = elgg_set_ignore_access(true);
		$options = array(
			'annotation_owner_guid' => $this->guid,
			'limit' => 0
		);
		if ($name) {
			$options['annotation_name'] = $name;
		}

		$r = elgg_delete_annotations($options);
		elgg_set_ignore_access($ia);
		return $r;
	}

	/**
	 * Disables annotations for this entity, optionally based on name.
	 *
	 * @param string $name An options name of annotations to disable.
	 * @return bool
	 * @since 1.8
	 */
	public function disableAnnotations($name = '') {
		$options = array(
			'guid' => $this->guid,
			'limit' => 0
		);
		if ($name) {
			$options['annotation_name'] = $name;
		}

		return elgg_disable_annotations($options);
	}

	/**
	 * Enables annotations for this entity, optionally based on name.
	 *
	 * @warning Before calling this, you must use {@link access_show_hidden_entities()}
	 *
	 * @param string $name An options name of annotations to enable.
	 * @return bool
	 * @since 1.8
	 */
	public function enableAnnotations($name = '') {
		$options = array(
			'guid' => $this->guid,
			'limit' => 0
		);
		if ($name) {
			$options['annotation_name'] = $name;
		}

		return elgg_enable_annotations($options);
	}

	/**
	 * Helper function to return annotation calculation results
	 *
	 * @param string $name        The annotation name.
	 * @param string $calculation A valid MySQL function to run its values through
	 * @return mixed
	 */
	private function getAnnotationCalculation($name, $calculation) {
		$options = array(
			'guid' => $this->getGUID(),
			'annotation_name' => $name,
			'annotation_calculation' => $calculation
		);

		return elgg_get_annotations($options);
	}

	/**
	 * Adds an annotation to an entity.
	 *
	 * @warning By default, annotations are private.
	 *
	 * @warning Annotating an unsaved entity more than once with the same name
	 *          will only save the last annotation.
	 *
	 * @param string $name      Annotation name
	 * @param mixed  $value     Annotation value
	 * @param int    $access_id Access ID
	 * @param int    $owner_id  GUID of the annotation owner
	 * @param string $vartype   The type of annotation value
	 *
	 * @return bool
	 */
	function annotate($name, $value, $access_id = ACCESS_PRIVATE, $owner_id = 0, $vartype = "") {
		if ((int) $this->guid > 0) {
			return create_annotation($this->getGUID(), $name, $value, $vartype, $owner_id, $access_id);
		} else {
			$this->temp_annotations[$name] = $value;
		}
		return true;
	}

	/**
	 * Returns an array of annotations.
	 *
	 * @param string $name   Annotation name
	 * @param int    $limit  Limit
	 * @param int    $offset Offset
	 * @param string $order  Order by time: asc or desc
	 *
	 * @return array
	 */
	function getAnnotations($name, $limit = 50, $offset = 0, $order = "asc") {
		if ((int) ($this->guid) > 0) {

			$options = array(
				'guid' => $this->guid,
				'annotation_name' => $name,
				'limit' => $limit,
				'offset' => $offset,
			);

			if ($order != 'asc') {
				$options['reverse_order_by'] = true;
			}

			return elgg_get_annotations($options);
		} else if (isset($this->temp_annotations[$name])) {
			return array($this->temp_annotations[$name]);
		} else {
			return array();
		}
	}

	/**
	 * Remove an annotation or all annotations for this entity.
	 *
	 * @warning Calling this method with no or an empty argument will remove
	 * all annotations on the entity.
	 *
	 * @param string $name Annotation name
	 * @return bool
	 * @deprecated 1.8 Use ->deleteAnnotations()
	 */
	function clearAnnotations($name = "") {
		elgg_deprecated_notice('ElggEntity->clearAnnotations() is deprecated by ->deleteAnnotations()', 1.8);
		return $this->deleteAnnotations($name);
	}

	/**
	 * Count annotations.
	 *
	 * @param string $name The type of annotation.
	 *
	 * @return int
	 */
	function countAnnotations($name = "") {
		return $this->getAnnotationCalculation($name, 'count');
	}

	/**
	 * Get the average of an integer type annotation.
	 *
	 * @param string $name Annotation name
	 *
	 * @return int
	 */
	function getAnnotationsAvg($name) {
		return $this->getAnnotationCalculation($name, 'avg');
	}

	/**
	 * Get the sum of integer type annotations of a given name.
	 *
	 * @param string $name Annotation name
	 *
	 * @return int
	 */
	function getAnnotationsSum($name) {
		return $this->getAnnotationCalculation($name, 'sum');
	}

	/**
	 * Get the minimum of integer type annotations of given name.
	 *
	 * @param string $name Annotation name
	 *
	 * @return int
	 */
	function getAnnotationsMin($name) {
		return $this->getAnnotationCalculation($name, 'min');
	}

	/**
	 * Get the maximum of integer type annotations of a given name.
	 *
	 * @param string $name Annotation name
	 *
	 * @return int
	 */
	function getAnnotationsMax($name) {
		return $this->getAnnotationCalculation($name, 'max');
	}

	/**
	 * Count the number of comments attached to this entity.
	 *
	 * @return int Number of comments
	 * @since 1.8.0
	 */
	function countComments() {
		$params = array('entity' => $this);
		$num = elgg_trigger_plugin_hook('comments:count', $this->getType(), $params);

		if (is_int($num)) {
			return $num;
		} else {
			return $this->getAnnotationCalculation('generic_comment', 'count');
		}
	}

	/**
	 * Gets an array of entities with a relationship to this entity.
	 *
	 * @param string $relationship Relationship type (eg "friends")
	 * @param bool   $inverse      Is this an inverse relationship?
	 * @param int    $limit        Number of elements to return
	 * @param int    $offset       Indexing offset
	 *
	 * @return array|false An array of entities or false on failure
	 */
	function getEntitiesFromRelationship($relationship, $inverse = false, $limit = 50, $offset = 0) {
		return elgg_get_entities_from_relationship(array(
			'relationship' => $relationship,
			'relationship_guid' => $this->getGUID(),
			'inverse_relationship' => $inverse,
			'limit' => $limit,
			'offset' => $offset
		));
	}

	/**
	 * Gets the number of of entities from a specific relationship type
	 *
	 * @param string $relationship         Relationship type (eg "friends")
	 * @param bool   $inverse_relationship Invert relationship
	 *
	 * @return int|false The number of entities or false on failure
	 */
	function countEntitiesFromRelationship($relationship, $inverse_relationship = FALSE) {
		return elgg_get_entities_from_relationship(array(
			'relationship' => $relationship,
			'relationship_guid' => $this->getGUID(),
			'inverse_relationship' => $inverse_relationship,
			'count' => TRUE
		));
	}

	/**
	 * Can a user edit this entity.
	 *
	 * @param int $user_guid The user GUID, optionally (default: logged in user)
	 *
	 * @return bool
	 */
	function canEdit($user_guid = 0) {
		return can_edit_entity($this->getGUID(), $user_guid);
	}

	/**
	 * Can a user edit metadata on this entity
	 *
	 * @param ElggMetadata $metadata  The piece of metadata to specifically check
	 * @param int          $user_guid The user GUID, optionally (default: logged in user)
	 *
	 * @return bool
	 */
	function canEditMetadata($metadata = null, $user_guid = 0) {
		return can_edit_entity_metadata($this->getGUID(), $user_guid, $metadata);
	}

	/**
	 * Can a user add an entity to this container
	 *
	 * @param int    $user_guid The user.
	 * @param string $type      The type of entity we're looking to write
	 * @param string $subtype   The subtype of the entity we're looking to write
	 *
	 * @return bool
	 */
	public function canWriteToContainer($user_guid = 0, $type = 'all', $subtype = 'all') {
		return can_write_to_container($user_guid, $this->guid, $type, $subtype);
	}

	/**
	 * Can a user comment on an entity?
	 *
	 * @tip Can be overridden by registering for the permissions_check:comment,
	 * <entity type> plugin hook.
	 *
	 * @param int $user_guid User guid (default is logged in user)
	 *
	 * @return bool
	 */
	public function canComment($user_guid = 0) {
		if ($user_guid == 0) {
			$user_guid = elgg_get_logged_in_user_guid();
		}
		$user = get_entity($user_guid);

		// By default, we don't take a position of whether commenting is allowed
		// because it is handled by the subclasses of ElggEntity
		$params = array('entity' => $this, 'user' => $user);
		return elgg_trigger_plugin_hook('permissions_check:comment', $this->type, $params, null);
	}

	/**
	 * Can a user annotate an entity?
	 *
	 * @tip Can be overridden by registering for the permissions_check:annotate,
	 * <entity type> plugin hook.
	 *
	 * @tip If you want logged out users to annotate an object, do not call
	 * canAnnotate(). It's easier than using the plugin hook.
	 *
	 * @param int    $user_guid       User guid (default is logged in user)
	 * @param string $annotation_name The name of the annotation (default is unspecified)
	 *
	 * @return bool
	 */
	public function canAnnotate($user_guid = 0, $annotation_name = '') {
		if ($user_guid == 0) {
			$user_guid = elgg_get_logged_in_user_guid();
		}
		$user = get_entity($user_guid);

		$return = true;
		if (!$user) {
			$return = false;
		}

		$params = array(
			'entity' => $this,
			'user' => $user,
			'annotation_name' => $annotation_name,
		);
		return elgg_trigger_plugin_hook('permissions_check:annotate', $this->type, $params, $return);
	}

	/**
	 * Returns the access_id.
	 *
	 * @return int The access ID
	 */
	public function getAccessID() {
		return $this->get('access_id');
	}

	/**
	 * Returns the guid.
	 *
	 * @return int|null GUID
	 */
	public function getGUID() {
		return $this->get('guid');
	}

	/**
	 * Returns the entity type
	 *
	 * @return string Entity type
	 */
	public function getType() {
		return $this->get('type');
	}

	/**
	 * Returns the entity subtype string
	 *
	 * @note This returns a string.  If you want the id, use ElggEntity::subtype.
	 *
	 * @return string The entity subtype
	 */
	public function getSubtype() {
		// If this object hasn't been saved, then return the subtype string.
		if (!((int) $this->guid > 0)) {
			return $this->get('subtype');
		}

		return get_subtype_from_id($this->get('subtype'));
	}

	/**
	 * Get the guid of the entity's owner.
	 *
	 * @return int The owner GUID
	 */
	public function getOwnerGUID() {
		return $this->owner_guid;
	}

	/**
	 * Return the guid of the entity's owner.
	 *
	 * @return int The owner GUID
	 * @deprecated 1.8 Use getOwnerGUID()
	 */
	public function getOwner() {
		elgg_deprecated_notice("ElggEntity::getOwner deprecated for ElggEntity::getOwnerGUID", 1.8);
		return $this->getOwnerGUID();
	}

	/**
	 * Gets the ElggEntity that owns this entity.
	 *
	 * @return ElggEntity The owning entity
	 */
	public function getOwnerEntity() {
		return get_entity($this->owner_guid);
	}

	/**
	 * Set the container for this object.
	 *
	 * @param int $container_guid The ID of the container.
	 *
	 * @return bool
	 */
	public function setContainerGUID($container_guid) {
		$container_guid = (int)$container_guid;

		return $this->set('container_guid', $container_guid);
	}

	/**
	 * Set the container for this object.
	 *
	 * @param int $container_guid The ID of the container.
	 *
	 * @return bool
	 * @deprecated 1.8 use setContainerGUID()
	 */
	public function setContainer($container_guid) {
		elgg_deprecated_notice("ElggObject::setContainer deprecated for ElggEntity::setContainerGUID", 1.8);
		$container_guid = (int)$container_guid;

		return $this->set('container_guid', $container_guid);
	}

	/**
	 * Gets the container GUID for this entity.
	 *
	 * @return int
	 */
	public function getContainerGUID() {
		return $this->get('container_guid');
	}

	/**
	 * Gets the container GUID for this entity.
	 *
	 * @return int
	 * @deprecated 1.8 Use getContainerGUID()
	 */
	public function getContainer() {
		elgg_deprecated_notice("ElggObject::getContainer deprecated for ElggEntity::getContainerGUID", 1.8);
		return $this->get('container_guid');
	}

	/**
	 * Get the container entity for this object.
	 *
	 * @return ElggEntity
	 * @since 1.8.0
	 */
	public function getContainerEntity() {
		return get_entity($this->getContainerGUID());
	}

	/**
	 * Returns the UNIX epoch time that this entity was last updated
	 *
	 * @return int UNIX epoch time
	 */
	public function getTimeUpdated() {
		return $this->get('time_updated');
	}

	/**
	 * Returns the URL for this entity
	 *
	 * @return string The URL
	 * @see register_entity_url_handler()
	 * @see ElggEntity::setURL()
	 */
	public function getURL() {
		if (!empty($this->url_override)) {
			return $this->url_override;
		}
		return get_entity_url($this->getGUID());
	}

	/**
	 * Overrides the URL returned by getURL()
	 *
	 * @warning This override exists only for the life of the object.
	 *
	 * @param string $url The new item URL
	 *
	 * @return string The URL
	 */
	public function setURL($url) {
		$this->url_override = $url;
		return $url;
	}

	/**
	 * Get the URL for this entity's icon
	 *
	 * Plugins can register for the 'entity:icon:url', <type> plugin hook
	 * to customize the icon for an entity.
	 *
	 * @param string $size Size of the icon: tiny, small, medium, large
	 *
	 * @return string The URL
	 * @since 1.8.0
	 */
	public function getIconURL($size = 'medium') {
		$size = elgg_strtolower($size);

		if (isset($this->icon_override[$size])) {
			elgg_deprecated_notice("icon_override on an individual entity is deprecated", 1.8);
			return $this->icon_override[$size];
		}

		$type = $this->getType();
		$params = array(
			'entity' => $this,
			'size' => $size,
		);

		$url = elgg_trigger_plugin_hook('entity:icon:url', $type, $params, null);
		if ($url == null) {
			$url = "_graphics/icons/default/$size.png";
		}

		return elgg_normalize_url($url);
	}

	/**
	 * Returns a URL for the entity's icon.
	 *
	 * @param string $size Either 'large', 'medium', 'small' or 'tiny'
	 *
	 * @return string The url or false if no url could be worked out.
	 * @deprecated Use getIconURL()
	 */
	public function getIcon($size = 'medium') {
		elgg_deprecated_notice("getIcon() deprecated by getIconURL()", 1.8);
		return $this->getIconURL($size);
	}

	/**
	 * Set an icon override for an icon and size.
	 *
	 * @warning This override exists only for the life of the object.
	 *
	 * @param string $url  The url of the icon.
	 * @param string $size The size its for.
	 *
	 * @return bool
	 * @deprecated 1.8 See getIconURL() for the plugin hook to use
	 */
	public function setIcon($url, $size = 'medium') {
		elgg_deprecated_notice("icon_override on an individual entity is deprecated", 1.8);

		$url = sanitise_string($url);
		$size = sanitise_string($size);

		if (!$this->icon_override) {
			$this->icon_override = array();
		}
		$this->icon_override[$size] = $url;

		return true;
	}

	/**
	 * Tests to see whether the object has been fully loaded.
	 *
	 * @return bool
	 */
	public function isFullyLoaded() {
		return ! ($this->attributes['tables_loaded'] < $this->attributes['tables_split']);
	}

	/**
	 * Save an entity.
	 *
	 * @return bool|int
	 * @throws IOException
	 */
	public function save() {
		$guid = $this->getGUID();
		if ($guid > 0) {

			// See #5600. This ensures the lower level can_edit_entity() check will use a
			// fresh entity from the DB so it sees the persisted owner_guid
			_elgg_disable_caching_for_entity($guid);

			$ret = update_entity(
				$guid,
				$this->get('owner_guid'),
				$this->get('access_id'),
				$this->get('container_guid'),
				$this->get('time_created')
			);

			_elgg_enable_caching_for_entity($guid);
			_elgg_cache_entity($this);

			return $ret;
		} else {
			// Create a new entity (nb: using attribute array directly
			// 'cos set function does something special!)
			$this->attributes['guid'] = create_entity($this->attributes['type'],
				$this->attributes['subtype'], $this->attributes['owner_guid'],
				$this->attributes['access_id'], $this->attributes['site_guid'],
				$this->attributes['container_guid']);

			if (!$this->attributes['guid']) {
				throw new IOException(elgg_echo('IOException:BaseEntitySaveFailed'));
			}

			// Save any unsaved metadata
			// @todo How to capture extra information (access id etc)
			if (sizeof($this->temp_metadata) > 0) {
				foreach ($this->temp_metadata as $name => $value) {
					$this->$name = $value;
					unset($this->temp_metadata[$name]);
				}
			}

			// Save any unsaved annotations.
			if (sizeof($this->temp_annotations) > 0) {
				foreach ($this->temp_annotations as $name => $value) {
					$this->annotate($name, $value);
					unset($this->temp_annotations[$name]);
				}
			}

			// Save any unsaved private settings.
			if (sizeof($this->temp_private_settings) > 0) {
				foreach ($this->temp_private_settings as $name => $value) {
					$this->setPrivateSetting($name, $value);
					unset($this->temp_private_settings[$name]);
				}
			}

			// set the subtype to id now rather than a string
			$this->attributes['subtype'] = get_subtype_id($this->attributes['type'],
				$this->attributes['subtype']);

			_elgg_cache_entity($this);

			return $this->attributes['guid'];
		}
	}

	/**
	 * Loads attributes from the entities table into the object.
	 *
	 * @param mixed $guid GUID of entity or stdClass object from entities table
	 *
	 * @return bool
	 */
	protected function load($guid) {
		if ($guid instanceof stdClass) {
			$row = $guid;
		} else {
			$row = get_entity_as_row($guid);
		}

		if ($row) {
			// Create the array if necessary - all subclasses should test before creating
			if (!is_array($this->attributes)) {
				$this->attributes = array();
			}

			// Now put these into the attributes array as core values
			$objarray = (array) $row;
			foreach ($objarray as $key => $value) {
				$this->attributes[$key] = $value;
			}

			// Increment the portion counter
			if (!$this->isFullyLoaded()) {
				$this->attributes['tables_loaded']++;
			}

			// guid needs to be an int  https://github.com/elgg/elgg/issues/4111
			$this->attributes['guid'] = (int)$this->attributes['guid'];

			// Cache object handle
			if ($this->attributes['guid']) {
				_elgg_cache_entity($this);
			}

			return true;
		}

		return false;
	}

	/**
	 * Disable this entity.
	 *
	 * Disabled entities are not returned by getter functions.
	 * To enable an entity, use {@link enable_entity()}.
	 *
	 * Recursively disabling an entity will disable all entities
	 * owned or contained by the parent entity.
	 *
	 * @internal Disabling an entity sets the 'enabled' column to 'no'.
	 *
	 * @param string $reason    Optional reason
	 * @param bool   $recursive Recursively disable all contained entities?
	 *
	 * @return bool
	 * @see enable_entity()
	 * @see ElggEntity::enable()
	 */
	public function disable($reason = "", $recursive = true) {
		if ($r = disable_entity($this->get('guid'), $reason, $recursive)) {
			$this->attributes['enabled'] = 'no';
		}

		return $r;
	}

	/**
	 * Enable an entity
	 *
	 * @warning Disabled entities can't be loaded unless
	 * {@link access_show_hidden_entities(true)} has been called.
	 *
	 * @see enable_entity()
	 * @see access_show_hiden_entities()
	 * @return bool
	 */
	public function enable() {
		if ($r = enable_entity($this->get('guid'))) {
			$this->attributes['enabled'] = 'yes';
		}

		return $r;
	}

	/**
	 * Is this entity enabled?
	 *
	 * @return boolean
	 */
	public function isEnabled() {
		if ($this->enabled == 'yes') {
			return true;
		}

		return false;
	}

	/**
	 * Delete this entity.
	 *
	 * @param bool $recursive Whether to delete all the entities contained by this entity
	 *
	 * @return bool
	 */
	public function delete($recursive = true) {
		return delete_entity($this->get('guid'), $recursive);
	}

	/*
	 * LOCATABLE INTERFACE
	 */

	/**
	 * Gets the 'location' metadata for the entity
	 *
	 * @return string The location
	 */
	public function getLocation() {
		return $this->location;
	}

	/**
	 * Sets the 'location' metadata for the entity
	 *
	 * @todo Unimplemented
	 *
	 * @param string $location String representation of the location
	 *
	 * @return bool
	 */
	public function setLocation($location) {
		$this->location = $location;
		return true;
	}

	/**
	 * Set latitude and longitude metadata tags for a given entity.
	 *
	 * @param float $lat  Latitude
	 * @param float $long Longitude
	 *
	 * @return bool
	 * @todo Unimplemented
	 */
	public function setLatLong($lat, $long) {
		$this->set('geo:lat', $lat);
		$this->set('geo:long', $long);

		return true;
	}

	/**
	 * Return the entity's latitude.
	 *
	 * @return float
	 * @todo Unimplemented
	 */
	public function getLatitude() {
		return (float)$this->get('geo:lat');
	}

	/**
	 * Return the entity's longitude
	 *
	 * @return float
	 */
	public function getLongitude() {
		return (float)$this->get('geo:long');
	}

	/*
	 * NOTABLE INTERFACE
	 */

	/**
	 * Set the time and duration of an object
	 *
	 * @param int $hour     If ommitted, now is assumed.
	 * @param int $minute   If ommitted, now is assumed.
	 * @param int $second   If ommitted, now is assumed.
	 * @param int $day      If ommitted, now is assumed.
	 * @param int $month    If ommitted, now is assumed.
	 * @param int $year     If ommitted, now is assumed.
	 * @param int $duration Duration of event, remainder of the day is assumed.
	 *
	 * @return true
	 * @todo Unimplemented
	 */
	public function setCalendarTimeAndDuration($hour = NULL, $minute = NULL, $second = NULL,
	$day = NULL, $month = NULL, $year = NULL, $duration = NULL) {

		$start = mktime($hour, $minute, $second, $month, $day, $year);
		$end = $start + abs($duration);
		if (!$duration) {
			$end = get_day_end($day, $month, $year);
		}

		$this->calendar_start = $start;
		$this->calendar_end = $end;

		return true;
	}

	/**
	 * Returns the start timestamp.
	 *
	 * @return int
	 * @todo Unimplemented
	 */
	public function getCalendarStartTime() {
		return (int)$this->calendar_start;
	}

	/**
	 * Returns the end timestamp.
	 *
	 * @todo Unimplemented
	 *
	 * @return int
	 */
	public function getCalendarEndTime() {
		return (int)$this->calendar_end;
	}

	/*
	 * EXPORTABLE INTERFACE
	 */

	/**
	 * Returns an array of fields which can be exported.
	 *
	 * @return array
	 */
	public function getExportableValues() {
		return array(
			'guid',
			'type',
			'subtype',
			'time_created',
			'time_updated',
			'container_guid',
			'owner_guid',
			'site_guid'
		);
	}

	/**
	 * Export this class into an array of ODD Elements containing all necessary fields.
	 * Override if you wish to return more information than can be found in
	 * $this->attributes (shouldn't happen)
	 *
	 * @return array
	 */
	public function export() {
		$tmp = array();

		// Generate uuid
		$uuid = guid_to_uuid($this->getGUID());

		// Create entity
		$odd = new ODDEntity(
			$uuid,
			$this->attributes['type'],
			get_subtype_from_id($this->attributes['subtype'])
		);

		$tmp[] = $odd;

		$exportable_values = $this->getExportableValues();

		// Now add its attributes
		foreach ($this->attributes as $k => $v) {
			$meta = NULL;

			if (in_array($k, $exportable_values)) {
				switch ($k) {
					case 'guid':			// Dont use guid in OpenDD
					case 'type':			// Type and subtype already taken care of
					case 'subtype':
						break;

					case 'time_created':	// Created = published
						$odd->setAttribute('published', date("r", $v));
						break;

					case 'site_guid':	// Container
						$k = 'site_uuid';
						$v = guid_to_uuid($v);
						$meta = new ODDMetaData($uuid . "attr/$k/", $uuid, $k, $v);
						break;

					case 'container_guid':	// Container
						$k = 'container_uuid';
						$v = guid_to_uuid($v);
						$meta = new ODDMetaData($uuid . "attr/$k/", $uuid, $k, $v);
						break;

					case 'owner_guid':			// Convert owner guid to uuid, this will be stored in metadata
						$k = 'owner_uuid';
						$v = guid_to_uuid($v);
						$meta = new ODDMetaData($uuid . "attr/$k/", $uuid, $k, $v);
						break;

					default:
						$meta = new ODDMetaData($uuid . "attr/$k/", $uuid, $k, $v);
				}

				// set the time of any metadata created
				if ($meta) {
					$meta->setAttribute('published', date("r", $this->time_created));
					$tmp[] = $meta;
				}
			}
		}

		// Now we do something a bit special.
		/*
		 * This provides a rendered view of the entity to foreign sites.
		 */

		elgg_set_viewtype('default');
		$view = elgg_view_entity($this, array('full_view' => true));
		elgg_set_viewtype();

		$tmp[] = new ODDMetaData($uuid . "volatile/renderedentity/", $uuid,
			'renderedentity', $view, 'volatile');

		return $tmp;
	}

	/*
	 * IMPORTABLE INTERFACE
	 */

	/**
	 * Import data from an parsed ODD xml data array.
	 *
	 * @param ODD $data XML data
	 *
	 * @return true
	 *
	 * @throws InvalidParameterException
	 */
	public function import(ODD $data) {
		if (!($data instanceof ODDEntity)) {
			throw new InvalidParameterException(elgg_echo('InvalidParameterException:UnexpectedODDClass'));
		}

		// Set type and subtype
		$this->attributes['type'] = $data->getAttribute('class');
		$this->attributes['subtype'] = $data->getAttribute('subclass');

		// Set owner
		$this->attributes['owner_guid'] = elgg_get_logged_in_user_guid(); // Import as belonging to importer.

		// Set time
		$this->attributes['time_created'] = strtotime($data->getAttribute('published'));
		$this->attributes['time_updated'] = time();

		return true;
	}

	/*
	 * SYSTEM LOG INTERFACE
	 */

	/**
	 * Return an identification for the object for storage in the system log.
	 * This id must be an integer.
	 *
	 * @return int
	 */
	public function getSystemLogID() {
		return $this->getGUID();
	}

	/**
	 * For a given ID, return the object associated with it.
	 * This is used by the river functionality primarily.
	 *
	 * This is useful for checking access permissions etc on objects.
	 *
	 * @param int $id GUID.
	 *
	 * @todo How is this any different or more useful than get_entity($guid)
	 * or new ElggEntity($guid)?
	 *
	 * @return int GUID
	 */
	public function getObjectFromID($id) {
		return get_entity($id);
	}

	/**
	 * Returns tags for this entity.
	 *
	 * @warning Tags must be registered by {@link elgg_register_tag_metadata_name()}.
	 *
	 * @param array $tag_names Optionally restrict by tag metadata names.
	 *
	 * @return array
	 */
	public function getTags($tag_names = NULL) {
		if ($tag_names && !is_array($tag_names)) {
			$tag_names = array($tag_names);
		}

		$valid_tags = elgg_get_registered_tag_metadata_names();
		$entity_tags = array();

		foreach ($valid_tags as $tag_name) {
			if (is_array($tag_names) && !in_array($tag_name, $tag_names)) {
				continue;
			}

			if ($tags = $this->$tag_name) {
				// if a single tag, metadata returns a string.
				// if multiple tags, metadata returns an array.
				if (is_array($tags)) {
					$entity_tags = array_merge($entity_tags, $tags);
				} else {
					$entity_tags[] = $tags;
				}
			}
		}

		return $entity_tags;
	}
}
