// fan4life main
// ESP32-C3 | Arduino core 3.x
// 2-pin 5V fan via IRLZ44N MOSFET (PWM)
// DHT22 temp/humidity, SSD1306 OLED, WiFi + MQTT (state publish only)

#include <U8g2lib.h>
#include <DHT.h>
#include <WiFi.h>
#include <PubSubClient.h>
#include "secrets.h"

// --- MQTT topics (publish only) ---
#define TOPIC_TEMP      "esp32c3_fan/temperature"
#define TOPIC_HUMIDITY  "esp32c3_fan/humidity"
#define TOPIC_FAN_STATE "esp32c3_fan/fan/state"
#define TOPIC_FAN_SPEED "esp32c3_fan/fan/speed"

// --- Pins ---
#define DHTPIN    2
#define DHTTYPE   DHT22
#define FAN_PIN   3
#define OLED_SDA  5
#define OLED_SCL  6

// --- PWM ---
#define PWM_FREQ  20000
#define PWM_RES   8

// --- Temperature -> fan level table ---
// Level 0 = off. Higher temp -> higher level. Hysteresis: drop a level
// only after temp falls TEMP_HYST below the level's threshold.
#define TEMP_HYST 1.0
struct FanLevel { const char* label; float tempOn; uint8_t duty; };
const FanLevel LEVELS[] = {
  { "1", 28.0, 230 },
  { "2", 30.0, 236 },
  { "3", 32.0, 242 },
  { "4", 34.0, 249 },
  { "5", 36.0, 255 },
};
const uint8_t NUM_LEVELS = sizeof(LEVELS) / sizeof(LEVELS[0]);
#define TEMP_OFF (LEVELS[0].tempOn - TEMP_HYST)

// --- OLED ---
#define OLED_RESET U8X8_PIN_NONE
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, OLED_RESET, OLED_SCL, OLED_SDA);

DHT dht(DHTPIN, DHTTYPE);
WiFiClient wifiClient;
PubSubClient mqtt(wifiClient);

int xOffset = 30;
int yOffset = 30;

uint8_t fanLevel = 0;  // 0 = off, 1..NUM_LEVELS

void applyFan() {
  if (fanLevel == 0) {
    ledcWrite(FAN_PIN, 0);
    return;
  }
  uint8_t duty = LEVELS[fanLevel - 1].duty;
  if (duty > 0 && duty < 200) {
    ledcWrite(FAN_PIN, 255);  // kick-start
    delay(300);
  }
  ledcWrite(FAN_PIN, duty);
}

void publishFan() {
  mqtt.publish(TOPIC_FAN_STATE, fanLevel > 0 ? "ON" : "OFF", true);
  const char* label = fanLevel > 0 ? LEVELS[fanLevel - 1].label : "0";
  mqtt.publish(TOPIC_FAN_SPEED, label, true);
}

void setFanLevel(uint8_t level) {
  if (level == fanLevel) return;
  fanLevel = level;
  applyFan();
  publishFan();
}

// Pick the level for a given temperature, with hysteresis from current level.
uint8_t levelForTemp(float tempC) {
  uint8_t target = 0;
  for (uint8_t i = 0; i < NUM_LEVELS; i++) {
    if (tempC >= LEVELS[i].tempOn) target = i + 1;
  }
  // Hysteresis: only drop if temp is clearly below the current level's on-point.
  if (target < fanLevel) {
    float dropBelow = LEVELS[fanLevel - 1].tempOn - TEMP_HYST;
    if (tempC > dropBelow) target = fanLevel;
  }
  return target;
}

void connectWiFi() {
  Serial.printf("Connecting to %s\n", WIFI_SSID);

  WiFi.mode(WIFI_STA);
  WiFi.setTxPower(WIFI_POWER_8_5dBm);
  WiFi.disconnect(true, true);
  delay(200);

  WiFi.setAutoReconnect(true);
  WiFi.persistent(false);
  WiFi.begin(WIFI_SSID, WIFI_PASS);

  uint8_t result = WiFi.waitForConnectResult(15000);

  if (result == WL_CONNECTED) {
    Serial.printf("WiFi connected: %s\n", WiFi.localIP().toString().c_str());
  } else {
    Serial.printf("WiFi failed, result=%u, status=%d\n", result, WiFi.status());
  }
}

void connectMQTT() {
  while (!mqtt.connected()) {
    Serial.print("Connecting MQTT...");
    if (mqtt.connect(MQTT_CLIENT, MQTT_USER, MQTT_PASS)) {
      Serial.println("connected!");
      publishFan();
    } else {
      Serial.printf("failed rc=%d, retry in 5s\n", mqtt.state());
      delay(5000);
    }
  }
}

void handle_oled(float tempC, float humidity) {
  (void)humidity;
  char tempStr[12];
  snprintf(tempStr, sizeof(tempStr), "%.1f", tempC);

  u8g2.clearBuffer();

  // Large temperature reading + small °C suffix, centered as a unit.
  u8g2.setFont(u8g2_font_logisoso22_tn);
  int tempW = u8g2.getStrWidth(tempStr);
  const int suffixW = 10;  // approx width reserved for "°C"
  const int gap = 2;
  int totalW = tempW + gap + suffixW;
  int tempX = (128 - totalW) / 2;
  int tempY = 46;
  u8g2.drawStr(tempX, tempY, tempStr);

  u8g2.setFont(u8g2_font_6x10_tr);
  int suffixX = tempX + tempW + gap;
  u8g2.drawStr(suffixX, tempY - 14, "o");
  u8g2.drawStr(suffixX, tempY - 2, "C");

  // Fan level indicator: 5 stacked bars at the bottom.
  // Filled bars = current level, empty bars = remaining.
  const int barCount = NUM_LEVELS;
  const int barW = 18;
  const int barH = 6;
  const int barGap = 2;
  const int barsW = barCount * barW + (barCount - 1) * barGap;
  const int barY = 56;
  int barX0 = (128 - barsW) / 2;
  for (uint8_t i = 0; i < barCount; i++) {
    int x = barX0 + i * (barW + barGap);
    if (i < fanLevel) {
      u8g2.drawBox(x, barY, barW, barH);
    } else {
      u8g2.drawFrame(x, barY, barW, barH);
    }
  }

  u8g2.sendBuffer();
}

void setup() {
  Serial.begin(115200);
  delay(500);

  ledcAttach(FAN_PIN, PWM_FREQ, PWM_RES);
  ledcWrite(FAN_PIN, 0);

  dht.begin();

  u8g2.begin();
  u8g2.setContrast(255);
  u8g2.setBusClock(400000);
  u8g2.clearDisplay();

  connectWiFi();
  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  connectMQTT();

  Serial.println("Ready.");
}

void loop() {
  if (WiFi.status() != WL_CONNECTED) connectWiFi();
  if (!mqtt.connected()) connectMQTT();
  mqtt.loop();

  float tempC    = dht.readTemperature();
  float humidity = dht.readHumidity();

  if (isnan(tempC) || isnan(humidity)) {
    Serial.println("DHT22 read failed");
    delay(2000);
    return;
  }

  // Auto fan control: temperature -> level (with hysteresis)
  setFanLevel(levelForTemp(tempC));

  // Publish telemetry
  char buf[10];
  dtostrf(tempC,    4, 1, buf); mqtt.publish(TOPIC_TEMP,     buf);
  dtostrf(humidity, 4, 0, buf); mqtt.publish(TOPIC_HUMIDITY, buf);

  Serial.printf("T:%.1fC  H:%.0f%%  Fan:%s L%u\n",
                tempC, humidity, fanLevel > 0 ? "ON" : "OFF", fanLevel);
  handle_oled(tempC, humidity);

  delay(3000);
}
