using System;
using OpenCvSharp;

namespace OpenCVStudio_v0
{
    /// <summary>
    /// Аппроксимирует найденные контуры
    /// многоугольниками методом Cv2.ApproxPolyDP.
    ///
    /// Текущее изображение не изменяется.
    /// Набор контуров заменяется упрощёнными контурами.
    /// </summary>
    internal sealed class ApproxPolyDPExecutor : IPipelineBlockExecutor
    {
        public string BlockType => "ApproxPolyDP";

        public void Execute(PipelineContext context, ProjectBlock block, ProjectManager projectManager)
        {
            ArgumentNullException.ThrowIfNull(context);
            ArgumentNullException.ThrowIfNull(block);
            ArgumentNullException.ThrowIfNull(projectManager);
            ValidateContext(context, block);
            ApproxPolyDPEpsilonMode epsilonMode = projectManager.GetBlockParameter(block.Id, "EpsilonMode", ApproxPolyDPEpsilonMode.PerimeterPercent);
            double epsilonValue = projectManager.GetBlockParameter(block.Id, "Epsilon", 1.0);
            bool closed = projectManager.GetBlockParameter(block.Id, "Closed", true);
            epsilonValue = Math.Clamp(epsilonValue, 0.001, 100_000.0);
            ContourData sourceData = context.Contours!;
            OpenCvSharp. Point[][] approximatedContours = new OpenCvSharp.Point[sourceData.Count][];
            int sourcePointCount = 0;
            int resultPointCount = 0;
            for (int contourIndex = 0; contourIndex < sourceData.Count; contourIndex++)
            {
                OpenCvSharp.Point[] sourceContour = sourceData.Contours[contourIndex];
                sourcePointCount += sourceContour?.Length ?? 0;
                OpenCvSharp.Point[] approximatedContour = ApproximateContour(sourceContour, epsilonMode, epsilonValue, closed);
                approximatedContours[contourIndex] = approximatedContour;
                resultPointCount += approximatedContour.Length;
            }
            /*
             * Количество контуров и их порядок не меняются,
             * поэтому иерархия остаётся действительной.
             */
            context.Contours = new ContourData(approximatedContours, sourceData.Hierarchy);
            /*
             * Эти результаты были рассчитаны
             * для старых точек контуров.
             */
            context.ContourAreas = null;
            context.ArcLengths = null;
            context.BoundingRects = null;
            context.MinAreaRects = null;
            int removedPointCount = sourcePointCount - resultPointCount;
            context.ResultText = $"Аппроксимация контуров: контуров {sourceData.Count}; точек было {sourcePointCount}; стало {resultPointCount}; удалено {removedPointCount}.";
        }

        /// <summary>
        /// Аппроксимирует один контур.
        /// </summary>
        private static OpenCvSharp.Point[] ApproximateContour(OpenCvSharp.Point[]? sourceContour, ApproxPolyDPEpsilonMode epsilonMode, double epsilonValue, bool closed)
        {
            if (sourceContour == null || sourceContour.Length == 0)
            {
                return Array.Empty<OpenCvSharp.Point>();
            }
            /*
             * Для контура из одной точки
             * аппроксимация не требуется.
             */
            if (sourceContour.Length == 1)
            {
                return new[] { sourceContour[0] };
            }
            double epsilon = CalculateEpsilon(sourceContour, epsilonMode, epsilonValue, closed);
            OpenCvSharp.Point[] result = Cv2.ApproxPolyDP(sourceContour, epsilon, closed);
            return result ?? Array.Empty<OpenCvSharp.Point>();
        }

        /// <summary>
        /// Рассчитывает фактическое значение Epsilon.
        /// </summary>
        private static double CalculateEpsilon(OpenCvSharp.Point[] contour, ApproxPolyDPEpsilonMode epsilonMode, double epsilonValue, bool closed)
        {
            return epsilonMode switch
            {
                ApproxPolyDPEpsilonMode.Pixels => epsilonValue,
                ApproxPolyDPEpsilonMode.PerimeterPercent => CalculatePerimeterEpsilon(contour, epsilonValue, closed),
                _ => throw new ArgumentOutOfRangeException(nameof(epsilonMode), epsilonMode, "Неизвестный режим расчёта Epsilon.")
            };
        }

        /// <summary>
        /// Рассчитывает Epsilon как процент
        /// от периметра контура.
        /// </summary>
        private static double CalculatePerimeterEpsilon(OpenCvSharp.Point[] contour, double percentage, bool closed)
        {
            double perimeter = Cv2.ArcLength(contour, closed);
            double epsilon = perimeter * percentage / 100.0;
            /*
             * OpenCV допускает нулевой Epsilon,
             * но минимальное положительное значение
             * делает поведение блока предсказуемее.
             */
            return Math.Max(epsilon, 0.001);
        }

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