from reedsolo import RSCodec
from qrcode.util import pattern_position, ALPHA_NUM,\
    MODE_NUMBER, MODE_ALPHA_NUM, MODE_8BIT_BYTE, MODE_KANJI,\
    mode_sizes_for_version, mask_func

from qrcode.constants import ERROR_CORRECT_L, ERROR_CORRECT_M, ERROR_CORRECT_Q, ERROR_CORRECT_H
from qrcode.base import RS_BLOCK_OFFSET, RS_BLOCK_TABLE


class QRMatrix:
    """
    The QRMatrix class allows decoding QR codes from numpy arrays
    """

    # TODO adapt if possible to python-qrcode module. First implement a function to parse it.

    # Already parsed:
    alignment_pattern_locations_table = {ii + 1: pattern_position(ii + 1) for ii in range(40)}
    alphanumeric_table = ALPHA_NUM.decode()
    # (4) -> ((i div 2) + (j div 3)) mod 2    // 100 // 001
    # (5) -> (ij) mod 2 + (ij) mod 3          // 101 // 000

    # DATA MASK PATTERN // Binary Indicator // XORed with 101
    # (0) -> (i+j) mod 2                      // 000 // 101
    # (1) -> i mod 2                          // 001 // 100
    # (2) -> j mod 3                          // 010 // 111
    # (3) -> (i+j) mod 3                      // 011 // 110
    # (4) -> ((i div 2) + (j div 3)) mod 2    // 100 // 001
    # (5) -> (ij) mod 2 + (ij) mod 3          // 101 // 000
    # (6) -> ((ij) mod 2 + (ij) mod 3) mod 2  // 110 // 011
    # (7) -> ((i+j) mod 2 + (ij) mod 3) mod 2 // 111 // 010
    mask_patterns = {'000': 5, '001': 4, '010': 7, '011': 6, '100': 1, '101': 0, '110': 3, '111': 2}

    def __init__(self, np_array):
        """
        Initialization function of the :class:`qrdecode.decoder.main.QRMatrix` class.

        :param np_array: The numpy array to be used.
        :type np_array: numpy.ndarray
        :return: Void function.
        :rtype: None

        """
        self.matrix = np_array
        self.version = self.__getVersion()
        self.error_correction_level = self.__getErrorCorrectionLevel()
        self.mask_pattern = self.__getMaskPattern()

    def __str__(self):
        """
        This function will print a representation of the array.

        :return: Void function.
        :rtype: None

        """
        pass

    def __getVersion(self):
        """
        Gets the version of the QRcode.

        :return: The version of the QRcode.
        :rtype: int

        """

        # Currently only checks matrix dimension. It should be enough
        return ((len(self.matrix) - 21) // 4) + 1

    def __getErrorCorrectionLevel(self):
        """
        Extracts the error correction level from the matrix.

        :return: The error correction level L, M, Q, or H.
        :rtype: int

        """

        # Currently checks only upper-left format_info to retrieve it without BCH code error correction
        # We XOR with 10 before retrieving it as it is encoded this way

        return self.__bitXOR(self.matrix[8][0], 1) * 2 + self.__bitXOR(self.matrix[8][1], 0) * 1

    def getDarkModule(self):
        """
        Gets the value of the Dark Module.

        :return: The value of the Dark Module.
        :rtype: int

        """
        return self.matrix[len(self.matrix) - 8][8]

    def __isOutOfBounds(self, x, y):
        """
        Checks if the x, y coordinates are out of bounds.

        :param x: The first coordinate.
        :type x: int
        :param y: The second coordinate.
        :type y: int
        :return: True if the coordinates are out of bounds. False otherwise.
        :rtype: bool

        """

        # Checks if it is outside the matrix's boundaries
        if x > len(self.matrix) - 1 or y > len(self.matrix) - 1:
            return True
        elif x < 0 or y < 0:
            return True

        # Checks if it is on an orientation marker of format_info area
        elif x < 9 and (y < 9 or y >= len(self.matrix) - 8):
            return True
        elif x >= len(self.matrix) - 8 and y < 9:
            return True

        else:
            return False

    def __isInPositionPattern(self, x, y):
        """
        Checks if a module is in a Position Pattern location.

        :param x: The first coordinate.
        :type x: int
        :param y: The second coordinate.
        :type y: int
        :return: True if x or y are equal to 6. False otherwise.
        :rtype: bool

        """
        return x == 6 or y == 6

    def __isInAlignmentPattern(self, x, y):
        """
        Checks if a module is in an Alignment Pattern location.

        :param x: The first coordinate.
        :type x: int
        :param y: The second coordinate.
        :type y: int
        :return: True if the module is an Alignment Pattern location. False otherwise.
        :rtype: bool

        """
        if self.version > 1:

            orientation_markers_positions =\
                [
                    (self.alignment_pattern_locations_table[self.version][0],
                     self.alignment_pattern_locations_table[self.version][0]),

                    (self.alignment_pattern_locations_table[self.version][0],
                     self.alignment_pattern_locations_table[self.version][-1]),

                    (self.alignment_pattern_locations_table[self.version][-1],
                     self.alignment_pattern_locations_table[self.version][0])
                ]

            for i in self.alignment_pattern_locations_table[self.version]:
                for j in self.alignment_pattern_locations_table[self.version]:

                    if (i,j) not in orientation_markers_positions:
                        if (x in range(i-2, i+3) and y in range (j-2, j+3)):
                            return True
            return False

    def __isInFixedArea(self, x, y):
        """
        Checks if a module is in a Fixed Area.

        :param x: The first coordinate.
        :type x: int
        :param y: The second coordinate.
        :type y: int
        :return: True if the module is in a Fixed Area. False otherwise.
        :rtype: bool

        """
        return self.__isInPositionPattern(x, y) or \
               self.__isInAlignmentPattern(x, y) or \
               self.__isInVersionInfoArea(x, y)


    def __isInVersionInfoArea(self, x, y):
        """
        Checks if a module is in Version Info Area.

        :param x: The first coordinate.
        :type x: int
        :param y: The second coordinate.
        :type y: int
        :return: True if the module is in a Fixed Area. False otherwise.
        :rtype: bool

        """
        if self.version >= 7:
            return x in range(6) and y in range(len(self.matrix)-11, len(self.matrix)-8) or \
                   x in range(len(self.matrix)-11, len(self.matrix)-8) and y in range(6)

        else:
            return False

    def __getMaskPattern(self):
        """
        Gets the mask pattern used during the encoding.

        :return: The mask pattern used during the encoding.
        :rtype: int

        """
        k = ""
        for i in self.matrix[8][2:5]:
            k += str(i)

        return self.mask_patterns[k]


    def __bitXOR(self, a, b):
        """
        Simply performs a XOR bit operation. Assumes a and b equal 1 (True) or 0 (False).

        :param a: First element to be XORed.
        :type a: int
        :param b: Second element to be XORed.
        :type b: int
        :return: The result of the XOR operation, that can be 1 or 0.
        :rtype: int

        """
        if (a and not b) or (not a and b):
            return 1
        else:
            return 0

    def __getBitStream(self):
        """
        Gets the BitStream of the matrix containing the QR.

        :return: The bitstream containing the QR.
        :rtype: array

        """

        def orderBitStream(bit_stream):
            """
            Orders the bit_stream parameter.

            :param bit_stream: The bitstream to be oredered.
            :type bit_stream: array
            :return: The ordered bitstream.
            :rtype: array

            """
            cw_size = 8
            ordered_bit_stream = []

            offset = RS_BLOCK_OFFSET[self.error_correction_level]
            # print("offset", offset)
            rs_block = RS_BLOCK_TABLE[(self.version - 1) * 4 + offset]
            # print("rs_block", rs_block)

            list_n_blocks = [x for x in rs_block[0::3]]
            sum_n_blocks = sum(list_n_blocks)
            # print("sum_n_blocks", sum_n_blocks)

            list_n_data_cw_per_block = []
            list_n_error_cw_per_block = []

            for i in range(0, len(rs_block), 3):
                list_n_data_cw_per_block += ([rs_block[2 + i] for _ in range(rs_block[i])])
                list_n_error_cw_per_block += ([(rs_block[1 + i] - rs_block[2 + i]) for _ in range(rs_block[i])])

            # print("list_n_data_cw_per_block", list_n_data_cw_per_block)
            # print("list_n_error_cw_per_block", list_n_error_cw_per_block)

            n_cw = len(bit_stream) // 8
            n_remainder_bits = len(bit_stream) % 8

            # Data codewords reordering
            for i in range(sum_n_blocks):
                for j in range(list_n_data_cw_per_block[0]):
                    start = i*cw_size + j * sum_n_blocks * cw_size
                    # print("start1", start)

                    for bit in bit_stream[start:start + cw_size]:
                        ordered_bit_stream.append(bit)
                        last_data_pos = start + cw_size

            # Means 2 groups of blocks which second group has 1 more data/error cw
            if len(list_n_blocks) == 2:
                for i in range(list_n_blocks[-1]):
                    start = i*cw_size + (list_n_data_cw_per_block[-1] - 1) * sum_n_blocks * cw_size
                    # print("start2", start)

                    for bit in bit_stream[start:start + cw_size]:
                        ordered_bit_stream.append(bit)
                        last_data_pos = start + cw_size

            # correction codewords reordering
            for i in range(sum_n_blocks):

                for j in range(list_n_error_cw_per_block[i]):
                    start = last_data_pos + (i*cw_size) + j * sum_n_blocks * cw_size
                    # print("start3", start)

                    for bit in bit_stream[start:start + cw_size]:
                        ordered_bit_stream.append(bit)

            # Add remaining bits
            for _ in range(n_remainder_bits):
                ordered_bit_stream.append(0)

            # For debug purposes
            # print("Bit Stream no_order:", bit_stream)
            # print("Bit Stream Ordered:", ordered_bit_stream)
            # print(n_cw)
            # print(len(ordered_bit_stream), len(bit_stream))
            ###############

            return ordered_bit_stream

        bit_stream_original = [] # Just for tests
        bit_stream = []
        mod_num = 0
        x, y, direction = len(self.matrix) - 1, len(self.matrix) - 1, -1

        # print((len(self.matrix) - 9, 0))
        while (x, y) != (len(self.matrix) - 8, 1): # The following to the last bit to decode

            if self.__isOutOfBounds(x, y):
                mult = 1
                if (x, y) == (len(self.matrix), 10):
                    mult = 9
                direction, y, x = -direction, y - 2, x - mult * direction

            if not self.__isInFixedArea(x, y):
                try:
                    bit_stream_original.append(self.matrix[x][y]) # Just for test
                    bit_stream.append(self.__bitXOR(self.matrix[x][y], int(mask_func(self.mask_pattern)(x, y))))
                    if (x, y) == (9, 5):
                        mod_num = 1

                except:
                    pass
                    # raise Exception("CRASH", (x,y))

            if y % 2 != mod_num:
                x, y = x + direction, y + 1

            else:
                y -= 1

        if not ((self.version < 3)
                or ((self.version == 3) and ((self.error_correction_level == 'L')
                                             or (self.error_correction_level == 'M')))
                or (((self.version == 4) or (self.version == 5)) and (self.error_correction_level == 'L'))):

            return orderBitStream(bit_stream)

        else:
            return bit_stream

    def __correctBitstreamRS(self, bit_stream):
        """
        Returns the corrected bitstream for the bitstream parameter.

        :param bit_stream: The bitstream to be corrected.
        :type bit_stream: array
        :return: The corrected bitstream.
        :rtype: array

        """

        offset = RS_BLOCK_OFFSET[self.error_correction_level]
        # print(offset)
        rs_block = RS_BLOCK_TABLE[(self.version - 1) * 4 + offset]
        # print(rs_block)

        list_n_blocks = [x for x in rs_block[0::3]]
        sum_n_blocks = sum(list_n_blocks)
        # print(sum_n_blocks)

        list_n_data_cw_per_block = []
        list_n_error_cw_per_block = []

        for i in range(0, len(rs_block), 3):
            list_n_data_cw_per_block += ([rs_block[2 + i] for _ in range(rs_block[i])])
            list_n_error_cw_per_block += ([(rs_block[1+i] - rs_block[2 + i]) for _ in range(rs_block[i])])

        # print("list_n_data_cw_per_block", list_n_data_cw_per_block)
        # print("list_n_error_cw_per_block", list_n_error_cw_per_block)

        n_data_cw = sum(list_n_data_cw_per_block)
        n_error_cw = sum(list_n_error_cw_per_block)

        b_dec = bytes()

        start_data = 0
        start_error = n_data_cw*8

        for i in range(sum_n_blocks):
            rsc = RSCodec(list_n_error_cw_per_block[i])
            b = bytes()

            for j in range(list_n_data_cw_per_block[i]):
                int_byte = self.__decodeBits(bit_stream, start_data + j*8, 8)
                b += bytes([int_byte])
            start_data += list_n_data_cw_per_block[i]*8

            for k in range(list_n_error_cw_per_block[i]):
                int_byte = self.__decodeBits(bit_stream, start_error + k*8, 8)
                b += bytes([int_byte])
            start_error += list_n_error_cw_per_block[i]*8

            b_dec += rsc.decode(b)[0]


        corrected_data_bit_stream = []
        for i in b_dec:
            corrected_data_bit_stream += [int(x) for x in list('{0:08b}'.format(i))]

        return corrected_data_bit_stream



    def __decodeBits(self, bit_stream, start, number_of_bits):
        """
        This function decodes the bits. The starting value will correspond with the highest power of 2. This function
        will return the value generated from the bits.

        :param bit_stream: The bit_stream of the matrix.
        :type bit_stream: array
        :param start: The starting index to decode.
        :type start: int
        :param number_of_bits: The number of bits to generate a character.
        :type number_of_bits: int
        :return: Void function.
        :rtype: None

        """

        factor = 2 << (number_of_bits - 2) # Bitwise to perform power operation
        value = 0
        # print(bit_stream[start:start + number_of_bits])
        for i in bit_stream[start:start + number_of_bits]:
            value += i * factor
            if factor == 1:
                return int(value)
            factor /= 2

    def decode(self):
        """
        Decodes the QR contained in the image.

        :return: The decoded data of the QR.
        :rtype: string

        """

    #####################
    # Auxiliar functions
    # Maybe they could go outside(?)
    #

        def byteDecode(bit_stream, data_length):
            """
            Decodes the bitstream that contains a byte code.

            :param bit_stream: The bitstream to be decoded.
            :type bit_stream: array
            :param data_length: The length of the data.
            :type data_length: int
            :return: The data and the decoding index.
            :rtype: string, int

            """
            n_bits_per_group = 8
            data = ""

            for i in range(data_length):
                dec_index = (4 + n_bits_char_count) + i * n_bits_per_group
                data += chr(
                    self.__decodeBits(
                        bit_stream, dec_index, n_bits_per_group))

            dec_index += n_bits_per_group
            return data, dec_index

        def numericDecode(bit_stream, data_length):
            """
            Decodes the bitstream that contains a numeric code.

            :param bit_stream: The bitstream to be decoded.
            :type bit_stream: array
            :param data_length: The length of the data.
            :type data_length: int
            :return: The data and the decoding index.
            :rtype: string, int

            """

            n_bits_per_group = 10  # Or 7 or 4
            n_bits_last_group = 10 # By default
            data = ""

            if data_length % 3 == 2:
                n_bits_last_group = 7

            elif data_length % 3 == 1:
                n_bits_last_group = 4

            for i in range(data_length//3):
                dec_index = (4 + n_bits_char_count) + i * n_bits_per_group
                data += str(
                    self.__decodeBits(
                        bit_stream, dec_index, n_bits_per_group))

            if n_bits_last_group != 10:
                dec_index = (4 + n_bits_char_count) + (data_length//3) * n_bits_per_group
                data += str(
                    self.__decodeBits(
                        bit_stream, dec_index, n_bits_last_group))

            dec_index += n_bits_last_group

            return data, dec_index

        def alphaNumericDecode(bit_stream, data_length):
            """
            Decodes the bitstream that contains an alphanumeric code.

            :param bit_stream: The bitstream to be decoded.
            :type bit_stream: array
            :param data_length: The length of the data.
            :type data_length: int
            :return: The data and the decoding index.
            :rtype: string, int

            """
            n_bits_per_group = 11  # 6 the last one, but different interpretation
            data = ""

            for i in range(data_length//2):
                dec_index = (4 + n_bits_char_count) + i * n_bits_per_group
                bit_group_int = self.__decodeBits(
                    bit_stream, dec_index, n_bits_per_group)

                data += self.alphanumeric_table[bit_group_int // 45]
                data += self.alphanumeric_table[bit_group_int % 45]

            if not data_length % 2 == 0:
                dec_index = (4 + n_bits_char_count) + (data_length//2) * n_bits_per_group
                bit_group_int = self.__decodeBits(bit_stream, dec_index, 6)
                data += self.alphanumeric_table[bit_group_int]

                dec_index += 6

            else:
                dec_index += n_bits_per_group

            return data, dec_index

        def encodingModeInfo(encoding_mode):
            """
            Decodes the bitstream that contains a numeric code.

            :param encoding_mode: The encoding mode.
            :type encoding_mode: string
            :return: The data and the decoding index.
            :rtype: string, int
            :raises: Exception
            .. note::
                The encoding modes can be "Numeric", "Alphanumeric

            """
            n_bits_char_count = mode_sizes_for_version(self.version)[encoding_mode]

            if encoding_mode == MODE_NUMBER:
                decodeFunction = numericDecode

            elif encoding_mode == MODE_ALPHA_NUM:
                decodeFunction = alphaNumericDecode

            elif encoding_mode == MODE_8BIT_BYTE:
                decodeFunction = byteDecode

            else:
                raise Exception(encoding_mode, "Encoding mode not supported")

            return n_bits_char_count, decodeFunction

    #
    #
    #
    #####################

        # It contains data and error codewords, as it is going to be used to error correction
        bit_stream_no_corrected = self.__getBitStream()

        # It only contains data codewords as it is already corrected, so error correction codewords
        # are useless for data decoding process
        bit_stream = self.__correctBitstreamRS(bit_stream_no_corrected)

        data = ""

        # Do: decode first data segment
        segment = 0
        dec_index = 0

        encoding_mode_list = [self.__decodeBits(bit_stream, dec_index, 4)]
        n_bits_char_count, decodeFunction = encodingModeInfo(encoding_mode_list[segment])
        data_length_list = [self.__decodeBits(bit_stream, dec_index + 4, n_bits_char_count)]
        dec_data, dec_index = decodeFunction(bit_stream, data_length_list[segment])
        data += dec_data
        terminator_found = bit_stream[dec_index:dec_index + 4] == [0, 0, 0, 0]

        # If no terminator found means there is another segment (with another encoding format)
        # While: no terminator is found, decode following data segments
        while not terminator_found:
            segment += 1
            encoding_mode_list.append(self.__decodeBits(bit_stream, dec_index, 4))
            n_bits_char_count, decodeFunction = encodingModeInfo(encoding_mode_list[segment])
            data_length_list.append(self.__decodeBits(bit_stream, dec_index + 4, n_bits_char_count))
            dec_data, dec_index_i = decodeFunction(bit_stream[dec_index:], data_length_list[segment])
            dec_index += dec_index_i
            data += dec_data
            terminator_found = bit_stream[dec_index:dec_index + 4] == [0, 0, 0, 0]

        # Prints relevant info
        err_c_levels = {
            ERROR_CORRECT_L: 'L',
            ERROR_CORRECT_M: 'M',
            ERROR_CORRECT_Q: 'Q',
            ERROR_CORRECT_H: 'H'
        }
    
        enc_str = {
            MODE_NUMBER: "Numeric",
            MODE_ALPHA_NUM: "Alphanumeric",
            MODE_8BIT_BYTE: "Byte",
            MODE_KANJI: "Kanji"
        }

        print("-----------------")
        print("QR version:", self.version)
        print("Mask Pattern:", self.mask_pattern)
        print("Error correction level:", err_c_levels[self.error_correction_level])
        print("Encoding mode(s):", [enc_str[i] for i in encoding_mode_list])
        print("Data Length(s):", data_length_list)
        print("Data:", data)

        return data