<?php

/**
 * Invoice Ninja (https://invoiceninja.com).
 *
 * @link https://github.com/invoiceninja/invoiceninja source repository
 *
 * @copyright Copyright (c) 2026. Invoice Ninja LLC (https://invoiceninja.com)
 *
 * @license https://www.elastic.co/licensing/elastic-license
 */

namespace App\Utils;

/**
 * BcMath utility class for precise financial calculations
 *
 * This class provides static methods to replace float arithmetic and comparisons
 * with bcmath equivalents for consistent and accurate monetary calculations.
 *
 * All methods use a default scale of 2 decimal places for currency calculations.
 * You can override the scale for specific calculations if needed.
 */
class BcMath
{
    /**
     * Default scale for currency calculations (2 decimal places)
     */
    private const DEFAULT_SCALE = 10;

    /**
     * normalizeNumber
     *
     * @param  mixed $number
     * @return string
     */
    // private static function normalizeNumberX(mixed $number): string
    // {
    //     if ($number === null || $number === '') {
    //         return '0';
    //     } elseif (is_float($number)) {
    //         return rtrim(rtrim(number_format($number, 10, '.', ''), '0'), '.');
    //     }

    //     return (string) $number;
    // }

    private static function normalizeNumber(mixed $number): string
    {
        if ($number === null || $number === '') {
            return '0';
        }

        if (is_float($number) && !is_finite($number)) {
            return '0';
        }

        $str = (string) $number;

        if (stripos($str, 'e') !== false) {
            // Float was small/large enough that PHP used scientific notation.
            // Convert to fixed-point without padding extra precision.
            return rtrim(rtrim(sprintf('%.20F', (float) $str), '0'), '.') ?: '0';
        }

        return $str;
    }
    /**
     * Add two numbers using bcmath
     *
     * @param string|float|int $left
     * @param string|float|int $right
     * @param int|null $scale
     * @return string
     */
    public static function add($left, $right, ?int $scale = null): string
    {
        $scale ??= self::DEFAULT_SCALE;
        return bcadd(self::normalizeNumber($left), self::normalizeNumber($right), $scale);
    }

    /**
     * Subtract two numbers using bcmath
     *
     * @param string|float|int $left
     * @param string|float|int $right
     * @param int|null $scale
     * @return string
     */
    public static function sub($left, $right, ?int $scale = null): string
    {
        $scale ??= self::DEFAULT_SCALE;
        return bcsub(self::normalizeNumber($left), self::normalizeNumber($right), $scale);
    }

    /**
     * Multiply two numbers using bcmath
     *
     * @param string|float|int $left
     * @param string|float|int $right
     * @param int|null $scale
     * @return string
     */
    public static function mul($left, $right, ?int $scale = null): string
    {
        $scale ??= self::DEFAULT_SCALE;
        return bcmul(self::normalizeNumber($left), self::normalizeNumber($right), $scale);
    }

    /**
     * Divide two numbers using bcmath
     *
     * @param string|float|int $left
     * @param string|float|int $right
     * @param int|null $scale
     * @return string
     */
    public static function div($left, $right, ?int $scale = null): string
    {
        $scale ??= self::DEFAULT_SCALE;
        return bcdiv(self::normalizeNumber($left), self::normalizeNumber($right), $scale);
    }

    /**
     * Calculate modulo using bcmath
     *
     * @param string|float|int $left
     * @param string|float|int $right
     * @param int|null $scale
     * @return string
     */
    public static function mod($left, $right, ?int $scale = null): string
    {
        $scale ??= self::DEFAULT_SCALE;
        return bcmod(self::normalizeNumber($left), self::normalizeNumber($right), $scale);
    }

    /**
     * Calculate power using bcmath
     *
     * @param string|float|int $base
     * @param string|float|int $exponent
     * @param int|null $scale
     * @return string
     */
    public static function pow($base, $exponent, ?int $scale = null): string
    {
        $scale ??= self::DEFAULT_SCALE;
        return bcpow(self::normalizeNumber($base), self::normalizeNumber($exponent), $scale);
    }

    /**
     * Calculate square root using bcmath
     *
     * @param string|float|int $number
     * @param int|null $scale
     * @return string
     */
    public static function sqrt($number, ?int $scale = null): string
    {
        $scale ??= self::DEFAULT_SCALE;
        return bcsqrt(self::normalizeNumber($number), $scale);
    }

    /**
     * Round a number to specified decimal places using bcmath
     *
     * @param string|float|int $number
     * @param int $precision
     * @return string
     */
    public static function round($number, int $precision = self::DEFAULT_SCALE): string
    {
        $number = self::normalizeNumber($number);

        /** New rounding implementation to work around changes to rounding in PHP 8.4 */
        $multiplier = bcpow('10', (string) $precision, 0);

        // Shift by 10^precision (keep 1 decimal to see the rounding digit),
        // add/subtract 0.5, truncate, then shift back
        $shifted = bcmul($number, $multiplier, 1);

        if (bccomp($shifted, '0', 1) >= 0) {
            $rounded = bcadd($shifted, '0.5', 0);
        } else {
            $rounded = bcsub($shifted, '0.5', 0);
        }

        return bcdiv($rounded, $multiplier, $precision);

    }

    /**
     * Compare two numbers using bcmath
     *
     * @param string|float|int $left
     * @param string|float|int $right
     * @param int|null $scale
     * @return int Returns -1, 0, or 1 if left is less than, equal to, or greater than right
     */
    public static function comp($left, $right, ?int $scale = null): int
    {
        $scale ??= self::DEFAULT_SCALE;
        return bccomp(self::normalizeNumber($left), self::normalizeNumber($right), $scale);
    }

    /**
     * Check if two numbers are equal using bcmath
     *
     * @param string|float|int $left
     * @param string|float|int $right
     * @param int|null $scale
     * @return bool
     */
    public static function equal($left, $right, ?int $scale = null): bool
    {
        return self::comp($left, $right, $scale) === 0;
    }

    /**
     * Check if left number is greater than right number using bcmath
     *
     * @param string|float|int $left
     * @param string|float|int $right
     * @param int|null $scale
     * @return bool
     */
    public static function greaterThan($left, $right, ?int $scale = null): bool
    {
        return self::comp($left, $right, $scale) === 1;
    }

    /**
     * Check if left number is greater than or equal to right number using bcmath
     *
     * @param string|float|int $left
     * @param string|float|int $right
     * @param int|null $scale
     * @return bool
     */
    public static function greaterThanOrEqual($left, $right, ?int $scale = null): bool
    {
        return self::comp($left, $right, $scale) >= 0;
    }

    /**
     * Check if left number is less than right number using bcmath
     *
     * @param string|float|int $left
     * @param string|float|int $right
     * @param int|null $scale
     * @return bool
     */
    public static function lessThan($left, $right, ?int $scale = null): bool
    {
        return self::comp($left, $right, $scale) === -1;
    }

    /**
     * Check if left number is less than or equal to right number using bcmath
     *
     * @param string|float|int $left
     * @param string|float|int $right
     * @param int|null $scale
     * @return bool
     */
    public static function lessThanOrEqual($left, $right, ?int $scale = null): bool
    {
        return self::comp($left, $right, $scale) <= 0;
    }

    /**
     * Check if a number is zero using bcmath
     *
     * @param string|float|int $number
     * @param int|null $scale
     * @return bool
     */
    public static function isZero($number, ?int $scale = null): bool
    {
        return self::equal($number, '0', $scale);
    }

    /**
     * Check if a number is positive using bcmath
     *
     * @param string|float|int $number
     * @param int|null $scale
     * @return bool
     */
    public static function isPositive($number, ?int $scale = null): bool
    {
        return self::greaterThan($number, '0', $scale);
    }

    /**
     * Check if a number is negative using bcmath
     *
     * @param string|float|int $number
     * @param int|null $scale
     * @return bool
     */
    public static function isNegative($number, ?int $scale = null): bool
    {
        return self::lessThan($number, '0', $scale);
    }

    /**
     * Get the absolute value using bcmath
     *
     * @param string|float|int $number
     * @param int|null $scale
     * @return string
     */
    public static function abs($number, ?int $scale = null): string
    {
        $scale ??= self::DEFAULT_SCALE;
        $number = (string) $number;

        if (self::isNegative($number, $scale)) {
            return self::mul($number, '-1', $scale);
        }

        return $number;
    }

    /**
     * Calculate percentage using bcmath
     *
     * @param string|float|int $part
     * @param string|float|int $total
     * @param int|null $scale
     * @return string
     */
    public static function percentage($part, $total, ?int $scale = null): string
    {
        $scale ??= self::DEFAULT_SCALE;

        if (self::isZero($total, $scale)) {
            return '0';
        }

        return self::mul(self::div($part, $total, $scale + 2), '100', $scale);
    }

    /**
     * Calculate sum of an array of numbers using bcmath
     *
     * @param array $numbers
     * @param int|null $scale
     * @return string
     */
    public static function sum(array $numbers, ?int $scale = null): string
    {
        $scale ??= self::DEFAULT_SCALE;
        $result = '0';

        foreach ($numbers as $number) {
            $result = self::add($result, $number, $scale);
        }

        return $result;
    }

    /**
     * Calculate average of an array of numbers using bcmath
     *
     * @param array $numbers
     * @param int|null $scale
     * @return string
     */
    public static function avg(array $numbers, ?int $scale = null): string
    {
        $scale ??= self::DEFAULT_SCALE;
        $count = count($numbers);

        if ($count === 0) {
            return '0';
        }

        $sum = self::sum($numbers, $scale);
        return self::div($sum, (string) $count, $scale);
    }

    /**
     * Format a number as currency string with proper precision
     *
     * @param string|float|int $number
     * @param int $precision
     * @return string
     */
    public static function formatCurrency($number, int $precision = self::DEFAULT_SCALE): string
    {
        $rounded = self::round($number, $precision);
        return number_format((float) $rounded, $precision, '.', '');
    }

    /**
     * Convert a number to float for compatibility with existing code
     * Use this sparingly and only when you need to pass values to functions that expect floats
     *
     * @param string|float|int $number
     * @return float
     */
    public static function toFloat($number): float
    {
        return (float) $number;
    }

    /**
     * Convert a number to string for consistent handling
     *
     * @param string|float|int $number
     * @return string
     */
    public static function toString($number): string
    {
        return (string) $number;
    }
}
