<?php
namespace php_rutils;

/**
 * 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, 'strlen');

        $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));
    }
}
