
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using NDepend.Attributes;


namespace NDepend.PowerTools.AppWords {

   // This non-trivial class is 100% unit-tested by Test_WordExtracter
   [FullCovered]
   static class WordExtracter {

      internal static IEnumerable<string> ExtractWordsFromIdentifier(string identifier) {
         Debug.Assert(identifier != null);

         // 
         var wordsToSplit = identifier.Split(new[] {
               '.', // Used in namespace identifier  "System.Collections"
               '_', // Often used in identifier, especially constant like "WM_CONTEXTMENU"
               '+'  // Used for nested types identfier like "System.Xaml.XamlNode+InternalNodeType"
            });

         var words = new List<string>();
         foreach (var wordToSplit in wordsToSplit) {
            var wordsSlitted = SplitIdentifierInWords(wordToSplit);
            var wordsNormalized = NormalizeWords(wordsSlitted);
            var wordsFiltered = FilterWords(wordsNormalized);
            // Notice: at the end of words listed you might see concatened words like 'Contextmenu'
            //         that typically comes from constants identifier like WM_CONTEXTMENU, that concatenate words.
            words.AddRange(wordsFiltered);
         }


         Debug.Assert(!words.Contains("Try"));
         return words;
      }





      //
      //  SplitIdentifierInWords
      //
      private static bool IsUpperOrDigit(char c) {
         return char.IsUpper(c) || char.IsDigit(c);
      }


      private static IEnumerable<string> SplitIdentifierInWords(string wordToSplit) {
         Debug.Assert(wordToSplit != null);
         // Anonymous namespace has an empty "" identifier  +  Don't keep single letter identifier
         if (wordToSplit.Length < 2) { return new string[0];  }

         var words = new List<string>();
         var sbCurrentWord = new StringBuilder();
         var chars = wordToSplit.ToCharArray();

         for (var i = 0; i < chars.Length; i++) {
            char currentChar = chars[i];
            if (i == chars.Length - 1) { goto TERMINATE_WORD; }

            char? previousChar = i > 0 ? chars[i - 1] : (char?)null;
            char nextChar = chars[i + 1];

            if (IsUpperOrDigit(currentChar) && previousChar != null) {
               if (IsUpperOrDigit(previousChar.Value) && char.IsLower(nextChar)) {
                  // Stop filling a Upper only word
                  goto START_NEW_WORD;
               }
               if (char.IsLower(previousChar.Value)) {
                  // Start a new word
                  goto START_NEW_WORD;
               }
            }
            sbCurrentWord.Append(currentChar);
            continue;

TERMINATE_WORD:
            sbCurrentWord.Append(currentChar);
            words.Add(sbCurrentWord.ToString());
            sbCurrentWord = new StringBuilder();
            continue;
START_NEW_WORD:
            words.Add(sbCurrentWord.ToString());
            sbCurrentWord = new StringBuilder();
            sbCurrentWord.Append(currentChar);
         }
         return words;
      }



      //
      //  NormalizeWords
      //
      private static readonly HashSet<string> s_WordsWithSAtAtEnd = new HashSet<string> {
         "Analysis", "Has", "Anonymous", "Class", "Process", "This", "Progress", "Status"
      };

      private static IEnumerable<string> NormalizeWords(IEnumerable<string> words) {
         var wordsNormalized = new List<string>();
         foreach (var word in words) {
            // '<' '>' '$' are used in identifiers generated by compilers.
            var wordNormalized = word.Replace("<", "");
            wordNormalized = wordNormalized.Replace(">", "");
            wordNormalized = wordNormalized.Replace("$", "");
            // '@' can be used to prefix an identifier
            wordNormalized = wordNormalized.Replace("@", "");

            wordNormalized = char.ToUpper(wordNormalized[0]) + wordNormalized.ToLower().Substring(1, wordNormalized.Length - 1);

            // Remove plural 'ies' of words with more than 3 chars!   Assemblies -> Assembly
            if (wordNormalized.LastIndexOf("ies") == wordNormalized.Length -3 && wordNormalized.Length > 3) {
               wordNormalized = wordNormalized.Substring(0, wordNormalized.Length - 3) + "y";
            }
            // Remove plural 's' of words with more than 2 chars!   Certificates -> Certificate
            if (wordNormalized[wordNormalized.Length - 1] == 's' && wordNormalized.Length > 2 && !s_WordsWithSAtAtEnd.Contains(wordNormalized)) {
               wordNormalized = wordNormalized.Substring(0, wordNormalized.Length - 1);
            }

            // Remove last digit char
            if (wordNormalized.Length > 1 && char.IsDigit(wordNormalized[wordNormalized.Length - 1]) && !char.IsDigit(wordNormalized[wordNormalized.Length - 2])) {
               wordNormalized = wordNormalized.Substring(0, wordNormalized.Length - 1);
            }

            // Filter empty and one char words
            if (wordNormalized.Length <= 1) { continue; }
            wordsNormalized.Add(wordNormalized);
         }
         return wordsNormalized;
      }




      //
      //  FilterWords
      //

      // Filter common programming word avoid noise in domain specific words
      private static readonly HashSet<string> s_CommonInfrastructureWordsToFilter = new HashSet<string> {
         "Get", "Set", "Ctor", "Cctor",
         "On", "Off", "In", "Out", "All", "Any", "None", "No", "Nb", "As", "Id", "Up", "Down", "Than", "Test",
         "Of", "Add", "Sub", "By", "Value", "For", "Do", "While", "During", "New", "Base", "Global", 
         "From", "Where", "Select", "Order", "Group", "Default", "When", "Sum", "Avg", 
         "Collection", "List", "Dictionary", "Hash", "Key", "Size", "Length", "Count", "Set", "Both",
         "Root", "Tree", "Child", "Children", "Parent", "Main", "Current", "Container",
         "Is", "Has", "Have", "Are", "To", "Tag", "Go", "Run", "Execute",
         "With", "Without","And", "Or", "Not", "Try",
         "Init",  "Initialize", "Empty", "Start", "Stop",
         "Core", "Kernel", "Util", "Helper", "Impl", 
         "Text", "String", "Name", "New", "Class", "Process", "This", "Null",
         "Result", "Element", "Object", "Int", "Long", "Short", "Bool", "Boolean",
         "Xml", "Sql", "View", "Ui", "Control", "Data", "Info", "Information",
         "Delegate", "Action", "Func", "Function", "Method",
         "Public", "Internal", "Protected", "Private",
         "Item", "Entity", "Element", "Elem", "Component",
         "Load", "Save", "Insert", "Delete", "Min", "Max", "Show", "Hide", "Column", "Row", 
         "Check", "Contract", "Event", "Param", "Active", "File", "Fill", "Cache", "Cached", "Valid", "Invalid", "Validate", 
      };

      private static IEnumerable<string> FilterWords(IEnumerable<string> words) {
         return words
            // Remove empty and one char word
            .Where(word => word.Length > 1)
            .Where(word => !s_CommonInfrastructureWordsToFilter.Contains(word)).ToList();
      }
   }
}
