using System.IO.Compression;
using System.Text;

namespace QYZH.InteractiveMagazine.Common.Helpers;

/// <summary>
/// 二维码帮助类
/// </summary>
public static class QrCodeHelper
{
    private const int Version = 4;
    private const int Size = Version * 4 + 17;
    private const int DataCodewordCount = 80;
    private const int ErrorCorrectionCodewordCount = 20;
    private const int QuietZone = 4;
    private const int Scale = 10;
    private static readonly int[] AlignmentPatternPositions = [6, 26];

    /// <summary>
    /// 生成二维码PNG图片字节
    /// </summary>
    /// <param name="content">二维码内容</param>
    /// <returns>PNG图片字节</returns>
    public static byte[] GeneratePng(string content)
    {
        var dataCodewords = CreateDataCodewords(content);
        var allCodewords = dataCodewords.Concat(CreateErrorCorrectionCodewords(dataCodewords)).ToArray();
        var modules = BuildModules(allCodewords);
        var imageSize = (Size + QuietZone * 2) * Scale;
        var pixels = new byte[imageSize * imageSize * 4];

        for (var i = 0; i < pixels.Length; i += 4)
        {
            pixels[i] = 255;
            pixels[i + 1] = 255;
            pixels[i + 2] = 255;
            pixels[i + 3] = 255;
        }

        for (var y = 0; y < Size; y++)
        {
            for (var x = 0; x < Size; x++)
            {
                if (modules[y, x])
                {
                    FillModule(pixels, imageSize, x + QuietZone, y + QuietZone);
                }
            }
        }

        return EncodePng(imageSize, imageSize, pixels);
    }

    private static byte[] CreateDataCodewords(string content)
    {
        var bytes = Encoding.UTF8.GetBytes(content);
        if (bytes.Length > 78)
        {
            throw new InvalidOperationException("二维码内容过长");
        }

        var bits = new List<bool>();
        AppendBits(bits, 0b0100, 4);
        AppendBits(bits, bytes.Length, 8);
        foreach (var value in bytes)
        {
            AppendBits(bits, value, 8);
        }

        var remaining = DataCodewordCount * 8 - bits.Count;
        AppendBits(bits, 0, Math.Min(4, remaining));

        while (bits.Count % 8 != 0)
        {
            bits.Add(false);
        }

        var data = BitsToBytes(bits);
        var pad = true;
        while (data.Count < DataCodewordCount)
        {
            data.Add((byte)(pad ? 0xEC : 0x11));
            pad = !pad;
        }

        return data.ToArray();
    }

    private static bool[,] BuildModules(byte[] codewords)
    {
        var modules = new bool[Size, Size];
        var reserved = new bool[Size, Size];

        AddFinder(modules, reserved, 0, 0);
        AddFinder(modules, reserved, Size - 7, 0);
        AddFinder(modules, reserved, 0, Size - 7);
        AddTimingPatterns(modules, reserved);
        AddAlignmentPatterns(modules, reserved);
        ReserveFormatAreas(reserved);

        modules[Version * 4 + 9, 8] = true;
        reserved[Version * 4 + 9, 8] = true;

        AddDataModules(modules, reserved, codewords);
        AddFormatBits(modules);

        return modules;
    }

    private static void AddFinder(bool[,] modules, bool[,] reserved, int left, int top)
    {
        for (var y = -1; y <= 7; y++)
        {
            for (var x = -1; x <= 7; x++)
            {
                var px = left + x;
                var py = top + y;
                if (px < 0 || py < 0 || px >= Size || py >= Size)
                {
                    continue;
                }

                reserved[py, px] = true;
                modules[py, px] = x >= 0 && x <= 6 && y >= 0 && y <= 6
                    && (x == 0 || x == 6 || y == 0 || y == 6 || (x >= 2 && x <= 4 && y >= 2 && y <= 4));
            }
        }
    }

    private static void AddTimingPatterns(bool[,] modules, bool[,] reserved)
    {
        for (var i = 8; i < Size - 8; i++)
        {
            var isDark = i % 2 == 0;
            modules[6, i] = isDark;
            modules[i, 6] = isDark;
            reserved[6, i] = true;
            reserved[i, 6] = true;
        }
    }

    private static void AddAlignmentPatterns(bool[,] modules, bool[,] reserved)
    {
        foreach (var centerY in AlignmentPatternPositions)
        {
            foreach (var centerX in AlignmentPatternPositions)
            {
                if (reserved[centerY, centerX])
                {
                    continue;
                }

                for (var y = -2; y <= 2; y++)
                {
                    for (var x = -2; x <= 2; x++)
                    {
                        var px = centerX + x;
                        var py = centerY + y;
                        reserved[py, px] = true;
                        modules[py, px] = Math.Max(Math.Abs(x), Math.Abs(y)) != 1;
                    }
                }
            }
        }
    }

    private static void ReserveFormatAreas(bool[,] reserved)
    {
        for (var i = 0; i <= 8; i++)
        {
            reserved[8, i] = true;
            reserved[i, 8] = true;
        }

        for (var i = 0; i < 8; i++)
        {
            reserved[8, Size - 1 - i] = true;
        }

        for (var i = 0; i < 7; i++)
        {
            reserved[Size - 1 - i, 8] = true;
        }
    }

    private static void AddDataModules(bool[,] modules, bool[,] reserved, byte[] codewords)
    {
        var bits = new List<bool>();
        foreach (var codeword in codewords)
        {
            AppendBits(bits, codeword, 8);
        }

        var bitIndex = 0;
        var upward = true;
        for (var right = Size - 1; right >= 1; right -= 2)
        {
            if (right == 6)
            {
                right--;
            }

            for (var i = 0; i < Size; i++)
            {
                var y = upward ? Size - 1 - i : i;
                for (var j = 0; j < 2; j++)
                {
                    var x = right - j;
                    if (reserved[y, x])
                    {
                        continue;
                    }

                    var bit = bitIndex < bits.Count && bits[bitIndex++];
                    if ((x + y) % 2 == 0)
                    {
                        bit = !bit;
                    }

                    modules[y, x] = bit;
                }
            }

            upward = !upward;
        }
    }

    private static void AddFormatBits(bool[,] modules)
    {
        var format = GetFormatBits();

        for (var i = 0; i <= 5; i++)
        {
            modules[i, 8] = GetBit(format, i);
        }

        modules[7, 8] = GetBit(format, 6);
        modules[8, 8] = GetBit(format, 7);
        modules[8, 7] = GetBit(format, 8);

        for (var i = 9; i < 15; i++)
        {
            modules[8, 14 - i] = GetBit(format, i);
        }

        for (var i = 0; i < 8; i++)
        {
            modules[8, Size - 1 - i] = GetBit(format, i);
        }

        for (var i = 8; i < 15; i++)
        {
            modules[Size - 15 + i, 8] = GetBit(format, i);
        }

        modules[Size - 8, 8] = true;
    }

    private static int GetFormatBits()
    {
        const int errorCorrectionLevelBits = 1;
        const int mask = 0;
        var data = (errorCorrectionLevelBits << 3) | mask;
        var value = data << 10;
        const int generator = 0x537;

        for (var i = 14; i >= 10; i--)
        {
            if (((value >> i) & 1) != 0)
            {
                value ^= generator << (i - 10);
            }
        }

        return ((data << 10) | (value & 0x3FF)) ^ 0x5412;
    }

    private static byte[] CreateErrorCorrectionCodewords(byte[] data)
    {
        var generator = CreateGeneratorPolynomial(ErrorCorrectionCodewordCount);
        var result = new byte[ErrorCorrectionCodewordCount];

        foreach (var b in data)
        {
            var factor = b ^ result[0];
            Array.Copy(result, 1, result, 0, result.Length - 1);
            result[^1] = 0;

            for (var i = 0; i < result.Length; i++)
            {
                result[i] ^= GaloisMultiply(generator[i + 1], factor);
            }
        }

        return result;
    }

    private static byte[] CreateGeneratorPolynomial(int degree)
    {
        var result = new List<byte> { 1 };
        for (var i = 0; i < degree; i++)
        {
            var next = new byte[result.Count + 1];
            for (var j = 0; j < result.Count; j++)
            {
                next[j] ^= result[j];
                next[j + 1] ^= GaloisMultiply(result[j], GaloisPower(i));
            }

            result = next.ToList();
        }

        return result.ToArray();
    }

    private static byte GaloisPower(int exponent)
    {
        var value = 1;
        for (var i = 0; i < exponent; i++)
        {
            value <<= 1;
            if ((value & 0x100) != 0)
            {
                value ^= 0x11D;
            }
        }

        return (byte)value;
    }

    private static byte GaloisMultiply(int x, int y)
    {
        var result = 0;
        while (y != 0)
        {
            if ((y & 1) != 0)
            {
                result ^= x;
            }

            x <<= 1;
            if ((x & 0x100) != 0)
            {
                x ^= 0x11D;
            }

            y >>= 1;
        }

        return (byte)result;
    }

    private static void AppendBits(List<bool> bits, int value, int count)
    {
        for (var i = count - 1; i >= 0; i--)
        {
            bits.Add(((value >> i) & 1) != 0);
        }
    }

    private static List<byte> BitsToBytes(List<bool> bits)
    {
        var result = new List<byte>();
        for (var i = 0; i < bits.Count; i += 8)
        {
            var value = 0;
            for (var j = 0; j < 8; j++)
            {
                value = (value << 1) | (bits[i + j] ? 1 : 0);
            }

            result.Add((byte)value);
        }

        return result;
    }

    private static bool GetBit(int value, int index)
    {
        return ((value >> index) & 1) != 0;
    }

    private static void FillModule(byte[] pixels, int imageSize, int moduleX, int moduleY)
    {
        var startX = moduleX * Scale;
        var startY = moduleY * Scale;

        for (var y = 0; y < Scale; y++)
        {
            for (var x = 0; x < Scale; x++)
            {
                var index = ((startY + y) * imageSize + startX + x) * 4;
                pixels[index] = 0;
                pixels[index + 1] = 0;
                pixels[index + 2] = 0;
                pixels[index + 3] = 255;
            }
        }
    }

    private static byte[] EncodePng(int width, int height, byte[] rgba)
    {
        using var output = new MemoryStream();
        WriteUInt(output, 0x89504E47);
        WriteUInt(output, 0x0D0A1A0A);

        using (var ihdr = new MemoryStream())
        {
            WriteUInt(ihdr, (uint)width);
            WriteUInt(ihdr, (uint)height);
            ihdr.WriteByte(8);
            ihdr.WriteByte(6);
            ihdr.WriteByte(0);
            ihdr.WriteByte(0);
            ihdr.WriteByte(0);
            WriteChunk(output, "IHDR", ihdr.ToArray());
        }

        var stride = width * 4;
        using (var raw = new MemoryStream())
        {
            for (var y = 0; y < height; y++)
            {
                raw.WriteByte(0);
                raw.Write(rgba, y * stride, stride);
            }

            using var compressed = new MemoryStream();
            using (var zlib = new ZLibStream(compressed, CompressionLevel.SmallestSize, true))
            {
                raw.Position = 0;
                raw.CopyTo(zlib);
            }

            WriteChunk(output, "IDAT", compressed.ToArray());
        }

        WriteChunk(output, "IEND", []);
        return output.ToArray();
    }

    private static void WriteChunk(Stream stream, string type, byte[] data)
    {
        WriteUInt(stream, (uint)data.Length);
        var typeBytes = Encoding.ASCII.GetBytes(type);
        stream.Write(typeBytes);
        stream.Write(data);
        WriteUInt(stream, Crc32(typeBytes.Concat(data).ToArray()));
    }

    private static void WriteUInt(Stream stream, uint value)
    {
        stream.WriteByte((byte)(value >> 24));
        stream.WriteByte((byte)(value >> 16));
        stream.WriteByte((byte)(value >> 8));
        stream.WriteByte((byte)value);
    }

    private static uint Crc32(byte[] bytes)
    {
        var crc = 0xFFFFFFFFu;
        foreach (var b in bytes)
        {
            crc ^= b;
            for (var i = 0; i < 8; i++)
            {
                crc = (crc & 1) == 1 ? (crc >> 1) ^ 0xEDB88320u : crc >> 1;
            }
        }

        return ~crc;
    }
}
