using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using CsvHelper;
using CsvHelper.Configuration;
using MathNet.Numerics.Statistics;
using NUnit.Framework;

namespace Core.Tests
{
    [TestFixture]
    public class WordleUtilTests
    {
        private static readonly char[] Alphabet = Enumerable.Range(0, 26).Select(r => Convert.ToChar('A' + r)).ToArray();
        

        [Test]
        public void GameTest()
        {
            var actual = "NAIVE";

            var dictionarySearcher = new DictionarySearcher(5);
            var frequency = new LetterPositionFrequency(dictionarySearcher, 5);
            dictionarySearcher.SortDictionary(frequency);

            var wordleState = new WordleUtil.WordleState();

            do
            {
                var results = 
                    dictionarySearcher
                        .FindWords(wordleState.InWord, wordleState.NotInWord, 5)
                        .Where(r => Regex.IsMatch(r, WordleUtil.GetRegexRestrictions(wordleState.Criteria)));

                //var guess = frequency.ScoreWords(results).OrderBy(f => f.Item2).Last().Item1;
                var guess = results.First();
                var response = WordleUtil.GetGuessResult(actual, guess);
                wordleState.Update(guess, response);
                
                Console.WriteLine(guess);

            } while (!wordleState.IsSolved());
        }


        [Test, Explicit]
        public void GameTest_NaiveBayes_WriteOutput()
        {
            var wordleUtil = new WordleUtil();
            var dictionarySearcher = new DictionarySearcher(5);
            var wordFrequency = new WordFrequency();
            var actual = "NAIVE";


            var steps = 0;
            var wordleState = new WordleUtil.WordleState();
            do
            {
                var constrainedWords =
                    dictionarySearcher
                        .FindWords(wordleState.InWord, wordleState.NotInWord, 5)
                        .Where(r => Regex.IsMatch(r, WordleUtil.GetRegexRestrictions(wordleState.Criteria)))
                        .OrderByDescending(w => wordFrequency.GetWordFreq(w))
                        .ToList();

                if (constrainedWords.First() == "NAIVE")
                {
                    constrainedWords.RemoveAt(0);
                    constrainedWords.Add("NAIVE");
                }
                if (constrainedWords.Contains("WAIVE"))
                {
                    constrainedWords.Remove("WAIVE");
                    constrainedWords.Add("WAIVE");
                }
                var letterCountPercents = GetLetterCountPercents(constrainedWords);
                SetWordleConstraints(letterCountPercents, wordleState);

                var frequency = new LetterPositionFrequency(constrainedWords.ToList(), 5);
                var sortedWords = frequency.SortWords(constrainedWords, wordleUtil);
                

                var guess = sortedWords.First();

                var highestCounts =
                    string.Join("",
                        Enumerable.Range(0, 5).Select(i => letterCountPercents.Last()[i].OrderByDescending(s => s.Count).First().Letter));

                WriteLetterCountPercentsCsv(letterCountPercents, constrainedWords, @"o:\tmp\WordleFreq\" + $"{steps}-{guess}", new[] { highestCounts, guess });
                var response = WordleUtil.GetGuessResult(actual, guess);
                wordleState.Update(guess, response);

                Console.WriteLine(guess); 
                steps++;

            } while (!wordleState.IsSolved());
        }

        [Test, Explicit]
        public void GameTest_NaiveBayes()
        {
            var wordleUtil = new WordleUtil();
            var dictionarySearcher = new DictionarySearcher(5);
            var wordFrequency = new WordFrequency();
            var actual = "NAIVE";

            var steps = 0;
            var wordleState = new WordleUtil.WordleState();
            do
            {
                var constrainedWords =
                    dictionarySearcher
                        .FindWords(wordleState.InWord, wordleState.NotInWord, 5)
                        .Where(r => Regex.IsMatch(r, WordleUtil.GetRegexRestrictions(wordleState.Criteria)))
                        .OrderByDescending(w => wordFrequency.GetWordFreq(w))
                        .ToList();
                
                var frequency = new LetterPositionFrequency(constrainedWords.ToList(), 5);
                var sortedWords = frequency.SortWords(constrainedWords, wordleUtil);

                //sortedWords.Reverse();

                var guess = sortedWords.First();

                var response = WordleUtil.GetGuessResult(actual, guess);
                wordleState.Update(guess, response);

                Console.WriteLine(guess); 
            } while (!wordleState.IsSolved());
        }

        [Test, Explicit]
        public void GameTest_NaiveBayes_Reverse()
        {
            var wordleUtil = new WordleUtil();
            var dictionarySearcher = new DictionarySearcher(5);
            var wordFrequency = new WordFrequency();
            var actual = "NAIVE";


            var steps = 0;
            var wordleState = new WordleUtil.WordleState();
            do
            {
                var constrainedWords =
                    dictionarySearcher
                        .FindWords(wordleState.InWord, wordleState.NotInWord, 5)
                        .Where(r => Regex.IsMatch(r, WordleUtil.GetRegexRestrictions(wordleState.Criteria)))
                        .OrderByDescending(w => wordFrequency.GetWordFreq(w))
                        .ToList();

                if (constrainedWords.First() == "NAIVE")
                {
                    constrainedWords.RemoveAt(0);
                    constrainedWords.Add("NAIVE");
                }
                if (constrainedWords.Contains("WAIVE"))
                {
                    constrainedWords.Remove("WAIVE");
                    constrainedWords.Add("WAIVE");
                }
                var letterCountPercents = GetLetterCountPercents(constrainedWords);
                SetWordleConstraints(letterCountPercents, wordleState);

                var frequency = new LetterPositionFrequency(constrainedWords.ToList(), 5);
                var sortedWords = frequency.SortWords(constrainedWords, wordleUtil);
                sortedWords.Reverse();
                var bannedWords = new string[] { "TAINT", "AALII", "LAIGH", "SAIDS", "SAINS" };
                sortedWords = sortedWords.SkipWhile(bannedWords.Contains).ToList();

                var guess = sortedWords.First();

                var highestCounts =
                    string.Join("",
                        Enumerable.Range(0, 5).Select(i => letterCountPercents.Last()[i].OrderBy(s => s.Count).First().Letter));

                WriteLetterCountPercentsCsv(letterCountPercents, constrainedWords, @"o:\tmp\WordleFreqReverse\" + $"{steps}-{guess}", new[] { highestCounts, guess });
                var response = WordleUtil.GetGuessResult(actual, guess);
                wordleState.Update(guess, response);

                Console.WriteLine(guess);
                steps++;

            } while (!wordleState.IsSolved());
        }

        private static void SetWordleConstraints(List<LetterCountPercent[][]> letterCountPercents, WordleUtil.WordleState wordleState)
        {
            foreach (var letter in wordleState.NotInWord)
            {
                var k = letter - 'A';
                foreach (var letterCountPercent in letterCountPercents)
                {
                    for (int j = 0; j < 5; j++)
                        letterCountPercent[j][k].State = "N";
                }
            }

            for (var j = 0; j < wordleState.Criteria.Length; j++)
            {
                var criteria = wordleState.Criteria[j];
                if (criteria.Correct != null)
                {
                    var k = criteria.Correct.Value - 'A';
                    foreach (var letterCountPercent in letterCountPercents)
                    {
                        letterCountPercent[j][k].State = "C";
                    }
                }

                if (!string.IsNullOrEmpty(criteria.OtherPosition))
                {
                    foreach (var letter in criteria.OtherPosition)
                    {
                        var k = letter - 'A';
                        foreach (var letterCountPercent in letterCountPercents)
                        {
                            letterCountPercent[j][k].State = "P";
                        }
                    }
                }
            }
        }

        public class LetterCountPercent
        {
            public char Letter { get; }
            public int Count { get; }
            public float Percentage { get; }
            
            /// <summary>
            /// C: Correct, P: Present, N: Not Present, G: Guess, A: Active
            /// </summary>
            public string State { get; set; }

            public LetterCountPercent(char letter, int count, double percentage, string state)
            {
                Letter = letter;
                Count = count;
                Percentage = (float)percentage;
                State = state;
            }
        }


        [Test]
        public void NotInWordleList()
        {
            var dictionarySearcher = new DictionarySearcher(5);
            var allFiveLeterWords = dictionarySearcher.FindWords("", "", 5);
            var wordFrequency = new WordFrequency();
            var wordleUtil = new WordleUtil();
            var r =
                from w in allFiveLeterWords
                where !wordleUtil.Words.Contains(w)
                let f = wordFrequency.GetWordFreq(w)
                select Tuple.Create(w,f);

            foreach (var s in r.OrderByDescending(t => t.Item2).Take(20))
            {
                Console.WriteLine(s);
            }
        }

        [Test, Explicit]
        public void LetterFrequenceStatistics_NoConstraints()
        {
            var frequency = new WordFrequency();
            
            var wordleUtil = new WordleUtil();

            var words =  wordleUtil.Words.OrderByDescending(w => frequency.GetWordFreq(w)).ToList();

            //var dictionarySearcher = new DictionarySearcher(5);
            //var words =  dictionarySearcher.GetWords().OrderByDescending(w => frequency.GetWordFreq(w)).ToList();

            void MoveToFirst(string w)
            {
                words.Remove(w);
                words.Insert(0, w);
            }
            MoveToFirst("LATER");
            MoveToFirst("AFTER");
            MoveToFirst("FIRST");

            var results = GetLetterCountPercents(words);

            var highestCounts =
                string.Join("",
                Enumerable.Range(0, 5).Select(i => results.Last()[i].OrderByDescending(s => s.Count).First().Letter));
            
            WriteLetterCountPercentsCsv(results, words, @"o:\tmp\WordleFreq\NoConstraint", new []{ highestCounts, "SORES", "CARES"});
        }

        [Test, Explicit]
        public void LetterFrequenceStatistics_NoConstraints_Reverse()
        {
            var frequency = new WordFrequency();

            var wordleUtil = new WordleUtil();

            var words = wordleUtil.Words.OrderByDescending(w => frequency.GetWordFreq(w)).ToList();

            //var dictionarySearcher = new DictionarySearcher(5);
            //var words =  dictionarySearcher.GetWords().OrderByDescending(w => frequency.GetWordFreq(w)).ToList();

            void MoveToFirst(string w)
            {
                words.Remove(w);
                words.Insert(0, w);
            }
            MoveToFirst("LATER");
            MoveToFirst("AFTER");
            MoveToFirst("FIRST");

            var results = GetLetterCountPercents(words);

            var highestCounts =
                string.Join("",
                    Enumerable.Range(0, 5).Select(i => results.Last()[i].OrderByDescending(s => s.Count).Last().Letter));

            WriteLetterCountPercentsCsv(results, words, @"o:\tmp\WordleFreq\NoConstraint", new[] { highestCounts, "SORES", "CARES" });
        }

        private static List<LetterCountPercent[][]> GetLetterCountPercents(List<string> words)
        {
            var freqList = new Dictionary<char, int>[5];
            for (var i = 0; i < 5; i++)
                freqList[i] = Alphabet.ToDictionary(c => c, j => 0);

            var wordCount = 0;

            var results = new List<LetterCountPercent[][]> ();

            foreach (var word in words)
            {
                wordCount++;

                for (var wordIndex = 0; wordIndex < word.Length; wordIndex++)
                {
                    var c = word[wordIndex];
                    freqList[wordIndex][c]++;
                }

                // get percentage
                var freqPercents =
                    freqList.Select(
                            f => (
                                    from cFreqs in f
                                    select Tuple.Create(cFreqs.Key, cFreqs.Value * 1.0 / wordCount))
                                .ToDictionary(cp => cp.Item1, cp => cp.Item2))
                        .ToArray();

                // normalize
                var maxPercent = freqPercents.Max(fp => fp.Max(p => p.Value));
                foreach (var freqPercent in freqPercents)
                foreach (var c in freqPercent.Keys.ToArray())
                    freqPercent[c] /= maxPercent;

                var result =
                    (from letter in Enumerable.Range(0, 5)
                        let item =
                            (from freq in freqList[letter]
                                from freqPercent in freqPercents[letter]
                                where freq.Key == freqPercent.Key
                                let state = word[letter] == freq.Key ? "A" : null
                                select new LetterCountPercent(freq.Key, freq.Value, freqPercent.Value, state)).ToArray()
                        select item).ToArray();

                results.Add(result);
            }

            return results;
        }

        private static void WriteLetterCountPercentsCsv(IReadOnlyList<LetterCountPercent[][]> results, IReadOnlyList<string> words, string folder, IList<string> g = null)
        {
            // hack:
            IList<string> guesses = null;
            if (g != null)
            {
                guesses = new List<string>(g);
                guesses.Add(guesses.Last());
            }

            try
            {
                if (Directory.Exists(folder))
                    Directory.Delete(folder, true);
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
            }
            Directory.CreateDirectory(folder);
            
            using (var writer = new StreamWriter(Path.Combine(folder, "freq_words.csv")))
            {
                writer.WriteLine("word");

                writer.WriteLine("     ");
                writer.WriteLine("     ");
                
                for (int i = 0; i < words.Count; i++)
                {
                    writer.WriteLine(words[i]);
                }

                writer.WriteLine("     ");
                writer.WriteLine("     ");

                for (int i = 0; i < (guesses?.Count ?? 0); i++)
                {
                    writer.WriteLine(guesses[i]);
                }
            }

            for (int j = 0; j < 5; j++)
            {
                using (var writer = new StreamWriter(Path.Combine(folder, "freq_letterCounts_" + j + ".csv")))
                {
                    writer.WriteLine(string.Join(",", Alphabet));
                    writer.WriteLine(string.Join(",", results[0][j].Select(lcp => 0)));
                    //writer.WriteLine(string.Join(",", results[0][j].Select(lcp => 0)));
                    for (var index = 0; index < results.Count + 3 + (guesses?.Count ?? 0); index++)
                    {
                        var indexX = index < results.Count ? index : results.Count - 1;
                        writer.WriteLine(string.Join(",", results[indexX][j].Select(lcp => lcp.Count)));
                    }
                }
            }

            for (int j = 0; j < 5; j++)
            {
                using (var writer = new StreamWriter(Path.Combine(folder, "freq_letterPercents_" + j + ".csv")))
                {
                    writer.WriteLine(string.Join(",", Alphabet));
                    writer.WriteLine(string.Join(",", results[0][j].Select(lcp => 0)));
                    writer.WriteLine(string.Join(",", results[0][j].Select(lcp => 0)));
                    for (var index = 0; index < results.Count + 2 + (guesses?.Count ?? 0); index++)
                    {
                        var indexX = index < results.Count ? index : results.Count - 1;
                        writer.WriteLine(string.Join(",", results[indexX][j].Select(lcp => lcp.Percentage)));
                    }
                }
            }

            for (int j = 0; j < 5; j++)
            {
                using (var writer = new StreamWriter(Path.Combine(folder, "freq_letterState_" + j + ".csv")))
                {
                    writer.WriteLine(string.Join(",", Alphabet));
                    writer.WriteLine(string.Join(",", results[0][j].Select(lcp => lcp.State == "A" || string.IsNullOrEmpty(lcp.State) ? " " : lcp.State)));
                    writer.WriteLine(string.Join(",", results[0][j].Select(lcp => lcp.State == "A" || string.IsNullOrEmpty(lcp.State) ? " " : lcp.State)));
                    
                    
                    for (var index = 0; index < results.Count; index++)
                    {
                        writer.WriteLine(string.Join(",", results[index][j].Select(lcp => string.IsNullOrEmpty(lcp.State) ? " " : lcp.State)));
                    }

                    writer.WriteLine(string.Join(",", results[results.Count - 1][j].Select(lcp => lcp.State == "A" || string.IsNullOrEmpty(lcp.State) ? " " : lcp.State)));
                    writer.WriteLine(string.Join(",", results[results.Count - 1][j].Select(lcp => lcp.State == "A" || string.IsNullOrEmpty(lcp.State) ? " " : lcp.State)));


                    for (int e = 0; e < (guesses?.Count ?? 0); e++)
                    {
                        var letter = guesses[e][j];
                        writer.WriteLine(string.Join(",", results[results.Count - 1][j].Select(lcp => lcp.Letter == letter ? "G" : lcp.State == "A" || string.IsNullOrEmpty(lcp.State) ? " " : lcp.State)));
                    }
                }
            }
        }

        [Test]
        public void NotInDictionary()
        {
            var dictionarySearcher = new DictionarySearcher(5);
            var allFiveLeterWords = dictionarySearcher.FindWords("", "", 5);
            var wordFrequency = new WordFrequency();
            var wordleUtil = new WordleUtil();
            var r =
                from w in wordleUtil.Words
                where !allFiveLeterWords.Contains(w)
                let f = wordFrequency.GetWordFreq(w)
                select Tuple.Create(w, f);

            foreach (var s in r.OrderByDescending(t => t.Item2).Take(20))
            {
                Console.WriteLine(s);
            }
        }

        [Test, Explicit]
        public void WordleStats()
        {
            var actual = "GROUP";

            var dictionarySearcher = new DictionarySearcher(5);
            var allFiveLeterWords = dictionarySearcher.FindWords("", "");
            var wordleUtil = new WordleUtil();
            var frequency = new LetterPositionFrequency(wordleUtil, 5);
            dictionarySearcher.SortDictionary(frequency);
             
            // Console.WriteLine(dictionarySearcher.FindWords("", "", 5).Count());

            var solutionSteps = new List<double>();
            Parallel.ForEach(allFiveLeterWords, nextWord =>
            {
                var guess = nextWord;
                var response = WordleUtil.GetGuessResult(actual, guess);

                var wordleState = new WordleUtil.WordleState();
                wordleState.Update(guess, response);

                //Console.WriteLine("----");
                //Console.WriteLine(guess);

                var steps = 1;
                while (!wordleState.IsSolved())
                {
                    var results =
                        dictionarySearcher
                            .FindWords(wordleState.InWord, wordleState.NotInWord, 5)
                            .Where(r => Regex.IsMatch(r, WordleUtil.GetRegexRestrictions(wordleState.Criteria)));
                    guess = results.First();
                    //guess = frequency.ScoreWords(results).OrderBy(f => f.Item2).Last().Item1;
                    response = WordleUtil.GetGuessResult(actual, guess);
                    wordleState.Update(guess, response);
                    //Console.WriteLine(guess);
                    steps++;
                }
                solutionSteps.Add(steps);
            });
            //Console.WriteLine(string.Join(",", solutionSteps));
            Console.WriteLine($"Mean: {solutionSteps.Mean()}, StdDev: {solutionSteps.StandardDeviation()}");
            //Console.WriteLine(solutionSteps.Sum() * 1d / solutionSteps.Count );
        }


        [Test]
        public void RepeatedLettersStatistics()
        {
            var wordleUtil = new WordleUtil();
            var re =
                from w in wordleUtil.Words
                let repeats = WordleUtil.RepeatedLettersCount(w)
                group repeats by repeats
                into g
                select g;
            var max = re.Max(r => r.Count());
            Console.WriteLine(string.Join(", ", re.Select(r => r.Key + ": " + r.Count() +  $" {(r.Count() * 1.0) / max}")));
            Console.WriteLine(string.Join(", ", wordleUtil.Words.Where(w => WordleUtil.RepeatedLettersCount(w) == 3)));
            Console.WriteLine(string.Join(", ", wordleUtil.Words.Where(w => WordleUtil.RepeatedLettersCount(w) == 2)));
            Console.WriteLine(string.Join(", ", wordleUtil.Words.Where(w => WordleUtil.RepeatedLettersCount(w) == 1)));
        }


        [Test]
        public void SolveAllWordleWords_SortedDictionary()
        {
            var dictionarySearcher = new DictionarySearcher(5);
            var wordleUtil = new WordleUtil();
            var frequency = new LetterPositionFrequency(wordleUtil, 5);
            dictionarySearcher.SortDictionary(frequency);

            var solutionSteps = new List<double>();
            Parallel.ForEach(wordleUtil.Words, actual =>
            {
                var steps = 0;
                var wordleState = new WordleUtil.WordleState();
                do
                {
                    var results =
                        dictionarySearcher
                            .FindWords(wordleState.InWord, wordleState.NotInWord, 5)
                            .Where(r => Regex.IsMatch(r, WordleUtil.GetRegexRestrictions(wordleState.Criteria)));

                    if (steps <= 1)
                        results = results.SkipWhile(WordleUtil.RepeatedLetters);

                    var guess = results.First();
                    var response = WordleUtil.GetGuessResult(actual, guess);
                    wordleState.Update(guess, response);

                    //Console.WriteLine(guess);
                    steps++;

                } while (!wordleState.IsSolved());
                solutionSteps.Add(steps);
            });
            
            Console.WriteLine($"Mean: {solutionSteps.Mean()}, StdDev: {solutionSteps.StandardDeviation()}");
        }


        [Test]
        public void SolveAllWordleWords_NaiveBayes()
        {
            var dictionarySearcher = new DictionarySearcher(5);
            var wordleUtil = new WordleUtil();
            var solutionSteps = new List<double>();

            Parallel.ForEach(wordleUtil.Words, actual =>
            {
                var steps = 0;
                var wordleState = new WordleUtil.WordleState();
                do
                {
                    var results =
                        dictionarySearcher
                            .FindWords(wordleState.InWord, wordleState.NotInWord, 5)
                            .Where(r => Regex.IsMatch(r, WordleUtil.GetRegexRestrictions(wordleState.Criteria)))
                            .ToList();

                    var frequency = new LetterPositionFrequency(results, 5);
                    results = frequency.SortWords(results, wordleUtil);

                    var guess = results.First();
                    var response = WordleUtil.GetGuessResult(actual, guess);
                    wordleState.Update(guess, response);

                    //Console.WriteLine(guess);
                    steps++;

                } while (!wordleState.IsSolved());
                solutionSteps.Add(steps);
            });

            Console.WriteLine($"Mean: {solutionSteps.Mean()}, StdDev: {solutionSteps.StandardDeviation()}");
        }
    }
}