<?php
/**
 * For the full copyright and license information, please view the
 * docs/licenses/LICENSE.txt file that was distributed with this source code.
 */

namespace PrestaShop\PrestaShop\Core\Cart;

use CartCore;
use Currency;
use Exception;
use PrestaShop\PrestaShop\Core\FeatureFlag\FeatureFlagStateCheckerInterface;
use PrestaShop\PrestaShop\Core\Localization\CLDR\ComputingPrecision;
use Tools;

/**
 * provides methods to process cart calculation.
 */
class Calculator
{
    /**
     * @var CartCore
     */
    protected $cart;

    /**
     * @var int
     */
    protected $id_carrier;

    /**
     * @var int|null
     */
    protected $orderId;

    /**
     * @var CartRowCollection collection of cart content row (product+qty)
     */
    protected $cartRows;

    /**
     * @var CartRuleCollection
     */
    protected $cartRules;

    /**
     * @var Fees
     */
    protected $fees;

    /**
     * @var CartRuleCalculator
     */
    protected $cartRuleCalculator;

    /**
     * indicates if cart was already processed.
     *
     * @var bool
     */
    protected $isProcessed = false;

    /**
     * @var int|null
     */
    protected $computePrecision;

    /**
     * @param CartCore $cart
     * @param int $carrierId
     * @param int|null $computePrecision
     * @param int|null $orderId
     */
    public function __construct(CartCore $cart, $carrierId, ?int $computePrecision = null, ?int $orderId = null, ?FeatureFlagStateCheckerInterface $featureFlagManager = null)
    {
        $this->setCart($cart);
        $this->setCarrierId($carrierId);
        $this->orderId = $orderId;
        $this->cartRows = new CartRowCollection();
        $this->fees = new Fees($this->orderId);
        $this->cartRules = new CartRuleCollection();
        $this->cartRuleCalculator = new CartRuleCalculator($featureFlagManager);

        if (null === $computePrecision) {
            $currency = new Currency((int) $cart->id_currency);
            $computePrecision = (new ComputingPrecision())->getPrecision($currency->precision);
        }
        $this->computePrecision = $computePrecision;
    }

    /**
     * insert a new cart row in the calculator.
     *
     * @param CartRow $cartRow cart item row (product+qty informations)
     *
     * @return $this
     */
    public function addCartRow(CartRow $cartRow)
    {
        // reset state
        $this->isProcessed = false;

        $this->cartRows->addCartRow($cartRow);

        return $this;
    }

    /**
     * insert a new cart rule in the calculator.
     *
     * @param CartRuleData $cartRule
     *
     * @return $this
     */
    public function addCartRule(CartRuleData $cartRule)
    {
        // reset state
        $this->isProcessed = false;

        $this->cartRules->addCartRule($cartRule);

        return $this;
    }

    /**
     * run the whole calculation process: calculate rows, discounts, fees.
     *
     * @param int $computePrecision Not used since 1.7.7.0, kept for backward compatibility
     *
     * @return $this
     */
    public function processCalculation($computePrecision = null)
    {
        // calculate product rows
        $this->calculateRows();
        // calculate fees
        $this->calculateFees();
        // calculate discounts
        $this->calculateCartRules();
        // store state
        $this->isProcessed = true;

        return $this;
    }

    /**
     * @param bool $ignoreProcessedFlag force getting total even if calculation was not made internaly
     *
     * @return AmountImmutable
     *
     * @throws Exception
     */
    public function getTotal($ignoreProcessedFlag = false)
    {
        if (!$this->isProcessed && !$ignoreProcessedFlag) {
            throw new Exception('Cart must be processed before getting its total');
        }

        $amount = $this->rounded($this->getRowTotalWithoutDiscount(), $this->computePrecision);
        $amount = $amount->sub($this->rounded($this->getDiscountTotal(), $this->computePrecision));
        $shippingFees = $this->fees->getInitialShippingFees();
        if (null !== $shippingFees) {
            $amount = $amount->add($this->rounded($shippingFees, $this->computePrecision));
        }
        $wrappingFees = $this->fees->getFinalWrappingFees();
        if (null !== $wrappingFees) {
            $amount = $amount->add($this->rounded($wrappingFees, $this->computePrecision));
        }

        return $amount;
    }

    /**
     * @return AmountImmutable
     *
     * @throws Exception
     */
    public function getRowTotal()
    {
        $amount = new AmountImmutable();
        foreach ($this->cartRows as $cartRow) {
            $amount = $amount->add($cartRow->getFinalTotalPrice());
        }

        return $amount;
    }

    /**
     * @return AmountImmutable
     *
     * @throws Exception
     */
    public function getRowTotalWithoutDiscount()
    {
        $amount = new AmountImmutable();
        foreach ($this->cartRows as $cartRow) {
            $amount = $amount->add($cartRow->getInitialTotalPrice());
        }

        return $amount;
    }

    /**
     * @return AmountImmutable
     *
     * @throws Exception
     */
    public function getDiscountTotal()
    {
        $amount = new AmountImmutable();
        $isFreeShippingAppliedToAmount = false;
        foreach ($this->cartRules as $cartRule) {
            if ((bool) $cartRule->getRuleData()['free_shipping']) {
                if ($isFreeShippingAppliedToAmount) {
                    $initialShippingFees = $this->getFees()->getInitialShippingFees();
                    $amount = $amount->sub($initialShippingFees);
                }
                $isFreeShippingAppliedToAmount = true;
            }
            $amount = $amount->add($cartRule->getDiscountApplied());
        }

        $allowedMaxDiscount = $this->getRowTotalWithoutDiscount();

        if (null !== $this->getFees()->getFinalShippingFees()) {
            $shippingDiscount = (new AmountImmutable())
                ->add($this->getFees()->getInitialShippingFees())
                ->sub($this->getFees()->getFinalShippingFees())
            ;
            $allowedMaxDiscount = $allowedMaxDiscount->add($shippingDiscount);
        }
        // discount cannot be above total cart price
        if ($amount > $allowedMaxDiscount) {
            $amount = $allowedMaxDiscount;
        }

        return $amount;
    }

    /**
     * @param CartCore $cart
     *
     * @return Calculator
     */
    protected function setCart($cart)
    {
        // reset state
        $this->isProcessed = false;

        $this->cart = $cart;

        return $this;
    }

    /**
     * @param mixed $id_carrier
     *
     * @return Calculator
     */
    protected function setCarrierId($id_carrier)
    {
        // reset state
        $this->isProcessed = false;

        $this->id_carrier = $id_carrier;

        return $this;
    }

    /**
     * @return Fees
     */
    public function getFees()
    {
        return $this->fees;
    }

    /**
     * @return CartCore
     */
    public function getCart()
    {
        return $this->cart;
    }

    /**
     * Calculate row total.
     *
     * @param CartRow $cartRow
     */
    protected function calculateRowTotal(CartRow $cartRow)
    {
        $cartRow->processCalculation($this->cart);
    }

    /**
     * calculate only product rows.
     */
    public function calculateRows()
    {
        foreach ($this->cartRows as $cartRow) {
            $this->calculateRowTotal($cartRow);
        }
    }

    /**
     * calculate only cart rules (rows and fees have to be calculated first).
     */
    public function calculateCartRules()
    {
        $this->cartRuleCalculator->setCartRules($this->cartRules)
            ->setCartRows($this->cartRows)
            ->setCalculator($this)
            ->applyCartRules();
    }

    /**
     * calculate only cart rules (rows and fees have to be calculated first), but don't process free-shipping discount
     * (avoid loop on shipping calculation)
     */
    public function calculateCartRulesWithoutFreeShipping()
    {
        $this->cartRuleCalculator->setCartRules($this->cartRules)
            ->setCartRows($this->cartRows)
            ->setCalculator($this)
            ->applyCartRulesWithoutFreeShipping();
    }

    /**
     * calculate wrapping and shipping fees (rows have to be calculated first).
     *
     * @param int|null $computePrecision Not used since 1.7.7.0, kept for backward compatibility
     */
    public function calculateFees($computePrecision = null)
    {
        $this->fees->processCalculation($this->cart, $this->cartRows, $this->computePrecision, $this->id_carrier);
    }

    /**
     * @return CartRuleCollection
     */
    public function getCartRulesData()
    {
        return $this->cartRuleCalculator->getCartRulesData();
    }

    /**
     * @param AmountImmutable $amount
     * @param int $computePrecision
     *
     * @return AmountImmutable
     */
    private function rounded(AmountImmutable $amount, int $computePrecision)
    {
        return new AmountImmutable(
            Tools::ps_round($amount->getTaxIncluded(), $computePrecision),
            Tools::ps_round($amount->getTaxExcluded(), $computePrecision)
        );
    }
}
