"""
Embedded Python Blocks:

Each time this file is saved, GRC will instantiate the first class it finds
to get ports and parameters of your block. The arguments to __init__  will
be the parameters. All of them are required to have default values!
"""

import numpy as np
from gnuradio import gr
import numpy as np

def binary(num, length=8):
        """
        Format an integer to binary without the leading '0b'
        """
        return format(num, '0{}b'.format(length))


class fec_hamming(object):
    """
    Class responsible for creating hamming block codes and then 
    encoding and decoding. Methods provided include hamm_gen,
    hamm_encoder(), hamm_decoder().
    
    Parameters
    ----------
    j: Hamming code order (in terms of parity bits) where n = 2^j-1, 
    k = n-j, and the rate is k/n. 
    
    Returns
    -------
    
    Examples
    --------
    
    Andrew Smit November 2018 
    """
    
    def __init__(self,j):
        self.j = j
        self.G, self.H, self.R, self.n, self.k = self.hamm_gen(self.j)
        #log.info('(%d,%d) hamming code object' %(self.n,self.k))

    def hamm_gen(self,j):
        """
        Generates parity check matrix (H) and generator
        matrix (G). 
        
        Parameters
        ----------
        j: Number of Hamming code parity bits with n = 2^j-1 and k = n-j
        
        returns
        -------
        G: Systematic generator matrix with left-side identity matrix
        H: Systematic parity-check matrix with right-side identity matrix
        R: k x k identity matrix
        n: number of total bits/block
        k: number of source bits/block
        
        Andrew Smit November 2018
        
        """
        if(j < 3):
            raise ValueError('j must be > 2')

        # calculate codeword length
        n = 2**j-1
        
        # calculate source bit length
        k = n-j
        
        # Allocate memory for Matrices
        G = np.zeros((k,n),dtype=int)
        H = np.zeros((j,n),dtype=int)
        P = np.zeros((j,k),dtype=int)
        R = np.zeros((k,n),dtype=int)
        
        # Encode parity-check matrix columns with binary 1-n
        for i in range(1,n+1):
            b = list(binary(i,j))
            for m in range(0,len(b)):
                b[m] = int(b[m])
            H[:,i-1] = np.array(b)

        # Reformat H to be systematic
        H1 = np.zeros((1,j),dtype=int)
        H2 = np.zeros((1,j),dtype=int)
        for i in range(0,j):
            idx1 = 2**i-1
            idx2 = n-i-1
            H1[0,:] = H[:,idx1]
            H2[0,:] = H[:,idx2]
            H[:,idx1] = H2
            H[:,idx2] = H1
        
        # Get parity matrix from H
        P = H[:,:k]
        
        # Use P to calcuate generator matrix P
        G[:,:k] = np.diag(np.ones(k))
        G[:,k:] = P.T
        
        # Get k x k identity matrix
        R[:,:k] = np.diag(np.ones(k))

        return G, H, R, n, k
    
    def hamm_encoder(self,x):
        """
        Encodes input bit array x using hamming block code.
        
        parameters
        ----------
        x: array of source bits to be encoded by block encoder.
        
        returns
        -------
        codewords: array of code words generated by generator
        matrix G and input x.
        
        Andrew Smit November 2018
        """
        if(np.dtype(x[0]) != int):
            raise ValueError('Error: Invalid data type. Input must be a vector of ints')

        if(len(x) % self.k or len(x) < self.k):
            raise ValueError('Error: Invalid input vector length. Length must be a multiple of %d' %self.k)

        N_symbols = int(len(x)/self.k)
        codewords = np.zeros(N_symbols*self.n)
        x = np.reshape(x,(1,len(x)))

        for i in range(0,N_symbols):
            codewords[i*self.n:(i+1)*self.n] = np.matmul(x[:,i*self.k:(i+1)*self.k],self.G)%2
        return codewords
    
    def hamm_decoder(self,codewords):
        """
        Decode hamming encoded codewords. Make sure code words are of
        the appropriate length for the object.
        
        parameters
        ---------
        codewords: bit array of codewords 
        
        returns
        -------
        decoded_bits: bit array of decoded source bits
        
        Andrew Smit November 2018
        """
        if(np.dtype(codewords[0]) != int):
            raise ValueError('Error: Invalid data type. Input must be a vector of ints')

        if(len(codewords) % self.n or len(codewords) < self.n):
            raise ValueError('Error: Invalid input vector length. Length must be a multiple of %d' %self.n)

        # Calculate the number of symbols (codewords) in the input array
        N_symbols = int(len(codewords)/self.n)
        
        # Allocate memory for decoded sourcebits
        decoded_bits = np.zeros(N_symbols*self.k)
        
        # Loop through codewords to decode one block at a time
        codewords = np.reshape(codewords,(1,len(codewords)))
        for i in range(0,N_symbols):
            
            # find the syndrome of each codeword
            S = np.matmul(self.H,codewords[:,i*self.n:(i+1)*self.n].T) % 2

            # convert binary syndrome to an integer
            bits = ''
            for m in range(0,len(S)):
                bit = str(int(S[m,:]))
                bits = bits + bit
            error_pos = int(bits,2)
            h_pos = self.H[:,error_pos-1]
            
            # Use the syndrome to find the position of an error within the block
            bits = ''
            for m in range(0,len(S)):
                bit = str(int(h_pos[m]))
                bits = bits + bit
            decoded_pos = int(bits,2)-1

            # correct error if present
            if(error_pos):
                codewords[:,i*self.n+decoded_pos] = (codewords[:,i*self.n+decoded_pos] + 1) % 2
                
            # Decode the corrected codeword
            decoded_bits[i*self.k:(i+1)*self.k] = np.matmul(self.R,codewords[:,i*self.n:(i+1)*self.n].T).T % 2
        return decoded_bits.astype(np.byte)

# Create a possible series of matrices that could be utilized by the program
ham_74 = np.array([ [1,1,0,1,1,0,0],
                    [1,0,1,1,0,1,0],
                    [0,1,1,1,0,0,1] ])
                    
# Class which can utilize a reversed hamming code class
class rev_ham(fec_hamming):

    # This will have parity checks at the end of the matrix by default. This will allow for hamming codes to be written in either way
    def __init__(self, j):
        super().__init__(j)
        n = 2**j-1
        k = n-j
        self.n, self.k, self.j = n,k,j
        # Create R which is reversed in a way to enable the message to go through
        self.R = np.zeros((self.k, self.n), dtype=int)
        self.R[:,self.n-self.k:] = np.diag(np.ones(self.k))

        # Create H again
        H1 = np.zeros((self.j, self.n),dtype=int)

        H1[:,:self.j] = self.H[:,self.k:]
        H1[:,self.j:] = self.H[:,:self.k]
        self.H = H1

class blk(gr.sync_block):  # other base classes are basic_block, decim_block, interp_block
    """Embedded Python Block example - a simple multiply const"""

    def __init__(self):  # only default arguments here
        """arguments to this function show up as parameters in GRC"""
        gr.sync_block.__init__(
            self,
            name='Embedded Python Block',   # will show up in GRC
            in_sig=[np.byte],
            out_sig=[np.byte]
        )
        
        # Create hamming code data
        self.ham = fec_hamming(3)
        self.ham.H = ham_74
        

    def work(self, input_items, output_items):
        """
        Set up system to utilize hamming codes. Tak input items, and convert to
        seven bit int array which can be decoded and returned as a list
        """
        
        current = self.decode((np.unpackbits(np.array(np.uint8(input_items[0][3])))))
        
        # current will contain 4 bits and must aftr this be packed with a second
        # char to fully represent a true byte
        
        if sum(current) == 0:
            return 0
        
        for i in range(0, len(current)):
            output_items[0][i] = current[i]
        
        return len(output_items[0])   
        
    """
    This method will implement the inputed data, then turning it into decoded
    data. Will take series of 7 bits and import that to the rest of the program.
    """
    def decode(self, codewords):
        #print(codewords)
        coding = np.array(codewords[1:], dtype=int)
        #for i in range(7):
         #   coding[i] = int(codewords[i+1])
        current = self.ham.hamm_decoder(coding).tolist()      
        #print(current)
        return current
    
