from enum import Enum
from .transport import Transport, TcpTransport


class GqrxClientError:
    """
    An error generated by the Gqrx Client.
    """
    pass


class CommandFailedGqrxClientError(GqrxClientError):
    """
    This error is raised when the Gqrx Client responds with a negative report command response (RPRT 1).
    This typically indicates that Gqrx does not understand or is unable to comply with the request that was issued.
    """
    pass


class DemodulatorMode(Enum):
    """
    The list of known and available Gqrx demodulator modes.
    """

    OFF = 'OFF'
    """ Turn off all signal processing (gqrx can still be used to view the real time spectrum) """

    RAW = 'RAW'
    """ Pass raw I/Q data through without any demodulation """

    AM = 'AM'
    """ Amplitude Modulation, used by broadcasting stations """

    AMS = 'AMS'
    """ Probably: Amplitude Modulation (Sync) """

    LSB = 'LSB'
    """ Lower Side Band (single side amplitude modulation) """

    USB = 'USB'
    """ Upper Side Band (single side amplitude modulation) """

    CWL = 'CWL'
    """ Lower Side Band Morse code on-off modulation. Same as CWR. """

    CWR = 'CWR'
    """ Same as CWL. """

    CWU = 'CWU'
    """ Upper Side Band Morse code on-off modulation. Same as CW. """

    CW = 'CW'
    """ Same as CWU. """

    FM = 'FM'
    """ Frequency Modulation """

    WFM = 'WFM'
    """ Wide Frequency Modulation, used by broadcasting stations """

    WFM_ST = 'WFM_ST'
    """ Stereo Wide Frequency Modulation, used by broadcasting stations """

    WFM_ST_OIRT = 'WFM_ST_OIRT'
    """ OIRT Stereo Wide Frequency Modulation, used by broadcasting stations belonging to the "Organisation Internationale de Radiodiffusion et de Télévision" (International Radio and Television Organisation) """


class GqrxClient:
    """
    A Gqrx Client.
    Use an instance of this class to interface with a remote Gqrx application.

    Example::

        client = GqrxClient()
        client.open()
        client.frequency = 137912500
        client.demodulator = (DemodulatorMode.WFM, 38000)
        client.dsp = True
        client.recording = True
    """

    def __init__(self):
        """
        Create a new Gqrx client instance.
        """
        self.transport: Transport = None

    def open(self, addr: tuple[str, int] = ('127.0.0.1', 7356)):
        """
        Establish a connection to a Gqrx server instance.

        Args:
            addr (tuple[str, int]): A tuple specifying the hostname and port to connect to. Defaults to ('127.0.0.1', 7356).
        """
        transport = TcpTransport()
        transport.open(addr)
        self.set_transport(transport)
    
    def close(self):
        """
        Close the current connection.
        This method closes the underlying transport and disassociates it from this Gqrx client instance.
        """
        self.transport.close()
        self.transport = None
    
    def set_transport(self, transport: Transport | None):
        """
        Use the given transport for this Gqrx client instance.
        
        If, for whatever reason it might be necessary, you can implement a custom transport and
        then use this method to associate the transport with this client.

        As the transport class does not define a default open method (because it cannot possibly know
        what steps or parameters may or may not be required for different transport types), this method
        is unable to open the underlying transport, so you must do that separately (e.g., before you
        call this method).

        Args:
            transport (Transport | None): The transport to use.
        """
        self.transport = transport

    def _do(self, command: str) -> str:
        """
        Perform a simple transactional command which anticipates a single line response.

        Args:
            command (str): The command to transmit.

        Returns:
            str: The response to the command that was issued.
        """
        self.transport.send(bytes(command, 'ascii'))
        return self.transport.recv().decode('ascii')

    def _check_ok(self, response):
        if response != 'RPRT 0':
            raise CommandFailedGqrxClientError()

    @property
    def frequency(self) -> int:
        """
        Get or set the frequency that Gqrx is currently tuned to.
        """
        response = self._do('f')
        return int(response)

    @frequency.setter
    def frequency(self, value: int):
        response = self._do(f'F {value}')
        self._check_ok(response)
    
    @property
    def demodulator(self) -> tuple[DemodulatorMode, int]:
        """
        Get or set the demodulator configuration.
        When retrieved, this property returns a tuple containing the demodulator mode and the passband width.
        When setting this value, you can either supply a (`DemodulatorMode`, `Passband`) tuple, or you can simply
        pass a `DemodulatorMode` by itself in which case a default corresponding passband will be set for you by Gqrx.

        Returns:
            (DemodulatorMode, int): A tuple containing the current demodulator mode and passband width.
        
        Example::

            # Set the mode and use the default passband for that mode
            client.demodulator = DemodulatorMode.AM
            
            # Set the demodulator mode and passband
            client.demodulator = (DemodulatorMode.WFM, 38000)

            # Get and display the 
            (mode, passband) = client.demodulator
            print(f"Mode = {mode}, Passband = {passband}")
        """
        self.transport.send(bytes('m', 'ascii'))
        mode = self.transport.recv().decode('ascii')
        passband = self.transport.recv().decode('ascii')
        return (DemodulatorMode[mode], int(passband))

    @demodulator.setter
    def demodulator(self, value: DemodulatorMode | str | tuple[DemodulatorMode | str, int | None]):

        # If the value is not a tuple, make it one
        if not isinstance(value, tuple):
            value = (value, None)

        # Extract the mode and passband
        (mode, passband) = value

        # If the mode is an enum instance, get the value
        if isinstance(mode, DemodulatorMode):
            mode = mode.value

        # If no passband is set, only send the mode
        if passband is None:
            response = self._do(f'M {mode}')
        
        # Otherwise, send the mode and passband
        else:
            response = self._do(f'M {mode} {passband}')
        
        # Make sure things don't break
        self._check_ok(response)

    @property
    def dsp(self) -> bool:
        """
        Get or set whether digital signal processing is active (the "play button").

        Returns:
            bool: True if Gqrx DSP is currently active, False otherwise.
        """
        response = self._do('u DSP')
        return bool(int(response))
    
    @dsp.setter
    def dsp(self, value: bool):
        response = self._do(f'U DSP {int(value)}')
        self._check_ok(response)

    @property
    def recording(self) -> bool:
        """
        Get or set whether recording is active.
        Note that in order for recording to actually happen, digital signal processing
        must also be enabled. This is a Gqrx thing.

        Returns:
            bool: True if Gqrx is currently recording, False otherwise.
        """
        response = self._do('u RECORD')
        return bool(int(response))

    @recording.setter
    def recording(self, value: bool):
        response = self._do(f'U RECORD {int(value)}')
        self._check_ok(response)
    
    def aos(self):
        """
        Send an 'acquisition of signal' event to Gqrx. The current effect of this
        event in Gqrx is to begin recording.

        Raises:
            CommandFailedGqrxClientError: The Gqrx remote responded with a failure report.
        """
        response = self._do(f'AOS')
        self._check_ok(response)
    
    def los(self):
        """
        Send a 'loss of signal' event to Gqrx. The current effect of this
        event in Gqrx is to finish recording.

        Raises:
            CommandFailedGqrxClientError: The Gqrx remote responded with a failure report.
        """
        response = self._do(f'LOS')
        self._check_ok(response)
    
