<?php
/**
 * Jingga
 *
 * PHP Version 8.2
 *
 * @package   phpOMS\Utils\Converter
 * @copyright Dennis Eichhorn
 * @license   OMS License 2.2
 * @version   1.0.0
 * @link      https://jingga.app
 */
declare(strict_types=1);

namespace phpOMS\Utils\Converter;

/**
 * Numeric converter.
 *
 * @package phpOMS\Utils\Converter
 * @license OMS License 2.2
 * @link    https://jingga.app
 * @since   1.0.0
 */
final class Numeric
{
    /**
     * Romans association.
     *
     * @var array<string, int>
     * @since 1.0.0
     */
    public const ROMANS = [
        'M'  => 1000, 'CM' => 900, 'D' => 500, 'CD' => 400, 'C' => 100,
        'XC' => 90, 'L' => 50, 'XL' => 40, 'X' => 10,
        'IX' => 9, 'V' => 5, 'IV' => 4, 'I' => 1,
    ];

    /**
     * Constructor.
     *
     * @since 1.0.0
     * @codeCoverageIgnore
     */
    private function __construct()
    {
    }

    /**
     * Convert base.
     *
     * @param string $numberInput   Input number
     * @param string $fromBaseInput Input layout (e.g. 0123456789ABCDEF)
     * @param string $toBaseInput   Output layout (e.g. 0123456789ABCDEF)
     *
     * @return string
     *
     * @since 1.0.0
     */
    public static function convertBase(string $numberInput, string $fromBaseInput, string $toBaseInput) : string
    {
        if ($fromBaseInput === $toBaseInput) {
            return $numberInput;
        }

        $fromBase  = \str_split($fromBaseInput, 1);
        $toBase    = \str_split($toBaseInput, 1);
        $number    = \str_split($numberInput, 1);
        $fromLen   = \strlen($fromBaseInput);
        $toLen     = \strlen($toBaseInput);
        $numberLen = \strlen($numberInput);
        $newOutput = '';

        if ($toBaseInput === '0123456789') {
            $newOutput = '0';

            for ($i = 1; $i <= $numberLen; ++$i) {
                $newOutput = \bcadd(
                    $newOutput,
                    \bcmul(
                        (string) \array_search($number[$i - 1], $fromBase),
                        \bcpow((string) $fromLen, (string) ($numberLen - $i))
                    )
                );
            }

            return $newOutput;
        }

        $base10 = (int) ($fromBaseInput !== '0123456789' ? self::convertBase($numberInput, $fromBaseInput, '0123456789') : $numberInput);

        if ($base10 < \strlen($toBaseInput)) {
            return $toBase[$base10];
        }

        while ($base10 !== '0') {
            $newOutput = $toBase[(int) \bcmod((string) $base10, (string) $toLen)] . $newOutput;
            $base10    = \bcdiv((string) $base10, (string) $toLen, 0);
        }

        return $newOutput;
    }

    /**
     * Convert arabic to roman.
     *
     * Be aware that there is no standard for larger roman numbers.
     *
     * @param int $arabic Arabic number
     *
     * @return string
     *
     * @since 1.0.0
     */
    public static function arabicToRoman(int $arabic) : string
    {
        $result = '';

        while ($arabic > 0) {
            foreach (self::ROMANS as $rom => $arb) {
                if ($arabic >= $arb) {
                    $arabic -= $arb;
                    $result .= $rom;
                    break;
                }
            }
        }

        return $result;
    }

    /**
     * Convert roman to arabic.
     *
     * @param string $roman Roman number
     *
     * @return int
     *
     * @since 1.0.0
     */
    public static function romanToArabic(string $roman) : int
    {
        $result = 0;

        foreach (self::ROMANS as $key => $value) {
            while (\str_starts_with($roman, $key)) {
                $result += $value;
                $temp = \substr($roman, \strlen($key));

                if ($temp !== false) {
                    $roman = $temp;
                }
            }
        }

        return $result;
    }

    /**
     * Convert numeric to alpha.
     *
     * This can be used for alpha lists such as e.g. word uses.
     *
     * @param int $number Number to convert
     *
     * @return string
     *
     * @since 1.0.0
     */
    public static function numericToAlpha(int $number) : string
    {
        $alpha = '';

        for ($i = 1; $number >= 0 && $i < 10; ++$i) {
            $alpha = \chr(0x41 + (int) ($number % \pow(26, $i) / \pow(26, $i - 1))) . $alpha;
            $number -= \pow(26, $i);
        }

        return $alpha;
    }

    /**
     * Convert alpha to numeric.
     *
     * @param string $alpha Alpha to convert
     *
     * @return int
     *
     * @since 1.0.0
     */
    public static function alphaToNumeric(string $alpha) : int
    {
        $numeric = 0;
        $length  = \strlen($alpha);

        for ($i = 0; $i < $length; ++$i) {
            $numeric += \pow(26, $i) * (\ord($alpha[$length - $i - 1]) - 0x40);
        }

        return (int) $numeric - 1;
    }
}
