<?php

/**
 * Plural forms and in-word representation for numerals
 * Class Numeral
 * @package php_rutils
 */
class Numeral
{
    private static $_FRACTIONS = array(
        array('десятая', 'десятых', 'десятых'),
        array('сотая', 'сотых', 'сотых'),
        array('тысячная', 'тысячных', 'тысячных'),
        array('десятитысячная', 'десятитысячных', 'десятитысячных'),
        array('стотысячная', 'стотысячных', 'стотысячных'),
        array('миллионная', 'милллионных', 'милллионных'),
        array('десятимиллионная', 'десятимилллионных', 'десятимиллионных'),
        array('стомиллионная', 'стомилллионных', 'стомиллионных'),
        array('миллиардная', 'миллиардных', 'миллиардных'),
    ); //Forms (1, 2, 5) for fractions

    private static $_ONES = array(
        array('', '', ''),
        array('один', 'одна', 'одно'),
        array('два', 'две', 'два'),
        array('три', 'три', 'три'),
        array('четыре', 'четыре', 'четыре'),
        array('пять', 'пять', 'пять'),
        array('шесть', 'шесть', 'шесть'),
        array('семь', 'семь', 'семь'),
        array('восемь', 'восемь', 'восемь'),
        array('девять', 'девять', 'девять'),
    ); //Forms (MALE, FEMALE, NEUTER) for ones

    private static $_TENS = array(
        0 => '',
        //1 - special variant
        10 => 'десять',
        11 => 'одиннадцать',
        12 => 'двенадцать',
        13 => 'тринадцать',
        14 => 'четырнадцать',
        15 => 'пятнадцать',
        16 => 'шестнадцать',
        17 => 'семнадцать',
        18 => 'восемнадцать',
        19 => 'девятнадцать',
        2 => 'двадцать',
        3 => 'тридцать',
        4 => 'сорок',
        5 => 'пятьдесят',
        6 => 'шестьдесят',
        7 => 'семьдесят',
        8 => 'восемьдесят',
        9 => 'девяносто',
    ); //Tens

    private static $_HUNDREDS = array(
        0 => '',
        1 => 'сто',
        2 => 'двести',
        3 => 'триста',
        4 => 'четыреста',
        5 => 'пятьсот',
        6 => 'шестьсот',
        7 => 'семьсот',
        8 => 'восемьсот',
        9 => 'девятьсот',
    ); //Hundreds

    /**
     * Get proper case with value
     * @param int $amount Amount of objects
     * @param array $variants Variants (forms) of object in such form: array('1 object', '2 objects', '5 objects')
     * @param string|null $absence If amount is zero will return it
     * @return string|null
     */
    public function getPlural($amount, array $variants, $absence = null)
    {
        if ($amount || $absence === null)
            $result = RUtils::formatNumber($amount).' '.$this->choosePlural($amount, $variants);
        else
            $result = $absence;
        return $result;
    }

    /**
     * Choose proper case depending on amount
     * @param int $amount Amount of objects
     * @param string[] $variants Variants (forms) of object in such form: array('1 object', '2 objects', '5 objects')
     * @return string Proper variant
     * @throws \InvalidArgumentException Variants' length lesser than 3
     */
    public function choosePlural($amount, array $variants)
    {
        if (sizeof($variants) < 3)
            throw new \InvalidArgumentException('Incorrect values length (must be 3)');

        $amount = abs($amount);
        $mod10 = $amount%10;
        $mod100 = $amount%100;

        if ($mod10 == 1 && $mod100 != 11)
            $variant = 0;
        elseif ($mod10 >= 2 && $mod10 <= 4 && !($mod100 > 10 && $mod100 < 20))
            $variant = 1;
        else
            $variant = 2;

        return $variants[$variant];
    }

    /**
     * Get sum in words
     * @param int $amount Amount of objects (0 <= amount <= PHP_INT_MAX)
     * @param int $gender Gender of object (MALE, FEMALE or NEUTER)
     * @param array $variants Variants (forms) of object in such form: array('1 object', '2 objects', '5 objects')
     * @return string In-words representation objects' amount
     * @throws \RangeException
     * @throws \InvalidArgumentException
     */
    public function sumString($amount, $gender, array $variants=null)
    {
        if ($variants === null)
            $variants = array_fill(0, 3, '');
        if (sizeof($variants) < 3)
            throw new \InvalidArgumentException('Incorrect items length (must be 3)');
        if ($amount < 0)
            throw new \InvalidArgumentException('Amount must be positive or 0');

        if ($amount == 0)
            return  trim('ноль '.$variants[2]);

        $result = '';
        $tmpVal = $amount;

        //ones
        list($result, $tmpVal) = $this->_sumStringOneOrder($result, $tmpVal, $gender, $variants);
        //thousands
        list($result, $tmpVal) = $this->_sumStringOneOrder($result, $tmpVal,  RUtils::FEMALE,
                                                           array('тысяча', 'тысячи', 'тысяч'));
        //millions
        list($result, $tmpVal) = $this->_sumStringOneOrder($result, $tmpVal,  RUtils::MALE,
                                                           array('миллион', 'миллиона', 'миллионов'));
        //billions
        list($result,) = $this->_sumStringOneOrder($result, $tmpVal, RUtils::MALE,
                                                   array('миллиард', 'миллиарда', 'миллиардов'));
        return trim($result);
    }

    /**
     * Make in-words representation of single order
     * @param string $prevResult In-words representation of lower orders
     * @param int $tmpVal Temporary value without lower orders
     * @param int $gender (MALE, FEMALE or NEUTER)
     * @param string[] $variants Variants of objects
     * @throws \RangeException
     * @return array ($result, $tmpVal)
     */
    private function _sumStringOneOrder($prevResult, $tmpVal, $gender, array $variants)
    {
        if ($tmpVal == 0)
            return array($prevResult, $tmpVal);

        $words = array();
        $fiveItems = $variants[2];
        $rest = $tmpVal%1000;
        if ($rest < 0)
            throw new \RangeException('Int overflow');

        $tmpVal = intval($tmpVal/1000);

        //check last digits are 0
        if ($rest == 0) {
            if (!$prevResult)
                $prevResult = $fiveItems.' ';
            return array($prevResult, $tmpVal);
        }

        //hundreds
        $words[] = self::$_HUNDREDS[intval($rest/100)];

        //tens
        $rest %= 100;
        $rest1 = intval($rest/10);
        $words[] = ($rest1 == 1) ? self::$_TENS[$rest] : self::$_TENS[$rest1];

        //ones
        if ($rest1 == 1) {
            $endWord = $fiveItems;
        }
        else {
            $amount = $rest%10;
            $words[] = self::$_ONES[$amount][$gender-1];
            $endWord = $this->choosePlural($amount, $variants);
        }
        $words[] = $endWord;

        $words[] = $prevResult;
        $words = array_filter($words, function($val) {
                return (strlen($val) > 0);
            });

        $result = trim(implode(' ', $words));
        return array($result, $tmpVal);
    }

    /**
     * Numeral in words
     * @param float $amount Amount of objects
     * @param int|null $gender (MALE, FEMALE, NEUTER or null)
     * @return string In-words representation of numeral
     */
    public function getInWords($amount, $gender=RUtils::MALE)
    {
        if ($amount == (int)$amount)
            return $this->getInWordsInt($amount, $gender);
        else
            return $this->getInWordsFloat($amount);
    }

    /**
     * Integer in words
     * @param int $amount Amount of objects (0 <= amount <= PHP_INT_MAX)
     * @param int $gender (MALE, FEMALE or NEUTER)
     * @return string In-words representation of numeral
     */
    public function getInWordsInt($amount, $gender=RUtils::MALE)
    {
        $amount = round($amount);
        return $this->sumString($amount, $gender);
    }

    /**
     * Float in words
     * @param float $amount Amount of objects
     * @return string In-words representation of float numeral
     */
    public function getInWordsFloat($amount)
    {
        $words = array();

        $intPart = (int)$amount;
        $pointVariants = array('целая', 'целых', 'целых');
        $words[] = $this->sumString($intPart, RUtils::FEMALE, $pointVariants);

        $remainder = $this->_getFloatRemainder($amount);
        $signs = strlen($remainder) - 1;
        $words[] = $this->sumString($remainder, RUtils::FEMALE, self::$_FRACTIONS[$signs]);

        $result = trim(implode(' ', $words));
        return $result;
    }

    /**
     * Get remainder of float, i.e. 2.05 -> '05'
     * @param float $value
     * @param int $signs
     * @return string
     */
    private function _getFloatRemainder($value, $signs=9)
    {
        if ($value == (int)$value)
            return '0';

        $signs = min($signs, sizeof(self::$_FRACTIONS));
        $value = number_format($value, $signs, '.', '');
        list(, $remainder) = explode('.', $value);
        $remainder = preg_replace('/0+$/', '', $remainder);
        if (!$remainder)
            $remainder = '0';

        return $remainder;
    }

    /**
     * Get string for money (RUB)
     * @param float $amount Amount of money
     * @param bool $zeroForKopeck If false, then zero kopecks ignored
     * @return string   In-words representation of money's amount
     * @throws \InvalidArgumentException
     */
    public function getRubles($amount, $zeroForKopeck=false)
    {
        if ($amount < 0)
            throw new \InvalidArgumentException('Amount must be positive or 0');

        $words = array();
        $amount = round($amount, 2);

        $iAmount = (int)$amount;
        if ($iAmount)
            $words[] = $this->sumString((int)$amount, RUtils::MALE,
                                        array('рубль', 'рубля', 'рублей'));

        $remainder = $this->_getFloatRemainder($amount, 2);
        if ($remainder || $zeroForKopeck) {
            if ($remainder < 10 && strlen($remainder) == 1)
                $remainder *= 10;
            $words[] = $this->sumString($remainder, RUtils::FEMALE,
                                        array('копейка', 'копейки', 'копеек'));
        }

        return trim(implode(' ', $words));
    }
}
