"""GMP252 CO2 sensor implementation."""

from multipledispatch import dispatch
from minimalmodbus import Instrument, BYTEORDER_LITTLE_SWAP

from atmospyre.sensors import Sensor
from atmospyre.sensors.read_tag import ReadTag
from atmospyre.sensors.read_tag import ReadTagMetadata


# ============================================================================
# GMP252-specific tags (empty structs)
# ============================================================================


class CO2Tag(ReadTag):
    """Tag for CO2 concentration measurement as floating point.

    This tag class represents CO2 concentration measurements from the
    GMP252 sensor in floating point format. It is used with the dispatch
    mechanism to route read operations to the appropriate register reading
    function.

    See Also
    --------
    ReadTag : Base tag class
    CO2 : Singleton instance of this tag class
    CO2IntTag : Integer variant of CO2 measurement
    """
    pass


class CO2IntTag(ReadTag):
    """Tag for CO2 concentration measurement as integer.

    This tag class represents CO2 concentration measurements from the
    GMP252 sensor in integer format. It is used with the dispatch
    mechanism to route read operations to the appropriate register reading
    function.

    See Also
    --------
    ReadTag : Base tag class
    CO2_INT : Singleton instance of this tag class
    CO2Tag : Floating point variant of CO2 measurement
    """
    pass


class TemperatureTag(ReadTag):
    """Tag for temperature measurement.

    This tag class represents temperature measurements from the GMP252
    sensor. The temperature reading reflects the measurement chamber
    temperature and can be used for temperature compensation or
    environmental monitoring.

    See Also
    --------
    ReadTag : Base tag class
    TEMPERATURE : Singleton instance of this tag class
    """
    pass


class StatusTag(ReadTag):
    """Tag for general device status.

    This tag class represents the general device status from the GMP252
    sensor. It provides information about the overall operational state
    of the device.

    See Also
    --------
    ReadTag : Base tag class
    STATUS : Singleton instance of this tag class
    CO2StatusTag : CO2-specific status information
    """
    pass


class CO2StatusTag(ReadTag):
    """Tag for CO2 measurement status.

    This tag class represents CO2-specific status information from the
    GMP252 sensor. It provides details about the CO2 measurement process
    and any measurement-specific conditions.

    See Also
    --------
    ReadTag : Base tag class
    CO2_STATUS : Singleton instance of this tag class
    StatusTag : General device status
    """
    pass


# ============================================================================
# Tag instances (singleton instances to use)
# ============================================================================

CO2 = CO2Tag(ReadTagMetadata(
    unit="ppm",
    description="CO2 concentration (float)",
    precision=1,
    source="GMP252 User Manual v1.2, Register 0 (Float, BYTEORDER_LITTLE_SWAP)"
))
"""CO2 concentration measurement tag (floating point).

This tag instance provides access to CO2 concentration readings from
the GMP252 sensor as floating point values, offering the highest
precision available from the device.

Attributes
----------
unit : str
    "ppm" (parts per million)
description : str
    "CO2 concentration (float)"
precision : int
    1 (one decimal place)
source : str
    Modbus register 0 (32-bit Float, BYTEORDER_LITTLE_SWAP)

Examples
--------
Read CO2 concentration as float:

>>> from atmospyre.sensors.gmp252 import GMP252, CO2
>>> sensor = GMP252(port='COM3', slave_address=1)
>>> result = sensor.read([CO2])
>>> print(result[CO2])
415.2

Read multiple measurements:

>>> from atmospyre.sensors.gmp252 import GMP252, CO2, TEMPERATURE
>>> sensor = GMP252(port='COM3')
>>> result = sensor.read([CO2, TEMPERATURE])
>>> print(f"CO2: {result[CO2]} ppm")
CO2: 415.2 ppm
>>> print(f"Temp: {result[TEMPERATURE]} °C")
Temp: 23.45 °C

Notes
-----
The floating point format provides the highest precision for CO2
measurements. For applications where integer values are sufficient,
consider using CO2_INT instead.

See Also
--------
CO2_INT : Integer variant of CO2 measurement
CO2Tag : Tag class for this measurement
"""

CO2_INT = CO2IntTag(ReadTagMetadata(
    unit="ppm",
    description="CO2 concentration (integer)",
    precision=0,
    source="GMP252 User Manual v1.2, Register 256 (Integer)"
))
"""CO2 concentration measurement tag (integer).

This tag instance provides access to CO2 concentration readings from
the GMP252 sensor as integer values. This format is useful when
fractional ppm values are not required.

Attributes
----------
unit : str
    "ppm" (parts per million)
description : str
    "CO2 concentration (integer)"
precision : int
    0 (integer values only)
source : str
    Modbus register 256 (16-bit Integer)

Examples
--------
Read CO2 concentration as integer:

>>> from atmospyre.sensors.gmp252 import GMP252, CO2_INT
>>> sensor = GMP252(port='COM3', slave_address=1)
>>> result = sensor.read([CO2_INT])
>>> print(result[CO2_INT])
415

Notes
-----
The integer format provides a simpler representation when fractional
ppm values are not needed. The floating point variant (CO2) offers
higher precision.

See Also
--------
CO2 : Floating point variant of CO2 measurement
CO2IntTag : Tag class for this measurement
"""

TEMPERATURE = TemperatureTag(ReadTagMetadata(
    unit="°C",
    description="Measurement temperature",
    precision=2,
    source="GMP252 User Manual v1.2, Register 4 (Float, BYTEORDER_LITTLE_SWAP)"
))
"""Temperature measurement tag.

This tag instance provides access to temperature readings from the
GMP252 sensor's measurement chamber. The temperature can be used
for temperature compensation or environmental monitoring.

Attributes
----------
unit : str
    "°C" (degrees Celsius)
description : str
    "Measurement temperature"
precision : int
    2 (two decimal places)
source : str
    Modbus register 4 (32-bit Float, BYTEORDER_LITTLE_SWAP)

Examples
--------
Read temperature:

>>> from atmospyre.sensors.gmp252 import GMP252, TEMPERATURE
>>> sensor = GMP252(port='COM3', slave_address=1)
>>> result = sensor.read([TEMPERATURE])
>>> print(result[TEMPERATURE])
23.45

Combined with CO2 measurement:

>>> from atmospyre.sensors.gmp252 import GMP252, CO2, TEMPERATURE
>>> sensor = GMP252(port='COM3')
>>> result = sensor.read([CO2, TEMPERATURE])
>>> print(f"CO2: {result[CO2]} ppm at {result[TEMPERATURE]} °C")
CO2: 415.2 ppm at 23.45 °C

Notes
-----
The temperature measurement reflects the measurement chamber temperature,
which is important for accurate CO2 readings as CO2 measurements can be
temperature-dependent.

See Also
--------
CO2 : CO2 concentration measurement
TemperatureTag : Tag class for this measurement
"""

STATUS = StatusTag(ReadTagMetadata(
    unit=None,
    description="General device status",
    precision=None,
    source="GMP252 User Manual v1.2, Register 2048 (Integer)"
))
"""General device status tag.

This tag instance provides access to the general operational status
of the GMP252 sensor. The status value indicates the overall health
and operational state of the device.

Attributes
----------
unit : None
    No unit (status code)
description : str
    "General device status"
precision : None
    Not applicable (status code)
source : str
    Modbus register 2048 (16-bit Integer)

Examples
--------
Read device status:

>>> from atmospyre.sensors.gmp252 import GMP252, STATUS
>>> sensor = GMP252(port='COM3', slave_address=1)
>>> result = sensor.read([STATUS])
>>> print(result[STATUS])
0

Check status before reading measurements:

>>> from atmospyre.sensors.gmp252 import GMP252, STATUS, CO2
>>> sensor = GMP252(port='COM3')
>>> result = sensor.read([STATUS, CO2])
>>> if result[STATUS] == 0:
...     print(f"Device OK. CO2: {result[CO2]} ppm")
... else:
...     print(f"Device status: {result[STATUS]}")
Device OK. CO2: 415.2 ppm

Notes
-----
Consult the GMP252 User Manual for interpretation of specific status
codes. A status value of 0 typically indicates normal operation.

See Also
--------
CO2_STATUS : CO2-specific status information
StatusTag : Tag class for this measurement
"""

CO2_STATUS = CO2StatusTag(ReadTagMetadata(
    unit=None,
    description="CO2 measurement status",
    precision=None,
    source="GMP252 User Manual v1.2, Register 2049 (Integer)"
))
"""CO2 measurement status tag.

This tag instance provides access to CO2-specific status information
from the GMP252 sensor. This status indicates the quality and
reliability of the CO2 measurement.

Attributes
----------
unit : None
    No unit (status code)
description : str
    "CO2 measurement status"
precision : None
    Not applicable (status code)
source : str
    Modbus register 2049 (16-bit Integer)

Examples
--------
Read CO2 status:

>>> from atmospyre.sensors.gmp252 import GMP252, CO2_STATUS
>>> sensor = GMP252(port='COM3', slave_address=1)
>>> result = sensor.read([CO2_STATUS])
>>> print(result[CO2_STATUS])
0

Validate CO2 measurement quality:

>>> from atmospyre.sensors.gmp252 import GMP252, CO2_STATUS, CO2
>>> sensor = GMP252(port='COM3')
>>> result = sensor.read([CO2_STATUS, CO2])
>>> if result[CO2_STATUS] == 0:
...     print(f"Valid CO2 reading: {result[CO2]} ppm")
... else:
...     print(f"CO2 status warning: {result[CO2_STATUS]}")
Valid CO2 reading: 415.2 ppm

Notes
-----
Consult the GMP252 User Manual for interpretation of specific CO2
status codes. This status provides more detailed information about
the CO2 measurement than the general device status.

See Also
--------
STATUS : General device status
CO2StatusTag : Tag class for this measurement
CO2 : CO2 concentration measurement
"""

# ============================================================================
# Dispatch namespace for GMP252 read functions
# ============================================================================


gmp252_namespace = {}


# ============================================================================
# GMP252 Read function implementations using dispatch decorator
# ============================================================================

@dispatch(object, CO2Tag, namespace=gmp252_namespace)
def _read(instrument: Instrument, tag: CO2Tag) -> float:
    """Read CO2 concentration as float from register 0.

    This function reads the CO2 concentration from Modbus register 0
    using a 32-bit floating point format with little-endian byte swap
    ordering.

    Parameters
    ----------
    instrument : Instrument
        minimalmodbus Instrument instance for Modbus communication
    tag : CO2Tag
        Tag instance identifying this measurement type

    Returns
    -------
    float
        CO2 concentration in ppm (parts per million)

    Notes
    -----
    Uses BYTEORDER_LITTLE_SWAP for Modbus register reading as specified
    in the GMP252 communication protocol. This provides the highest
    precision CO2 measurement available from the device.

    See Also
    --------
    CO2 : Tag instance for this measurement
    CO2Tag : Tag class definition
    """
    return instrument.read_float(0, byteorder=BYTEORDER_LITTLE_SWAP)


@dispatch(object, CO2IntTag, namespace=gmp252_namespace)
def _read(instrument: Instrument, tag: CO2IntTag) -> int:
    """Read CO2 concentration as integer from register 256.

    This function reads the CO2 concentration from Modbus register 256
    using a 16-bit integer format.

    Parameters
    ----------
    instrument : Instrument
        minimalmodbus Instrument instance for Modbus communication
    tag : CO2IntTag
        Tag instance identifying this measurement type

    Returns
    -------
    int
        CO2 concentration in ppm (parts per million)

    Notes
    -----
    The integer format provides a simpler representation when fractional
    ppm values are not required. For highest precision, use the floating
    point variant from register 0.

    See Also
    --------
    CO2_INT : Tag instance for this measurement
    CO2IntTag : Tag class definition
    """
    return instrument.read_register(256)


@dispatch(object, TemperatureTag, namespace=gmp252_namespace)
def _read(instrument: Instrument, tag: TemperatureTag) -> float:
    """Read temperature from register 4.

    This function reads the measurement chamber temperature from Modbus
    register 4 using a 32-bit floating point format with little-endian
    byte swap ordering.

    Parameters
    ----------
    instrument : Instrument
        minimalmodbus Instrument instance for Modbus communication
    tag : TemperatureTag
        Tag instance identifying this measurement type

    Returns
    -------
    float
        Measurement temperature in °C (degrees Celsius)

    Notes
    -----
    Uses BYTEORDER_LITTLE_SWAP for Modbus register reading as specified
    in the GMP252 communication protocol. The temperature reflects the
    measurement chamber temperature, which is important for accurate
    CO2 readings.

    See Also
    --------
    TEMPERATURE : Tag instance for this measurement
    TemperatureTag : Tag class definition
    """
    return instrument.read_float(4, byteorder=BYTEORDER_LITTLE_SWAP)


@dispatch(object, StatusTag, namespace=gmp252_namespace)
def _read(instrument: Instrument, tag: StatusTag) -> int:
    """Read general device status from register 2048.

    This function reads the general operational status of the device
    from Modbus register 2048 using a 16-bit integer format.

    Parameters
    ----------
    instrument : Instrument
        minimalmodbus Instrument instance for Modbus communication
    tag : StatusTag
        Tag instance identifying this measurement type

    Returns
    -------
    int
        General device status code

    Notes
    -----
    Consult the GMP252 User Manual for interpretation of specific status
    codes. A status value of 0 typically indicates normal operation.

    See Also
    --------
    STATUS : Tag instance for this measurement
    StatusTag : Tag class definition
    """
    return instrument.read_register(2048)


@dispatch(object, CO2StatusTag, namespace=gmp252_namespace)
def _read(instrument: Instrument, tag: CO2StatusTag) -> int:
    """Read CO2 measurement status from register 2049.

    This function reads CO2-specific status information from Modbus
    register 2049 using a 16-bit integer format.

    Parameters
    ----------
    instrument : Instrument
        minimalmodbus Instrument instance for Modbus communication
    tag : CO2StatusTag
        Tag instance identifying this measurement type

    Returns
    -------
    int
        CO2 measurement status code

    Notes
    -----
    This status provides detailed information about the CO2 measurement
    quality and reliability. Consult the GMP252 User Manual for
    interpretation of specific status codes.

    See Also
    --------
    CO2_STATUS : Tag instance for this measurement
    CO2StatusTag : Tag class definition
    """
    return instrument.read_register(2049)


# ============================================================================
# GMP252 Sensor Class
# ============================================================================


class GMP252(Sensor):
    """Vaisala GMP252 CO2 probe with Modbus RTU communication.

    The GMP252 is a professional CO2 sensor that communicates via Modbus
    RTU protocol. It measures CO2 concentration and temperature with high
    accuracy and stability, making it suitable for demanding applications
    in environmental monitoring, agriculture, and industrial processes.

    Parameters
    ----------
    port : str
        Serial port name (e.g., 'COM3' on Windows, '/dev/ttyUSB0' on Linux)
    slave_address : int, optional
        Modbus slave address, by default 1
    baudrate : int, optional
        Serial communication speed in baud, by default 19200
    stopbits : int, optional
        Number of stop bits, by default 2
    bytesize : int, optional
        Number of data bits per byte, by default 8
    parity : str, optional
        Parity checking method ('N' for None, 'E' for Even, 'O' for Odd),
        by default 'N'
    timeout : float, optional
        Read timeout in seconds, by default 0.5

    Attributes
    ----------
    port : str
        Serial port name
    slave_address : int
        Modbus slave address
    valid_tags : list[ReadTag]
        List of valid measurement tags for this sensor

    Raises
    ------
    SerialException
        If the serial port cannot be opened or configured
    ModbusException
        If Modbus communication fails

    Examples
    --------
    Basic initialization and single tag reading:

    Hint: use the module import if you plan to use multiple sensors.

    >>> from atmospyre.sensors.implementations.co2 import gmp252
    >>> sensor = gmp252.GMP252(port='COM3', slave_address=1)
    >>> result = sensor.read([gmp252.CO2])
    >>> print(result[gmp252.CO2])
    415.2

    Read multiple measurements:

    Hint: directly import if you plan to use only one sensors.

    >>> from atmospyre.sensors.implementations.co2.vaisala.gmp252 import GMP252, CO2, TEMPERATURE
    >>> sensor = GMP252(port='COM3')
    >>> result = sensor.read([CO2, TEMPERATURE])
    >>> print(f"CO2: {result[CO2]} ppm")
    CO2: 415.2 ppm
    >>> print(f"Temperature: {result[TEMPERATURE]} °C")
    Temperature: 23.45 °C

    Read all available measurements:

    >>> from atmospyre.sensors.implementations.co2.vaisala.gmp252 import (
    ...     GMP252, CO2, CO2_INT, TEMPERATURE, STATUS, CO2_STATUS
    ... )
    >>> sensor = GMP252(port='/dev/ttyUSB0')
    >>> result = sensor.read([CO2, TEMPERATURE, STATUS])
    >>> if result[STATUS] == 0:
    ...     print(f"CO2: {result[CO2]:.1f} ppm at {result[TEMPERATURE]:.2f} °C")
    CO2: 415.2 ppm at 23.45 °C

    Custom serial configuration:

    >>> sensor = GMP252(
    ...     port='COM3',
    ...     slave_address=2,
    ...     baudrate=9600,
    ...     timeout=1.0
    ... )

    Use with context manager:

    >>> with GMP252(port='COM3') as sensor:
    ...     result = sensor.read([CO2, TEMPERATURE])
    ...     print(f"CO2: {result[CO2]} ppm")
    CO2: 415.2 ppm
    """

    def __init__(
        self,
        port: str,
        slave_address: int = 1,
        baudrate: int = 19200,
        stopbits: int = 2,
        bytesize: int = 8,
        parity: str = 'N',
        timeout: float = 0.5
    ):
        """Initialize GMP252 sensor with serial communication parameters.

        Parameters
        ----------
        port : str
            Serial port name (e.g., 'COM3' on Windows, '/dev/ttyUSB0' on Linux)
        slave_address : int, optional
            Modbus slave address, by default 1
        baudrate : int, optional
            Serial communication speed in baud, by default 19200
        stopbits : int, optional
            Number of stop bits, by default 2
        bytesize : int, optional
            Number of data bits per byte, by default 8
        parity : str, optional
            Parity checking method ('N', 'E', 'O'), by default 'N'
        timeout : float, optional
            Read timeout in seconds, by default 0.5
        """
        super().__init__(
            port=port,
            valid_tags=[CO2, CO2_INT, TEMPERATURE, STATUS, CO2_STATUS],
            namespace=gmp252_namespace,
            slave_address=slave_address,
            baudrate=baudrate,
            stopbits=stopbits,
            bytesize=bytesize,
            parity=parity,
            timeout=timeout
        )