<?php
/**
 * BigDecimal implementation using native PHP BCMath.
 *
 * Replaces brick/math dependency with precise decimal arithmetic.
 * Requires PHP bcmath extension (available by default since PHP 7.0).
 *
 * @package CRTodayCurrencyBlocks
 */

declare(strict_types=1);

if (! defined('ABSPATH')) {
	exit;
}

/**
 * Immutable arbitrary-precision decimal number using BCMath.
 */
final class CRToday_BigDecimal {

	/**
	 * String representation of the decimal number.
	 *
	 * @var string
	 */
	private $value;

	/**
	 * Private constructor. Use static factory methods.
	 *
	 * @param string $value Decimal string.
	 */
	private function __construct(string $value) {
		$this->value = $this->normalize($value);
	}

	/**
	 * Create BigDecimal from numeric value.
	 *
	 * @param string|int|float $value Numeric value.
	 * @return self
	 */
	public static function of($value): self {
		if ($value instanceof self) {
			return $value;
		}

		if (is_int($value)) {
			return new self((string) $value);
		}

		if (is_float($value)) {
			// Use sprintf for exact float→string conversion
			return new self(sprintf('%.14F', $value));
		}

		if (is_string($value)) {
			return new self($value);
		}

		throw new InvalidArgumentException('Value must be numeric');
	}

	/**
	 * Get zero constant.
	 *
	 * @return self
	 */
	public static function zero(): self {
		return new self('0');
	}

	/**
	 * Get one constant.
	 *
	 * @return self
	 */
	public static function one(): self {
		return new self('1');
	}

	/**
	 * Normalize decimal string (remove trailing zeros after decimal point).
	 *
	 * @param string $value Decimal string.
	 * @return string Normalized string.
	 */
	private function normalize(string $value): string {
		// Remove whitespace
		$value = trim($value);

		// Validate numeric
		if (! is_numeric($value)) {
			throw new InvalidArgumentException( esc_html( "Invalid numeric value: $value" ) );
		}

		// Use bcadd with 0 to normalize format
		return bcadd($value, '0', 100);
	}

	/**
	 * Add two numbers.
	 *
	 * @param self|string|int|float $other Number to add.
	 * @param int                   $scale Decimal scale.
	 * @return self
	 */
	public function plus($other, int $scale = 100): self {
		$other_value = self::of($other)->value;
		$result = bcadd($this->value, $other_value, $scale);
		return new self($result);
	}

	/**
	 * Subtract number.
	 *
	 * @param self|string|int|float $other Number to subtract.
	 * @param int                   $scale Decimal scale.
	 * @return self
	 */
	public function minus($other, int $scale = 100): self {
		$other_value = self::of($other)->value;
		$result = bcsub($this->value, $other_value, $scale);
		return new self($result);
	}

	/**
	 * Multiply by number.
	 *
	 * @param self|string|int|float $other Number to multiply.
	 * @param int                   $scale Decimal scale.
	 * @return self
	 */
	public function multipliedBy($other, int $scale = 100): self {
		$other_value = self::of($other)->value;
		$result = bcmul($this->value, $other_value, $scale);
		return new self($result);
	}

	/**
	 * Divide by number.
	 *
	 * @param self|string|int|float $other         Number to divide by.
	 * @param int                   $scale         Decimal scale.
	 * @param int                   $rounding_mode Rounding mode constant.
	 * @return self
	 */
	public function dividedBy($other, int $scale = 100, int $rounding_mode = CRToday_RoundingMode::HALF_UP): self {
		$other_value = self::of($other)->value;

		if (bccomp($other_value, '0', 100) === 0) {
			throw new InvalidArgumentException('Division by zero');
		}

		$result = bcdiv($this->value, $other_value, $scale + 2);

		// Apply rounding
		return $this->apply_rounding($result, $scale, $rounding_mode);
	}

	/**
	 * Round to scale with rounding mode.
	 *
	 * @param int $decimals      Number of decimal places.
	 * @param int $rounding_mode Rounding mode constant.
	 * @return self
	 */
	public function toScale(int $decimals, int $rounding_mode = CRToday_RoundingMode::HALF_UP): self {
		return $this->apply_rounding($this->value, $decimals, $rounding_mode);
	}

	/**
	 * Apply rounding mode.
	 *
	 * @param string $value         Value to round.
	 * @param int    $decimals      Decimal places.
	 * @param int    $rounding_mode Rounding mode.
	 * @return self
	 */
	private function apply_rounding(string $value, int $decimals, int $rounding_mode): self {
		// Get sign
		$is_negative = (bccomp($value, '0', 100) < 0);
		$abs_value = $is_negative ? bcmul($value, '-1', 100) : $value;

		// Split into integer and fractional parts
		$parts = explode('.', $abs_value);
		$integer_part = $parts[0];
		$fractional_part = isset($parts[1]) ? $parts[1] : '';

		if (strlen($fractional_part) <= $decimals) {
			// No rounding needed
			return new self($value);
		}

		// Get digit at rounding position and next digit
		$round_digit = (int) substr($fractional_part, $decimals, 1);
		$next_digit = $decimals + 1 < strlen($fractional_part)
			? (int) substr($fractional_part, $decimals + 1, 1)
			: 0;

		$should_round_up = false;

		switch ($rounding_mode) {
			case CRToday_RoundingMode::DOWN:
				// Always truncate
				$should_round_up = false;
				break;

			case CRToday_RoundingMode::HALF_UP:
				// Round up if digit >= 5
				$should_round_up = ($round_digit >= 5);
				break;

			case CRToday_RoundingMode::HALF_EVEN:
				// Banker's rounding
				if ($round_digit > 5) {
					$should_round_up = true;
				} elseif ($round_digit === 5) {
					// Check if there are non-zero digits after
					$has_trailing = ($next_digit > 0) || (strlen($fractional_part) > $decimals + 1);
					if ($has_trailing) {
						$should_round_up = true;
					} else {
						// Round to even
						$last_kept_digit = $decimals > 0
							? (int) substr($fractional_part, $decimals - 1, 1)
							: (int) substr($integer_part, -1);
						$should_round_up = ($last_kept_digit % 2 !== 0);
					}
				}
				break;
		}

		// Truncate to scale
		$truncated = $integer_part . '.' . substr($fractional_part, 0, $decimals);

		if ($should_round_up) {
			// Add 1 at the last decimal position
			$increment = bcdiv('1', bcpow('10', (string) $decimals, 0), $decimals);
			$truncated = bcadd($truncated, $increment, $decimals);
		}

		// Restore sign
		$result = $is_negative ? bcmul($truncated, '-1', $decimals) : $truncated;

		return new self($result);
	}

	/**
	 * Compare with another number.
	 *
	 * @param self|string|int|float $other Number to compare.
	 * @return int -1 if less, 0 if equal, 1 if greater.
	 */
	public function compareTo($other): int {
		$other_value = self::of($other)->value;
		return bccomp($this->value, $other_value, 100);
	}

	/**
	 * Check if less than or equal to another number.
	 *
	 * @param self|string|int|float $other Number to compare.
	 * @return bool
	 */
	public function isLessThanOrEqualTo($other): bool {
		return $this->compareTo($other) <= 0;
	}

	/**
	 * Check if less than another number.
	 *
	 * @param self|string|int|float $other Number to compare.
	 * @return bool
	 */
	public function isLessThan($other): bool {
		return $this->compareTo($other) < 0;
	}

	/**
	 * Check if greater than another number.
	 *
	 * @param self|string|int|float $other Number to compare.
	 * @return bool
	 */
	public function isGreaterThan($other): bool {
		return $this->compareTo($other) > 0;
	}

	/**
	 * Check if greater than or equal to another number.
	 *
	 * @param self|string|int|float $other Number to compare.
	 * @return bool
	 */
	public function isGreaterThanOrEqualTo($other): bool {
		return $this->compareTo($other) >= 0;
	}

	/**
	 * Check if equal to another number.
	 *
	 * @param self|string|int|float $other Number to compare.
	 * @return bool
	 */
	public function isEqualTo($other): bool {
		return $this->compareTo($other) === 0;
	}

	/**
	 * Get string representation.
	 *
	 * @return string
	 */
	public function __toString(): string {
		return $this->value;
	}

	/**
	 * Get string value.
	 *
	 * @return string
	 */
	public function getValue(): string {
		return $this->value;
	}
}

/**
 * Rounding mode constants compatible with brick/math.
 */
final class CRToday_RoundingMode {
	/**
	 * Round towards zero (truncate).
	 */
	public const DOWN = 1;

	/**
	 * Round towards nearest neighbor, ties away from zero.
	 */
	public const HALF_UP = 4;

	/**
	 * Round towards nearest neighbor, ties to even (Banker's rounding).
	 */
	public const HALF_EVEN = 6;
}
