import sys
import argparse
import math
from typing import List, Tuple

# Braille bitmasks (Standard Braille Unicode offsets)
# 1 4  ->  0x01  0x08
# 2 5  ->  0x02  0x10
# 3 6  ->  0x04  0x20
DOTS = {1: 0x01, 2: 0x02, 3: 0x04, 4: 0x08, 5: 0x10, 6: 0x20}
BRAILLE_OFFSET = 0x2800

# The two paths that swap every row to create the "Zig-Zag"
PATH_A = [1, 5, 3] # Left-Heavy Zig
PATH_B = [4, 2, 6] # Right-Heavy Zag

def get_a26(c: str) -> int:
    """Converts char to 1-26 index. Returns 0 for non-alpha."""
    if not c or not c.isalpha(): return 0
    return ord(c.upper()) - 64

def from_a26(v: int) -> str:
    """Converts 1-26 index back to char."""
    return chr(v + 64) if 1 <= v <= 26 else " "

def encode(text: str) -> str:
    if not text: return ""
    
    # Calculate dimensions
    width = math.ceil(len(text) / 2)
    pairs: List[Tuple[int, int]] = []
    
    for i in range(width):
        c1 = text[i*2] if i*2 < len(text) else " "
        c2 = text[i*2+1] if i*2+1 < len(text) else " "
        pairs.append((get_a26(c1), get_a26(c2)))

    # Determine height based on the character with the highest index
    max_val = max((max(p1, p2) for p1, p2 in pairs), default=0)
    height = math.ceil(max_val / 3)

    if height == 0: return ""

    # Initialize grid with empty Braille pattern
    grid = [[BRAILLE_OFFSET for _ in range(width)] for _ in range(height)]

    for col, (v1, v2) in enumerate(pairs):
        # Fill Left Character (v1)
        for d_idx in range(v1):
            row = d_idx // 3
            sub_idx = d_idx % 3
            path = PATH_A if row % 2 == 0 else PATH_B
            grid[row][col] |= DOTS[path[sub_idx]]
        
        # Fill Right Character (v2)
        for d_idx in range(v2):
            row = d_idx // 3
            sub_idx = d_idx % 3
            path = PATH_B if row % 2 == 0 else PATH_A
            grid[row][col] |= DOTS[path[sub_idx]]

    return "\n".join("".join(chr(c) for c in row) for row in grid)

def decode(braille_str: str) -> str:
    lines = [line for line in braille_str.splitlines() if line.strip()]
    if not lines: return ""
    
    # Calculate max width safely
    width = max(len(line) for line in lines)
    height = len(lines)
    results = []

    for col in range(width):
        tally_left = 0
        tally_right = 0
        
        for row in range(height):
            try:
                char_code = ord(lines[row][col])
            except IndexError:
                char_code = BRAILLE_OFFSET

            cell_val = char_code - BRAILLE_OFFSET
            if cell_val < 0 or cell_val > 0xFF: cell_val = 0
            
            p_left = PATH_A if row % 2 == 0 else PATH_B
            p_right = PATH_B if row % 2 == 0 else PATH_A
            
            for dot in p_left:
                if cell_val & DOTS[dot]: tally_left += 1
            for dot in p_right:
                if cell_val & DOTS[dot]: tally_right += 1
        
        results.append(from_a26(tally_left))
        results.append(from_a26(tally_right))
        
    return "".join(results).rstrip()

def main():
    parser = argparse.ArgumentParser(description="Zig-Zag Cascading Braille Cipher")
    
    # Mode selection
    parser.add_argument("-d", "--decode", action="store_true", help="Enable Decode mode (default is Encode)")
    
    # Input sources (mutually exclusive)
    input_group = parser.add_mutually_exclusive_group()
    input_group.add_argument("-e", "--text", help="Text to encode (Direct string input)")
    input_group.add_argument("-i", "--input", help="Input file path")
    
    # Output destination
    parser.add_argument("-o", "--output", help="Output file path (prints to stdout if omitted)")

    args = parser.parse_args()
    
    # --- Input Handling ---
    source_text = ""
    
    if args.decode:
        # DECODE MODE
        if args.input:
            # Read from file
            try:
                with open(args.input, "r", encoding="utf-8") as f:
                    source_text = f.read()
            except FileNotFoundError:
                sys.exit(f"Error: Input file '{args.input}' not found.")
        elif not sys.stdin.isatty():
             # Piped input
             source_text = sys.stdin.read()
        else:
            # Interactive paste mode
            print("Paste Braille grid below. Press Ctrl+D (Unix) or Ctrl+Z then Enter (Win) to finish:")
            try:
                source_text = sys.stdin.read()
            except KeyboardInterrupt:
                sys.exit(0)
                
        if not source_text:
             sys.exit("Error: No input provided for decoding.")
        
        result = decode(source_text)

    else:
        # ENCODE MODE
        if args.text:
            source_text = args.text
        elif args.input:
            try:
                with open(args.input, "r", encoding="utf-8") as f:
                    source_text = f.read()
            except FileNotFoundError:
                sys.exit(f"Error: Input file '{args.input}' not found.")
        elif not sys.stdin.isatty():
             # Piped input
             source_text = sys.stdin.read()
        else:
             parser.print_help()
             sys.exit(1)

        result = encode(source_text)

    # --- Output Handling ---
    if args.output:
        try:
            with open(args.output, "w", encoding="utf-8") as f:
                f.write(result)
                # Add newline if writing to file for cleaner viewing
                if args.decode: f.write("\n") 
        except OSError as e:
            sys.exit(f"Error writing to file: {e}")
    else:
        print(result)

if __name__ == "__main__":
    main()
