using System;
using OpenCvSharp;

namespace OpenCVStudio_v0
{
    public sealed class SobelExecutor : IImageBlockExecutor
    {
        private const double MinimumScale = 0.000001;
        private const double MaximumScale = 10000.0;

        private const double MinimumDelta = -1000000.0;
        private const double MaximumDelta = 1000000.0;

        public string BlockType => "Sobel";

        public Mat Execute(Mat input, ProjectBlock block, ProjectManager projectManager)
        {
            ArgumentNullException.ThrowIfNull(input);
            ArgumentNullException.ThrowIfNull(block);
            ArgumentNullException.ThrowIfNull(projectManager);

            if (input.Empty())
                throw new ArgumentException("В блок оператора Собеля передано пустое изображение.", nameof(input));

            SobelDirection direction = projectManager.GetBlockParameter(block.Id, "Direction", SobelDirection.Both);
            int kernelSize = projectManager.GetBlockParameter(block.Id, "KernelSize", 3);
            double scale = projectManager.GetBlockParameter(block.Id, "Scale", 1.0);
            double delta = projectManager.GetBlockParameter(block.Id, "Delta", 0.0);
            bool convertToGray = projectManager.GetBlockParameter(block.Id, "ConvertToGray", true);
            RotateBorderMode borderMode = projectManager.GetBlockParameter(block.Id, "BorderMode", RotateBorderMode.Reflect101);

            ValidateParameters(input, direction, kernelSize, scale, delta, borderMode);

            Mat? preparedInput = null;

            try
            {
                preparedInput = PrepareInput(input, convertToGray);

                return direction switch
                {
                    SobelDirection.X => ExecuteSingleDirection(preparedInput, 1, 0, kernelSize, scale, delta, borderMode),
                    SobelDirection.Y => ExecuteSingleDirection(preparedInput, 0, 1, kernelSize, scale, delta, borderMode),
                    SobelDirection.Both => ExecuteBothDirections(preparedInput, kernelSize, scale, delta, borderMode),
                    _ => throw new ArgumentOutOfRangeException(nameof(direction), direction, "Неизвестное направление оператора Собеля.")
                };
            }
            finally
            {
                preparedInput?.Dispose();
            }
        }

        private static Mat PrepareInput(Mat input, bool convertToGray)
        {
            if (!convertToGray)
                return input.Clone();

            int channels = input.Channels();

            if (channels == 1)
                return input.Clone();

            Mat gray = new Mat();

            try
            {
                switch (channels)
                {
                    case 3:
                        Cv2.CvtColor(input, gray, ColorConversionCodes.BGR2GRAY);
                        break;

                    case 4:
                        Cv2.CvtColor(input, gray, ColorConversionCodes.BGRA2GRAY);
                        break;

                    default:
                        throw new ArgumentException($"Автоматическое преобразование в серый не поддерживает изображение с количеством каналов: {channels}.", nameof(input));
                }

                if (gray.Empty())
                    throw new InvalidOperationException("Не удалось преобразовать входное изображение в оттенки серого.");

                return gray;
            }
            catch
            {
                gray.Dispose();
                throw;
            }
        }

        private static Mat ExecuteSingleDirection(Mat input, int derivativeX, int derivativeY, int kernelSize, double scale, double delta, RotateBorderMode borderMode)
        {
            using Mat signedGradient = new Mat();

            Mat absoluteGradient = new Mat();

            try
            {
                Cv2.Sobel(input, signedGradient, MatType.CV_16S, derivativeX, derivativeY, kernelSize, scale, delta, ConvertBorderMode(borderMode));

                Cv2.ConvertScaleAbs(signedGradient, absoluteGradient);

                if (absoluteGradient.Empty())
                    throw new InvalidOperationException("Оператор Собеля вернул пустое изображение.");

                return absoluteGradient;
            }
            catch
            {
                absoluteGradient.Dispose();
                throw;
            }
        }

        private static Mat ExecuteBothDirections(Mat input, int kernelSize, double scale, double delta, RotateBorderMode borderMode)
        {
            using Mat signedGradientX = new Mat();
            using Mat signedGradientY = new Mat();

            using Mat absoluteGradientX = new Mat();
            using Mat absoluteGradientY = new Mat();

            Mat output = new Mat();

            try
            {
                BorderTypes borderType = ConvertBorderMode(borderMode);

                Cv2.Sobel(input, signedGradientX, MatType.CV_16S, 1, 0, kernelSize, scale, delta, borderType);
                Cv2.Sobel(input, signedGradientY, MatType.CV_16S, 0, 1, kernelSize, scale, delta, borderType);

                Cv2.ConvertScaleAbs(signedGradientX, absoluteGradientX);
                Cv2.ConvertScaleAbs(signedGradientY, absoluteGradientY);

                Cv2.AddWeighted(absoluteGradientX, 0.5, absoluteGradientY, 0.5, 0.0, output);

                if (output.Empty())
                    throw new InvalidOperationException("Не удалось объединить градиенты Собеля по направлениям X и Y.");

                return output;
            }
            catch
            {
                output.Dispose();
                throw;
            }
        }

        private static BorderTypes ConvertBorderMode(RotateBorderMode mode)
        {
            return mode switch
            {
                RotateBorderMode.Constant => BorderTypes.Constant,
                RotateBorderMode.Replicate => BorderTypes.Replicate,
                RotateBorderMode.Reflect => BorderTypes.Reflect,
                RotateBorderMode.Reflect101 => BorderTypes.Reflect101,
                _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, "Неизвестный режим обработки границ.")
            };
        }

        private static void ValidateParameters(Mat input, SobelDirection direction, int kernelSize, double scale, double delta, RotateBorderMode borderMode)
        {
            if (!Enum.IsDefined(direction))
                throw new ArgumentOutOfRangeException(nameof(direction), direction, "Неизвестное направление оператора Собеля.");

            ValidateKernelSize(kernelSize);
            ValidateScale(scale);
            ValidateDelta(delta);

            if (!Enum.IsDefined(borderMode))
                throw new ArgumentOutOfRangeException(nameof(borderMode), borderMode, "Неизвестный режим обработки границ.");

            if (input.Width < kernelSize || input.Height < kernelSize)
                throw new ArgumentException($"Размер ядра {kernelSize} × {kernelSize} превышает размер изображения {input.Width} × {input.Height}.", nameof(input));
        }

        private static void ValidateKernelSize(int value)
        {
            if (value != 1 && value != 3 && value != 5 && value != 7)
                throw new ArgumentOutOfRangeException(nameof(value), value, "Размер ядра Собеля должен быть равен 1, 3, 5 или 7.");
        }

        private static void ValidateScale(double value)
        {
            if (double.IsNaN(value) || double.IsInfinity(value) || value < MinimumScale || value > MaximumScale)
                throw new ArgumentOutOfRangeException(nameof(value), value, $"Масштаб должен находиться в диапазоне от {MinimumScale} до {MaximumScale}.");
        }

        private static void ValidateDelta(double value)
        {
            if (double.IsNaN(value) || double.IsInfinity(value) || value < MinimumDelta || value > MaximumDelta)
                throw new ArgumentOutOfRangeException(nameof(value), value, $"Смещение должно находиться в диапазоне от {MinimumDelta} до {MaximumDelta}.");
        }
    }
}