using SkiaSharp;

namespace Scry.Core.Imaging;

public static class PerceptualHash
{
    private const int HashSize = 32;
    private const int DctSize = 32;
    
    public static byte[] ComputeHash(SKBitmap image)
    {
        using var resized = ImagePreprocessor.Resize(image, DctSize, DctSize);
        using var grayscale = ImagePreprocessor.ToGrayscale(resized);
        
        var pixels = new double[DctSize, DctSize];
        for (var y = 0; y < DctSize; y++)
        {
            for (var x = 0; x < DctSize; x++)
            {
                var pixel = grayscale.GetPixel(x, y);
                pixels[y, x] = pixel.Red;
            }
        }
        
        var dct = ComputeDct(pixels);
        
        var lowFreq = new double[8 * 8];
        var idx = 0;
        for (var y = 0; y < 8; y++)
        {
            for (var x = 0; x < 8; x++)
            {
                if (x == 0 && y == 0) continue;
                lowFreq[idx++] = dct[y, x];
            }
        }
        
        var median = GetMedian(lowFreq.Take(63).ToArray());
        
        var hash = new byte[8];
        idx = 0;
        for (var y = 0; y < 8; y++)
        {
            for (var x = 0; x < 8; x++)
            {
                if (x == 0 && y == 0) continue;
                if (dct[y, x] > median)
                {
                    hash[idx / 8] |= (byte)(1 << (7 - idx % 8));
                }
                idx++;
            }
        }
        
        return hash;
    }
    
    public static byte[] ComputeColorHash(SKBitmap image)
    {
        using var resized = ImagePreprocessor.Resize(image, DctSize, DctSize);
        
        var redPixels = new double[DctSize, DctSize];
        var greenPixels = new double[DctSize, DctSize];
        var bluePixels = new double[DctSize, DctSize];
        
        for (var y = 0; y < DctSize; y++)
        {
            for (var x = 0; x < DctSize; x++)
            {
                var pixel = resized.GetPixel(x, y);
                redPixels[y, x] = pixel.Red;
                greenPixels[y, x] = pixel.Green;
                bluePixels[y, x] = pixel.Blue;
            }
        }
        
        var redHash = ComputeChannelHash(redPixels);
        var greenHash = ComputeChannelHash(greenPixels);
        var blueHash = ComputeChannelHash(bluePixels);
        
        var combined = new byte[redHash.Length + greenHash.Length + blueHash.Length];
        Array.Copy(redHash, 0, combined, 0, redHash.Length);
        Array.Copy(greenHash, 0, combined, redHash.Length, greenHash.Length);
        Array.Copy(blueHash, 0, combined, redHash.Length + greenHash.Length, blueHash.Length);
        
        return combined;
    }
    
    private static byte[] ComputeChannelHash(double[,] pixels)
    {
        var dct = ComputeDct(pixels);
        
        var lowFreq = new double[63];
        var idx = 0;
        for (var y = 0; y < 8; y++)
        {
            for (var x = 0; x < 8; x++)
            {
                if (x == 0 && y == 0) continue;
                lowFreq[idx++] = dct[y, x];
            }
        }
        
        var median = GetMedian(lowFreq);
        
        var hash = new byte[8];
        idx = 0;
        for (var y = 0; y < 8; y++)
        {
            for (var x = 0; x < 8; x++)
            {
                if (x == 0 && y == 0) continue;
                if (dct[y, x] > median)
                {
                    hash[idx / 8] |= (byte)(1 << (7 - idx % 8));
                }
                idx++;
            }
        }
        
        return hash;
    }
    
    private static double[,] ComputeDct(double[,] input)
    {
        var n = input.GetLength(0);
        var output = new double[n, n];
        
        var coefficients = new double[n];
        coefficients[0] = 1.0 / Math.Sqrt(n);
        for (var i = 1; i < n; i++)
        {
            coefficients[i] = Math.Sqrt(2.0 / n);
        }
        
        var cosTable = new double[n, n];
        for (var i = 0; i < n; i++)
        {
            for (var j = 0; j < n; j++)
            {
                cosTable[i, j] = Math.Cos((2 * j + 1) * i * Math.PI / (2 * n));
            }
        }
        
        var temp = new double[n, n];
        for (var u = 0; u < n; u++)
        {
            for (var y = 0; y < n; y++)
            {
                var sum = 0.0;
                for (var x = 0; x < n; x++)
                {
                    sum += input[y, x] * cosTable[u, x];
                }
                temp[u, y] = sum * coefficients[u];
            }
        }
        
        for (var u = 0; u < n; u++)
        {
            for (var v = 0; v < n; v++)
            {
                var sum = 0.0;
                for (var y = 0; y < n; y++)
                {
                    sum += temp[u, y] * cosTable[v, y];
                }
                output[v, u] = sum * coefficients[v];
            }
        }
        
        return output;
    }
    
    private static double GetMedian(double[] values)
    {
        var sorted = values.OrderBy(v => v).ToArray();
        var mid = sorted.Length / 2;
        return sorted.Length % 2 == 0 
            ? (sorted[mid - 1] + sorted[mid]) / 2 
            : sorted[mid];
    }
    
    public static int HammingDistance(byte[] hash1, byte[] hash2)
    {
        if (hash1.Length != hash2.Length)
            throw new ArgumentException("Hash lengths must match");
        
        var distance = 0;
        for (var i = 0; i < hash1.Length; i++)
        {
            var xor = hash1[i] ^ hash2[i];
            while (xor != 0)
            {
                distance += xor & 1;
                xor >>= 1;
            }
        }
        
        return distance;
    }
    
    public static float CalculateConfidence(int hammingDistance, int hashBits)
    {
        var similarity = 1.0f - (float)hammingDistance / hashBits;
        return similarity;
    }
}
