import os
from typing import List, Callable, Optional
from .models import Packet, Page, TeletextService

def load_t42(file_path: str, progress_callback: Optional[Callable[[int, int], None]] = None) -> TeletextService:
    service = TeletextService()
    
    if not os.path.exists(file_path):
        return service
        
    total_bytes = os.path.getsize(file_path)
    total_packets = total_bytes // 42
    processed_packets = 0
    
    # Magazine buffers: magazine -> {row_num: Packet}
    magazine_buffers = {m: {} for m in range(1, 9)}
    # Active page lookup: magazine -> Page object (for O(1) access)
    active_pages = {m: None for m in range(1, 9)}
    
    with open(file_path, 'rb') as f:
        while True:
            chunk = f.read(42)
            if not chunk: break
            if len(chunk) < 42: break
            
            processed_packets += 1
            if progress_callback and processed_packets % 500 == 0:
                progress_callback(processed_packets, total_packets)
                
            packet = Packet(chunk)
            service.all_packets.append(packet)
            
            mag = packet.magazine
            buffer = magazine_buffers[mag]
            
            if packet.row == 0:
                p_num, sub_code, control_bits, language = parse_header(packet.data)
                
                # Check Erase Page bit (C4 is bit 0 of control_bits)
                erase_page = bool(control_bits & 1)
                
                if erase_page:
                    magazine_buffers[mag] = {0: packet}
                    buffer = magazine_buffers[mag]
                else:
                    buffer[0] = packet
                
                # Create snapshot
                new_page = Page(
                    magazine=mag, 
                    page_number=p_num, 
                    sub_code=sub_code, 
                    control_bits=control_bits,
                    language=language
                )
                
                # Efficient cloning: use the existing Packet objects where possible, 
                # but we MUST clone the data bytearray if we plan to edit it later.
                for r_num, pkt in sorted(buffer.items()):
                    # Create a new packet shell sharing the original_data but with its own data bytearray
                    cloned_pkt = Packet(pkt.original_data)
                    cloned_pkt.data = bytearray(pkt.data)
                    new_page.packets.append(cloned_pkt)
                
                service.pages.append(new_page)
                active_pages[mag] = new_page # Update active page lookup
                
            elif 1 <= packet.row <= 31:
                # Update the running buffer
                buffer[packet.row] = packet
                
                # Update the active snapshot immediately
                target_page = active_pages[mag]
                if target_page:
                    # Update row in the current active page
                    found_row = False
                    for i, p in enumerate(target_page.packets):
                        if p.row == packet.row:
                            target_page.packets[i] = packet
                            found_row = True
                            break
                    if not found_row:
                        target_page.packets.append(packet)

    return service


def encode_hamming_8_4(value):
    # Value is 4 bits (0-15)
    d1 = (value >> 0) & 1
    d2 = (value >> 1) & 1
    d3 = (value >> 2) & 1
    d4 = (value >> 3) & 1
    
    # Parity bits (Odd parity default? Or standard Hamming?)
    # Teletext spec: 
    # P1 = 1 + D1 + D2 + D4 (mod 2) -> Inverse of even parity check?
    # Actually, simpler to look up or calculate. 
    # Let's match typical implementation:
    # P1 (b0) covers 1,3,7 (D1, D2, D4)
    # P2 (b2) covers 1,5,7 (D1, D3, D4)
    # P3 (b4) covers 3,5,7 (D2, D3, D4)
    # P4 (b6) covers all.
    # Teletext uses ODD parity for the hamming bits usually?
    # "Hamming 8/4 with odd parity"
    
    p1 = 1 ^ d1 ^ d2 ^ d4
    p2 = 1 ^ d1 ^ d3 ^ d4
    p3 = 1 ^ d2 ^ d3 ^ d4
    
    res = (p1 << 0) | (d1 << 1) | \
          (p2 << 2) | (d2 << 3) | \
          (p3 << 4) | (d3 << 5) | \
          (d4 << 7)
          
    # P4 (bit 6) makes total bits odd
    # Count set bits so far
    set_bits = bin(res).count('1')
    p4 = 1 if (set_bits % 2 == 0) else 0
    
    res |= (p4 << 6)
    
    return res


def decode_packet_header(b1, b2):
    """
    Decodes the Magazine and Row from the first 2 bytes of a T42 packet.
    """
    d1 = decode_hamming_8_4(b1)
    d2 = decode_hamming_8_4(b2)
    
    mag = (d1 & 0b0111)
    if mag == 0: mag = 8
    
    row = (d2 << 1) | ((d1 >> 3) & 1)
    return mag, row

def save_t42(file_path: str, service: TeletextService, progress_callback: Optional[Callable[[int, int], None]] = None):
    total_packets = len(service.all_packets)
    processed = 0
    
    with open(file_path, 'wb') as f:
        for packet in service.all_packets:
            processed += 1
            if progress_callback and processed % 100 == 0:
                progress_callback(processed, total_packets)

            # Check if we can reuse the original header (preserving parity/integrity)
            use_original_header = False
            
            if hasattr(packet, 'original_data') and len(packet.original_data) >= 2:
                # Try to decode the original header
                try:
                    orig_mag, orig_row = decode_packet_header(packet.original_data[0], packet.original_data[1])
                    if orig_mag == packet.magazine and orig_row == packet.row:
                        use_original_header = True
                except:
                    pass
            
            if use_original_header:
                header = packet.original_data[:2]
            else:
                # Reconstruct header bytes
                mag = packet.magazine
                if mag == 8: mag = 0 # 0 encoded as 8
                
                # Bits:
                # B1 data: M1(0) M2(1) M3(2) R1(3)
                m1 = (mag >> 0) & 1
                m2 = (mag >> 1) & 1
                m3 = (mag >> 2) & 1
                r1 = (packet.row >> 0) & 1
                
                b1_val = m1 | (m2 << 1) | (m3 << 2) | (r1 << 3)
                b1_enc = encode_hamming_8_4(b1_val)
                
                # B2 data: R2(0) R3(1) R4(2) R5(3)
                r2 = (packet.row >> 1) & 1
                r3 = (packet.row >> 2) & 1
                r4 = (packet.row >> 3) & 1
                r5 = (packet.row >> 4) & 1
                
                b2_val = r2 | (r3 << 1) | (r4 << 2) | (r5 << 3)
                b2_enc = encode_hamming_8_4(b2_val)
                
                header = bytes([b1_enc, b2_enc])
                
            f.write(header + packet.data)

def decode_hamming_8_4(byte_val):
    return ((byte_val >> 1) & 1) | \
           (((byte_val >> 3) & 1) << 1) | \
           (((byte_val >> 5) & 1) << 2) | \
           (((byte_val >> 7) & 1) << 3)

def parse_header(data: bytearray):
    # Data is 40 bytes (after MRAG).
    # Byte 0: Page Units (PU)
    # Byte 1: Page Tens (PT)
    # Byte 2: Subcode S1 (bits 0-3)
    # Byte 3: Subcode S2 (bits 4-6), C4 (bit 7)
    # Byte 4: Subcode S3 (bits 8-11)
    # Byte 5: Subcode S4 (bits 12-13), C5 (bit 14), C6 (bit 15)
    # Byte 6: C7-C10
    # Byte 7: C11-C14 (C12-C14 are Language)
    
    pu = decode_hamming_8_4(data[0])
    pt = decode_hamming_8_4(data[1])
    
    # Page number: pt (tens), pu (units). 0x00 to 0xFF.
    page_num = ((pt & 0xF) << 4) | (pu & 0xF)
    
    # Subcode (13 bits)
    s1 = decode_hamming_8_4(data[2])
    s2 = decode_hamming_8_4(data[3])
    s3 = decode_hamming_8_4(data[4])
    s4 = decode_hamming_8_4(data[5])
    
    sub_code = (s1 & 0xF) | \
              ((s2 & 0x7) << 4) | \
              ((s3 & 0xF) << 7) | \
              ((s4 & 0x3) << 11)
              
    # Control bits C4-C14
    c4 = (s2 >> 3) & 1
    c5 = (s4 >> 2) & 1
    c6 = (s4 >> 3) & 1
    
    c_7_10 = decode_hamming_8_4(data[6])
    c_11_14 = decode_hamming_8_4(data[7])
    
    # bitmask starting at index 0 for C4
    control_bits = c4 | (c5 << 1) | (c6 << 2) | \
                   ((c_7_10 & 0xF) << 3) | \
                   ((c_11_14 & 0xF) << 7)
                   
    # Language (C12, C13, C14)
    # c_11_14: bit 0:C11, bit 1:C12, bit 2:C13, bit 3:C14
    language = (c_11_14 >> 1) & 0x7
    
    return page_num, sub_code, control_bits, language

def save_tti(file_path: str, page: Page):
    """
    Saves a single Page object to a TTI file.
    """
    with open(file_path, 'w', encoding='latin-1') as f:
        # Header Info
        # DS - Source? Description?
        f.write(f"DS,Teletext Editor Export\n")
        f.write(f"SP,{file_path}\n")
        
        # PN - Page Number mpp00
        # Typically TTI uses decimal integer for mppss? 
        # Or mppss as hex digits? 
        # Standard convention: mppss where m is 1-8, pp is 00-FF, ss is 00-99
        # Example: Page 100 -> PN,10000
        # Example: Page 1F0 -> PN,1F000
        f.write(f"PN,{page.magazine}{page.page_number:02X}00\n")
        
        # SC - Subcode ssss
        f.write(f"SC,{page.sub_code:04X}\n")
        
        # PS - Page Status
        # 8000 is typical for "Transmission"
        f.write(f"PS,8000\n")
        
        # RE - Region (Language)
        f.write(f"RE,{page.language}\n")
        
        # Lines
        # We need to construct the 40-char string for each row
        # Row 0 is special (Header)
        
        # Get all packets for this page
        # Map row -> packet
        rows = {}
        for p in page.packets:
            rows[p.row] = p
            
        for r in range(26): # 0 to 25
            if r in rows:
                packet = rows[r]
                # Packet data is 40 bytes (after the 2-byte header we stripped in Packet class? No wait)
                # Packet.data in our model IS the 40 bytes of character data (we strip MRAG in __post_init__)
                # So we just decode it as latin-1 to get the chars
                
                # However, we must ensure we don't have newlines or nulls breaking the text file structure
                # TTI format usually accepts raw bytes 0x00-0xFF if strictly handled, but often expects
                # mapped control codes.
                # Standard VBIT2 TTI handling treats it as a binary-safe string if mapped to char.
                
                # Row 0 special handling: The first 8 bytes of Row 0 are usually header flags in the packet,
                # but visually they are "P100    ".
                # TTI usually expects the visual representation for the line content.
                # But for transmission, we want the raw bytes.
                # OL,r,String
                
                data_str = packet.data.decode('latin-1')
                f.write(f"OL,{r},{data_str}\n")
            else:
                # Empty line? Usually omitted or written as empty
                pass
