using System;
using System.Collections.Generic;
using System.Configuration;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using BallotTemplates;
using CrossCutting;
using CrossCutting.Generic;
using InterpreterLibrary.Interpreters;
using InterpreterLibrary.Models.Transform;
using InterpreterLibrary.Processing;
using MoreLinq;
using OpenCvSharp;
using OpenCvSharp.Blob;
using Tesseract;
using Rect = OpenCvSharp.Rect;

namespace InterpreterLibrary.Models
{
    public class ScannedBallotPaper
    {
        private int _minFillPercentageForAFfiliationBoxes;
        private double _minFillPercentageForCheckboxes;
        private readonly INumberInterpreter<Mat, Rect> _numberInterpreter;
        private readonly TesseractEngine _ocrEngine;

        private readonly BallotPreprocessorOutput _preprocessorResult;
        private bool DEBUG_SaveImages;
        private readonly IWriteInInterpreter<Mat, Rect, OpenCvSharp.Point> _writeInInterpreter;

        public ScannedBallotPaper(BallotPreprocessorOutput preprocessorResult, TesseractEngine ocrEngine,
            INumberInterpreter<Mat, Rect> numberInterpreter,
            IWriteInInterpreter<Mat, Rect, OpenCvSharp.Point> writeInInterpreter,
            ICheckboxInterpreter<Mat> checkboxClassifier)
        {
            ReadAppSettings();
            _preprocessorResult = preprocessorResult;
            _ocrEngine = ocrEngine;
            _numberInterpreter = numberInterpreter;
            _writeInInterpreter = writeInInterpreter;
            _checkboxClassifier = checkboxClassifier;

            Output.SetDirtLineChecker(_preprocessorResult.GetDirtLineChecker());
        }

        private void ReadAppSettings()
        {
            if (!int.TryParse(ConfigurationManager.AppSettings["MinFillPercentageForAFfiliationBoxes"], out _minFillPercentageForAFfiliationBoxes))
            {
                _minFillPercentageForAFfiliationBoxes = 15;
            }
            if (!double.TryParse(ConfigurationManager.AppSettings["MinFillPercentageForCheckboxes"],
                NumberStyles.Any,
                CultureInfo.InvariantCulture,
                out _minFillPercentageForCheckboxes))
            {
                _minFillPercentageForCheckboxes = 9.5;
            }
        }
        public ScannedBallotPaper Interpret()
        {
            return InterpretSequentially();
            
        }

        private ScannedBallotPaper InterpretSequentially()
        {
            DoTemplate();
            if (Template != null)
            {
                DoStamps();
                DoStrikeouts();
                DoRenumberings();
                DoPersonVotes();
                DoListSelections();
                DoWriteIns();
                FindBallotId();
                DoHeaders();
                FindBallotNumber();
                FindUnlistedAffiliationName();
                DoBlanks();
            }

            return this;
        }

        private ScannedBallotPaper InterpretParallell()
        {
            DoTemplate();
            if (Template != null)
            {
                var executionTimePerMethod = ExecuteParallellAndRecordTime(
                   
                   DoStamps,
                   
                   
                   
                   
                   DoWriteIns,
                   FindBallotId,
                   FindBallotNumber,
                   DoHeaders,
                   FindUnlistedAffiliationName,
                   
                   DoBlanks,
                   DoTheRest
                   );
                var totalExecutionTime = executionTimePerMethod.Sum(p => p.Item2);
            }

            return this;
        }

        private void DoTheRest()
        {
            
            DoRenumberings();
            DoStrikeouts();
            DoPersonVotes();
            DoListSelections();
            
            
            
            
        }

        private void DoEverything()
        {
            DoStamps();
            DoStrikeouts();
            DoRenumberings();
            DoPersonVotes();
            DoListSelections();
            DoWriteIns();
            FindBallotId();
            FindBallotNumber();
            DoHeaders();
            DoBlanks();
        }


        private void DoCheckboxes()
        {
            DoStrikeouts();
            DoPersonVotes();
            DoListSelections();
        }

        private void DoTextFields()
        {
            FindBallotId();
            FindBallotNumber();
            FindUnlistedAffiliationName();
        }

        private void FindUnlistedAffiliationName()
        {
            var affiliationField = Template.Inside.TextFields.Find(tf => tf.Id == "UnlistedAffiliation");
            if (affiliationField == null)
            {
                return;
            }
            var roi = TemplateFieldCoordinatesToOpenCvRect(affiliationField.TopLeft, affiliationField.BottomRight, _preprocessorResult.FrontTransform);
            var result = _writeInInterpreter.InterpretFull(_preprocessorResult.FrontImage.SubMat(roi), 0);
            var text = result.RawText;
            float confidence;
            var characters = result.Interpretations.OfType<ImageElementInterpretations<OpenCvSharp.Point, Mat>>().ToArray();
            if (characters.Any())
            {
                confidence = characters.Average(i => i.Interpretations.First().Confidence);
            }
            else
            {
                confidence = 1;
            }
            Output.UnlistedAffiliationName = new OcrResult() { Confidence = confidence, InterpretedText = text };
        }

        private (string, double)[] ExecuteParallellAndRecordTime(params Action[] actions)
        {
            var tasks = new List<Task>();
            var result = new List<(string, double)>();
            foreach (var action in actions)
            {
                tasks.Add(Task.Factory.StartNew(() =>
                {
                    var stopWatch = Stopwatch.StartNew();
                    action.Invoke();
                    stopWatch.Stop();
                    lock (result)
                    {
                        result.Add((action.Method.Name, stopWatch.Elapsed.TotalMilliseconds));
                    }
                }));
            }

            Task.WaitAll(tasks.ToArray());
            return result.OrderByDescending(p => p.Item2).ToArray();
        }

        public BallotTemplate Template => _preprocessorResult.Template;


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

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

        private void DoTemplate()
        {
            Output.Type = _preprocessorResult.Type;
            Output.TypeConfidence = _preprocessorResult.TypeConfidence;
        }

        private void DoStamps()
        {
            foreach (var area in Template.Outside.StampRegion.BlankFields)
            {
                var rect = TemplateFieldCoordinatesToOpenCvRect(area.TopLeft, area.BottomRight, _preprocessorResult.BackTransform);
                var maxPercentage = _preprocessorResult.BackImage.GetMaxFillPercentage(rect, area.Columns, area.Rows);
                Output.AddStampFillPercentage(Convert.ToSingle(maxPercentage));
            }
        }

        private void DoStrikeouts()
        {
            foreach (var field in Template.Inside.StrikeoutFields.Items)
            {
                var (content, fillPercentage) = GetCheckboxState(field);
                if (!string.IsNullOrEmpty(content) || fillPercentage > _minFillPercentageForCheckboxes)
                    Output.AddStrikeout(new Strikeout(
                        field.CandidateNumber, Convert.ToInt32(fillPercentage).ToString(),
                        Convert.ToSingle(fillPercentage),
                        field
                    ));
            }
        }

        private void DoRenumberings()
        {
            foreach (var field in Template.Inside.RenumberingFields.Items)
            {
                var state = GetCheckboxState(field);
                if (state.FillPercentage > _minFillPercentageForCheckboxes)
                    Output.AddRenumbering(new Renumbering(
                        field.CandidateNumber,
                        state.Content,
                        Convert.ToSingle(state.FillPercentage),
                        field)
                    );
            }
        }

        private void DoPersonVotes()
        {
            foreach (var field in Template.Inside.PersonVoteFields.Items)
            {
                var state = GetCheckboxState(field);
                if (!string.IsNullOrEmpty(state.Content))
                {
                    var personVoteNameField = Template.Inside.TextFields.FirstOrDefault(p =>
                        p.Id.Equals($"PersonVoteName{field.CandidateNumber}",
                            StringComparison.InvariantCultureIgnoreCase));
                    OcrResult candidateName = null;
                    if (personVoteNameField != null)
                    {
                        var personVoteCandidateNameRect = new Rect((int)personVoteNameField.TopLeft.X, (int)personVoteNameField.TopLeft.Y,
                            (int)Math.Abs(personVoteNameField.TopLeft.X - personVoteNameField.BottomRight.X),
                            (int)Math.Abs(personVoteNameField.TopLeft.Y - personVoteNameField.BottomRight.Y));
                        lock (_ocrEngine)
                        {
                            candidateName = _ocrEngine.ProcessCandidateNameText(_preprocessorResult.FrontImage, personVoteCandidateNameRect);
                        }
                    }
                    Output.AddPersonVote(new PersonVote
                    (
                        field.CandidateNumber,
                        state.Content,
                        Convert.ToSingle(state.FillPercentage),
                        field,
                        candidateName
                    ));
                }

            }

            
        }

        private void DoWriteIns()
        {
            var writeInFields =
                (
                    from wf in Template.Inside.WriteInFields.Items
                    where (!string.IsNullOrEmpty(wf.NameField?.Contour))
                    select new
                    {
                        Field = wf,
                        Template.Inside.WriteInFields.IsContourVisible,
                        SourceImage = _preprocessorResult.FrontImage,
                        Side = TemplateSide.Inside
                    })
                .Union(
                    from wf in Template.Outside.WriteInFields.Items
                    where (!string.IsNullOrEmpty(wf.NameField?.Contour))
                    select new
                    {
                        Field = wf,
                        Template.Outside.WriteInFields.IsContourVisible,
                        SourceImage = _preprocessorResult.BackImage,
                        Side = TemplateSide.Outside
                    }
                ).ToList();

            if (!writeInFields.Any())
            {
                return;
            }

            var src = _preprocessorResult.FrontImage;
            foreach (var entry in writeInFields)
            {
                var maskedImage =
                    entry.IsContourVisible
                        ? GetMaskedWriteInImage(entry.Field, entry.SourceImage)
                        : GetConvexHullWriteInImage(entry.Field, entry.SourceImage);
                if (DebugMode)
                {
                    maskedImage.SaveImage(Path.Combine(DebugImageDirectory, $"{maskedImage.GetHashCode()}{DateTime.Now.Ticks}-maskedImage.tiff"));
                }
                double nameFillPercentage = maskedImage.GetHorizontalMaxDensity();
                if (nameFillPercentage > 2)
                {
                    var fieldIndex = int.Parse(new string(entry.Field.Id.SkipWhile(c => !char.IsDigit(c)).ToArray()));
                    var interpretation = _writeInInterpreter.InterpretFull(maskedImage, _preprocessorResult.Template.DPI);
                    var nameText = interpretation.RawText;
                    if (DebugMode)
                    {
                        maskedImage.SaveImage(Path.Combine(DebugImageDirectory, $"{src.GetHashCode()}-{fieldIndex}-{nameText.Replace('?', '#')}.bmp"));
                    }
                    var writeIn = new WriteIn
                    {
                        FieldIndex = fieldIndex,
                        Name = nameText,
                        NameFillPercentage = (float)nameFillPercentage,
                        Affiliation = "",
                        AffiliationFillPercentage = 0,
                        TemplateField = entry.Field,
                        Side = entry.Side
                    };
                    Output.AddWriteIn(writeIn);
                }

                maskedImage.Dispose();
            }
        }

        private Mat GetMaskedWriteInImage(WriteInField field, Mat src)
        {

            var parts = field.NameField.Contour.Split(' ');
            var pg = from part in parts
                     let coords = part.Split(',')
                     select _preprocessorResult.FrontTransform.TransformPoint(
                         new System.Drawing.PointF(
                             int.Parse(coords.First()),
                             int.Parse(coords.Last())))
                ;
            var points = (from p in pg select new OpenCvSharp.Point((int)p.X, (int)p.Y)).ToArray();
            var minx = points.Min(p => p.X);
            var miny = points.Min(p => p.Y);
            var maxx = points.Max(p => p.X);
            var maxy = points.Max(p => p.Y);

            var nameRect = Rect.FromLTRB(minx, miny, maxx, maxy);
            Utils.FuzzyTrim(src, nameRect);
            var masker = new MatMasker(src, nameRect, Scalar.White);
            var translatedPoints = from p in points select p - new OpenCvSharp.Point(minx, miny);
            var maskedImage = masker.ApplyPolygonMask(translatedPoints.ToArray());
            if (DebugMode)
            {
                maskedImage.SaveImage(Path.Combine(DebugImageDirectory, $"{maskedImage.GetHashCode()}{DateTime.Now.Ticks}-maskedImage.tiff"));
            }

            return maskedImage;
        }

        private Mat GetConvexHullWriteInImage(WriteInField field, Mat src)
        {

            var parts = field.NameField.Contour.Split(' ');
            var pg = from part in parts
                     let coords = part.Split(',')
                     let p = _preprocessorResult.FrontTransform.TransformPoint(
                         new System.Drawing.PointF(
                             int.Parse(coords.First()),
                             int.Parse(coords.Last())))
                     select new OpenCvSharp.Point((int)p.X, (int)p.Y);

            var hull = Cv2.ConvexHull(pg.ToArray());
            var minx = hull.Min(p => p.X);
            var miny = hull.Min(p => p.Y);
            var maxx = hull.Max(p => p.X);
            var maxy = hull.Max(p => p.Y);

            var nameRect = Rect.FromLTRB(minx, miny, maxx, maxy);
            Utils.FuzzyTrim(src, nameRect);
            var masker = new MatMasker(src, nameRect, Scalar.White);
            var translatedPoints = from p in hull select p - new OpenCvSharp.Point(minx, miny);
            var maskedImage = masker.ApplyPolygonMask(translatedPoints.ToArray());
            if (DebugMode)
            {
                maskedImage.SaveImage(Path.Combine(DebugImageDirectory, $"{maskedImage.GetHashCode()}{DateTime.Now.Ticks}-maskedImage.tiff"));
            }

            return maskedImage;
        }

        private string ProcessHandwrittenNumber(Mat sourceImage, Rect area)
        {
            var result = _numberInterpreter.Interpret(
                sourceImage,
                new Rect(area.X, area.Y,
                    area.Width, area.Height));
            return result.OrderByDescending(p => p.Confidence).First().Value.ToString();
        }

        private void DoListSelections()
        {
            foreach (var field in Template.Inside.AffiliationSelectionFields.Items)
            {
                var (content, fillPercentage) = GetCheckboxState(field);
                if (fillPercentage > _minFillPercentageForAFfiliationBoxes)
                    Output.AddListSelection(new ListSelection
                    (
                        field.CandidateNumber,
                        content,
                        Convert.ToSingle(fillPercentage),
                        field)
                    );
            }
        }

        private readonly ICheckboxInterpreter<Mat> _checkboxClassifier;

        private (string Content, double FillPercentage) GetCheckboxState(BoundObject field)
        {
            DEBUG_SaveImages = false;
            var transformedFieldArea = TemplateFieldCoordinatesToOpenCvRect(field.TopLeft, field.BottomRight,
                _preprocessorResult.FrontTransform);

            var area = KeepRectInsideImage(
                transformedFieldArea,
                _preprocessorResult.FrontImage);

            if (DEBUG_SaveImages)
                using (var foo = _preprocessorResult.FrontImage.Clone(area))
                {
                    var path = $"c:\\temp\\frontImage-select3\\{field.Id}.{DateTime.Now.Ticks}";
                    foo.SaveImage($"{path}.png");
                }

            if (area.Width == 0 || area.Height == 0)
            {
                return ("", 0);
            }
            var numberOfBlackPixels = _preprocessorResult.FrontImage.GetNumberOfNonWhitePixelsInImage(area);
            var fillPercentage = 100 * numberOfBlackPixels / (area.Width * area.Height);
            if (fillPercentage < _minFillPercentageForCheckboxes)
            {
                return ("", fillPercentage);
            }

            var result = _checkboxClassifier.Interpret(_preprocessorResult.FrontImage.SubMat(area));

            switch (result.Result)
            {
                case CheckboxState.Checked:
                    return result.Confidence > 0.9 ? ("X", 35) : ("X", fillPercentage);
                case CheckboxState.NotChecked:
                    return result.Confidence > 0.9 ? ("", 0) : ("", fillPercentage);
                default:
                    return (((char)254).ToString(), 95);
            }
        }

        private void FindBallotId()
        {
            var ballotIdField = Template.Inside.TextFields.Find(tf => tf.Id == "BallotId");
            if (ballotIdField == null)
            {
                return;
            }
            var roi = TemplateFieldCoordinatesToOpenCvRect(
                ballotIdField.TopLeft,
                ballotIdField.BottomRight,
                _preprocessorResult.FrontTransform);
            lock (_ocrEngine)
            {
                Output.BallotId = _ocrEngine.ProcessNumericText(_preprocessorResult.FrontImage, roi);
            }
        }

        private void DoHeaders()
        {
            var affiliationNameFields = Template.Inside.TextFields.Where(tf => tf.Id.StartsWith("AffiliationName", StringComparison.CurrentCultureIgnoreCase));
            foreach (var affiliationNameField in affiliationNameFields)
            {
                var roi = TemplateFieldCoordinatesToOpenCvRect(
                    affiliationNameField.TopLeft,
                    affiliationNameField.BottomRight,
                    _preprocessorResult.FrontTransform);
                var subMat = _preprocessorResult.FrontImage.SubMat(roi);
                var inverted = new Mat();
                Cv2.BitwiseNot(subMat, inverted);

                var cvBlobs = new CvBlobs();
                CvBlobLib.Label(inverted, cvBlobs);

                var groupedBlobs = cvBlobs.Values.GroupWithTolerance(50, (blob) => blob.Rect.TopLeft.Y).Where(p => p.Count() > 1);

                var affiliationNameRoi = Cv2.BoundingRect(groupedBlobs.FirstOrDefault()?.SelectMany(p => p.Contour.ConvertToPolygon()));
                var contestRoi = Cv2.BoundingRect(groupedBlobs.LastOrDefault()?.SelectMany(p => p.Contour.ConvertToPolygon()));

                lock (_ocrEngine)
                {
                    using (var affiliationImageWithMargins = PadBlobImage(subMat, affiliationNameRoi))
                    using (var contestImageWithMargins = PadBlobImage(subMat, contestRoi))
                    {
                        var affiliationName = _ocrEngine.ProcessText(affiliationImageWithMargins);

                        var contestName = _ocrEngine.ProcessText(contestImageWithMargins);

                        Output.AddAffiliationName(new BallotHeaderResult()
                        {
                            AffiliationName = affiliationName.InterpretedText,
                            AffiliationNameConfidence = affiliationName.Confidence,
                            ContestName = contestName.InterpretedText,
                            ContestNameConfidence = contestName.Confidence,
                            TemplateFieldId = affiliationNameField.Id
                        });
                    }
                }
            }
        }

        internal static Mat PadBlobImage(Mat mainImage, Rect roi)
        {
            var marginSize = Math.Max((int)(roi.Height * 0.2), 1);
            var newSize = new Size(roi.Width + (2 * marginSize), roi.Height + (2 * marginSize));
            var ret = new Mat(newSize, mainImage.Type(), Scalar.White);
            var roiImage = mainImage.SubMat(roi.Top, roi.Top + roi.Height, roi.Left, roi.Left + roi.Width);

            var targetArea = ret.SubMat(
                marginSize, marginSize + roiImage.Height,
                marginSize, marginSize + roiImage.Width);
            roiImage.CopyTo(targetArea);
            return ret;
        }

        private void FindBallotNumber()
        {
            var ballotNoField = Template.Inside.TextFields.Find(tf => tf.Id == "BallotNo");
            if (ballotNoField == null)
            {
                return;
            }
            var roi = TemplateFieldCoordinatesToOpenCvRect(ballotNoField.TopLeft, ballotNoField.BottomRight, _preprocessorResult.FrontTransform);
            lock (_ocrEngine)
            {
                Output.BallotNo = _ocrEngine.ProcessNumericText(_preprocessorResult.FrontImage, roi);
            }
        }

        private void DoBlanks()
        {
            Output.BlankFillPercentage = 0;
            Template.Inside.BlankRegion.BlankFields.ForEach(field =>
            {
                var transformedRect =
                    TemplateFieldCoordinatesToOpenCvRect(field.TopLeft, field.BottomRight, _preprocessorResult.FrontTransform);

                var clippedRect = Rect.FromLTRB(
                        Math.Max(0, transformedRect.Left),
                        Math.Max(0, transformedRect.Top),
                        Math.Min(transformedRect.Right, _preprocessorResult.FrontImage.Width),
                        Math.Min(transformedRect.Bottom, _preprocessorResult.FrontImage.Height));

                Output.BlankFillPercentage = (int)Math.Max(Output.BlankFillPercentage,
                        _preprocessorResult.FrontImage.GetMaxFillPercentage(clippedRect,
                        field.Columns, field.Rows));

            });
        }

        private static Rect TemplateFieldCoordinatesToOpenCvRect(PointF topLeft, PointF bottomRight, IPointTransformer transform)
        {
            var p1 = transform.TransformPoint(topLeft);
            var p2 = transform.TransformPoint(bottomRight);
            return Rect.FromLTRB((int)p1.X, (int)p1.Y, (int)p2.X - 1, (int)p2.Y - 1);
        }

        private static Rect KeepRectInsideImage(Rect rect, Mat image)
        {
            var x1 = Math.Max(rect.Left, 0);
            var x2 = Math.Min(rect.Right, image.Width);
            var y1 = Math.Max(rect.Top, 0);
            var y2 = Math.Min(rect.Bottom, image.Height);
            if ((x1 < x2) & (y1 < y2))
                return new Rect(x1, y1, x2 - x1, y2 - y1);
            return new Rect(0, 0, 0, 0);
        }
    }
}
