using System;
using System.Collections.Generic;
using System.Linq;
using BallotTemplates;
using CrossCutting;
using InterpreterLibrary.Interpreters;
using InterpreterLibrary.Models;
using InterpreterLibrary.Models.Transform;
using OpenCvSharp;
using Tesseract;
using Rect = OpenCvSharp.Rect;

namespace InterpreterLibrary.Processing
{
    public class BallotPreprocessor : IDisposable
    {
        private readonly byte[] _firstImageBytes;
        private readonly TesseractEngine _ocrEngine;

        private readonly byte[] _secondImageBytes;
        private readonly bool _shouldDeskew;
        private readonly BallotTemplate[] _templateCandidates;
        private readonly BallotPreprocessorOptions _ballotPreprocessorOptions;
        private readonly (Mat first, Mat second) _rawImages;

        public BallotPreprocessor(byte[] firstImageBytes, byte[] secondImageBytes,
            TesseractEngine ocrEngine, BallotPreprocessorOptions ballotPreprocessorOptions,
            params BallotTemplate[] templateCandidates)
        {

            _ballotPreprocessorOptions = ballotPreprocessorOptions;

            _firstImageBytes = firstImageBytes;
            _secondImageBytes = secondImageBytes;
            _templateCandidates = templateCandidates;
            _ocrEngine = ocrEngine;
            _shouldDeskew = !ballotPreprocessorOptions.AreDeskewed;
            _rawImages = GetRawImages();
            TryMatchTemplate();
            Output.SetDirtLineChecker(() => HasBallotDirtLines(_rawImages.first));
        }


        private static bool HasBallotDirtLines(Mat frontImage)
        {
            var image = frontImage.Resize(new Size(frontImage.Width / 2, frontImage.Height / 2));
            var edges = new Mat();

            Cv2.Canny(image, edges, 0, 100);
            var segHoughP = Cv2.HoughLinesP(edges, 1, Math.PI / 180, 200, 275, 25);
            var topMargin = image.Height * 0.13;
            var bottomMargin = image.Height * 0.97;
            var relevantLines = segHoughP.Where(p =>
                (p.P1.Y > topMargin && p.P1.Y < bottomMargin) && (p.P2.Y > topMargin && p.P2.Y < bottomMargin));

            var dirtLines = relevantLines.Where(p => ((Math.Abs(p.P1.Y - p.P2.Y) >= 1) && Math.Abs(p.P1.X - p.P2.X) >= 1) ||
                                                     (Math.Abs(p.P1.X - p.P2.X) > (image.Width * 0.5)));
            return dirtLines.Any();
        }

        public BallotPreprocessorOutput Output { get; } = new BallotPreprocessorOutput();

        public void Dispose()
        {
            Output?.Dispose();
        }

        private void TryMatchTemplate()
        {
            var deskewedImages = GetDeskewedImages(_rawImages.first, _rawImages.second);
            var firstImage = deskewedImages.firstImage;
            var secondImage = deskewedImages.secondImage;
            
            IEnumerable<BallotTemplate> sortedTemplateCandidates;
            var isShortSideIn = firstImage.Height > firstImage.Width;
            if (isShortSideIn)
            {
                sortedTemplateCandidates =
                    (
                        from c in _templateCandidates
                        where c.PhysicalWidthMm > 0
                        orderby c.PhysicalHeightMm / c.PhysicalWidthMm
                        select c
                    )
                    .Union
                    (
                        from c in _templateCandidates
                        where c.PhysicalWidthMm <= 0
                        select c
                    )
                    .ToArray();
            }
            else
            {
                sortedTemplateCandidates = _templateCandidates.ToArray();
            }

            bool foundTemplate = false;
            var permutations = new ImagePairPermutations(firstImage, secondImage);
            foreach (var templateCandidate in sortedTemplateCandidates)
            {
                if (TryClassify(templateCandidate, permutations))
                {
                    foundTemplate = true;
                    break;
                }
            }
            if (foundTemplate)
            {
                return;
            }

            if (_ballotPreprocessorOptions.FallbackToFirstTemplateIfPossible)
            {
                foreach (var templateCandidate in sortedTemplateCandidates)
                {
                    if (!TryClassify(templateCandidate, permutations, true)) continue;
                    foundTemplate = true;
                    break;
                }
                if (foundTemplate)
                {
                    return;
                }
            }


            Output.FrontImage = firstImage;
            Output.BackImage = secondImage;
        }

        private (Mat first, Mat second) GetRawImages()
        {
            var firstImage = Mat.FromImageData(_firstImageBytes, ImreadModes.GrayScale);
            var secondImage = Mat.FromImageData(_secondImageBytes, ImreadModes.GrayScale);
            return (firstImage, secondImage);
        }

        private (Mat firstImage, Mat secondImage) GetDeskewedImages(Mat firstRawImage, Mat secondRawImage)
        {
            if (_shouldDeskew)
            {
                return (firstRawImage.Deskew(), secondRawImage.Deskew());
            }

            return (firstRawImage, secondRawImage);
        }

        private bool TryClassify(BallotTemplate template, ImagePairPermutations pairPermutations, bool ignoreTypeCode = false)
        {
            const int minimumNumberOfMarks = 3;
            const int maximumNumberOfMarks = 7;
            var interpreter = new AdjustmentMarkInterpreter(template, minimumNumberOfMarks, maximumNumberOfMarks);
            var result = interpreter.FindMarks(pairPermutations);
            if (!result.Success)
                return false;

            var frontTransform = CreateTransform(result.FrontMarks);

            if (!ignoreTypeCode)
            {
                var typeCode = ReadTypeCode(result.FrontImage, template.Inside, frontTransform);
                if (typeCode == null
                    || !string.Equals(typeCode.InterpretedText, template.TypeId)
                    || typeCode.Confidence < _ballotPreprocessorOptions.MinimumTypeIdConfidence)
                {
                    return false;
                }
                Output.TypeConfidence = typeCode.Confidence;
            }

            Output.Template = template;
            try
            {
                Output.FrontImage = CropToPhysicalSize(TransformImage(result.FrontImage, result.FrontMarks));
                Output.BackImage = CropToPhysicalSize(TransformImage(result.BackImage, result.BackMarks));

            }
            catch (OpenCVException)
            {
                Output.Template = null;
                return false;
            }

            Output.FrontTransform = new NullTransformer();
            Output.BackTransform = new NullTransformer();
            Output.IsReverseOrder = result.IsReversedOrder;

            Output.FrontRotationDegrees = (90 * ((4 - result.FrontQuarterTurns) % 4));
            Output.BackRotationDegrees = (90 * ((4 - result.BackQuarterTurns) % 4));
            return true;
        }

        private Mat CropToPhysicalSize(Mat transformedImage)
        {
            const double millimetersToInches = 1 / 25.4;
            var widthInPixels = (int)Math.Min(transformedImage.Width, (Output.Template.PhysicalWidthMm * millimetersToInches * Output.Template.DPI));
            var heightInPixels = (int)Math.Min(transformedImage.Height, (Output.Template.PhysicalHeightMm * millimetersToInches * Output.Template.DPI));
            return transformedImage.SubMat(new Rect(0, 0, widthInPixels, heightInPixels));
        }
        private static IPointTransformer CreateTransform(
            ICollection<CorrelatedAdjustmentMark> marks)
        {
            var pointPairs = (from mark in marks
                              select new Tuple<PointF, PointF>(
                                  new PointF(mark.Field.TopLeft.X, mark.Field.TopLeft.Y),
                                  new PointF(mark.Mark.Bounds.Location.X, mark.Mark.Bounds.Location.Y)))
                .Distinct().ToList();

            switch (pointPairs.Count)
            {
                case 3:
                    return new AffineTransformer(pointPairs.Take(3).ToList());
                case 4:
                    if (!CollinearPointsExist(marks))
                    {
                        return new PerspectiveTransformer(pointPairs.Take(4).ToList());
                    }
                    return new AffineTransformer(pointPairs.Take(3).ToList());
                default:
                    return new PerspectiveTransformer(pointPairs.Take(4).ToList());
            }
        }

        private static Mat TransformImage(
            Mat image,
            ICollection<CorrelatedAdjustmentMark> marks)
        {
            var pointPairs = (from mark in marks
                              select new Tuple<PointF, PointF>(
                                  new PointF(mark.Field.TopLeft.X, mark.Field.TopLeft.Y),
                                  new PointF(mark.Mark.Bounds.Location.X, mark.Mark.Bounds.Location.Y)))
                .ToList();
            switch (pointPairs.Count)
            {
                case 3:
                    return RigidTransformer.TransformMat(image, pointPairs);
                case 4:
                    if (CollinearPointsExist(marks))
                    {
                        return RigidTransformer.TransformMat(image, pointPairs);
                    }
                    return HomographicTransformer.TransformMat(image, pointPairs);
                default:
                    return HomographicTransformer.TransformMat(image, pointPairs);
            }

        }

        private static bool CollinearPointsExist(IEnumerable<CorrelatedAdjustmentMark> marks)
        {
            var foundMarkBounds = (from mark in marks select mark.Mark.Bounds).ToList();
            var combos = SelectN(3, foundMarkBounds);
            foreach (var rawCombo in combos)
            {
                var combo = rawCombo.OrderBy(c => c.X).ThenBy(c => c.Y).ToArray();
                var r0 = combo[0];
                var r1 = combo[1];
                var r2 = combo[2];

                var diagonalPoints = new[]
                {
                    new Point2f(r0.X, r0.Y),
                    new Point2f(r0.X + r0.Width, r0.Y),
                    new Point2f(r0.X + r0.Width, r0.Y + r0.Height),
                    new Point2f(r0.X, r0.Y + r0.Height),
                    new Point2f(r2.X, r2.Y),
                    new Point2f(r2.X + r2.Width, r2.Y),
                    new Point2f(r2.X + r2.Width, r2.Y + r2.Height),
                    new Point2f(r0.X, r0.Y + r0.Height),
                };
                var hull = Cv2.ConvexHull(diagonalPoints);
                var middlePoints = new[]
                {
                    new Point2f(r1.X, r1.Y),
                    new Point2f(r1.X + r1.Width, r1.Y),
                    new Point2f(r1.X + r1.Width, r1.Y + r1.Height),
                    new Point2f(r1.X, r1.Y + r1.Height),
                    new Point2f(r1.X + r1.Width /2, r1.Y + r1.Height/2)
                };

                if (middlePoints.Any(p => Cv2.PointPolygonTest(hull, p, false) >= 0))
                {
                    return true;
                }
            }

            return false;
        }

        private static IEnumerable<T[]> SelectN<T>(int n, IReadOnlyList<T> source)
        {
            var ret = new List<T[]>();
            if (n == 1)
            {
                ret.AddRange(from t in source select new[] { t });
            }
            else
            {
                for (var i = 0; i <= source.Count - n; ++i)
                {
                    var rest = source.Skip(i + 1).ToList();
                    var obj = source[i];
                    var subSelect = SelectN(n - 1, rest);
                    ret.AddRange(from combo in subSelect select new List<T> { obj }.Concat(combo).ToArray());
                }
            }

            return ret;
        }

        private OcrResult ReadTypeCode(Mat frontImage, BallotTemplatePageInside templatePage,
            IPointTransformer transform)
        {
            var typeField = templatePage.TextFields.FirstOrDefault(field => field.IsRecognitionField);
            if (typeField == null)
                return null;
            var topLeft = transform.TransformPoint(typeField.TopLeft);
            var bottomRight = transform.TransformPoint(typeField.BottomRight);
            var x1 = (int)Math.Max(0, topLeft.X);
            var y1 = (int)Math.Max(0, topLeft.Y);
            var x2 = (int)Math.Max(x1, Math.Min(frontImage.Width, bottomRight.X));
            var y2 = (int)Math.Max(y1, Math.Min(frontImage.Height, bottomRight.Y));
            var rect = new Rect(x1, y1, x2 - x1, y2 - y1);
            if (rect.Width <= 0 || rect.Height <= 0)
            {
                return null;
            }


            return _ocrEngine.ProcessNumericText(frontImage, rect);
        }
    }
}
