using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using BallotTemplates;
using MoreLinq;
using Valg.EVA.Skanning.Contracts.CommonEnums;
using Valg.EVA.Skanning.ServicesModel.Internal.SystemSettings;

namespace Valg.EVA.Skanning.Services.Verification
{
    public class TemplateBasedLayoutDescriptionProvider
    {
        private readonly BallotTemplate _template;
        private readonly BallotPaperLayoutDescription _description;
        private (int min, int max)[] _insidePageBoundaries;
        private static readonly IDictionary<BallotPaperType, BallotPaperLayoutDescription> _cache = new Dictionary<BallotPaperType, BallotPaperLayoutDescription>();
        private static readonly List<BallotTemplate> _templates = new List<BallotTemplate>();

        public static BallotPaperLayoutDescription GetBallotPaperLayoutDescription(BallotPaperType? ballotPaperType)
        {

            if (ballotPaperType == null)
            {
                return BallotPaperLayoutDescription.Empty;
            }

            if (_cache.TryGetValue(ballotPaperType.Value, out var cached))
            {
                return cached;
            }

            if (!_templates.Any())
            {
                LoadTemplates();
            }
            var typeId = GetTemplateTypeId(ballotPaperType);
            var template = (from t in _templates
                            where t.TypeId == typeId
                            select t).FirstOrDefault();
            if (template == null)
            {
                return BallotPaperLayoutDescription.Empty;
            }

            var ret = new TemplateBasedLayoutDescriptionProvider(ballotPaperType.Value, template).GetBallotPaperLayoutDescription();
            _cache[ballotPaperType.Value] = ret;
            return ret;
        }

        private static void LoadTemplates()
        {
            var templatePath = Path.Combine(AssemblyDirectory, "templates", "Kommune2019");
            _templates.AddRange(from f in Directory.GetFiles(templatePath)
                                select BallotTemplate.Load(f));

        }

        private static string AssemblyDirectory
        {
            get
            {
                string codeBase = Assembly.GetExecutingAssembly().CodeBase;
                UriBuilder uri = new UriBuilder(codeBase);
                string path = Uri.UnescapeDataString(uri.Path);
                return Path.GetDirectoryName(path);
            }
        }
        internal TemplateBasedLayoutDescriptionProvider(BallotPaperType ballotPaperType, BallotTemplate template)
        {
            _template = template;
            _description = new BallotPaperLayoutDescription
            {
                BallotPaperType = ballotPaperType,
                TotalHeightInCm = template.PhysicalHeightMm / 10,
                TotalWidthInCm = template.PhysicalWidthMm / 10
            };
            DetectPageBoundaries();
            AddCandidateSections();
            AddContentAreaDescriptions();
        }

        private void AddContentAreaDescriptions()
        {
            var lst = new List<BallotPaperContentAreaDescription>
            {
                CreateContentAreaDescription(_template.Inside.TextFields.FirstOrDefault(tf => tf.Id == "BallotNo"),
                    BallotPaperContentAreaType.BallotNo, false, 5),
                CreateAggregateDescription(_template.Outside.StampRegion.BlankFields, BallotPaperContentAreaType.Stamp, 8),
            };
            lst.AddRange(CreateAffiliationAreaDescription());
            
            if (_template.Inside.BlankRegion.BlankFields.Any())
            {
                var desc = CreateAggregateDescription(_template.Inside.BlankRegion.BlankFields, BallotPaperContentAreaType.Blank, 0);
                desc.IsOnFrontPage = true;
                lst.Add(desc);
            }
            if (_template.Inside.WriteInFields.Items.Any())
            {
                lst.Add(CreateWriteInDescription(_template.Inside.WriteInFields.Items, 0,
                    true, 6));
            }
            if (_template.Outside.WriteInFields.Items.Any())
            {
                lst.Add(CreateWriteInDescription(_template.Outside.WriteInFields.Items,
                    lst.Count(d => d.ContentAreaType == BallotPaperContentAreaType.WriteIn + 1),
                    false, 6));
            }
            lst.AddRange(CreateCandidateContentAreas());

            _description.ContentAreaDescriptions = lst.ToArray();
        }

        private List<BallotPaperContentAreaDescription> CreateAffiliationAreaDescription()
        {
            return _template.Inside.TextFields.Any(tf => tf.Id.StartsWith("AffiliationName"))
                ? CreateActualAffiliationAreaDescription()
                : CreateGenericAffiliationAreaDescription();
        }
        private List<BallotPaperContentAreaDescription> CreateGenericAffiliationAreaDescription()
        {
            
            var templateHeightInPixels = MmToPixels(_template.PhysicalHeightMm);
            var typicalWidthM = 115;
            var typicalHeightMm = 15;

            var desc = new BallotPaperContentAreaDescription
            {
                ContentAreaType = BallotPaperContentAreaType.AffiliationName,
                TopLeftXInPercentRelativeToEntireSize = 100 * 14 / _template.PhysicalWidthMm,
                TopLeftYInPercentRelativeToEntireSize = 100 * 2 / _template.PhysicalHeightMm,
                CropRegionWidthInPercentRelativeToEntireSize = 100 * typicalWidthM / _template.PhysicalWidthMm,
                CropRegionHeightInPercentRelativeToEntireSize = 100 * typicalHeightMm / _template.PhysicalHeightMm,
                IsOnFrontPage = true
            };
            desc.SectionNumber = 1 ;
            return new List<BallotPaperContentAreaDescription> {desc};
        }

        private List<BallotPaperContentAreaDescription> CreateActualAffiliationAreaDescription()
        {
            var items = new List<BallotPaperContentAreaDescription>();
            int index = 0;
            foreach (var affilationField in _template.Inside.TextFields.Where(tf => tf.Id.StartsWith("AffiliationName")).OrderBy(p => p.Id))
            {
                var desc = new BallotPaperContentAreaDescription
                {
                    ContentAreaType = BallotPaperContentAreaType.AffiliationName,
                    IsOnFrontPage = true
                };
                SetGeometry(desc,
                    affilationField.TopLeft.X,
                    affilationField.TopLeft.Y,
                    affilationField.BottomRight.X,
                    affilationField.BottomRight.Y,
                    0);
                desc.SectionNumber = GetSectionNumber(affilationField.TopLeft.X);
                desc.Index = index;
                items.Add(desc);
                index++;
            }

            return items;
        }

        private double PixelXToPercentage(double pixelX)
        {
            var templateWidthInPixels = _template.PhysicalWidthMm / 25.4 * _template.DPI;
            return 100 * pixelX / templateWidthInPixels;
        }

        private double PixelYToPercentage(double pixelY)
        {
            var templateHeightInPixels = _template.PhysicalHeightMm / 25.4 * _template.DPI;
            return 100 * pixelY / templateHeightInPixels;
        }

        private BallotPaperContentAreaDescription CreateContentAreaDescription(
            BoundObject source,
            BallotPaperContentAreaType type,
            bool hidden,
            double marginMm)
        {
            var desc = new BallotPaperContentAreaDescription();
            desc.ContentAreaType = type;
            if (source == null) return desc;
            SetGeometry(desc, source.TopLeft.X, source.TopLeft.Y, source.BottomRight.X, source.BottomRight.Y, marginMm);
            desc.IsOnFrontPage = true;
            desc.SectionNumber = GetSectionNumber(source.TopLeft.X);
            return desc;
        }

        private int? GetSectionNumber(float topLeftX)
        {
            for (var i = 0; i < _insidePageBoundaries.Length; ++i)
            {
                var entry = _insidePageBoundaries[i];
                if (entry.min <= topLeftX && entry.max >= topLeftX)
                {
                    return i + 1;
                }
            }

            return null;
        }

        private BallotPaperContentAreaDescription CreateAggregateDescription(IReadOnlyCollection<BoundObject> source,
            BallotPaperContentAreaType type,
            double marginMm)
        {
            var desc = new BallotPaperContentAreaDescription { ContentAreaType = type };
            if (source == null) return desc;
            SetGeometry(desc,
                source.Min(f => f.TopLeft.X),
                source.Min(f => f.TopLeft.Y),
                source.Max(f => f.BottomRight.X),
                source.Max(f => f.BottomRight.Y),
                marginMm);
            desc.SectionNumber = GetSectionNumber(source.Min(f => f.TopLeft.X));

            return desc;
        }

        private int MmToPixels(double mm)
        {
            return Convert.ToInt32(mm / 25.4 * _template.DPI);
        }

        private BallotPaperContentAreaDescription CreateWriteInDescription(
            IReadOnlyCollection<WriteInField> source,
            int sectionNumber,
            bool isOnFrontPage,
            double marginMm)
        {
            var desc = new BallotPaperContentAreaDescription { ContentAreaType = BallotPaperContentAreaType.WriteIn };
            if (source == null) return desc;
            var fieldsWithContours = source.Where(f => f.NameField.BoundingPolygonPoints.Any()).ToList();
            if (!fieldsWithContours.Any()) return desc;

            double maxX;
            var affiliationPoints = fieldsWithContours.SelectMany(f => f.AffiliationField.BoundingPolygonPoints).ToArray();
            if (affiliationPoints.Any())
            {
                maxX = affiliationPoints.Max(point => point.X);
            }
            else
            {
                var minNameX = fieldsWithContours.Min(f => f.NameField.BoundingPolygonPoints.Min(p => p.X));
                var maxNameX = fieldsWithContours.Max(f => f.NameField.BoundingPolygonPoints.Max(p => p.X));
                maxX = maxNameX + (maxNameX - minNameX) * 0.12;
            }
            SetGeometry(desc,
                fieldsWithContours.Min(f => f.NameField.BoundingPolygonPoints.Min(p => p.X)),
                fieldsWithContours.Min(f => f.NameField.BoundingPolygonPoints.Min(p => p.Y)),
                maxX,
                fieldsWithContours.Max(f => f.NameField.BoundingPolygonPoints.Max(p => p.Y)),
                marginMm);
            desc.IsOnFrontPage = isOnFrontPage;
            desc.SectionNumber = sectionNumber;
            return desc;
        }

        private IEnumerable<BallotPaperContentAreaDescription> CreateCandidateContentAreas()
        {
            var ret = new List<BallotPaperContentAreaDescription>();
            var sectionNumber = 1;
            foreach (var (min, max) in _insidePageBoundaries)
            {
                var entries = _template.Inside.PersonVoteFields.Items.Where(pv => pv.TopLeft.X > min && pv.BottomRight.X < max)
                    .Union(_template.Inside.RenumberingFields.Items.Where(pv => pv.TopLeft.X > min && pv.BottomRight.X < max)).ToList();
                if (!entries.Any())
                {
                    continue;
                }
                var desc = new BallotPaperContentAreaDescription { ContentAreaType = BallotPaperContentAreaType.Candidates };
                desc.SectionNumber = sectionNumber;
                desc.Index = sectionNumber - 1;
                desc.IsOnFrontPage = true;
                ret.Add(desc);
                sectionNumber++;
            }

            return ret;
        }

        private void AddCandidateSections()
        {
            var sections = new List<CandidateSection>();
            var sectionIndex = 0;
            foreach (var (min, max) in _insidePageBoundaries)
            {
                var entries = _template.Inside.PersonVoteFields.Items.Where(pv => pv.TopLeft.X > min && pv.BottomRight.X < max)
                    .Union(_template.Inside.RenumberingFields.Items.Where(pv => pv.TopLeft.X > min && pv.BottomRight.X < max)).ToList();
                if (!entries.Any())
                {
                    continue;
                }
                var section = new CandidateSection()
                {
                    SectionIndex = sectionIndex++,
                    FirstCandidateInSectionRank = entries.Min(entry => entry.CandidateNumber),
                    LastCandidateInSectionRank = entries.Max(entry => entry.CandidateNumber),
                    MaxNumberOfCandidatesInSection = entries.Count
                };
                sections.Add(section);
            }

            _description.CandidateSections = sections.ToArray();
        }
        internal BallotPaperLayoutDescription GetBallotPaperLayoutDescription() => _description;

        private static string GetTemplateTypeId(BallotPaperType? ballotPaperType)
        {
            if (!ballotPaperType.HasValue)
            {
                return String.Empty;
            }

            return ((int)ballotPaperType).ToString("00");
        }

        private void DetectPageBoundaries()
        {
            var templateWidthInPixels = _template.PhysicalWidthMm / 25.4 * _template.DPI;
            _insidePageBoundaries = new(int min, int max)[] { (0, (int)templateWidthInPixels / 2), ((int)templateWidthInPixels / 2, (int)templateWidthInPixels) };
            if (_template == null) return;
            List<ChangeField> fields;
            if (_template.Inside.PersonVoteFields.Items.Any())
            {
                fields = new List<ChangeField>(_template.Inside.PersonVoteFields.Items);
            }
            else if (_template.Inside.AffiliationSelectionFields.Items.Any())
            {
                fields = new List<ChangeField>(_template.Inside.AffiliationSelectionFields.Items);
            }
            else
            {
                return;
            }
            var maxDistance = templateWidthInPixels * 0.05;
            var groups = new List<List<ChangeField>>();
            var cur = new List<ChangeField>() { fields.First() };
            fields.Remove(cur.First());
            while (fields.Any() && groups.Count < 2)
            {
                var first = cur.First();
                cur.AddRange(fields.Where(f => Math.Abs(f.TopLeft.X - first.TopLeft.X) < maxDistance));
                fields.RemoveAll(f => cur.Contains(f));
                groups.Add(cur);
                if (fields.Any())
                {
                    cur = new List<ChangeField>() { fields.First() };
                    fields.Remove(cur.First());
                }
                else
                {
                    break;
                }
            }

            if (groups.Count == 1)
            {
                _insidePageBoundaries = new[] { (0, (int)Math.Ceiling(templateWidthInPixels)) };
            }
            else
            {
                var width = (int)groups.Last().Min(f => f.TopLeft.X) - (int)groups.First().Min(f => f.TopLeft.X);
                var numberOfPages = (int)templateWidthInPixels / width;
                var boundaries = new List<(int min, int max)>();
                for (var i = 0; i < numberOfPages; ++i)
                {
                    boundaries.Add((i * width, (i + 1) * width));
                }
                _insidePageBoundaries = boundaries.ToArray();
            }
        }

        private void SetGeometry(BallotPaperContentAreaDescription desc, double x1, double y1, double x2, double y2, double marginMm)
        {
            var marginPixels = MmToPixels(marginMm);
            var templateHeightInPixels = MmToPixels(_template.PhysicalHeightMm);
            var templateWidthInPixels = MmToPixels(_template.PhysicalWidthMm);

            var minX = Math.Max(x1 - marginPixels, 0);
            var minY = Math.Max(y1 - marginPixels, 0);
            var maxX = Math.Min(x2 + marginPixels, templateWidthInPixels);
            var maxY = Math.Min(y2 + marginPixels, templateHeightInPixels);
            var widthPixels = maxX - minX;
            var heightPixels = maxY - minY;

            desc.TopLeftXInPercentRelativeToEntireSize = PixelXToPercentage(minX);
            desc.TopLeftYInPercentRelativeToEntireSize = PixelYToPercentage(minY);
            desc.CropRegionWidthInPercentRelativeToEntireSize = PixelXToPercentage(widthPixels);
            desc.CropRegionHeightInPercentRelativeToEntireSize = PixelYToPercentage(heightPixels);

        }
    }
}


