#include <Arduino.h>
#include <strings.h>
#include <Metriful_sensor.h>
#include <WiFi_functions.h>

// A struct that is used in config.h to set a device's name
// based on its WiFi MAC address. The config.h file must configure
// 'DeviceMapping deviceMapping[]', even if it is empty.
struct DeviceMapping
{
  char macAddress[18];
  char deviceName[50];
};

#include "config.h"

WiFiClient client;

// Buffers for assembling http POST requests.
char postBuffer[450] = {0};
char fieldBuffer[70] = {0};

// Structs for holding measurement data.
AirData_t airData = {0};
AirQualityData_t airQualityData = {0};
LightData_t lightData = {0}; 
ParticleData_t particleData = {0};
SoundData_t soundData = {0};

// Definition of the display attributes of data sent to Home Assistant.
// The attribute fields are: {name, unit, icon, decimal places}
HA_Attributes_t pressure = {"Pressure", "hPa", "weather-cloudy", 1};
HA_Attributes_t humidity = {"Humidity", "%", "water-percent", 1};
HA_Attributes_t temperature = {"Temperature", CELSIUS_SYMBOL, "thermometer", 1};
HA_Attributes_t illuminance = {"Illuminance", "lx", "white-balance-sunny", 2};
HA_Attributes_t whiteLightLevel = {"White light level", " ", "white-balance-sunny", 0};
HA_Attributes_t soundLevel = {"Sound level", "dBA", "microphone", 1};
HA_Attributes_t peakAmplitude = {"Sound peak", "mPa", "waveform", 2};
HA_Attributes_t estimatedCO2 = {"Estimated CO2", "ppm", "chart-bubble", 1};
HA_Attributes_t equivalentBreathVOC = {"Equivalent breath VOC", "ppm", "chart-bubble", 2};
HA_Attributes_t AQ_index = {"Air Quality Index", " ", "thought-bubble-outline", 1};
HA_Attributes_t AQ_assessment = {"Air quality assessment", "", "flower-tulip", 0};
HA_Attributes_t AQ_calibration = {"Air quality calibration", "", "flower-tulip", 0};
HA_Attributes_t particleConcentration = {"Particle concentration", "ppL", "chart-bubble", 0};
HA_Attributes_t particleCalibration = {"Particle calibration", "", "chart-bubble", 0};

// Buffer for storing the device name.
char deviceName[50];

void setDeviceName()
{
  byte mac[6];
  char macAddress[18];
  WiFi.macAddress(mac);
  snprintf(
      macAddress, sizeof(macAddress) / sizeof(macAddress[0]),
      "%x:%x:%x:%x:%x:%x", mac[5], mac[4], mac[3], mac[2], mac[1], mac[0]);
  Serial.print("Device MAC address: ");
  Serial.println(macAddress);

  // Set de default device name, based on the MAC address.
  // The following code might set a specific device name,
  // based on the configuration data.
  snprintf(deviceName, sizeof(deviceName),
           "metriful_%x%x%x%x%x%x", mac[5], mac[4], mac[3], mac[2], mac[1], mac[0]);

  if (sizeof(deviceMapping) > 0)
  {
    struct DeviceMapping *ptr = deviceMapping;
    struct DeviceMapping *last = deviceMapping + sizeof(deviceMapping) / sizeof(deviceMapping[0]);
    while (ptr < last)
    {
      if (strcasecmp(macAddress, ptr->macAddress) == 0)
      {
        strncpy(deviceName, ptr->deviceName, sizeof(deviceName));
      }
      ptr++;
    }
  }

  Serial.print("Device name: ");
  Serial.println(deviceName);
}

void setup()
{
  SensorHardwareSetup(I2C_ADDRESS);
  connectToWiFi(WIFI_SSID, WIFI_PASSWORD);

  setDeviceName();

  // Apply settings to the Metriful MS430 board and enter cycle mode.
  uint8_t particleSensorCode = PARTICLE_SENSOR;
  uint8_t cycle_period = CYCLE_PERIOD;  
  TransmitI2C(I2C_ADDRESS, PARTICLE_SENSOR_SELECT_REG, &particleSensorCode, 1);
  TransmitI2C(I2C_ADDRESS, CYCLE_TIME_PERIOD_REG, &cycle_period, 1);
  ready_assertion_event = false;
  TransmitI2C(I2C_ADDRESS, CYCLE_MODE_CMD, 0, 0);
}

// Send data to Home Assistant as an HTTP POST request.
void http_POST_Home_Assistant(const HA_Attributes_t * attributes, const char * valueText) {
  client.stop();
  if (client.connect(HOME_ASSISTANT_IP, 8123)) {
    // Form the URL from the name but replace spaces with underscores
    strcpy(fieldBuffer, attributes->name);
    for (uint8_t i=0; i<strlen(fieldBuffer); i++) {
      if (fieldBuffer[i] == ' ') {
        fieldBuffer[i] = '_';
      }
    }
    sprintf(postBuffer, "POST /api/states/%s.%s HTTP/1.1", deviceName, fieldBuffer);
    client.println(postBuffer);
    client.println("Host: " HOME_ASSISTANT_IP ":8123");
    client.println("Content-Type: application/json");
    client.println("Authorization: Bearer " LONG_LIVED_ACCESS_TOKEN);
    
    // Assemble the JSON content string:
    sprintf(postBuffer,
            "{\"state\":%s,\"attributes\":{\"unit_of_measurement\""
            ":\"%s\",\"friendly_name\":\"%s\",\"icon\":\"mdi:%s\"}}",
            valueText, attributes->unit, attributes->name, attributes->icon);

    sprintf(fieldBuffer,"Content-Length: %u", strlen(postBuffer));  
    client.println(fieldBuffer);
    client.println();
    client.print(postBuffer);
  }
  else {
    Serial.println("Client connection failed.");
  }
}

// Send numeric data with specified sign, integer and fractional parts.
void sendNumericData(const HA_Attributes_t * attributes, uint32_t valueInteger, 
                             uint8_t valueDecimal, bool isPositive) {
  char valueText[20] = {0};
  const char * sign = isPositive ? "" : "-";
  switch (attributes->decimalPlaces) {
    case 0:
    default:
      sprintf(valueText,"%s%" PRIu32, sign, valueInteger);
      break;
    case 1:
      sprintf(valueText,"%s%" PRIu32 ".%u", sign, valueInteger, valueDecimal);
      break;
    case 2:
      sprintf(valueText,"%s%" PRIu32 ".%02u", sign, valueInteger, valueDecimal);
      break;
  }
  http_POST_Home_Assistant(attributes, valueText);
}

// Send a text string: must have quotation marks added.
void sendTextData(const HA_Attributes_t * attributes, const char * valueText) {
  char quotedText[20] = {0};
  sprintf(quotedText,"\"%s\"", valueText);
  http_POST_Home_Assistant(attributes, quotedText);
}

void wait_for_new_readings()
{
  Serial.println("Waiting for new data from sensors ...");
  while (!ready_assertion_event) {
    yield();
  }
  ready_assertion_event = false;
}

void read_sensor_data_from_i2c()
{
  // Read data from the MS430 into the data structs. 
  Serial.println("Reading data from sensors");
  ReceiveI2C(I2C_ADDRESS, AIR_DATA_READ, (uint8_t *) &airData, AIR_DATA_BYTES);
  ReceiveI2C(I2C_ADDRESS, AIR_QUALITY_DATA_READ, (uint8_t *) &airQualityData, AIR_QUALITY_DATA_BYTES);
  ReceiveI2C(I2C_ADDRESS, LIGHT_DATA_READ, (uint8_t *) &lightData, LIGHT_DATA_BYTES);
  ReceiveI2C(I2C_ADDRESS, SOUND_DATA_READ, (uint8_t *) &soundData, SOUND_DATA_BYTES);
  ReceiveI2C(I2C_ADDRESS, PARTICLE_DATA_READ, (uint8_t *) &particleData, PARTICLE_DATA_BYTES);
}

void make_sure_wifi_is_connected() {
  uint8_t wifiStatus = WiFi.status();
  if (wifiStatus != WL_CONNECTED) {
    Serial.print("Wifi diconnected! Status: ");
    Serial.println(interpret_WiFi_status(wifiStatus));
    connectToWiFi(WIFI_SSID, WIFI_PASSWORD);
    ready_assertion_event = false;
  }  
}

void send_data_to_home_assistant() {
  Serial.println("Send data to Home Assistant");

  uint8_t T_intPart = 0;
  uint8_t T_fractionalPart = 0;
  bool isPositive = true;
  getTemperature(&airData, &T_intPart, &T_fractionalPart, &isPositive);
  if (T_intPart > 0 || T_fractionalPart > 0)
    sendNumericData(&temperature, (uint32_t)T_intPart, T_fractionalPart, isPositive);

  uint32_t P_intPart = airData.P_Pa / 100;
  uint32_t P_fractionalPart = floor((airData.P_Pa % 100) / 10 + 0.5);
  if (P_intPart > 0 || P_fractionalPart > 0)
    sendNumericData(&pressure, P_intPart, P_fractionalPart, true);

  if (airData.H_pc_int > 0 || airData.H_pc_fr_1dp > 0)
    sendNumericData(&humidity, (uint32_t) airData.H_pc_int, airData.H_pc_fr_1dp, true);

  if (lightData.illum_lux_int > 0 || lightData.illum_lux_fr_2dp > 0)
    sendNumericData(&illuminance, (uint32_t) lightData.illum_lux_int, lightData.illum_lux_fr_2dp, true);

  if (soundData.SPL_dBA_int > 0 || soundData.SPL_dBA_fr_1dp > 0)
    sendNumericData(&soundLevel, (uint32_t) soundData.SPL_dBA_int, soundData.SPL_dBA_fr_1dp, true);

  if (soundData.peak_amp_mPa_int > 0 || soundData.peak_amp_mPa_fr_2dp > 0)
    sendNumericData(&peakAmplitude, (uint32_t) soundData.peak_amp_mPa_int, soundData.peak_amp_mPa_fr_2dp, true);

  // When the particle sensor is not connected, we get 65535 readings.
  // We want to skip those. Also skip sending data while the sensor
  // is warming up. This way, invalid data is not logged in
  // Home Assistant.
  sendNumericData(&particleCalibration, (uint32_t)particleData.valid, 0, true);
  if (particleData.valid != 0 && particleData.concentration_int < 60000)
  {
    sendNumericData(&particleConcentration, (uint32_t)particleData.concentration_int, particleData.concentration_fr_2dp, true);
  }

  // While the Metriful board is still in its self-calibration
  // phase, do not send air quality data to Home Assistant.
  sendNumericData(&AQ_calibration, airQualityData.AQI_accuracy, 0, true);
  if (airQualityData.AQI_accuracy > 0) {
    sendNumericData(&estimatedCO2, (uint32_t) airQualityData.CO2e_int, airQualityData.CO2e_fr_1dp, true);
    sendNumericData(&equivalentBreathVOC, (uint32_t) airQualityData.bVOC_int, airQualityData.bVOC_fr_2dp, true);
    sendNumericData(&AQ_index, (uint32_t)airQualityData.AQI_int, airQualityData.AQI_fr_1dp, true);
    sendTextData(&AQ_assessment, interpret_AQI_value(airQualityData.AQI_int));
  }
}

void log_measurements_to_console() {
  Serial.println("");
  printAirData(&airData, false);
  printAirQualityData(&airQualityData, false);
  printLightData(&lightData, false);
  printSoundData(&soundData, false);
  printParticleData(&particleData, false, PARTICLE_SENSOR);
  Serial.println("");
}

void loop()
{
  wait_for_new_readings();
  read_sensor_data_from_i2c();
  make_sure_wifi_is_connected();
  send_data_to_home_assistant();
  log_measurements_to_console();
}