using System;
using System.Collections.Generic;
using OpenCvSharp;

namespace OpenCVStudio_v0
{
    /// <summary>
    /// Фильтрует найденные контуры
    /// по площади, ширине, высоте и периметру.
    ///
    /// Текущее изображение не изменяется.
    /// Контуры в PipelineContext заменяются
    /// отфильтрованным набором.
    /// </summary>
    internal sealed class ContourFilterExecutor : IPipelineBlockExecutor
    {
        public string BlockType => "ContourFilter";

        public void Execute(PipelineContext context, ProjectBlock block, ProjectManager projectManager)
        {
            ArgumentNullException.ThrowIfNull(context);
            ArgumentNullException.ThrowIfNull(block);
            ArgumentNullException.ThrowIfNull(projectManager);
            ValidateContext(context, block);
            FilterParameters parameters = ReadParameters(block, projectManager);
            ContourData sourceData = context.Contours!;
            int sourceCount = sourceData.Count;
            List< OpenCvSharp. Point[]> filteredContours = new List<OpenCvSharp.Point[]>(sourceCount);
            for (int contourIndex = 0; contourIndex < sourceCount; contourIndex++)
            {
                OpenCvSharp.Point[] contour = sourceData.Contours[contourIndex];
                if (!IsContourAccepted(contour, parameters))
                {
                    continue;
                }
                filteredContours.Add(contour);
            }
            /*
             * После фильтрации исходная иерархия
             * больше не соответствует индексам контуров.
             *
             * Поэтому пока сохраняем пустую иерархию.
             * Для DrawContours, ContourArea, ArcLength
             * и BoundingRect она не требуется.
             */
            context.Contours = new ContourData(filteredContours.ToArray(), Array.Empty<HierarchyIndex>());
            /*
             * Результаты, рассчитанные для предыдущего
             * набора контуров, больше недействительны.
             */
          
            context.ContourAreas = null;
            context.ArcLengths = null;
            context.BoundingRects = null;
            context.MinAreaRects = null;


            int acceptedCount = filteredContours.Count;
            int removedCount = sourceCount - acceptedCount;
            context.ResultText = $"Фильтр контуров: было {sourceCount}; оставлено {acceptedCount}; удалено {removedCount}.";
        }

        /// <summary>
        /// Проверяет наличие контуров.
        /// </summary>
        private static void ValidateContext(PipelineContext context, ProjectBlock block)
        {
            if (context.Contours == null)
            {
                throw new InvalidOperationException($"Перед блоком «{block.Name}» должен быть выполнен блок FindContours.");
            }
        }

        /// <summary>
        /// Считывает и проверяет параметры блока.
        /// </summary>
        private static FilterParameters ReadParameters(ProjectBlock block, ProjectManager projectManager)
        {
            double minimumArea = projectManager.GetBlockParameter(block.Id, "MinimumArea", 100.0);
            double maximumArea = projectManager.GetBlockParameter(block.Id, "MaximumArea", 1_000_000.0);
            int minimumWidth = projectManager.GetBlockParameter(block.Id, "MinimumWidth", 5);
            int maximumWidth = projectManager.GetBlockParameter(block.Id, "MaximumWidth", 100_000);
            int minimumHeight = projectManager.GetBlockParameter(block.Id, "MinimumHeight", 5);
            int maximumHeight = projectManager.GetBlockParameter(block.Id, "MaximumHeight", 100_000);
            double minimumPerimeter = projectManager.GetBlockParameter(block.Id, "MinimumPerimeter", 0.0);
            double maximumPerimeter = projectManager.GetBlockParameter(block.Id, "MaximumPerimeter", 1_000_000.0);
            minimumArea = Math.Max(0.0, minimumArea);
            maximumArea = Math.Max(minimumArea, maximumArea);
            minimumWidth = Math.Max(0, minimumWidth);
            maximumWidth = Math.Max(minimumWidth, maximumWidth);
            minimumHeight = Math.Max(0, minimumHeight);
            maximumHeight = Math.Max(minimumHeight, maximumHeight);
            minimumPerimeter = Math.Max(0.0, minimumPerimeter);
            maximumPerimeter = Math.Max(minimumPerimeter, maximumPerimeter);
            return new FilterParameters(minimumArea, maximumArea, minimumWidth, maximumWidth, minimumHeight, maximumHeight, minimumPerimeter, maximumPerimeter);
        }

        /// <summary>
        /// Проверяет один контур по всем условиям.
        /// </summary>
        private static bool IsContourAccepted(OpenCvSharp.Point[]? contour, FilterParameters parameters)
        {
            if (contour == null || contour.Length == 0)
            {
                return false;
            }
            double area = contour.Length >= 3 ? Math.Abs(Cv2.ContourArea(contour)) : 0.0;
            if (area < parameters.MinimumArea || area > parameters.MaximumArea)
            {
                return false;
            }
            Rect rectangle = Cv2.BoundingRect(contour);
            if (rectangle.Width < parameters.MinimumWidth || rectangle.Width > parameters.MaximumWidth)
            {
                return false;
            }
            if (rectangle.Height < parameters.MinimumHeight || rectangle.Height > parameters.MaximumHeight)
            {
                return false;
            }
            double perimeter = contour.Length >= 2 ? Cv2.ArcLength(contour, true) : 0.0;
            if (perimeter < parameters.MinimumPerimeter || perimeter > parameters.MaximumPerimeter)
            {
                return false;
            }
            return true;
        }

        /// <summary>
        /// Проверенные параметры фильтрации.
        /// </summary>
        private readonly record struct FilterParameters(
            double MinimumArea,
            double MaximumArea,
            int MinimumWidth,
            int MaximumWidth,
            int MinimumHeight,
            int MaximumHeight,
            double MinimumPerimeter,
            double MaximumPerimeter);
    }
}