using System;
using System.Collections.Generic;
using System.IO;

namespace Boggle
{
    /// <summary>
    /// Supported lists.
    /// </summary>
    public enum BoggleLists
    {
        /// <summary>
        /// Scrabble dictionary for USA, Canada and Thailand
        /// </summary>
        TWS,

        /// <summary>
        /// Scrabble dictionary used world wide except in USA, Canada and Thailand.
        /// </summary>
        //SOWPODS,

        /// <summary>
        /// A huge dictionary found on official Internet Scrabble Club's website (http://www.isc.ro/en/commands/lists.html)
        /// </summary>
        ZINGARELLI
    }

    public class BoggleList
    {
        /// <summary>
        /// Contains a hybrid tree that has been optimized for recursive search.
        /// </summary>
        private Dictionary<string, ListWord> _WordList = new Dictionary<string, ListWord>(27); //top level will store only 3 letter words or substrings. Declaring proper size makes insertion operation O(1)
        public Dictionary<string, ListWord> Wordlist
        {
            get
            {
                return _WordList;
            }
        }

        /// <summary>
        /// Contains a list of dictionaries that have been loaded, indexed by enum that contains dictionaries we support.
        /// </summary>
        private Dictionary<BoggleLists, Dictionary<string, ListWord>> _LoadedDictionaries;

        /// <summary>
        /// Determines if we have an absolute path
        /// </summary>
        /// <param name="filePath">File that contains the list SORTED list of words. Path can be relative or absolute.</param>
        /// <returns>The absolute file path that we can use to access it.</returns>
        private string GetAbsoluteFilePath(string filePath)
        {
            //check if the absolute file path exists.            
            if (!File.Exists(filePath))
            {
                //try checking if this is a relative file path and prepend current directory's path.
                filePath = string.Format("{0}\\{1}", Directory.GetCurrentDirectory(), filePath);
                if (!File.Exists(filePath))
                {
                    filePath = string.Empty;
                }
            }

            return filePath;
        }

        /// <summary>
        /// Curates the list for use by this solver. Basically culls out words that do not meet the criteria.
        /// Criteria: Min_word_length < word_length < Max_word_length. Max_word_length = sideLength^2 + 1.
        /// </summary>
        /// <param name="filePath"> Path to file that contains the SORTED list of words. 
        ///                         It can be relative to current execution directory or absolute.</param>
        /// <param name="boardSideLength">Length of the side of board in number of tiles.</param>
        /// <param name="minWordLength">Number of alphabets that can be there in the shortest word on the board.</param>
        private void CurateList(string filePath, int boardSideLength, int minWordLength)
        {
            //filePath = GetAbsoluteFilePath(filePath);
            //true tells stream reader to auto detect encoding of the file.
            StreamReader rawList = new StreamReader(filePath, true);
            //Create a new file. Do not append anything.
            string curatedFilePath = string.Format("{0}.{1}.{2}", filePath, Constants.CURATED_FILE_SUFFIX, minWordLength.ToString());
            StreamWriter curatedList = new StreamWriter(curatedFilePath, false);

            string word;
            //write out the words whose number of characters are between the shortest and largest provided length.
            while ((word = rawList.ReadLine()) != null)
            {
                if (word.Length >= minWordLength && word.Length <= boardSideLength * boardSideLength + 1)
                {
                    curatedList.WriteLine(word);
                }
            }

            curatedList.Flush();
            curatedList.Close();
            rawList.Close();
        }

        public BoggleList()
        {
            //Initialize the dictionary containing the lists.
            _LoadedDictionaries = new Dictionary<BoggleLists, Dictionary<string, ListWord>>(Enum.GetNames(typeof(BoggleLists)).Length);
        }

        /// <summary>
        /// Loads the list depending on the selected dictionary.
        /// </summary>
        /// <param name="dict">Type of dictionary to load.</param>
        /// <param name="boardSideLength"></param>
        /// <param name="minWordLength"></param>
        /// <returns>A hybird tree containing the words with each level containing one alphabet (or QU)</returns>
        public Dictionary<string, ListWord> LoadList(BoggleLists dict, int boardSideLength, int minWordLength, string listRootPath)
        {
            if (_LoadedDictionaries.ContainsKey(dict))
                return _LoadedDictionaries[dict];

            string filePath = string.Empty;

            if (NeedsCuration(dict, minWordLength, listRootPath, ref filePath))
                CurateList(filePath, boardSideLength, minWordLength);

            _WordList = new Dictionary<string, ListWord>(27);

            //true tells stream reader to auto detect encoding of the file.
            StreamReader rawList = new StreamReader(filePath, true);
            string word, substring;
            ListWord prevListWord;
            int startIndex;

            while ((word = rawList.ReadLine()) != null)
            {
                //First deal with the head node and then deal with following letters.
                startIndex = 0;
                word = word.ToUpper();
                substring = word.Substring(0, 1);
                //if the string starts with Q and there is a 'U' following it, then put QU on tile and increment starting index accordingly.
                if (substring == "Q" && word[1] == 'U')
                {
                    substring += "U";
                    startIndex++;
                }

                //If there is no space allocated for this letter at root level, allocate some.
                if (!_WordList.ContainsKey(substring))
                {
                    _WordList.Add(substring, new ListWord());
                }
                prevListWord = _WordList[substring];

                //fit the word in the tree structure
                for (int i = startIndex + 1; i < word.Length; i++)
                {
                    //We are dealing with one letter at each level
                    substring = word.Substring(i, 1);
                    //But if the letter is Q and there is a U following it, then treat this as one letter.
                    if (substring == "Q" && i + 1 < word.Length && word[i + 1] == 'U')
                    {
                        substring += "U";
                        //also increment the index as we have consumed the next letter.
                        i++;
                    }
                    //Has this combination of letters appeared before?
                    if (prevListWord.Next == null)
                    {
                        //if not, then make some space to store this combination
                        prevListWord.Next = new Dictionary<string, ListWord>(27);
                        //and store it.
                        prevListWord.Next.Add(substring, new ListWord());
                    }
                    //if it has,
                    else if (!prevListWord.Next.ContainsKey(substring))
                    {
                        //then just store it.
                        prevListWord.Next.Add(substring, new ListWord());
                    }

                    //Update the dictionary to point to current substring's last character/entry in the tree.
                    prevListWord = prevListWord.Next[substring];
                }
                //set the value of the leaf node to be true to point that we have completed the word.
                prevListWord.IsWord = true;
            }

            //Store it in the collection 
            _LoadedDictionaries.Add(dict, _WordList);
            return _WordList;
        }

        /// <summary>
        /// Checks to see if the proper file exists that contains the appropriate words.
        /// </summary>
        /// <param name="list">Type of list to check for.</param>
        /// <param name="minWordLength">Minimum number of LETTERS the word should contain. </param>
        /// <param name="filePath">Reference to path of file. If the list will need curation, it will store path to source list. 
        /// If list is there, it will store the path to the curated list.</param>
        /// <returns>Whether there is a need to run the curation procedure or not.</returns>
        private bool NeedsCuration(BoggleLists list, int minWordLength, string wordlistContainer, ref string filePath)
        {
            bool needsCuration = false;
            switch (list)
            {
                case BoggleLists.TWS:
                    filePath = GetAbsoluteFilePath(string.Format("{0}/{1}.{2}.{3}", filePath, Constants.TWS_FILE_PATH, Constants.CURATED_FILE_SUFFIX, minWordLength.ToString()));
                    if (filePath == string.Empty)
                    {
                        filePath = string.Format("{0}/{1}", wordlistContainer, Constants.TWS_FILE_PATH);
                        needsCuration = true;
                    }
                    break;
                case BoggleLists.ZINGARELLI:
                    filePath = GetAbsoluteFilePath(string.Format("{0}/{1}.{2}.{3}", filePath, Constants.ZINGARELLI_FILE_PATH, Constants.CURATED_FILE_SUFFIX, minWordLength.ToString()));
                    if (filePath == string.Empty)
                    {
                        filePath = string.Format("{0}/{1}", wordlistContainer, Constants.ZINGARELLI_FILE_PATH);
                        needsCuration = true;
                    }
                    break;
                default:
                    throw new ArgumentException(string.Format("Support for {0} wordlist has not been coded yet :(", list));
            }

            return needsCuration;
        }
    }
}
