<?php
/**
 * Elgg annotations
 * Functions to manage object annotations.
 *
 * @package Elgg
 * @subpackage Core
 */

/**
 * Convert a database row to a new ElggAnnotation
 *
 * @param stdClass $row Db row result object
 *
 * @return ElggAnnotation
 * @access private
 */
function row_to_elggannotation($row) {
	if (!($row instanceof stdClass)) {
		// @todo should throw in this case?
		return $row;
	}

	return new ElggAnnotation($row);
}

/**
 * Get a specific annotation by its id.
 * If you want multiple annotation objects, use
 * {@link elgg_get_annotations()}.
 *
 * @param int $id The id of the annotation object being retrieved.
 *
 * @return ElggAnnotation|false
 */
function elgg_get_annotation_from_id($id) {
	return elgg_get_metastring_based_object_from_id($id, 'annotations');
}

/**
 * Deletes an annotation using its ID.
 *
 * @param int $id The annotation ID to delete.
 * @return bool
 */
function elgg_delete_annotation_by_id($id) {
	$annotation = elgg_get_annotation_from_id($id);
	if (!$annotation) {
		return false;
	}
	return $annotation->delete();
}

/**
 * Create a new annotation.
 *
 * @param int    $entity_guid Entity Guid
 * @param string $name        Name of annotation
 * @param string $value       Value of annotation
 * @param string $value_type  Type of value (default is auto detection)
 * @param int    $owner_guid  Owner of annotation (default is logged in user)
 * @param int    $access_id   Access level of annotation
 *
 * @return int|bool id on success or false on failure
 */
function create_annotation($entity_guid, $name, $value, $value_type = '',
$owner_guid = 0, $access_id = ACCESS_PRIVATE) {
	global $CONFIG;

	$result = false;

	$entity_guid = (int)$entity_guid;
	//$name = sanitise_string(trim($name));
	//$value = sanitise_string(trim($value));
	$value_type = detect_extender_valuetype($value, sanitise_string(trim($value_type)));

	$owner_guid = (int)$owner_guid;
	if ($owner_guid == 0) {
		$owner_guid = elgg_get_logged_in_user_guid();
	}

	$access_id = (int)$access_id;
	$time = time();

	// Add the metastring
	$value = add_metastring($value);
	if (!$value) {
		return false;
	}

	$name = add_metastring($name);
	if (!$name) {
		return false;
	}

	$entity = get_entity($entity_guid);

	if (elgg_trigger_event('annotate', $entity->type, $entity)) {
		// If ok then add it
		$result = insert_data("INSERT into {$CONFIG->dbprefix}annotations
			(entity_guid, name_id, value_id, value_type, owner_guid, time_created, access_id) VALUES
			($entity_guid,'$name',$value,'$value_type', $owner_guid, $time, $access_id)");

		if ($result !== false) {
			$obj = elgg_get_annotation_from_id($result);
			if (elgg_trigger_event('create', 'annotation', $obj)) {
				return $result;
			} else {
				// plugin returned false to reject annotation
				elgg_delete_annotation_by_id($result);
				return FALSE;
			}
		}
	}

	return $result;
}

/**
 * Update an annotation.
 *
 * @param int    $annotation_id Annotation ID
 * @param string $name          Name of annotation
 * @param string $value         Value of annotation
 * @param string $value_type    Type of value
 * @param int    $owner_guid    Owner of annotation
 * @param int    $access_id     Access level of annotation
 *
 * @return bool
 */
function update_annotation($annotation_id, $name, $value, $value_type, $owner_guid, $access_id) {
	global $CONFIG;

	$annotation_id = (int)$annotation_id;
	$name = (trim($name));
	$value = (trim($value));
	$value_type = detect_extender_valuetype($value, sanitise_string(trim($value_type)));

	$owner_guid = (int)$owner_guid;
	if ($owner_guid == 0) {
		$owner_guid = elgg_get_logged_in_user_guid();
	}

	$access_id = (int)$access_id;

	$access = get_access_sql_suffix();

	// Add the metastring
	$value = add_metastring($value);
	if (!$value) {
		return false;
	}

	$name = add_metastring($name);
	if (!$name) {
		return false;
	}

	// If ok then add it
	$result = update_data("UPDATE {$CONFIG->dbprefix}annotations
		set name_id='$name', value_id='$value', value_type='$value_type', access_id=$access_id, owner_guid=$owner_guid
		where id=$annotation_id and $access");

	if ($result !== false) {
		// @todo add plugin hook that sends old and new annotation information before db access
		$obj = elgg_get_annotation_from_id($annotation_id);
		elgg_trigger_event('update', 'annotation', $obj);
	}

	return $result;
}

/**
 * Returns annotations.  Accepts all elgg_get_entities() options for entity
 * restraints.
 *
 * @see elgg_get_entities
 *
 * @param array $options Array in format:
 *
 * annotation_names              => NULL|ARR Annotation names
 * annotation_values             => NULL|ARR Annotation values
 * annotation_ids                => NULL|ARR annotation ids
 * annotation_case_sensitive     => BOOL Overall Case sensitive
 * annotation_owner_guids        => NULL|ARR guids for annotation owners
 * annotation_created_time_lower => INT Lower limit for created time.
 * annotation_created_time_upper => INT Upper limit for created time.
 * annotation_calculation        => STR Perform the MySQL function on the annotation values returned.
 *                                   Do not confuse this "annotation_calculation" option with the
 *                                   "calculation" option to elgg_get_entities_from_annotation_calculation().
 *                                   The "annotation_calculation" option causes this function to
 *                                   return the result of performing a mathematical calculation on
 *                                   all annotations that match the query instead of ElggAnnotation
 *                                   objects.
 *                                   See the docs for elgg_get_entities_from_annotation_calculation()
 *                                   for the proper use of the "calculation" option.
 *
 *
 * @return ElggAnnotation[]|mixed
 * @since 1.8.0
 */
function elgg_get_annotations(array $options = array()) {

	// @todo remove support for count shortcut - see #4393
	if (isset($options['__egefac']) && $options['__egefac']) {
		unset($options['__egefac']);
	} else {
		// support shortcut of 'count' => true for 'annotation_calculation' => 'count'
		if (isset($options['count']) && $options['count']) {
			$options['annotation_calculation'] = 'count';
			unset($options['count']);
		}		
	}
	
	$options['metastring_type'] = 'annotations';
	return elgg_get_metastring_based_objects($options);
}

/**
 * Deletes annotations based on $options.
 *
 * @warning Unlike elgg_get_annotations() this will not accept an empty options array!
 *          This requires at least one constraint: annotation_owner_guid(s),
 *          annotation_name(s), annotation_value(s), or guid(s) must be set.
 *
 * @param array $options An options array. {@See elgg_get_annotations()}
 * @return bool|null true on success, false on failure, null if no annotations to delete.
 * @since 1.8.0
 */
function elgg_delete_annotations(array $options) {
	if (!elgg_is_valid_options_for_batch_operation($options, 'annotations')) {
		return false;
	}

	$options['metastring_type'] = 'annotations';
	return elgg_batch_metastring_based_objects($options, 'elgg_batch_delete_callback', false);
}

/**
 * Disables annotations based on $options.
 *
 * @warning Unlike elgg_get_annotations() this will not accept an empty options array!
 *
 * @param array $options An options array. {@See elgg_get_annotations()}
 * @return bool|null true on success, false on failure, null if no annotations disabled.
 * @since 1.8.0
 */
function elgg_disable_annotations(array $options) {
	if (!elgg_is_valid_options_for_batch_operation($options, 'annotations')) {
		return false;
	}
	
	// if we can see hidden (disabled) we need to use the offset
	// otherwise we risk an infinite loop if there are more than 50
	$inc_offset = access_get_show_hidden_status();

	$options['metastring_type'] = 'annotations';
	return elgg_batch_metastring_based_objects($options, 'elgg_batch_disable_callback', $inc_offset);
}

/**
 * Enables annotations based on $options.
 *
 * @warning Unlike elgg_get_annotations() this will not accept an empty options array!
 *
 * @warning In order to enable annotations, you must first use
 * {@link access_show_hidden_entities()}.
 *
 * @param array $options An options array. {@See elgg_get_annotations()}
 * @return bool|null true on success, false on failure, null if no metadata enabled.
 * @since 1.8.0
 */
function elgg_enable_annotations(array $options) {
	if (!$options || !is_array($options)) {
		return false;
	}

	$options['metastring_type'] = 'annotations';
	return elgg_batch_metastring_based_objects($options, 'elgg_batch_enable_callback');
}

/**
 * Returns a rendered list of annotations with pagination.
 *
 * @param array $options Annotation getter and display options.
 * {@see elgg_get_annotations()} and {@see elgg_list_entities()}.
 *
 * @return string The list of entities
 * @since 1.8.0
 */
function elgg_list_annotations($options) {
	$defaults = array(
		'limit' => 25,
		'offset' => (int) max(get_input('annoff', 0), 0),
	);

	$options = array_merge($defaults, $options);

	return elgg_list_entities($options, 'elgg_get_annotations', 'elgg_view_annotation_list');
}

/**
 * Entities interfaces
 */

/**
 * Returns entities based upon annotations.  Also accepts all options available
 * to elgg_get_entities() and elgg_get_entities_from_metadata().
 *
 * Entity creation time is selected as maxtime. To sort based upon
 * this, pass 'order_by' => 'maxtime asc' || 'maxtime desc'
 *
 * @see elgg_get_entities
 * @see elgg_get_entities_from_metadata
 *
 * @param array $options Array in format:
 *
 * 	annotation_names => NULL|ARR annotations names
 *
 * 	annotation_values => NULL|ARR annotations values
 *
 * 	annotation_name_value_pairs => NULL|ARR (name = 'name', value => 'value',
 * 	'operator' => '=', 'case_sensitive' => TRUE) entries.
 * 	Currently if multiple values are sent via an array (value => array('value1', 'value2')
 * 	the pair's operator will be forced to "IN".
 *
 * 	annotation_name_value_pairs_operator => NULL|STR The operator to use for combining
 *  (name = value) OPERATOR (name = value); default AND
 *
 * 	annotation_case_sensitive => BOOL Overall Case sensitive
 *
 *  order_by_annotation => NULL|ARR (array('name' => 'annotation_text1', 'direction' => ASC|DESC,
 *  'as' => text|integer),
 *
 *  Also supports array('name' => 'annotation_text1')
 *
 *  annotation_owner_guids => NULL|ARR guids for annotaiton owners
 *
 * @return mixed If count, int. If not count, array. false on errors.
 * @since 1.7.0
 */
function elgg_get_entities_from_annotations(array $options = array()) {
	$defaults = array(
		'annotation_names'						=>	ELGG_ENTITIES_ANY_VALUE,
		'annotation_values'						=>	ELGG_ENTITIES_ANY_VALUE,
		'annotation_name_value_pairs'			=>	ELGG_ENTITIES_ANY_VALUE,

		'annotation_name_value_pairs_operator'	=>	'AND',
		'annotation_case_sensitive' 			=>	TRUE,
		'order_by_annotation'					=>	array(),

		'annotation_created_time_lower'			=>	ELGG_ENTITIES_ANY_VALUE,
		'annotation_created_time_upper'			=>	ELGG_ENTITIES_ANY_VALUE,

		'annotation_owner_guids'				=>	ELGG_ENTITIES_ANY_VALUE,

		'order_by'								=>	'maxtime desc',
		'group_by'								=>	'a.entity_guid'
	);

	$options = array_merge($defaults, $options);

	$singulars = array('annotation_name', 'annotation_value',
	'annotation_name_value_pair', 'annotation_owner_guid');

	$options = elgg_normalise_plural_options_array($options, $singulars);
	$options = elgg_entities_get_metastrings_options('annotation', $options);

	if (!$options) {
		return false;
	}

	// special sorting for annotations
	//@todo overrides other sorting
	$options['selects'][] = "max(n_table.time_created) as maxtime";
	$options['group_by'] = 'n_table.entity_guid';

	$time_wheres = elgg_get_entity_time_where_sql('a', $options['annotation_created_time_upper'],
		$options['annotation_created_time_lower']);

	if ($time_wheres) {
		$options['wheres'] = array_merge($options['wheres'], $time_wheres);
	}

	return elgg_get_entities_from_metadata($options);
}

/**
 * Returns a viewable list of entities from annotations.
 *
 * @param array $options Options array
 *
 * @see elgg_get_entities_from_annotations()
 * @see elgg_list_entities()
 *
 * @return string
 */
function elgg_list_entities_from_annotations($options = array()) {
	return elgg_list_entities($options, 'elgg_get_entities_from_annotations');
}

/**
 * Get entities ordered by a mathematical calculation on annotation values
 *
 * @param array $options An options array:
 * 	'calculation'            => The calculation to use. Must be a valid MySQL function.
 *                              Defaults to sum.  Result selected as 'annotation_calculation'.
 *                              Don't confuse this "calculation" option with the
 *                              "annotation_calculation" option to elgg_get_annotations().
 *                              This "calculation" option is applied to each entity's set of
 *                              annotations and is selected as annotation_calculation for that row.
 *                              See the docs for elgg_get_annotations() for proper use of the
 *                              "annotation_calculation" option.
 *	'order_by'               => The order for the sorting. Defaults to 'annotation_calculation desc'.
 *	'annotation_names'       => The names of annotations on the entity.
 *	'annotation_values'	     => The values of annotations on the entity.
 *
 *	'metadata_names'         => The name of metadata on the entity.
 *	'metadata_values'        => The value of metadata on the entitiy.
 *
 * @return mixed If count, int. If not count, array. false on errors.
 */
function elgg_get_entities_from_annotation_calculation($options) {
	$db_prefix = elgg_get_config('dbprefix');
	$defaults = array(
		'calculation' => 'sum',
		'order_by' => 'annotation_calculation desc'
	);

	$options = array_merge($defaults, $options);

	$function = sanitize_string(elgg_extract('calculation', $options, 'sum', false));

	// you must cast this as an int or it sorts wrong.
	$options['selects'][] = 'e.*';
	$options['selects'][] = "$function(cast(a_msv.string as signed)) as annotation_calculation";

	// need our own join to get the values because the lower level functions don't
	// add all the joins if it's a different callback.
	$options['joins'][] = "JOIN {$db_prefix}metastrings a_msv ON n_table.value_id = a_msv.id";

	// don't need access control because it's taken care of by elgg_get_annotations.
	$options['group_by'] = 'n_table.entity_guid';

	$options['callback'] = 'entity_row_to_elggstar';

	// see #4393
	// @todo remove after the 'count' shortcut is removed from elgg_get_annotations()
	$options['__egefac'] = true;

	return elgg_get_annotations($options);
}

/**
 * List entities from an annotation calculation.
 *
 * @see elgg_get_entities_from_annotation_calculation()
 *
 * @param array $options An options array.
 *
 * @return string
 */
function elgg_list_entities_from_annotation_calculation($options) {
	$defaults = array(
		'calculation' => 'sum',
		'order_by' => 'annotation_calculation desc'
	);
	$options = array_merge($defaults, $options);

	return elgg_list_entities($options, 'elgg_get_entities_from_annotation_calculation');
}

/**
 * Export the annotations for the specified entity
 *
 * @param string $hook        'export'
 * @param string $type        'all'
 * @param mixed  $returnvalue Default return value
 * @param mixed  $params      Parameters determining what annotations to export
 *
 * @elgg_plugin_hook export all
 *
 * @return array
 * @throws InvalidParameterException
 * @access private
 */
function export_annotation_plugin_hook($hook, $type, $returnvalue, $params) {
	// Sanity check values
	if ((!is_array($params)) && (!isset($params['guid']))) {
		throw new InvalidParameterException(elgg_echo('InvalidParameterException:GUIDNotForExport'));
	}

	if (!is_array($returnvalue)) {
		throw new InvalidParameterException(elgg_echo('InvalidParameterException:NonArrayReturnValue'));
	}

	$guid = (int)$params['guid'];
	$options = array('guid' => $guid, 'limit' => 0);
	if (isset($params['name'])) {
		$options['annotation_name'] = $params['name'];
	}

	$result = elgg_get_annotations($options);

	if ($result) {
		foreach ($result as $r) {
			$returnvalue[] = $r->export();
		}
	}

	return $returnvalue;
}

/**
 * Get the URL for this item of metadata, by default this links to the
 * export handler in the current view.
 *
 * @param int $id Annotation id
 *
 * @return mixed
 */
function get_annotation_url($id) {
	$id = (int)$id;

	if ($extender = elgg_get_annotation_from_id($id)) {
		return get_extender_url($extender);
	}
	return false;
}

/**
 * Check to see if a user has already created an annotation on an object
 *
 * @param int    $entity_guid     Entity guid
 * @param string $annotation_type Type of annotation
 * @param int    $owner_guid      Defaults to logged in user.
 *
 * @return bool
 * @since 1.8.0
 */
function elgg_annotation_exists($entity_guid, $annotation_type, $owner_guid = NULL) {
	global $CONFIG;

	if (!$owner_guid && !($owner_guid = elgg_get_logged_in_user_guid())) {
		return FALSE;
	}

	$entity_guid = sanitize_int($entity_guid);
	$owner_guid = sanitize_int($owner_guid);
	$annotation_type = sanitize_string($annotation_type);

	$sql = "SELECT a.id FROM {$CONFIG->dbprefix}annotations a" .
			" JOIN {$CONFIG->dbprefix}metastrings m ON a.name_id = m.id" .
			" WHERE a.owner_guid = $owner_guid AND a.entity_guid = $entity_guid" .
			" AND m.string = '$annotation_type'";

	if (get_data_row($sql)) {
		return TRUE;
	}

	return FALSE;
}

/**
 * Return the URL for a comment
 *
 * @param ElggAnnotation $comment The comment object 
 * @return string
 * @access private
 */
function elgg_comment_url_handler(ElggAnnotation $comment) {
	$entity = $comment->getEntity();
	if ($entity) {
		return $entity->getURL() . '#item-annotation-' . $comment->id;
	}
	return "";
}

/**
 * Register an annotation url handler.
 *
 * @param string $extender_name The name, default 'all'.
 * @param string $function_name The function.
 *
 * @return string
 */
function elgg_register_annotation_url_handler($extender_name = "all", $function_name) {
	return elgg_register_extender_url_handler('annotation', $extender_name, $function_name);
}

/**
 * Register annotation unit tests
 *
 * @param string $hook
 * @param string $type
 * @param array $value
 * @param array $params
 * @return array
 * @access private
 */
function annotations_test($hook, $type, $value, $params) {
	global $CONFIG;
	$value[] = $CONFIG->path . 'engine/tests/api/annotations.php';
	return $value;
}

/**
 * Initialize the annotation library
 * @access private
 */
function elgg_annotations_init() {
	elgg_register_annotation_url_handler('generic_comment', 'elgg_comment_url_handler');

	elgg_register_plugin_hook_handler("export", "all", "export_annotation_plugin_hook", 2);
	elgg_register_plugin_hook_handler('unit_test', 'system', 'annotations_test');
}

elgg_register_event_handler('init', 'system', 'elgg_annotations_init');
