import os
import sys
import time
import serial
import threading

# Platform-specific character reading for non-blocking terminal input
if os.name == 'nt':
    import msvcrt
    def get_char():
        if msvcrt.kbhit():
            return msvcrt.getch().decode('utf-8', errors='ignore')
        return None
else:
    import select
    import tty
    import termios

    def get_char():
        # Reads a single keypress without waiting for Enter (POSIX).
        fd = sys.stdin.fileno()
        old_settings = termios.tcgetattr(fd)
        try:
            tty.setraw(sys.stdin.fileno())
            rlist, _, _ = select.select([sys.stdin], [], [], 0.05)
            if rlist:
                return sys.stdin.read(1)
            return None
        finally:
            termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)

# --- CONFIGURATION ---
# May need to change COM6 and ttyUSB0 to match hardware used.

SERIAL_PORT = 'COM6' if os.name == 'nt' else '/dev/ttyUSB0'  
BAUD_RATE = 9600
CHAR_DELAY = 0.005  # Fixed delay between characters in seconds
BLOCK_SIZE = 4      # Data bytes per output block

def read_from_serial(ser, stop_event):
    # Continuously reads and displays incoming characters from the serial port.
    while not stop_event.is_set():
        if ser.in_waiting > 0:
            try:
                data = ser.read(ser.in_waiting).decode('ascii', errors='replace')
                print(data, end='', flush=True)
            except Exception as e:
                print(f"\n[Read Error]: {e}")
        time.sleep(0.01)

def send_ascii_with_delay(ser, text):
    # Sends ASCII string character-by-character with a fixed delay.
    for char in text:
        ser.write(char.encode('ascii', errors='ignore'))
        ser.flush()
        time.sleep(CHAR_DELAY)

def send_escape_codes(ser, count=2):
    # Sends ASCII Escape characters (\x1B) with delay.
    ESC_CHAR = '\x1B'
    print(f"\n[System]: Sending {count} ASCII Escape code(s)...")
    for _ in range(count):
        ser.write(ESC_CHAR.encode('ascii'))
        ser.flush()
        time.sleep(CHAR_DELAY)

def process_asc_file(file_path, block_size):
    # Parses each line: treats the FIRST item as the address, 
    # and chunks the REPEAT data into blocks of `block_size`.
    # Outputs lines terminated ONLY with CR ('\r').
    with open(file_path, 'r', encoding='ascii', errors='ignore') as file:
        for line in file:
            tokens = line.strip().split()
            if not tokens:
                continue

            base_addr_str = tokens[0]
            data_tokens = tokens[1:]

            try:
                base_addr = int(base_addr_str, 16)
                is_hex = True
            except ValueError:
                try:
                    base_addr = int(base_addr_str, 10)
                    is_hex = False
                except ValueError:
                    continue

            if not data_tokens:
                yield f"{base_addr_str}:\r"
                continue

            for i in range(0, len(data_tokens), block_size):
                chunk = data_tokens[i : i + block_size]
                current_addr = base_addr + i
                addr_fmt = f"{current_addr:04X}" if is_hex else f"{current_addr}"
                data_str = " ".join(chunk)
                yield f"{addr_fmt}:{data_str}\r"

def process_raw_file(file_path):
    # Reads a file line by line without conversion.
    with open(file_path, 'r', encoding='ascii', errors='ignore') as file:
        for line in file:
            yield line

def handle_upload(ser, last_filename):
    # Prompts for a file and transmits it over serial.
    default_prompt = f" [{last_filename}]" if last_filename else ""
    prompt_str = f"\nEnter file path to upload{default_prompt}: "
    
    file_path = input(prompt_str).strip()
    
    # Remove quotes if user dragged and dropped file into terminal
    file_path = file_path.strip('\'"')

    # If user pressed enter without typing, use previous file
    if not file_path and last_filename:
        file_path = last_filename

    if not file_path:
        print("[Upload Cancelled]: No filename provided.")
        return last_filename

    if not os.path.exists(file_path):
        print(f"[Error]: File '{file_path}' not found.")
        return last_filename

    try:
        send_escape_codes(ser, count=2)
        time.sleep(0.2)

        # Check file extension
        _, ext = os.path.splitext(file_path)
        is_asc = ext.lower() == '.asc'

        if is_asc:
            print(f"\n[Uploading - ASC Mode]: {file_path} ...")
            generator = process_asc_file(file_path, BLOCK_SIZE)
        else:
            print(f"\n[Uploading - Raw Text Mode]: {file_path} ...")
            generator = process_raw_file(file_path)

        for formatted_line in generator:
            send_ascii_with_delay(ser, formatted_line)
        
        return file_path  # Return new filename to save as last_filename
    except Exception as e:
        print(f"\n[Upload Failed]: {e}")
        return last_filename

def interactive_terminal():
    try:
        ser = serial.Serial(SERIAL_PORT, BAUD_RATE, timeout=1)
        print(f"Connected to {SERIAL_PORT} at {BAUD_RATE} bps.")
        print("Type directly to send characters to serial.")
        print("Press 'U' to upload a file (.asc files will be converted).")
        print("Press 'Q' to quit.")
        print("-" * 40)
    except serial.SerialException as e:
        print(f"Failed to open serial port {SERIAL_PORT}: {e}")
        return

    stop_event = threading.Event()
    reader_thread = threading.Thread(target=read_from_serial, args=(ser, stop_event), daemon=True)
    reader_thread.start()

    last_filename = None

    try:
        while True:
            char = get_char()
            if char:
                # Trigger exit on 'q' or 'Q'
                if char in ('q', 'Q'):
                    print("\n\n'q' pressed. Exiting terminal session...")
                    break
                # Trigger upload menu on 'u' or 'U'
                elif char in ('u', 'U'):
                    last_filename = handle_upload(ser, last_filename)
                else:
                    # Pass-through any other character directly to the serial port
                    ser.write(char.encode('ascii', errors='ignore'))
                    ser.flush()

    except KeyboardInterrupt:
        print("\n\nExiting terminal session...")
    finally:
        stop_event.set()
        reader_thread.join(timeout=1.0)
        if ser.is_open:
            ser.close()
            print("Serial port closed.")

if __name__ == '__main__':
    interactive_terminal()
