using JM1Vision;
using MainShell.Common;
using MainShell.Log;
using MainShell.Models;
using MaxwellFramework.Core.Attributes;
using SemiconductorVisionAlgorithm.SemiParams;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace MainShell.Vision
{
    /// <summary>
    /// Die 定位服务。
    /// </summary>
    public class FindDieService : IFindDieService
    {
        private readonly IImageCaptureService _imageCaptureService;

        public FindDieService(IImageCaptureService imageCaptureService)
        {
            _imageCaptureService = imageCaptureService ?? throw new ArgumentNullException(nameof(imageCaptureService));
        }

        public async Task<VisionProcessResult<FindDieResult>> ProcessAsync(
            FindDieRequest request,
            CancellationToken cancellationToken = default(CancellationToken))
        {
            if (request == null)
            {
                throw new ArgumentNullException(nameof(request));
            }

            FindDieImageRequest imageRequest = CreateImageRequest(request);
            return await ExecuteWithCaptureAsync(
                request.CameraSource,
                request.CaptureOptions,
                imageRequest,
                ProcessImageAsync,
                cancellationToken).ConfigureAwait(false);
        }

        public Task<VisionProcessResult<FindDieResult>> ProcessImageAsync(
            MxImage image,
            FindDieImageRequest request,
            CancellationToken cancellationToken = default(CancellationToken))
        {
            if (image == null)
            {
                throw new ArgumentNullException(nameof(image));
            }

            if (request == null)
            {
                throw new ArgumentNullException(nameof(request));
            }

            FindDieImageRequest normalizedRequest = NormalizeRequest(request);
            VisionProcessResult validationResult = ValidateRequest(normalizedRequest);
            if (validationResult != null)
            {
                return Task.FromResult(CreateFailureResult<FindDieResult>(validationResult));
            }

            try
            {
                cancellationToken.ThrowIfCancellationRequested();
                VisionProcessResult<FindDieResult> result = ExecuteSingleMatch(image, normalizedRequest);
                if (!result.Succeeded)
                {
                    result.Message.LogSysError();
                }

                return Task.FromResult(result);
            }
            catch (OperationCanceledException ex)
            {
                string message = $"FindDieService: process image was cancelled. Timeout={normalizedRequest.TimeoutMilliseconds} ms.";
                message.LogInfo();
                return Task.FromResult(VisionProcessResult<FindDieResult>.Failure(
                    VisionFailureCategory.Cancelled,
                    VisionErrorCode.OperationCancelled,
                    null,
                    MessageKey.VisionOperationCancelled,
                    message,
                    ex));
            }
            catch (Exception ex)
            {
                string message = $"FindDieService: process image failed. {ex.Message}";
                message.LogSysError();
                return Task.FromResult(VisionProcessResult<FindDieResult>.Failure(
                    VisionFailureCategory.Algorithm,
                    VisionErrorCode.TemplateMatchFailed,
                    VisionAlarmIds.TemplateMatchFailed,
                    MessageKey.VisionTemplateMatchFailed,
                    message,
                    ex));
            }
        }

        public async Task<VisionProcessResult<FindDiesResult>> ProcessMultipleAsync(
            FindDieRequest request,
            CancellationToken cancellationToken = default(CancellationToken))
        {
            if (request == null)
            {
                throw new ArgumentNullException(nameof(request));
            }

            FindDieImageRequest imageRequest = CreateImageRequest(request);
            return await ExecuteWithCaptureAsync(
                request.CameraSource,
                request.CaptureOptions,
                imageRequest,
                ProcessMultipleImageAsync,
                cancellationToken).ConfigureAwait(false);
        }

        public Task<VisionProcessResult<FindDiesResult>> ProcessMultipleImageAsync(
            MxImage image,
            FindDieImageRequest request,
            CancellationToken cancellationToken = default(CancellationToken))
        {
            if (image == null)
            {
                throw new ArgumentNullException(nameof(image));
            }

            if (request == null)
            {
                throw new ArgumentNullException(nameof(request));
            }

            FindDieImageRequest normalizedRequest = NormalizeRequest(request);
            VisionProcessResult validationResult = ValidateRequest(normalizedRequest);
            if (validationResult != null)
            {
                return Task.FromResult(CreateFailureResult<FindDiesResult>(validationResult));
            }

            try
            {
                cancellationToken.ThrowIfCancellationRequested();
                VisionProcessResult<FindDiesResult> result = ExecuteMultipleMatch(image, normalizedRequest);
                if (!result.Succeeded)
                {
                    result.Message.LogSysError();
                }

                return Task.FromResult(result);
            }
            catch (OperationCanceledException ex)
            {
                string message = $"FindDieService: multiple die process image was cancelled. Timeout={normalizedRequest.TimeoutMilliseconds} ms.";
                message.LogInfo();
                return Task.FromResult(VisionProcessResult<FindDiesResult>.Failure(
                    VisionFailureCategory.Cancelled,
                    VisionErrorCode.OperationCancelled,
                    null,
                    MessageKey.VisionOperationCancelled,
                    message,
                    ex));
            }
            catch (Exception ex)
            {
                string message = $"FindDieService: multiple die process image failed. {ex.Message}";
                message.LogSysError();
                return Task.FromResult(VisionProcessResult<FindDiesResult>.Failure(
                    VisionFailureCategory.Algorithm,
                    VisionErrorCode.TemplateMatchFailed,
                    VisionAlarmIds.TemplateMatchFailed,
                    MessageKey.VisionTemplateMatchFailed,
                    message,
                    ex));
            }
        }

        private static FindDieImageRequest NormalizeRequest(FindDieImageRequest request)
        {
            FindDieImageRequest normalizedRequest = new FindDieImageRequest();
            normalizedRequest.TimeoutMilliseconds = request.TimeoutMilliseconds > 0 ? request.TimeoutMilliseconds : 3000;
            normalizedRequest.Parameters = CloneParameters(request.Parameters);
            return normalizedRequest;
        }

        private static VisionProcessResult ValidateRequest(FindDieImageRequest request)
        {
            if (request.Parameters == null)
            {
                return VisionProcessResult.Failure(
                    VisionFailureCategory.Validation,
                    VisionErrorCode.RequestInvalid,
                    VisionAlarmIds.RequestInvalid,
                    MessageKey.VisionRequestInvalid,
                    "FindDieService: parameters cannot be null.");
            }

            if (request.TimeoutMilliseconds <= 0 || request.TimeoutMilliseconds > 60000)
            {
                return VisionProcessResult.Failure(
                    VisionFailureCategory.Validation,
                    VisionErrorCode.TimeoutInvalid,
                    VisionAlarmIds.RequestInvalid,
                    MessageKey.VisionTimeoutInvalid,
                    $"FindDieService: TimeoutMilliseconds {request.TimeoutMilliseconds} is invalid. Expected range is [1, 60000].");
            }

            if (string.IsNullOrWhiteSpace(request.Parameters.TemplatePath))
            {
                return VisionProcessResult.Failure(
                    VisionFailureCategory.Validation,
                    VisionErrorCode.TemplatePathEmpty,
                    VisionAlarmIds.TemplatePathEmpty,
                    MessageKey.VisionTemplatePathEmpty,
                    "FindDieService: TemplatePath cannot be empty.");
            }

            if (string.IsNullOrWhiteSpace(request.Parameters.SerialNumber))
            {
                return VisionProcessResult.Failure(
                    VisionFailureCategory.Validation,
                    VisionErrorCode.RequestInvalid,
                    VisionAlarmIds.RequestInvalid,
                    MessageKey.VisionRequestInvalid,
                    "FindDieService: SerialNumber cannot be empty.");
            }

            if (request.Parameters.CurrentRuler == null)
            {
                return VisionProcessResult.Failure(
                    VisionFailureCategory.Validation,
                    VisionErrorCode.RequestInvalid,
                    VisionAlarmIds.RequestInvalid,
                    MessageKey.VisionRequestInvalid,
                    "FindDieService: CurrentRuler cannot be null.");
            }

            if (request.Parameters.DieWidth <= 0d || request.Parameters.DieHeight <= 0d)
            {
                return VisionProcessResult.Failure(
                    VisionFailureCategory.Validation,
                    VisionErrorCode.RequestInvalid,
                    VisionAlarmIds.RequestInvalid,
                    MessageKey.VisionRequestInvalid,
                    $"FindDieService: DieWidth {request.Parameters.DieWidth} and DieHeight {request.Parameters.DieHeight} must be greater than 0.");
            }

            if (request.Parameters.ScoreThreshold <= 0d || request.Parameters.ScoreThreshold > 1d)
            {
                return VisionProcessResult.Failure(
                    VisionFailureCategory.Validation,
                    VisionErrorCode.MinScoreInvalid,
                    VisionAlarmIds.TemplateMinScoreInvalid,
                    MessageKey.VisionTemplateMinScoreInvalid,
                    $"FindDieService: ScoreThreshold {request.Parameters.ScoreThreshold} is invalid. Expected range is (0, 1].");
            }

            return null;
        }

        private static FindDieParameters CloneParameters(FindDieParameters parameters)
        {
            if (parameters == null)
            {
                return new FindDieParameters();
            }

            FindDieParameters clone = new FindDieParameters();
            clone.MinScore = parameters.MinScore;
            clone.TemplatePath = parameters.TemplatePath;
            clone.SerialNumber = parameters.SerialNumber;
            clone.CurrentRuler = parameters.CurrentRuler == null
                ? null
                : new Point(parameters.CurrentRuler.X, parameters.CurrentRuler.Y);
            clone.OverlapX = parameters.OverlapX;
            clone.OverlapY = parameters.OverlapY;
            clone.DieWidth = parameters.DieWidth;
            clone.DieHeight = parameters.DieHeight;
            clone.ScoreThreshold = parameters.ScoreThreshold > 0d ? parameters.ScoreThreshold : parameters.MinScore;
            clone.AngleTolerance = parameters.AngleTolerance;
            return clone;
        }

        private static FindDieImageRequest CreateImageRequest(FindDieRequest request)
        {
            FindDieImageRequest imageRequest = new FindDieImageRequest();
            imageRequest.TimeoutMilliseconds = request.TimeoutMilliseconds;
            imageRequest.Parameters = CloneParameters(request.Parameters);
            return imageRequest;
        }

        private static VisionProcessResult<FindDieResult> ExecuteSingleMatch(MxImage image, FindDieImageRequest request)
        {
            VisionProcessResult<FindDiesResult> multipleResult = ExecuteMultipleMatch(image, request);
            if (!multipleResult.Succeeded)
            {
                return CreateFailureResult<FindDieResult>(multipleResult);
            }

            FindDieResult result = SelectPrimaryResult(multipleResult.Data);
            if (result == null)
            {
                return VisionProcessResult<FindDieResult>.Failure(
                    VisionFailureCategory.Algorithm,
                    VisionErrorCode.TemplateResultInvalid,
                    VisionAlarmIds.TemplateResultInvalid,
                    MessageKey.VisionTemplateMatchFailed,
                    $"FindDieService: no valid die result was found. TemplatePath={request.Parameters.TemplatePath}.");
            }

            return VisionProcessResult<FindDieResult>.Success(result);
        }

        private static VisionProcessResult<FindDiesResult> ExecuteMultipleMatch(MxImage image, FindDieImageRequest request)
        {
            Camera camera = image.Image;
            if (camera == null)
            {
                return VisionProcessResult<FindDiesResult>.Failure(
                    VisionFailureCategory.Algorithm,
                    VisionErrorCode.ImageNull,
                    VisionAlarmIds.ImageNull,
                    MessageKey.VisionImageIsNull,
                    "FindDieService: input image does not contain a valid camera object.");
            }

            double[] uniqueRows = null;
            double[] uniqueCols = null;
            double[] uniqueAngles = null;
            double[] overlapRows = null;
            double[] overlapCols = null;
            double[] overlapAngles = null;
            double[] pixelRows = null;
            double[] pixelCols = null;
            double[] pixelAngles = null;

            bool succeeded = JM1Manager.Instance.get_target_pos_nodisp(
                camera,
                request.Parameters.TemplatePath,
                request.Parameters.SerialNumber,
                request.Parameters.CurrentRuler,
                request.Parameters.OverlapX,
                request.Parameters.OverlapY,
                request.Parameters.DieWidth,
                request.Parameters.DieHeight,
                request.Parameters.ScoreThreshold,
                out uniqueRows,
                out uniqueCols,
                out uniqueAngles,
                out overlapRows,
                out overlapCols,
                out overlapAngles,
                out pixelRows,
                out pixelCols,
                out pixelAngles);

            if (!succeeded)
            {
                return VisionProcessResult<FindDiesResult>.Failure(
                    VisionFailureCategory.Algorithm,
                    VisionErrorCode.TemplateMatchFailed,
                    VisionAlarmIds.TemplateMatchFailed,
                    MessageKey.VisionTemplateMatchFailed,
                    $"FindDieService: die match failed. TemplatePath={request.Parameters.TemplatePath}.");
            }

            if (!HasConsistentResult(uniqueRows, uniqueCols, uniqueAngles) ||
                !HasConsistentResult(overlapRows, overlapCols, overlapAngles) ||
                !HasConsistentResult(pixelRows, pixelCols, pixelAngles))
            {
                return VisionProcessResult<FindDiesResult>.Failure(
                    VisionFailureCategory.Algorithm,
                    VisionErrorCode.TemplateResultInvalid,
                    VisionAlarmIds.TemplateResultInvalid,
                    MessageKey.VisionTemplateMatchFailed,
                    $"FindDieService: die match returned inconsistent result data. TemplatePath={request.Parameters.TemplatePath}.");
            }

            if (uniqueRows.Length + overlapRows.Length == 0)
            {
                return VisionProcessResult<FindDiesResult>.Failure(
                    VisionFailureCategory.Algorithm,
                    VisionErrorCode.TemplateMatchFailed,
                    VisionAlarmIds.TemplateMatchFailed,
                    MessageKey.VisionTemplateMatchFailed,
                    $"FindDieService: die match returned no results. TemplatePath={request.Parameters.TemplatePath}.");
            }

            FindDiesResult result = CreateMultipleResult(uniqueRows, uniqueCols, uniqueAngles, overlapRows, overlapCols, overlapAngles, pixelRows, pixelCols, pixelAngles);
            ApplyAngleTolerance(result, request.Parameters.AngleTolerance);
            if (result.Items.Count == 0)
            {
                return VisionProcessResult<FindDiesResult>.Failure(
                    VisionFailureCategory.Algorithm,
                    VisionErrorCode.TemplateResultInvalid,
                    VisionAlarmIds.TemplateResultInvalid,
                    MessageKey.VisionTemplateMatchFailed,
                    $"FindDieService: die results were empty after angle filtering. TemplatePath={request.Parameters.TemplatePath}.");
            }

            return VisionProcessResult<FindDiesResult>.Success(result);
        }

        private static FindDiesResult CreateMultipleResult(
            double[] uniqueRows,
            double[] uniqueCols,
            double[] uniqueAngles,
            double[] overlapRows,
            double[] overlapCols,
            double[] overlapAngles,
            double[] pixelRows,
            double[] pixelCols,
            double[] pixelAngles)
        {
            FindDiesResult result = new FindDiesResult();
            AppendDieResults(result.Items, uniqueRows, uniqueCols, uniqueAngles, false);
            AppendDieResults(result.Items, overlapRows, overlapCols, overlapAngles, true);
            AppendPixelResults(result.PixelItems, pixelRows, pixelCols, pixelAngles);
            return result;
        }

        private static void AppendDieResults(
            IList<FindDieResult> destination,
            double[] rows,
            double[] cols,
            double[] angles,
            bool isOverlap)
        {
            if (destination == null || rows == null || cols == null || angles == null)
            {
                return;
            }

            for (int i = 0; i < rows.Length; i++)
            {
                FindDieResult item = new FindDieResult();
                item.CenterX = cols[i];
                item.CenterY = rows[i];
                item.Angle = angles[i];
                item.PixelX = double.NaN;
                item.PixelY = double.NaN;
                item.IsOverlap = isOverlap;
                item.Match = CreateMatchResult(angles[i], isOverlap);
                destination.Add(item);
            }
        }

        private static void AppendPixelResults(
            IList<FindDiePixelResult> destination,
            double[] rows,
            double[] cols,
            double[] angles)
        {
            if (destination == null || rows == null || cols == null || angles == null)
            {
                return;
            }

            for (int i = 0; i < rows.Length; i++)
            {
                FindDiePixelResult item = new FindDiePixelResult();
                item.PixelX = cols[i];
                item.PixelY = rows[i];
                item.Angle = angles[i];
                destination.Add(item);
            }
        }

        private static VisionMatchResult CreateMatchResult(double angle, bool isOverlap)
        {
            VisionMatchResult match = new VisionMatchResult();
            match.OffsetX = 0d;
            match.OffsetY = 0d;
            match.Angle = angle;
            match.Score = 0d;
            match.Tag = isOverlap ? "Overlap" : "Unique";
            return match;
        }

        private static void ApplyAngleTolerance(FindDiesResult result, double? angleTolerance)
        {
            if (result == null || !angleTolerance.HasValue || angleTolerance.Value <= 0d)
            {
                return;
            }

            List<double> itemAngles = new List<double>();
            foreach (FindDieResult item in result.Items)
            {
                itemAngles.Add(item.Angle * 180.0d / Math.PI);
            }

            if (itemAngles.Count == 0)
            {
                return;
            }

            double averageAngle = itemAngles.Average();
            List<FindDieResult> filteredItems = new List<FindDieResult>();
            foreach (FindDieResult item in result.Items)
            {
                double angleDegree = item.Angle * 180.0d / Math.PI;
                if (Math.Abs(angleDegree - averageAngle) <= angleTolerance.Value)
                {
                    filteredItems.Add(item);
                }
            }

            List<FindDiePixelResult> filteredPixelItems = new List<FindDiePixelResult>();
            foreach (FindDiePixelResult item in result.PixelItems)
            {
                double angleDegree = item.Angle * 180.0d / Math.PI;
                if (Math.Abs(angleDegree - averageAngle) <= angleTolerance.Value)
                {
                    filteredPixelItems.Add(item);
                }
            }

            result.Items.Clear();
            foreach (FindDieResult item in filteredItems)
            {
                result.Items.Add(item);
            }

            result.PixelItems.Clear();
            foreach (FindDiePixelResult item in filteredPixelItems)
            {
                result.PixelItems.Add(item);
            }
        }

        private static FindDieResult SelectPrimaryResult(FindDiesResult result)
        {
            if (result == null || result.Items == null || result.Items.Count == 0)
            {
                return null;
            }

            foreach (FindDieResult item in result.Items)
            {
                if (!item.IsOverlap)
                {
                    return item;
                }
            }

            return result.Items[0];
        }

        private static bool HasConsistentResult(double[] rows, double[] cols, double[] angles)
        {
            if (rows == null || cols == null || angles == null)
            {
                return false;
            }

            if (rows.Length != cols.Length || rows.Length != angles.Length)
            {
                return false;
            }

            for (int i = 0; i < rows.Length; i++)
            {
                if (double.IsNaN(rows[i]) || double.IsInfinity(rows[i]) ||
                    double.IsNaN(cols[i]) || double.IsInfinity(cols[i]) ||
                    double.IsNaN(angles[i]) || double.IsInfinity(angles[i]))
                {
                    return false;
                }
            }

            return true;
        }

        private async Task<VisionProcessResult<TResult>> ExecuteWithCaptureAsync<TResult>(
            CameraType cameraSource,
            CameraCaptureOptions captureOptions,
            FindDieImageRequest imageRequest,
            Func<MxImage, FindDieImageRequest, CancellationToken, Task<VisionProcessResult<TResult>>> imageProcessor,
            CancellationToken cancellationToken)
        {
            ImageCaptureResult captureResult = null;
            MxImage image = null;

            try
            {
                captureResult = await _imageCaptureService.CaptureAsync(
                    cameraSource,
                    captureOptions,
                    cancellationToken).ConfigureAwait(false);

                if (captureResult == null)
                {
                    return VisionProcessResult<TResult>.Failure(
                        VisionFailureCategory.Capture,
                        VisionErrorCode.NoFrame,
                        VisionAlarmIds.NoFrame,
                        MessageKey.VisionNoFrameReturned,
                        $"FindDieService: image capture returned null result for camera '{cameraSource}'.");
                }

                if (!captureResult.Succeeded)
                {
                    return CreateFailureResult<TResult>(VisionResultMapper.CreateCaptureFailure(captureResult));
                }

                image = captureResult.Image;
                if (image == null)
                {
                    string message = $"FindDieService: image capture succeeded but returned null image for camera '{cameraSource}'.";
                    message.LogSysError();
                    return VisionProcessResult<TResult>.Failure(
                        VisionFailureCategory.Capture,
                        VisionErrorCode.ImageNull,
                        VisionAlarmIds.ImageNull,
                        MessageKey.VisionImageIsNull,
                        message);
                }

                return await imageProcessor(image, imageRequest, cancellationToken).ConfigureAwait(false);
            }
            catch (OperationCanceledException ex)
            {
                string message = $"FindDieService: capture orchestration was cancelled for camera '{cameraSource}'.";
                message.LogInfo();
                return VisionProcessResult<TResult>.Failure(
                    VisionFailureCategory.Cancelled,
                    VisionErrorCode.OperationCancelled,
                    null,
                    MessageKey.VisionOperationCancelled,
                    message,
                    ex);
            }
            catch (Exception ex)
            {
                string message = $"FindDieService: capture orchestration failed for camera '{cameraSource}'. {ex.Message}";
                message.LogSysError();
                return VisionProcessResult<TResult>.Failure(
                    VisionFailureCategory.Algorithm,
                    VisionErrorCode.TemplateMatchFailed,
                    VisionAlarmIds.TemplateMatchFailed,
                    MessageKey.VisionTemplateMatchFailed,
                    message,
                    ex);
            }
            finally
            {
                if (image != null)
                {
                    image.Dispose();
                }
            }
        }

        private static VisionProcessResult<TData> CreateFailureResult<TData>(VisionProcessResult result)
        {
            return VisionProcessResult<TData>.Failure(
                result.FailureCategory,
                result.ErrorCode,
                result.AlarmId,
                result.UserMessageKey,
                result.Message,
                result.Exception);
        }
    }
}
