#include "settings.h"
#include "env.h"
#include <driver/pcnt.h> 
#include <soc/pcnt_struct.h>
#include <ArduinoJson.h>
#include <Wire.h>
#include "Adafruit_FRAM_I2C.h"
#include "Adafruit_SHT4x.h"
#include "Adafruit_VEML7700.h"
#include "Adafruit_MAX1704X.h"
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>

// Settings for moisture sensor.
#define PCNT_UNIT       PCNT_UNIT_0 
#define PCNT_INPUT_SIG  MOISTUREPIN  
#define PCNT_INPUT_CTRL GPIO_NUM_0 
#define PCNT_H_LIM_VAL  overflow

uint16_t measurement_delay_ms = 100;
uint8_t frequency_mult = 500 / measurement_delay_ms;
int16_t pulse_count = 0;
uint32_t mult = 0;
uint32_t overflow = 20000;
  
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;

struct __attribute__((packed)) Report {
  int16_t temperature;
  uint8_t humidity;
  uint8_t soil_moisture; 
  uint16_t light;
  uint8_t battery; 
};

// Capacitance measured in dry air (calibration value)
uint32_t c_dry;
// Capacitance measured in water (calibration value)
uint32_t c_wet;

// FRAM
Adafruit_FRAM_I2C fram = Adafruit_FRAM_I2C();

// SHT40 temperature and humidity sensor
Adafruit_SHT4x sht4 = Adafruit_SHT4x();

// VEML7700 ambient light sensor
Adafruit_VEML7700 veml = Adafruit_VEML7700();

// MAX17048 battery monitor
Adafruit_MAX17048 maxlipo;

// MQTT
WiFiClient espClient;
PubSubClient client(espClient);
unsigned int mqtt_messages = 0;
unsigned int watering_time = 0;

/**
  Setup FRAM.
*/
void setupFRAM() {
  fram.begin();
  
  int address = 0x0;
  fram.read(address, (uint8_t*)&c_dry, sizeof(c_dry));
  address += sizeof(c_dry);
  fram.read(address, (uint8_t*)&c_wet, sizeof(c_wet));
  address += sizeof(c_wet);
}

/**
  Setup SHT40.
*/
void setupSHT() {
  sht4.begin();
  sht4.setPrecision(SHT4X_HIGH_PRECISION);
  sht4.setHeater(SHT4X_NO_HEATER);
}

/**
  Setup VEML.
*/
void setupVEML() {
  veml.begin();
}

/**
  Setup MAXLIPO.
*/
void setupMAX() {
  maxlipo.begin();
}

/**
  Increase mult when PCNT_H_LIM_VAL is reached.
*/
static void IRAM_ATTR pcnt_intr_handler(void *arg) {
  portENTER_CRITICAL_ISR(&timerMux);
  mult++;
  PCNT.int_clr.val = BIT(PCNT_UNIT);
  portEXIT_CRITICAL_ISR(&timerMux);
}

/**
  Power on I2C sensors.
*/
void powerON() {
  // Set the PNP transistor gate at LOW to power up the devices
  digitalWrite(I2C_CONTROLLER, LOW);
  delay(100);
}

/**
  Power off I2C sensors.
*/
void powerOFF() {
  // Turn off I2C and set the PNP transistor gate at HIGH to power off devices
  digitalWrite(I2C_CONTROLLER, HIGH);
}

/**
  Turn on I2C.
*/
void turnOnI2C() {
  Wire.begin(SDA_PIN, SCL_PIN);
}

/**
  Turn off I2C.
*/
void turnOffI2C() {
  Wire.end();
  pinMode(SDA, INPUT);
  pinMode(SCK, INPUT);
}

/**
  Switch on wifi an connect to the box.
*/
void switchOnWifi() {
  unsigned reconnection_attempt = 10;
  WiFi.begin(SSID, PASSWORD, CHANNEL, BSID);

  // Try to connect to the box for 5 seconds
  while (WiFi.status() != WL_CONNECTED) {
    if (DEBUG) {
      Serial.print(".");
    }
    delay(500);
    reconnection_attempt -= 1;
    if (reconnection_attempt <= 0) {
      break;
    }
  }

  if (DEBUG && WiFi.status() == WL_CONNECTED) {
    Serial.print("Wifi connection succeed. Local IP: ");
    Serial.println(WiFi.localIP());
  } else if (DEBUG) {
    Serial.println("Wifi connection failed.");
  }
}

/**
  Switch off wifi.
*/
void switchOffWifi() {
  if (WiFi.status() == WL_CONNECTED) {
    WiFi.disconnect(true);
    WiFi.mode(WIFI_OFF);
  
    if (DEBUG) {
      Serial.println("Wifi is OFF.");
    }
  }
}

/**
  MQTT callback when a message is received. Payload should contain the watering time in seconds.
*/
void callback(char* topic, byte* payload, unsigned int length) {
  mqtt_messages += 1;

  char msg[length + 1];
  memcpy(msg, payload, length);
  msg[length] = '\0';
  watering_time = atoi(msg);
}

/**
  Setup PCNT for moisture sensor.
*/
void setupPCNT() {
  pcnt_config_t pcnt_config = {};
  
  pcnt_config.pulse_gpio_num = PCNT_INPUT_SIG;
  pcnt_config.ctrl_gpio_num = PCNT_INPUT_CTRL; 
  pcnt_config.unit = PCNT_UNIT;
  pcnt_config.channel = PCNT_CHANNEL_0;
  pcnt_config.counter_h_lim = PCNT_H_LIM_VAL; 
  pcnt_config.pos_mode = PCNT_COUNT_INC; 
  pcnt_config.neg_mode = PCNT_COUNT_INC; 
  pcnt_config.lctrl_mode = PCNT_MODE_DISABLE;
  pcnt_config.hctrl_mode = PCNT_MODE_KEEP;
  pcnt_unit_config(&pcnt_config);
  
  pcnt_counter_pause(PCNT_UNIT);
  pcnt_counter_clear(PCNT_UNIT);

  pcnt_event_enable(PCNT_UNIT, PCNT_EVT_H_LIM);                     
  pcnt_isr_register(pcnt_intr_handler, NULL, 0, NULL);                    
  pcnt_intr_enable(PCNT_UNIT); 
  
  pcnt_counter_resume(PCNT_UNIT);
}

/**
  Setup.
*/
void setup() {
  // Open serial communication
  Serial.begin(9600);

  pinMode(I2C_CONTROLLER, OUTPUT);
  pinMode(MOISTUREPIN, INPUT);
  pinMode(PUMP_CONTROLLER, OUTPUT);
  // Set the PNP transistor gate at HIGH to power down the pump
  digitalWrite(PUMP_CONTROLLER, HIGH);
  
  // Setup wifi
  WiFi.mode(WIFI_STA);
  WiFi.persistent(false);
  // WiFi.config(IP, GATEWAY, SUBNET);

  powerON();
  turnOnI2C();
  
  setupFRAM();
  setupSHT();
  setupVEML();
  setupPCNT();
  setupMAX();
  
  switchOnWifi();
  client.setServer(mqtt_server, mqtt_port);
  client.setCallback(callback);
  
  // Deepsleep
  if (ENABLE_DEEPSLEEP) {
    esp_sleep_enable_timer_wakeup(SLEEP_DELAY * 1000 * 1000);

    Report report = getReport();
    sendReport(report);
    check_watering();
    
    switchOffWifi();
    turnOffI2C();
    powerOFF();
    delay(100);
    esp_deep_sleep_start();
  }
}

/**
  Get the temperature of the air with SHT40.
*/
void getTemperatureAndHumidity(int16_t *temperature, uint8_t *humidity) {
  sensors_event_t humidity_event, temp_event;

  sht4.getEvent(&humidity_event, &temp_event);

  float h = humidity_event.relative_humidity;
  float t = temp_event.temperature;

  // Clamp values using constrain()
  h = constrain(h, 0.0f, 100.0f);
  t = constrain(t, -100.0f, 300.0f);

  *humidity = static_cast<uint8_t>(round(h));
  *temperature = static_cast<int16_t>(round(t * 100.0f));
  
  if (DEBUG) {
    Serial.print("Temperature: ");
    Serial.print(t);
    Serial.println("*C");
    
    Serial.print("Humidity: ");
    Serial.print(h);
    Serial.println("%");
  }
}

/**
  Get the soil moisture level with the PCNT.
*/
void getSoilMoisture(uint8_t *soil_moisture) {
  pulse_count = 0;
  mult = 0;

  pcnt_counter_clear(PCNT_UNIT); 
  delay(measurement_delay_ms);
  pcnt_get_counter_value(PCNT_UNIT, &pulse_count); 
  uint32_t frequency = (pulse_count + (mult * overflow)) * frequency_mult;
  // Frequency in kHz
  frequency = frequency / 1000;
  // Capacitance in pF
  uint32_t capacitance = (1.44 * 1e6) / ((RESISTOR_A + 2 * RESISTOR_B) * frequency);
  
  capacitance = constrain(capacitance, c_dry, c_wet);
  *soil_moisture = map(capacitance, c_dry, c_wet, 0, 100);

  if (DEBUG) {
    Serial.print("Soil moisture: ");
    Serial.print(*soil_moisture);
    Serial.println("%");
  }
}

/**
  Get the amount of ambient light in lux with VEML7700.
*/
void getAmbientLight(uint16_t *light) {
  float light_event = veml.readLux(VEML_LUX_AUTO);
  light_event = constrain(light_event, 0.0, 65535.0);
  *light = static_cast<uint16_t>(round(light_event));
  
  if (DEBUG) {
    Serial.print("Ambient light: ");
    Serial.print(*light);
    Serial.println(" lux");
  }
}

/**
  Get the battery level with MAX17048.
*/
void getBatteryCharge(uint8_t *battery) {
  float cellVoltage = maxlipo.cellVoltage();

  if (!isnan(cellVoltage)) {
    float battery_event = maxlipo.cellPercent();
    *battery = static_cast<uint8_t>(constrain(round(battery_event), 0, 101));
    
    if (DEBUG) {
      Serial.print("Battery charge: ");
      Serial.print(*battery);
      Serial.println("%");
    }
  } else {
    *battery = 0;
    
    if (DEBUG) {
      Serial.println("Failed to read cell voltage, check battery is connected!");
    }
  }
}

/**
  Get a report.
*/
Report getReport() {
  if (DEBUG) {
    Serial.println("Retrieving report...");
  }

  Report report;
  
  getTemperatureAndHumidity(&report.temperature, &report.humidity);
  getSoilMoisture(&report.soil_moisture);
  getAmbientLight(&report.light);
  getBatteryCharge(&report.battery);

  return report;
}

/**
  Convert a report into a json string.
*/
String reportToJson(const Report& report) {
  StaticJsonDocument<256> doc;  
  doc["temperature"] = String(report.temperature / 100.0, 2); // 2 decimals
  doc["humidity"] = report.humidity;
  doc["moisture"] = report.soil_moisture;
  doc["light"] = report.light;
  doc["battery"] = report.battery;

  String reportJson;
  serializeJson(doc, reportJson);

  if (DEBUG) {
    Serial.print("Report : ");
    Serial.println(reportJson);
  }
  
  return reportJson; 
}

/**
  Connect to MQTT broker.
*/
void reconnect() {
  unsigned reconnection_attempt = 10;

  // Try to connect to the box for 5 seconds
  while (!client.connected()) {
    client.connect("ESP32Client", mqtt_user, mqtt_password);
    if (DEBUG) {
      Serial.print(".");
    }
    delay(500);
    reconnection_attempt -= 1;
    if (reconnection_attempt <= 0) {
      break;
    }
  }

  if (client.connected()) {
    client.subscribe(watering_topic);
    if (DEBUG) {
      Serial.print("Connected to MQTT broker.");
    }  
  } else if (DEBUG) {
    Serial.println("Connexion to MQTT broker failed.");
  }
}

/**
  Drain topic and return true if at least one message was retrieved.
*/
bool drainIncoming() {
  unsigned int window_ms = 100; 
  mqtt_messages = 0; 
  watering_time = 0;
  
  unsigned long start = millis();
  while ((millis() - start) < window_ms) {
    client.loop();     
    delay(0);        
  }

  if (mqtt_messages == 0) {
    if (DEBUG) {
      Serial.println("No message received.");
    }
    return false;
  } else {
    if (DEBUG) {
      Serial.print(mqtt_messages);
      Serial.println(" messages received.");
    }
    return true;
  }
}

/**
  Send all the report to the MQTT broker.
*/
void sendReport(Report report) {
  String reportJson;

  if (DEBUG) {
    Serial.println("Sending reports...");
  }

  if (WiFi.status() == WL_CONNECTED) {
    if (!client.connected()) {
      reconnect();
    }
    
    if (client.connected()) {
      reportJson = reportToJson(report);
      bool success = client.publish(report_topic, reportJson.c_str());

      if (success && DEBUG) {
        Serial.println("Report sent to MQTT broker.");
      } 
      if (!success && DEBUG) {
        Serial.println("Failed to sent the report to MQTT broker.");
      }

    } else if (DEBUG) {
      Serial.println("No connexion to MQTT broker");
    }
  } else if (DEBUG) {
    Serial.println("Not connected to Wifi");
  }
}
  
/**
  Water plant.
*/
void water_plant() {
  if (DEBUG) {
    Serial.print("Plant will be watered ");
    Serial.print(watering_time);
    Serial.println("s");
  }
  
  // Set the PNP transistor gate at LOW to power up the devices
  digitalWrite(PUMP_CONTROLLER, LOW);  

  if (DEBUG) {
    Serial.println("PUMP ON");
  }
  
  delay(watering_time * 1000);            
  // Turn off I2C and set the PNP transistor gate at HIGH to power off devices    
  digitalWrite(PUMP_CONTROLLER, HIGH);  

  if (DEBUG) {
    Serial.println("PUMP OFF");
  }
}

/**
 Check if plant needs to be watered.
*/
void check_watering() {
  bool wateringTrigger = drainIncoming();

  if (wateringTrigger) {
    if (DEBUG) {
      Serial.println("Plant needs to be watered!");
    }
    water_plant();
  }
}

/**
  Loop.
*/
void loop(void) {
  // If deep sleep mode is not enable, run the code every 10 seconds
  Report report = getReport();
  sendReport(report);
  check_watering();
  delay(60000);
}
