using System;
using OpenCvSharp;

namespace OpenCVStudio_v0
{
    public sealed class ScharrExecutor : 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 => "Scharr";

        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);
            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(direction, scale, delta, borderMode);

            using Mat preparedInput = PrepareInput(input, convertToGray);

            return direction switch
            {
                SobelDirection.X => ExecuteSingleDirection(preparedInput, 1, 0, scale, delta, borderMode),
                SobelDirection.Y => ExecuteSingleDirection(preparedInput, 0, 1, scale, delta, borderMode),
                SobelDirection.Both => ExecuteBothDirections(preparedInput, scale, delta, borderMode),
                _ => throw new ArgumentOutOfRangeException(nameof(direction))
            };
        }

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

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

            Mat gray = new Mat();

            try
            {
                if (input.Channels() == 3)
                    Cv2.CvtColor(input, gray, ColorConversionCodes.BGR2GRAY);
                else if (input.Channels() == 4)
                    Cv2.CvtColor(input, gray, ColorConversionCodes.BGRA2GRAY);
                else
                    throw new ArgumentException("Неподдерживаемое количество каналов.");

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

        private static Mat ExecuteSingleDirection(Mat input, int dx, int dy, double scale, double delta, RotateBorderMode borderMode)
        {
            using Mat gradient16 = new Mat();

            Mat output = new Mat();

            try
            {
                Cv2.Scharr(input, gradient16, MatType.CV_16S, dx, dy, scale, delta, ConvertBorderMode(borderMode));

                Cv2.ConvertScaleAbs(gradient16, output);

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

        private static Mat ExecuteBothDirections(Mat input, double scale, double delta, RotateBorderMode borderMode)
        {
            using Mat gradX16 = new Mat();
            using Mat gradY16 = new Mat();

            using Mat absX = new Mat();
            using Mat absY = new Mat();

            Mat output = new Mat();

            try
            {
                BorderTypes border = ConvertBorderMode(borderMode);

                Cv2.Scharr(input, gradX16, MatType.CV_16S, 1, 0, scale, delta, border);
                Cv2.Scharr(input, gradY16, MatType.CV_16S, 0, 1, scale, delta, border);

                Cv2.ConvertScaleAbs(gradX16, absX);
                Cv2.ConvertScaleAbs(gradY16, absY);

                Cv2.AddWeighted(absX, 0.5, absY, 0.5, 0, output);

                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))
            };
        }

        private static void ValidateParameters(SobelDirection direction, double scale, double delta, RotateBorderMode borderMode)
        {
            if (!Enum.IsDefined(direction))
                throw new ArgumentOutOfRangeException(nameof(direction));

            if (scale < MinimumScale || scale > MaximumScale)
                throw new ArgumentOutOfRangeException(nameof(scale));

            if (delta < MinimumDelta || delta > MaximumDelta)
                throw new ArgumentOutOfRangeException(nameof(delta));

            if (!Enum.IsDefined(borderMode))
                throw new ArgumentOutOfRangeException(nameof(borderMode));
        }
    }
}