<?php

declare (strict_types=1);
namespace Staatic\Vendor\Brick\Math;

use InvalidArgumentException;
use LogicException;
use Staatic\Vendor\Brick\Math\Exception\DivisionByZeroException;
use Staatic\Vendor\Brick\Math\Exception\MathException;
use Staatic\Vendor\Brick\Math\Exception\NegativeNumberException;
use Staatic\Vendor\Brick\Math\Internal\Calculator;
final class BigDecimal extends BigNumber
{
    /**
     * @readonly
     * @var string
     */
    private $value;
    /**
     * @readonly
     * @var int
     */
    private $scale;
    protected function __construct(string $value, int $scale = 0)
    {
        $this->value = $value;
        $this->scale = $scale;
    }
    /**
     * @param BigNumber $number
     * @return static
     */
    protected static function from($number)
    {
        return $number->toBigDecimal();
    }
    /**
     * @param BigNumber|int|float|string $value
     * @param int $scale
     */
    public static function ofUnscaledValue($value, $scale = 0): BigDecimal
    {
        if ($scale < 0) {
            throw new InvalidArgumentException('The scale cannot be negative.');
        }
        return new BigDecimal((string) BigInteger::of($value), $scale);
    }
    public static function zero(): BigDecimal
    {
        static $zero;
        if ($zero === null) {
            $zero = new BigDecimal('0');
        }
        return $zero;
    }
    public static function one(): BigDecimal
    {
        static $one;
        if ($one === null) {
            $one = new BigDecimal('1');
        }
        return $one;
    }
    public static function ten(): BigDecimal
    {
        static $ten;
        if ($ten === null) {
            $ten = new BigDecimal('10');
        }
        return $ten;
    }
    /**
     * @param BigNumber|int|float|string $that
     */
    public function plus($that): BigDecimal
    {
        $that = BigDecimal::of($that);
        if ($that->value === '0' && $that->scale <= $this->scale) {
            return $this;
        }
        if ($this->value === '0' && $this->scale <= $that->scale) {
            return $that;
        }
        [$a, $b] = $this->scaleValues($this, $that);
        $value = Calculator::get()->add($a, $b);
        $scale = $this->scale > $that->scale ? $this->scale : $that->scale;
        return new BigDecimal($value, $scale);
    }
    /**
     * @param BigNumber|int|float|string $that
     */
    public function minus($that): BigDecimal
    {
        $that = BigDecimal::of($that);
        if ($that->value === '0' && $that->scale <= $this->scale) {
            return $this;
        }
        [$a, $b] = $this->scaleValues($this, $that);
        $value = Calculator::get()->sub($a, $b);
        $scale = $this->scale > $that->scale ? $this->scale : $that->scale;
        return new BigDecimal($value, $scale);
    }
    /**
     * @param BigNumber|int|float|string $that
     */
    public function multipliedBy($that): BigDecimal
    {
        $that = BigDecimal::of($that);
        if ($that->value === '1' && $that->scale === 0) {
            return $this;
        }
        if ($this->value === '1' && $this->scale === 0) {
            return $that;
        }
        $value = Calculator::get()->mul($this->value, $that->value);
        $scale = $this->scale + $that->scale;
        return new BigDecimal($value, $scale);
    }
    /**
     * @param BigNumber|int|float|string $that
     * @param int|null $scale
     * @param RoundingMode $roundingMode
     */
    public function dividedBy($that, $scale = null, $roundingMode = RoundingMode::UNNECESSARY): BigDecimal
    {
        $that = BigDecimal::of($that);
        if ($that->isZero()) {
            throw DivisionByZeroException::divisionByZero();
        }
        if ($scale === null) {
            $scale = $this->scale;
        } elseif ($scale < 0) {
            throw new InvalidArgumentException('Scale cannot be negative.');
        }
        if ($that->value === '1' && $that->scale === 0 && $scale === $this->scale) {
            return $this;
        }
        $p = $this->valueWithMinScale($that->scale + $scale);
        $q = $that->valueWithMinScale($this->scale - $scale);
        $result = Calculator::get()->divRound($p, $q, $roundingMode);
        return new BigDecimal($result, $scale);
    }
    /**
     * @param BigNumber|int|float|string $that
     */
    public function exactlyDividedBy($that): BigDecimal
    {
        $that = BigDecimal::of($that);
        if ($that->value === '0') {
            throw DivisionByZeroException::divisionByZero();
        }
        [, $b] = $this->scaleValues($this, $that);
        $d = \rtrim($b, '0');
        $scale = \strlen($b) - \strlen($d);
        $calculator = Calculator::get();
        foreach ([5, 2] as $prime) {
            for (;;) {
                $lastDigit = (int) $d[-1];
                if ($lastDigit % $prime !== 0) {
                    break;
                }
                $d = $calculator->divQ($d, (string) $prime);
                $scale++;
            }
        }
        return $this->dividedBy($that, $scale)->stripTrailingZeros();
    }
    /**
     * @param int $exponent
     */
    public function power($exponent): BigDecimal
    {
        if ($exponent === 0) {
            return BigDecimal::one();
        }
        if ($exponent === 1) {
            return $this;
        }
        if ($exponent < 0 || $exponent > Calculator::MAX_POWER) {
            throw new InvalidArgumentException(\sprintf('The exponent %d is not in the range 0 to %d.', $exponent, Calculator::MAX_POWER));
        }
        return new BigDecimal(Calculator::get()->pow($this->value, $exponent), $this->scale * $exponent);
    }
    /**
     * @param BigNumber|int|float|string $that
     */
    public function quotient($that): BigDecimal
    {
        $that = BigDecimal::of($that);
        if ($that->isZero()) {
            throw DivisionByZeroException::divisionByZero();
        }
        $p = $this->valueWithMinScale($that->scale);
        $q = $that->valueWithMinScale($this->scale);
        $quotient = Calculator::get()->divQ($p, $q);
        return new BigDecimal($quotient, 0);
    }
    /**
     * @param BigNumber|int|float|string $that
     */
    public function remainder($that): BigDecimal
    {
        $that = BigDecimal::of($that);
        if ($that->isZero()) {
            throw DivisionByZeroException::divisionByZero();
        }
        $p = $this->valueWithMinScale($that->scale);
        $q = $that->valueWithMinScale($this->scale);
        $remainder = Calculator::get()->divR($p, $q);
        $scale = $this->scale > $that->scale ? $this->scale : $that->scale;
        return new BigDecimal($remainder, $scale);
    }
    /**
     * @param BigNumber|int|float|string $that
     */
    public function quotientAndRemainder($that): array
    {
        $that = BigDecimal::of($that);
        if ($that->isZero()) {
            throw DivisionByZeroException::divisionByZero();
        }
        $p = $this->valueWithMinScale($that->scale);
        $q = $that->valueWithMinScale($this->scale);
        [$quotient, $remainder] = Calculator::get()->divQR($p, $q);
        $scale = $this->scale > $that->scale ? $this->scale : $that->scale;
        $quotient = new BigDecimal($quotient, 0);
        $remainder = new BigDecimal($remainder, $scale);
        return [$quotient, $remainder];
    }
    /**
     * @param int $scale
     */
    public function sqrt($scale): BigDecimal
    {
        if ($scale < 0) {
            throw new InvalidArgumentException('Scale cannot be negative.');
        }
        if ($this->value === '0') {
            return new BigDecimal('0', $scale);
        }
        if ($this->value[0] === '-') {
            throw new NegativeNumberException('Cannot calculate the square root of a negative number.');
        }
        $value = $this->value;
        $addDigits = 2 * $scale - $this->scale;
        if ($addDigits > 0) {
            $value .= \str_repeat('0', $addDigits);
        } elseif ($addDigits < 0) {
            if (-$addDigits >= \strlen($this->value)) {
                return new BigDecimal('0', $scale);
            }
            $value = \substr($value, 0, $addDigits);
        }
        $value = Calculator::get()->sqrt($value);
        return new BigDecimal($value, $scale);
    }
    /**
     * @param int $n
     */
    public function withPointMovedLeft($n): BigDecimal
    {
        if ($n === 0) {
            return $this;
        }
        if ($n < 0) {
            return $this->withPointMovedRight(-$n);
        }
        return new BigDecimal($this->value, $this->scale + $n);
    }
    /**
     * @param int $n
     */
    public function withPointMovedRight($n): BigDecimal
    {
        if ($n === 0) {
            return $this;
        }
        if ($n < 0) {
            return $this->withPointMovedLeft(-$n);
        }
        $value = $this->value;
        $scale = $this->scale - $n;
        if ($scale < 0) {
            if ($value !== '0') {
                $value .= \str_repeat('0', -$scale);
            }
            $scale = 0;
        }
        return new BigDecimal($value, $scale);
    }
    public function stripTrailingZeros(): BigDecimal
    {
        if ($this->scale === 0) {
            return $this;
        }
        $trimmedValue = \rtrim($this->value, '0');
        if ($trimmedValue === '') {
            return BigDecimal::zero();
        }
        $trimmableZeros = \strlen($this->value) - \strlen($trimmedValue);
        if ($trimmableZeros === 0) {
            return $this;
        }
        if ($trimmableZeros > $this->scale) {
            $trimmableZeros = $this->scale;
        }
        $value = \substr($this->value, 0, -$trimmableZeros);
        $scale = $this->scale - $trimmableZeros;
        return new BigDecimal($value, $scale);
    }
    public function abs(): BigDecimal
    {
        return $this->isNegative() ? $this->negated() : $this;
    }
    public function negated(): BigDecimal
    {
        return new BigDecimal(Calculator::get()->neg($this->value), $this->scale);
    }
    /**
     * @param BigNumber|int|float|string $that
     */
    public function compareTo($that): int
    {
        $that = BigNumber::of($that);
        if ($that instanceof BigInteger) {
            $that = $that->toBigDecimal();
        }
        if ($that instanceof BigDecimal) {
            [$a, $b] = $this->scaleValues($this, $that);
            return Calculator::get()->cmp($a, $b);
        }
        return -$that->compareTo($this);
    }
    public function getSign(): int
    {
        return $this->value === '0' ? 0 : ($this->value[0] === '-' ? -1 : 1);
    }
    public function getUnscaledValue(): BigInteger
    {
        return self::newBigInteger($this->value);
    }
    public function getScale(): int
    {
        return $this->scale;
    }
    public function getIntegralPart(): string
    {
        if ($this->scale === 0) {
            return $this->value;
        }
        $value = $this->getUnscaledValueWithLeadingZeros();
        return \substr($value, 0, -$this->scale);
    }
    public function getFractionalPart(): string
    {
        if ($this->scale === 0) {
            return '';
        }
        $value = $this->getUnscaledValueWithLeadingZeros();
        return \substr($value, -$this->scale);
    }
    public function hasNonZeroFractionalPart(): bool
    {
        return $this->getFractionalPart() !== \str_repeat('0', $this->scale);
    }
    public function toBigInteger(): BigInteger
    {
        $zeroScaleDecimal = $this->scale === 0 ? $this : $this->dividedBy(1, 0);
        return self::newBigInteger($zeroScaleDecimal->value);
    }
    public function toBigDecimal(): BigDecimal
    {
        return $this;
    }
    public function toBigRational(): BigRational
    {
        $numerator = self::newBigInteger($this->value);
        $denominator = self::newBigInteger('1' . \str_repeat('0', $this->scale));
        return self::newBigRational($numerator, $denominator, \false);
    }
    /**
     * @param int $scale
     * @param RoundingMode $roundingMode
     */
    public function toScale($scale, $roundingMode = RoundingMode::UNNECESSARY): BigDecimal
    {
        if ($scale === $this->scale) {
            return $this;
        }
        return $this->dividedBy(BigDecimal::one(), $scale, $roundingMode);
    }
    public function toInt(): int
    {
        return $this->toBigInteger()->toInt();
    }
    public function toFloat(): float
    {
        return (float) (string) $this;
    }
    public function __toString(): string
    {
        if ($this->scale === 0) {
            return $this->value;
        }
        $value = $this->getUnscaledValueWithLeadingZeros();
        return \substr($value, 0, -$this->scale) . '.' . \substr($value, -$this->scale);
    }
    public function __serialize(): array
    {
        return ['value' => $this->value, 'scale' => $this->scale];
    }
    public function __unserialize(array $data): void
    {
        if (isset($this->value)) {
            throw new LogicException('__unserialize() is an internal function, it must not be called directly.');
        }
        $this->value = $data['value'];
        $this->scale = $data['scale'];
    }
    private function scaleValues(BigDecimal $x, BigDecimal $y): array
    {
        $a = $x->value;
        $b = $y->value;
        if ($b !== '0' && $x->scale > $y->scale) {
            $b .= \str_repeat('0', $x->scale - $y->scale);
        } elseif ($a !== '0' && $x->scale < $y->scale) {
            $a .= \str_repeat('0', $y->scale - $x->scale);
        }
        return [$a, $b];
    }
    private function valueWithMinScale(int $scale): string
    {
        $value = $this->value;
        if ($this->value !== '0' && $scale > $this->scale) {
            $value .= \str_repeat('0', $scale - $this->scale);
        }
        return $value;
    }
    private function getUnscaledValueWithLeadingZeros(): string
    {
        $value = $this->value;
        $targetLength = $this->scale + 1;
        $negative = $value[0] === '-';
        $length = \strlen($value);
        if ($negative) {
            $length--;
        }
        if ($length >= $targetLength) {
            return $this->value;
        }
        if ($negative) {
            $value = \substr($value, 1);
        }
        $value = \str_pad($value, $targetLength, '0', \STR_PAD_LEFT);
        if ($negative) {
            $value = '-' . $value;
        }
        return $value;
    }
}
