<?php

namespace FluentSupport\Framework\Support;

class Pluralizer {

    /**
     * Plural word form rules.
     *
     * @var array
     */
    public static $plural = array(
        '/(quiz)$/i' => "$1zes",
        '/^(ox)$/i' => "$1en",
        '/([m|l])ouse$/i' => "$1ice",
        '/(matr|vert|ind)ix$|ex$/i' => "$1ices",
        '/(stoma|epo|monar|matriar|patriar|oligar|eunu)ch$/i' => "$1chs",
        '/(x|ch|ss|sh)$/i' => "$1es",
        '/([^aeiouy]|qu)y$/i' => "$1ies",
        '/(hive)$/i' => "$1s",
        '/(?:([^f])fe|([lr])f)$/i' => "$1$2ves",
        '/(shea|lea|loa|thie)f$/i' => "$1ves",
        '/sis$/i' => "ses",
        '/([ti])um$/i' => "$1a",
        '/(torped|embarg|tomat|potat|ech|her|vet)o$/i' => "$1oes",
        '/(bu)s$/i' => "$1ses",
        '/(alias)$/i' => "$1es",
        '/(fung)us$/i' => "$1i",
        '/(ax|test)is$/i' => "$1es",
        '/(us)$/i' => "$1es",
        '/s$/i' => "s",
        '/$/' => "s",
    );

    /**
     * Singular word form rules.
     *
     * @var array
     */
    public static $singular = array(
        '/(quiz)zes$/i' => "$1",
        '/(matr)ices$/i' => "$1ix",
        '/(vert|vort|ind)ices$/i' => "$1ex",
        '/^(ox)en$/i' => "$1",
        '/(alias)es$/i' => "$1",
        '/(octop|vir|fung)i$/i' => "$1us",
        '/(cris|ax|test)es$/i' => "$1is",
        '/(shoe)s$/i' => "$1",
        '/(o)es$/i' => "$1",
        '/(bus)es$/i' => "$1",
        '/([m|l])ice$/i' => "$1ouse",
        '/(x|ch|ss|sh)es$/i' => "$1",
        '/(m)ovies$/i' => "$1ovie",
        '/(s)eries$/i' => "$1eries",
        '/([^aeiouy]|qu)ies$/i' => "$1y",
        '/([lr])ves$/i' => "$1f",
        '/(tive)s$/i' => "$1",
        '/(hive)s$/i' => "$1",
        '/(li|wi|kni)ves$/i' => "$1fe",
        '/(shea|loa|lea|thie)ves$/i' => "$1f",
        '/(^analy)ses$/i' => "$1sis",
        '/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i' => "$1$2sis",
        '/([ti])a$/i' => "$1um",
        '/(n)ews$/i' => "$1ews",
        '/(h|bl)ouses$/i' => "$1ouse",
        '/(corpse)s$/i' => "$1",
        '/(gallows|headquarters)$/i' => "$1",
        '/(us)es$/i' => "$1",
        '/(us|ss)$/i' => "$1",
        '/s$/i' => "",
    );

    /**
     * Irregular word forms.
     *
     * @var array
     */
    public static $irregular = array(
        'child' => 'children',
        'corpus' => 'corpora',
        'criterion' => 'criteria',
        'foot' => 'feet',
        'freshman' => 'freshmen',
        'goose' => 'geese',
        'genus' => 'genera',
        'human' => 'humans',
        'man' => 'men',
        'move' => 'moves',
        'nucleus' => 'nuclei',
        'ovum' => 'ova',
        'person' => 'people',
        'phenomenon' => 'phenomena',
        'radius' => 'radii',
        'sex' => 'sexes',
        'stimulus' => 'stimuli',
        'syllabus' => 'syllabi',
        'tax' => 'taxes',
        'tech' => 'techs',
        'tooth' => 'teeth',
        'viscus' => 'viscera',
    );

    /**
     * Uncountable word forms.
     *
     * @var array
     */
    public static $uncountable = array(
        'audio',
        'bison',
        'chassis',
        'compensation',
        'coreopsis',
        'data',
        'deer',
        'education',
        'equipment',
        'fish',
        'gold',
        'information',
        'money',
        'moose',
        'offspring',
        'plankton',
        'police',
        'rice',
        'series',
        'sheep',
        'species',
        'swine',
        'traffic',
    );

    /**
     * The cached copies of the plural inflections.
     *
     * @var array
     */
    protected static $pluralCache = array();

    /**
     * The cached copies of the singular inflections.
     *
     * @var array
     */
    protected static $singularCache = array();

    /**
     * Get the singular form of the given word.
     *
     * @param  string  $value
     * @return string
     */
    public static function singular($value)
    {
        if (isset(static::$singularCache[$value]))
        {
            return static::$singularCache[$value];
        }

        $result = static::inflect($value, static::$singular, static::$irregular);

        return static::$singularCache[$value] = $result ?: $value;
    }

    /**
     * Get the plural form of the given word.
     *
     * @param  string  $value
     * @param  int     $count
     * @return string
     */
    public static function plural($value, $count = 2)
    {
        if ($count == 1) return $value;

        if (in_array($value, static::$irregular)) return $value;

        // First we'll check the cache of inflected values. We cache each word that
        // is inflected so we don't have to spin through the regular expressions
        // on each subsequent method calls for this word by the app developer.
        if (isset(static::$pluralCache[$value]))
        {
            return static::$pluralCache[$value];
        }

        $irregular = array_flip(static::$irregular);

        // When doing the singular to plural transformation, we'll flip the irregular
        // array since we need to swap sides on the keys and values. After we have
        // the transformed value we will cache it in memory for faster look-ups.
        $plural = static::$plural;

        $result = static::inflect($value, $plural, $irregular);

        return static::$pluralCache[$value] = $result;
    }

    /**
     * Perform auto inflection on an English word.
     *
     * @param  string  $value
     * @param  array   $source
     * @param  array   $irregular
     * @return string
     */
    protected static function inflect($value, $source, $irregulars)
    {
        if (static::uncountable($value)) return $value;

        // Next, we will check the "irregular" patterns which contain words that are
        // not easily summarized in regular expression rules, like "children" and
        // "teeth", both of which cannot get inflected using our typical rules.
        foreach ($irregulars as $irregular => $pattern) {
            
            if (preg_match($pattern = '/'.$pattern.'$/i', $value)) {

                $irregular = static::matchCase($irregular, $value);

                return preg_replace($pattern, $irregular, $value);
            }
        }

        // Finally, we'll spin through the array of regular expressions and look for
        // matches for the word. If we find a match, we will cache and return the
        // transformed value so we will quickly look it up on subsequent calls.
        foreach ($source as $pattern => $inflected)
        {
            if (preg_match($pattern, $value))
            {
                $inflected = preg_replace($pattern, $inflected, $value);

                return static::matchCase($inflected, $value);
            }
        }
    }

    /**
     * Determine if the given value is uncountable.
     *
     * @param  string  $value
     * @return bool
     */
    protected static function uncountable($value)
    {
        return in_array(strtolower($value), static::$uncountable);
    }

    /**
     * Attempt to match the case on two strings.
     *
     * @param  string  $value
     * @param  string  $comparison
     * @return string
     */
    protected static function matchCase($value, $comparison)
    {
        $functions = array('mb_strtolower', 'mb_strtoupper', 'ucfirst', 'ucwords');

        foreach ($functions as $function)
        {
            if (call_user_func($function, $comparison) === $comparison)
            {
                return call_user_func($function, $value);
            }
        }

        return $value;
    }

}
