#!/usr/bin/env python3
"""
Simulator Data Source - Unified interface for receiving GPS data from flight simulators

This module provides a unified interface to receive GPS data from different simulator sources:
- Aerofly UDP (ForeFlight protocol)
- FSWidget TCP protocol

Both abflug_client.py and rewinger.py use this module to get simulator position data.
"""

import socket
import threading
import time
import re
from typing import Optional, Callable
from dataclasses import dataclass
from enum import Enum

# Constants
DEFAULT_AEROFLY_PORT = 49002  # Port for Aerofly UDP
FSWIDGET_PORT = 58585  # Fixed port for FSWidget TCP protocol
FSWIDGET_REQUEST = "req_gmap_dat\n"  # Request string for FSWidget


def get_local_ip() -> str:
    """Get the primary local IP address used for outbound traffic."""
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.connect(("8.8.8.8", 80))
        ip = s.getsockname()[0]
        s.close()
        return ip
    except Exception:
        return "127.0.0.1"


def discover_fswidget_ip(timeout_per_host: float = 0.2) -> Optional[str]:
    """Autodetect FSWidget TCP endpoint on the local /24 network."""
    local_ip = get_local_ip()
    if local_ip.startswith("127."):
        return "localhost"

    parts = local_ip.split(".")
    if len(parts) != 4:
        return "localhost"

    base_prefix = ".".join(parts[:3])

    def try_host(host: str) -> bool:
        try:
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            sock.settimeout(timeout_per_host)
            sock.connect((host, FSWIDGET_PORT))
            sock.sendall(FSWIDGET_REQUEST.encode("utf-8"))
            data = sock.recv(128)
            sock.close()
            msg = data.decode("utf-8", errors="ignore").strip()
            if msg.count("|") >= 7:
                return True
        except Exception:
            return False
        return False

    if try_host(local_ip):
        return local_ip

    for i in range(1, 255):
        candidate = f"{base_prefix}.{i}"
        if candidate == local_ip:
            continue
        if try_host(candidate):
            return candidate

    return None


def discover_udp_ip(udp_port: int = DEFAULT_AEROFLY_PORT, timeout_per_host: float = 0.2, scan_duration: float = 2.0) -> Optional[str]:
    """
    Autodetect UDP simulator endpoint on the local /24 network.
    
    This function scans the local network for UDP traffic on the specified port.
    It listens for XGPS messages and identifies the source IP address.
    
    Args:
        udp_port: UDP port to scan (default: 49002)
        timeout_per_host: Timeout per host check (default: 0.2 seconds)
        scan_duration: Total time to spend scanning (default: 2.0 seconds)
    
    Returns:
        IP address string if found, None otherwise
    """
    local_ip = get_local_ip()
    if local_ip.startswith("127."):
        # For localhost, just try binding to the port
        try:
            test_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            test_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            test_sock.settimeout(0.5)
            test_sock.bind(('', udp_port))
            # Try to receive data
            try:
                data, addr = test_sock.recvfrom(1024)
                message = data.decode('utf-8')
                if message.startswith('XGPS'):
                    test_sock.close()
                    return "127.0.0.1"
            except socket.timeout:
                pass
            test_sock.close()
        except Exception:
            pass
        return None

    parts = local_ip.split(".")
    if len(parts) != 4:
        return None

    base_prefix = ".".join(parts[:3])
    
    # Create a UDP socket to listen for simulator data
    listen_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    listen_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    if hasattr(socket, 'SO_REUSEPORT'):
        listen_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
    listen_sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
    listen_sock.settimeout(0.5)
    
    try:
        listen_sock.bind(('', udp_port))
    except Exception:
        listen_sock.close()
        return None
    
    # Listen for UDP messages for a short duration
    start_time = time.time()
    found_ip = None
    
    while time.time() - start_time < scan_duration:
        try:
            data, addr = listen_sock.recvfrom(1024)
            message = data.decode('utf-8')
            # Check if this is a valid GPS message
            if message.startswith('XGPS'):
                found_ip = addr[0]
                break
        except socket.timeout:
            continue
        except Exception:
            continue
    
    listen_sock.close()
    return found_ip


class DataSourceType(Enum):
    """Type of data source."""
    UDP = "udp"
    FSWIDGET = "fswidget"


@dataclass
class GPSData:
    """
    GPS data received from simulator.
    
    Units:
    - altitude: feet (converted from meters for UDP)
    - track: degrees (true north)
    - magnetic_track: degrees (magnetic north, 0.0 if not available)
    - indicated_air_speed: knots (0.0 if not available)
    - vertical_speed: feet per minute (0.0 if not available)
    - ground_speed: knots (normalized - UDP converted from m/s, FSWidget already in knots)
    """
    latitude: float
    longitude: float
    altitude: float
    track: float
    magnetic_track: float
    indicated_air_speed: float
    vertical_speed: float
    ground_speed: float  # Always in knots (normalized from both sources)
    raw_message: str
    source_type: DataSourceType


class SimulatorDataSource:
    """
    Unified interface for receiving GPS data from flight simulators.
    Supports both UDP (Aerofly) and TCP (FSWidget) protocols.
    """
    
    def __init__(self, 
                 source_type: DataSourceType = DataSourceType.UDP,
                 udp_port: int = DEFAULT_AEROFLY_PORT,
                 fswidget_ip: str = "localhost",
                 fswidget_port: int = FSWIDGET_PORT,
                 auto_detect: bool = False):
        """
        Initialize the simulator data source.
        
        Args:
            source_type: Type of data source (UDP or FSWidget)
            udp_port: UDP port for Aerofly (default: 49002)
            fswidget_ip: IP address for FSWidget TCP (default: localhost)
            fswidget_port: Port for FSWidget TCP (default: 58585)
            auto_detect: If True, automatically detect the simulator IP address (default: False)
        """
        self.source_type = source_type
        self.udp_port = udp_port
        self.fswidget_ip = fswidget_ip
        self.fswidget_port = fswidget_port
        self.auto_detect = auto_detect
        self.detected_ip: Optional[str] = None  # Store detected IP for reference
        
        # State
        self.running = False
        self.latest_gps: Optional[GPSData] = None
        self.last_gps_time: float = 0.0
        
        # Sockets
        self.udp_socket: Optional[socket.socket] = None
        self.fswidget_socket: Optional[socket.socket] = None
        
        # Threads
        self.receive_thread: Optional[threading.Thread] = None
        
        # Callback for when new GPS data is received
        self.on_gps_update: Optional[Callable[[GPSData], None]] = None
        
        # Statistics
        self.messages_received = 0
        self.start_time = time.time()
    
    def parse_udp_gps_data(self, message: str) -> Optional[GPSData]:
        """
        Parse GPS data from Aerofly UDP message.
        
        ForeFlight UDP protocol format:
        XGPS<simulator_name>,<longitude>,<latitude>,<altitude_msl>,<track_true_north>,<groundspeed_m/s>
        """
        # Match XGPS followed by optional simulator name and data
        pattern = r'XGPS(?:[^,]+)?,([-\d.]+),([-\d.]+),([-\d.]+),([-\d.]+),([-\d.]+)'
        match = re.match(pattern, message)
        if not match:
            return None
        # ForeFlight/Aerofly format: XGPS...,longitude,latitude,altitude,track,ground_speed
        # UDP data is missing the magnetic_track, indicated_air_speed, and vertical_speed
        # so we need to set them to 0.0
        # because FSWidget provides them - but UDP does not so we nee to maintain consistency between the data sources.
        magnetic_track = 0.0
        indicated_air_speed = 0.0
        vertical_speed = 0.0
        longitude, latitude, altitude, track, ground_speed_mps = map(float, match.groups())
        altitude = altitude * 3.28084 # convert altitude to feet to match FSWidget and having aircraft status in feet (as we cannot distinguish the unit from the data source)
        # Convert ground speed from m/s to knots (UDP provides in m/s, FSWidget provides in knots)
        # Normalize to knots for consistency: 1 m/s ≈ 1.944 knots
        ground_speed = ground_speed_mps * 1.944
        
        # Check for "menu state" condition (ignore)
        if (latitude == 0.0 and longitude == 0.0 and 
            altitude == 0.0 and track == 90.0 and ground_speed_mps == 0.0):
            return None
        
        return GPSData(
            latitude=latitude,
            longitude=longitude,
            altitude=altitude,
            track=track,
            magnetic_track=magnetic_track,
            indicated_air_speed=indicated_air_speed,
            vertical_speed=vertical_speed,
            ground_speed=ground_speed,
            raw_message=message.strip(),
            source_type=DataSourceType.UDP
        )
    
    def parse_fswidget_data(self, message: str) -> Optional[GPSData]:
        """
        Parse GPS data from FSWidget TCP protocol.
        
        FSWidget protocol format:
        lat|lon|alt|GPS track|magnetic_track|Indicated Air Speed|Ground Speed|Vertical Speed\n
        Parts: ['41.787090', '12.147029', '6399', '321', '320', '250', '271', '3624']
        """
        # Remove any leading/trailing whitespace and newlines
        message = message.strip()
        
        # Handle case where message might have newlines in the middle
        message = message.replace('\n', '').replace('\r', '')
        
        # Split by pipe delimiter
        parts = message.split('|')
        if len(parts) < 8:
            return None
        
        try:
            # Clean each part - strip whitespace
            latitude_str = parts[0].strip()
            longitude_str = parts[1].strip()
            altitude_str = parts[2].strip()
            track_str = parts[3].strip()
            magnetic_track_str = parts[4].strip()
            indicated_air_speed_str = parts[5].strip()
            ground_speed_str = parts[6].strip()
            vertical_speed_str = parts[7].strip()
            latitude = float(latitude_str)
            longitude = float(longitude_str)
            altitude = float(altitude_str)
            track = float(track_str)
            magnetic_track = float(magnetic_track_str)
            indicated_air_speed = float(indicated_air_speed_str)
            # FSWidget provides ground speed in parts[6] in knots (already in correct units)
            # No conversion needed - GPSData stores ground_speed in knots for consistency
            ground_speed = float(ground_speed_str)
            vertical_speed = float(vertical_speed_str)
            # Check for invalid data
            if (latitude == 0.0 and longitude == 0.0 and 
                altitude == 0.0 and track == 0.0):
                return None
            
            return GPSData(
                latitude=latitude,
                longitude=longitude,
                altitude=altitude,
                track=track,
                magnetic_track=magnetic_track,
                indicated_air_speed=indicated_air_speed,
                vertical_speed=vertical_speed,
                ground_speed=ground_speed,
                raw_message=message,
                source_type=DataSourceType.FSWIDGET
            )
        except (ValueError, IndexError):
            return None
    
    def _receive_loop_udp(self) -> None:
        """Receive GPS data from Aerofly via UDP."""
        while self.running:
            try:
                if not self.udp_socket:
                    time.sleep(0.1)
                    continue
                
                data, _ = self.udp_socket.recvfrom(1024)
                message = data.decode('utf-8')
                
                # Only process GPS messages
                if not message.startswith('XGPS'):
                    continue
                
                gps = self.parse_udp_gps_data(message)
                if gps:
                    self.latest_gps = gps
                    self.last_gps_time = time.time()
                    self.messages_received += 1
                    
                    # Call callback if set
                    if self.on_gps_update:
                        try:
                            self.on_gps_update(gps)
                        except Exception as e:
                            print(f"Error in GPS update callback: {e}")
                    
            except socket.timeout:
                continue
            except Exception as e:
                if self.running:
                    print(f"Error receiving UDP data: {e}")
    
    def _receive_loop_tcp_fswidget(self) -> None:
        """Receive GPS data from FSWidget via TCP."""
        reconnect_delay = 1.0  # Start with 1 second delay
        
        while self.running:
            try:
                # Connect to FSWidget TCP server
                # Use getaddrinfo to automatically handle both IPv4 and IPv6 addresses
                try:
                    # Get address info - this handles both IPv4 and IPv6
                    addr_info = socket.getaddrinfo(
                        self.fswidget_ip,
                        self.fswidget_port,
                        socket.AF_UNSPEC,  # Try both IPv4 and IPv6
                        socket.SOCK_STREAM
                    )
                    # Use the first result (prefer IPv6 if available, fallback to IPv4)
                    family, socktype, proto, canonname, sockaddr = addr_info[0]
                    
                    # Create socket with the appropriate family (AF_INET or AF_INET6)
                    self.fswidget_socket = socket.socket(family, socktype, proto)
                    self.fswidget_socket.settimeout(5.0)  # Connection timeout
                    self.fswidget_socket.connect(sockaddr)
                except socket.gaierror as e:
                    # DNS resolution failed
                    raise socket.error(f"Failed to resolve address {self.fswidget_ip}: {e}")
                
                self.fswidget_socket.settimeout(1.0)  # Read timeout
                
                # Reset reconnect delay on successful connection
                reconnect_delay = 1.0
                
                # Main receive loop
                while self.running:
                    try:
                        # Send request
                        self.fswidget_socket.sendall(FSWIDGET_REQUEST.encode('utf-8'))
                        
                        # Receive response - read until we get a newline (complete message)
                        data = b''
                        while True:
                            chunk = self.fswidget_socket.recv(1)
                            if not chunk:
                                raise socket.error("Connection closed by server")
                            data += chunk
                            # Stop when we receive newline (end of message)
                            if chunk == b'\n':
                                break
                            # Safety: if we get way too much data, something is wrong
                            if len(data) > 100:
                                break
                        
                        message = data.decode('utf-8').strip()
                        
                        # Parse FSWidget data
                        gps = self.parse_fswidget_data(message)
                        if gps:
                            self.latest_gps = gps
                            self.last_gps_time = time.time()
                            self.messages_received += 1
                            
                            # Call callback if set
                            if self.on_gps_update:
                                try:
                                    self.on_gps_update(gps)
                                except Exception as e:
                                    print(f"Error in GPS update callback: {e}")
                        
                        # Small delay before next request
                        time.sleep(0.1)
                        
                    except socket.timeout:
                        # Timeout is normal, just retry
                        continue
                    except socket.error as e:
                        # Connection error, break to reconnect
                        if self.running:
                            print(f"FSWidget connection error: {e}")
                        break
                    except Exception as e:
                        if self.running:
                            print(f"Error processing FSWidget data: {e}")
                        break
                
                # Close socket before reconnecting
                if self.fswidget_socket:
                    try:
                        self.fswidget_socket.close()
                    except:
                        pass
                    self.fswidget_socket = None
                    
            except socket.error as e:
                if self.running:
                    print(f"FSWidget connection failed: {e}, retrying in {reconnect_delay:.1f}s...")
                time.sleep(reconnect_delay)
                # Exponential backoff, max 10 seconds
                reconnect_delay = min(reconnect_delay * 2, 10.0)
            except Exception as e:
                if self.running:
                    print(f"Unexpected error in FSWidget receive loop: {e}")
                time.sleep(reconnect_delay)
                reconnect_delay = min(reconnect_delay * 2, 10.0)
    
    def _receive_loop(self) -> None:
        """Main receive loop - dispatches to UDP or TCP based on source type."""
        if self.source_type == DataSourceType.FSWIDGET:
            self._receive_loop_tcp_fswidget()
        else:
            self._receive_loop_udp()
    
    def start(self) -> None:
        """Start receiving data from the simulator."""
        if self.running:
            return
        
        # Auto-detect simulator IP if requested
        if self.auto_detect:
            if self.source_type == DataSourceType.FSWIDGET:
                detected = discover_fswidget_ip()
                if detected:
                    self.fswidget_ip = detected
                    self.detected_ip = detected
                    print(f"Auto-detected FSWidget at: {detected}")
                else:
                    print("Warning: Could not auto-detect FSWidget, using default: localhost")
            else:
                # For UDP, detect which IP is sending data
                detected = discover_udp_ip(self.udp_port)
                if detected:
                    self.detected_ip = detected
                    print(f"Auto-detected UDP simulator at: {detected}")
                else:
                    print("Warning: Could not auto-detect UDP simulator, will listen on all interfaces")
        
        # Create socket based on source type
        if self.source_type == DataSourceType.FSWIDGET:
            # TCP socket will be created in receive loop
            pass
        else:
            # Create UDP socket for receiving from Aerofly
            self.udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            self.udp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            # Enable SO_REUSEPORT to allow multiple programs to bind to the same port
            if hasattr(socket, 'SO_REUSEPORT'):
                self.udp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
            self.udp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
            self.udp_socket.settimeout(1.0)
            self.udp_socket.bind(('', self.udp_port))
        
        # Start receive thread
        self.running = True
        self.receive_thread = threading.Thread(target=self._receive_loop, daemon=True)
        self.receive_thread.start()
    
    def stop(self) -> None:
        """Stop receiving data from the simulator."""
        self.running = False
        
        if self.receive_thread:
            self.receive_thread.join(timeout=2)
        
        if self.udp_socket:
            try:
                self.udp_socket.close()
            except:
                pass
            self.udp_socket = None
        
        if self.fswidget_socket:
            try:
                self.fswidget_socket.close()
            except:
                pass
            self.fswidget_socket = None
    
    def get_latest_gps(self) -> Optional[GPSData]:
        """Get the latest GPS data received."""
        return self.latest_gps
    
    def is_receiving_data(self, timeout_seconds: float = 5.0) -> bool:
        """
        Check if data is being received (not stale).
        
        Args:
            timeout_seconds: Maximum age of data to consider as "receiving" (default: 5.0)
        
        Returns:
            True if data was received within the timeout period, False otherwise
        """
        if not self.latest_gps:
            return False
        
        age = time.time() - self.last_gps_time
        return age < timeout_seconds

