"""
bme280_sim.py

Classe de simulation du capteur BME280.

But :
- Fournir la même interface que la classe BME280 utilisée sur le Pico.
- Permettre de tester le code (altitude, enregistrement, radio, etc.)
  sur un PC sans capteur réel.

API mimée :
    __init__(...)
    read_raw_data(result)
    read_compensated_data(result=None)
    values   -> ("23.4C", "1012.8hPa", "45.0%")
    raw_values -> (23.4, 1012.8, 45.0)

La simulation génère un “profil de vol” simple :
- une altitude qui monte puis redescend,
- pression calculée à partir de l’altitude,
- température qui diminue légèrement avec l’altitude,
- humidité approximativement constante.
"""

from array import array
import math
import time

# Valeurs / constantes reprises pour compatibilité
BME280_OSAMPLE_1 = 1
BME280_OSAMPLE_2 = 2
BME280_OSAMPLE_4 = 3
BME280_OSAMPLE_8 = 4
BME280_OSAMPLE_16 = 5

BME280_I2CADDR = 0x76  # valeur classique, peu importe ici

# Les registres ne sont pas utilisés, mais gardés pour compatibilité
BME280_REGISTER_CONTROL = 0xF4
BME280_REGISTER_CONTROL_HUM = 0xF2


class BME280:
    """
    Classe de simulation : même nom et même interface que le driver réel.

    Important :
    - Le paramètre i2c est ignoré (mais accepté pour compatibilité).
    - Les méthodes renvoient des valeurs “raisonnables” mais simulées.
    """

    def __init__(self,
                 mode=BME280_OSAMPLE_1,
                 address=BME280_I2CADDR,
                 i2c=None,
                 **kwargs):
        # On garde la même vérification du mode pour rester compatible
        if mode not in [BME280_OSAMPLE_1, BME280_OSAMPLE_2,
                        BME280_OSAMPLE_4, BME280_OSAMPLE_8,
                        BME280_OSAMPLE_16]:
            raise ValueError(
                "Unexpected mode value {0}".format(mode)
            )

        self._mode = mode
        self.address = address

        # --- Paramètres de scénario de simulation ------------------------

        # Altitude max supposée pendant le “vol” (en m)
        self._altitude_max = kwargs.get("altitude_max", 1000.0)

        # Durée totale du cycle montée + descente (en secondes)
        self._flight_duration = kwargs.get("flight_duration", 60.0)

        # Pression au niveau de la mer (hPa)
        self._P0 = kwargs.get("sea_level_pressure", 1013.25)

        # Température au sol (°C)
        self._T0 = kwargs.get("ground_temperature", 15.0)

        # Gradient thermique approximatif (°C / m)
        self._temp_lapse_rate = kwargs.get("temp_lapse_rate", 0.0065)

        # Humidité relative constante (%)
        self._H0 = kwargs.get("humidity", 50.0)

        # Heure de départ (pour faire évoluer la simulation avec le temps réel)
        self._t0 = time.time()

        # tableau de travail pour rester compatible
        self._l3_resultarray = array("i", [0, 0, 0])

    # ------------------------------------------------------------------ #
    #   Partie “modèle physique” simple pour la simulation               #
    # ------------------------------------------------------------------ #

    def _current_altitude(self):
        """
        Calcule une altitude simulée en fonction du temps.

        On fait une montée puis une descente sur un cycle de duration
        '_flight_duration' secondes, sous forme de triangle.
        """
        elapsed = (time.time() - self._t0) % self._flight_duration
        half = self._flight_duration / 2.0

        if elapsed <= half:
            # montée
            alt = (elapsed / half) * self._altitude_max
        else:
            # descente
            alt = ((self._flight_duration - elapsed) / half) * self._altitude_max

        return alt

    def _simulated_env(self):
        """
        Retourne (température °C, pression hPa, humidité %) simulées.
        """
        alt = self._current_altitude()

        # Température qui diminue avec l'altitude
        temp_c = self._T0 - self._temp_lapse_rate * alt

        # Formule barométrique simplifiée
        # P = P0 * (1 - L*h/T0)^(g*M/(R*L))  ≈ P0 * (1 - 2.25577e-5 * h)^5.2559
        P = self._P0 * (1.0 - 2.25577e-5 * alt) ** 5.2559

        # Humidité approx. constante (on pourrait y ajouter un peu de bruit)
        H = self._H0

        return temp_c, P, H

    # ------------------------------------------------------------------ #
    #   Méthodes “compatibles driver réel”                               #
    # ------------------------------------------------------------------ #

    def read_raw_data(self, result):
        """
        Simule la lecture des données brutes depuis le capteur.

        Dans le driver réel, 'result' contient des valeurs brutes (ADC).
        Ici on va simplement générer des valeurs cohérentes,
        mais on ne cherche pas à reproduire exactement le format binaire.
        Cette méthode n'est généralement pas utilisée directement
        par les élèves, mais on la fournit pour compatibilité.
        """
        temp_c, P_hpa, H_pct = self._simulated_env()

        # On fabrique des “fausses” valeurs brutes en faisant l’inverse
        # des conversions de read_compensated_data.
        temp_raw = int(temp_c * 100)                 # centi-degrés
        pressure_pa = int(P_hpa * 100)               # hPa -> Pa
        press_raw = pressure_pa * 256                # Pa -> format interne *256
        hum_raw = int(H_pct * 1024)                  # % -> format interne *1024

        result[0] = temp_raw
        result[1] = press_raw
        result[2] = hum_raw

    def read_compensated_data(self, result=None):
        """
        Retourne (temp, pressure, humidity) dans le même format que le driver :

        - temp     : centi-degrés Celsius    (23.45°C -> 2345)
        - pressure : Pa * 256                (1013.25hPa -> 101325 Pa -> *256)
        - humidity : % * 1024                (45.0% -> 46080)

        Si 'result' est fourni (array de longueur 3), il est réutilisé.
        """
        temp_c, P_hpa, H_pct = self._simulated_env()

        temp_raw = int(round(temp_c * 100))            # centi-degrés
        pressure_pa = int(round(P_hpa * 100))          # hPa -> Pa
        press_raw = pressure_pa * 256                  # Pa -> interne
        hum_raw = int(round(H_pct * 1024))             # % -> interne

        if result is None:
            return array("i", (temp_raw, press_raw, hum_raw))

        result[0] = temp_raw
        result[1] = press_raw
        result[2] = hum_raw
        return result

    @property
    def values(self):
        """
        Version “humaine”, comme dans le driver réel :

        -> ("23.4C", "1012.8hPa", "45.0%")
        """
        t, p, h = self.read_compensated_data()

        # --- conversion identique au driver d'origine -------------------
        p = p // 256           # Pa
        pi = p // 100          # hPa entier
        pd = p - pi * 100      # 2 décimales

        hi = h // 1024         # % entier
        hd = h * 100 // 1024 - hi * 100  # 2 décimales
        # ----------------------------------------------------------------

        return ("{}C".format(t / 100),
                "{}.{:02d}hPa".format(pi, pd),
                "{}.{:02d}%".format(hi, hd))

    @property
    def raw_values(self):
        """
        Version “ordinateur” : valeurs numériques directement utilisables.

        -> (température °C, pression hPa, humidité %)
        """
        t, p, h = self.read_compensated_data()

        # même conversion que dans le driver, mais on retourne des floats
        p = p // 256           # Pa
        pi = p // 100          # hPa entier
        pd = p - pi * 100      # 2 décimales

        hi = h // 1024         # % entier
        hd = h * 100 // 1024 - hi * 100

        temp_c = t / 100.0
        press_hpa = pi + (pd / 100.0)
        hum_pct = hi + (hd / 100.0)

        return (temp_c, press_hpa, hum_pct)
