<?php
/**
 * Subscription Discounts class
 *
 * This class handles all purchasing discounts for members with an active subscription plan
 *
 */
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) exit;


class PMS_WOO_Subscription_Discounts {


    /** @var bool Whether products on sale are excluded from discounts. */
    private $exclude_on_sale_products = false;

    /** @var bool Whether cumulative membership discounts are allowed. */
    private $allow_cumulative_discounts = false;

    /** @var bool Whether the current user is logged in and is an active member. */
    private $is_active_member = false;



    /**
     * Set up subscription discounts for members
     *
     */
    public function __construct() {

        // init discounts so we don't hook too early
        add_action('init', array($this, 'init'));

    }


    /**
     * Init member discounts.
     *
     * We follow here a pattern common in many price-affecting extensions, due to
     * the need to produce a "price before/after discount" type of HTML output,
     * so shop customers can easily understand the deal they're being offered.
     *
     * To do so we need to juggle WooCommerce prices, we start off by instantiating
     * this class with our discounts active, so we can be sure to always pass those
     * to other extensions if a member is logged in. In Paid Member Subscriptions, we
     * filter sale prices and pass member discounts as apparent sale prices. So WooCommerce
     * core can trigger the HTML output sought by Paid Member Subscriptions, which shows a
     * before/after price change.
     *
     * Extensions and third party code that need to know if PMS price modifiers
     * are being applied or not in these two phases, can use doing_action and hook into
     * 'pms_woo_subscription_discounts_enable_price_adjustments' and
     * 'pms_woo_subscription_discounts_disable_price_adjustments', or call directly the
     * callback methods found in this class, which we use to add and remove
     * price modifiers.
     */
    public function init() {

        // check if on sale products are excluded from membership discounts
        $options = get_option( 'pms_woocommerce_settings' );

        if ( !empty($options) && isset($options['exclude_on_sale']) ) {
            $this->exclude_on_sale_products = true;
        }

        // check if "allow cumulative discounts" is set for members of multiple subscription plans with overlapping discount codes for the same products
        if ( !empty($options) && isset($options['cumulative_discounts']) ) {
            $this->allow_cumulative_discounts = true;
        }

        // "Product Discounted - Membership Required" message, displayed to non-members if the product has a membership discount.
        add_action( 'woocommerce_single_product_summary', array($this, 'product_discounted_membership_required_message'), 31 );

        // Refreshes the mini cart upon member login.
        add_action('wp_login', array($this, 'refresh_cart_upon_member_login'), 10, 2);

        // Clear all transients for variation prices after updating/saving a subscription plan (due to possible discount codes modification) which will result in price changes
        add_action('save_post_pms-subscription', array($this, 'after_subscription_plan_discounts_updated'), 10 ,1);
        // Clear all transients for variation prices after updating PMS WooCommerce settings
        add_action('update_option_pms_settings', array($this, 'after_pms_woocommerce_settings_updated'), 10, 2);

        // Subscription discount class methods are available on both frontend and backend,
        // but the hooks below should run in frontend only for logged in members.
        if (!(is_admin() && !is_ajax())) {

            $this->is_active_member = pms_is_member( get_current_user_id() );

            // Initialize discount actions that will be called in this class methods
            add_action('pms_woo_discounts_enable_price_adjustments', array($this, 'enable_price_adjustments'));
            add_action('pms_woo_discounts_disable_price_adjustments', array($this, 'disable_price_adjustments'));

            if ($this->is_active_member) {

                // Load the current member discount context before common Woo rendering and calculation hooks
                add_action( 'woocommerce_before_single_product', array( $this, 'load_member_discount_context' ) );
                add_action( 'woocommerce_before_shop_loop_item', array( $this, 'load_member_discount_context' ) );
                add_action( 'woocommerce_before_calculate_totals', array($this, 'load_member_discount_context') );
                add_action( 'woocommerce_before_mini_cart_contents', array($this, 'load_member_discount_context') );

                // Activate discounts for logged in, active members.
                do_action( 'pms_woo_discounts_enable_price_adjustments' );

                // Force calculations in cart.
                add_filter( 'woocommerce_update_cart_action_cart_updated', '__return_true' );

               // Replace "Sale" badge with "Member Discount!" for products which have valid membership discounts
                add_action( 'woocommerce_sale_flash' , array($this, 'get_member_discount_badge' ), 10, 3 );
                add_filter( 'woocommerce_sale_badge_text', array( $this, 'get_member_discount_badge_text' ), 10, 2 );
                add_filter( 'woocommerce_blocks_product_grid_item_html', array( $this, 'get_member_block_grid_item_html' ), 10, 3 );
                // Display "Member Discount" suffix for variation prices.
                add_filter( 'woocommerce_get_price_html', array( $this, 'get_member_price_html' ), 999, 2 );

            }
        }
    }


    /**
     * Build member discount context data for a user
     *
     * - used to collect the active subscription ids and applicable plan-level discount rules for a member
     *
     * @param int $member_id User id
     * @return array
     */
    private function get_member_discount_context_data( $member_id ) {

        $context = array(
            'discounts'     => array(),
            'subscriptions' => array(),
        );

        if ( empty( $member_id ) ) {
            return $context;
        }

        $member = pms_get_member( $member_id );

        if ( empty( $member ) || empty( $member->subscriptions ) ) {
            return $context;
        }

        foreach ( $member->subscriptions as $subscription ) {

            if ( $subscription['status'] == 'active' || $subscription['status'] == 'canceled' ) {

                $context['subscriptions'][] = $subscription['subscription_plan_id'];
                $discounts                  = get_post_meta( (int)$subscription['subscription_plan_id'], 'pms-woo-subscription-plan-product-discounts', true );

                if ( !empty($discounts) ) {

                    foreach ( $discounts as $key => $discount ) {
                        if ( isset( $discount['status'] ) && $discount['status'] == 'inactive' ) {
                            unset( $discounts[$key] );
                        }
                    }

                    $context['discounts'] = array_merge( $context['discounts'], $discounts );
                }
            }
        }

        return $context;
    }


    /**
     * Load member discount context into globals and return it
     *
     * - used both as an eager bootstrap on Woo hooks and as a lazy fallback during discount resolution
     *
     * @param int|null $member_id Optional user id defaults to current user
     * @return array
     */
    public function load_member_discount_context( $member_id = null ) {

        global $pms_woo_member_discounts, $pms_woo_member_subscriptions;

        // Normalize the hook argument to a user id when possible
        // - this method is also used as a Woo action callback
        // - hooks like `woocommerce_before_calculate_totals` pass objects such as `WC_Cart` as the first argument
        // - any non-user hook payload should fall back to the current logged-in user
        if ( is_numeric( $member_id ) ) {
            $member_id = (int) $member_id;
        } else {
            $member_id = get_current_user_id();
        }

        if ( empty( $member_id ) ) {
            return array(
                'discounts'     => array(),
                'subscriptions' => array(),
            );
        }

        // Globals are meant to cache the current logged-in user's discount context.
        if ( $member_id === get_current_user_id() ) {
            if ( !isset( $pms_woo_member_discounts ) || !isset( $pms_woo_member_subscriptions ) ) {
                $context                      = $this->get_member_discount_context_data( $member_id );
                $pms_woo_member_discounts     = $context['discounts'];
                $pms_woo_member_subscriptions = $context['subscriptions'];
            }

            return array(
                'discounts'     => $pms_woo_member_discounts,
                'subscriptions' => $pms_woo_member_subscriptions,
            );
        }

        return $this->get_member_discount_context_data( $member_id );
    }

    //@TODO: check this shit
    /**
     * Clear all transients for variation prices after updating PMS WooCommerce settings
     *
     * Fires after updating the PMS WooCommerce settings page, and if there are changes, clears all transients variation prices
     * @param mixed $old_value the old option value
     * @param mixed $value the new option value
     */
    public function after_pms_woocommerce_settings_updated( $old_value, $value ){

        // check if PMS WooCommerce settings have been updated (modified)
        if ( isset($value['woocommerce']) && isset($old_value['woocommerce']) ) {

            if (count($value['woocommerce']) != count($old_value['woocommerce'])) {

                // if so, clear all transients variation prices
                $this->clear_all_variation_prices_transients();

            }
        }
    }

    /**
     * Clear all transients for variation prices after saving a Subscription Plan details
     *
     * If there are any active membership discounts attached, clears all transients variation prices
     * @param int $post_id the subscription plan id
     */
    public function after_subscription_plan_discounts_updated( $post_id ){

        if ( empty($post_id) ){
            // get current post id
            global $post;

            $post_id = $post->ID;
        }

        $discounts = get_post_meta( $post_id, 'pms-woo-subscription-plan-product-discounts', true );

        if ( !empty($discounts) ) {
            // check if subscription plan has active membership discounts
            foreach ($discounts as $key => $discount) {
                if ($discount['status'] == 'inactive') unset($discounts[$key]);
            }
        }

        // If we have active membership discounts for this subscription plan, clear all transients for variation prices
        if ( !empty($discounts) ) {
            $this->clear_all_variation_prices_transients();
        }

    }


    /**
     * Clear all variation prices transients
     *
     * Single transient is used per product for all variation prices, so we must remove all of them
     */
    public function clear_all_variation_prices_transients(){

        global $wpdb;

        $table = $wpdb->options;

        $wpdb->query( $wpdb->prepare( "
          DELETE FROM `$table`
          WHERE option_name LIKE %s", '%\_wc\_var\_prices\_%'
        ) );

    }



    /**
     * * Enable price adjustments for product prices based on membership discounts for logged in members
     *
     */
    public function enable_price_adjustments() {

        // Apply membership discount to product price.
        add_filter( 'woocommerce_product_get_sale_price',              array( $this, 'get_member_price' ), 999, 2 );
        add_filter( 'woocommerce_product_variation_get_sale_price',    array( $this, 'get_member_price' ), 999, 2 );
        add_filter( 'woocommerce_product_get_price',                   array( $this, 'get_member_price' ), 999, 2 );
        add_filter( 'woocommerce_product_variation_get_price',         array( $this, 'get_member_price' ), 999, 2 );

        // Replace regular price with sale (using a filter)
        /** @see \PMS_WOO_Subscription_Discounts::member_prices_display_sale_price() */
        add_filter( 'woocommerce_product_get_regular_price',           array( $this, 'get_member_regular_price' ), 999, 2 );
        add_filter( 'woocommerce_product_variation_get_regular_price', array( $this, 'get_member_regular_price' ), 999, 2 );


        // Apply membership discount to variation price.
        add_filter( 'woocommerce_variation_prices_sale_price',    array( $this, 'get_member_variation_price' ), 999, 3 );
        add_filter( 'woocommerce_variation_prices_price',         array( $this, 'get_member_variation_price' ), 999, 3 );
        add_filter( 'woocommerce_variation_prices_regular_price', array( $this, 'get_member_variation_regular_price' ), 999, 3 );
        // Clear variation prices cache.
        add_filter( 'woocommerce_get_variation_prices_hash',      array( $this, 'set_user_variation_prices_hash' ), 999, 2 );

    }


    /**
     * Disable price adjustments.
     *
     * Calling this method will **disable** membership adjustments
     * for product prices that have member discounts for logged in members.
     *
     */
    public function disable_price_adjustments() {

        // Restore prices to original amount before membership discount.
        remove_filter( 'woocommerce_product_get_sale_price',              array( $this, 'get_member_price' ), 999 );
        remove_filter( 'woocommerce_product_get_price',                   array( $this, 'get_member_price' ), 999 );
        remove_filter( 'woocommerce_product_variation_get_price',         array( $this, 'get_member_price' ), 999 );
        remove_filter( 'woocommerce_product_variation_get_sale_price',    array( $this, 'get_member_price' ), 999 );
        remove_filter( 'woocommerce_product_get_regular_price',           array( $this, 'get_member_regular_price' ), 999 );
        remove_filter( 'woocommerce_product_variation_get_regular_price', array( $this, 'get_member_regular_price' ), 999 );

        remove_filter( 'woocommerce_variation_prices_sale_price',    array( $this, 'get_member_variation_price' ), 999 );
        remove_filter( 'woocommerce_variation_prices_price',         array( $this, 'get_member_variation_price' ), 999 );
        remove_filter( 'woocommerce_variation_prices_regular_price', array( $this, 'get_member_variation_regular_price' ), 999 );
        remove_filter( 'woocommerce_get_variation_prices_hash',      array( $this, 'set_user_variation_prices_hash' ), 999 );
    }


    /**
     * Check if a product is to be excluded from membership discounts.
     *
     * @param int|\WP_Post|\WC_Product $product Product object or id.
     * @return bool
     */
    public function is_product_excluded_from_member_discounts( $product ) {

        if ( is_numeric( $product ) ) {
            $product = wc_get_product( $product );
        } elseif ( $product instanceof WP_Post ) {
            $product = wc_get_product( $product );
        }

        if ( ! $product instanceof WC_Product ) {
            return false;
        }

        // if is variation, use the id of the parent product
        if ($product->is_type( 'variation' )) {
            $product_id = $product->get_parent_id();
        } else {
            $product_id = $product->get_id();
        }

        // exclude if product-level setting is set to exclude this product from all membership discounts
        $exclude_product = 'exclude' === get_post_meta( (int)$product_id, 'pms-woo-product-membership-discounts-behaviour', true);
        // exclude if product is on sale and global PMS setting is enabled to exclude all products on sale from membership discounts
        $exclude_on_sale = $this->exclude_on_sale_products && $this->get_product_unfiltered_sale_status( $product );

        /**
         * Filter product from having discount rules applied.
         *
         * @param bool $exclude Whether the product is excluded from discount rules.
         * @param \WC_Product $product The product object.
         */
         $exclude = (bool) apply_filters( 'pms_woo_exclude_product_from_member_discounts', $exclude_product || $exclude_on_sale, $product );

        return $exclude;
    }


    /**
     * Adjust discounted product price HTML.
     *
     * @param string $html The price HTML maybe after discount.
     * @param \WC_Product|\WC_Product_Variable|\WC_Product_Variation $product The product object for which we may have discounts.
     * @return string The original price HTML if no discount or a new formatted string showing before/after discount.
     */
    public function get_member_price_html( $html, $product ) {
        global $post;

        /**
         * Controls whether or not member prices should use discount format when displayed.
         *
         * @param bool $use_discount_format Defaults to true.
         */
        $use_discount_format = (bool) apply_filters( 'pms_woo_member_prices_use_discount_format', true );

        // Bail out if any of the following conditions applies:
        // - custom code set to not to use discount format
        // - user is not logged in or an active member
        // - product is excluded from discount rules
        // - current user has no discounts for the product
        if (      $use_discount_format
            &&   $this->is_active_member
            && ! $this->is_product_excluded_from_member_discounts( $product )
            &&  $this->get_user_membership_discounts( $product ) ) {

            $html_after_discount  = $html;

            // Add "Member Discount!" suffix for single variation prices.
            if ( $product->is_type( 'variation' ) ) {

                $html_after_discount .= ' ' . $this->get_member_discount_badge( '', $post, $product );

            }

            /**
             * Filter the HTML price after member discounts have been applied.
             *
             * @since 1.7.2
             * @param string $html The price HTML output.
             * @param \WC_Product $product The product the discounted price is meant for.
             * @param string $html_before_discount Original HTML before discounts.
             * @param string $html_after_discount Original HTML after discounts.
             *
             */
            $html = (string) apply_filters( 'pms_woo_get_discounted_price_html', $html_after_discount, $product );
        }

        /**
         * Filter the HTML price after member discounts may have been applied.
         *
         * @since 1.7.1
         * @param string $html The price HTML.
         * @param \WC_Product $product The product the price is meant for.
         */
        return apply_filters( 'pms_woo_get_price_html', $html, $product );
    }



    /**
     * Get the unfiltered sale status.
     *
     * Used to determine if a product was marked on sale before membership price adjustments.
     *
     * @param \WC_Product $product The product object.
     * @return bool
     */
    private function get_product_unfiltered_sale_status( $product ) {

        // Temporarily disable membership price adjustments.
        do_action( 'pms_woo_discounts_disable_price_adjustments' );
        remove_filter( 'woocommerce_get_price_html', array( $this, 'get_member_price_html' ), 999, 2 );

        $on_sale = $product->is_on_sale();

        // Re-enable membership price adjustments.
        do_action( 'pms_woo_discounts_enable_price_adjustments' );
        add_filter( 'woocommerce_get_price_html', array( $this, 'get_member_price_html' ), 999, 2 );

        return $on_sale;
    }


    /**
     * Whether to show sale prices as regular when displaying discounts to members.
     *
     * @return bool
     */
    private function member_prices_display_sale_price() {

        /**
         * Controls whether or not member prices should display sale prices as well.
         *
         * @param bool $display_sale_price Defaults to false.
         */
        return (bool) apply_filters( 'pms_woo_member_prices_display_sale_price', false );
    }


    /**
     * Apply purchasing member discounts to product price.
     *
     * @param string|int|float $price Price to discount (normally a float, maybe a string number).
     * @param \WC_Product $product The product object.
     * @return float Price.
     */
    public function get_member_price( $price, $product ) {

        // Bail out if any of the following is true:
        // - user is not logged in or an active member
        // - product is excluded from member discounts
        // - user has no member discount over the product
        if (     $this->is_active_member
            && ! $this->is_product_excluded_from_member_discounts( $product )
            &&   $this->get_user_membership_discounts( $product ) ) {

            // Account also for variation sale price filter.
            if ( in_array( current_filter(), array( 'woocommerce_product_get_sale_price', 'woocommerce_product_variation_get_sale_price' ), false ) ) {
                $member_price = $product->get_price();
            } else {
                $member_price = $this->get_discounted_price( $price, $product, get_current_user_id() );
            }

            $price = is_numeric( $member_price ) ? $member_price : $price;
        }

        return $price;
    }


    /**
     * Apply purchasing discounts to variation price.
     *
     * @param string|int|float $price Price to discount (normally a float, maybe a string number).
     * @param \WC_Product_Variation $variation The variation object.
     * @param \WC_Product $product The product object.
     * @return float Price.
     */
    public function get_member_variation_price( $price, $variation, $product ) {

        // Bail out if any of the following is true:
        // - user is not logged in or not an active member
        // - product is excluded from member discounts
        // - user has no member discount over the product
        if (     $this->is_active_member
            && ! $this->is_product_excluded_from_member_discounts( $variation )
            &&   $this->get_user_membership_discounts( $variation ) ) {

            if ( 'woocommerce_variation_prices_sale_price' === current_filter() ) {
                $member_price =  apply_filters( 'woocommerce_variation_prices_price', $variation->get_price('edit'), $variation, $product );
            } else {
                $member_price = $this->get_discounted_price( $price, $variation, get_current_user_id() );
            }

            $price = is_numeric( $member_price ) ? $member_price : $price;
        }

        return $price;
    }


    /**
     * Replace regular prices with sale before discounts when calculating price html strings.
     * We basically show sale prices to members as reference instead of regular prices.
     *
     * @param string|int|float $regular_price Regular price used as reference.
     * @param \WC_Product $product The product object.
     * @return float Price.
     */
    public function get_member_regular_price( $regular_price, $product ) {

        // Bail out if any of the following is true:
        // - user is not logged in or not an active member
        // - product is excluded from member discounts
        // - user has no member discount over the product

        if (      $this->is_active_member
            && !$this->is_product_excluded_from_member_discounts( $product )
            &&  $this->get_user_membership_discounts( $product )
            && $this->member_prices_display_sale_price() ) {

            // Temporarily disable membership price adjustments, so we can get sale price without querying the db.
            do_action( 'pms_woo_discounts_disable_price_adjustments' );

            if ( $product->is_on_sale() ) {
                $regular_price = $product->get_sale_price();
            }

            // Re-enable membership price adjustments.
            do_action( 'pms_woo_discounts_enable_price_adjustments' );
        }

        return $regular_price;
    }


    /**
     * Replace regular prices with sale before discounts when calculating price html strings.
     * We basically show sale prices to members as reference instead of regular prices.
     *
     * @param string|int|float $regular_price Regular price used as reference.
     * @param \WC_Product_Variation $variation The variation object.
     * @param \WC_Product $product The product object.
     * @return float Price.
     */
    public function get_member_variation_regular_price( $regular_price, $variation, $product ) {

        // Bail out if any of the following is true:
        // - user is not logged in or not an active member
        // - product is excluded from member discounts
        // - user has no member discount over the product
        if (      $this->is_active_member
            && ! $this->is_product_excluded_from_member_discounts( $product )
            &&   $this->get_user_membership_discounts( $product )
            &&   $this->member_prices_display_sale_price() ) {

            // Temporarily disable membership price adjustments.
            do_action( 'pms_woo_discounts_disable_price_adjustments' );

            $price         = apply_filters( 'woocommerce_variation_prices_price', $variation->get_price( 'edit' ), $variation, $product );
            $sale_price    = apply_filters( 'woocommerce_variation_prices_sale_price', $variation->get_sale_price( 'edit' ), $variation, $product );
            $regular_price = $regular_price !== $sale_price && $price === $sale_price ? $sale_price : $regular_price;

            // Re-enable membership price adjustments.
            do_action( 'pms_woo_discounts_enable_price_adjustments' );
        }

        return $regular_price;
    }


    /**
     * Add the current user ID to the variation prices hash for caching.
     *
     * @param array $data The existing hash data.
     * @param \WC_Product $product The current product variation.
     * @return array $data The hash data with a user ID added if applicable.
     */
    public function set_user_variation_prices_hash( $data, $product ) {

        // Bail out if:
        // - user is not logged in or not an active member
        // - logged in user has no membership discount over the product
        // - product is being explicitly excluded from member discounts
        if (      $this->is_active_member
            && ! $this->is_product_excluded_from_member_discounts( $product )
            &&   $this->get_user_membership_discounts( $product ) ){

            $data[] = get_current_user_id();

            if ( $this->member_prices_display_sale_price() ) {
                $data[] = 'member_prices_display_sale_price';
            }
        }

        return $data;
    }


    /**
     * Get member discount badge.
     *
     * @param \WC_Product $product The product object to output a badge for (passed to filter).
     * @param bool $variation Whether to output a discount badge specific for a product variation (default false).
     * @return string
     */
    public function get_member_discount_badge( $badge, $post, $product ) {

        // Bail out if any of the following conditions applies:
        // - no member user is logged in
        // - product is excluded from membership discount
        // - current user has no discounts for the product
        if ( $this->is_active_member
            && !$this->is_product_excluded_from_member_discounts( $product )
            && $this->get_user_membership_discounts( $product ) ) {

            $options = get_option( 'pms_woocommerce_settings' );

            $label = !empty( $options['discount_badge'] ) ? $options['discount_badge'] : __( 'Member discount!', 'paid-member-subscriptions' );

            if ( $product->is_type( array( 'variation' ) ) ) {

                $badge = '<span class="pms-woo-variation-member-discount-badge">' . esc_html($label) . '</span>';

                /**
                 * Filter the variation member discount badge.
                 *
                 * @param string $badge The badge HTML.
                 * @param \WC_Product|\WC_Product_Variation $variation The product variation.
                 */
                $badge = apply_filters('pms_woo_variation_member_discount_badge', $badge, $product);
            }
            else {

                $badge = '<span class="onsale pms-woo-member-discount-badge">' . esc_html($label) . '</span>';

               /**
                * Filter the member discount badge.
                *
                * @param string $badge The badge HTML.
                * @param \WP_Post $post The product post object.
                */
                $badge = apply_filters('pms_woo_member_discount_badge', $badge, $product);
            }
        }

        return $badge;
    }


    /**
     * Get member discount badge text for block-based WooCommerce sale badges
     *
     * - used by the modern Woo block sale badge path that does not call `woocommerce_sale_flash`
     *
     * @param string      $sale_text The default sale badge text
     * @param \WC_Product $product The product object
     * @return string
     */
    public function get_member_discount_badge_text( $sale_text, $product ) {

        if ( $this->is_active_member
            && !$this->is_product_excluded_from_member_discounts( $product )
            && $this->get_user_membership_discounts( $product ) ) {

            $options = get_option( 'pms_woocommerce_settings' );

            return !empty( $options['discount_badge'] ) ? $options['discount_badge'] : __( 'Member discount!', 'paid-member-subscriptions' );
        }

        return $sale_text;
    }


    /**
     * Replace hardcoded sale badges used by older WooCommerce product grid blocks
     *
     * - used for legacy Woo blocks that bypass both `woocommerce_sale_flash` and `woocommerce_sale_badge_text`
     *
     * @param string       $html Product grid item HTML
     * @param object|array $data Product grid item data
     * @param \WC_Product  $product Product object
     * @return string
     */
    public function get_member_block_grid_item_html( $html, $data, $product ) {

        $current_badge = is_object( $data ) && isset( $data->badge ) ? $data->badge : '';

        if ( empty( $current_badge ) ) {
            return $html;
        }

        if ( $this->is_active_member
            && !$this->is_product_excluded_from_member_discounts( $product )
            && $this->get_user_membership_discounts( $product ) ) {

            $member_badge = $this->get_member_block_grid_badge_markup();

            if ( $member_badge !== $current_badge ) {
                $html = str_replace( $current_badge, $member_badge, $html );
            }
        }

        return $html;
    }


    /**
     * Get member badge markup for older WooCommerce product grid blocks
     *
     * - keeps the original Woo grid badge structure and swaps only the visible label
     *
     * @return string
     */
    private function get_member_block_grid_badge_markup() {

        $options = get_option( 'pms_woocommerce_settings' );
        $label   = !empty( $options['discount_badge'] ) ? $options['discount_badge'] : __( 'Member discount!', 'paid-member-subscriptions' );

        return '<div class="wc-block-grid__product-onsale">
            <span aria-hidden="true">' . esc_html( $label ) . '</span>
            <span class="screen-reader-text">' . esc_html__( 'Product on sale', 'paid-member-subscriptions' ) . '</span>
        </div>';
    }


    /**
     * Get product discounted price for member.
     *
     * @param float $base_price Original price.
     * @param int|\WC_Product $product Product ID or product object.
     * @param int|null $member_id Optional, defaults to current user id.
     * @return float|null The discounted price or null if no discount applies.
     */
    public function get_discounted_price( $base_price, $product, $member_id = null ) {

        if ( empty( $member_id ) ) {
            $member_id = get_current_user_id();
        }

        if ( is_numeric( $product ) ) {
            $product = wc_get_product( (int) $product );
        }

        $price          = null;
        $product_id     = null;
        $member_discounts = array();

        // We need a product and a user to get a member discounted price.
        if ( $product instanceof WC_Product && $member_id > 0 ) {

            // if is variation, use the id of the parent product
            if ($product->is_type( 'variation' )) {
                $product_id = $product->get_parent_id();
            } else {
                $product_id = $product->get_id();
            }

            $member_discounts = $this->get_user_membership_discounts($product, $member_id);
        }


        if ( $product_id && !empty($member_discounts) ) {
            // We have membership discounts that need to be applied to the product price

            $price = (float)$base_price;
            $discounted_price = $price;
            $prices = array();

            $discounts_behaviour = get_post_meta((int)$product_id, 'pms-woo-product-membership-discounts-behaviour', true);
            //for products added in Woocommerce before activating PMS and running the save_post hook added in PMS
            if ( empty($discounts_behaviour) )
                $discounts_behaviour = 'default';

            $discount_location = array();
            $discount_type = array('fixed', 'percent');

            switch ($discounts_behaviour) {
                case 'default':  // best price
                    $discount_location = array('subscription_plan', 'product');
                    break;
                case 'ignore':  //apply only discounts set per product, ignore the rest
                    $discount_location = array('product');
            }


            // Apply discounts and store both lowest individual price (after applying just one discount -> $prices) and lowest cumulative price (after applying all discounts -> $discounted_price)
            foreach ($discount_location as $location) {

                foreach ($discount_type as $type) {

                    if (!empty($member_discounts[$location])) {

                        if (!empty($member_discounts[$location][$type])) {

                            foreach ($member_discounts[$location][$type] as $discount_amount) {

                                switch ($type) {
                                    case 'fixed' :
                                        $discounted_price = max($discounted_price - (float)$discount_amount, 0);
                                        $prices[] = max($price - (float)$discount_amount, 0);
                                        break;

                                    case 'percent' :
                                        $discounted_price = max($discounted_price * (100 - (float)$discount_amount) / 100, 0);
                                        $prices[] = max($price * (100 - (float)$discount_amount) / 100, 0);
                                        break;
                                }

                            }
                        }
                    }
                }
            }


            /**
             * Filter whether to allow stacking product discounts for members of multiple plans
             * with overlapping discount rules for the same products
             *
             * @param bool $allow_cumulative_discounts Default false (do not allow).
             * @param int $member_id The user id discounts are calculated for.
             * @param \WC_Product $product The product object being discounted.
             */
            $allow_cumulative_discounts = apply_filters('pms_woo_allow_cumulative_member_discounts', $this->allow_cumulative_discounts, $member_id, $product);

            // Pick the best price based on $allow_cumulative_discounts option value.
            if (true === $allow_cumulative_discounts) {
                $price = $discounted_price;

            } else if (!empty($prices)) {
                $price = min($prices);
            }

            // Sanity check.
            if ($price >= $base_price) {
                $price = null;
            }

        }// end if ( $product_id && !empty($member_discounts) )

        /**
         * Filter discounted membership price of a product.
         *
         * @param null|float $price The discounted price or null if no discount applies.
         * @param float $base_price The original price (not discounted by PMS).
         * @param int $product_id The id of the product (or variation) the price is for.
         * @param int $member_id The id of the logged in member (it's zero for non logged in users).
         * @param \WC_Product $product The product object for the price being discounted.
         */
        return apply_filters( 'pms_woo_get_discounted_price', $price, $base_price, $product_id, $member_id, $product );
    }


    /**
     * Check if the user has any membership discounts for the product
     *
     * @param int|\WC_Product $product Product ID or object.
     * @param null|int $user_id Optional, defaults to current user id.
     * @return array()| $member_discount containing an array of all user discounts for the product
     *         empty array if no membership discounts found for this product
     */
    public function get_user_membership_discounts( $the_product, $the_user = null ) {

        // initialize the $member_discount array
        $member_discounts = array();

        // Get the product.
        if ( is_numeric( $the_product ) ) {
            $the_product = wc_get_product( (int) $the_product );
        } elseif ( null === $the_product ) {
            global $product;

            if ( $product instanceof WC_Product ) {
                $the_product = $product;
            }
        }

        // bail out if no product
        if ( ! $the_product instanceof WC_Product ) {
            return $member_discounts;
        }


        // get the user id
        if ( null === $the_user ) {
            $member_id = get_current_user_id();
        } elseif ( is_numeric( $the_user ) ) {
            $member_id = (int) $the_user;
        } elseif ( isset( $the_user->ID ) ) {
            $member_id = (int) $the_user->ID;
        } else {
            return $member_discounts;
        }

        // bail out if user is not logged in
        if ( 0 === $member_id ) {
            return $member_discounts;
        }

        $member_discount_context = $this->load_member_discount_context( $member_id );
        $member_plan_discounts   = isset( $member_discount_context['discounts'] ) ? $member_discount_context['discounts'] : array();
        $member_subscriptions    = isset( $member_discount_context['subscriptions'] ) ? $member_discount_context['subscriptions'] : array();

        // if is variation, use the id of the parent product
        if ($the_product->is_type( 'variation' )) {
            $product_id = $the_product->get_parent_id();
        } else {
            $product_id = $the_product->get_id();
        }

        // get discounts behaviour for this product
        $discounts_behaviour = get_post_meta((int)$product_id, 'pms-woo-product-membership-discounts-behaviour', true);
        //for products added in Woocommerce before activating PMS and running the save_post hook added in PMS
        if ( empty($discounts_behaviour) )
            $discounts_behaviour = 'default';

        // check if there are any global subscription discounts that apply to this product
        if ( !empty($member_plan_discounts) && ($discounts_behaviour == 'default') ) {

            foreach ($member_plan_discounts as $discount) {

                // don't save inactive discounts
                if ( $discount['status'] == 'inactive' || !isset( $discount['discount-for'] ) )
                    continue;

                if ( ($discount['discount-for'] == 'products') &&
                    ( !isset($discount['name']) || ( isset($discount['name']) && in_array($product_id, $discount['name'])) ) ){

                    $member_discounts['subscription_plan'][$discount['type']][] = $discount['amount'];
                }

                if ( ($discount['discount-for'] == 'product-categories') &&
                    ( ( !isset($discount['name']) && has_term('', 'product_cat', (int)$product_id) ) || ( isset($discount['name']) && has_term( $discount['name'], 'product_cat', (int)$product_id ) ) ) ) {

                    $member_discounts['subscription_plan'][$discount['type']][] = $discount['amount'];
                }

            }
        }

        // check if there are any discounts set per product that apply to logged in member
        $product_discounts = get_post_meta( (int)$product_id, 'pms-woo-product-membership-discounts', true );
        //for products added in Woocommerce before activating PMS and running the save_post hook added in PMS
        if( $product_discounts === '' )
            $product_discounts = array();

        if ( !empty($product_discounts) ){

            foreach ( $product_discounts as $product_discount ){

                if ( $product_discount['status'] == 'inactive' )
                    continue;

                if ( !empty($member_subscriptions) && in_array( $product_discount['subscription-plan'], $member_subscriptions ) )
                    $member_discounts['product'][$product_discount['type']][] = $product_discount['amount'];
            }

        }

        return $member_discounts;
    }


    /**
     * Function that checks if there are any active membership discounts that apply to this product
     *
     * @param null|int|\WC_Product $product Product ID or object.
     * @return bool
     */
    public function product_has_member_discounts( $the_product = null ){

        // Get the product.
        if ( is_numeric( $the_product ) ) {
            $the_product = wc_get_product( (int) $the_product );
        } elseif ( null === $the_product ) {
            global $product;

            if ( $product instanceof WC_Product ) {
                $the_product = $product;
            }
        }

        // bail out if no product
        if ( ! $the_product instanceof WC_Product ) {
            return false;
        }

        // if is variation, use the id of the parent product
        if ($the_product->is_type( 'variation' )) {
            $product_id = $the_product->get_parent_id();
        } else {
            $product_id = $the_product->get_id();
        }


        // first check if there are any discounts set per product
        $product_discounts = get_post_meta( (int)$product_id, 'pms-woo-product-membership-discounts', true );
        //for products added in Woocommerce before activating PMS and running the save_post hook added in PMS
        if( $product_discounts === '' )
            $product_discounts = array();

        if ( !empty($product_discounts) ) {

            foreach ($product_discounts as $product_discount) {

                if ($product_discount['status'] == 'active' && !empty($product_discount['subscription-plan'])) {

                    // product has membership discounts
                    return true;
                }
            }
        }


        // see if product is not excluded from global membership discounts (per subscription plan)
        $discounts_behaviour = get_post_meta( (int)$product_id, 'pms-woo-product-membership-discounts-behaviour', true);
        //for products added in Woocommerce before activating PMS and running the save_post hook added in PMS
        if ( empty($discounts_behaviour) )
            $discounts_behaviour = 'default';

        if ( $discounts_behaviour == 'ignore')
            return false;


        // check if there are global membership discounts set per subscription plan that apply to this product
        $subscription_plans = pms_get_subscription_plans();

        if ( !empty($subscription_plans) ) {

            foreach ( $subscription_plans as $subscription_plan ) {

                $subscription_plan_discounts = get_post_meta( (int)$subscription_plan->id, 'pms-woo-subscription-plan-product-discounts', true );

                if ( !empty($subscription_plan_discounts) ){

                    foreach ( $subscription_plan_discounts as $discount ) {

                        // don't save inactive discounts
                        if ( $discount['status'] == 'active' && isset( $discount['discount-for'] ) ) {

                            if (($discount['discount-for'] == 'products') &&
                                (!isset($discount['name']) || (isset($discount['name']) && in_array($product_id, $discount['name'])))) {
                                // product has membership discounts
                                return true;
                            }

                            if (($discount['discount-for'] == 'product-categories') &&
                                ((!isset($discount['name']) && has_term('', 'product_cat', $product_id)) || (isset($discount['name']) && has_term($discount['name'], 'product_cat', $product_id)))) {
                                // product has membership discounts
                                return true;
                            }
                        }
                    }

                }
            }
        }

        return false;
    }


    /**
     * Function that displays the 'Product Discounted - Membership Required' message to non-members if a product has a membership discount.
     * Displays below add to cart buttons.
     *
     **/
    function product_discounted_membership_required_message(){

        global $product;

        $options = get_option( 'pms_woocommerce_settings' );

        if ( !empty($options) && !empty($options['product_discounted_message']) ) {

            // check if product has any membership discounts and user doesn't have any or is a non member
            if ( !$this->is_product_excluded_from_member_discounts($product) && $this->product_has_member_discounts($product) && !$this->get_user_membership_discounts($product) ) {

                /**
                 * Filter for Product Discounted - Membership Required message
                 */
                $message = apply_filters( 'pms_woo_product_discounted_membership_required_message', '<div class="woocommerce"> <div class="woocommerce-info pms-woo-product-discounted-membership-required">'. $options['product_discounted_message'] . '</div> </div>', $options );

                echo wp_kses_post($message);
            }

        }
    }



    /**
     * Refresh cart fragments upon member login.
     *
     * This is useful if a non-logged in member added items to cart,
     * which should have otherwise membership discounts applied.
     *
     *
     * @see \WC_Cart::reset()
     *
     * @param string $user_login User login name.
     * @param \WP_User $user User that just logged in.
     */
    public function refresh_cart_upon_member_login( $user_login, $user ) {

        // small "hack" to trigger a refresh in cart contents
        // that will set any membership discounts to products that apply
        if ( $user_login && pms_is_member( $user->ID ) ) {

            $this->reset_cart_session_data();
        }
    }


    /**
     * Reset cart session data.
     *
     * @see \WC_Cart::reset() private method
     *
     */
    private function reset_cart_session_data() {

        $wc = WC();

        // Some very zealous sanity checks here:
        if ( $wc && isset( $wc->cart->cart_session_data ) ) {

            $session_data = $wc->cart->cart_session_data;

            if ( ! empty( $session_data ) ) {

                foreach ( $session_data as $key => $default ) {

                    if ( isset( $wc->session->$key ) ) {
                        unset( $wc->session->$key );
                    }
                }
            }

            // WooCommerce core filter.
            do_action( 'woocommerce_cart_reset', $wc->cart, true );
        }
    }


} // end class PMS_WOO_Subscription_Discounts

$pms_woo_subscription_discounts = new PMS_WOO_Subscription_Discounts();
