#!/usr/bin/env python3
"""
flash_upload.py — Upload audio to Baby Mobile v2 via serial.

This is a fallback for uploading audio if USB mass storage isn't working.
Normally you'd just drag files onto the USB drive.

Usage:
  python3 flash_upload.py /dev/ttyACM0 song1.mp3 song2.wav song3.raw

Audio files will be auto-converted to 8kHz/8-bit/unsigned PCM using ffmpeg.

Track table format (v2):
  [0]     magic = 0xBB
  [1]     num_tracks
  [2..3]  reserved
  [4..]   12-byte entries: start(4) + length(4) + rate_khz(1) + bits(1) + pad(2)
"""

import serial
import struct
import sys
import time
import os
import subprocess
import shutil

BAUD = 115200
PAGE_SIZE = 256
SECTOR_SIZE = 4096
AUDIO_START = 0x1000
MAX_TRACKS = 32
FLASH_SIZE = 16 * 1024 * 1024
TABLE_MAGIC = 0xBB
ENTRY_SIZE = 12


def open_serial(port):
    ser = serial.Serial(port, BAUD, timeout=2)
    time.sleep(2)
    ser.read(ser.in_waiting)
    return ser


def convert_to_raw(input_path):
    """Convert any audio file to 8kHz 8-bit unsigned PCM."""
    if input_path.endswith('.raw'):
        return input_path

    raw_path = input_path + ".raw"
    if not shutil.which('ffmpeg'):
        print(f"  ERROR: ffmpeg not found. Convert manually:")
        print(f"    ffmpeg -i {input_path} -ar 8000 -ac 1 -f u8 -acodec pcm_u8 {raw_path}")
        sys.exit(1)

    print(f"  Converting {os.path.basename(input_path)}...")
    result = subprocess.run([
        'ffmpeg', '-y', '-i', input_path,
        '-ar', '8000', '-ac', '1', '-f', 'u8', '-acodec', 'pcm_u8',
        raw_path
    ], capture_output=True)

    if result.returncode != 0:
        print(f"  ERROR: ffmpeg failed: {result.stderr.decode()[-200:]}")
        sys.exit(1)

    return raw_path


def build_track_table(tracks):
    """Build v2 track table."""
    table = bytearray()
    table.append(TABLE_MAGIC)
    table.append(len(tracks))
    table.extend(b'\x00\x00')  # reserved

    for start, length in tracks:
        table.extend(struct.pack('>I', start))
        table.extend(struct.pack('>I', length))
        table.append(8)   # sample rate kHz
        table.append(8)   # bits per sample
        table.extend(b'\x00\x00')

    # Pad to page boundary
    while len(table) % PAGE_SIZE != 0:
        table.append(0xFF)

    return bytes(table)


def serial_cmd(ser, cmd):
    """Send a single-char command and read response line."""
    ser.write(cmd.encode())
    return ser.readline().decode().strip()


def serial_write_page(ser, addr, data):
    """Write up to 256 bytes via the serial 'W' command."""
    assert len(data) <= 256
    ser.write(b'W')
    ser.write(struct.pack('>I', addr)[1:])   # 3-byte addr
    ser.write(struct.pack('>H', len(data)))   # 2-byte length
    ser.write(data)
    return ser.readline().decode().strip()


def serial_write_data(ser, start_addr, data, label=""):
    """Write arbitrary-length data, page by page."""
    total = len(data)
    written = 0
    addr = start_addr

    while written < total:
        chunk = data[written:written + PAGE_SIZE]
        if len(chunk) < PAGE_SIZE:
            chunk = chunk + b'\x80' * (PAGE_SIZE - len(chunk))

        serial_write_page(ser, addr, chunk)
        written += PAGE_SIZE
        addr += PAGE_SIZE

        pct = min(100, written * 100 // total)
        print(f"\r  Writing {label}: {pct}%", end="", flush=True)

    print()


def main():
    if len(sys.argv) < 3:
        print("Usage: python3 flash_upload.py <port> <audio_file> [...]")
        print("  Supported: .raw .mp3 .wav .ogg .flac .aac")
        sys.exit(1)

    port = sys.argv[1]
    audio_files = sys.argv[2:]

    # Convert
    print("=== Preparing audio ===")
    raw_files = []
    for f in audio_files:
        raw = convert_to_raw(f)
        size = os.path.getsize(raw)
        print(f"  {os.path.basename(f)}: {size/1024:.0f} KB ({size/8000:.1f}s)")
        raw_files.append(raw)

    total = sum(os.path.getsize(f) for f in raw_files)
    print(f"  Total: {total/1024:.0f} KB ({total/8000/60:.1f} min)")

    # Connect
    print(f"\n=== Connecting to {port} ===")
    ser = open_serial(port)
    print(f"  {serial_cmd(ser, 'I')}")

    # Erase
    print("\n=== Erasing flash ===")
    ser.write(b'E')
    while True:
        line = ser.readline().decode().strip()
        if line: print(f"  {line}")
        if "Done" in line: break

    # Write tracks
    print("\n=== Writing audio ===")
    tracks = []
    addr = AUDIO_START

    for i, raw in enumerate(raw_files):
        with open(raw, 'rb') as f:
            data = f.read()

        tracks.append((addr, len(data)))
        serial_write_data(ser, addr, data, label=f"Track {i+1}")

        addr += len(data)
        # Align to sector
        if addr % SECTOR_SIZE:
            addr = ((addr // SECTOR_SIZE) + 1) * SECTOR_SIZE

    # Write track table
    print("\n=== Writing track table ===")
    table = build_track_table(tracks)
    serial_write_data(ser, 0, table, label="Track table")

    # Summary
    print(f"\n=== Done! {len(tracks)} tracks uploaded ===")
    for i, (s, l) in enumerate(tracks):
        print(f"  Track {i+1}: 0x{s:06X}, {l/8000:.1f}s")

    # Cleanup
    for raw, orig in zip(raw_files, audio_files):
        if raw != orig and os.path.exists(raw):
            os.remove(raw)

    ser.close()


if __name__ == "__main__":
    main()
