/**
 * Copyright 2013 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed under the Eclipse Public License version 1.0, available at
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Copied from: ModeShape (http://www.jboss.org/modeshape/)
 */
package org.jboss.forge.addon.text;

import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.jboss.forge.furnace.container.simple.Service;
import org.jboss.forge.furnace.util.Assert;
import org.jboss.forge.furnace.util.Strings;

/**
 * Transforms words to singular, plural, humanized (human readable), underscore, camel case, or ordinal form. This is
 * inspired by the <a href="http://api.rubyonrails.org/classes/Inflector.html">Inflector</a> class in <a
 * href="http://www.rubyonrails.org">Ruby on Rails</a>, which is distributed under the <a
 * href="http://wiki.rubyonrails.org/rails/pages/License">Rails license</a>.
 */
public class Inflector implements Service
{
   public Inflector()
   {
      initialize();
   }

   protected class Rule
   {
      protected final String expression;
      protected final Pattern expressionPattern;
      protected final String replacement;

      protected Rule(String expression, String replacement)
      {
         this.expression = expression;
         this.replacement = replacement != null ? replacement : "";
         this.expressionPattern = Pattern.compile(this.expression, Pattern.CASE_INSENSITIVE);
      }

      /**
       * Apply the rule against the input string, returning the modified string or null if the rule didn't apply (and no
       * modifications were made)
       * 
       * @param input the input string
       * @return the modified string if this rule applied, or null if the input was not modified by this rule
       */
      protected String apply(String input)
      {
         Matcher matcher = this.expressionPattern.matcher(input);
         if (!matcher.find())
            return null;
         return matcher.replaceAll(this.replacement);
      }

      @Override
      public int hashCode()
      {
         return expression.hashCode();
      }

      @Override
      public boolean equals(Object obj)
      {
         if (obj == this)
            return true;
         if (obj != null && obj.getClass() == this.getClass())
         {
            final Rule that = (Rule) obj;
            if (this.expression.equalsIgnoreCase(that.expression))
               return true;
         }
         return false;
      }

      @Override
      public String toString()
      {
         return expression + ", " + replacement;
      }
   }

   private final LinkedList<Rule> plurals = new LinkedList<Rule>();
   private final LinkedList<Rule> singulars = new LinkedList<Rule>();
   /**
    * The lowercase words that are to be excluded and not processed. This map can be modified by the users via
    * {@link #getUncountables()}.
    */
   private final Set<String> uncountables = new HashSet<String>();

   protected Inflector(Inflector original)
   {
      this.plurals.addAll(original.plurals);
      this.singulars.addAll(original.singulars);
      this.uncountables.addAll(original.uncountables);
   }

   @Override
   public Inflector clone()
   {
      return new Inflector(this);
   }

   // ------------------------------------------------------------------------------------------------
   // Usage functions
   // ------------------------------------------------------------------------------------------------

   /**
    * Returns the plural form of the word in the string.
    * <p>
    * Examples:
    * 
    * <pre>
    *   inflector.pluralize(&quot;post&quot;)               #=&gt; &quot;posts&quot;
    *   inflector.pluralize(&quot;octopus&quot;)            #=&gt; &quot;octopi&quot;
    *   inflector.pluralize(&quot;sheep&quot;)              #=&gt; &quot;sheep&quot;
    *   inflector.pluralize(&quot;words&quot;)              #=&gt; &quot;words&quot;
    *   inflector.pluralize(&quot;the blue mailman&quot;)   #=&gt; &quot;the blue mailmen&quot;
    *   inflector.pluralize(&quot;CamelOctopus&quot;)       #=&gt; &quot;CamelOctopi&quot;
    * </pre>
    * 
    * </p>
    * <p>
    * Note that if the {@link Object#toString()} is called on the supplied object, so this method works for non-strings,
    * too.
    * </p>
    * 
    * @param word the word that is to be pluralized.
    * @return the pluralized form of the word, or the word itself if it could not be pluralized
    * @see #singularize(Object)
    */
   public String pluralize(Object word)
   {
      if (word == null)
         return null;
      String wordStr = word.toString().trim();
      if (wordStr.length() == 0)
         return wordStr;
      if (isUncountable(wordStr))
         return wordStr;
      for (Rule rule : this.plurals)
      {
         String result = rule.apply(wordStr);
         if (result != null)
            return result;
      }
      return wordStr;
   }

   public String pluralize(Object word,
            int count)
   {
      if (word == null)
         return null;
      if (count == 1 || count == -1)
      {
         return word.toString();
      }
      return pluralize(word);
   }

   /**
    * Returns the singular form of the word in the string.
    * <p>
    * Examples:
    * 
    * <pre>
    *   inflector.singularize(&quot;posts&quot;)             #=&gt; &quot;post&quot;
    *   inflector.singularize(&quot;octopi&quot;)            #=&gt; &quot;octopus&quot;
    *   inflector.singularize(&quot;sheep&quot;)             #=&gt; &quot;sheep&quot;
    *   inflector.singularize(&quot;words&quot;)             #=&gt; &quot;word&quot;
    *   inflector.singularize(&quot;the blue mailmen&quot;)  #=&gt; &quot;the blue mailman&quot;
    *   inflector.singularize(&quot;CamelOctopi&quot;)       #=&gt; &quot;CamelOctopus&quot;
    * </pre>
    * 
    * </p>
    * <p>
    * Note that if the {@link Object#toString()} is called on the supplied object, so this method works for non-strings,
    * too.
    * </p>
    * 
    * @param word the word that is to be pluralized.
    * @return the pluralized form of the word, or the word itself if it could not be pluralized
    * @see #pluralize(Object)
    */
   public String singularize(Object word)
   {
      if (word == null)
         return null;
      String wordStr = word.toString().trim();
      if (wordStr.length() == 0)
         return wordStr;
      if (isUncountable(wordStr))
         return wordStr;
      for (Rule rule : this.singulars)
      {
         String result = rule.apply(wordStr);
         if (result != null)
            return result;
      }
      return wordStr;
   }

   /**
    * Converts strings to lowerCamelCase. This method will also use any extra delimiter characters to identify word
    * boundaries.
    * <p>
    * Examples:
    * 
    * <pre>
    *   inflector.lowerCamelCase(&quot;active_record&quot;)       #=&gt; &quot;activeRecord&quot;
    *   inflector.lowerCamelCase(&quot;first_name&quot;)          #=&gt; &quot;firstName&quot;
    *   inflector.lowerCamelCase(&quot;name&quot;)                #=&gt; &quot;name&quot;
    *   inflector.lowerCamelCase(&quot;the-first_name&quot;,'-')  #=&gt; &quot;theFirstName&quot;
    * </pre>
    * 
    * </p>
    * 
    * @param lowerCaseAndUnderscoredWord the word that is to be converted to camel case
    * @param delimiterChars optional characters that are used to delimit word boundaries
    * @return the lower camel case version of the word
    * @see #underscore(String, char[])
    * @see #camelCase(String, boolean, char[])
    * @see #upperCamelCase(String, char[])
    */
   public String lowerCamelCase(String lowerCaseAndUnderscoredWord,
            char... delimiterChars)
   {
      return camelCase(lowerCaseAndUnderscoredWord, false, delimiterChars);
   }

   /**
    * Converts strings to UpperCamelCase. This method will also use any extra delimiter characters to identify word
    * boundaries.
    * <p>
    * Examples:
    * 
    * <pre>
    *   inflector.upperCamelCase(&quot;active_record&quot;)       #=&gt; &quot;SctiveRecord&quot;
    *   inflector.upperCamelCase(&quot;first_name&quot;)          #=&gt; &quot;FirstName&quot;
    *   inflector.upperCamelCase(&quot;name&quot;)                #=&gt; &quot;Name&quot;
    *   inflector.lowerCamelCase(&quot;the-first_name&quot;,'-')  #=&gt; &quot;TheFirstName&quot;
    * </pre>
    * 
    * </p>
    * 
    * @param lowerCaseAndUnderscoredWord the word that is to be converted to camel case
    * @param delimiterChars optional characters that are used to delimit word boundaries
    * @return the upper camel case version of the word
    * @see #underscore(String, char[])
    * @see #camelCase(String, boolean, char[])
    * @see #lowerCamelCase(String, char[])
    */
   public String upperCamelCase(String lowerCaseAndUnderscoredWord,
            char... delimiterChars)
   {
      return camelCase(lowerCaseAndUnderscoredWord, true, delimiterChars);
   }

   /**
    * By default, this method converts strings to UpperCamelCase. If the <code>uppercaseFirstLetter</code> argument to
    * false, then this method produces lowerCamelCase. This method will also use any extra delimiter characters to
    * identify word boundaries.
    * <p>
    * Examples:
    * 
    * <pre>
    *   inflector.camelCase(&quot;active_record&quot;,false)    #=&gt; &quot;activeRecord&quot;
    *   inflector.camelCase(&quot;active_record&quot;,true)     #=&gt; &quot;ActiveRecord&quot;
    *   inflector.camelCase(&quot;first_name&quot;,false)       #=&gt; &quot;firstName&quot;
    *   inflector.camelCase(&quot;first_name&quot;,true)        #=&gt; &quot;FirstName&quot;
    *   inflector.camelCase(&quot;name&quot;,false)             #=&gt; &quot;name&quot;
    *   inflector.camelCase(&quot;name&quot;,true)              #=&gt; &quot;Name&quot;
    * </pre>
    * 
    * </p>
    * 
    * @param lowerCaseAndUnderscoredWord the word that is to be converted to camel case
    * @param uppercaseFirstLetter true if the first character is to be uppercased, or false if the first character is to
    *           be lowercased
    * @param delimiterChars optional characters that are used to delimit word boundaries
    * @return the camel case version of the word
    * @see #underscore(String, char[])
    * @see #upperCamelCase(String, char[])
    * @see #lowerCamelCase(String, char[])
    */
   public String camelCase(String lowerCaseAndUnderscoredWord,
            boolean uppercaseFirstLetter,
            char... delimiterChars)
   {
      if (lowerCaseAndUnderscoredWord == null)
         return null;
      lowerCaseAndUnderscoredWord = lowerCaseAndUnderscoredWord.trim();
      if (lowerCaseAndUnderscoredWord.length() == 0)
         return "";
      if (uppercaseFirstLetter)
      {
         String result = lowerCaseAndUnderscoredWord;
         // Replace any extra delimiters with underscores (before the underscores are converted in the next step)...
         if (delimiterChars != null)
         {
            for (char delimiterChar : delimiterChars)
            {
               result = result.replace(delimiterChar, '_');
            }
         }

         // Change the case at the beginning at after each underscore ...
         return replaceAllWithUppercase(result, "(^|_)(.)", 2);
      }
      if (lowerCaseAndUnderscoredWord.length() < 2)
         return lowerCaseAndUnderscoredWord;
      return "" + Character.toLowerCase(lowerCaseAndUnderscoredWord.charAt(0))
               + camelCase(lowerCaseAndUnderscoredWord, true, delimiterChars).substring(1);
   }

   /**
    * Makes an underscored form from the expression in the string (the reverse of the
    * {@link #camelCase(String, boolean, char[]) camelCase} method. Also changes any characters that match the supplied
    * delimiters into underscore.
    * <p>
    * Examples:
    * 
    * <pre>
    *   inflector.underscore(&quot;activeRecord&quot;)     #=&gt; &quot;active_record&quot;
    *   inflector.underscore(&quot;ActiveRecord&quot;)     #=&gt; &quot;active_record&quot;
    *   inflector.underscore(&quot;firstName&quot;)        #=&gt; &quot;first_name&quot;
    *   inflector.underscore(&quot;FirstName&quot;)        #=&gt; &quot;first_name&quot;
    *   inflector.underscore(&quot;name&quot;)             #=&gt; &quot;name&quot;
    *   inflector.underscore(&quot;The.firstName&quot;)    #=&gt; &quot;the_first_name&quot;
    * </pre>
    * 
    * </p>
    * 
    * @param camelCaseWord the camel-cased word that is to be converted;
    * @param delimiterChars optional characters that are used to delimit word boundaries (beyond capitalization)
    * @return a lower-cased version of the input, with separate words delimited by the underscore character.
    */
   public String underscore(String camelCaseWord,
            char... delimiterChars)
   {
      if (camelCaseWord == null)
         return null;
      String result = camelCaseWord.trim();
      if (result.length() == 0)
         return "";
      result = result.replaceAll("([A-Z]+)([A-Z][a-z])", "$1_$2");
      result = result.replaceAll("([a-z\\d])([A-Z])", "$1_$2");
      result = result.replace('-', '_');
      if (delimiterChars != null)
      {
         for (char delimiterChar : delimiterChars)
         {
            result = result.replace(delimiterChar, '_');
         }
      }
      return result.toLowerCase();
   }

   /**
    * Returns a copy of the input with the first character converted to uppercase and the remainder to lowercase.
    * 
    * @param words the word to be capitalized
    * @return the string with the first character capitalized and the remaining characters lowercased
    */
   public String capitalize(String words)
   {
      if (words == null)
         return null;
      String result = words.trim();
      if (result.length() == 0)
         return "";
      if (result.length() == 1)
         return result.toUpperCase();
      return "" + Character.toUpperCase(result.charAt(0)) + result.substring(1).toLowerCase();
   }

   /**
    * Capitalizes the first word and turns underscores into spaces and strips trailing "_id" and any supplied removable
    * tokens. Like {@link #titleCase(String, String[])}, this is meant for creating pretty output.
    * <p>
    * Examples:
    * 
    * <pre>
    *   inflector.humanize(&quot;employee_salary&quot;)       #=&gt; &quot;Employee salary&quot;
    *   inflector.humanize(&quot;author_id&quot;)             #=&gt; &quot;Author&quot;
    * </pre>
    * 
    * </p>
    * 
    * @param lowerCaseAndUnderscoredWords the input to be humanized
    * @param removableTokens optional array of tokens that are to be removed
    * @return the humanized string
    * @see #titleCase(String, String[])
    */
   public String humanize(String lowerCaseAndUnderscoredWords,
            String... removableTokens)
   {
      if (lowerCaseAndUnderscoredWords == null)
         return null;
      String result = lowerCaseAndUnderscoredWords.trim();
      if (result.length() == 0)
         return "";
      // Remove a trailing "_id" token
      result = result.replaceAll("_id$", "");
      // Remove all of the tokens that should be removed
      if (removableTokens != null)
      {
         for (String removableToken : removableTokens)
         {
            result = result.replaceAll(removableToken, "");
         }
      }
      result = result.replaceAll("_+", " "); // replace all adjacent underscores with a single space
      return capitalize(result);
   }

   /**
    * Capitalizes all the words and replaces some characters in the string to create a nicer looking title. Underscores
    * are changed to spaces, a trailing "_id" is removed, and any of the supplied tokens are removed. Like
    * {@link #humanize(String, String[])}, this is meant for creating pretty output.
    * <p>
    * Examples:
    * 
    * <pre>
    *   inflector.titleCase(&quot;man from the boondocks&quot;)       #=&gt; &quot;Man From The Boondocks&quot;
    *   inflector.titleCase(&quot;x-men: the last stand&quot;)        #=&gt; &quot;X Men: The Last Stand&quot;
    * </pre>
    * 
    * </p>
    * 
    * @param words the input to be turned into title case
    * @param removableTokens optional array of tokens that are to be removed
    * @return the title-case version of the supplied words
    */
   public String titleCase(String words,
            String... removableTokens)
   {
      String result = humanize(words, removableTokens);
      result = replaceAllWithUppercase(result, "\\b([a-z])", 1); // change first char of each word to uppercase
      return result;
   }

   /**
    * Turns a non-negative number into an ordinal string used to denote the position in an ordered sequence, such as
    * 1st, 2nd, 3rd, 4th.
    * 
    * @param number the non-negative number
    * @return the string with the number and ordinal suffix
    */
   public String ordinalize(int number)
   {
      int remainder = number % 100;
      String numberStr = Integer.toString(number);
      if (11 <= number && number <= 13)
         return numberStr + "th";
      remainder = number % 10;
      if (remainder == 1)
         return numberStr + "st";
      if (remainder == 2)
         return numberStr + "nd";
      if (remainder == 3)
         return numberStr + "rd";
      return numberStr + "th";
   }

   // ------------------------------------------------------------------------------------------------
   // Management methods
   // ------------------------------------------------------------------------------------------------

   /**
    * Determine whether the supplied word is considered uncountable by the {@link #pluralize(Object) pluralize} and
    * {@link #singularize(Object) singularize} methods.
    * 
    * @param word the word
    * @return true if the plural and singular forms of the word are the same
    */
   public boolean isUncountable(String word)
   {
      if (word == null)
         return false;
      String trimmedLower = word.trim().toLowerCase();
      return this.uncountables.contains(trimmedLower);
   }

   /**
    * Get the set of words that are not processed by the Inflector. The resulting map is directly modifiable.
    * 
    * @return the set of uncountable words
    */
   public Set<String> getUncountables()
   {
      return uncountables;
   }

   public void addPluralize(String rule,
            String replacement)
   {
      final Rule pluralizeRule = new Rule(rule, replacement);
      this.plurals.addFirst(pluralizeRule);
   }

   public void addSingularize(String rule,
            String replacement)
   {
      final Rule singularizeRule = new Rule(rule, replacement);
      this.singulars.addFirst(singularizeRule);
   }

   public void addIrregular(String singular,
            String plural)
   {
      Assert.isTrue(!Strings.isNullOrEmpty(singular), "singular rule");
      Assert.isTrue(!Strings.isNullOrEmpty(plural), "plural rule");
      String singularRemainder = singular.length() > 1 ? singular.substring(1) : "";
      String pluralRemainder = plural.length() > 1 ? plural.substring(1) : "";
      addPluralize("(" + singular.charAt(0) + ")" + singularRemainder + "$", "$1" + pluralRemainder);
      addSingularize("(" + plural.charAt(0) + ")" + pluralRemainder + "$", "$1" + singularRemainder);
   }

   public void addUncountable(String... words)
   {
      if (words == null || words.length == 0)
         return;
      for (String word : words)
      {
         if (word != null)
            uncountables.add(word.trim().toLowerCase());
      }
   }

   /**
    * Utility method to replace all occurrences given by the specific backreference with its uppercased form, and remove
    * all other backreferences.
    * <p>
    * The Java {@link Pattern regular expression processing} does not use the preprocessing directives <code>\l</code>,
    * <code>&#92;u</code>, <code>\L</code>, and <code>\U</code>. If so, such directives could be used in the replacement
    * string to uppercase or lowercase the backreferences. For example, <code>\L1</code> would lowercase the first
    * backreference, and <code>&#92;u3</code> would uppercase the 3rd backreference.
    * </p>
    * 
    * @param input
    * @param regex
    * @param groupNumberToUppercase
    * @return the input string with the appropriate characters converted to upper-case
    */
   String replaceAllWithUppercase(String input,
            String regex,
            int groupNumberToUppercase)
   {
      Pattern underscoreAndDotPattern = Pattern.compile(regex);
      Matcher matcher = underscoreAndDotPattern.matcher(input);
      StringBuffer sb = new StringBuffer();
      while (matcher.find())
      {
         matcher.appendReplacement(sb, matcher.group(groupNumberToUppercase).toUpperCase());
      }
      matcher.appendTail(sb);
      return sb.toString();
   }

   /**
    * Completely remove all rules within this inflector.
    */
   public void clear()
   {
      this.uncountables.clear();
      this.plurals.clear();
      this.singulars.clear();
   }

   protected void initialize()
   {
      Inflector inflect = this;
      inflect.addPluralize("$", "s");
      inflect.addPluralize("s$", "s");
      inflect.addPluralize("(ax|test)is$", "$1es");
      inflect.addPluralize("(octop|vir)us$", "$1i");
      inflect.addPluralize("(octop|vir)i$", "$1i"); // already plural
      inflect.addPluralize("(alias|status)$", "$1es");
      inflect.addPluralize("(bu)s$", "$1ses");
      inflect.addPluralize("(buffal|tomat)o$", "$1oes");
      inflect.addPluralize("([ti])um$", "$1a");
      inflect.addPluralize("([ti])a$", "$1a"); // already plural
      inflect.addPluralize("sis$", "ses");
      inflect.addPluralize("(?:([^f])fe|([lr])f)$", "$1$2ves");
      inflect.addPluralize("(hive)$", "$1s");
      inflect.addPluralize("([^aeiouy]|qu)y$", "$1ies");
      inflect.addPluralize("(x|ch|ss|sh)$", "$1es");
      inflect.addPluralize("(matr|vert|ind)ix|ex$", "$1ices");
      inflect.addPluralize("([m|l])ouse$", "$1ice");
      inflect.addPluralize("([m|l])ice$", "$1ice");
      inflect.addPluralize("^(ox)$", "$1en");
      inflect.addPluralize("(quiz)$", "$1zes");
      // Need to check for the following words that are already pluralized:
      inflect.addPluralize("(people|men|children|sexes|moves|stadiums)$", "$1"); // irregulars
      inflect.addPluralize("(oxen|octopi|viri|aliases|quizzes)$", "$1"); // special rules

      inflect.addSingularize("s$", "");
      inflect.addSingularize("(s|si|u)s$", "$1s"); // '-us' and '-ss' are already singular
      inflect.addSingularize("(n)ews$", "$1ews");
      inflect.addSingularize("([ti])a$", "$1um");
      inflect.addSingularize("((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$", "$1$2sis");
      inflect.addSingularize("(^analy)ses$", "$1sis");
      inflect.addSingularize("(^analy)sis$", "$1sis"); // already singular, but ends in 's'
      inflect.addSingularize("([^f])ves$", "$1fe");
      inflect.addSingularize("(hive)s$", "$1");
      inflect.addSingularize("(tive)s$", "$1");
      inflect.addSingularize("([lr])ves$", "$1f");
      inflect.addSingularize("([^aeiouy]|qu)ies$", "$1y");
      inflect.addSingularize("(s)eries$", "$1eries");
      inflect.addSingularize("(m)ovies$", "$1ovie");
      inflect.addSingularize("(x|ch|ss|sh)es$", "$1");
      inflect.addSingularize("([m|l])ice$", "$1ouse");
      inflect.addSingularize("(bus)es$", "$1");
      inflect.addSingularize("(o)es$", "$1");
      inflect.addSingularize("(shoe)s$", "$1");
      inflect.addSingularize("(cris|ax|test)is$", "$1is"); // already singular, but ends in 's'
      inflect.addSingularize("(cris|ax|test)es$", "$1is");
      inflect.addSingularize("(octop|vir)i$", "$1us");
      inflect.addSingularize("(octop|vir)us$", "$1us"); // already singular, but ends in 's'
      inflect.addSingularize("(alias|status)es$", "$1");
      inflect.addSingularize("(alias|status)$", "$1"); // already singular, but ends in 's'
      inflect.addSingularize("^(ox)en", "$1");
      inflect.addSingularize("(vert|ind)ices$", "$1ex");
      inflect.addSingularize("(matr)ices$", "$1ix");
      inflect.addSingularize("(quiz)zes$", "$1");

      inflect.addIrregular("person", "people");
      inflect.addIrregular("man", "men");
      inflect.addIrregular("child", "children");
      inflect.addIrregular("sex", "sexes");
      inflect.addIrregular("move", "moves");
      inflect.addIrregular("stadium", "stadiums");

      inflect.addUncountable("equipment", "information", "rice", "money", "species", "series", "fish", "sheep");
   }

}
