using System;
using System.Collections.Generic;
using ZXing.Common;
using ZXing.Common.ReedSolomon;
using ZXing.QrCode.Internal;

namespace Disco.Services.Documents
{
    public static class QRCodeBinaryEncoder
    {
        public static byte[] Encode(string content, ErrorCorrectionLevel ecLevel, out int width, out int height)
        {
            var code = Encoder.encode(content, ecLevel, null);

            var array = code.Matrix.Array;
            width = code.Matrix.Width;
            height = code.Matrix.Height;

            return scaleMatrix(code.Matrix.Array, width, height);
        }

        public static byte[] Encode(string content, ErrorCorrectionLevel ecLevel, int width, int height)
        {
            var code = Encoder.encode(content, ecLevel, null);
            return scaleMatrix(code.Matrix.Array, width, height);
        }

        public static byte[] Encode(byte[] content, out int width, out int height)
        {
            var ecLevel = ErrorCorrectionLevel.L;
            var bits = new BitArray();

            var mode = Mode.BYTE;
            var bitsNeeded = 4 +
                mode.getCharacterCountBits(ZXing.QrCode.Internal.Version.getVersionForNumber(1)) +
                (content.Length * 8);

            var version = ChooseVersion(bitsNeeded, out ecLevel);
            var ecBlocks = version.getECBlocksForLevel(ecLevel);
            var totalByteCapacity = version.TotalCodewords - ecBlocks.TotalECCodewords;
            var totalBitCapacity = totalByteCapacity << 3;

            // Write the mode marker (BYTE)
            bits.appendBits(mode.Bits, 4);

            // Write the number of bytes
            bits.appendBits(content.Length, mode.getCharacterCountBits(version));

            // Write the bytes
            for (int i = 0; i < content.Length; i++)
            {
                bits.appendBits(content[i], 8);
            }

            // Terminate the bit stream
            //  - Write Termination Mode if space
            for (int i = 0; i < 4 && bits.Size < totalBitCapacity; ++i)
            {
                bits.appendBit(false);
            }

            // Add 8-bit alignment padding
            var bitsInLastByte = bits.Size & 0x07;
            if (bitsInLastByte > 0)
            {
                for (int i = bitsInLastByte; i < 8; i++)
                {
                    bits.appendBit(false);
                }
            }

            // Fill remain space with padding patterns
            var paddingBytes = totalByteCapacity - bits.SizeInBytes;
            for (int i = 0; i < paddingBytes; ++i)
            {
                bits.appendBits((i & 0x01) == 0 ? 0xEC : 0x11, 8);
            }

            // Interleave data bits with error correction code.
            var finalBits = interleaveWithECBytes(bits, version.TotalCodewords, totalByteCapacity, ecBlocks.NumBlocks);

            //  Choose the mask pattern and set to "qrCode".
            int dimension = version.DimensionForVersion;
            ByteMatrix matrix = new ByteMatrix(dimension, dimension);
            int maskPattern = chooseMaskPattern(finalBits, ecLevel, version, matrix);

            // Build the matrix and set it to "qrCode".
            MatrixUtil.buildMatrix(finalBits, ecLevel, version, maskPattern, matrix);

            // Render matrix to bytes
            width = matrix.Width;
            height = matrix.Height;
            return scaleMatrix(matrix.Array, width, height);
            //return FlattenMatrix(matrix.Array, width, height);
        }

        private static ZXing.QrCode.Internal.Version ChooseVersion(int RequiredBits, out ErrorCorrectionLevel ECLevel)
        {
            var ecls = new ErrorCorrectionLevel[] { ErrorCorrectionLevel.H, ErrorCorrectionLevel.Q, ErrorCorrectionLevel.M, ErrorCorrectionLevel.L };
            int totalInputBytes = (RequiredBits + 7) / 8;

            // In the following comments, we use numbers of Version 7-H.
            for (int versionNum = 1; versionNum <= 40; versionNum++)
            {
                var version = ZXing.QrCode.Internal.Version.getVersionForNumber(versionNum);
                // numBytes = 196
                int numBytes = version.TotalCodewords;
                // getNumECBytes = 130

                if (numBytes >= totalInputBytes)
                {
                    for (int ecl = 0; ecl < ecls.Length; ecl++)
                    {
                        var ecBlocks = version.getECBlocksForLevel(ecls[ecl]);
                        int numEcBytes = ecBlocks.TotalECCodewords;
                        // getNumDataBytes = 196 - 130 = 66
                        int numDataBytes = numBytes - numEcBytes;
                        if (numDataBytes >= totalInputBytes)
                        {
                            ECLevel = ecls[ecl];
                            return version;
                        }
                    }
                }
            }
            throw new ArgumentException("Data too big", nameof(RequiredBits));
        }

        private static BitArray interleaveWithECBytes(BitArray bits, int numTotalBytes, int numDataBytes, int numRSBlocks)
        {
            // Step 1.  Divide data bytes into blocks and generate error correction bytes for them. We'll
            // store the divided data bytes blocks and error correction bytes blocks into "blocks".
            int dataBytesOffset = 0;
            int maxNumDataBytes = 0;
            int maxNumEcBytes = 0;

            // Since, we know the number of reedsolmon blocks, we can initialize the vector with the number.
            var blocks = new List<Tuple<byte[], byte[]>>(numRSBlocks);

            for (int i = 0; i < numRSBlocks; ++i)
            {

                int numDataBytesInBlock;
                int numEcBytesInBlock;
                getNumDataBytesAndNumECBytesForBlockID(
                    numTotalBytes, numDataBytes, numRSBlocks, i,
                    out numDataBytesInBlock, out numEcBytesInBlock);

                byte[] dataBytes = new byte[numDataBytesInBlock];
                bits.toBytes(8 * dataBytesOffset, dataBytes, 0, numDataBytesInBlock);
                byte[] ecBytes = generateECBytes(dataBytes, numEcBytesInBlock);
                blocks.Add(new Tuple<byte[], byte[]>(dataBytes, ecBytes));

                maxNumDataBytes = Math.Max(maxNumDataBytes, numDataBytesInBlock);
                maxNumEcBytes = Math.Max(maxNumEcBytes, ecBytes.Length);
                dataBytesOffset += numEcBytesInBlock;
            }

            BitArray result = new BitArray();

            // First, place data blocks.
            for (int i = 0; i < maxNumDataBytes; ++i)
            {
                foreach (Tuple<byte[], byte[]> block in blocks)
                {
                    byte[] dataBytes = block.Item1;
                    if (i < dataBytes.Length)
                    {
                        result.appendBits(dataBytes[i], 8);
                    }
                }
            }
            // Then, place error correction blocks.
            for (int i = 0; i < maxNumEcBytes; ++i)
            {
                foreach (Tuple<byte[], byte[]> block in blocks)
                {
                    byte[] ecBytes = block.Item2;
                    if (i < ecBytes.Length)
                    {
                        result.appendBits(ecBytes[i], 8);
                    }
                }
            }

            return result;
        }

        private static void getNumDataBytesAndNumECBytesForBlockID(int numTotalBytes, int numDataBytes, int numRSBlocks, int blockID, out int numDataBytesInBlock, out int numECBytesInBlock)
        {
            // numRsBlocksInGroup2 = 196 % 5 = 1
            int numRsBlocksInGroup2 = numTotalBytes % numRSBlocks;
            // numRsBlocksInGroup1 = 5 - 1 = 4
            int numRsBlocksInGroup1 = numRSBlocks - numRsBlocksInGroup2;
            // numTotalBytesInGroup1 = 196 / 5 = 39
            int numTotalBytesInGroup1 = numTotalBytes / numRSBlocks;
            // numTotalBytesInGroup2 = 39 + 1 = 40
            int numTotalBytesInGroup2 = numTotalBytesInGroup1 + 1;
            // numDataBytesInGroup1 = 66 / 5 = 13
            int numDataBytesInGroup1 = numDataBytes / numRSBlocks;
            // numDataBytesInGroup2 = 13 + 1 = 14
            int numDataBytesInGroup2 = numDataBytesInGroup1 + 1;
            // numEcBytesInGroup1 = 39 - 13 = 26
            int numEcBytesInGroup1 = numTotalBytesInGroup1 - numDataBytesInGroup1;
            // numEcBytesInGroup2 = 40 - 14 = 26
            int numEcBytesInGroup2 = numTotalBytesInGroup2 - numDataBytesInGroup2;

            if (blockID < numRsBlocksInGroup1)
            {

                numDataBytesInBlock = numDataBytesInGroup1;
                numECBytesInBlock = numEcBytesInGroup1;
            }
            else
            {
                numDataBytesInBlock = numDataBytesInGroup2;
                numECBytesInBlock = numEcBytesInGroup2;
            }
        }

        private static byte[] generateECBytes(byte[] dataBytes, int numEcBytesInBlock)
        {
            int numDataBytes = dataBytes.Length;
            int[] toEncode = new int[numDataBytes + numEcBytesInBlock];
            for (int i = 0; i < numDataBytes; i++)
            {
                toEncode[i] = dataBytes[i] & 0xFF;

            }
            new ReedSolomonEncoder(GenericGF.QR_CODE_FIELD_256).encode(toEncode, numEcBytesInBlock);

            byte[] ecBytes = new byte[numEcBytesInBlock];
            for (int i = 0; i < numEcBytesInBlock; i++)
            {
                ecBytes[i] = (byte)toEncode[numDataBytes + i];

            }
            return ecBytes;
        }

        private static int chooseMaskPattern(BitArray bits, ErrorCorrectionLevel ecLevel, ZXing.QrCode.Internal.Version version, ByteMatrix matrix)
        {
            int minPenalty = Int32.MaxValue;  // Lower penalty is better.
            int bestMaskPattern = -1;
            // We try all mask patterns to choose the best one.
            for (int maskPattern = 0; maskPattern < QRCode.NUM_MASK_PATTERNS; maskPattern++)
            {

                MatrixUtil.buildMatrix(bits, ecLevel, version, maskPattern, matrix);
                int penalty = MaskUtil.applyMaskPenaltyRule1(matrix)
                    + MaskUtil.applyMaskPenaltyRule2(matrix)
                    + MaskUtil.applyMaskPenaltyRule3(matrix)
                    + MaskUtil.applyMaskPenaltyRule4(matrix);
                if (penalty < minPenalty)
                {

                    minPenalty = penalty;
                    bestMaskPattern = maskPattern;
                }
            }
            return bestMaskPattern;
        }

        private static byte[] scaleMatrix(byte[][] matrix, int width, int height)
        {
            var matrixWidth = matrix[0].Length;
            var matrixHeight = matrix.Length;
            width = Math.Max(width, matrixWidth);
            height = Math.Max(height, matrixHeight);
            var byteColumns = (width + 7) / 8;
            var outputBytes = new byte[byteColumns * height];
            var scale = Math.Max(1, Math.Min(width / (matrixWidth + 1), height / (matrixHeight + 1)));
            var offsetX = (width - (matrixWidth * scale)) / 2;
            var offsetY = (height - (matrixHeight * scale)) / 2;
            // initialize output bytes
            for (int i = 0; i < outputBytes.Length; i++)
            {
                outputBytes[i] = 0xFF;
            }
            // render row
            for (int rowIndex = 0; rowIndex < matrixHeight; rowIndex++)
            {
                var rowMatrix = matrix[rowIndex];
                var rowLocation = ((rowIndex * scale) + offsetY) * byteColumns;
                var bitOffset = offsetX;
                for (int c = 0; c < matrixWidth; c++)
                {
                    if (rowMatrix[c] == 1)
                    {
                        for (int cS = 0; cS < scale; cS++)
                        {
                            int index = rowLocation + (bitOffset / 8);
                            outputBytes[index] = (byte)(outputBytes[index] ^ ((byte)(0x80 >> (bitOffset % 8))));
                            bitOffset++;
                        }
                    }
                    else
                    {
                        bitOffset += scale;
                    }
                }
                // Write row for scale
                for (int i = 1; i < scale; i++)
                {
                    var offsetLocation = rowLocation + (i * byteColumns);
                    Array.Copy(outputBytes, rowLocation, outputBytes, offsetLocation, byteColumns);
                }
            }

            return outputBytes;
        }

    }
}
