<?php
namespace Components\Support;

/**
 * Format: support class for number, date, and misc formatting helpers.
 *
 * Notes
 * - All helpers are static and safe for PHP 7.4.
 * - Focus: humanNumber() for compact, locale-aware numbers (e.g., 743 · 1.3k · 4.7m).
 */
class Format
{
    /**
     * Format a number in human-friendly short form with locale separators.
     * Examples:
     *  - 743 => "743"
     *  - 1300 => "1.3k" (or "1,3k" in locales using comma as decimal separator)
     *  - 4700000 => "4.7m"
     *
     * Options:
     *  - locale (string|null): ICU/locale code for Intl NumberFormatter. Defaults to system/intl default.
     *  - decimals (int): fraction digits for values with suffix (k/m/b/t). Default 1.
     *  - trim_zeros (bool): remove trailing zeros and dangling decimal separator. Default true.
     *  - suffixes (array): custom suffix map [1=>'k',2=>'m',3=>'b',4=>'t'].
     *  - upper (bool): use uppercase suffixes (K/M/B/T). Default false.
     *  - decimal_sep (string|null): override decimal separator. Default from locale.
     *
     * @param int|float|string $number
     * @param array $options
     * @return string
     */
    public static function humanNumber($number, array $options = []): string
    {
        if ($number === null || $number === '') {
            return '0';
        }
        // Normalize to float (safe for magnitudes used here)
        $num = 0.0;
        if (is_numeric($number)) {
            $num = (float)$number;
        } else {
            // Non-numeric input – best effort
            $num = (float)@preg_replace('/[^0-9\-\.eE]/', '', (string)$number);
        }

        if (!is_finite($num)) {
            return '0';
        }

        $sign = ($num < 0) ? '-' : '';
        $abs = abs($num);

        $decimals = isset($options['decimals']) ? max(0, (int)$options['decimals']) : 1;
        $trimZeros = array_key_exists('trim_zeros', $options) ? (bool)$options['trim_zeros'] : true;
        $upper = (bool)($options['upper'] ?? false);
        $locale = isset($options['locale']) && is_string($options['locale']) && $options['locale'] !== '' ? (string)$options['locale'] : null;
        $decimalSep = is_string($options['decimal_sep'] ?? null) ? (string)$options['decimal_sep'] : null;

        // Determine separators from locale if not overridden
        if ($decimalSep === null) {
            $seps = self::localeSeparators($locale);
            $decimalSep = $seps['decimal'];
        }

        // Default suffixes
        $suffixes = $options['suffixes'] ?? [1 => 'k', 2 => 'm', 3 => 'b', 4 => 't'];
        if ($upper) {
            foreach ($suffixes as $i => $s) { $suffixes[$i] = strtoupper((string)$s); }
        }

        // Choose unit index
        $divisors = [1, 1e3, 1e6, 1e9, 1e12];
        $idx = 0;
        while ($idx < 4 && $abs >= 1000) { $abs /= 1000; $idx++; }

        if ($idx === 0) {
            // No suffix – show integer without decimals
            // Use plain integer string; for compact output we avoid grouping for < 1000 by definition
            return $sign . (string)(int)round($num, 0);
        }

        // Recompute with divisor precisely to avoid drift and apply rounding
        $div = $divisors[$idx];
        $scaled = abs($num) / $div;

        // Round with fixed decimals
        $rounded = self::roundFixed($scaled, $decimals);

        // If rounding bumps to 1000, escalate to next unit
        if ($rounded >= 1000 && $idx < 4) {
            $idx++;
            $div = $divisors[$idx];
            $scaled = abs($num) / $div;
            $rounded = self::roundFixed($scaled, $decimals);
        }

        // Compose numeric part string without grouping
        $fracDigits = $decimals;
        $str = number_format($rounded, $fracDigits, '.', '');
        if ($trimZeros && $fracDigits > 0) {
            // Trim trailing zeros and optional dot
            $str = rtrim(rtrim($str, '0'), '.');
        }
        // Replace decimal separator as per locale
        if ($decimalSep !== '.') {
            $str = str_replace('.', $decimalSep, $str);
        }

        $suffix = $suffixes[$idx] ?? ($upper ? 'K' : 'k');
        return $sign . $str . $suffix;
    }

    /**
     * Determine decimal and thousands separators for a locale using Intl if available,
     * else fallback to localeconv() and common defaults.
     * @param string|null $locale
     * @return array{decimal:string, thousands:string}
     */
    private static function localeSeparators(?string $locale): array
    {
        // Intl extension preferred
        if (class_exists('NumberFormatter')) {
            try {
                /** @noinspection PhpFullyQualifiedNameUsageInspection */
                $fmt = new \NumberFormatter($locale ?: (class_exists('Locale') ? \Locale::getDefault() : ''), \NumberFormatter::DECIMAL);
                $dec = (string)$fmt->getSymbol(\NumberFormatter::DECIMAL_SEPARATOR_SYMBOL);
                $grp = (string)$fmt->getSymbol(\NumberFormatter::GROUPING_SEPARATOR_SYMBOL);
                $dec = $dec !== '' ? $dec : '.';
                $grp = $grp !== '' ? $grp : ',';
                return ['decimal' => $dec, 'thousands' => $grp];
            } catch (\Throwable $e) {
                // fall through
            }
        }
        // Fallback to C locale conventions
        $lc = @localeconv();
        $dec = isset($lc['decimal_point']) && $lc['decimal_point'] !== '' ? (string)$lc['decimal_point'] : '.';
        $grp = isset($lc['thousands_sep']) && $lc['thousands_sep'] !== '' ? (string)$lc['thousands_sep'] : ',';
        return ['decimal' => $dec, 'thousands' => $grp];
    }

    /**
     * Round the number to a fixed number of decimals and return a float
     * that preserves the intended decimal precision for number_format.
     */
    private static function roundFixed(float $value, int $decimals): float
    {
        if ($decimals <= 0) { return (float)round($value); }
        $factor = pow(10, $decimals);
        return floor($value * $factor + 0.5) / $factor; // classic round-half-up
    }
}
