using SkiaSharp;

namespace Scry.Core.Imaging;

/// <summary>
/// Detects card boundaries in images using edge detection and contour analysis.
/// </summary>
public static class CardDetector
{
    /// <summary>
    /// Standard MTG card aspect ratio (height / width).
    /// Cards are 63mm x 88mm = 1.397 aspect ratio.
    /// </summary>
    private const float CardAspectRatio = 88f / 63f; // ~1.397
    private const float AspectRatioTolerance = 0.25f; // Allow 25% deviation
    
    /// <summary>
    /// Minimum area of detected card relative to image area.
    /// </summary>
    private const float MinCardAreaRatio = 0.05f; // Card must be at least 5% of image
    
    /// <summary>
    /// Maximum area of detected card relative to image area.
    /// </summary>
    private const float MaxCardAreaRatio = 0.98f; // Card can be at most 98% of image
    
    /// <summary>
    /// Result of card detection.
    /// </summary>
    public record CardDetectionResult(
        bool Found,
        SKPoint[] Corners,
        float Confidence,
        string? DebugMessage = null
    )
    {
        public static CardDetectionResult NotFound(string reason) => 
            new(false, Array.Empty<SKPoint>(), 0, reason);
        
        public static CardDetectionResult Success(SKPoint[] corners, float confidence) =>
            new(true, corners, confidence);
    }
    
    /// <summary>
    /// Detect a card in the image and return its corner points.
    /// </summary>
    public static CardDetectionResult DetectCard(SKBitmap image)
    {
        // Step 1: Convert to grayscale
        using var grayscale = ToGrayscale(image);
        
        // Step 2: Apply Gaussian blur to reduce noise
        using var blurred = ApplyGaussianBlur(grayscale, 5);
        
        // Step 3: Apply Canny edge detection
        using var edges = ApplyCannyEdgeDetection(blurred, 50, 150);
        
        // Step 4: Find contours
        var contours = FindContours(edges);
        
        if (contours.Count == 0)
            return CardDetectionResult.NotFound("No contours found");
        
        // Step 5: Find the best card-like quadrilateral
        var imageArea = image.Width * image.Height;
        var bestQuad = FindBestCardQuadrilateral(contours, imageArea);
        
        if (bestQuad == null)
            return CardDetectionResult.NotFound("No card-like quadrilateral found");
        
        // Step 6: Order corners consistently (top-left, top-right, bottom-right, bottom-left)
        var orderedCorners = OrderCorners(bestQuad);
        
        // Calculate confidence based on how well the shape matches a card
        var confidence = CalculateConfidence(orderedCorners, imageArea);
        
        return CardDetectionResult.Success(orderedCorners, confidence);
    }
    
    /// <summary>
    /// Convert image to grayscale.
    /// </summary>
    private static SKBitmap ToGrayscale(SKBitmap source)
    {
        var result = new SKBitmap(source.Width, source.Height, SKColorType.Gray8, SKAlphaType.Opaque);
        
        for (var y = 0; y < source.Height; y++)
        {
            for (var x = 0; x < source.Width; x++)
            {
                var pixel = source.GetPixel(x, y);
                var gray = (byte)(0.299 * pixel.Red + 0.587 * pixel.Green + 0.114 * pixel.Blue);
                result.SetPixel(x, y, new SKColor(gray, gray, gray));
            }
        }
        
        return result;
    }
    
    /// <summary>
    /// Apply Gaussian blur using a simple box blur approximation.
    /// </summary>
    private static SKBitmap ApplyGaussianBlur(SKBitmap source, int radius)
    {
        var result = new SKBitmap(source.Width, source.Height, source.ColorType, source.AlphaType);
        
        // Use SkiaSharp's built-in blur
        using var surface = SKSurface.Create(new SKImageInfo(source.Width, source.Height));
        var canvas = surface.Canvas;
        
        using var paint = new SKPaint
        {
            ImageFilter = SKImageFilter.CreateBlur(radius, radius)
        };
        
        canvas.DrawBitmap(source, 0, 0, paint);
        
        using var image = surface.Snapshot();
        image.ReadPixels(result.Info, result.GetPixels(), result.RowBytes, 0, 0);
        
        return result;
    }
    
    /// <summary>
    /// Apply Canny edge detection.
    /// </summary>
    private static SKBitmap ApplyCannyEdgeDetection(SKBitmap source, int lowThreshold, int highThreshold)
    {
        var width = source.Width;
        var height = source.Height;
        
        // Step 1: Compute gradients using Sobel operators
        var gradientX = new float[height, width];
        var gradientY = new float[height, width];
        var magnitude = new float[height, width];
        var direction = new float[height, width];
        
        // Sobel kernels
        int[,] sobelX = { { -1, 0, 1 }, { -2, 0, 2 }, { -1, 0, 1 } };
        int[,] sobelY = { { -1, -2, -1 }, { 0, 0, 0 }, { 1, 2, 1 } };
        
        for (var y = 1; y < height - 1; y++)
        {
            for (var x = 1; x < width - 1; x++)
            {
                float gx = 0, gy = 0;
                
                for (var ky = -1; ky <= 1; ky++)
                {
                    for (var kx = -1; kx <= 1; kx++)
                    {
                        var pixel = source.GetPixel(x + kx, y + ky).Red;
                        gx += pixel * sobelX[ky + 1, kx + 1];
                        gy += pixel * sobelY[ky + 1, kx + 1];
                    }
                }
                
                gradientX[y, x] = gx;
                gradientY[y, x] = gy;
                magnitude[y, x] = MathF.Sqrt(gx * gx + gy * gy);
                direction[y, x] = MathF.Atan2(gy, gx);
            }
        }
        
        // Step 2: Non-maximum suppression
        var suppressed = new float[height, width];
        
        for (var y = 1; y < height - 1; y++)
        {
            for (var x = 1; x < width - 1; x++)
            {
                var angle = direction[y, x] * 180 / MathF.PI;
                if (angle < 0) angle += 180;
                
                float neighbor1, neighbor2;
                
                if (angle is < 22.5f or >= 157.5f)
                {
                    neighbor1 = magnitude[y, x - 1];
                    neighbor2 = magnitude[y, x + 1];
                }
                else if (angle is >= 22.5f and < 67.5f)
                {
                    neighbor1 = magnitude[y - 1, x + 1];
                    neighbor2 = magnitude[y + 1, x - 1];
                }
                else if (angle is >= 67.5f and < 112.5f)
                {
                    neighbor1 = magnitude[y - 1, x];
                    neighbor2 = magnitude[y + 1, x];
                }
                else
                {
                    neighbor1 = magnitude[y - 1, x - 1];
                    neighbor2 = magnitude[y + 1, x + 1];
                }
                
                if (magnitude[y, x] >= neighbor1 && magnitude[y, x] >= neighbor2)
                    suppressed[y, x] = magnitude[y, x];
            }
        }
        
        // Step 3: Double thresholding and edge tracking
        var result = new SKBitmap(width, height, SKColorType.Gray8, SKAlphaType.Opaque);
        var strong = new bool[height, width];
        var weak = new bool[height, width];
        
        for (var y = 0; y < height; y++)
        {
            for (var x = 0; x < width; x++)
            {
                if (suppressed[y, x] >= highThreshold)
                    strong[y, x] = true;
                else if (suppressed[y, x] >= lowThreshold)
                    weak[y, x] = true;
            }
        }
        
        // Edge tracking by hysteresis
        for (var y = 1; y < height - 1; y++)
        {
            for (var x = 1; x < width - 1; x++)
            {
                byte value = 0;
                
                if (strong[y, x])
                {
                    value = 255;
                }
                else if (weak[y, x])
                {
                    // Check if connected to strong edge
                    for (var dy = -1; dy <= 1; dy++)
                    {
                        for (var dx = -1; dx <= 1; dx++)
                        {
                            if (strong[y + dy, x + dx])
                            {
                                value = 255;
                                break;
                            }
                        }
                        if (value == 255) break;
                    }
                }
                
                result.SetPixel(x, y, new SKColor(value, value, value));
            }
        }
        
        return result;
    }
    
    /// <summary>
    /// Find contours in a binary edge image.
    /// </summary>
    private static List<List<SKPoint>> FindContours(SKBitmap edges)
    {
        var width = edges.Width;
        var height = edges.Height;
        var visited = new bool[height, width];
        var contours = new List<List<SKPoint>>();
        
        // Simple contour tracing
        for (var y = 1; y < height - 1; y++)
        {
            for (var x = 1; x < width - 1; x++)
            {
                if (visited[y, x]) continue;
                if (edges.GetPixel(x, y).Red < 128) continue;
                
                var contour = TraceContour(edges, x, y, visited);
                if (contour.Count >= 4)
                {
                    contours.Add(contour);
                }
            }
        }
        
        return contours;
    }
    
    /// <summary>
    /// Trace a contour starting from a point.
    /// </summary>
    private static List<SKPoint> TraceContour(SKBitmap edges, int startX, int startY, bool[,] visited)
    {
        var contour = new List<SKPoint>();
        var queue = new Queue<(int x, int y)>();
        queue.Enqueue((startX, startY));
        
        // 8-directional neighbors
        int[] dx = { -1, 0, 1, 1, 1, 0, -1, -1 };
        int[] dy = { -1, -1, -1, 0, 1, 1, 1, 0 };
        
        while (queue.Count > 0 && contour.Count < 10000) // Limit to prevent runaway
        {
            var (x, y) = queue.Dequeue();
            
            if (x < 0 || x >= edges.Width || y < 0 || y >= edges.Height)
                continue;
            if (visited[y, x])
                continue;
            if (edges.GetPixel(x, y).Red < 128)
                continue;
            
            visited[y, x] = true;
            contour.Add(new SKPoint(x, y));
            
            for (var i = 0; i < 8; i++)
            {
                queue.Enqueue((x + dx[i], y + dy[i]));
            }
        }
        
        return contour;
    }
    
    /// <summary>
    /// Find the best quadrilateral that matches a card shape.
    /// </summary>
    private static SKPoint[]? FindBestCardQuadrilateral(List<List<SKPoint>> contours, float imageArea)
    {
        SKPoint[]? bestQuad = null;
        var bestScore = float.MinValue;
        
        foreach (var contour in contours)
        {
            // Simplify contour using Douglas-Peucker algorithm
            var simplified = SimplifyContour(contour, contour.Count * 0.02f);
            
            // Try to approximate as quadrilateral
            var quad = ApproximateQuadrilateral(simplified);
            if (quad == null) continue;
            
            // Check if it's a valid card shape
            var area = CalculateQuadArea(quad);
            var areaRatio = area / imageArea;
            
            if (areaRatio < MinCardAreaRatio || areaRatio > MaxCardAreaRatio)
                continue;
            
            // Check aspect ratio
            var aspectScore = CalculateAspectRatioScore(quad);
            if (aspectScore < 0.5f) continue;
            
            // Check if it's convex
            if (!IsConvex(quad)) continue;
            
            // Score based on area (prefer larger) and aspect ratio match
            var score = areaRatio * aspectScore;
            
            if (score > bestScore)
            {
                bestScore = score;
                bestQuad = quad;
            }
        }
        
        return bestQuad;
    }
    
    /// <summary>
    /// Simplify a contour using Douglas-Peucker algorithm.
    /// </summary>
    private static List<SKPoint> SimplifyContour(List<SKPoint> contour, float epsilon)
    {
        if (contour.Count < 3) return contour;
        
        // Find the point farthest from line between first and last
        var first = contour[0];
        var last = contour[^1];
        
        var maxDist = 0f;
        var maxIndex = 0;
        
        for (var i = 1; i < contour.Count - 1; i++)
        {
            var dist = PointToLineDistance(contour[i], first, last);
            if (dist > maxDist)
            {
                maxDist = dist;
                maxIndex = i;
            }
        }
        
        if (maxDist > epsilon)
        {
            var left = SimplifyContour(contour.Take(maxIndex + 1).ToList(), epsilon);
            var right = SimplifyContour(contour.Skip(maxIndex).ToList(), epsilon);
            
            return left.Take(left.Count - 1).Concat(right).ToList();
        }
        
        return new List<SKPoint> { first, last };
    }
    
    /// <summary>
    /// Calculate distance from point to line.
    /// </summary>
    private static float PointToLineDistance(SKPoint point, SKPoint lineStart, SKPoint lineEnd)
    {
        var dx = lineEnd.X - lineStart.X;
        var dy = lineEnd.Y - lineStart.Y;
        var lengthSquared = dx * dx + dy * dy;
        
        if (lengthSquared == 0)
            return Distance(point, lineStart);
        
        var t = ((point.X - lineStart.X) * dx + (point.Y - lineStart.Y) * dy) / lengthSquared;
        t = Math.Clamp(t, 0, 1);
        
        var projection = new SKPoint(lineStart.X + t * dx, lineStart.Y + t * dy);
        return Distance(point, projection);
    }
    
    /// <summary>
    /// Try to approximate a contour as a quadrilateral.
    /// </summary>
    private static SKPoint[]? ApproximateQuadrilateral(List<SKPoint> contour)
    {
        if (contour.Count < 4) return null;
        
        // If already 4 points, use them
        if (contour.Count == 4)
            return contour.ToArray();
        
        // Find convex hull
        var hull = ConvexHull(contour);
        if (hull.Count < 4) return null;
        
        // Find 4 corners by finding the 4 points that form the largest quadrilateral
        if (hull.Count == 4)
            return hull.ToArray();
        
        // Find the 4 most extreme points
        var corners = FindExtremePoints(hull);
        return corners.Length == 4 ? corners : null;
    }
    
    /// <summary>
    /// Calculate convex hull using Graham scan.
    /// </summary>
    private static List<SKPoint> ConvexHull(List<SKPoint> points)
    {
        if (points.Count < 3) return points;
        
        // Find bottom-most point (or left-most in case of tie)
        var start = points.OrderBy(p => p.Y).ThenBy(p => p.X).First();
        
        // Sort by polar angle
        var sorted = points
            .Where(p => p != start)
            .OrderBy(p => MathF.Atan2(p.Y - start.Y, p.X - start.X))
            .ThenBy(p => Distance(p, start))
            .ToList();
        
        var hull = new List<SKPoint> { start };
        
        foreach (var point in sorted)
        {
            while (hull.Count > 1 && CrossProduct(hull[^2], hull[^1], point) <= 0)
            {
                hull.RemoveAt(hull.Count - 1);
            }
            hull.Add(point);
        }
        
        return hull;
    }
    
    /// <summary>
    /// Find the 4 most extreme points of a convex hull.
    /// </summary>
    private static SKPoint[] FindExtremePoints(List<SKPoint> hull)
    {
        if (hull.Count <= 4) return hull.ToArray();
        
        // Find points with min/max X and Y
        var minX = hull.OrderBy(p => p.X).First();
        var maxX = hull.OrderByDescending(p => p.X).First();
        var minY = hull.OrderBy(p => p.Y).First();
        var maxY = hull.OrderByDescending(p => p.Y).First();
        
        var extremes = new HashSet<SKPoint> { minX, maxX, minY, maxY };
        
        if (extremes.Count == 4)
            return extremes.ToArray();
        
        // If we have duplicates, add more points
        var sorted = hull.OrderBy(p => MathF.Atan2(p.Y - hull.Average(h => h.Y), p.X - hull.Average(h => h.X))).ToList();
        
        var step = sorted.Count / 4;
        return new[]
        {
            sorted[0],
            sorted[step],
            sorted[step * 2],
            sorted[step * 3]
        };
    }
    
    /// <summary>
    /// Calculate cross product of vectors (b-a) and (c-b).
    /// </summary>
    private static float CrossProduct(SKPoint a, SKPoint b, SKPoint c)
    {
        return (b.X - a.X) * (c.Y - a.Y) - (b.Y - a.Y) * (c.X - a.X);
    }
    
    /// <summary>
    /// Calculate distance between two points.
    /// </summary>
    private static float Distance(SKPoint a, SKPoint b)
    {
        var dx = b.X - a.X;
        var dy = b.Y - a.Y;
        return MathF.Sqrt(dx * dx + dy * dy);
    }
    
    /// <summary>
    /// Calculate the area of a quadrilateral.
    /// </summary>
    private static float CalculateQuadArea(SKPoint[] quad)
    {
        // Shoelace formula
        var area = 0f;
        for (var i = 0; i < 4; i++)
        {
            var j = (i + 1) % 4;
            area += quad[i].X * quad[j].Y;
            area -= quad[j].X * quad[i].Y;
        }
        return Math.Abs(area) / 2;
    }
    
    /// <summary>
    /// Calculate how well the aspect ratio matches a card.
    /// </summary>
    private static float CalculateAspectRatioScore(SKPoint[] quad)
    {
        // Calculate width and height of the quadrilateral
        var width1 = Distance(quad[0], quad[1]);
        var width2 = Distance(quad[2], quad[3]);
        var height1 = Distance(quad[1], quad[2]);
        var height2 = Distance(quad[3], quad[0]);
        
        var avgWidth = (width1 + width2) / 2;
        var avgHeight = (height1 + height2) / 2;
        
        // Ensure height > width (portrait orientation)
        var aspectRatio = avgWidth > avgHeight 
            ? avgWidth / avgHeight 
            : avgHeight / avgWidth;
        
        var expectedRatio = CardAspectRatio;
        var deviation = Math.Abs(aspectRatio - expectedRatio) / expectedRatio;
        
        // Score from 0 to 1 based on how close to expected ratio
        return Math.Max(0, 1 - deviation / AspectRatioTolerance);
    }
    
    /// <summary>
    /// Check if a quadrilateral is convex.
    /// </summary>
    private static bool IsConvex(SKPoint[] quad)
    {
        var sign = 0;
        
        for (var i = 0; i < 4; i++)
        {
            var cross = CrossProduct(quad[i], quad[(i + 1) % 4], quad[(i + 2) % 4]);
            
            if (Math.Abs(cross) < 0.0001f) continue;
            
            var currentSign = cross > 0 ? 1 : -1;
            
            if (sign == 0)
                sign = currentSign;
            else if (sign != currentSign)
                return false;
        }
        
        return true;
    }
    
    /// <summary>
    /// Order corners consistently: top-left, top-right, bottom-right, bottom-left.
    /// </summary>
    private static SKPoint[] OrderCorners(SKPoint[] corners)
    {
        // Find center
        var centerX = corners.Average(c => c.X);
        var centerY = corners.Average(c => c.Y);
        
        // Classify each corner
        var topLeft = corners.Where(c => c.X < centerX && c.Y < centerY).OrderBy(c => c.X + c.Y).FirstOrDefault();
        var topRight = corners.Where(c => c.X >= centerX && c.Y < centerY).OrderBy(c => c.Y - c.X).FirstOrDefault();
        var bottomRight = corners.Where(c => c.X >= centerX && c.Y >= centerY).OrderByDescending(c => c.X + c.Y).FirstOrDefault();
        var bottomLeft = corners.Where(c => c.X < centerX && c.Y >= centerY).OrderByDescending(c => c.Y - c.X).FirstOrDefault();
        
        // Handle edge cases by sorting by angle from center
        if (topLeft == default || topRight == default || bottomRight == default || bottomLeft == default)
        {
            var sorted = corners.OrderBy(c => MathF.Atan2(c.Y - centerY, c.X - centerX)).ToArray();
            // Rotate to start with top-left
            var minSum = sorted.Select((c, i) => (c.X + c.Y, i)).Min().i;
            return sorted.Skip(minSum).Concat(sorted.Take(minSum)).ToArray();
        }
        
        return new[] { topLeft, topRight, bottomRight, bottomLeft };
    }
    
    /// <summary>
    /// Calculate confidence of the detection.
    /// </summary>
    private static float CalculateConfidence(SKPoint[] corners, float imageArea)
    {
        var area = CalculateQuadArea(corners);
        var areaScore = Math.Min(area / imageArea / 0.5f, 1f); // Prefer larger cards
        var aspectScore = CalculateAspectRatioScore(corners);
        
        return areaScore * 0.4f + aspectScore * 0.6f;
    }
}
