using System;
using System.Collections.Generic;
using System.Linq;
using MinimumEditDistance;
using NHunspell;
using OpenCvSharp;
using YSoft.Rqa.Db.Context;
using YSoft.Rqa.Db.Model.EmbeddedDevice.Regions;

namespace OCR
{
    class Postprocessing
    {
        private static readonly RqaContext RqaContext = new RqaContext();
        private static readonly List<string> YSoftDictionary = RqaContext.FeatureRegions.OfType<TextRegion>().Select(x => x.Text).ToList().SelectMany(x => x.Split(' ')).ToList();
        private static readonly Hunspell EnglishDictionary = new Hunspell("en_us.aff","en_us.dic");


        /// <summary>
        /// Filters recognized characters, replaces chars with pobability lower than 0.3 with unrecognized "-"
        /// </summary>
        /// <param name="classificationResult">classification result</param>
        /// <returns>filtered classification result</returns>
        public ClassificationResult FilterClassificationResults(ClassificationResult classificationResult)
        {
            var label = classificationResult.Probability < 0.3 ? -1 : classificationResult.Label;
            var filtered = new ClassificationResult {
                Label = label,
                Probability = label==-1 ? 0.0f : classificationResult.Probability,
                DecodedLabel = label==-1 ? "-" : classificationResult.DecodedLabel
            };
            return filtered;
        }


        /// <summary>
        /// Transforms words probability to credibility according to dictionaries
        /// </summary>
        /// <param name="words">list of words</param>
        /// <returns>list of words with enhanced probability</returns>
        public List<Word> DecideWithDictionary(List<Word> words)
        {
            var decided = words.Select(x=>(Word)x.Clone()).ToList();
            foreach (var possibleWord in decided)
            {
                //skip single characters
                if (possibleWord.PossibleWords.Count == 1)
                    continue;
                
                for (var index = 1; index < possibleWord.PossibleWords.Count; index++) {
                    var word = possibleWord.PossibleWords[index];
                    var length = word.Word.Length;
                    //nhunspell - find in english dictionary
                    var correct = EnglishDictionary.Spell(word.Word);
                    //check context dictionary
                    var dist = CalculateLevenshteinDistances(word.Word, YSoftDictionary, 1);
                    var minDist = dist.Min();
                    //found in context dictionary
                    if (minDist==0) {
                        word.Probability += 1;
                        continue;
                    }
                    //found in english dictionary
                    if (correct) {
                        word.Probability = (word.Probability + 1) / 2;
                        continue;
                    }
                    //try correction with context dictionary
                    var corr = Enumerable.Range(0, dist.Count).Where(i => dist[i] == minDist).ToList();
                    var possWords = new List<string>();
                    foreach (var i in corr)
                        possWords.Add(YSoftDictionary[i]);
                    var list = possWords.GroupBy(x => x).Select(g => g.Key).ToList();
                    var probability = 1 - minDist / (float)list.First().Length; // corrects one fourth of word length mistakes
                    if (probability>=0.75) {
                        word.Word = list.First();
                        word.Probability = (word.Probability + length - minDist) / (length - minDist + 1);
                    }
                }
            }
            return decided;
        }


        /// <summary>
        /// Assembles possible words from classified characters
        /// </summary>
        /// <param name="classificationResults">list of classification results</param>
        /// <returns>list of possible words</returns>
        public List<Word> GetPossibleWords(List<ClassificationCombinedResult> classificationResults)
        {
            var textBoxIndexes = classificationResults.Select(x => x.CharacterBox.RlsaBoundingBoxIndex).Distinct().ToList();
            List<Word> possibleWords = new List<Word>();
            for (var index = 0; index < textBoxIndexes.Count; index++) {   // individual text boxes
                var textBoxIndex = textBoxIndexes[index];
                var textBoxChars =
                    classificationResults.FindAll(x => x.CharacterBox.RlsaBoundingBoxIndex == textBoxIndex).ToList(); // charactes in each text box
                var lastCharsIndexes = Enumerable.Range(0, textBoxChars.Count).Where(i => textBoxChars[i].CharacterBox.IsLast)
                    .ToList();
                var startIndex = 0;
                for (int wordInBox = 0; wordInBox < lastCharsIndexes.Count; wordInBox++) {
                    var lastCharIndex = lastCharsIndexes[wordInBox];
                    if (lastCharIndex == startIndex) { // single characters
                        var maxIndex = FindMaxProbability(new List<ClassificationCombinedResult> {textBoxChars[startIndex]});
                        var soloWidth = textBoxChars[startIndex].CharacterBox.BoundingBox.Width;
                        var soloHeight = textBoxChars[startIndex].CharacterBox.BoundingBox.Height;
                        possibleWords.Add(new Word {
                            TextBoxId = index,
                            PossibleWords = new List<PossibleWord> {
                                new PossibleWord {
                                    Word = textBoxChars[startIndex].DecodedLabels[maxIndex.First()],
                                    Probability = textBoxChars[startIndex].Probabilities[maxIndex.First()]
                                }
                            },
                            WordInBoxId = wordInBox,
                            Position = new Position {
                                Center = new Point(textBoxChars[startIndex].CharacterBox.BoundingBox.Left+soloWidth/2, textBoxChars[startIndex].CharacterBox.BoundingBox.Top+soloHeight/2),
                                Height = soloHeight,
                                Width = soloWidth,
                                TopLeft = textBoxChars[startIndex].CharacterBox.BoundingBox.TopLeft,
                                BoundingRect = new Rect(textBoxChars[startIndex].CharacterBox.BoundingBox.TopLeft, new Size(soloWidth,soloHeight))
                            }
                        });
                        startIndex = lastCharIndex + 1;
                        continue;
                    }
                    //get word characters
                    var charsRange = textBoxChars.GetRange(startIndex, lastCharIndex - startIndex + 1);
                    var words = new List<PossibleWord> {
                        new PossibleWord {
                            Word = String.Join("", charsRange.Select(x => x.DecodedLabels[0])),
                            Probability = charsRange.Select(x => x.Probabilities[0]).Sum()/charsRange.Count
                        },
                        new PossibleWord {
                            Word = String.Join("", charsRange.Select(x => x.DecodedLabels[1])),
                            Probability = charsRange.Select(x => x.Probabilities[1]).Sum()/charsRange.Count
                        },
                        new PossibleWord {
                            Word = String.Join("", charsRange.Select(x => x.DecodedLabels[2])),
                            Probability = charsRange.Select(x => x.Probabilities[2]).Sum()/charsRange.Count
                        },
                        new PossibleWord {
                            Word = textBoxChars[startIndex].DecodedLabels[2] + String.Join("",
                                               textBoxChars.GetRange(startIndex + 1, lastCharIndex - startIndex)
                                                   .Select(x => x.DecodedLabels[1])),
                            Probability = (textBoxChars.GetRange(startIndex + 1, lastCharIndex - startIndex)
                                    .Select(x => x.Probabilities[1]).Sum() + textBoxChars[startIndex].Probabilities[2])/charsRange.Count
                        }
                    };
                    // compute word possition
                    var width = charsRange.Last().CharacterBox.BoundingBox.Right -
                                charsRange.First().CharacterBox.BoundingBox.Left;
                    var height = charsRange.First().CharacterBox.BoundingBox.Height;
                    var center = new Point(
                        charsRange.First().CharacterBox.BoundingBox.Left + width / 2,
                        charsRange.First().CharacterBox.BoundingBox.Top + height / 2);
                    var position = new Position {
                        TopLeft = charsRange.First().CharacterBox.BoundingBox.TopLeft,
                        Center = center,
                        Width = width,
                        Height = height,
                        BoundingRect = new Rect(charsRange.First().CharacterBox.BoundingBox.TopLeft, new Size(width,height))
                    };

                    possibleWords.Add(new Word {
                        TextBoxId = index,
                        PossibleWords = words,
                        WordInBoxId = wordInBox,
                        Position = position
                    });
                    startIndex = lastCharIndex + 1;
                }
            }
            return possibleWords;
        }


        /// <summary>
        /// calculates Levenshtein distances of word against dictionary entries
        /// </summary>
        /// <param name="word"></param>
        /// <param name="dictionary"></param>
        /// <param name="cost">cost of one substitution</param>
        /// <returns>list of distances</returns>
        private List<int> CalculateLevenshteinDistances(string word, List<string> dictionary, int cost)
        {
            var distances = new List<int>();
            foreach (var entry in dictionary)
                distances.Add(Levenshtein.CalculateDistance(word, entry, cost));
            return distances;
        }
        

        /// <summary>
        /// Finds results with maximal probability for each character
        /// </summary>
        /// <param name="results">list of classification results</param>
        /// <returns>list of indexes</returns>
        private List<int> FindMaxProbability(List<ClassificationCombinedResult> results)
        {
            var indexes = new List<int>();
            foreach (var result in results)
                indexes.Add(result.Probabilities.IndexOf(result.Probabilities.Max()));
            return indexes;
        }
    }
}
