<?php

/**
 * bbPress Common Engagements
 *
 * This file contains the common classes and functions for interacting with the
 * bbPress engagements API. See `includes/users/engagements.php` for more.
 *
 * @package bbPress
 * @subpackage Common
 */

// Exit if accessed directly
defined( 'ABSPATH' ) || exit;

/**
 * Return the strategy used for storing user engagements
 *
 * @since 2.6.0 bbPress (r6722)
 *
 * @param string $rel_key  The key used to index this relationship
 * @param string $rel_type The type of meta to look in
 *
 * @return string
 */
function bbp_user_engagements_interface( $rel_key = '', $rel_type = 'post' ) {
	return apply_filters( 'bbp_user_engagements_interface', bbpress()->engagements, $rel_key, $rel_type );
}

/**
 * Base strategy class for interfacing with User Engagements, which other
 * classes will extend.
 *
 * @since 2.6.0 bbPress (r6722)
 */
class BBP_User_Engagements_Base {

	/**
	 * Type of strategy being used.
	 *
	 * @since 2.6.0 bbPress (r6737)
	 *
	 * @var string
	 */
	public $type = '';

	/**
	 * Add a user id to an object
	 *
	 * @since 2.6.0 bbPress (r6722)
	 *
	 * @param int    $object_id The object id
	 * @param int    $user_id   The user id
	 * @param string $meta_key  The relationship key
	 * @param string $meta_type The relationship type (usually 'post')
	 * @param bool   $unique    Whether meta key should be unique to the object
	 *
	 * @return bool Returns true on success, false on failure
	 */
	public function add_user_to_object( $object_id = 0, $user_id = 0, $meta_key = '', $meta_type = 'post', $unique = false ) {

	}

	/**
	 * Remove a user id from an object
	 *
	 * @since 2.6.0 bbPress (r6722)
	 *
	 * @param int    $object_id The object id
	 * @param int    $user_id   The user id
	 * @param string $meta_key  The relationship key
	 * @param string $meta_type The relationship type (usually 'post')
	 *
	 * @return bool Returns true on success, false on failure
	 */
	public function remove_user_from_object( $object_id = 0, $user_id = 0, $meta_key = '', $meta_type = 'post' ) {

	}

	/**
	 * Remove a user id from all objects
	 *
	 * @since 2.6.0 bbPress (r6722)
	 *
	 * @param int    $user_id   The user id
	 * @param string $meta_key  The relationship key
	 * @param string $meta_type The relationship type (usually 'post')
	 *
	 * @return bool Returns true on success, false on failure
	 */
	public function remove_user_from_all_objects( $user_id = 0, $meta_key = '', $meta_type = 'post' ) {

	}

	/**
	 * Remove an object from all users
	 *
	 * @since 2.6.0 bbPress (r6722)
	 *
	 * @param int    $object_id The object id
	 * @param int    $user_id   The user id
	 * @param string $meta_key  The relationship key
	 * @param string $meta_type The relationship type (usually 'post')
	 *
	 * @return bool Returns true on success, false on failure
	 */
	public function remove_object_from_all_users( $object_id = 0, $meta_key = '', $meta_type = 'post' ) {

	}

	/**
	 * Remove all users from all objects
	 *
	 * @since 2.6.0 bbPress (r6722)
	 *
	 * @param string $meta_key  The relationship key
	 * @param string $meta_type The relationship type (usually 'post')
	 *
	 * @return bool Returns true on success, false on failure
	 */
	public function remove_all_users_from_all_objects( $meta_key = '', $meta_type = 'post' ) {

	}

	/**
	 * Get users of an object
	 *
	 * @since 2.6.0 bbPress (r6722)
	 *
	 * @param int    $object_id The object id
	 * @param string $meta_key  The key used to index this relationship
	 * @param string $meta_type The type of meta to look in
	 *
	 * @return array Returns ids of users
	 */
	public function get_users_for_object( $object_id = 0, $meta_key = '', $meta_type = 'post' ) {

	}

	/**
	 * Get the part of the query responsible for JOINing objects to relationships.
	 *
	 * @since 2.6.0 bbPress (r6737)
	 *
	 * @param array  $args
	 * @param string $meta_key
	 * @param string $meta_type
	 *
	 * @return array
	 */
	public function get_query( $args = array(), $context_key = '', $meta_key = '', $meta_type = 'post' ) {

	}
}

/**
 * Meta strategy for interfacing with User Engagements
 *
 * @since 2.6.0 bbPress (r6722)
 */
class BBP_User_Engagements_Meta extends BBP_User_Engagements_Base {

	/**
	 * Type of strategy being used.
	 *
	 * @since 2.6.0 bbPress (r6737)
	 *
	 * @var string
	 */
	public $type = 'meta';

	/**
	 * Add a user id to an object
	 *
	 * @since 2.6.0 bbPress (r6722)
	 *
	 * @param int    $object_id The object id
	 * @param int    $user_id   The user id
	 * @param string $meta_key  The relationship key
	 * @param string $meta_type The relationship type (usually 'post')
	 * @param bool   $unique    Whether meta key should be unique to the object
	 *
	 * @return bool Returns true on success, false on failure
	 */
	public function add_user_to_object( $object_id = 0, $user_id = 0, $meta_key = '', $meta_type = 'post', $unique = false ) {
		return add_metadata( $meta_type, $object_id, $meta_key, $user_id, $unique );
	}

	/**
	 * Remove a user id from an object
	 *
	 * @since 2.6.0 bbPress (r6722)
	 *
	 * @param int    $object_id The object id
	 * @param int    $user_id   The user id
	 * @param string $meta_key  The relationship key
	 * @param string $meta_type The relationship type (usually 'post')
	 *
	 * @return bool Returns true on success, false on failure
	 */
	public function remove_user_from_object( $object_id = 0, $user_id = 0, $meta_key = '', $meta_type = 'post' ) {
		return delete_metadata( $meta_type, $object_id, $meta_key, $user_id, false );
	}

	/**
	 * Remove a user id from all objects
	 *
	 * @since 2.6.0 bbPress (r6722)
	 *
	 * @param int    $user_id   The user id
	 * @param string $meta_key  The relationship key
	 * @param string $meta_type The relationship type (usually 'post')
	 *
	 * @return bool Returns true on success, false on failure
	 */
	public function remove_user_from_all_objects( $user_id = 0, $meta_key = '', $meta_type = 'post' ) {
		return delete_metadata( $meta_type, null, $meta_key, $user_id, true );
	}

	/**
	 * Remove an object from all users
	 *
	 * @since 2.6.0 bbPress (r6722)
	 *
	 * @param int    $object_id The object id
	 * @param int    $user_id   The user id
	 * @param string $meta_key  The relationship key
	 * @param string $meta_type The relationship type (usually 'post')
	 *
	 * @return bool Returns true on success, false on failure
	 */
	public function remove_object_from_all_users( $object_id = 0, $meta_key = '', $meta_type = 'post' ) {
		return delete_metadata( $meta_type, $object_id, $meta_key, null, false );
	}

	/**
	 * Remove all users from all objects
	 *
	 * @since 2.6.0 bbPress (r6722)
	 *
	 * @param string $meta_key  The relationship key
	 * @param string $meta_type The relationship type (usually 'post')
	 *
	 * @return bool Returns true on success, false on failure
	 */
	public function remove_all_users_from_all_objects( $meta_key = '', $meta_type = 'post' ) {
		return delete_metadata( $meta_type, null, $meta_key, null, true );
	}

	/**
	 * Get users of an object
	 *
	 * @since 2.6.0 bbPress (r6722)
	 *
	 * @param int    $object_id The object id
	 * @param string $meta_key  The key used to index this relationship
	 * @param string $meta_type The type of meta to look in
	 *
	 * @return array Returns ids of users
	 */
	public function get_users_for_object( $object_id = 0, $meta_key = '', $meta_type = 'post' ) {
		return wp_parse_id_list( get_metadata( $meta_type, $object_id, $meta_key, false ) );
	}

	/**
	 * Get the part of the query responsible for JOINing objects to relationships.
	 *
	 * @since 2.6.0 bbPress (r6737)
	 *
	 * @param array  $args
	 * @param string $meta_key
	 * @param string $meta_type
	 *
	 * @return array
	 */
	public function get_query( $args = array(), $context_key = '', $meta_key = '', $meta_type = 'post' ) {

		// Backwards compat for pre-2.6.0
		if ( is_numeric( $args ) ) {
			$args = array(
				'meta_query' => array( array(
					'key'     => $meta_key,
					'value'   => bbp_get_user_id( $args, false, false ),
					'compare' => 'NUMERIC'
				) )
			);
		}

		// Default arguments
		$defaults = array(
			'meta_query' => array( array(
				'key'     => $meta_key,
				'value'   => bbp_get_displayed_user_id(),
				'compare' => 'NUMERIC'
			) )
		);

		// Parse arguments
		return bbp_parse_args( $args, $defaults, $context_key );
	}
}

/**
 * Term strategy for interfacing with User Engagements
 *
 * @since 2.6.0 bbPress (r6737)
 */
class BBP_User_Engagements_Term extends BBP_User_Engagements_Base {

	/**
	 * Type of strategy being used.
	 *
	 * @since 2.6.0 bbPress (r6737)
	 *
	 * @var string
	 */
	public $type = 'term';

	/**
	 * Register an engagement taxonomy just-in-time for immediate use
	 *
	 * @since 2.6.0 bbPress (r6737)
	 *
	 * @param string $tax_key
	 * @param string $object_type
	 */
	private function jit_taxonomy( $tax_key = '', $object_type = 'user' ) {

		// Bail if taxonomy already exists
		if ( taxonomy_exists( $tax_key ) ) {
			return;
		}

		// Register the taxonomy
		register_taxonomy( $tax_key, 'bbp_' . $object_type, array(
			'labels'                => array(),
			'description'           => '',
			'public'                => false,
			'publicly_queryable'    => false,
			'hierarchical'          => false,
			'show_ui'               => false,
			'show_in_menu'          => false,
			'show_in_nav_menus'     => false,
			'show_tagcloud'         => false,
			'show_in_quick_edit'    => false,
			'show_admin_column'     => false,
			'meta_box_cb'           => false,
			'capabilities'          => array(),
			'rewrite'               => false,
			'query_var'             => '',
			'update_count_callback' => '',
			'show_in_rest'          => false,
			'rest_base'             => false,
			'rest_controller_class' => false,
			'_builtin'              => false
		) );
	}

	/**
	 * Add a user id to an object
	 *
	 * @since 2.6.0 bbPress (r6737)
	 *
	 * @param int    $object_id The object id
	 * @param int    $user_id   The user id
	 * @param string $meta_key  The relationship key
	 * @param string $meta_type The relationship type (usually 'post')
	 * @param bool   $unique    Whether meta key should be unique to the object
	 *
	 * @return bool Returns true on success, false on failure
	 */
	public function add_user_to_object( $object_id = 0, $user_id = 0, $meta_key = '', $meta_type = 'post', $unique = false ) {
		$user_key = "{$meta_key}_user_id_{$user_id}";
		$tax_key  = "{$meta_key}_{$meta_type}";
		$this->jit_taxonomy( $tax_key );

		return wp_add_object_terms( $object_id, $user_key, $tax_key );
	}

	/**
	 * Remove a user id from an object
	 *
	 * @since 2.6.0 bbPress (r6737)
	 *
	 * @param int    $object_id The object id
	 * @param int    $user_id   The user id
	 * @param string $meta_key  The relationship key
	 * @param string $meta_type The relationship type (usually 'post')
	 *
	 * @return bool Returns true on success, false on failure
	 */
	public function remove_user_from_object( $object_id = 0, $user_id = 0, $meta_key = '', $meta_type = 'post' ) {
		$user_key = "{$meta_key}_user_id_{$user_id}";
		$tax_key  = "{$meta_key}_{$meta_type}";
		$this->jit_taxonomy( $tax_key );

		return wp_remove_object_terms( $object_id, $user_key, $tax_key );
	}

	/**
	 * Remove a user id from all objects
	 *
	 * @since 2.6.0 bbPress (r6737)
	 *
	 * @param int    $user_id   The user id
	 * @param string $meta_key  The relationship key
	 * @param string $meta_type The relationship type (usually 'post')
	 *
	 * @return bool Returns true on success, false on failure
	 */
	public function remove_user_from_all_objects( $user_id = 0, $meta_key = '', $meta_type = 'post' ) {
		$user_key = "{$meta_key}_user_id_{$user_id}";
		$tax_key  = "{$meta_key}_{$meta_type}";
		$this->jit_taxonomy( $tax_key );
		$term     = get_term_by( 'slug', $user_key, $tax_key );

		return wp_delete_term( $term->term_id, $tax_key );
	}

	/**
	 * Remove an object from all users
	 *
	 * @since 2.6.0 bbPress (r6737)
	 *
	 * @param int    $object_id The object id
	 * @param int    $user_id   The user id
	 * @param string $meta_key  The relationship key
	 * @param string $meta_type The relationship type (usually 'post')
	 *
	 * @return bool Returns true on success, false on failure
	 */
	public function remove_object_from_all_users( $object_id = 0, $meta_key = '', $meta_type = 'post' ) {
		return wp_delete_object_term_relationships( $object_id, get_object_taxonomies( 'bbp_user' ) );
	}

	/**
	 * Remove all users from all objects
	 *
	 * @since 2.6.0 bbPress (r6737)
	 *
	 * @param string $meta_key  The relationship key
	 * @param string $meta_type The relationship type (usually 'post')
	 *
	 * @return bool Returns true on success, false on failure
	 */
	public function remove_all_users_from_all_objects( $meta_key = '', $meta_type = 'post' ) {
		// TODO
	}

	/**
	 * Get users of an object
	 *
	 * @since 2.6.0 bbPress (r6737)
	 *
	 * @param int    $object_id The object id
	 * @param string $meta_key  The key used to index this relationship
	 * @param string $meta_type The type of meta to look in
	 *
	 * @return array Returns ids of users
	 */
	public function get_users_for_object( $object_id = 0, $meta_key = '', $meta_type = 'post' ) {
		$user_key = "{$meta_key}_user_id_";
		$tax_key  = "{$meta_key}_{$meta_type}";
		$this->jit_taxonomy( $tax_key );

		// Get terms
		$terms = get_terms( array(
			'object_ids' => $object_id,
			'taxonomy'   => $tax_key
		) );

		// Slug part to replace
		$user_ids = array();

		// Loop through terms and get the user ID
		foreach ( $terms as $term ) {
			$user_ids[] = str_replace( $user_key, '', $term->slug );
		}

		// Parse & return
		return wp_parse_id_list( $user_ids );
	}

	/**
	 * Get the part of the query responsible for JOINing objects to relationships.
	 *
	 * @since 2.6.0 bbPress (r6737)
	 *
	 * @param array  $args
	 * @param string $meta_key
	 * @param string $meta_type
	 *
	 * @return array
	 */
	public function get_query( $args = array(), $context_key = '', $meta_key = '', $meta_type = 'post' ) {
		$tax_key  = "{$meta_key}_{$meta_type}";
		$user_key = "{$meta_key}_user_id_";

		// Make sure the taxonomy is registered
		$this->jit_taxonomy( $tax_key );

		// Backwards compat for pre-2.6.0
		if ( is_numeric( $args ) ) {
			$args = array(
				'tax_query' => array( array(
					'taxonomy' => $tax_key,
					'terms'    => $user_key . bbp_get_user_id( $args, false, false ),
					'field'    => 'slug'
				) )
			);
		}

		// Default arguments
		$defaults = array(
			'tax_query' => array( array(
				'taxonomy' => $tax_key,
				'terms'    => $user_key . bbp_get_displayed_user_id(),
				'field'    => 'slug'
			) )
		);

		// Parse arguments
		return bbp_parse_args( $args, $defaults, $context_key );
	}
}

/**
 * User strategy for interfacing with User Engagements
 *
 * This strategy largely exists for backwards compatibility with bbPress 2.5,
 * or installations that have not upgraded their databases to 2.6 or above.
 *
 * Note: this strategy is going to be a bit less tidy than the others, because
 * it needs to do weird things to maintain the 2.5 status-quo. Do not use this
 * strategy as an example when building your own.
 *
 * @since 2.6.0 bbPress (r6844)
 */
class BBP_User_Engagements_User extends BBP_User_Engagements_Base {

	/**
	 * Type of strategy being used.
	 *
	 * @since 2.6.0 bbPress (r6844)
	 *
	 * @var string
	 */
	public $type = 'user';

	/**
	 * Private function to map 2.6 meta keys to 2.5 user-option keys.
	 *
	 * @since 2.6.0 bbPress (r6844)
	 *
	 * @param string $meta_key
	 * @param int    $object_id
	 * @param bool   $prefix
	 *
	 * @return string
	 */
	private function get_user_option_key( $meta_key = '', $object_id = 0, $prefix = false ) {
		switch ( $meta_key ) {

			// Favorites
			case '_bbp_favorite' :
				$key = '_bbp_favorites';
				break;

			// Subscriptions
			case '_bbp_subscription' :

				// Maybe guess at post type
				$post_type = ! empty( $object_id )
					? get_post_type( $object_id )
					: bbp_get_topic_post_type();

				// Forums & Topics used different keys :/
				$key = ( bbp_get_forum_post_type() === $post_type )
					? '_bbp_forum_subscriptions'
					: '_bbp_subscriptions';

				break;

			// Unknown, so pluralize
			default :
				$key = "{$meta_key}s";
				break;
		}

		// Maybe prefix the key (for use in raw database queries)
		if ( true === $prefix ) {
			$key = bbp_db()->get_blog_prefix() . $key;
		}

		// Return the old (pluralized) user option key
		return $key;
	}

	/**
	 * Private function to get a 2.5 compatible cache key.
	 *
	 * This method exists to provide backwards compatibility with bbPress 2.5,
	 * which had caching surrounding the FIND_IN_SET usermeta queries.
	 *
	 * @since 2.6.3 bbPress (r6991)
	 *
	 * @param string $meta_key
	 * @param int    $object_id
	 *
	 * @return string
	 */
	private function get_cache_key( $meta_key = '', $object_id = 0 ) {

		// No negative numbers in cache keys (zero is weird, but not disallowed)
		$object_id = absint( $object_id );

		// Maybe guess at post type
		$post_type = ! empty( $object_id )
			? get_post_type( $object_id )
			: bbp_get_topic_post_type();

		switch ( $meta_key ) {

			// Favorites
			case '_bbp_favorite' :
				$key = 'bbp_get_topic_favoriters_';
				break;

			// Subscriptions
			case '_bbp_subscription' :

				// Forums & Topics used different keys :/
				$key = ( bbp_get_forum_post_type() === $post_type )
					? 'bbp_get_forum_subscribers_'
					: 'bbp_get_topic_subscribers_';

				break;

			// Unknown, so pluralize
			default :
				$nounize = rtrim( $meta_key, 'e' );
				$key     = "bbp_get_{$post_type}_{$nounize}ers_";
				break;
		}

		// Return the old (pluralized) user option key with object ID appended
		return "{$key}{$object_id}";
	}

	/**
	 * Get the user engagement cache for a given meta key and object ID.
	 *
	 * This method exists to provide backwards compatibility with bbPress 2.5,
	 * which had caching surrounding the FIND_IN_SET queries in usermeta.
	 *
	 * @since 2.6.3 bbPress (r6991)
	 *
	 * @param string $meta_key
	 * @param int    $object_id
	 *
	 * @return mixed Results from cache get
	 */
	private function cache_get( $meta_key = '', $object_id = 0 ) {
		$cache_key = $this->get_cache_key( $meta_key, $object_id );

		return wp_cache_get( $cache_key, 'bbpress_engagements' );
	}

	/**
	 * Set the user engagement cache for a given meta key and object ID.
	 *
	 * This method exists to provide backwards compatibility with bbPress 2.5,
	 * which had caching surrounding the FIND_IN_SET queries in usermeta.
	 *
	 * @since 2.6.3 bbPress (r6991)
	 *
	 * @param string $meta_key
	 * @param int    $object_id
	 *
	 * @return mixed Results from cache set
	 */
	private function cache_set( $meta_key = '', $object_id = 0, $user_ids = array() ) {
		$cache_key = $this->get_cache_key( $meta_key, $object_id );
		$user_ids  = $this->parse_comma_list( $user_ids );

		return wp_cache_set( $cache_key, $user_ids, 'bbpress_engagements' );
	}

	/**
	 * Delete the user engagement cache for a given meta key and object ID.
	 *
	 * This method exists to provide backwards compatibility with bbPress 2.5,
	 * which had caching surrounding the FIND_IN_SET queries in usermeta.
	 *
	 * @since 2.6.3 bbPress (r6991)
	 *
	 * @param string $meta_key
	 * @param int    $object_id
	 *
	 * @return mixed Results from cache delete
	 */
	private function cache_delete( $meta_key = '', $object_id = 0 ) {
		$cache_key = $this->get_cache_key( $meta_key, $object_id );

		return wp_cache_delete( $cache_key, 'bbpress_engagements' );
	}

	/**
	 * Turn a comma-separated string into an array of integers
	 *
	 * @since 2.6.0 bbPress (r6844)
	 *
	 * @param string $results
	 * @return array
	 */
	private function parse_comma_list( $results = '' ) {
		return array_filter( wp_parse_id_list( $results ) );
	}

	/**
	 * Add a user id to an object
	 *
	 * @since 2.6.0 bbPress (r6844)
	 *
	 * @param int    $object_id The object id
	 * @param int    $user_id   The user id
	 * @param string $meta_key  The relationship key
	 * @param string $meta_type The relationship type (usually 'post')
	 * @param bool   $unique    Whether meta key should be unique to the object
	 *
	 * @return bool Returns true on success, false on failure
	 */
	public function add_user_to_object( $object_id = 0, $user_id = 0, $meta_key = '', $meta_type = 'post', $unique = false ) {
		$retval     = false;
		$option_key = $this->get_user_option_key( $meta_key, $object_id );
		$object_ids = $this->parse_comma_list( get_user_option( $option_key, $user_id ) );
		$exists     = array_search( $object_id, $object_ids );

		// Not already added, so add it
		if ( false === $exists ) {
			$object_ids[] = $object_id;
			$object_ids   = implode( ',', $this->parse_comma_list( $object_ids ) );
			$retval       = update_user_option( $user_id, $option_key, $object_ids );

			// Delete cache if successful (accounts for int & true)
			if ( false !== $retval ) {
				$this->cache_delete( $meta_key, $object_id );
			}
		}

		// Return true if added, or false if not
		return $retval;
	}

	/**
	 * Remove a user id from an object
	 *
	 * @since 2.6.0 bbPress (r6844)
	 *
	 * @param int    $object_id The object id
	 * @param int    $user_id   The user id
	 * @param string $meta_key  The relationship key
	 * @param string $meta_type The relationship type (usually 'post')
	 *
	 * @return bool Returns true on success, false on failure
	 */
	public function remove_user_from_object( $object_id = 0, $user_id = 0, $meta_key = '', $meta_type = 'post' ) {
		$retval     = false;
		$option_key = $this->get_user_option_key( $meta_key, $object_id );
		$object_ids = $this->parse_comma_list( get_user_option( $option_key, $user_id ) );
		$exists     = array_search( $object_id, $object_ids );

		// Exists, so remove it
		if ( false !== $exists ) {
			unset( $object_ids[ $exists ] );

			$object_ids = implode( ',', $this->parse_comma_list( $object_ids ) );
			$retval     = ! empty( $object_ids )
				? update_user_option( $user_id, $option_key, $object_ids )
				: delete_user_option( $user_id, $option_key );

			// Delete cache if successful (accounts for int & true)
			if ( false !== $retval ) {
				$this->cache_delete( $meta_key, $object_id );
			}
		}

		// Return true if removed, or false if not
		return $retval;
	}

	/**
	 * Remove a user id from all objects
	 *
	 * @since 2.6.0 bbPress (r6844)
	 *
	 * @param int    $user_id   The user id
	 * @param string $meta_key  The relationship key
	 * @param string $meta_type The relationship type (usually 'post')
	 *
	 * @return bool Returns true on success, false on failure
	 */
	public function remove_user_from_all_objects( $user_id = 0, $meta_key = '', $meta_type = 'post' ) {

		// Get the key
		$option_key = $this->get_user_option_key( $meta_key );

		// Get the option
		$object_ids = $this->parse_comma_list( get_user_option( $option_key, $user_id ) );

		// Attempt to delete the user option
		$retval = delete_user_option( $user_id, $option_key );

		// Try to delete caches, but only if everything else succeeded
		if ( ! empty( $retval ) && ! empty( $object_ids ) ) {
			foreach ( $object_ids as $object_id ) {
				$this->cache_delete( $meta_key, $object_id );
			}
		}

		// Return true if user was removed, or false if not
		return $retval;
	}

	/**
	 * Remove an object from all users
	 *
	 * @since 2.6.0 bbPress (r6844)
	 *
	 * @param int    $object_id The object id
	 * @param int    $user_id   The user id
	 * @param string $meta_key  The relationship key
	 * @param string $meta_type The relationship type (usually 'post')
	 *
	 * @return bool Returns true on success, false on failure
	 */
	public function remove_object_from_all_users( $object_id = 0, $meta_key = '', $meta_type = 'post' ) {

		// Query for users
		$user_ids = $this->get_users_for_object( $object_id, $meta_key, $meta_type );
		$u_count  = count( $user_ids );

		// Count number of removals
		$removed  = array();
		$r_count  = 0;

		// Users have engaged, so remove them
		if ( ! empty( $u_count ) ) {

			// Loop through users and remove them from the object
			foreach ( $user_ids as $user_id ) {
				$removed[] = $this->remove_user_from_object( $object_id, $user_id, $meta_key, $meta_type );
			}

			// Count the removed users
			$r_count = count( $removed );
		}

		// Return true if successfully removed from all users
		return ( $r_count === $u_count );
	}

	/**
	 * Remove all users from all objects
	 *
	 * @since 2.6.0 bbPress (r6844)
	 *
	 * @param string $meta_key  The relationship key
	 * @param string $meta_type The relationship type (usually 'post')
	 *
	 * @return bool Returns true on success, false on failure
	 */
	public function remove_all_users_from_all_objects( $meta_key = '', $meta_type = 'post' ) {

		// Query for users
		$option_key = $this->get_user_option_key( $meta_key, 0, true );
		$bbp_db     = bbp_db();
		$user_ids   = $bbp_db->get_col( "SELECT user_id FROM {$bbp_db->usermeta} WHERE meta_key = '{$option_key}'" );
		$u_count    = count( $user_ids );

		// Count number of removals
		$removed    = array();
		$r_count    = 0;

		// Users have engaged, so remove them
		if ( ! empty( $u_count ) ) {

			// Loop through users and remove their user options
			foreach ( $user_ids as $user_id ) {
				$removed[] = $this->remove_user_from_all_objects( $user_id, $meta_key );
			}

			// Count the removed users
			$r_count = count( $removed );
		}

		// Return true if successfully removed from all users
		return ( $r_count === $u_count );
	}

	/**
	 * Get users of an object
	 *
	 * The database queries in this function were cached in bbPress versions
	 * older than 2.6, but no longer are to avoid cache pollution.
	 *
	 * @since 2.6.0 bbPress (r6844)
	 *
	 * @param int    $object_id The object id
	 * @param string $meta_key  The key used to index this relationship
	 * @param string $meta_type The type of meta to look in
	 *
	 * @return array Returns ids of users
	 */
	public function get_users_for_object( $object_id = 0, $meta_key = '', $meta_type = 'post' ) {

		// Try to get user IDs from cache
		$user_ids = $this->cache_get( $meta_key, $object_id );

		// Cache is empty, so hit the database
		if ( false === $user_ids ) {
			$option_key = $this->get_user_option_key( $meta_key, $object_id, true );
			$bbp_db     = bbp_db();
			$user_ids   = $bbp_db->get_col( "SELECT user_id FROM {$bbp_db->usermeta} WHERE meta_key = '{$option_key}' and FIND_IN_SET('{$object_id}', meta_value) > 0" );

			// Always cache results (even if empty, to prevent multiple misses)
			$this->cache_set( $meta_key, $object_id, $user_ids );
		}

		// Return parsed IDs
		return $this->parse_comma_list( $user_ids );
	}

	/**
	 * Get the part of the query responsible for JOINing objects to relationships.
	 *
	 * @since 2.6.0 bbPress (r6844)
	 *
	 * @param array  $args
	 * @param string $meta_key
	 * @param string $meta_type
	 *
	 * @return array
	 */
	public function get_query( $args = array(), $context_key = '', $meta_key = '', $meta_type = 'post' ) {
		$user_id    = bbp_get_user_id( $args, true, true );
		$option_key = $this->get_user_option_key( $meta_key );
		$object_ids = $this->parse_comma_list( get_user_option( $option_key, $user_id ) );

		// Maybe trick WP_Query into ".ID IN (0)" to return no results
		if ( empty( $object_ids ) ) {
			$object_ids = array( 0 );
		}

		// Maybe include these post IDs
		$args = array(
			'post__in' => $object_ids
		);

		// Parse arguments
		return bbp_parse_args( $args, array(), $context_key );
	}
}
