// force the compiler to show a warning to confirm that this file is included  WLEDMM: commented this warning as we want serious warnings ;-)
// #warning **** Included USERMOD_BME280 version 2.0 ****

#ifndef WLED_ENABLE_MQTT
#error "This user mod requires MQTT to be enabled."
#endif

#pragma once

#include "wled.h"
#include <Arduino.h>
#include <Wire.h>
#include <BME280I2C.h>               // BME280 sensor
#include <EnvironmentCalculations.h> // BME280 extended measurements

class UsermodBME280 : public Usermod
{
private:
  
  // NOTE: Do not implement any compile-time variables, anything the user needs to configure
  // should be configurable from the Usermod menu using the methods below
  // key settings set via usermod menu
  uint8_t  TemperatureDecimals = 0;  // Number of decimal places in published temperaure values
  uint8_t  HumidityDecimals = 0;    // Number of decimal places in published humidity values
  uint8_t  PressureDecimals = 0;    // Number of decimal places in published pressure values
  uint16_t TemperatureInterval = 5; // Interval to measure temperature (and humidity, dew point if available) in seconds
  uint16_t PressureInterval = 300;  // Interval to measure pressure in seconds
  bool PublishAlways = false;             // Publish values even when they have not changed
  bool UseCelsius = true;                 // Use Celsius for Reporting
  bool HomeAssistantDiscovery = false;    // Publish Home Assistant Device Information
  bool enabled = true;

  // set the default pins based on the architecture, these get overridden by Usermod menu settings
  #ifdef ESP8266
    //uint8_t RST_PIN = 16; // Un-comment for Heltec WiFi-Kit-8
  #endif
  int8_t ioPin[2] = {i2c_scl, i2c_sda};   // I2C pins: SCL, SDA...defaults to Arch hardware pins but overridden at setup()
  bool initDone = false;

  // BME280 sensor settings
  BME280I2C::Settings settings{
      BME280::OSR_X16, // Temperature oversampling x16
      BME280::OSR_X16, // Humidity oversampling x16
      BME280::OSR_X16, // Pressure oversampling x16
      // Defaults
      BME280::Mode_Forced,
      BME280::StandbyTime_1000ms,
      BME280::Filter_Off,
      BME280::SpiEnable_False,
      BME280I2C::I2CAddr_0x76 // I2C address. I2C specific. Default 0x76
  };

  BME280I2C bme{settings};

  uint8_t sensorType;

  // Measurement timers
  long timer;
  long lastTemperatureMeasure = 0;
  long lastPressureMeasure = 0;

  // Current sensor values
  float sensorTemperature;
  float sensorHumidity;
  float sensorHeatIndex;
  float sensorDewPoint;
  float sensorPressure;
  String tempScale;
  // Track previous sensor values
  float lastTemperature;
  float lastHumidity;
  float lastHeatIndex;
  float lastDewPoint;
  float lastPressure;

  // MQTT topic strings for publishing Home Assistant discovery topics
  bool mqttInitialized = false;

  // strings to reduce flash memory usage (used more than twice)
  static const char _name[];
  static const char _enabled[];

  // Read the BME280/BMP280 Sensor (which one runs depends on whether Celsius or Fahrenheit being set in Usermod Menu)
  void UpdateBME280Data(int SensorType)
  {
    float _temperature, _humidity, _pressure;
    if (!enabled || (sensorType == 0)) return;  // WLEDMM bugfix

    if (UseCelsius) {
      BME280::TempUnit tempUnit(BME280::TempUnit_Celsius);
      EnvironmentCalculations::TempUnit envTempUnit(EnvironmentCalculations::TempUnit_Celsius);
      BME280::PresUnit presUnit(BME280::PresUnit_hPa);

      bme.read(_pressure, _temperature, _humidity, tempUnit, presUnit);

      sensorTemperature = _temperature;
      sensorHumidity = _humidity;
      sensorPressure = _pressure;
      tempScale = F("°C");
      if (sensorType == 1)
      {
        sensorHeatIndex = EnvironmentCalculations::HeatIndex(_temperature, _humidity, envTempUnit);
        sensorDewPoint = EnvironmentCalculations::DewPoint(_temperature, _humidity, envTempUnit);
      }
    } else {
      BME280::TempUnit tempUnit(BME280::TempUnit_Fahrenheit);
      EnvironmentCalculations::TempUnit envTempUnit(EnvironmentCalculations::TempUnit_Fahrenheit);
      BME280::PresUnit presUnit(BME280::PresUnit_hPa);

      bme.read(_pressure, _temperature, _humidity, tempUnit, presUnit);

      sensorTemperature = _temperature;
      sensorHumidity = _humidity;
      sensorPressure = _pressure;
      tempScale = F("°F");
      if (sensorType == 1)
      {
        sensorHeatIndex = EnvironmentCalculations::HeatIndex(_temperature, _humidity, envTempUnit);
        sensorDewPoint = EnvironmentCalculations::DewPoint(_temperature, _humidity, envTempUnit);
      }
    }
  }

  // Procedure to define all MQTT discovery Topics 
  void _mqttInitialize()
  {
    char mqttTemperatureTopic[128];
    char mqttHumidityTopic[128];
    char mqttPressureTopic[128];
    char mqttHeatIndexTopic[128];
    char mqttDewPointTopic[128];
    snprintf_P(mqttTemperatureTopic, 127, PSTR("%s/temperature"), mqttDeviceTopic);
    snprintf_P(mqttPressureTopic, 127, PSTR("%s/pressure"), mqttDeviceTopic);
    snprintf_P(mqttHumidityTopic, 127, PSTR("%s/humidity"), mqttDeviceTopic);
    snprintf_P(mqttHeatIndexTopic, 127, PSTR("%s/heat_index"), mqttDeviceTopic);
    snprintf_P(mqttDewPointTopic, 127, PSTR("%s/dew_point"), mqttDeviceTopic);

    if (HomeAssistantDiscovery) {
      _createMqttSensor(F("Temperature"), mqttTemperatureTopic, "temperature", tempScale);
      _createMqttSensor(F("Pressure"), mqttPressureTopic, "pressure", F("hPa"));
      _createMqttSensor(F("Humidity"), mqttHumidityTopic, "humidity", F("%"));
      _createMqttSensor(F("HeatIndex"), mqttHeatIndexTopic, "temperature", tempScale);
      _createMqttSensor(F("DewPoint"), mqttDewPointTopic, "temperature", tempScale);
    }
  }

  // Create an MQTT Sensor for Home Assistant Discovery purposes, this includes a pointer to the topic that is published to in the Loop.
  void _createMqttSensor(const String &name, const String &topic, const String &deviceClass, const String &unitOfMeasurement)
  {
    String t = String(F("homeassistant/sensor/")) + mqttClientID + F("/") + name + F("/config");
    
    StaticJsonDocument<600> doc;
    
    doc[F("name")] = String(serverDescription) + " " + name;
    doc[F("state_topic")] = topic;
    doc[F("unique_id")] = String(mqttClientID) + name;
    if (unitOfMeasurement != "")
      doc[F("unit_of_measurement")] = unitOfMeasurement;
    if (deviceClass != "")
      doc[F("device_class")] = deviceClass;
    doc[F("expire_after")] = 1800;

    JsonObject device = doc.createNestedObject(F("device")); // attach the sensor to the same device
    device[F("name")] = serverDescription;
    device[F("identifiers")] = "wled-sensor-" + String(mqttClientID);
    device[F("manufacturer")] = F(WLED_BRAND); //WLEDMM + Moustachauve/Wled-Native
    device[F("model")] = F(WLED_PRODUCT_NAME); //WLEDMM + Moustachauve/Wled-Native
    device[F("sw_version")] = versionString;

    String temp;
    serializeJson(doc, temp);
    DEBUG_PRINTLN(t);
    DEBUG_PRINTLN(temp);

    mqtt->publish(t.c_str(), 0, true, temp.c_str());
  }

    void publishMqtt(const char *topic, const char* state) {
      //Check if MQTT Connected, otherwise it will crash the 8266
      if (WLED_MQTT_CONNECTED){
        char subuf[128];
        snprintf_P(subuf, 127, PSTR("%s/%s"), mqttDeviceTopic, topic);
        mqtt->publish(subuf, 0, false, state);
      }
    }

public:
  void setup()
  {
    bool HW_Pins_Used = (ioPin[0]==i2c_scl && ioPin[1]==i2c_sda); // note whether architecture-based hardware SCL/SDA pins used
    //PinOwner po = PinOwner::UM_BME280; // defaults to being pinowner for SCL/SDA pins  // WLEDMM not needed
    PinManagerPinType pins[2] = { { ioPin[0], true }, { ioPin[1], true } };  // allocate pins
    //if (HW_Pins_Used) po = PinOwner::HW_I2C; // allow multiple allocations of HW I2C bus pins  // WLEDMM not needed
    // WLEDMM join I2C HW wire
    if (!pinManager.joinWire()) { sensorType=0; enabled = false; return; }
    //if (!pinManager.allocateMultiplePins(pins, 2, po)) { sensorType=0; return; }
    //Wire.begin(ioPin[1], ioPin[0]);

    if (!bme.begin())
    {
      sensorType = 0;
      DEBUG_PRINTLN(F("Could not find BME280 I2C sensor!"));
    }
    else
    {
      switch (bme.chipModel())
      {
      case BME280::ChipModel_BME280:
        sensorType = 1;
        DEBUG_PRINTLN(F("Found BME280 sensor! Success."));
        break;
      case BME280::ChipModel_BMP280:
        sensorType = 2;
        DEBUG_PRINTLN(F("Found BMP280 sensor! No Humidity available."));
        break;
      default:
        sensorType = 0;
        DEBUG_PRINTLN(F("Found UNKNOWN sensor! Error!"));
      }
    }
    initDone=true;
  }

  void loop()
  {
    if (!enabled || strip.isUpdating()) return;

    // BME280 sensor MQTT publishing
    // Check if sensor present and Connected, otherwise it will crash the MCU
    if (sensorType != 0)
    {
      // Timer to fetch new temperature, humidity and pressure data at intervals
      timer = millis();

      if (timer - lastTemperatureMeasure >= TemperatureInterval * 1000)
      {
        lastTemperatureMeasure = timer;

        UpdateBME280Data(sensorType);

        float temperature = roundf(sensorTemperature * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals);
        float humidity, heatIndex, dewPoint;

        // If temperature has changed since last measure, create string populated with device topic
        // from the UI and values read from sensor, then publish to broker
        if (temperature != lastTemperature || PublishAlways)
        {
          publishMqtt("temperature", String(temperature, TemperatureDecimals).c_str());
        }

        lastTemperature = temperature; // Update last sensor temperature for next loop

        if (sensorType == 1) // Only if sensor is a BME280
        {
          humidity = roundf(sensorHumidity * powf(10, HumidityDecimals)) / powf(10, HumidityDecimals);
          heatIndex = roundf(sensorHeatIndex * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals);
          dewPoint = roundf(sensorDewPoint * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals);

          if (humidity != lastHumidity || PublishAlways)
          {
            publishMqtt("humidity", String(humidity, HumidityDecimals).c_str());
          }

          if (heatIndex != lastHeatIndex || PublishAlways)
          {
            publishMqtt("heat_index", String(heatIndex, TemperatureDecimals).c_str());
          }

          if (dewPoint != lastDewPoint || PublishAlways)
          {
            publishMqtt("dew_point", String(dewPoint, TemperatureDecimals).c_str());
          }

          lastHumidity = humidity;
          lastHeatIndex = heatIndex;
          lastDewPoint = dewPoint;
        }
      }

      if (timer - lastPressureMeasure >= PressureInterval * 1000)
      {
        lastPressureMeasure = timer;

        float pressure = roundf(sensorPressure * powf(10, PressureDecimals)) / powf(10, PressureDecimals);

        if (pressure != lastPressure || PublishAlways)
        {
          publishMqtt("pressure", String(pressure, PressureDecimals).c_str());
        }

        lastPressure = pressure;
      }
    }
  }

  void onMqttConnect(bool sessionPresent)
  {
    if (WLED_MQTT_CONNECTED && !mqttInitialized)
    {
      _mqttInitialize();
      mqttInitialized = true;
    }
  }

    /*
     * API calls te enable data exchange between WLED modules
     */
    inline float getTemperatureC() {
      if (UseCelsius) {
        return (float)roundf(sensorTemperature * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals);
      } else {
        return (float)roundf(sensorTemperature * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals) * 1.8f + 32;
      }      
    }

    inline float getTemperatureF() {
      if (UseCelsius) {
        return ((float)roundf(sensorTemperature * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals) -32) * 0.56f;
      } else {
        return (float)roundf(sensorTemperature * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals);
      }
    }

    inline float getHumidity() {
      return (float)roundf(sensorHumidity * powf(10, HumidityDecimals));
    }

    inline float getPressure() {
      return (float)roundf(sensorPressure * powf(10, PressureDecimals));
    }

    inline float getDewPointC() {
      if (UseCelsius) {
        return (float)roundf(sensorDewPoint * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals);
      } else {
        return (float)roundf(sensorDewPoint * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals) * 1.8f + 32;
      }
    }

    inline float getDewPointF() {
      if (UseCelsius) {
        return ((float)roundf(sensorDewPoint * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals) -32) * 0.56f;
      } else {
        return (float)roundf(sensorDewPoint * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals);
      }
    }

    inline float getHeatIndexC() {
      if (UseCelsius) {
        return (float)roundf(sensorHeatIndex * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals);
      } else {
        return (float)roundf(sensorHeatIndex * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals) * 1.8f + 32;
      }
    }

    inline float getHeatIndexF() {
      if (UseCelsius) {
        return ((float)roundf(sensorHeatIndex * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals) -32) * 0.56f;
      } else {
        return (float)roundf(sensorHeatIndex * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals);
      }
    }

  // Publish Sensor Information to Info Page
  void addToJsonInfo(JsonObject &root)
  {
    JsonObject user = root[F("u")];
    if (user.isNull()) user = root.createNestedObject(F("u"));
    
    if (sensorType==0) //No Sensor
    {
      // if we sensor not detected, let the user know
      JsonArray temperature_json = user.createNestedArray(F("BME/BMP280 Sensor"));
      temperature_json.add(F("Not Found"));
    }
    else if (sensorType==2) //BMP280
    {
      
      JsonArray temperature_json = user.createNestedArray(F("Temperature"));
      JsonArray pressure_json = user.createNestedArray(F("Pressure"));
      temperature_json.add(roundf(sensorTemperature * powf(10, TemperatureDecimals)));
      temperature_json.add(tempScale);
      pressure_json.add(roundf(sensorPressure * powf(10, PressureDecimals)));
      pressure_json.add(F("hPa"));
    }
    else if (sensorType==1) //BME280
    {
      JsonArray temperature_json = user.createNestedArray(F("Temperature"));
      JsonArray humidity_json = user.createNestedArray(F("Humidity"));
      JsonArray pressure_json = user.createNestedArray(F("Pressure"));
      JsonArray heatindex_json = user.createNestedArray(F("Heat Index"));
      JsonArray dewpoint_json = user.createNestedArray(F("Dew Point"));
      temperature_json.add(roundf(sensorTemperature * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals));
      temperature_json.add(tempScale);
      humidity_json.add(roundf(sensorHumidity * powf(10, HumidityDecimals)));
      humidity_json.add(F("%"));
      pressure_json.add(roundf(sensorPressure * powf(10, PressureDecimals)));
      pressure_json.add(F("hPa"));
      heatindex_json.add(roundf(sensorHeatIndex * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals));
      heatindex_json.add(tempScale);
      dewpoint_json.add(roundf(sensorDewPoint * powf(10, TemperatureDecimals)) / powf(10, TemperatureDecimals));
      dewpoint_json.add(tempScale);
    }
      return;
  }

  void appendConfigData() {
    oappend(SET_F("addHB('BME280');"));

    oappend(SET_F("addInfo('BME280/BMP280:pin[]',0,'','I2C/SPI CLK');"));
    oappend(SET_F("dRO('BME280/BMP280:pin[]',0);")); // disable read only pins
    oappend(SET_F("rOpt('BME280/BMP280:pin[]',0,'use global (")); oappendi(i2c_scl); oappend(")',-1);"); 

    oappend(SET_F("addInfo('BME280/BMP280:pin[]',1,'','I2C/SPI DTA');"));
    oappend(SET_F("rOpt('BME280/BMP280:pin[]',1,'use global (")); oappendi(i2c_sda); oappend(")',-1);"); 
  }
      

  // Save Usermod Config Settings
  void addToConfig(JsonObject& root)
  {
    JsonObject top = root.createNestedObject(FPSTR(_name));
    top[FPSTR(_enabled)] = enabled;
    top[F("TemperatureDecimals")] = TemperatureDecimals;
    top[F("HumidityDecimals")] = HumidityDecimals;
    top[F("PressureDecimals")] = PressureDecimals;
    top[F("TemperatureInterval")] = TemperatureInterval;
    top[F("PressureInterval")] = PressureInterval;
    top[F("PublishAlways")] = PublishAlways;
    top[F("UseCelsius")] = UseCelsius;
    top[F("HomeAssistantDiscovery")] = HomeAssistantDiscovery;
    JsonArray io_pin = top.createNestedArray(F("pin"));
    //WLEDMM: avoid global pin hijacking
    io_pin.add((ioPin[0]==i2c_scl)?-1:ioPin[0]);
    io_pin.add((ioPin[1]==i2c_sda)?-1:ioPin[1]);
    DEBUG_PRINTLN(F("BME280 config saved."));
  }

  // Read Usermod Config Settings
  bool readFromConfig(JsonObject& root)
  {
    // default settings values could be set here (or below using the 3-argument getJsonValue()) instead of in the class definition or constructor
    // setting them inside readFromConfig() is slightly more robust, handling the rare but plausible use case of single value being missing after boot (e.g. if the cfg.json was manually edited and a value was removed)

    int8_t newPin[2]; for (byte i=0; i<2; i++) newPin[i] = ioPin[i]; // prepare to note changed pins

    JsonObject top = root[FPSTR(_name)];
    if (top.isNull()) {
      DEBUG_PRINT(F(_name));
      DEBUG_PRINTLN(F(": No config found. (Using defaults.)"));
      return false;
    }
    bool configComplete = !top.isNull();

    configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled);
    // A 3-argument getJsonValue() assigns the 3rd argument as a default value if the Json value is missing
    configComplete &= getJsonValue(top[F("TemperatureDecimals")], TemperatureDecimals, 1);
    configComplete &= getJsonValue(top[F("HumidityDecimals")], HumidityDecimals, 0);
    configComplete &= getJsonValue(top[F("PressureDecimals")], PressureDecimals, 0);
    configComplete &= getJsonValue(top[F("TemperatureInterval")], TemperatureInterval, 30);
    configComplete &= getJsonValue(top[F("PressureInterval")], PressureInterval, 30);
    configComplete &= getJsonValue(top[F("PublishAlways")], PublishAlways, false);
    configComplete &= getJsonValue(top[F("UseCelsius")], UseCelsius, true);
    configComplete &= getJsonValue(top[F("HomeAssistantDiscovery")], HomeAssistantDiscovery, false);
    for (byte i=0; i<2; i++) configComplete &= getJsonValue(top[F("pin")][i], newPin[i], ioPin[i]);

    DEBUG_PRINT(FPSTR(_name));
    if (!initDone) {
      // first run: reading from cfg.json
      for (byte i=0; i<2; i++) ioPin[i] = newPin[i];
      DEBUG_PRINTLN(F(" config loaded."));
    } else {
      DEBUG_PRINTLN(F(" config (re)loaded."));
      // changing parameters from settings page
      bool pinsChanged = false;
      for (byte i=0; i<2; i++) if (ioPin[i] != newPin[i]) { pinsChanged = true; break; } // check if any pins changed
      if (pinsChanged) { //if pins changed, deallocate old pins and allocate new ones
        PinOwner po = PinOwner::UM_BME280;
        if (ioPin[0]==i2c_scl && ioPin[1]==i2c_sda) po = PinOwner::HW_I2C;  // allow multiple allocations of HW I2C bus pins
        pinManager.deallocateMultiplePins((const uint8_t *)ioPin, 2, po);  // deallocate pins
        for (byte i=0; i<2; i++) ioPin[i] = newPin[i];
        setup();
      }
      // use "return !top["newestParameter"].isNull();" when updating Usermod with new features
      return !top[F("pin")].isNull();
    }

    return configComplete;
  }

  uint16_t getId() {
    return USERMOD_ID_BME280;
  }
};

const char UsermodBME280::_name[]                      PROGMEM = "BME280/BMP280";
const char UsermodBME280::_enabled[]                   PROGMEM = "enabled";
