<?php
/**
 * Handler for coupons related stuff in cart, checkout etc.
 *
 * @package Masteriyo\Addons\Coupons
 *
 * @since 2.5.12
 */

namespace Masteriyo\Addons\Coupons;

use Masteriyo\Addons\Coupons\Models\OrderItemCoupon;

defined( 'ABSPATH' ) || exit;

class Coupons {
	/**
	 * Contains an array of coupon codes applied to the cart.
	 *
	 * @since 2.5.12
	 *
	 * @var array
	 */
	protected $applied_coupons = array();

	/**
	 * Contains array of discount totals by coupon.
	 *
	 * @since 2.5.12
	 *
	 * @var array
	 */
	protected $discount_totals_by_coupon = array();

	/**
	 * Contains array of discount totals by item.
	 *
	 * @since 2.5.12
	 *
	 * @var array
	 */
	protected $discount_totals_by_item = array();

	/**
	 * Initialize the application.
	 *
	 * @since 2.5.12
	 */
	public function init() {
		$this->init_hooks();
	}

	/**
	 * Initialize hooks.
	 *
	 * @since 2.5.12
	 */
	public function init_hooks() {
		add_action( 'masteriyo_checkout_set_order_data_from_cart', array( $this, 'checkout_set_order_data_from_cart' ), 10, 3 );
		add_filter( 'masteriyo_get_items_key', array( $this, 'get_items_key' ), 10, 2 );
		add_action( 'masteriyo_load_cart_from_session', array( $this, 'load_cart_from_session' ) );
		add_action( 'masteriyo_before_cart_emptied', array( $this, 'before_cart_emptied' ) );
		add_action( 'masteriyo_before_destroy_cart_session', array( $this, 'before_destroy_cart_session' ), 10, 1 );
		add_action( 'masteriyo_calculate_item_totals', array( $this, 'calculate_discounts' ), 10, 1 );
		add_action( 'masteriyo_before_calculate_totals', array( $this, 'apply_automatic_coupons_to_cart_item' ), 10, 1 );
		add_action( 'masteriyo_totals_get_discounted_price_in_cents', array( $this, 'get_discounted_price_in_cents' ), 10, 3 );

		// Fix for coupon not being removed.
		add_action( 'wp_loaded', array( $this, 'load_cart_from_session' ) );
	}

	/**
	 * Set coupon data from cart while processing checkout.
	 *
	 * @since 2.5.12
	 *
	 * @param \Masteriyo\Models\Order\Order $order Order object.
	 * @param \Masteriyo\Checkout $checkout Checkout object.
	 * @param \Masteriyo\Cart\Cart $cart Cart object.
	 */
	public function checkout_set_order_data_from_cart( $order, $checkout, $cart ) {
		$order->set_discount_total( $cart->get_discount_total() );

		if ( floatval( $order->get_exchange_rate() ) && floatval( $order->get_discount_total() ) > 0 ) {
			$conversion_discount_total = ( 1 / floatval( $order->get_exchange_rate() ) ) * floatval( $order->get_discount_total() );
			$order->set_conversion_discount_total( masteriyo_format_decimal( $conversion_discount_total ) );
		}

		$this->create_order_coupon_lines( $order, $checkout, $cart );
	}

	/**
	 * Add coupon lines to the order.
	 *
	 * @since 2.5.12
	 *
	 * @param \Masteriyo\Models\Order\Order $order Order object.
	 * @param \Masteriyo\Checkout $checkout Checkout object.
	 * @param \Masteriyo\Cart\Cart $cart Cart object.
	 */
	public function create_order_coupon_lines( $order, $checkout, $cart ) {
		foreach ( $this->get_applied_coupons() as $code ) {
			$coupon = masteriyo_get_coupon_by_code( $code );

			if ( is_null( $coupon ) ) {
				continue;
			}

			$item = masteriyo_create_order_item_coupon_object();
			$item->set_props(
				array(
					'code'     => $code,
					'discount' => $this->get_coupon_discount_amount( $code ),
				)
			);
			$item->add_meta_data( 'coupon_data', $coupon->get_data() );

			/**
			 * Action hook to adjust item before save.
			 *
			 * @since 2.5.12
			 *
			 * @param \Masteriyo\Addons\Coupons\Models\OrderItemCoupon $item Order item coupon object.
			 * @param string $code Coupon code.
			 * @param \Masteriyo\Addons\Coupons\Models\Coupon $coupon Coupon object.
			 * @param \Masteriyo\Models\Order\Order $order Order object.
			 */
			do_action( 'masteriyo_checkout_create_order_coupon_item', $item, $code, $coupon, $order );

			// Add item to order and save.
			$order->add_item( $item );

			// Increase coupon usage count.
			masteriyo_create_coupon_store()->increase_usage_count( $coupon, $order->get_customer_id() );
		}
	}

	/**
	 * Get key for where a certain item type is stored in _items.
	 *
	 * @since 2.5.12
	 *
	 * @param string $items_key
	 * @param \Masteriyo\Models\Order\OrderItem $item object Order item (product, shipping, fee, coupon, tax).
	 *
	 * @return string
	 */
	public function get_items_key( $items_key, $item ) {
		if ( is_a( $item, OrderItemCoupon::class ) ) {
			return 'coupon_lines';
		}
		return $items_key;
	}

	/**
	 * Load coupon related stuff to cart from session.
	 *
	 * @since 2.5.12
	 */
	public function load_cart_from_session() {
		$this->set_applied_coupons( masteriyo_create_session_object()->get( 'applied_coupons', array() ) );
	}

	/**
	 * Empty coupon related stuff when cart is emptied.
	 *
	 * @since 2.5.12
	 */
	public function before_cart_emptied() {
		$this->set_applied_coupons( array() );
	}

	/**
	 * Empty coupon related stuff when cart session is being destroyed.
	 *
	 * @since 2.5.12
	 *
	 * @param \Masteriyo\Cart\Cart $cart
	 */
	public function before_destroy_cart_session( $cart ) {
		masteriyo_create_session_object()->put( 'applied_coupons', null );
	}

	/**
	 * Applies a coupon code passed to the method.
	 *
	 * @since 2.5.12
	 *
	 * @param string $coupon_code The code to apply.
	 *
	 * @return bool True if the coupon is applied, false if it does not exist or cannot be applied.
	 */
	public function apply_coupon( $coupon_code ) {
		$coupon_code = masteriyo_format_coupon_code( $coupon_code );
		$the_coupon  = masteriyo_get_coupon_by_code( $coupon_code );

		if ( is_null( $the_coupon ) ) {
			return false;
		}

		if ( $this->is_coupon_applied( $coupon_code ) ) {
			return true;
		}

		$applied_coupons = $this->get_applied_coupons();
		$is_stackable    = 'yes' === $the_coupon->get_stackable();

		/**
		 * Filter whether multiple coupon should enable.
		 *
		 * @since 2.5.12
		 *
		 * @param boolean $enable Multiple coupon enable ( Default: false)
		 * @param string $coupon_code Coupon code.
		 */
		if ( false === apply_filters( 'masteriyo_enable_multiple_coupon', $is_stackable, $coupon_code ) ) {
			$applied_coupons = array();
		}
		$applied_coupons = array_unique( array_merge( $applied_coupons, array( $coupon_code ) ) );

		$this->set_applied_coupons( $applied_coupons );

		masteriyo_create_session_object()->put( 'applied_coupons', $this->get_applied_coupons() );
		masteriyo_create_session_object()->save_data();
		masteriyo_create_cart_object()->calculate_totals();

		/**
		 * Fires after a coupon is applied.
		 *
		 * @since 2.5.12
		 *
		 * @param string $coupon_code
		 */
		do_action( 'masteriyo_coupon_applied', $coupon_code );

		return true;
	}

	/**
	 * Remove a single coupon by code.
	 *
	 * @since 2.5.12
	 *
	 * @param string $coupon_code Code of the coupon to remove.
	 *
	 * @return bool
	 */
	public function remove_coupon( $coupon_code ) {
		$coupon_code     = masteriyo_format_coupon_code( $coupon_code );
		$applied_coupons = $this->get_applied_coupons();
		$position        = array_search( $coupon_code, array_map( 'masteriyo_format_coupon_code', $applied_coupons ), true );

		if ( false === $position ) {
			return false;
		}

		unset( $applied_coupons[ $position ] );
		$this->set_applied_coupons( $applied_coupons );

		masteriyo_create_session_object()->put( 'refresh_totals', true );
		masteriyo_create_session_object()->put( 'applied_coupons', $applied_coupons );
		masteriyo_create_session_object()->save_data();
		masteriyo_create_cart_object()->calculate_totals();

		/**
		 * Fires after an applied coupon is removed.
		 *
		 * @since 2.5.12
		 *
		 * @param string $coupon_code
		 */
		do_action( 'masteriyo_applied_coupon_removed', $coupon_code );

		return true;
	}

	/**
	 * Check if a coupon has been applied.
	 *
	 * @since 2.5.12
	 *
	 * @param string $coupon_code
	 *
	 * @return boolean
	 */
	public function is_coupon_applied( $coupon_code ) {
		$coupon_code     = masteriyo_format_coupon_code( $coupon_code );
		$applied_coupons = $this->get_applied_coupons();
		$position        = array_search( $coupon_code, array_map( 'masteriyo_format_coupon_code', $applied_coupons ), true );

		if ( false === $position ) {
			return false;
		}
		return true;
	}

	/**
	 * Set applied coupons.
	 *
	 * @since 2.5.12
	 *
	 * @param string[] $coupons
	 */
	public function set_applied_coupons( $coupons ) {
		$this->applied_coupons = (array) $coupons;
	}

	/**
	 * Get applied coupons.
	 *
	 * @since 2.5.12
	 *
	 * @return string[]
	 */
	public function get_applied_coupons() {
		return $this->applied_coupons;
	}

	/**
	 * Sets the discount totals by coupon.
	 *
	 * @since 2.5.12
	 *
	 * @param array $value Value to set.
	 */
	public function set_discount_totals_by_coupon( $value = array() ) {
		$this->discount_totals_by_coupon = (array) $value;
	}

	/**
	 * Get the discount totals by coupon.
	 *
	 * @since 2.5.12
	 *
	 * @return array
	 */
	public function get_discount_totals_by_coupon() {
		return (array) $this->discount_totals_by_coupon;
	}

	/**
	 * Sets the discount totals by item.
	 *
	 * @since 2.5.12
	 *
	 * @param array $value Value to set.
	 */
	public function set_discount_totals_by_item( $value = array() ) {
		$this->discount_totals_by_item = (array) $value;
	}

	/**
	 * Get the discount totals by item.
	 *
	 * @since 2.5.12
	 *
	 * @return array
	 */
	public function get_discount_totals_by_item() {
		return (array) $this->discount_totals_by_item;
	}

	/**
	 * Get array of applied coupon objects and codes.
	 *
	 * @since 2.5.12
	 *
	 * @return \Masteriyo\Addons\Coupons\Models\Coupon[] of applied coupons
	 */
	public function get_coupons() {
		$coupons = array();

		foreach ( $this->applied_coupons as $code ) {
			$coupon = masteriyo_get_coupon_by_code( $code );

			if ( is_null( $coupon ) ) {
				continue;
			}

			$coupons[ $code ] = $coupon;
		}

		return $coupons;
	}

	/**
	 * Get the discount amount for a used coupon.
	 *
	 * @since 2.5.12
	 *
	 * @param string $code coupon code.
	 *
	 * @return float discount amount
	 */
	public function get_coupon_discount_amount( $code ) {
		$totals          = $this->get_discount_totals_by_coupon();
		$discount_amount = isset( $totals[ $code ] ) ? $totals[ $code ] : 0;

		return masteriyo_round_discount( $discount_amount, masteriyo_get_price_decimals() );
	}

	/**
	 * Calculate COUPON based discounts which change item prices.
	 *
	 * @since 2.5.12
	 *
	 * @param \Masteriyo\Cart\Totals $totals
	 */
	public function calculate_discounts( $totals ) {
		$discounts = new CouponDiscounts( $totals->cart );

		$discounts->set_items( $totals->items );

		foreach ( masteriyo_coupons()->get_coupons() as $coupon ) {
			$discounts->apply_coupon( $coupon );
		}

		$discounts_by_coupon = $discounts->get_discounts_by_coupon( true );
		$discounts_by_coupon = masteriyo_remove_number_precision_deep( $discounts_by_coupon );
		$discounts_by_item   = (array) $discounts->get_discounts_by_item( true );

		masteriyo_coupons()->set_discount_totals_by_coupon( $discounts_by_coupon );
		masteriyo_coupons()->set_discount_totals_by_item( $discounts_by_item );

		$totals->set_total( 'discounts_total', array_sum( $discounts_by_item ) );
		$totals->cart->set_discount_total( $totals->get_total( 'discounts_total' ) );
	}

	/**
	 * Applies automatic coupons to a cart item.
	 *
	 * Iterates through all automatic coupons and applies them to the items in the cart.
	 * If the cart is not an instance of \Masteriyo\Cart\Cart, or if there are no
	 * items or automatic coupons, the function exits early.
	 *
	 * @since 2.18.3
	 *
	 * @param \Masteriyo\Cart\Cart $cart The cart object.
	 * @return void
	 */
	public function apply_automatic_coupons_to_cart_item( $cart ) {
		$automatic_coupons = array_filter( array_map( 'masteriyo_get_coupon', masteriyo_get_automatic_coupon_ids() ) );

		if ( empty( $automatic_coupons ) ) {
			return;
		}

		foreach ( $automatic_coupons as $coupon ) {
			if ( ! is_wp_error( $coupon->is_valid( $cart ) ) ) {
				$this->apply_coupon( $coupon->get_code() );
			}
		}
	}

	/**
	 * Get discounted price of an item with precision (in cents).
	 *
	 * @since 2.5.12
	 *
	 * @param integer $price
	 * @param string $item_key Item to get the price of.
	 * @param \Masteriyo\Cart\Totals $totals
	 *
	 * @return integer
	 */
	public function get_discounted_price_in_cents( $price, $item_key, $totals ) {
		$discounts = masteriyo_coupons()->get_discount_totals_by_item();
		$price     = isset( $discounts[ $item_key ] ) ? $price - $discounts[ $item_key ] : $price;
		return $price;
	}
}
