<?php
/**
 * Rules Engine: The brains behind this whole operation
 *
 * @package     GamiPress\Rules_Engine
 * @author      GamiPress <contact@gamipress.com>, Ruben Garcia <rubengcdev@gmail.com>
 * @since       1.0.0
 */
// Exit if accessed directly
if( !defined( 'ABSPATH' ) ) exit;

/**
 * Check if user should earn an achievement, and award it if so.
 *
 * @since  1.0.0
 * @since  1.8.2 Function now informs if achievement has been awarded or not
 *
 * @param  int      $achievement_id     The given achievement ID to possibly award
 * @param  int      $user_id            The given user's ID
 * @param  string   $trigger            The trigger
 * @param  int      $site_id            The triggered site id
 * @param  array    $args               The triggered args
 *
 * @return bool|WP_Error                 True if achievement has been awarded, false or a WP_Error object otherwise
 */
function gamipress_maybe_award_achievement_to_user( $achievement_id = 0, $user_id = 0, $trigger = '', $site_id = 0, $args = array() ) {

	// Set to current site id
	if ( ! $site_id ) {
		$site_id = get_current_blog_id();
    }

	// Grab current user ID if one isn't specified
	if ( ! $user_id ) {
		$user_id = get_current_user_id();
    }

	$result = true;

	// Checks if the user has access to this achievement
	if ( ! gamipress_user_has_access_to_achievement( $user_id, $achievement_id, $trigger, $site_id, $args ) ) {
        $result = new WP_Error( 'gamipress_not_access_achievement', __( 'User hasn\'t access to this achievement.', 'gamipress' ) );
    }

    // Checks if the user has completed the achievement
    if ( $result === true && ! gamipress_check_achievement_completion_for_user( $achievement_id, $user_id, $trigger, $site_id, $args ) ) {
        $result = new WP_Error( 'gamipress_not_completed_achievement', __( 'User hasn\'t completed this achievement yet.', 'gamipress' ) );
    }

    // Only award the achievement if no errors confirmed
    if ( $result === true ) {
        gamipress_award_achievement_to_user( $achievement_id, $user_id, false, $trigger, $site_id, $args );
    }

    /**
     * After check if user should earn an achievement, and award it if so.
     *
     * @since  1.8.2
     *
     * @param  bool|WP_Error    $result             True if achievement has been awarded, false or a WP_Error object otherwise
     * @param  int              $user_id            The given user's ID
     * @param  int              $achievement_id     The given achievement ID to possibly award
     * @param  string           $trigger            The trigger
     * @param  int              $site_id            The triggered site id
     * @param  array            $args               The triggered args
     *
     * @return bool|WP_Error                         True if achievement has been awarded, false or a WP_Error object otherwise
     */
	return apply_filters( 'gamipress_maybe_award_achievement_to_user', $result, $user_id, $achievement_id, $trigger, $site_id, $args );

}

/* --------------------------------------------------
 * Access Checks
   -------------------------------------------------- */

/**
 * Check if user may access/earn achievement.
 *
 * @since  1.0.0
 *
 * @param  int      $achievement_id     The given achievement ID to possibly award
 * @param  int      $user_id            The given user's ID
 * @param  string   $trigger            The trigger
 * @param  int      $site_id            The triggered site id
 * @param  array    $args               The triggered args
 *
 * @return bool                    	    True if user has access, false otherwise
 */
function gamipress_user_has_access_to_achievement( $user_id = 0, $achievement_id = 0, $trigger = '', $site_id = 0, $args = array() ) {

	// Set to current site id
	if ( ! $site_id ) {
		$site_id = get_current_blog_id();
    }

	// Assume we have access
	$return = true;

	// If the achievement is not published, we do not have access
	if ( gamipress_get_post_status( $achievement_id ) !== 'publish' ) {
		$return = false;
    }

	// If we've exceeded the max earnings, we do not have access
	if ( $return && gamipress_achievement_user_exceeded_max_earnings( $user_id, $achievement_id ) ) {
		$return = false;
    }

	// Get the achievement post type to exclude some of them
	$post_type = gamipress_get_post_type( $achievement_id );

	if ( $return
        && ! in_array( $post_type, array( 'points-award', 'points-deduct' ) ) // If not is a points award or deduct
        && $parent_id = absint( gamipress_get_post_field( 'post_parent', $achievement_id ) )  // If achievement has a parent
    ) {

		// If we don't have access to the parent, we do not have access to this
		if ( ! gamipress_user_has_access_to_achievement( $user_id, $parent_id, $trigger, $site_id, $args ) ) {
			$return = false;
        }

		// If the parent requires sequential steps, confirm we've earned all previous steps
		if ( $return && gamipress_is_achievement_sequential( $parent_id ) ) {

			foreach ( gamipress_get_children_of_achievement( $parent_id ) as $sibling ) {
				// If this is the current step, we're good to go
				if ( $sibling->ID == $achievement_id ) {
				    break;
                }

                // Skip optional requirements
                if( ( bool ) gamipress_get_post_meta( $sibling->ID, '_gamipress_optional' ) ) {
                    continue;
                }

				// If we haven't earned any previous step, we can't earn this one
				if ( ! gamipress_has_user_earned_achievement( $sibling->ID, $user_id ) ) {
					$return = false;
					break;
				}
			}

		}
	}

    /**
     * Available filter for custom overrides
     *
     * @since  1.0.0
     *
     * @param  bool     $return             True if user has access, false otherwise
     * @param  int      $user_id            The given user's ID
     * @param  int      $achievement_id     The given achievement ID to possibly award
     * @param  string   $trigger            The trigger
     * @param  int      $site_id            The triggered site id
     * @param  array    $args               The triggered args
     *
     * @return bool                    	    True if user has access, false otherwise
     */
	return apply_filters( 'user_has_access_to_achievement', $return, $user_id, $achievement_id, $trigger, $site_id, $args );

}

/**
 * Checks if a user is allowed to work on a given requirement related to a specific ID
 *
 * @since  1.0.8
 *
 * @param bool      $return             The default return value
 * @param int       $user_id            The given user's ID
 * @param int       $requirement_id     The given requirement's post ID
 * @param string    $trigger            The trigger triggered
 * @param int       $site_id            The site id
 * @param array     $args               Arguments of this trigger
 *
 * @return bool                         True if user has access to the requirement, false otherwise
 */
function gamipress_user_has_access_to_specific_requirement( $return = false, $user_id = 0, $requirement_id = 0, $trigger = '', $site_id = 0, $args = array() ) {

    // Bail if access is not already granted
    if( ! $return ) {
        return $return;
    }

	// If we're not working with a requirement, bail here
	if ( ! in_array( gamipress_get_post_type( $requirement_id ), gamipress_get_requirement_types_slugs() ) ) {
		return $return;
    }

	// If is specific trigger rules engine needs the attached id
	if( $return && in_array( $trigger, array_keys( gamipress_get_specific_activity_triggers() ) ) ) {

		$specific_id = gamipress_specific_trigger_get_id( $trigger, $args );
		$required_id = absint( gamipress_get_post_meta( $requirement_id, '_gamipress_achievement_post' ) );

		// True if there is a specific id, a attached id and both are equal
		$return = (bool) (
			$specific_id !== 0
			&& $required_id !== 0
			&& $specific_id === $required_id
		);

	}

	// Send back our eligibility
	return $return;
}
add_filter( 'user_has_access_to_achievement', 'gamipress_user_has_access_to_specific_requirement', 10, 6 );

/**
 * Checks if a user is allowed to work on a given step
 *
 * @since  1.0.0
 *
 * @param  bool     $return     The default return value
 * @param  int      $user_id    The given user's ID
 * @param  int      $step_id    The given step's post ID
 *
 * @return bool                 True if user has access to step, false otherwise
 */
function gamipress_user_has_access_to_step( $return = false, $user_id = 0, $step_id = 0 ) {

    // Bail if access is not already granted
    if( ! $return ) {
        return $return;
    }

	// If we're not working with a step, bail here
	if ( 'step' !== gamipress_get_post_type( $step_id ) ) {
		return $return;
    }

	// Prevent user from earning steps with no parents
	$achievement = gamipress_get_step_achievement( $step_id );

	if ( ! $achievement ) {
		return false;
    }

	// Prevent user from earning steps if achievement is not setup to be earned through steps
    if( 'triggers' !== gamipress_get_post_meta( $achievement->ID, '_gamipress_earned_by' ) ) {
        return false;
    }

    if( $return ) {

        $earned_times = gamipress_get_earnings_count( array(
            'user_id'   => absint( $user_id ),
            'post_id'   => absint( $step_id ),
            'since'     => absint( gamipress_achievement_last_user_activity( $achievement->ID, $user_id ) )
        ) );

        // Prevent user from repeatedly earning the same step
        if ( $earned_times > 0 ) {
            $return = false;
        }

    }

	// Send back our eligibility
	return $return;
}
add_filter( 'user_has_access_to_achievement', 'gamipress_user_has_access_to_step', 10, 3 );

/**
 * Checks if a user is allowed to work on a given points award
 *
 * @since  1.0.0
 *
 * @param  bool     $return   			The default return value
 * @param  int      $user_id  			The given user's ID
 * @param  int      $points_award_id  	The given points award's post ID
 *
 * @return bool              			True if user has access to step, false otherwise
 */
function gamipress_user_has_access_to_points_award( $return = false, $user_id = 0, $points_award_id = 0 ) {

    // Bail if access is not already granted
    if( ! $return ) {
        return $return;
    }

	// If we're not working with a points award, bail here
	if ( 'points-award' !== gamipress_get_post_type( $points_award_id ) ) {
		return $return;
    }

	// Prevent user from earning points awards with no points type
	$points_type = gamipress_get_points_award_points_type( $points_award_id );

	if ( ! $points_type ) {
		return false;
    }

	$maximum_earnings = absint( gamipress_get_post_meta( $points_award_id, '_gamipress_maximum_earnings' ) );

	// No maximum earnings set
	if( $maximum_earnings === 0 ) {
		return $return;
    }

    if ( $return ) {

        $earned_times = gamipress_get_earnings_count( array(
            'user_id'   => absint( $user_id ),
            'post_id'   => absint( $points_award_id ),
            'since'     => absint( gamipress_achievement_last_user_activity( $points_award_id, $user_id ) )
        ) );
        
        // Prevent user to exceed maximum earnings the same points award
        if ( $earned_times >= $maximum_earnings ) {
            $return = false;
        }

    }

	// Send back our eligibility
	return $return;

}
add_filter( 'user_has_access_to_achievement', 'gamipress_user_has_access_to_points_award', 10, 3 );

/**
 * Checks if a user is allowed to work on a given points deduct
 *
 * @since  1.3.7
 *
 * @param  bool     $return   			The default return value
 * @param  int      $user_id  			The given user's ID
 * @param  int      $points_deduct_id  	The given points deduct's post ID
 *
 * @return bool              			True if user has access to step, false otherwise
 */
function gamipress_user_has_access_to_points_deduct( $return = false, $user_id = 0, $points_deduct_id = 0 ) {

    // Bail if access is not already granted
    if( ! $return ) {
        return $return;
    }

	// If we're not working with a step, bail here
	if ( 'points-deduct' !== gamipress_get_post_type( $points_deduct_id ) ) {
		return $return;
    }

	// Prevent user from earning points deducts with no points type
	$points_type = gamipress_get_points_deduct_points_type( $points_deduct_id );

	if ( ! $points_type ) {
		return false;
    }

	$maximum_earnings = absint( gamipress_get_post_meta( $points_deduct_id, '_gamipress_maximum_earnings' ) );

	// No maximum earnings set
	if( $maximum_earnings === 0 ) {
		return $return;
	}

    if ( $return ) {

        $earned_times = gamipress_get_earnings_count( array(
            'user_id'   => absint( $user_id ),
            'post_id'   => absint( $points_deduct_id ),
            'since'     => absint( gamipress_achievement_last_user_activity( $points_deduct_id, $user_id ) )
        ) );

        // Prevent user to exceed maximum earnings the same points deduct
        if ( $earned_times >= $maximum_earnings ) {
            $return = false;
        }

    }

	// Send back our eligibility
	return $return;

}
add_filter( 'user_has_access_to_achievement', 'gamipress_user_has_access_to_points_deduct', 10, 3 );

/**
 * Checks if a user is allowed to work on a given rank requirement
 *
 * @since  1.0.0
 *
 * @param  bool     $return   The default return value
 * @param  int      $user_id  The given user's ID
 * @param  int      $rank_requirement_id  The given rank requirement's post ID
 *
 * @return bool              True if user has access to step, false otherwise
 */
function gamipress_user_has_access_to_rank_requirement( $return = false, $user_id = 0, $rank_requirement_id = 0 ) {

    // Bail if access is not already granted
    if( ! $return ) {
        return $return;
    }

	// If we're not working with a rank requirement, bail here
	if ( 'rank-requirement' !== gamipress_get_post_type( $rank_requirement_id ) ) {
		return $return;
    }

	// If is a rank requirement, we need to check if rank requirement is for next rank and not other
	$rank = gamipress_get_rank_requirement_rank( $rank_requirement_id );

	// Bail if not rank assigned to this rank requirement
	if( ! $rank ) {
		return false;
    }

	$next_user_rank_id = gamipress_get_next_user_rank_id( $user_id, $rank->post_type );

	if( $return && $next_user_rank_id === 0 ) {
		$return = false;
    }

	if ( $return && absint( $rank->ID ) !== $next_user_rank_id ) {
		$return = false;
    }

    if ( $return ) {

        $earned_times = gamipress_get_earnings_count( array(
            'user_id'   => absint( $user_id ),
            'post_id'   => absint( $rank_requirement_id ),
            'since'     => absint( gamipress_achievement_last_user_activity( $rank->ID, $user_id ) )
        ) );

        // Prevent user from repeatedly earning the same requirement
        if ( $earned_times > 0 ) {
            $return = false;
        }

    }

	// Send back our eligibility
	return $return;

}
add_filter( 'user_has_access_to_achievement', 'gamipress_user_has_access_to_rank_requirement', 10, 3 );

/**
 * Check if user meets the post type requirement for a given achievement
 *
 * @since  1.9.9
 *
 * @param  bool     $return             True if user has access, false otherwise
 * @param  int      $user_id            The given user's ID
 * @param  int      $achievement_id     The given achievement ID to possibly award
 * @param  string   $trigger            The trigger
 * @param  int      $site_id            The triggered site id
 * @param  array    $args               The triggered args
 *
 * @return bool                         True if user has access to step, false otherwise
 */
function gamipress_user_meets_post_type_requirement( $return = false, $user_id = 0, $achievement_id = 0, $trigger = '', $site_id = 0, $args = array() ) {

    // Bail if access is not already granted
    if( ! $return ) {
        return $return;
    }

    $post_type = gamipress_get_post_type( $achievement_id );

    // Bail if not is a requirement
    if( ! in_array( $post_type, gamipress_get_requirement_types_slugs() ) ) {
        return $return;
    }

    if( empty( $trigger ) ) {
        $trigger = gamipress_get_post_meta( $achievement_id, '_gamipress_trigger_type' );
    }

    if( in_array( $trigger, array(
        'gamipress_new_comment_post_type',
        'gamipress_user_post_comment_post_type',
        'gamipress_spam_comment_post_type',
        'gamipress_publish_post_type',
        'gamipress_delete_post_type',
        'gamipress_post_type_visit',
        'gamipress_user_post_type_visit',
    ) ) ) {

        $index = 2;

        if( in_array( $trigger, array(
            'gamipress_new_comment_post_type',
            'gamipress_user_post_comment_post_type',
            'gamipress_spam_comment_post_type',
        ) ) ) {
            $index = 3;
        }

        $post_type = gamipress_get_event_arg( $args, 'post_type', $index );
        $post_type_required = gamipress_get_post_meta( $achievement_id, '_gamipress_post_type_required' );

        // Deserve if post type matches
        $return = (bool) ( $post_type === $post_type_required );
    }

    // Send back our eligibility
    return $return;

}
add_filter( 'user_has_access_to_achievement', 'gamipress_user_meets_post_type_requirement', 10, 6 );

/**
 * Check if user meets the role requirement for a given achievement
 *
 * @since  1.8.9
 *
 * @param  bool     $return             True if user has access, false otherwise
 * @param  int      $user_id            The given user's ID
 * @param  int      $achievement_id     The given achievement ID to possibly award
 * @param  string   $trigger            The trigger
 * @param  int      $site_id            The triggered site id
 * @param  array    $args               The triggered args
 *
 * @return bool                         True if user has access to step, false otherwise
 */
function gamipress_user_meets_role_requirement( $return = false, $user_id = 0, $achievement_id = 0, $trigger = '', $site_id = 0, $args = array() ) {

    // Bail if access is not already granted
    if( ! $return ) {
        return $return;
    }

    $post_type = gamipress_get_post_type( $achievement_id );

    // Bail if not is a requirement
    if( ! in_array( $post_type, gamipress_get_requirement_types_slugs() ) ) {
        return $return;
    }

    if( empty( $trigger ) ) {
        $trigger = gamipress_get_post_meta( $achievement_id, '_gamipress_trigger_type' );
    }

    if( in_array( $trigger, array(
        'gamipress_add_specific_role',
        'gamipress_set_specific_role',
        'gamipress_remove_specific_role'
    ) ) ) {
        $role = gamipress_get_event_arg( $args, 'role', 1 );
        $role_required = gamipress_get_post_meta( $achievement_id, '_gamipress_user_role_required' );

        // Deserve if role matches
        $return = (bool) ( $role === $role_required );
    }

    // Send back our eligibility
    return $return;

}
add_filter( 'user_has_access_to_achievement', 'gamipress_user_meets_role_requirement', 10, 6 );

/**
 * Check if user meets the meta requirement for a given achievement
 *
 * @since  1.0.0
 *
 * @param bool $return          The default return value
 * @param int $user_id          The given user's ID
 * @param int $requirement_id   The given requirement's post ID
 * @param string $trigger       The trigger triggered
 * @param int $site_id          The site id
 * @param array $args           Arguments of this trigger
 *
 * @return bool True if user has access to the requirement, false otherwise
 */
function gamipress_user_meets_meta_requirement( $return = false, $user_id = 0, $requirement_id = 0, $trigger = '', $site_id = 0, $args = array() ) {

    // If we're not working with a requirement, bail here
    if ( ! in_array( get_post_type( $requirement_id ), gamipress_get_requirement_types_slugs() ) )
        return $return;

    // Check if user has access to the achievement ($return will be false if user has exceed the limit or achievement is not published yet)
    if( ! $return )
        return $return;

    // Rules engine needs to check if meta key matches required ones
    if( $return && ( $trigger === 'gamipress_update_post_meta_any_value'
        || $trigger === 'gamipress_update_user_meta_any_value' ) ) {

        $meta_key = $args[2]; 
        $required_meta_key = gamipress_get_post_meta( $requirement_id, '_gamipress_meta_key_required', true );

        $return = (bool) ( $meta_key === $required_meta_key );
        
    }

    // Rules engine needs to check if meta key and values matches required ones
    if( $return && ( $trigger === 'gamipress_update_post_meta_specific_value'
        || $trigger === 'gamipress_update_user_meta_specific_value' ) ) {

        $meta_key = $args[2];
        $meta_value = $args[3];
        $required_meta_key = gamipress_get_post_meta( $requirement_id, '_gamipress_meta_key_required', true );
        $required_meta_value = gamipress_get_post_meta( $requirement_id, '_gamipress_meta_value_required', true );
        
        if ( is_numeric( $meta_value ) ){
            
            $meta_value = strval( $meta_value );
        }
        
        $return = (bool) ( $meta_key === $required_meta_key );

        if ( $return ) {
            // Check if field value matches the required one (with support for arrays)
            if( is_array( $meta_value ) )
                $return = (bool) ( in_array( $required_meta_value, $meta_value ) );
            else
                $return = (bool) ( $meta_value === $required_meta_value );
        }
 
    }

    // Send back our eligibility
    return $return;

}
add_filter( 'user_has_access_to_achievement', 'gamipress_user_meets_meta_requirement', 10, 6 );

/* --------------------------------------------------
 * Completion Checks
   -------------------------------------------------- */

/**
 * Check if user has completed an achievement
 *
 * @since  1.0.0
 *
 * @param  int      $achievement_id The given achievement ID to verify
 * @param  int      $user_id        The given user's ID
 * @param  string   $trigger_type   The trigger
 * @param  int      $site_id        The triggered site id
 * @param  array    $args           The triggered args
 *
 * @return bool                    True if user has completed achievement, false otherwise
 */
function gamipress_check_achievement_completion_for_user( $achievement_id = 0, $user_id = 0, $trigger_type = '', $site_id = 0, $args = array() ) {

	// Assume the user has completed the achievement
	$return = true;

	// Set to current site id
	if ( ! $site_id ) {
		$site_id = get_current_blog_id();
    }

    // Points types can't be earned
	if( gamipress_get_post_type( $achievement_id ) === 'points-type' ) {
	    return false;
    }

	// If the user has not already earned the achievement...
	if ( gamipress_get_earnings_count( array(
		'user_id'   => absint( $user_id ),
		'post_id'   => absint( $achievement_id ),
		'since'     => gamipress_achievement_last_user_activity( $achievement_id, $user_id ) + 1
	) ) === 0 ) {

		// Grab published required achievements for this achievement
		$required_achievements = gamipress_get_required_achievements_for_achievement( $achievement_id, 'publish' );

		// If we have requirements, loop through each and make sure they've been completed
		if ( is_array( $required_achievements ) && ! empty( $required_achievements ) ) {

			foreach ( $required_achievements as $requirement ) {

                // Skip optional requirements
                if( ( bool ) gamipress_get_post_meta( $requirement->ID, '_gamipress_optional' ) ) {
                    continue;
                }

				$requirement_earned = gamipress_get_earnings_count( array(
					'user_id'   => $user_id,
					'post_id'   => $requirement->ID,
					'since'     => gamipress_achievement_last_user_activity( $achievement_id, $user_id ) - 1
				) );

				// Has the user already earned the requirements?
				if ( $requirement_earned === 0 ) {
					$return = false;
					break;
				}
			}

		}
	}

    /**
     * Available filter to support custom earning rules
     *
     * @since  1.0.0
     *
     * @param  bool     $return         True if user has completed achievement, false otherwise
     * @param  int      $user_id        The given user's ID
     * @param  int      $achievement_id The given achievement ID to verify
     * @param  string   $trigger_type   The trigger
     * @param  int      $site_id        The triggered site id
     * @param  array    $args           The triggered args
     *
     * @return bool                    True if user has completed achievement, false otherwise
     */
	return apply_filters( 'user_deserves_achievement', $return, $user_id, $achievement_id, $trigger_type, $site_id, $args );

}

/**
 * Check if user meets the points requirement for a given achievement
 *
 * @since   1.0.0
 * @updated 1.6.4 Added checks for 'points-balance' event
 *
 * @param  bool 	$return 		The current status of whether or not the user deserves this achievement
 * @param  int	    $user_id 		The given user's ID
 * @param  int 	    $achievement_id The given achievement's post ID
 * @param  string   $trigger_type   The trigger
 * @param  int      $site_id        The triggered site id
 * @param  array    $args           The triggered args
 *
 * @return bool Our possibly updated earning status
 */
function gamipress_user_meets_points_requirement( $return = false, $user_id = 0, $achievement_id = 0, $trigger_type = '', $site_id = 0, $args = array() ) {

    // Check if user has access to the achievement ($return will be false if user has exceed the limit or achievement is not published yet)
    if( ! $return ) {
        return $return;
    }

    if( empty( $trigger_type ) ) {
        $trigger_type = gamipress_get_post_meta( $achievement_id, '_gamipress_trigger_type' );
    }

	// First, see if the achievement requires a minimum amount of points
	if (
		'points' === gamipress_get_post_meta( $achievement_id, '_gamipress_earned_by' ) 			// Check for achievements earned by points
		|| 'earn-points' === $trigger_type 	                                                        // Check for requirements with earn points activity
	) {

		// Grab our user's points and see if they at least as many as required
		$points_required        = absint( gamipress_get_post_meta( $achievement_id, '_gamipress_points_required' ) );
		$points_type_required   = gamipress_get_post_meta( $achievement_id, '_gamipress_points_type_required' );
        $last_activity          = absint( gamipress_achievement_last_user_activity( $achievement_id, $user_id ) );

		// Get user points earned since last time has earning the achievement
		$awarded_points    		= gamipress_get_user_points_awarded_in_loop( $user_id, $points_type_required, $achievement_id );

		if( $awarded_points >= $points_required ) {

			// If the user just earned the achievement, though, don't let them earn it again
			// This prevents an infinite loop if the achievement has no maximum earnings limit
			$minimum_time  	= current_time( 'timestamp' ) - 1;

			if ( $last_activity > $minimum_time ) {
				$return = false;
            }

            // Increase the points awarded of this loop since the achievement will be awarded
            gamipress_update_user_points_awarded_in_loop( $points_required, $points_type_required, $achievement_id, $user_id );
            
		} else {
            $return = false;
        }

	} else if( 'points-balance' === $trigger_type ) {

        // Grab our user's points and see if they at least as many as required
        $points_condition       = gamipress_get_post_meta( $achievement_id, '_gamipress_points_condition' );
        $points_required        = absint( gamipress_get_post_meta( $achievement_id, '_gamipress_points_required' ) );
        $points_type_required   = gamipress_get_post_meta( $achievement_id, '_gamipress_points_type_required' );

        // Get user points balance
        $points_balance    		= absint( gamipress_get_user_points( $user_id, $points_type_required ) );

        if( empty( $points_condition ) ) {
            $points_condition = 'greater_or_equal';
        }

        if( gamipress_number_condition_matches( $points_balance, $points_required, $points_condition ) ) {

            // If the user just earned the achievement, though, don't let them earn it again
            // This prevents an infinite loop if the achievement has no maximum earnings limit
            $minimum_time  	= current_time( 'timestamp' ) - 1;
            $last_activity  = absint( gamipress_achievement_last_user_activity( $achievement_id, $user_id ) );

            if ( 'rank-requirement' === gamipress_get_post_type( $achievement_id ) ) {
                $return = true;
            } elseif ( $last_activity > $minimum_time ) {
                $return = false;
            }

        } else {
            $return = false;
        }

	} else if( 'gamipress_expend_points' === $trigger_type ) {

		// Grab our user's points expended and see if they at least as many as required
		$points_required        = absint( gamipress_get_post_meta( $achievement_id, '_gamipress_points_required' ) );
		$points_type_required   = gamipress_get_post_meta( $achievement_id, '_gamipress_points_type_required' );

		// Get user points expended since last time has earning the achievement
		$expended_points 		= gamipress_get_user_points_expended( $user_id, $points_type_required, gamipress_achievement_last_user_activity( $achievement_id, $user_id ) - 1 );

		if ( $expended_points < $points_required )
			$return = false;

	}

	// Return our eligibility status
	return $return;

}
add_filter( 'user_deserves_achievement', 'gamipress_user_meets_points_requirement', 10, 6 );

/**
 * Check if user meets the rank requirement for a given achievement
 *
 * @since  1.3.1
 *
 * @param  bool     $return         The current status of whether or not the user deserves this achievement
 * @param  int      $user_id        The given user's ID
 * @param  int      $achievement_id The given achievement's post ID
 * @param  string   $trigger_type   The trigger
 * @param  int      $site_id        The triggered site id
 * @param  array    $args           The triggered args
 *
 * @return bool                    Our possibly updated earning status
 */
function gamipress_user_meets_rank_requirement( $return = false, $user_id = 0, $achievement_id = 0, $trigger_type = '', $site_id = 0, $args = array() ) {

    // Check if user has access to the achievement ($return will be false if user has exceed the limit or achievement is not published yet)
    if( ! $return ) {
        return $return;
    }

    if( empty( $trigger_type ) ) {
        $trigger_type = gamipress_get_post_meta( $achievement_id, '_gamipress_trigger_type' );
    }

	// First, see if the achievement requires a minimum rank
	if (
		'rank' === gamipress_get_post_meta( $achievement_id, '_gamipress_earned_by' ) 				// Check for achievements earned by rank
		|| 'earn-rank' === $trigger_type 	                                                        // Check for requirements with earn rank activity
		|| 'revoke-rank' === $trigger_type 	                                                        // Check for requirements with revoke rank activity
	) {
		// Grab our user's rank and compared it with the required one
		$rank_required   = absint( gamipress_get_post_meta( $achievement_id, '_gamipress_rank_required' ) );
		$user_rank_id    = gamipress_get_user_rank_id( $user_id, gamipress_get_post_type( $rank_required ) );

		if ( $user_rank_id === $rank_required )
			$return = true;
		else
			$return = false;

		if( $return ) {

			// If the user just earned the achievement, though, don't let them earn it again
			// This prevents an infinite loop if the achievement has no maximum earnings limit
			$last_activity 	= gamipress_achievement_last_user_activity( $achievement_id, $user_id );
			$minimum_time 	= current_time( 'timestamp' ) - 1;

			if ( $last_activity > $minimum_time )
				$return = false;

		}
	}

	// Return our eligibility status
	return $return;

}
add_filter( 'user_deserves_achievement', 'gamipress_user_meets_rank_requirement', 10, 6 );

/**
 * Validate whether or not the user has completed all requirements for an achievement
 *
 * @since  1.0.0
 *
 * @param  bool     $return         True if user deserves achievement, false otherwise
 * @param  int      $user_id  		The given user's ID
 * @param  int      $achievement_id The achievement post ID
 * @param  string   $trigger_type   The trigger
 * @param  int      $site_id        The triggered site id
 * @param  array    $args           The triggered args
 *
 * @return bool              		True if user deserves limit requirements, false otherwise
 */
function gamipress_user_deserves_limit_requirements( $return = false, $user_id = 0, $achievement_id = 0, $trigger_type = '', $site_id = 0, $args = array() ) {

    // Check if user has access to the achievement ($return will be false if user has exceed the limit or achievement is not published yet)
    if( ! $return ) {
        return $return;
    }

    // If we're not working with a requirement, bail here
    if ( ! in_array( gamipress_get_post_type( $achievement_id ), gamipress_get_requirement_types_slugs() ) ) {
        return $return;
    }

    if( empty( $trigger_type ) ) {
        $trigger_type = gamipress_get_post_meta( $achievement_id, '_gamipress_trigger_type' );
    }

	// Check if activity trigger is excluded from this check
	if( in_array( $trigger_type, gamipress_get_activity_triggers_excluded_from_activity_limit() ) ) {
		return $return;
    }

    // Check if is limited over time
    $since = gamipress_get_achievement_limit_timestamp( $achievement_id );

    if( $since > 0 ) {

        // Activity count limited to a timestamp
        $activity_count = absint( gamipress_get_achievement_activity_count( $user_id, $achievement_id, $since ) );

        if ( $activity_count !== 0 ) {

            // Activity count limit over time
            $limit = absint( gamipress_get_post_meta( $achievement_id, '_gamipress_limit' ) );

            // Force bail if user exceeds the limit over time
            if( $activity_count > $limit ) {
                return false;
            }

        }

    }

    // Get the required number of check-ins
    $count = absint( gamipress_get_post_meta( $achievement_id, '_gamipress_count' ) );

    if ( $count > 1 ) {
        
        // Grab the relevant activity count
        $activity_count = absint( gamipress_get_achievement_activity_count( $user_id, $achievement_id ) );

        // If exceed the required number of check-ins, then deserve the achievement
        if ( $activity_count < $count ) {
            return false;
        }

    }

	return $return;
}
add_filter( 'user_deserves_achievement', 'gamipress_user_deserves_limit_requirements', 10, 6 );

/**
 * Count the user's relevant actions for a given achievement
 *
 * @since  1.0.0
 *
 * @param  int $user_id 		The given user's ID
 * @param  int $achievement_id 	The given achievement's ID
 * @param  int $since 			Timestamp since retrieve the count
 *
 * @return int          		The total activity count
 */
function gamipress_get_achievement_activity_count( $user_id = 0, $achievement_id = 0, $since = 0 ) {

	// Setup site id
	$site_id = get_current_blog_id();

	// Assume the user has no relevant activities
	$activity_count = 0;

	$post_type = gamipress_get_post_type( $achievement_id );

	if ( in_array( $post_type, gamipress_get_requirement_types_slugs() ) ) {

		// Grab the requirement object
		$requirement = gamipress_get_requirement_object( $achievement_id );

		// Determine which type of trigger we're using and return the corresponding activities
		switch( $requirement['trigger_type'] ) {
			case 'specific-achievement':

				if( $post_type === 'step' && $since === 0 ) {

					// Get our parent achievement
					$achievement = gamipress_get_step_achievement( $achievement_id );

					// If the user has any interaction with this achievement, only get activity since that date
					if ( $achievement && $date = gamipress_achievement_last_user_activity( $achievement->ID, $user_id ) ) {
						$since = $date;
                    }

				}

				// Get our achievement activity
                $activity_count = gamipress_get_earnings_count( array(
					'user_id'   => absint( $user_id ),
					'post_id'   => absint( $requirement['achievement_post'] ),
					'since'     => $since
				) );
				break;
			case 'any-achievement':
				$trigger = 'gamipress_unlock_' . $requirement['achievement_type'];
				break;
			case 'all-achievements':
				$trigger = 'gamipress_unlock_all_' . $requirement['achievement_type'];
				break;
            case 'revoke-any-achievement':
                $trigger = 'gamipress_revoke_' . $requirement['achievement_type'];
                break;
			default:
				$trigger = $requirement['trigger_type'];
				break;
		}

		// Just continue if trigger is set
		if( isset( $trigger ) ) {

            // If since is not defined check it from new ways
			if( $since === 0 ) {

				if( gamipress_get_post_type( $achievement_id ) === 'rank-requirement' ) {
					// If is a rank requirement, we need to get the last time user earned the latest rank of type
					$rank = gamipress_get_rank_requirement_rank( $achievement_id );

					$since = gamipress_get_rank_earned_time( $user_id, $rank->post_type );
				} else {

                    // Get activity count from last time earned
                    $since = gamipress_achievement_last_user_activity( $achievement_id, $user_id );

                    // If user hasn't earned this yet, then get activity count from publish date
                    if( $since === 0 ) {
                        $parent_id = absint( gamipress_get_post_field( 'post_parent', $achievement_id ) );

                        // Try to get the date from the parent (achievement, rank or points type), if not possible, get it from the requirement
                        if( $parent_id !== 0 ) {
                            $since = strtotime( gamipress_get_post_date( $parent_id ) );
                        } else {
                            $since = strtotime( gamipress_get_post_date( $achievement_id ) );
                        }
                    }

                    if( defined( 'GAMIPRESS_DOING_ACTIVITY_RECOUNT' ) && GAMIPRESS_DOING_ACTIVITY_RECOUNT ) {
                        // Reduce it in 1 if check comes from the recount activity tool
                        $since -= 1;
                    }
				}

			}

            $activity_count = gamipress_get_user_trigger_count( $user_id, $trigger, $since, $site_id, $requirement );

		}

	}

    /**
     * Available filter for overriding user activity count
     *
     * @since  1.0.0
     *
     * @param  int $activity_count  The achievement's activity count
     * @param  int $user_id 		The given user's ID
     * @param  int $achievement_id 	The given achievement's ID
     * @param  int $since 			Timestamp since retrieve the count
     *
     * @return int          		The total activity count
     */
	return absint( apply_filters( "gamipress_{$post_type}_activity_count", $activity_count, $user_id, $achievement_id, $since ) );
}

/**
 * Count the user's relevant actions for a given achievement applying the requirement limits
 *
 * @since  2.0.2
 *
 * @param  int $user_id 		The given user's ID
 * @param  int $achievement_id 	The given achievement's ID
 * @param  int $since 			Timestamp since retrieve the count
 *
 * @return int          		The total activity count
 */
function gamipress_get_achievement_activity_count_limited( $user_id = 0, $achievement_id = 0, $since = 0 ) {

    // Assume the user has no relevant activities
    $activity_count = 0;

    $post_type = gamipress_get_post_type( $achievement_id );

    if ( in_array( $post_type, gamipress_get_requirement_types_slugs() ) ) {

        $limit_type = gamipress_get_post_meta( $achievement_id, '_gamipress_limit_type' );

        if( empty( $limit_type ) ) {
            $limit_type = 'unlimited';
        }

        if( $limit_type === 'unlimited' ) {
            // Times activity has triggered
            $activity_count = absint( gamipress_get_achievement_activity_count( $user_id, $achievement_id, $since ) );
        } else {

            // Grab the requirement object
            $requirement = gamipress_get_requirement_object( $achievement_id );

            $where = array(
                'type' => 'event_trigger',
            );

            // Determine which type of trigger we're using and return the corresponding activities
            switch( $requirement['trigger_type'] ) {
                case 'specific-achievement':
                    $trigger = 'gamipress_unlock_' . $requirement['achievement_type'];
                    $where['post_id'] = absint( $requirement['achievement_post'] );
                    break;
                case 'any-achievement':
                    $trigger = 'gamipress_unlock_' . $requirement['achievement_type'];
                    break;
                case 'all-achievements':
                    $trigger = 'gamipress_unlock_all_' . $requirement['achievement_type'];
                    break;
                case 'revoke-specific-achievement':
                    $trigger = 'gamipress_revoke_' . $requirement['achievement_type'];
                    $where['post_id'] = absint( $requirement['achievement_post'] );
                    break;
                case 'revoke-any-achievement':
                    $trigger = 'gamipress_revoke_' . $requirement['achievement_type'];
                    break;
                default:
                    $trigger = $requirement['trigger_type'];
                    break;
            }

            $where['trigger_type'] = $trigger;
            $group_by = '';

            // Grab the requirement object
            $requirement = gamipress_get_requirement_object( $achievement_id );
            $site_id = get_current_blog_id();

            /**
             * Filter required to get the same where conditions as in the gamipress_get_user_trigger_count() function
             *
             * @see gamipress_get_user_trigger_count()
             */
            $where = apply_filters( 'gamipress_get_user_trigger_count_log_meta', $where, $user_id, $trigger, $since, $site_id, $requirement );

            /**
             * Filter to override the where data to filter the logs count applying the requirement limits
             *
             * @since   2.0.4
             *
             * @param  array    $log_meta       The meta data to filter the logs count
             * @param  int      $user_id        The given user's ID
             * @param  string   $trigger        The given trigger we're checking
             * @param  int      $since 	        The since timestamp where retrieve the logs
             * @param  int      $site_id        The desired Site ID to check
             * @param  array    $args           The triggered args or requirement object
             *
             * @return array                    The where data to filter the logs count
             */
            $where = apply_filters( 'gamipress_get_achievement_activity_count_limited_where', $where, $user_id, $trigger, $since, $site_id, $requirement );

            // If since is not defined check it from new ways
            if( $since === 0 ) {

                if( $post_type === 'rank-requirement' ) {
                    // If is a rank requirement, we need to get the last time user earned the latest rank of type
                    $rank = gamipress_get_rank_requirement_rank( $achievement_id );

                    $since = gamipress_get_rank_earned_time( $user_id, $rank->post_type );
                } else {

                    // Get activity count from last time earned
                    $since = gamipress_achievement_last_user_activity( $achievement_id, $user_id );

                    // If user hasn't earned this yet, then get activity count from publish date
                    if( $since === 0 ) {
                        $since = strtotime( gamipress_get_post_date( $achievement_id ) ) - 1;
                    }

                }

            }

            // Setup the group by clause based on the requirement limit type
            switch( $limit_type ) {
                case 'minutely':
                    $group_by = 'YEAR(l.date), MONTH(l.date), DAY(l.date), HOUR(l.date), MINUTE(l.date)';
                    break;
                case 'hourly':
                    $group_by = 'YEAR(l.date), MONTH(l.date), DAY(l.date), HOUR(l.date)';
                    break;
                case 'daily':
                    $group_by = 'YEAR(l.date), MONTH(l.date), DAY(l.date)';
                    break;
                case 'weekly':
                    $group_by = "YEAR(l.date), DATE_FORMAT( l.date, '%u' )";
                    break;
                case 'monthly':
                    $group_by = 'YEAR(l.date), MONTH(l.date)';
                    break;
                case 'yearly':
                    $group_by = 'YEAR(l.date)';
                    break;
            }

            // Get the user activity counts grouped by date
            $results = gamipress_query_logs( array(
                'select'    => 'LEAST(COUNT(*), ' . $requirement['limit'] . ') AS count', // Least() returns the lowest value (like PHP min() function)
                'user_id'   => $user_id,
                'where'     => $where,
                'since'     => $since,
                'group_by'  => $group_by,
            ) );

            if( is_array( $results ) ) {
                foreach( $results as $result ) {
                    $activity_count += absint( $result->count );
                }
            }

        }

    }

    /**
     * Available filter for overriding user activity count applying the requirement limits
     *
     * @since  1.0.0
     *
     * @param  int $activity_count  The achievement's activity count
     * @param  int $user_id 		The given user's ID
     * @param  int $achievement_id 	The given achievement's ID
     * @param  int $since 			Timestamp since retrieve the count
     *
     * @return int          		The total activity count
     */
    return absint( apply_filters( "gamipress_{$post_type}_activity_count_limited", $activity_count, $user_id, $achievement_id, $since ) );

}

/**
 * Get the start date where an achievement is limiting
 *
 * @param int $achievement_id	The achievement ID
 *
 * @return int 					Timestamp from the limit starts
 */
function gamipress_get_achievement_limit_timestamp( $achievement_id = 0 ) {

	$limit_type = gamipress_get_post_meta( $achievement_id, '_gamipress_limit_type' );

    // No limit
	if( ! $limit_type || $limit_type === 'unlimited' ) {
		return 0;
    }

	$now    = current_time( 'timestamp' );
	$from   = current_time( 'timestamp' );

	$hour   = date( 'H', $now );
	$minute = date( 'i', $now );
	$second = date( 's', $now );
	$day    = date( 'n', $now );
	$month  = date( 'j', $now );
	$year   = date( 'Y', $now );

	switch( $limit_type ) {
        case 'minutely':
            $from = mktime( $hour, $minute - 1, $second, $day, $month, $year );
            break;
        case 'hourly':
            $from = mktime( $hour - 1, $minute, $second, $day, $month, $year );
            break;
		case 'daily':
			$from = mktime( 0, 0, 0, $day, $month, $year );
			break;
		case 'weekly':
			$from = mktime( 0, 0, 0, $day, $month - date( 'N', $now ) + 1 );
			break;
		case 'monthly':
			$from =  mktime( 0, 0, 0, $day, 1, $year );
			break;
		case 'yearly':
			$from =  mktime( 0, 0, 0, 1, 1, $year );
			break;
	}

    /**
     * Available filter to override the start date where an achievement is limiting
     *
     * @since 1.0.0
     *
     * @param int       $from	        The start date where an achievement is limiting
     * @param int       $achievement_id	The achievement ID
     * @param string    $limit_type	    The achievement limit type
     *
     * @return int 					    Timestamp from the limit starts
     */
	return apply_filters( 'gamipress_get_achievement_limit_timestamp', $from, $achievement_id, $limit_type );

}

/* --------------------------------------------------
 * Award Checks
   -------------------------------------------------- */

/**
 * Award an achievement to the user
 *
 * @since  1.0.0
 *
 * @param  int 	    $achievement_id The given achievement ID to award
 * @param  int 	    $user_id        The given user's ID
 * @param  int 	    $admin_id       The given admin's ID
 * @param  string 	$trigger    	The trigger
 * @param  int 	    $site_id        The triggered site id
 * @param  array 	$args           The triggered args
 *
 * @return mixed                   False on not achievement, void otherwise
 */
function gamipress_award_achievement_to_user( $achievement_id = 0, $user_id = 0, $admin_id = 0, $trigger = '', $site_id = 0, $args = array() ) {

	global $wp_filter;

	// Sanity Check: ensure we're working with an achievement or requirement post
	if ( ! ( gamipress_is_achievement( $achievement_id ) || gamipress_is_requirement( $achievement_id ) ) ) {
		return false;
    }

	// Use the current user ID if none specified
	if ( $user_id == 0 ) {
		$user_id = wp_get_current_user()->ID;
    }

	// Get the current site ID none specified
	if ( ! $site_id ) {
		$site_id = get_current_blog_id();
    }

	// Setup our achievement
	$achievement = gamipress_build_achievement_object( $achievement_id );

	// Update user's earned achievements
	gamipress_update_user_achievements( array( 'user_id' => $user_id, 'new_achievements' => array( $achievement ) ) );

	// Log the earning of the award
	gamipress_log_user_achievement_award( $user_id, $achievement_id, $admin_id, $trigger );
    
	// Available hook for unlocking any achievement of this achievement type
	do_action( 'gamipress_unlock_' . $achievement->post_type, $user_id, $achievement_id, $trigger, $site_id, $args );

	// Patch for WordPress to support recursive actions, specifically for gamipress_award_achievement
	// Because global iteration is fun, assuming we can get this fixed for WordPress 3.9
	$is_recursive_filter = ( current_filter() === 'gamipress_award_achievement');
	$current_key = null;

    $gamipress_award_achievement_filter = $wp_filter[ 'gamipress_award_achievement' ];

    // Ensure that filter is an array
    if ( ! is_array( $gamipress_award_achievement_filter ) ) {
        $gamipress_award_achievement_filter = (array) $gamipress_award_achievement_filter;
    }

	// Get current position
	if ( $is_recursive_filter ) {
        $current_key = key( $gamipress_award_achievement_filter );
    }

    /**
     * Available hook to do other things with each awarded achievement
     *
     * @since  1.0.0
     *
     * @param  int 	    $user_id        The given user's ID
     * @param  int 	    $achievement_id The given achievement ID to award
     * @param  string 	$trigger    	The trigger
     * @param  int 	    $site_id        The triggered site id
     * @param  array 	$args           The triggered args
     */
	do_action( 'gamipress_award_achievement', $user_id, $achievement_id, $trigger, $site_id, $args );

	if ( $is_recursive_filter ) {
        
		reset( $gamipress_award_achievement_filter );

		while ( key( $gamipress_award_achievement_filter ) !== $current_key ) {
			next( $gamipress_award_achievement_filter );
		}

	}

}

/**
 * Award additional achievements to user
 *
 * @since   1.0.0
 * @update  1.6.9 Correctly add the trigger type to the maybe award function call
 *
 * @param  int $user_id        The given user's ID
 * @param  int $achievement_id The given achievement's post ID
 *
 * @return void
 */
function gamipress_maybe_award_additional_achievements_to_user( $user_id = 0, $achievement_id = 0 ) {

    // Get the achievement post type
    $post_type = gamipress_get_post_type( $achievement_id );
    $achievement_types = gamipress_get_achievement_types_slugs();
    $rank_types = gamipress_get_achievement_types_slugs();

	// Get achievements that can be earned from completing this achievement
	$dependent_achievements = gamipress_get_dependent_achievements( $achievement_id );

    // See if the user has unlocked all achievements of a given type
    gamipress_maybe_trigger_unlock_all( $user_id, $achievement_id );

	// Loop through each dependent achievement and see if it can be awarded
	foreach ( $dependent_achievements as $achievement ) {
	    $trigger = '';

        $trigger_type = gamipress_get_post_meta( $achievement->ID, '_gamipress_trigger_type' );

        // Skip achievements for revoking
        if( in_array( $trigger_type, array( 'revoke-specific-achievement', 'revoke-rank' ) ) ) {
            continue;
        }

	    if( in_array( $post_type, $achievement_types ) ) {
            $trigger = 'specific-achievement';
        } else if( in_array( $post_type, $rank_types ) ) {
            $trigger = 'earn-rank';
        }

		gamipress_maybe_award_achievement_to_user( $achievement->ID, $user_id, $trigger );
	}

}
add_action( 'gamipress_award_achievement', 'gamipress_maybe_award_additional_achievements_to_user', 10, 2 );

/**
 * Check if the user has unlocked all achievements of a given type
 *
 * Triggers hook gamipress_unlock_all_{$post_type}
 *
 * @since  1.0.0
 *
 * @param  int $user_id        The given user's ID
 * @param  int $achievement_id The given achievement's post ID
 *
 * @return void
 */
function gamipress_maybe_trigger_unlock_all( $user_id = 0, $achievement_id = 0 ) {

    // Get the post type of the earned achievement
    $post_type = gamipress_get_post_type( $achievement_id );

	// Grab our user's (presumably updated) earned achievements
	$earned_achievements = gamipress_get_user_achievements( array( 'user_id' => $user_id, 'achievement_type' => $post_type ) );

	// Hook for unlocking all achievements of this achievement type
	if ( $all_achievements_of_type = gamipress_get_achievements( array( 'post_type' => $post_type ) ) ) {

		// Assume we can award the user for unlocking all achievements of this type
		$all_per_type = true;

		// Loop through each of our achievements of this type
		foreach ( $all_achievements_of_type as $achievement ) {

			// Assume the user hasn't earned this achievement
			$found_achievement = false;

			// Loop through each earned achievement and see if we've earned it
			foreach ( $earned_achievements as $earned_achievement ) {
				if ( $earned_achievement->ID == $achievement->ID ) {
					$found_achievement = true;
					break;
				}
			}

			// If we haven't earned this single achievement, we haven't earned them all
			if ( ! $found_achievement ) {
				$all_per_type = false;
				break;
			}
		}

		// If we've earned all achievements of this type, trigger our hook
		if ( $all_per_type ) {
			do_action( 'gamipress_unlock_all_' . $post_type, $user_id, $achievement_id );
		}
	}
}

/**
 * Award new points to the user based on logged activites and earned achievements
 *
 * @since  1.0.0
 *
 * @param  int $user_id        The given user's ID
 * @param  int $achievement_id The given achievement's post ID
 */
function gamipress_maybe_award_points( $user_id = 0, $achievement_id = 0 ) {
	// Grab our points from the provided post
	$points = absint( gamipress_get_post_meta( $achievement_id, '_gamipress_points' ) );
	$points_type = gamipress_get_post_meta( $achievement_id, '_gamipress_points_type' );
	$points_types = gamipress_get_points_types();

	if ( isset( $points_types[$points_type] ) ) {

        /**
         * Filter available to override the amount of points to award or deduct
         *
         * @since  6.9.8
         *
         * @param  int      $points         The points amount
         * @param  string   $points_type    The points type
         * @param  int      $user_id        The given user's ID
         * @param  int      $achievement_id The given achievement's post ID
         *
         * @return int
         */
        $points = apply_filters( 'gamipress_maybe_award_points_amount', $points, $points_type, $user_id, $achievement_id );

        if( ! empty( $points ) ) {
            $post_type = gamipress_get_post_type( $achievement_id );

            if( $post_type === 'points-deduct' ) {
                gamipress_deduct_points_to_user( $user_id, $points, $points_type, array( 'achievement_id' => $achievement_id ) );
            } else {
                gamipress_award_points_to_user( $user_id, $points, $points_type, array( 'achievement_id' => $achievement_id ) );
            }
        }
	}

}
add_action( 'gamipress_award_achievement', 'gamipress_maybe_award_points', 10, 2 );

/**
 * Award same achievement to the user based on how many points has earn
 *
 * @since  1.0.0
 *
 * @param  int $user_id        The given user's ID
 * @param  int $achievement_id The given achievement's post ID
 */
function gamipress_maybe_award_multiple_points( $user_id = 0, $achievement_id = 0 ) {

    $post_type = gamipress_get_post_type( $achievement_id );

    // First, see if the requirement requires a minimum amount of points (just requirements has this meta)
    // Note: Rank requirements are excluded from this
    if ( 'earn-points' === gamipress_get_post_meta( $achievement_id, '_gamipress_trigger_type' )
         && ! in_array( $post_type, array( 'rank-requirement' ) ) ) {

        // Grab our user's points and see if they at least as many as required
        $points_required        = absint( gamipress_get_post_meta( $achievement_id, '_gamipress_points_required' ) );
        $points_type_required   = gamipress_get_post_meta( $achievement_id, '_gamipress_points_type_required' );
        $multiple_award_key     = "gamipress_doing_multiple_{$points_type_required}_award";

        // Check if we are in a loop of multiple points to award
        if( ! ( isset( $GLOBALS[$multiple_award_key] ) && $GLOBALS[$multiple_award_key] === true ) ) {

            $last_achievement_activity = absint( gamipress_achievement_last_user_activity( $achievement_id, $user_id ) );

            // Get user points earned since last time has earning the achievement
            $user_last_points = gamipress_get_user_points_awarded_in_loop( $user_id, $points_type_required, $achievement_id );

            if( $user_last_points >= $points_required && $points_required > 0 ) {

                // Starting multiple points award
                $GLOBALS[$multiple_award_key] = true;

                // Set times to award
                $times_to_award = intval( $user_last_points / $points_required );

                // Check the maximum times this requirement could be earned
                $maximum_earnings = absint( gamipress_get_post_meta( $achievement_id, '_gamipress_maximum_earnings' ) );

                // If maximum earnings is different to 0, we need to set how many times to award it
                if( $maximum_earnings !== 0 ) {

                    $earned_times = gamipress_get_earnings_count( array(
                        'user_id'   => absint( $user_id ),
                        'post_id'   => absint( $achievement_id ),
                        'since'     => $last_achievement_activity
                    ) );

                    // If times to award and earned times exceed the maximum earnings
                    if( ( $times_to_award + $earned_times ) >= $maximum_earnings )
                        $times_to_award = ( $maximum_earnings - $earned_times );

                }

                // Award same achievement many times (rules engine will check limited times to earn it)
                for( $i=0; $i < $times_to_award; $i++ ) {
                    gamipress_award_achievement_to_user( $achievement_id, $user_id );

                    // On every loop increase the points awarded
                    gamipress_update_user_points_awarded_in_loop( $points_required, $points_type_required, $achievement_id, $user_id );
                }

                // Ending multiple points award
                $GLOBALS[$multiple_award_key] = false;

            }

        }

    }

}
add_action( 'gamipress_award_achievement', 'gamipress_maybe_award_multiple_points', 15, 2 );

/**
 * Award new rank to the user based on logged activites and earned achievements
 *
 * @since  1.3.1
 *
 * @param  int $user_id        The given user's ID
 * @param  int $achievement_id The given achievement's post ID
 */
function gamipress_maybe_award_rank( $user_id = 0, $achievement_id = 0 ) {

	if( gamipress_get_post_type( $achievement_id ) !== 'rank-requirement' ) {
		return;
    }
    
	// Get the requirement rank
	$rank = gamipress_get_rank_requirement_rank( $achievement_id );
    
	if( ! $rank )
		return;

	$old_rank = gamipress_get_user_rank( $user_id, $rank->post_type );
    
	// Return if current rank is this one
	if( $old_rank && $old_rank->ID === $rank->ID )
		return;

	// Get all requirements of this rank
	$requirements = gamipress_get_rank_requirements( $rank->ID );

	$completed = true;

	foreach( $requirements as $requirement ) {

        // Skip optional requirements
        if( ( bool ) gamipress_get_post_meta( $requirement->ID, '_gamipress_optional' ) ) {
            continue;
        }

		// Check if rank requirement has been earned
		if( gamipress_get_earnings_count( array(
			'user_id'   => $user_id,
			'post_id'   => $requirement->ID,
			'since'     => strtotime( $rank->post_date )
		) ) === 0 ) {
			$completed = false;
			break;
		}

	}

	// If all rank requirements has been completed, award the rank
	if ( $completed ) {
		gamipress_update_user_rank( $user_id, $rank->ID, false, $achievement_id );
	}

}
add_action( 'gamipress_award_achievement', 'gamipress_maybe_award_rank', 10, 2 );

/* --------------------------------------------------
 * Revoke Checks
   -------------------------------------------------- */

/**
 * Revoke an achievement to the user
 *
 * @since  	1.4.3
 *
 * @param int 	$achievement_id The given achievement's post ID
 * @param int 	$user_id        The given user's ID
 * @param int 	$earning_id     The user's earning ID
 *
 * @return void
 */
function gamipress_revoke_achievement_to_user( $achievement_id = 0, $user_id = 0, $earning_id = 0 ) {

	// Use the current user's ID if none specified
	if ( ! $user_id ) {
		$user_id = get_current_user_id();
    }

	// Setup CT object
	$ct_table = ct_setup_table( 'gamipress_user_earnings' );

    // If earning ID not provide, find it
	if( $earning_id === 0 ) {

		$query = new CT_Query( array(
			'user_id' => $user_id,
			'post_id' => $achievement_id,
			'items_per_page' => 1
		) );

		$results = $query->get_results();

		if( count( $results ) > 0 ) {
			$earning_id = $results[0]->user_earning_id;
		}

	}

    $earning = false;

    if( $earning_id ) {
        $earning = ct_get_object( $earning_id );
    }

    // If achievement ID provided, check if requirement should get revoked too
    if( $achievement_id !== 0 ) {

        // Setup vars
        $post_type = gamipress_get_post_type( $achievement_id );
        $points_types = gamipress_get_points_types();
        $is_achievement = in_array( $post_type, gamipress_get_achievement_types_slugs() );
        $is_rank = in_array( $post_type, gamipress_get_rank_types_slugs() );

        // If is achievement or rank, revoke also all its requirements
        if ( $is_achievement || $is_rank ) {

            /**
             * Available filter to determine if should revoke requirements when parent element is revoked too
             *
             * @since  	1.8.7
             *
             * @param bool 	$revoke         Revoke confirmation, by default true
             * @param int 	$user_id        The given user ID
             * @param int 	$achievement_id The post ID to revoke
             * @param int 	$earning_id     The user earning ID
             */
            $revoke_requirements = apply_filters( 'gamipress_revoke_requirements_on_revoke_parent', true, $achievement_id, $user_id, $earning_id );

            if( $revoke_requirements ) {

                $requirements = array();

                if ( $is_achievement ) {
                    // Get the achievement steps
                    $requirements = gamipress_get_achievement_steps( $achievement_id );
                } else if ( $is_rank ) {
                    // Get the rank requirements
                    $requirements = gamipress_get_rank_requirements( $achievement_id );
                }

                foreach( $requirements as $requirement ) {
                    gamipress_revoke_achievement_to_user( $requirement->ID, $user_id );
                    $last_interaction = current_time( 'timestamp' );
                    update_user_meta( $user_id, '_gamipress_' . $requirement->ID . '_last_interaction_timestamp', $last_interaction );
                }

            }

        }

        // If is revoking the current user rank, update user rank with the previous one
        if( $is_rank ) {

            $user_rank_id = gamipress_get_user_rank_id( $user_id, $post_type );

            if( $achievement_id === $user_rank_id ) {

                $prev_rank_id = gamipress_get_prev_rank_id( $user_rank_id );

                $prev_rank_query = new CT_Query( array(
                    'user_id' => $user_id,
                    'post_id' => $prev_rank_id,
                    'items_per_page' => 1
                ) );

                $prev_rank_earnings = $prev_rank_query->get_results();

                // If previous rank is registered in user earnings, remove the action to register it in user earnings
                if( count( $prev_rank_earnings ) > 0 ) {
                    remove_action( 'gamipress_update_user_rank', 'gamipress_register_user_rank_earning', 10 );
                }

                // Update the user rank with the previous one
                gamipress_update_user_rank( $user_id, $prev_rank_id );

                // Restore the removed action
                if( count( $prev_rank_earnings ) > 0 ) {
                    add_action( 'gamipress_update_user_rank', 'gamipress_register_user_rank_earning', 10, 5 );
                }

            }

        }

        if( $earning ) {
            // Get the points and points type from the user earning entry
            $points = absint( $earning->points );
            $points_type = $earning->points_type;
        } else {
            // Get the points and points type from the post provided
            $points = absint( gamipress_get_post_meta( $achievement_id, '_gamipress_points' ) );
            $points_type = gamipress_get_post_meta( $achievement_id, '_gamipress_points_type' );
        }

        if( $points !== 0 ) {

            // Turn the "points-type" post type to award or deduct type depending on the amount of points assigned
            if( $post_type === 'points-type' ) {
                if( $points > 0 ) {
                    $post_type = 'points-award';
                } else {
                    $post_type = 'points-deduct';
                }
            }

            if ( in_array( $post_type, gamipress_get_achievement_types_slugs() ) || $post_type === 'points-award' ) {

                /**
                 * Available filter to determine if should deduct points when an element is revoked
                 *
                 * @since  	1.8.9
                 *
                 * @param bool 	$revoke         Deduct confirmation, by default true
                 * @param int 	$user_id        The given user ID
                 * @param int 	$achievement_id The post ID to revoke (commonly an achievement ID or points award ID)
                 * @param int 	$earning_id     The user earning ID
                 */
                $deduct_points = apply_filters( 'gamipress_deduct_points_on_revoke', true, $achievement_id, $user_id, $earning_id );

                if( $deduct_points ) {

                    if ( ! empty( $points ) && isset( $points_types[$points_type] ) ) {
                        // Revoke points awarded from this achievement or points award
                        gamipress_deduct_points_to_user( $user_id, $points, $points_type, array( 'achievement_id' => $achievement_id ) );
                    }

                }

            }

            if ( $post_type === 'points-deduct' ) {

                /**
                 * Available filter to determine if should award points when an element is revoked
                 *
                 * @since  	1.8.9
                 *
                 * @param bool 	$revoke         Award confirmation, by default true
                 * @param int 	$user_id        The given user ID
                 * @param int 	$achievement_id The post ID to revoke (commonly a points deduct ID)
                 * @param int 	$earning_id     The user earning ID
                 */
                $award_points = apply_filters( 'gamipress_award_points_on_revoke', true, $achievement_id, $user_id, $earning_id );

                if( $award_points ) {

                    if ( ! empty( $points ) && isset( $points_types[$points_type] ) ) {
                        // Restore points deducted from these points deduct
                        gamipress_award_points_to_user( $user_id, $points, $points_type, array( 'achievement_id' => $achievement_id ) );
                    }

                }

            }

        }

        /**
         * Available action per type for triggering other processes
         *
         * @since 2.3.1
         *
         * @param int 	$user_id        The given user's ID
         * @param int 	$achievement_id The given achievement's post ID
         * @param int 	$earning_id     The user's earning ID
         */
        do_action( 'gamipress_revoke_' . $post_type, $user_id, $achievement_id, $earning_id );

    }

    /**
     * Available action for triggering other processes
     *
     * @since 1.4.3
     *
     * @param int 	$user_id        The given user's ID
     * @param int 	$achievement_id The given achievement's post ID
     * @param int 	$earning_id     The user's earning ID
     */
	do_action( 'gamipress_revoke_achievement_to_user', $user_id, $achievement_id, $earning_id );

	if( $earning_id ) {
		$ct_table->db->delete( $earning_id );
	}

    /**
     * Available action for triggering after removing achievement
     *
     * @since 2.5.9
     *
     * @param int 	$user_id        The given user's ID
     * @param int 	$achievement_id The given achievement's post ID
     * @param int 	$earning_id     The user's earning ID
     */
	do_action( 'gamipress_revoke_achievement_to_user_after', $user_id, $achievement_id, $earning_id );

    ct_reset_setup_table();

}

/**
 * Award additional achievements to user
 *
 * @since 2.3.1
 *
 * @param  int $user_id        The given user's ID
 * @param  int $achievement_id The given achievement's post ID
 *
 * @return void
 */
function gamipress_maybe_award_additional_achievements_to_user_on_revoke( $user_id = 0, $achievement_id = 0 ) {

    // Get the achievement post type
    $post_type = gamipress_get_post_type( $achievement_id );
    $achievement_types = gamipress_get_achievement_types_slugs();
    $rank_types = gamipress_get_achievement_types_slugs();

    // Get achievements that can be earned from completing this achievement
    $dependent_achievements = gamipress_get_dependent_achievements( $achievement_id );

    // Loop through each dependent achievement and see if it can be awarded
    foreach ( $dependent_achievements as $achievement ) {
        $trigger = '';

        $trigger_type = gamipress_get_post_meta( $achievement->ID, '_gamipress_trigger_type' );

        // Skip achievements for awarding
        if( in_array( $trigger_type, array( 'specific-achievement', 'earn-rank' ) ) ) {
            continue;
        }

        if( in_array( $post_type, $achievement_types ) ) {
            $trigger = 'revoke-specific-achievement';
        } else if( in_array( $post_type, $rank_types ) ) {
            $trigger = 'revoke-rank';
        }

        gamipress_maybe_award_achievement_to_user( $achievement->ID, $user_id, $trigger );
    }

}
add_action( 'gamipress_revoke_achievement_to_user', 'gamipress_maybe_award_additional_achievements_to_user_on_revoke', 10, 2 );