#!/usr/bin/env python3
"""Decompress the BWT+MTF+LZW equation database files."""
import json, struct, sys
from pathlib import Path

def lzw_decode(data: bytes, max_dict: int = 4096) -> list[int]:
    """Decode LZW-compressed data into a list of integers."""
    result = []
    if not data:
        return result
    
    dict_size = 256
    dictionary = {i: bytes([i]) for i in range(dict_size)}
    
    # Read 12-bit values regardless of max_dict
    bits = []
    for byte in data:
        bits.extend([(byte >> (7 - i)) & 1 for i in range(8)])
    
    pos = 0
    def read_bits(n):
        nonlocal pos
        val = 0
        for _ in range(n):
            val = (val << 1) | bits[pos]
            pos += 1
        return val
    
    code_bits = 9
    prev = read_bits(code_bits)
    result.append(prev)
    prev_bytes = dictionary[prev]
    
    while pos < len(bits):
        code = read_bits(code_bits)
        if code in dictionary:
            entry = dictionary[code]
        elif code == dict_size:
            entry = prev_bytes + prev_bytes[:1]
        else:
            break
        
        result.extend(entry)
        dictionary[dict_size] = prev_bytes + entry[:1]
        dict_size += 1
        if dict_size >= (1 << code_bits) and code_bits < 12:
            code_bits += 1
        prev_bytes = entry
    
    return result


def mtf_decode(data: list[int]) -> list[int]:
    """Move-to-front decoding."""
    alphabet = list(range(256))
    result = []
    for code in data:
        if code < len(alphabet):
            val = alphabet[code]
            result.append(val)
            alphabet.pop(code)
            alphabet.insert(0, val)
    return result


def bwt_inverse(data: list[int], primary_index: int) -> bytes:
    """Inverse Burrows-Wheeler transform."""
    if not data:
        return b''
    n = len(data)
    # Count occurrences
    counts = [0] * 256
    for b in data:
        counts[b] += 1
    
    # Compute cumulative counts (start positions)
    start = [0] * 256
    total = 0
    for i in range(256):
        start[i] = total
        total += counts[i]
    
    # Build next array
    next_idx = [0] * n
    temp_count = [0] * 256
    for i in range(n):
        b = data[i]
        next_idx[i] = start[b] + temp_count[b]
        temp_count[b] += 1
    
    # Reconstruct
    result = bytearray()
    idx = primary_index
    for _ in range(n):
        idx = next_idx[idx]
        result.append(data[idx])
    
    return bytes(result)


def decompress(path: Path) -> dict:
    """Decompress an equation database file."""
    raw = path.read_bytes()
    
    # Read header
    offset = 0
    header_len = struct.unpack('>I', raw[offset:offset+4])[0]
    offset += 4
    header = json.loads(raw[offset:offset+header_len].decode())
    offset += header_len
    
    chain = header.get("chain", ["bwt", "mtf", "lzw"])
    meta = header.get("metadata", {})
    compressed = raw[offset:]
    
    # Apply decompression steps in reverse order
    data = compressed
    
    # Step 1: LZW decode
    lzw_meta = meta.get("lzw", {})
    integers = lzw_decode(data, lzw_meta.get("max_dict", 4096))
    
    # Step 2: MTF decode
    integers = mtf_decode(integers)
    
    # Step 3: BWT inverse
    bwt_meta = meta.get("bwt", {})
    primary = bwt_meta.get("primary_index", 0)
    raw_bytes = bwt_inverse(integers, primary)
    
    # Parse as JSON
    return json.loads(raw_bytes.decode("utf-8"))


def main():
    path = Path(sys.argv[1]) if len(sys.argv) > 1 else \
        "/home/allaun/Research Stack/3-Mathematical-Models/equations_compressed/chain_equations_20260504_134248.compressed"
    
    print(f"Decompressing {path}...")
    data = decompress(path)
    
    if isinstance(data, dict):
        print(f"Type: dict with {len(data)} keys")
        for k, v in data.items():
            if isinstance(v, list):
                print(f"  {k}: list[{len(v)}]")
                if len(v) > 0:
                    print(f"    first: {str(v[0])[:200]}")
            elif isinstance(v, dict):
                print(f"  {k}: dict with {len(v)} keys")
            else:
                print(f"  {k}: {type(v).__name__} = {str(v)[:200]}")
    elif isinstance(data, list):
        print(f"Type: list[{len(data)}]")
        if len(data) > 0:
            print(f"  first: {str(data[0])[:200]}")


if __name__ == "__main__":
    main()
