<?php

declare(strict_types=1);

namespace Brick\Money;

use Brick\Money\Exception\CurrencyConversionException;

/**
 * Compares monies in different currencies.
 *
 * The converted amounts are never rounded before comparison, so this comparator is more precise
 * than converting a Money to another Currency, then using the resulting Money's built-in comparison methods.
 *
 * Note that the comparison is always performed by converting the first Money into the currency of the second Money.
 * This order is important because some exchange rate providers may only have one-way rates,
 * or may use a different rate in each direction.
 */
final class MoneyComparator
{
    /**
     * The exchange rate provider.
     */
    private readonly ExchangeRateProvider $exchangeRateProvider;

    /**
     * Class constructor.
     *
     * @param ExchangeRateProvider $provider The exchange rate provider.
     */
    public function __construct(ExchangeRateProvider $provider)
    {
        $this->exchangeRateProvider = $provider;
    }

    /**
     * Compares the given monies.
     *
     * The amount is not rounded before comparison, so the results are more relevant than when using
     * `convert($a, $b->getCurrency())->compareTo($b)`.
     *
     * Note that the comparison is performed by converting A into B's currency.
     * This order is important if the exchange rate provider uses different exchange rates
     * when converting back and forth two currencies.
     *
     * @param Money $a
     * @param Money $b
     *
     * @return int -1, 0 or 1.
     *
     * @throws CurrencyConversionException If the exchange rate is not available.
     */
    public function compare(Money $a, Money $b) : int
    {
        $aCurrencyCode = $a->getCurrency()->getCurrencyCode();
        $bCurrencyCode = $b->getCurrency()->getCurrencyCode();

        if ($aCurrencyCode === $bCurrencyCode) {
            return $a->compareTo($b);
        }

        $aAmount = $a->getAmount();
        $bAmount = $b->getAmount();

        $exchangeRate = $this->exchangeRateProvider->getExchangeRate($aCurrencyCode, $bCurrencyCode);

        $aAmount = $aAmount->toBigRational()->multipliedBy($exchangeRate);

        return $aAmount->compareTo($bAmount);
    }

    /**
     * @param Money $a
     * @param Money $b
     *
     * @return bool
     *
     * @throws CurrencyConversionException If the exchange rate is not available.
     */
    public function isEqual(Money $a, Money $b) : bool
    {
        return $this->compare($a, $b) === 0;
    }

    /**
     * @param Money $a
     * @param Money $b
     *
     * @return bool
     *
     * @throws CurrencyConversionException If the exchange rate is not available.
     */
    public function isLess(Money $a, Money $b) : bool
    {
        return $this->compare($a, $b) < 0;
    }

    /**
     * @param Money $a
     * @param Money $b
     *
     * @return bool
     *
     * @throws CurrencyConversionException If the exchange rate is not available.
     */
    public function isLessOrEqual(Money $a, Money $b) : bool
    {
        return $this->compare($a, $b) <= 0;
    }

    /**
     * @param Money $a
     * @param Money $b
     *
     * @return bool
     *
     * @throws CurrencyConversionException If the exchange rate is not available.
     */
    public function isGreater(Money $a, Money $b) : bool
    {
        return $this->compare($a, $b) > 0;
    }

    /**
     * @param Money $a
     * @param Money $b
     *
     * @return bool
     *
     * @throws CurrencyConversionException If the exchange rate is not available.
     */
    public function isGreaterOrEqual(Money $a, Money $b) : bool
    {
        return $this->compare($a, $b) >= 0;
    }

    /**
     * Returns the smallest of the given monies.
     *
     * The monies are compared from left to right. This distinction can be important if the exchange rate provider does
     * not have bidirectional exchange rates, or applies different rates depending on the direction of the conversion.
     *
     * For example, when comparing [A, B, C], this method will first compare A against B, then min(A,B) against C.
     *
     * If several monies are equal to the minimum value, the first one is returned.
     *
     * @param Money    $money  The first money.
     * @param Money ...$monies The subsequent monies.
     *
     * @return Money The smallest Money.
     *
     * @throws CurrencyConversionException If an exchange rate is not available.
     */
    public function min(Money $money, Money ...$monies) : Money
    {
        $min = $money;

        foreach ($monies as $money) {
            if ($this->isGreater($min, $money)) {
                $min = $money;
            }
        }

        return $min;
    }

    /**
     * Returns the largest of the given monies.
     *
     * The monies are compared from left to right. This distinction can be important if the exchange rate provider does
     * not have bidirectional exchange rates, or applies different rates depending on the direction of the conversion.
     *
     * For example, when comparing [A, B, C], this method will first compare A against B, then max(A,B) against C.
     *
     * If several monies are equal to the maximum value, the first one is returned.
     *
     * @param Money    $money  The first money.
     * @param Money ...$monies The subsequent monies.
     *
     * @return Money The largest Money.
     *
     * @throws CurrencyConversionException If an exchange rate is not available.
     */
    public function max(Money $money, Money ...$monies) : Money
    {
        $max = $money;

        foreach ($monies as $money) {
            if ($this->isLess($max, $money)) {
                $max = $money;
            }
        }

        return $max;
    }
}
