using System;
using System.Collections.Generic;
using System.IO;
using ProtoBuf.Serializers;

namespace Boggle
{
    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<string, Dictionary<string, ListWord>> _LoadedDictionaries;
        
        public BoggleList()
        {
            //Initialize the dictionary containing the lists.
            _LoadedDictionaries = new Dictionary<string, Dictionary<string, ListWord>>(Enum.GetNames(typeof(SupportedLists)).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 void LoadList(SupportedLists dict, int boardSideLength, int minWordLength, string currentWorkingDirectory)
        {
            int maxWordLength = boardSideLength * boardSideLength + 1;
            string dictPath = this.GetSerializedDictPath(dict, minWordLength, maxWordLength, currentWorkingDirectory);

            if (_LoadedDictionaries.ContainsKey(dictPath))
            {
                _WordList = _LoadedDictionaries[dictPath];
                return;
            }

            //Load the seriazlized dictionary if it exists on the disk.
            if (SerializedDictExists(dictPath))
            {
                _LoadedDictionaries.Add(dictPath, DeserializeDict(dictPath));
                _WordList = _LoadedDictionaries[dictPath];
                return;
            }

            _WordList = new Dictionary<string, ListWord>(27);
            string rawListPath = GetWordlistPath(dict);
            //true tells stream reader to auto detect encoding of the file.
            StreamReader rawList = new StreamReader(rawListPath, true);
            string word, substring;
            ListWord prevListWord;
            int startIndex;

            while ((word = rawList.ReadLine()) != null)
            {
                //no need to process the word if it cannot logically lie on the board.
                if (word.Length < minWordLength || word.Length > maxWordLength)
                    continue;
                //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(dictPath, _WordList);

            //Writes the dictionary just constructed to disk
            SerializeDict(_WordList, dictPath);
        }

        private string GetSerializedDictPath(SupportedLists list, int minWordLength, int maxWordLength, string currentWorkingDirectory)
        {
            string serializationFolder = string.Format("{0}/{1}", currentWorkingDirectory, Constants.SERIALIZATION_FOLDER);
            if (!Directory.Exists(serializationFolder))
            {
                Directory.CreateDirectory(serializationFolder);
            }
            string filename = string.Format("{0}/{1}.{2}.{3}.{4}", serializationFolder,
                                                                   list,
                                                                   minWordLength,
                                                                   maxWordLength,
                                                                   Constants.SERIALIZED_FILE_SUFFIX
                                            );
            //File creation is done by serialization function. No need to do it here. In case there is an error and we cannot
            //load the raw dictionary into our hybrid tree, we would be left with an empty and a useless file.
            return filename;
        }

        private bool SerializedDictExists(string serializedObjPath)
        {
            return File.Exists(serializedObjPath);
        }

        private void SerializeDict(Dictionary<string, ListWord> funkyDict, string serializationPath)
        {
            Stream serializationFile = File.Open(serializationPath, FileMode.CreateNew, FileAccess.Write);
            ProtoBuf.Serializer.Serialize <Dictionary<string, ListWord>>(serializationFile, funkyDict);
            serializationFile.Flush();
            serializationFile.Close();
        }

        private Dictionary<string, ListWord> DeserializeDict(string serializedObjPath)
        {
            Stream serializationFile = File.Open(serializedObjPath, FileMode.Open, FileAccess.Read);
            Dictionary<string, ListWord> funkyDict = ProtoBuf.Serializer.Deserialize<Dictionary<string, ListWord>>(serializationFile);
            serializationFile.Flush();
            serializationFile.Close();
            return funkyDict;
        }

        private string GetWordlistPath(SupportedLists list)
        {
            switch (list)
            {
                case SupportedLists.TWS:
                    return Constants.TWS_FILE_PATH;
                case SupportedLists.ZINGARELLI:
                    return Constants.ZINGARELLI_FILE_PATH;
                default:
                    throw new ArgumentException(string.Format("Support for {0} wordlist has not been coded yet :(", list));;
            }
        }
    }
}
