using System;
using System.Collections.Generic;
using System.Linq;
using BallotTemplates;
using InterpreterLibrary.Models;
using MoreLinq;
using OpenCvSharp;
using OpenCvSharp.Blob;

namespace InterpreterLibrary.Interpreters
{
    public class AdjustmentMarkInterpreter
    {
        private readonly AdjustmentMarkChecker _adjustmentMarkCheckerBack;
        private readonly AdjustmentMarkChecker _adjustmentMarkCheckerFront;

        private readonly BallotTemplate _template;

        private readonly int _requiredNumberOfMarks;

        private readonly int _maxNumberOfMarks;

        private readonly float _approxTemplateXSize;

        private readonly float _approxTemplateYSize;

        public AdjustmentMarkInterpreter(BallotTemplate template, int requiredNumberOfMarks, int maxNumberOfMarks)
        {
            _maxNumberOfMarks = maxNumberOfMarks;
            _requiredNumberOfMarks = requiredNumberOfMarks;
            _template = template;
            _adjustmentMarkCheckerFront = new AdjustmentMarkChecker(_template.Inside.AdjustmentFields.Type);
            _adjustmentMarkCheckerBack = new AdjustmentMarkChecker(_template.Outside.AdjustmentFields.Type);
            _approxTemplateXSize = _template.PhysicalWidthMm / 25.4f * _template.DPI;
            _approxTemplateYSize = _template.PhysicalHeightMm / 25.4f * _template.DPI;
        }


        public (bool Success,
            ICollection<CorrelatedAdjustmentMark> FrontMarks,
            ICollection<CorrelatedAdjustmentMark> BackMarks,
            Mat FrontImage,
            Mat BackImage, 
            bool IsReversedOrder,
            int FrontQuarterTurns,
            int BackQuarterTurns) FindMarks(ImagePairPermutations pairPermutations)
        {
            var flipped = false;
            ICollection<CorrelatedAdjustmentMark> frontMarks = new List<CorrelatedAdjustmentMark>();
            Mat frontImage = null;
            ICollection<CorrelatedAdjustmentMark> backMarks = new List<CorrelatedAdjustmentMark>();
            Mat backImage = null;
            int backTurns = 0, frontTurns = 0;

            var frontFields = SortAdjustmentFields(_template.Inside.AdjustmentFields);
            var backFields = SortAdjustmentFields(_template.Outside.AdjustmentFields);

            var rotationSteps = pairPermutations.Original.First.Width > pairPermutations.Original.First.Height
                ? new[] {0, 2, 1, 3}
                : new[] {1, 3, 0, 2};

            foreach (var step in rotationSteps)
            {
                var permutation = pairPermutations.Get(step);
                var img1 = permutation.First;
                var img2 = permutation.Second;
                if (frontImage == null)
                {
                    if (backImage == null)
                    {
                        frontMarks = FindMarksInImage(img1, frontFields, _adjustmentMarkCheckerFront);
                        if (frontMarks.Count > 0)
                        {
                            frontImage = img1;
                            frontTurns = step;
                        }
                        else
                        {
                            frontMarks = FindMarksInImage(img2, frontFields, _adjustmentMarkCheckerFront);
                            if (frontMarks.Count > 0)
                            {
                                frontImage = img2;
                                frontTurns = step;
                                flipped = true;
                            }
                        }
                    }
                    else
                    {
                        var img = flipped ? img2 : img1;
                        frontMarks = FindMarksInImage(img, frontFields, _adjustmentMarkCheckerFront);
                        if (frontMarks.Count > 0)
                        {
                            frontImage = img;
                            frontTurns = step;
                        }
                    }
                }
                if (backImage == null)
                {
                    if (frontImage == null)
                    {
                        backMarks = FindMarksInImage(img2, backFields, _adjustmentMarkCheckerBack);
                        if (backMarks.Count > 0)
                        {
                            backImage = img2;
                            backTurns = step;
                        }
                        else
                        {
                            backMarks = FindMarksInImage(img1, backFields, _adjustmentMarkCheckerBack);
                            if (backMarks.Count > 0)
                            {
                                backImage = img1;
                                backTurns = step;
                                flipped = true;
                            }
                        }
                    }
                    else
                    {
                        var img = flipped ? img1 : img2;
                        backMarks = FindMarksInImage(img, backFields, _adjustmentMarkCheckerBack);
                        if (backMarks.Count > 0)
                        {
                            backImage = img;
                            backTurns = step;
                        }
                    }
                }

                if (backImage != null && frontImage != null)
                {
                    break; 
                }
            }

            var success = frontImage != null
                          && backImage != null
                          && frontMarks.Count >= _requiredNumberOfMarks
                          && backMarks.Count >= _requiredNumberOfMarks;
            return (success, frontMarks, backMarks, frontImage, backImage, flipped, frontTurns, backTurns);
        }

        internal static IReadOnlyList<AdjustmentField> SortAdjustmentFields(AdjustmentFields adjustmentFields)
        {
            if (adjustmentFields.Items.Count == 0)
            {
                return adjustmentFields.Items;
            }

            var first = adjustmentFields.Items.MinBy(f => Math.Pow(f.TopLeft.X, 2) + Math.Pow(f.TopLeft.Y, 2))
                .FirstOrDefault();
            var ret = new List<AdjustmentField> {first};
            while (ret.Count < adjustmentFields.Items.Count)
            {
                var rest = adjustmentFields.Items.Except(ret);
                var cur = ret.Last();
                var farthest = rest.MaxBy(f =>
                    Math.Pow(f.TopLeft.X - cur.TopLeft.X, 2) +
                    Math.Pow(f.TopLeft.Y - cur.TopLeft.Y, 2)).FirstOrDefault();
                ret.Add(farthest);
            }
            return ret.AsReadOnly();
        }

        private ICollection<CorrelatedAdjustmentMark> FindMarksInImage(Mat image, IReadOnlyList<AdjustmentField> adjustmentFields, AdjustmentMarkChecker adjustmentMarkChecker)
        {
            var marks = new List<CorrelatedAdjustmentMark>();
            for (var i = 0; i < adjustmentFields.Count - _requiredNumberOfMarks + 1; ++i)
            {
                var field = adjustmentFields[i];
                {
                    var mark = FindMark(image, adjustmentMarkChecker, field);
                    if (mark != null)
                    {
                        marks.Add(mark);
                        foreach (var frontField in adjustmentFields.Skip(i + 1))
                        {
                            var frontMark = FindMark(image, adjustmentMarkChecker, frontField);
                            if (frontMark == null)
                                continue;
                            marks.Add(frontMark);
                            if (marks.Count >= _maxNumberOfMarks)
                            {
                                break;
                            }
                        }
                        return marks;
                    }
                }
            }
            return marks;
        }


        private CorrelatedAdjustmentMark FindMark(Mat sourceImage, AdjustmentMarkChecker checker, AdjustmentField field)
        {
            const float marginFactor = 2.0f;
            const float sizeFactor = 0.1f;

            var xMargin = marginFactor * (field.BottomRight.X - field.TopLeft.X);
            var yMargin = marginFactor * (field.BottomRight.Y - field.TopLeft.Y);
            var dx = sizeFactor * (field.BottomRight.X - field.TopLeft.X);
            var dy = sizeFactor * (field.BottomRight.Y - field.TopLeft.Y);

            var templatePixelsToScannedBallotPixelsX = sourceImage.Width / _approxTemplateXSize;
            var templatePixelsToScannedBallotPixelsY = sourceImage.Height / _approxTemplateYSize;


            var searchAreaForAdjustmentMark = FindSearchAreaForAdjustmentMark(sourceImage, field, xMargin, yMargin, templatePixelsToScannedBallotPixelsX, templatePixelsToScannedBallotPixelsY);

            if (searchAreaForAdjustmentMark.Width <= 0 || searchAreaForAdjustmentMark.Height <= 0)
            {
                return null;
            }

            return FindCorrelatedAdjustmentMark(
                checker,
                field,
                dx, dy,
                templatePixelsToScannedBallotPixelsX, templatePixelsToScannedBallotPixelsY,
                searchAreaForAdjustmentMark,
                sourceImage);


        }

        private static CorrelatedAdjustmentMark FindCorrelatedAdjustmentMark(
            AdjustmentMarkChecker checker,
            AdjustmentField field,
            float dx, float dy,
            float templatePixelsToScannedBallotPixelsX, float templatePixelsToScannedBallotPixelsY,
            Rectangle searchAreaForAdjustmentMark,
            Mat sourceImage)
        {
            var rc = new Rect(searchAreaForAdjustmentMark.X, searchAreaForAdjustmentMark.Y, searchAreaForAdjustmentMark.Width, searchAreaForAdjustmentMark.Height);
            var clip = sourceImage.SubMat(rc);

            var minWidth = (int)((field.BottomRight.X - field.TopLeft.X - 2 * dx) *
                                  templatePixelsToScannedBallotPixelsX);
            var minHeight = (int)((field.BottomRight.Y - field.TopLeft.Y - 2 * dy) *
                                   templatePixelsToScannedBallotPixelsY);
            var idealSquaredDiagonal = GetSquaredDiagonal(
                                           templatePixelsToScannedBallotPixelsX * (field.BottomRight.X - field.TopLeft.X),
                                           templatePixelsToScannedBallotPixelsY * (field.BottomRight.Y - field.TopLeft.Y));

            var inverted = new Mat();
            Cv2.BitwiseNot(clip, inverted);
            var cvBlobs = new CvBlobs();
            CvBlobLib.Label(inverted, cvBlobs);

            return (from blob in cvBlobs.Values.Where(blob =>
                    blob.Rect.Width >= minWidth && blob.Rect.Height > minHeight)
                    where checker.IsMatch(inverted.SubMat(blob.Rect))
                    select new CorrelatedAdjustmentMark
                    {
                        Mark = new AdjustmentMark
                        {
                            Type = AdjustmentMarkType.None,
                            Bounds = new Rectangle(blob.Rect.X + searchAreaForAdjustmentMark.X,
                                blob.Rect.Y + searchAreaForAdjustmentMark.Y, blob.Rect.Width, blob.Rect.Height)
                        },
                        Field = field
                    }).OrderBy(cam =>
                    Math.Abs(GetSquaredDiagonal(cam.Mark.Bounds.Width, cam.Mark.Bounds.Height) - idealSquaredDiagonal))
                .FirstOrDefault();
        }

        private static double GetSquaredDiagonal(double x, double y)
        {
            return Math.Pow(x, 2) + Math.Pow(y, 2);
        }

        private static Rectangle FindSearchAreaForAdjustmentMark(Mat sourcePix, AdjustmentField field, float xMargin, float yMargin, float templatePixelsToScannedBallotPixelsX, float templatePixelsToScannedBallotPixelsY)
        {
            return Rectangle.FromCoords(
                (int)Math.Max((field.TopLeft.X - xMargin) * templatePixelsToScannedBallotPixelsX, 0),
                (int)Math.Max((field.TopLeft.Y - yMargin) * templatePixelsToScannedBallotPixelsY, 0),
                (int)Math.Min(sourcePix.Width, (field.BottomRight.X + xMargin) * templatePixelsToScannedBallotPixelsX),
                (int)Math.Min(sourcePix.Height,
                    (field.BottomRight.Y + yMargin) * templatePixelsToScannedBallotPixelsY));
        }
    }
}
