#!/usr/bin/env python3
import os
import sys
import time
import socket
import struct
import argparse
import random
import tempfile
import subprocess
from tqdm import tqdm  # For progress bar

# Default configuration values
DEFAULT_MULTICAST_ADDR = "239.192.55.1"
DEFAULT_PORT = 1681
DEFAULT_ZONE_INFO = "100000000000000000000000"  # Zone 1
DEFAULT_SEND_DUPLICATES = True
DEFAULT_CHUNK_SIZE = 900
DEFAULT_TTL = 2
DEFAULT_HEADER = "4d454c"  # MEL header
DEFAULT_COMMAND = "070301"  # Stream command

# Audio quality presets
HIGH_QUALITY = {
    "bitrate": "64k",
    "sample_rate": 48000,
    "channels": 1,  # mono
    "sample_format": "s32",  # 16-bit samples (default for MP3)
    "description": "Ass but no Durchfall"
    # Note: Original software appears to use LAME 3.99.5
}

LOW_QUALITY = {
    "bitrate": "64k",
    "sample_rate": 32000,
    "channels": 1,  # mono
    "sample_format": "s16",  # 16-bit samples (default for MP3)
    "description": "Ass with durchfalls"
}

def compute_psa_checksum(data: bytes) -> bytes:
    """Compute PSA checksum for packet data."""
    var_e = 0x0000  # Starting seed value
    for i in range(len(data)):
        var_e ^= (data[i] + i) & 0xFFFF
    return var_e.to_bytes(2, 'big')  # 2-byte checksum, big-endian

def create_mp3_packet(sequence_num, zone_info, stream_id, mp3_chunk):
    """Create a PSA packet containing MP3 data."""
    # Part 1: Header (static "MEL")
    header = bytes.fromhex("4d454c")
    
    # Prepare all the components that will go into the payload
    # Start marker (0100)
    start_marker = bytes.fromhex("0100")
    
    # Sequence number (2 bytes, Little Endian with FF padding)
    seq = sequence_num.to_bytes(1, 'little') + b'\xff'
    
    # Command (static 070301)
    command = bytes.fromhex(DEFAULT_COMMAND)
    
    # Zone info
    zones = bytes.fromhex(zone_info)
    
    # Stream ID (4 bytes)
    stream_id_bytes = stream_id.to_bytes(4, 'little')
    
    # Constant data (appears in all original packets)
    constant_data = bytes.fromhex("05080503e8")
    
    # Build the payload (everything that comes after start_marker)
    payload = seq + command + zones + stream_id_bytes + constant_data + mp3_chunk
    
    # Calculate the length for the header
    # Length is everything AFTER the length field: start_marker + payload
    # But NOT including checksum (which is calculated last)
    length_value = len(start_marker + payload) + 7
    
    # Insert the length as a 2-byte value (big endian)
    length_bytes = length_value.to_bytes(2, 'big')
    
    # Assemble the packet without checksum
    packet_data = header + length_bytes + start_marker + payload
    
    # Calculate and append checksum
    checksum = compute_psa_checksum(packet_data)
    
    # Debug the packet length
    final_packet = packet_data + checksum
    print(f"Packet length: {len(final_packet)} bytes, Length field: {length_value} (0x{length_value:04x})")
    
    return final_packet

def send_multicast_packet(sock, packet, multicast_addr, port, send_duplicates=True):
    """Send a packet to the multicast address."""
    try:
        # Send the packet
        sock.sendto(packet, (multicast_addr, port))
        
        # Send a duplicate (common in multicast to improve reliability)
        if send_duplicates:
            sock.sendto(packet, (multicast_addr, port))
        
        return True
    except Exception as e:
        print(f"Error sending packet: {e}")
        return False

def setup_multicast_socket(ttl=DEFAULT_TTL):
    """Set up a socket for sending multicast UDP."""
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
    sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)
    return sock

def check_dependencies():
    """Check if required dependencies are installed."""
    try:
        subprocess.run(['ffmpeg', '-version'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    except FileNotFoundError:
        print("Error: ffmpeg is required but not found.")
        print("Please install ffmpeg and make sure it's available in your PATH.")
        sys.exit(1)

def get_audio_info(mp3_file):
    """Get audio file information using ffprobe."""
    try:
        # Get audio stream information
        cmd = [
            'ffprobe', '-v', 'quiet', '-print_format', 'json',
            '-show_streams', '-select_streams', 'a:0', mp3_file
        ]
        result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
        
        if result.returncode != 0:
            print(f"Error analyzing audio file: {result.stderr}")
            return None
        
        import json
        info = json.loads(result.stdout)
        
        if 'streams' not in info or len(info['streams']) == 0:
            print("No audio stream found in the file.")
            return None
        
        stream = info['streams'][0]
        
        # Extract relevant information
        bit_rate = int(stream.get('bit_rate', '0')) // 1000 if 'bit_rate' in stream else None
        sample_rate = int(stream.get('sample_rate', '0'))
        channels = int(stream.get('channels', '0'))
        codec_name = stream.get('codec_name', 'unknown')
        
        return {
            'bit_rate': bit_rate,
            'sample_rate': sample_rate,
            'channels': channels,
            'codec_name': codec_name
        }
    
    except Exception as e:
        print(f"Error getting audio information: {e}")
        return None

def transcode_mp3(input_file, quality_preset, include_metadata=False):
    """Transcode MP3 file to match required specifications."""
    print(f"Transcoding audio to {quality_preset['description']}...")
    
    # Create temporary output file
    fd, temp_output = tempfile.mkstemp(suffix='.mp3')
    os.close(fd)
    
    try:
        # Base command
        cmd = [
            'ffmpeg',
            '-y',  # Overwrite output file
            '-i', input_file,
            '-codec:a', 'libmp3lame',  # Force LAME encoder
            '-ac', str(quality_preset['channels']),
            '-ar', str(quality_preset['sample_rate']),
            '-b:a', quality_preset['bitrate'],
            '-sample_fmt', quality_preset['sample_format'],  # Use configurable sample format
        ]
        
        # Add options to minimize metadata if requested
        if not include_metadata:
            cmd.extend(['-metadata', 'title=', '-metadata', 'artist=', 
                       '-metadata', 'album=', '-metadata', 'comment=',
                       '-map_metadata', '-1'])  # Strip all metadata
        
        cmd.append(temp_output)
        
        result = subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True)
        
        if result.returncode != 0:
            print(f"Error transcoding audio: {result.stderr}")
            os.remove(temp_output)
            return None
        
        return temp_output
    
    except Exception as e:
        print(f"Error during transcoding: {e}")
        if os.path.exists(temp_output):
            os.remove(temp_output)
        return None

def prepare_mp3_file(mp3_file, quality, include_metadata=False):
    """Ensure the MP3 file meets the required specifications."""
    # Check if quality is valid
    quality_preset = HIGH_QUALITY if quality == "high" else LOW_QUALITY
    
    # Get audio file information
    info = get_audio_info(mp3_file)
    if not info:
        return None
    
    print(f"Audio file details: {info['codec_name']}, {info['bit_rate']}kbps, "
          f"{info['sample_rate']}Hz, {info['channels']} channel(s)")
    
    # Check if transcoding is needed
    needs_transcode = False
    
    if info['codec_name'].lower() != 'mp3':
        print("Non-MP3 format detected. Transcoding required.")
        needs_transcode = True
    elif info['bit_rate'] is None or abs(info['bit_rate'] - int(quality_preset['bitrate'][:-1])) > 5:
        print(f"Bitrate mismatch: {info['bit_rate']}kbps vs {quality_preset['bitrate']}. Transcoding required.")
        needs_transcode = True
    elif info['sample_rate'] != quality_preset['sample_rate']:
        print(f"Sample rate mismatch: {info['sample_rate']}Hz vs {quality_preset['sample_rate']}Hz. Transcoding required.")
        needs_transcode = True
    elif info['channels'] != quality_preset['channels']:
        print(f"Channel count mismatch: {info['channels']} vs {quality_preset['channels']} (mono). Transcoding required.")
        needs_transcode = True
    
    # If transcoding is needed, do it
    if needs_transcode:
        return transcode_mp3(mp3_file, quality_preset, include_metadata)
    else:
        print("Audio file already meets required specifications.")
        return mp3_file

def calculate_delay_ms(chunk_size, quality):
    """Calculate the appropriate delay between chunks for real-time streaming."""
    # Calculate bytes_per_second dynamically from the bitrate
    if quality == "high":
        # 128kbps = 16,000 bytes/second
        bytes_per_second = 16000
    else:
        # 40kbps = 5,000 bytes/second
        bytes_per_second = 5000
    
    # Calculate how long this chunk would play for in real-time
    delay_ms = (chunk_size / bytes_per_second) * 1000
    return delay_ms

def stream_mp3_to_psa(mp3_file, multicast_addr, port, zone_info, send_duplicates,
                      chunk_size, quality, include_metadata=False):
    """Stream an MP3 file to PSA system."""
    try:
        # Check dependencies
        check_dependencies()
        
        # Prepare MP3 file (transcode if needed)
        prepared_file = prepare_mp3_file(mp3_file, quality, include_metadata)
        if not prepared_file:
            print("Failed to prepare MP3 file.")
            return
        
        using_temp_file = prepared_file != mp3_file
        
        try:
            # Generate random stream ID for this transmission
            stream_id = random.randint(0, 0xFFFFFFFF)  # Changed to 32-bit to accommodate 4 bytes
            
            # Open the MP3 file
            with open(prepared_file, 'rb') as f:
                mp3_data = f.read()
            
            # Calculate number of chunks
            chunks = [mp3_data[i:i+chunk_size] for i in range(0, len(mp3_data), chunk_size)]
            total_chunks = len(chunks)
            
            # Calculate the appropriate delay for real-time streaming
            delay_ms = calculate_delay_ms(chunk_size, quality)
            
            print(f"Streaming {os.path.basename(mp3_file)} ({len(mp3_data)/1024:.2f} KB)")
            print(f"Split into {total_chunks} packets of {chunk_size} bytes each")
            print(f"Sending to multicast {multicast_addr}:{port}")
            print(f"Stream ID: {stream_id:08x}, Zone info: {zone_info}")
            print(f"Duplicate packets: {'Yes' if send_duplicates else 'No'}")
            print(f"Quality preset: {HIGH_QUALITY['description'] if quality == 'high' else LOW_QUALITY['description']}")
            print(f"Including metadata: {'Yes' if include_metadata else 'No'}")
            print(f"Real-time streaming delay: {delay_ms:.2f}ms per packet")
            
            # Setup multicast socket
            sock = setup_multicast_socket()
            
            # Process and send each chunk
            sequence_num = 0
            with tqdm(total=total_chunks, desc="Streaming progress") as pbar:
                try:
                    for i, chunk in enumerate(chunks):
                        # Create packet with current sequence number
                        packet = create_mp3_packet(sequence_num, zone_info, stream_id, chunk)
                        
                        # Send the packet
                        success = send_multicast_packet(sock, packet, multicast_addr, port, send_duplicates)
                        
                        # Increment sequence num
                        sequence_num = (sequence_num + 1) % 256
                        
                        # Update progress bar
                        pbar.update(1)
                        
                        # Delay to match real-time playback
                        if i < total_chunks - 1:  # No need to wait after the last chunk
                            time.sleep(delay_ms / 1000.0)
                except Exception as e:
                    print(f"Error during streaming: {e}")
            
            print("\nStreaming completed successfully!")
            
        finally:
            # Clean up temporary file if created
            if using_temp_file and os.path.exists(prepared_file):
                os.remove(prepared_file)
                
    except FileNotFoundError:
        print(f"Error: MP3 file {mp3_file} not found")
        sys.exit(1)
    except Exception as e:
        print(f"Error during streaming: {e}")
        sys.exit(1)

def main():
    # Set up command line arguments
    parser = argparse.ArgumentParser(description="Stream MP3 to Bodet PSA System")
    parser.add_argument("mp3_file", help="Path to MP3 file to stream")
    parser.add_argument("-a", "--addr", default=DEFAULT_MULTICAST_ADDR, 
                        help=f"Multicast address (default: {DEFAULT_MULTICAST_ADDR})")
    parser.add_argument("-p", "--port", type=int, default=DEFAULT_PORT,
                        help=f"UDP port (default: {DEFAULT_PORT})")
    parser.add_argument("-z", "--zone", default=DEFAULT_ZONE_INFO, 
                        help=f"Hex zone info (default: Zone 1)")
    parser.add_argument("-n", "--no-duplicates", action="store_true",
                        help="Don't send duplicate packets (default: send duplicates)")
    parser.add_argument("-c", "--chunk-size", type=int, default=DEFAULT_CHUNK_SIZE,
                        help=f"MP3 chunk size in bytes (default: {DEFAULT_CHUNK_SIZE})")
    parser.add_argument("-q", "--quality", choices=["high", "low"], default="high",
                        help="Audio quality preset: high (128kbps, 48kHz) or low (40kbps, 32kHz) (default: high)")
    parser.add_argument("-m", "--include-metadata", action="store_true",
                        help="Include metadata in MP3 stream (default: no metadata)")
    parser.add_argument("-s", "--sample-format", choices=["s16", "s24", "s32"], default=None,
                        help="Sample format to use for transcoding (default: s16)")
    
    args = parser.parse_args()
    
    # Stream the MP3 file
    stream_mp3_to_psa(
        args.mp3_file,
        args.addr,
        args.port,
        args.zone,
        not args.no_duplicates,
        args.chunk_size,
        args.quality,
        args.include_metadata
    )

if __name__ == "__main__":
    main()
