using SkiaSharp;

namespace Scry.Core.Imaging;

/// <summary>
/// Performs perspective correction to transform a quadrilateral region into a rectangle.
/// </summary>
public static class PerspectiveCorrection
{
    /// <summary>
    /// Standard output size for corrected card images.
    /// Using 480x670 to maintain card aspect ratio (63:88).
    /// </summary>
    public const int OutputWidth = 480;
    public const int OutputHeight = 670;
    
    /// <summary>
    /// Apply perspective correction to extract and normalize a card from an image.
    /// </summary>
    /// <param name="source">Source image containing the card.</param>
    /// <param name="corners">Four corners of the card in order: top-left, top-right, bottom-right, bottom-left.</param>
    /// <param name="outputWidth">Width of the output image.</param>
    /// <param name="outputHeight">Height of the output image.</param>
    /// <returns>A new bitmap with the perspective-corrected card.</returns>
    public static SKBitmap WarpPerspective(
        SKBitmap source, 
        SKPoint[] corners,
        int outputWidth = OutputWidth,
        int outputHeight = OutputHeight)
    {
        if (corners.Length != 4)
            throw new ArgumentException("Exactly 4 corners required", nameof(corners));
        
        // Determine if the card is in landscape orientation (rotated 90°)
        var width1 = Distance(corners[0], corners[1]);
        var height1 = Distance(corners[1], corners[2]);
        
        // If width > height, card is landscape - swap dimensions and reorder corners
        SKPoint[] orderedCorners;
        int targetWidth, targetHeight;
        
        if (width1 > height1)
        {
            // Card is landscape - rotate corners to portrait
            orderedCorners = new[] { corners[1], corners[2], corners[3], corners[0] };
            targetWidth = outputWidth;
            targetHeight = outputHeight;
        }
        else
        {
            orderedCorners = corners;
            targetWidth = outputWidth;
            targetHeight = outputHeight;
        }
        
        // Compute the perspective transform matrix
        var matrix = ComputePerspectiveTransform(orderedCorners, targetWidth, targetHeight);
        
        // Apply the transform
        var result = new SKBitmap(targetWidth, targetHeight, SKColorType.Rgba8888, SKAlphaType.Premul);
        
        for (var y = 0; y < targetHeight; y++)
        {
            for (var x = 0; x < targetWidth; x++)
            {
                // Apply inverse transform to find source coordinates
                var srcPoint = ApplyInverseTransform(matrix, x, y);
                
                // Bilinear interpolation for smooth sampling
                var color = SampleBilinear(source, srcPoint.X, srcPoint.Y);
                result.SetPixel(x, y, color);
            }
        }
        
        return result;
    }
    
    /// <summary>
    /// Compute a perspective transform matrix from quad corners to rectangle.
    /// Uses the Direct Linear Transform (DLT) algorithm.
    /// </summary>
    private static float[] ComputePerspectiveTransform(SKPoint[] src, int dstWidth, int dstHeight)
    {
        // Destination corners (rectangle)
        var dst = new SKPoint[]
        {
            new(0, 0),
            new(dstWidth - 1, 0),
            new(dstWidth - 1, dstHeight - 1),
            new(0, dstHeight - 1)
        };
        
        // Build the 8x8 matrix for solving the homography
        // We're solving for the 8 parameters of the 3x3 perspective matrix (h33 = 1)
        var A = new double[8, 8];
        var b = new double[8];
        
        for (var i = 0; i < 4; i++)
        {
            var sx = src[i].X;
            var sy = src[i].Y;
            var dx = dst[i].X;
            var dy = dst[i].Y;
            
            A[i * 2, 0] = sx;
            A[i * 2, 1] = sy;
            A[i * 2, 2] = 1;
            A[i * 2, 3] = 0;
            A[i * 2, 4] = 0;
            A[i * 2, 5] = 0;
            A[i * 2, 6] = -dx * sx;
            A[i * 2, 7] = -dx * sy;
            b[i * 2] = dx;
            
            A[i * 2 + 1, 0] = 0;
            A[i * 2 + 1, 1] = 0;
            A[i * 2 + 1, 2] = 0;
            A[i * 2 + 1, 3] = sx;
            A[i * 2 + 1, 4] = sy;
            A[i * 2 + 1, 5] = 1;
            A[i * 2 + 1, 6] = -dy * sx;
            A[i * 2 + 1, 7] = -dy * sy;
            b[i * 2 + 1] = dy;
        }
        
        // Solve using Gaussian elimination
        var h = SolveLinearSystem(A, b);
        
        // Return the 3x3 matrix as a flat array [h11, h12, h13, h21, h22, h23, h31, h32, h33]
        return new float[]
        {
            (float)h[0], (float)h[1], (float)h[2],
            (float)h[3], (float)h[4], (float)h[5],
            (float)h[6], (float)h[7], 1f
        };
    }
    
    /// <summary>
    /// Solve a linear system Ax = b using Gaussian elimination with partial pivoting.
    /// </summary>
    private static double[] SolveLinearSystem(double[,] A, double[] b)
    {
        var n = b.Length;
        var augmented = new double[n, n + 1];
        
        // Create augmented matrix
        for (var i = 0; i < n; i++)
        {
            for (var j = 0; j < n; j++)
                augmented[i, j] = A[i, j];
            augmented[i, n] = b[i];
        }
        
        // Forward elimination with partial pivoting
        for (var col = 0; col < n; col++)
        {
            // Find pivot
            var maxRow = col;
            for (var row = col + 1; row < n; row++)
            {
                if (Math.Abs(augmented[row, col]) > Math.Abs(augmented[maxRow, col]))
                    maxRow = row;
            }
            
            // Swap rows
            for (var j = 0; j <= n; j++)
            {
                (augmented[col, j], augmented[maxRow, j]) = (augmented[maxRow, j], augmented[col, j]);
            }
            
            // Eliminate
            for (var row = col + 1; row < n; row++)
            {
                if (Math.Abs(augmented[col, col]) < 1e-10) continue;
                
                var factor = augmented[row, col] / augmented[col, col];
                for (var j = col; j <= n; j++)
                {
                    augmented[row, j] -= factor * augmented[col, j];
                }
            }
        }
        
        // Back substitution
        var x = new double[n];
        for (var i = n - 1; i >= 0; i--)
        {
            x[i] = augmented[i, n];
            for (var j = i + 1; j < n; j++)
            {
                x[i] -= augmented[i, j] * x[j];
            }
            if (Math.Abs(augmented[i, i]) > 1e-10)
                x[i] /= augmented[i, i];
        }
        
        return x;
    }
    
    /// <summary>
    /// Apply the inverse of the perspective transform to map destination to source coordinates.
    /// </summary>
    private static SKPoint ApplyInverseTransform(float[] H, float x, float y)
    {
        // H maps src -> dst, we need dst -> src
        // Compute inverse of H
        var inv = InvertMatrix3x3(H);
        
        // Apply inverse transform
        var w = inv[6] * x + inv[7] * y + inv[8];
        if (Math.Abs(w) < 1e-10) w = 1e-10f;
        
        var srcX = (inv[0] * x + inv[1] * y + inv[2]) / w;
        var srcY = (inv[3] * x + inv[4] * y + inv[5]) / w;
        
        return new SKPoint(srcX, srcY);
    }
    
    /// <summary>
    /// Invert a 3x3 matrix.
    /// </summary>
    private static float[] InvertMatrix3x3(float[] m)
    {
        var det = m[0] * (m[4] * m[8] - m[5] * m[7])
                - m[1] * (m[3] * m[8] - m[5] * m[6])
                + m[2] * (m[3] * m[7] - m[4] * m[6]);
        
        if (Math.Abs(det) < 1e-10f)
            det = 1e-10f;
        
        var invDet = 1f / det;
        
        return new float[]
        {
            (m[4] * m[8] - m[5] * m[7]) * invDet,
            (m[2] * m[7] - m[1] * m[8]) * invDet,
            (m[1] * m[5] - m[2] * m[4]) * invDet,
            (m[5] * m[6] - m[3] * m[8]) * invDet,
            (m[0] * m[8] - m[2] * m[6]) * invDet,
            (m[2] * m[3] - m[0] * m[5]) * invDet,
            (m[3] * m[7] - m[4] * m[6]) * invDet,
            (m[1] * m[6] - m[0] * m[7]) * invDet,
            (m[0] * m[4] - m[1] * m[3]) * invDet
        };
    }
    
    /// <summary>
    /// Sample a pixel using bilinear interpolation.
    /// </summary>
    private static SKColor SampleBilinear(SKBitmap source, float x, float y)
    {
        // Clamp to valid range
        x = Math.Clamp(x, 0, source.Width - 1);
        y = Math.Clamp(y, 0, source.Height - 1);
        
        var x0 = (int)Math.Floor(x);
        var y0 = (int)Math.Floor(y);
        var x1 = Math.Min(x0 + 1, source.Width - 1);
        var y1 = Math.Min(y0 + 1, source.Height - 1);
        
        var xFrac = x - x0;
        var yFrac = y - y0;
        
        var c00 = source.GetPixel(x0, y0);
        var c10 = source.GetPixel(x1, y0);
        var c01 = source.GetPixel(x0, y1);
        var c11 = source.GetPixel(x1, y1);
        
        // Interpolate
        var r = Lerp(Lerp(c00.Red, c10.Red, xFrac), Lerp(c01.Red, c11.Red, xFrac), yFrac);
        var g = Lerp(Lerp(c00.Green, c10.Green, xFrac), Lerp(c01.Green, c11.Green, xFrac), yFrac);
        var b = Lerp(Lerp(c00.Blue, c10.Blue, xFrac), Lerp(c01.Blue, c11.Blue, xFrac), yFrac);
        var a = Lerp(Lerp(c00.Alpha, c10.Alpha, xFrac), Lerp(c01.Alpha, c11.Alpha, xFrac), yFrac);
        
        return new SKColor((byte)r, (byte)g, (byte)b, (byte)a);
    }
    
    /// <summary>
    /// Linear interpolation.
    /// </summary>
    private static float Lerp(float a, float b, float t)
    {
        return a + (b - a) * t;
    }
    
    /// <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);
    }
}
