import numpy as np
import scipy.io as sio
import os

# ------------------ LOAD LUT ------------------

def load_LUT(mat_filename=None):
    """
    Loads the list of Huffman Codebooks (LUTs)

    Returns:
        huffLUT : list (index 1..11 used, index 0 unused)
    """
    if mat_filename is None:
        current_dir = os.path.dirname(os.path.abspath(__file__))
        mat_filename = os.path.join(current_dir, "huffCodebooks.mat")

    mat = sio.loadmat(mat_filename)


    huffCodebooks_raw = mat['huffCodebooks'].squeeze()

    huffCodebooks = []
    for i in range(11):
        huffCodebooks.append(np.array(huffCodebooks_raw[i]))

    # Build inverse VLC tables
    invTable = [None] * 11

    for i in range(11):
        h = huffCodebooks[i][:, 2].astype(int)       # column 3
        hlength = huffCodebooks[i][:, 1].astype(int) # column 2

        hbin = []
        for j in range(len(h)):
            hbin.append(format(h[j], f'0{hlength[j]}b'))

        invTable[i] = vlc_table(hbin)

    # Build Huffman LUT dicts
    huffLUT = [None] * 12  # index 0 unused
    params = [
        (4, 1, True),
        (4, 1, True),
        (4, 2, False),
        (4, 2, False),
        (2, 4, True),
        (2, 4, True),
        (2, 7, False),
        (2, 7, False),
        (2, 12, False),
        (2, 12, False),
        (2, 16, False),
    ]

    for i, (nTupleSize, maxAbs, signed) in enumerate(params, start=1):
        huffLUT[i] = {
            'LUT': huffCodebooks[i-1],
            'invTable': invTable[i-1],
            'codebook': i,
            'nTupleSize': nTupleSize,
            'maxAbsCodeVal': maxAbs,
            'signedValues': signed
        }

    return huffLUT

def vlc_table(code_array):
    """
    codeArray: list of strings, each string is a Huffman codeword (e.g. '0101')
    returns:
        h : NumPy array of shape (num_nodes, 3)
            columns:
              [ next_if_0 , next_if_1 , symbol_index ]
    """
    h = np.zeros((1, 3), dtype=int)

    for code_index, code in enumerate(code_array, start=1):
        word = [int(bit) for bit in code]
        h_index = 0

        for bit in word:
            k = bit
            next_node = h[h_index, k]
            if next_node == 0:
                h = np.vstack([h, [0, 0, 0]])
                new_index = h.shape[0] - 1
                h[h_index, k] = new_index
                h_index = new_index
            else:
                h_index = next_node

        h[h_index, 2] = code_index

    return h

# ------------------ ENCODE ------------------

def encode_huff(coeff_sec, huff_LUT_list, force_codebook = None):
    """
    Huffman-encode a sequence of quantized coefficients.

    This function selects the appropriate Huffman codebook based on the
    maximum absolute value of the input coefficients, encodes the coefficients
    into a binary Huffman bitstream, and returns both the bitstream and the
    selected codebook index.

    This is the Python equivalent of the MATLAB `encodeHuff.m` function used
    in audio/image coding (e.g., scale factor band encoding). The input
    coefficient sequence is grouped into fixed-size tuples as defined by
    the chosen Huffman LUT. Zero-padding may be applied internally.

    Parameters
    ----------
    coeff_sec : array_like of int
        1-D array of quantized integer coefficients to encode.
        Typically corresponds to a "section" or scale-factor band.

    huff_LUT_list : list
        List of Huffman lookup-table dictionaries as returned by `loadLUT()`.
        Index 1..11 correspond to valid Huffman codebooks.
        Index 0 is unused.

    Returns
    -------
    huffSec : str
        Huffman-encoded bitstream represented as a string of '0' and '1'
        characters.

    huffCodebook : int
        Index (1..11) of the Huffman codebook used for encoding.
        A value of 0 indicates a special all-zero section.
    """
    if force_codebook is not None:
            return huff_LUT_code_1(huff_LUT_list[force_codebook], coeff_sec)
    
    maxAbsVal = np.max(np.abs(coeff_sec))

    if maxAbsVal == 0:
        huffCodebook = 0
        huffSec = huff_LUT_code_0()

    elif maxAbsVal == 1:
        candidates = [1, 2]
        huffSec1 = huff_LUT_code_1(huff_LUT_list[candidates[0]], coeff_sec)
        huffSec2 = huff_LUT_code_1(huff_LUT_list[candidates[1]], coeff_sec)
        if len(huffSec1) <= len(huffSec2):
            huffSec = huffSec1
            huffCodebook = candidates[0]
        else:
            huffSec = huffSec2
            huffCodebook = candidates[1]

    elif maxAbsVal == 2:
        candidates = [3, 4]
        huffSec1 = huff_LUT_code_1(huff_LUT_list[candidates[0]], coeff_sec)
        huffSec2 = huff_LUT_code_1(huff_LUT_list[candidates[1]], coeff_sec)
        if len(huffSec1) <= len(huffSec2):
            huffSec = huffSec1
            huffCodebook = candidates[0]
        else:
            huffSec = huffSec2
            huffCodebook = candidates[1]

    elif maxAbsVal in (3, 4):
        candidates = [5, 6]
        huffSec1 = huff_LUT_code_1(huff_LUT_list[candidates[0]], coeff_sec)
        huffSec2 = huff_LUT_code_1(huff_LUT_list[candidates[1]], coeff_sec)
        if len(huffSec1) <= len(huffSec2):
            huffSec = huffSec1
            huffCodebook = candidates[0]
        else:
            huffSec = huffSec2
            huffCodebook = candidates[1]

    elif maxAbsVal in (5, 6, 7):
        candidates = [7, 8]
        huffSec1 = huff_LUT_code_1(huff_LUT_list[candidates[0]], coeff_sec)
        huffSec2 = huff_LUT_code_1(huff_LUT_list[candidates[1]], coeff_sec)
        if len(huffSec1) <= len(huffSec2):
            huffSec = huffSec1
            huffCodebook = candidates[0]
        else:
            huffSec = huffSec2
            huffCodebook = candidates[1]

    elif maxAbsVal in (8, 9, 10, 11, 12):
        candidates = [9, 10]
        huffSec1 = huff_LUT_code_1(huff_LUT_list[candidates[0]], coeff_sec)
        huffSec2 = huff_LUT_code_1(huff_LUT_list[candidates[1]], coeff_sec)
        if len(huffSec1) <= len(huffSec2):
            huffSec = huffSec1
            huffCodebook = candidates[0]
        else:
            huffSec = huffSec2
            huffCodebook = candidates[1]

    elif maxAbsVal in (13, 14, 15):
        huffCodebook = 11
        huffSec = huff_LUT_code_1(huff_LUT_list[huffCodebook], coeff_sec)

    else:
        huffCodebook = 11
        huffSec = huff_LUT_code_ESC(huff_LUT_list[huffCodebook], coeff_sec)

    return huffSec, huffCodebook

def huff_LUT_code_1(huff_LUT, coeff_sec):
    LUT = huff_LUT['LUT']
    nTupleSize = huff_LUT['nTupleSize']
    maxAbsCodeVal = huff_LUT['maxAbsCodeVal']
    signedValues = huff_LUT['signedValues']

    numTuples = int(np.ceil(len(coeff_sec) / nTupleSize))

    if signedValues:
        coeff = coeff_sec + maxAbsCodeVal
        base = 2 * maxAbsCodeVal + 1
    else:
        coeff = coeff_sec
        base = maxAbsCodeVal + 1

    coeffPad = np.zeros(numTuples * nTupleSize, dtype=int)
    coeffPad[:len(coeff)] = coeff

    huffSec = []

    powers = base ** np.arange(nTupleSize - 1, -1, -1)

    for i in range(numTuples):
        nTuple = coeffPad[i*nTupleSize:(i+1)*nTupleSize]
        huffIndex = int(np.abs(nTuple) @ powers)

        hexVal = LUT[huffIndex, 2]
        huffLen = LUT[huffIndex, 1]

        bits = format(int(hexVal), f'0{int(huffLen)}b')

        if signedValues:
            huffSec.append(bits)
        else:
            signBits = ''.join('1' if v < 0 else '0' for v in nTuple)
            huffSec.append(bits + signBits)

    return ''.join(huffSec)

def huff_LUT_code_0():
    return ''

def huff_LUT_code_ESC(huff_LUT, coeff_sec):
    LUT = huff_LUT['LUT']
    nTupleSize = huff_LUT['nTupleSize']
    maxAbsCodeVal = huff_LUT['maxAbsCodeVal']

    numTuples = int(np.ceil(len(coeff_sec) / nTupleSize))
    base = maxAbsCodeVal + 1

    coeffPad = np.zeros(numTuples * nTupleSize, dtype=int)
    coeffPad[:len(coeff_sec)] = coeff_sec

    huffSec = []
    powers = base ** np.arange(nTupleSize - 1, -1, -1)

    for i in range(numTuples):
        nTuple = coeffPad[i*nTupleSize:(i+1)*nTupleSize]

        lnTuple = nTuple.astype(float)
        lnTuple[lnTuple == 0] = np.finfo(float).eps

        N4 = np.maximum(0, np.floor(np.log2(np.abs(lnTuple))).astype(int))
        N = np.maximum(0, N4 - 4)
        esc = np.abs(nTuple) > 15

        nTupleESC = nTuple.copy()
        nTupleESC[esc] = np.sign(nTupleESC[esc]) * 16

        huffIndex = int(np.abs(nTupleESC) @ powers)

        hexVal = LUT[huffIndex, 2]
        huffLen = LUT[huffIndex, 1]

        bits = format(int(hexVal), f'0{int(huffLen)}b')

        escSeq = ''
        for k in range(nTupleSize):
            if esc[k]:
                escSeq += '1' * N[k]
                escSeq += '0'
                escSeq += format(abs(nTuple[k]) - (1 << N4[k]), f'0{N4[k]}b')

        signBits = ''.join('1' if v < 0 else '0' for v in nTuple)
        huffSec.append(bits + signBits + escSeq)

    return ''.join(huffSec)

# ------------------ DECODE ------------------

def decode_huff(huff_sec, huff_LUT):
    """
    Decode a Huffman-encoded stream.

    Parameters
    ----------
    huff_sec : array-like of int or str
        Huffman encoded stream as a sequence of 0 and 1 (string or list/array).
    huff_LUT : dict
        Huffman lookup table with keys:
            - 'invTable': inverse table (numpy array)
            - 'codebook': codebook number
            - 'nTupleSize': tuple size
            - 'maxAbsCodeVal': maximum absolute code value
            - 'signedValues': True/False

    Returns
    -------
    decCoeffs : list of int
        Decoded quantized coefficients.
    """

    h = huff_LUT['invTable']
    huffCodebook = huff_LUT['codebook']
    nTupleSize = huff_LUT['nTupleSize']
    maxAbsCodeVal = huff_LUT['maxAbsCodeVal']
    signedValues = huff_LUT['signedValues']

    # Convert string to array of ints
    if isinstance(huff_sec, str):
        huff_sec = np.array([int(b) for b in huff_sec])

    eos = False
    decCoeffs = []
    streamIndex = 0

    while not eos:
        wordbit = 0
        r = 0  # start at root

        # Decode Huffman word using inverse table
        while True:
            b = huff_sec[streamIndex + wordbit]
            wordbit += 1
            rOld = r
            r = h[rOld, b]
            if h[r, 0] == 0 and h[r, 1] == 0:
                symbolIndex = h[r, 2] - 1  # zero-based
                streamIndex += wordbit
                break

        # Decode n-tuple magnitudes
        if signedValues:
            base = 2 * maxAbsCodeVal + 1
            nTupleDec = []
            tmp = symbolIndex
            for p in reversed(range(nTupleSize)):
                val = tmp // (base ** p)
                nTupleDec.append(val - maxAbsCodeVal)
                tmp = tmp % (base ** p)
            nTupleDec = np.array(nTupleDec)
        else:
            base = maxAbsCodeVal + 1
            nTupleDec = []
            tmp = symbolIndex
            for p in reversed(range(nTupleSize)):
                val = tmp // (base ** p)
                nTupleDec.append(val)
                tmp = tmp % (base ** p)
            nTupleDec = np.array(nTupleDec)

            # Apply sign bits
            nTupleSignBits = huff_sec[streamIndex:streamIndex + nTupleSize]
            nTupleSign = -(np.sign(nTupleSignBits - 0.5))
            streamIndex += nTupleSize
            nTupleDec = nTupleDec * nTupleSign

        # Handle escape sequences
        escIndex = np.where(np.abs(nTupleDec) == 16)[0]
        if huffCodebook == 11 and escIndex.size > 0:
            for idx in escIndex:
                N = 0
                b = huff_sec[streamIndex]
                while b:
                    N += 1
                    b = huff_sec[streamIndex + N]
                streamIndex += N
                N4 = N + 4
                escape_word = huff_sec[streamIndex:streamIndex + N4]
                escape_value = 2 ** N4 + int("".join(map(str, escape_word)), 2)
                nTupleDec[idx] = escape_value
                streamIndex += N4 + 1
            # Apply signs again
            nTupleDec[escIndex] *= nTupleSign[escIndex]

        decCoeffs.extend(nTupleDec.tolist())

        if streamIndex >= len(huff_sec):
            eos = True

    return decCoeffs
    

