using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using CrossCutting;
using CrossCutting.Generic;
using InterpreterLibrary.Models;
using InterpreterLibrary.Processing;
using MoreLinq;
using NistUtilities.OpenCvSharp;
using OpenCvSharp;
using OpenCvSharp.Blob;
using Point = OpenCvSharp.Point;
using Size = OpenCvSharp.Size;

namespace InterpreterLibrary.Interpreters
{
    public class WriteInInterpreter : IWriteInInterpreter<Mat, Rect, Point>
    {
        private readonly ICharacterClassifier _letterClassifier;
        private string _rawText = string.Empty;
        private List<TextElement> _elements;
        private List<CharacterClassificationResult> _interpretations;
        private Mat _mat;
        private double _minTextHeightPixels;

        public WriteInInterpreter(ICharacterClassifier classifier)
        {
            _letterClassifier = classifier;
        }

        public bool DebugMode { get; set; }
        public string DebugImageDirectory { get; set; }

        private void InterpretElementsFull()
        {
            _interpretations = new List<CharacterClassificationResult>();
            foreach (var elt in _elements)
            {
                if (elt is SpaceElement)
                {
                    _interpretations.Add(new CharacterClassificationResult
                    {
                        Character = ' ',
                        Confidence = 1f
                    });
                }
                else
                {
                    var inkElement = (InkElement)elt;
                    if (inkElement.Interpretation == null)
                    {
                        var image = GetBlobsMat(inkElement.Blobs, _mat);
                        var floatData = new EmnistAdapter(image).To28X28Floats();
                        var result = _letterClassifier.ClassifyEx(floatData).ToArray();
                        if (DebugMode)
                        {
                            var winner = result.First();
                            Directory.CreateDirectory(Path.Combine(DebugImageDirectory, "CharacterImages"));
                            var filename = $"{winner.Character}-{Guid.NewGuid()}.bmp";
                            try
                            {
                                image.SaveImage(Path.Combine(DebugImageDirectory, "CharacterImages", filename));
                            }
                            catch (Exception e)
                            {
                                Console.WriteLine(e);
                            }
                        }

                        inkElement.Interpretations = new List<CharacterClassificationResult>(result);
                    }

                    if (DebugMode)
                    {
                        var image = _mat.SubMat(inkElement.Bounds);
                        Directory.CreateDirectory(Path.Combine(DebugImageDirectory, "CharacterImages"));
                        var filename = $"{inkElement.Interpretation?.Character}-{Guid.NewGuid()}.bmp";
                        try
                        {
                            image.SaveImage(Path.Combine(DebugImageDirectory, "CharacterImages", filename));

                        }
                        catch (Exception e)
                        {
                            Console.WriteLine(e);
                        }
                    }

                    if (inkElement.Interpretation != null)
                    {
                        _interpretations.Add(inkElement.Interpretation);
                    }

                }
            }
            _rawText = new string((from i in _interpretations
                                   select i.Character).ToArray());

        }

        private void FindElementsInCvBlobs(CvBlobs allBlobs)
        {
            _elements = new List<TextElement>();
            var minHeight = allBlobs.Values.MedianOfLargerHalf(b => b.Rect.Height) / 2;
            var minWidth = allBlobs.Values.Average(b => b.Rect.Width) / 2;

            var blobs = new Stack<CvBlob>(from blob in allBlobs.Values
                                          where ((blob.Rect.Height >= minHeight
                                                  || blob.Rect.Width >= minWidth))
                                          orderby blob.MinX descending
                                          select blob);

            switch (blobs.Count)
            {
                case 0:
                    return;
                case 1:
                    var elt = new InkElement();
                    elt.AddBlob(blobs.Pop());
                    _elements.Add(elt);
                    return;
            }

            var inkElements = new List<InkElement>();
            var addedIds = new HashSet<int>();
            var notANeighbour = new HashSet<int>();
            var widthThreshold = blobs.MedianOfLargerHalf(b => b.Rect.Width) * 1.5;
            while (blobs.Any())
            {
                var blob = blobs.Pop();
                if (addedIds.Contains(blob.Label))
                {
                    continue;
                }
                addedIds.Add(blob.Label);
                if (blob.Rect.Width > widthThreshold)
                {
                    var parts = SplitBlob(blob);
                    if (parts.Length > 1)
                    {
                        var wholeResult = ClassifyBlob(blob);
                        var confidences = new List<float>();
                        foreach (var part in parts)
                        {
                            var result = ClassifyBlob(part);
                            confidences.Add(result.Confidence);
                        }

                        var avg = confidences.Average();
                        if (avg > wholeResult.Confidence)
                        {
                            blob = parts.First();
                            blob.Label = blobs.Any() ? blobs.Max(b => b.Label) + 1 : 1;

                            foreach (var part in parts.Skip(1).Reverse())
                            {
                                part.Label = blobs.Any() ? blobs.Max(b => b.Label) + 1 : 1;
                                notANeighbour.Add(part.Label);
                                blobs.Push(part);
                            }
                        }
                    }
                }
                var neighbours = FindNeighboursAbove(blob, blobs.Except(new[] { blob }).ToList());
                var inkElement = new InkElement();
                inkElement.AddBlob(blob);
                foreach (var neighbour in neighbours.Where(n => !addedIds.Contains(n.Label) && !notANeighbour.Contains(n.Label)))
                {
                    inkElement.AddBlob(neighbour);
                    addedIds.Add(neighbour.Label);
                }

                if (inkElement.Bounds.Height >= _minTextHeightPixels)
                {
                    using (var blobsImage = GetBlobsMat(inkElement.Blobs.ToArray(), _mat))
                    {
                        inkElement.Interpretations = new[] { _letterClassifier.Classify(
                            new EmnistAdapter(blobsImage).To28X28Floats())};
                    }
                    inkElements.Add(inkElement);
                }
            }

            
            
            
            if (inkElements.Count >= 2)
            {
                var avgHeight = inkElements.Average(elt => elt.Bounds.Height);
                var maxGapWidthForMerging = 0.25;
                var maxCombinedWidthForMerging = 1.4;
                var lastIsPending = true;
                for (var i = 0; i < inkElements.Count - 1; ++i)
                {
                    var elt1 = inkElements[i];
                    var elt2 = inkElements[i + 1];
                    var gap = (elt2.Bounds.Left - elt1.Bounds.Right) / avgHeight;
                    var combinedWidth = (elt2.Bounds.Right - elt1.Bounds.Left) / avgHeight;
                    var centerDistanceBetweenNeighbours = ((elt2.Bounds.Left + elt2.Bounds.Width / 2) - (elt1.Bounds.Left + elt1.Bounds.Width / 2)) / avgHeight;
                    if (IsSpace2(gap, centerDistanceBetweenNeighbours))
                    {
                        _elements.Add(elt1);
                        _elements.Add(new SpaceElement());
                    }
                    else if (gap < maxGapWidthForMerging && combinedWidth < maxCombinedWidthForMerging)
                    {
                        var combinedBlobs = new List<CvBlob>(elt1.Blobs).Concat(elt2.Blobs);
                        using (var bmpBoth = GetBlobsMat(combinedBlobs.ToArray(), _mat))
                        {
                            var cFirst = elt1.Interpretation;
                            var cSecond = elt2.Interpretation;
                                var cBoth = _letterClassifier.Classify(
                                    new EmnistAdapter(bmpBoth).To28X28Floats());
                                if (cBoth.Confidence > (cFirst?.Confidence + cSecond?.Confidence) / 2)
                                {
                                    foreach (var blob in elt2.Blobs)
                                    {
                                        elt1.AddBlob(blob);
                                    }

                                    elt1.Interpretations = new[] { cBoth };
                                    ++i;
                                    lastIsPending = i < inkElements.Count - 1;
                                }
                            _elements.Add(elt1);
                        }
                    }
                    else
                    {
                        var section = _mat.SubMat(elt1.Bounds);
                        elt1.Interpretations = _letterClassifier.ClassifyEx(
                            new EmnistAdapter(section).To28X28Floats());
                        _elements.Add(elt1);
                    }
                }
                if (lastIsPending)
                {
                    _elements.Add(inkElements.Last());
                }
            }
            else
            {
                _elements.AddRange(inkElements);
            }

        }

        private CharacterClassificationResult ClassifyBlob(CvBlob blob)
        {
            var masker = new MatMasker(_mat);
            using (var maskedImage = masker.MaskBlob(blob))
            {
                var floatData = new EmnistAdapter(maskedImage).To28X28Floats();
                return _letterClassifier.Classify(floatData);
            }
        }

        private CvBlob[] SplitBlob(CvBlob blob)
        {
            var middle = blob.MinX + (blob.Rect.Width / 2);
            var widthOfInterest = 5 * blob.Rect.Width / 12;
            var x = middle - widthOfInterest / 2;
            var roi = Rect.FromLTRB(x, blob.Rect.Top, x + widthOfInterest, blob.Rect.Bottom);
            var densities = HandwritingSegregator.FindVerticalDensities(_mat.SubMat(roi));
            var ordered = densities.Select((d, i) => new { X = x + i, Density = (int)Math.Round(d, 0) }).ToArray();
            var lowest = ordered.Min(e => e.Density);
            var mins = ordered.Where(e => e.Density == lowest);
            var min = mins.MinBy(e => Math.Abs(middle - e.X)).FirstOrDefault();

            var masked = new MatMasker(_mat).MaskBlobInPlace(blob);
            var part1 = new Mat();
            Cv2.BitwiseNot(masked, part1);
            Cv2.Rectangle(
                part1,
                new Point(min.X + 1, 0),
                new Point(part1.Width, part1.Height),
                Scalar.Black,
                -1);
            var part2 = new Mat();
            Cv2.BitwiseNot(masked, part2);
            Cv2.Rectangle(
                part2,
                new Point(0, 0),
                new Point(min.X, part2.Height),
                Scalar.Black,
                -1);
            var cvBlobs1 = new CvBlobs();
            CvBlobLib.Label(part1, cvBlobs1);
            var cvBlobs2 = new CvBlobs();
            CvBlobLib.Label(part2, cvBlobs2);
            var ret = new List<CvBlob>();
            ret.AddRange(cvBlobs1.Select(e => e.Value));
            ret.AddRange(cvBlobs2.Select(e => e.Value));
            return ret.ToArray();
        }

        private bool IsSpace2(double gapWidth, double centerDistanceBetweenNeighbours)
        {
            if (gapWidth < 1)
            {
                return false;
            }
            return centerDistanceBetweenNeighbours >= 1.5;
        }

        private IEnumerable<CvBlob> FindNeighboursAbove(CvBlob blob, IReadOnlyCollection<CvBlob> blobs)
        {
            var ret = new List<CvBlob> { blob };
            var above = new List<CvBlob>(
                from b in blobs
                let cp = b.Centroid
                where b.Label != blob.Label
                      && ((cp.X >= blob.Rect.Left && cp.X <= blob.Rect.Right)
                          || (blob.Centroid.X >= b.Rect.Left && blob.Centroid.X <= b.Rect.Right))
                select b
            );

            ret.AddRange(above.Where(b => ret.All(b2 => b2.Label != b.Label)));
            return ret.Distinct();
        }

        private static Rect FindTallestLine(Mat image)
        {
            var lines = HandwritingSegregator.FindLines(image).ToArray();
            return lines.Any()
                ? lines.MaxBy(line => line.Height).FirstOrDefault()
                : Rect.Empty;
        }

        private Mat GetBlobsMat(IReadOnlyCollection<CvBlob> blobs, Mat image)
        {
            var bounds = Rect.FromLTRB(
                blobs.Min(b => b.Rect.Left),
                blobs.Min(b => b.Rect.Top),
                blobs.Max(b => b.Rect.Right),
                blobs.Max(b => b.Rect.Bottom));

            var ret = new Mat(new Size(bounds.Width, bounds.Height), image.Type(), Scalar.White);
            var maskImage = new Mat(new Size(bounds.Width, bounds.Height), image.Type(), Scalar.Black);
            var hulls = from blob in blobs
                        select Cv2.ConvexHull(
                            (
                                from p in blob.Contour.ConvertToPolygon().AsReadOnly()
                                select new Point((p.X - bounds.X), (p.Y - bounds.Y))
                            ).ToList()).Select(p => new Point(p.X, p.Y));

            foreach (var hull in hulls)
            {
                maskImage.FillConvexPoly(hull, Scalar.White);
            }
            image.SubMat(bounds).CopyTo(ret, maskImage);

            return ret;
        }


        public InterpretationResult<Mat, Rect, Point> InterpretFull(Mat image, int dpi)
        {
            _minTextHeightPixels = image.Height * 0.25;
            _rawText = "";
            var dy = 0;
            _mat = image.Clone();
            var inverted = new Mat();
            Cv2.BitwiseNot(_mat, inverted);
            var cvBlobs = new CvBlobs();
            CvBlobLib.Label(inverted, cvBlobs);
            if (!cvBlobs.Any())
            {
                return new InterpretationResult<Mat, Rect, Point>(
                    "",
                    new List<Rect>(), 
                    new IInterpretations[] {}
                );
            }
            FindElementsInCvBlobs(cvBlobs);
            InterpretElementsFull();

            var lst = new List<IInterpretations>();
            var rects = new List<Rect>();
            var masker = new MatMasker(_mat, Scalar.White);
            foreach (var element in _elements)
            {
                switch (element)
                {
                    case InkElement ink:
                        var ie = new ImageElementInterpretations<Point, Mat>();
                        if (ink.Blobs.Count > 1)
                        {
                            var hull = ink.GetConvexHull().ToArray();
                            var left = hull.Min(p => p.X);
                            var top = hull.Min(p => p.Y);
                            var right = hull.Max(p => p.X);
                            var bottom = hull.Max(p => p.Y);
                            if (right <= left || bottom <= top) 
                            {
                                continue;
                            }
                            ie.Image = masker.ApplyPolygonMask(hull).SubMat(top, bottom, left, right);
                            ie.ContourPolygon = hull;
                        }
                        else
                        {
                            ie.Image = masker.MaskBlob(ink.Blobs.First());
                            ie.ContourPolygon = ink.Blobs.First().Contour.ConvertToPolygon();
                        }
                        ie.Interpretations = ink.Interpretations;
                        lst.Add(ie);
                        rects.Add(Rect.FromLTRB(
                            ie.ContourPolygon.Min(pg => pg.X),
                            ie.ContourPolygon.Min(pg => pg.Y) + dy,
                            ie.ContourPolygon.Max(pg => pg.X),
                            ie.ContourPolygon.Max(pg => pg.Y) + dy
                        ));
                        break;
                    case SpaceElement _:
                        lst.Add(new Space());
                        break;
                }
            }

            return new InterpretationResult<Mat, Rect, Point>(
                _rawText,
                rects,
                lst
            );

        }
    }

    internal class SpaceElement : TextElement
    {
    }
    internal abstract class TextElement
    {
    }
}

