#include <LiquidCrystal_I2C.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include "RTClib.h"
#include <IRremote.h>
#include <ESPAsyncWebSrv.h>

#define TEMP_PIN 39
#define LED_1_PIN 16

#define RELAY_PIN 18
#define RECEIVER_PIN 17
#define SHOW_IP_BUTTON_PIN 33

#define MESSURMENT_SWITCH_PIN 2
#define MODE_SWITCH_PIN 4

#define ENCODER_CLK_PIN 25
#define ENCODER_DT_PIN  26

const char* ssid = "Wokwi-GUEST";
const char* password = "";

String deviceId = "12345";

unsigned long lastTime = 0;
unsigned long timerDelay = 10000 ; // 10s

long targetTemperatureInC = 25.0;

bool relayState = false;
String termoState = "NONE";
int showIpButtonLastState = HIGH;
int modeLastState = LOW;

unsigned long prevMillis = 0.0;

unsigned long timerInS = 0;
bool timerIsOn = false;
bool timerOnPause = false;

char buffer[7] = "000000";
int currentIndex = 5;

IRrecv RECEIVER = IRrecv(RECEIVER_PIN);
LiquidCrystal_I2C LCD = LiquidCrystal_I2C(0x27, 20, 4);
RTC_DS1307 RTC;
AsyncWebServer server(80);

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

  analogReadResolution(10);
  initLCD();
  initRTC();
  initRelay();
  initEncoder();
  initOtherPins();
  
  WiFi.begin(ssid, password, 6);
}

// START ------------------- LOOP -------------------

void loop() {
  tempLoop();
  relayLoop();

  setRelayState();
  showTime();
  wifiIconAnimation();
  showIp();
}

void tempLoop() {
  int tempVal = analogRead(TEMP_PIN);
  float tempC = readTempC(tempVal);

  printTemp(tempC);
  sendTempAfterDelay(tempC);
  
}

void relayLoop() {
  int modeSwitchState = digitalRead(MODE_SWITCH_PIN);

  if (modeSwitchState != HIGH) {
    if (modeLastState != modeSwitchState) {
      relayState = false;
      modeLastState = modeSwitchState;
    }
    printTimer();
    if (RECEIVER.decode()) {
      changeRelayState();
      setTimer();
      RECEIVER.resume();
    } else {
      highlightTimerIndex();
    }
    startTimer();
    printRelayState();
  } else {
    modeLastState = modeSwitchState;
  }
}

// END ------------------- LOOP -------------------

// START ------------------- TEMPERATURE -------------------

void printTemp(float tempC) {
  int tempSwitchState = digitalRead(MESSURMENT_SWITCH_PIN);
  int modeSwitchState = digitalRead(MODE_SWITCH_PIN);
  if (tempSwitchState == HIGH) {
    printTemp(tempC, "C");
    if (modeSwitchState == HIGH) {
      printTargetTemp(targetTemperatureInC, "C");
    }
  } else {
    float tempF = readTempF(tempC);
    printTemp(tempF, "F");
    if (modeSwitchState == HIGH) {
      float targetTempF = readTempF(targetTemperatureInC);
      printTargetTemp(targetTempF, "F");
    }
  }
  if (modeSwitchState == HIGH) {
    setTempState(tempC);
  }
}

void setTempState(int tempC) {
  LCD.setCursor(18, 1);
  LCD.print(" ");
  if (tempC < targetTemperatureInC) {
    termoState = "HIGH";
    relayState = true;
    LCD.write((byte)262);
  } else if (tempC > targetTemperatureInC) {
    termoState = "LOW";
    relayState = false;
    LCD.write((byte)263);
  } else {
    termoState = "NONE";
    relayState = false;
    LCD.print(" ");
  }
}

float readTempC(int analogValue) {
  const float BETA = 3950;
  return 1 / (log(1 / (1023. / analogValue - 1)) / BETA + 1.0 / 298.15) - 273.15;
}

float readTempF(float tempC) {
  return (tempC - 32.0) / 1.8;
}

void sendTempAfterDelay(float tempC) {
  unsigned long currentTime = millis();
  if ((unsigned long)(currentTime - lastTime) >= timerDelay && WiFi.status() == WL_CONNECTED) {
    digitalWrite(LED_1_PIN, HIGH);

    String body = R"(
      {
        "data": [
          "attributes": {
            "mode": "%TERMOSTAT_MODE%",
            "deviceId": "%DEVICE_ID%",
            "deviceHash": "%DEVICE_HASH%"
            "devices": [
                {
                  "type": "relay", 
                  %TIMER_SECTION%
                  "state": "%RELAY_STATUS%"
                }, 
                {
                  "type": "termometer",
                  "state": "%TERMO_STATE%"
                  "temperature": [
                    {
                      "value": "%TEMPERATURE_C%",
                      "messurmentUnit": "C"
                    },
                    {
                      "value": "%TEMPERATURE_F%",
                      "messurmentUnit": "F"
                    }
                  ]
                }
            ]
          }
        ]
      }
    )";

    if (!modeLastState) {
      String timerSection = R"(,
            "timer": {
              "timerInSeconds": "%TIMER_IN_SECONDS%"
              "timeLeft": "%TIME_LEFT%"
            }
      )";

      body.replace("%TIMER_SECTION%", timerSection);
      body.replace("%TIMER_IN_SECONDS%", String(timerInS));
      body.replace("%TIME_LEFT%", String(timeLeftOnTimer()));
    } else {
      body.replace("%TIMER_SECTION%", "");
    }

    body.replace("%TERMOSTAT_MODE%", modeLastState ? "TARGET_TEMPERATURE" : "HEAT_BY_TIMER");
    body.replace("%RELAY_STATUS%", relayState ? "ON" : "OFF");
    body.replace("%TERMO_STATE%", termoState);
    body.replace("%DEVICE_ID%", deviceId);
    body.replace("%TEMPERATURE_C%", String(tempC));
    body.replace("%TEMPERATURE_F%", String(readTempF(tempC)));

    HTTPClient http;
    http.useHTTP10(true);

    http.addHeader("Content-Type", "application/json");
    http.addHeader("Api-Token", "sImp0aSI6InBqcTNtODViMW9iYiJ9");
    Serial.println("Request send to: http://myesp32.termostat:80/api/v1/tempermostat/" + deviceId);
    Serial.println("body: " + body);
    http.begin("http://myesp32.termostat:80/api/v1/tempermostat/" + deviceId);
    http.POST(body);
    Serial.println("Request end...");
    http.end();
    lastTime = currentTime;
    delay(250);
    digitalWrite(LED_1_PIN, LOW);
  }
}

void printTemp(float temp, String messurment)
{
  char tempString[6] = "     ";
  dtostrf(temp, 3, 1, tempString);

  LCD.setCursor(0, 0);
  LCD.print("CURRENT ");
  LCD.write((byte)256);
  LCD.print(" ");
  LCD.print(tempString);
  LCD.print((char)223);
  LCD.print(messurment);
  LCD.print("  ");
}

void printTargetTemp(float temp, String messurment)
{
  char tempString[6] = "     ";
  dtostrf(temp, 3, 1, tempString);

  LCD.setCursor(0, 1);
  LCD.print("TARGET  ");
  LCD.write((byte)256);
  LCD.print(" ");
  LCD.print(tempString);
  LCD.print((char)223);
  LCD.print(messurment);
  LCD.print("  ");
}

// END ------------------- TEMPERATURE -------------------

void readEncoder() {
  int dtValue = digitalRead(ENCODER_DT_PIN);
  if (dtValue == HIGH) {
    ++targetTemperatureInC;
  }
  if (dtValue == LOW) {
    --targetTemperatureInC;
  }
}

// START ------------------- LCD -------------------

void showIp() {
  int value = digitalRead((SHOW_IP_BUTTON_PIN));
  if (showIpButtonLastState != value) {
    showIpButtonLastState = value;
    if (value == HIGH) {
      LCD.setCursor(0, 0);
      LCD.clear();
      LCD.print("IP: ");
      LCD.print(WiFi.localIP());
      delay(5000);
      LCD.clear();
    }
  }
}

void showTime() {
  LCD.setCursor(0, 3);
  LCD.write((byte)257);
  LCD.write((byte)258);
  DateTime now = RTC.now();
  LCD.print(now.hour(), DEC);
  LCD.print(':');
  LCD.print(now.minute(), DEC);
  LCD.print(':');
  LCD.print(now.second(), DEC);
  LCD.print("  ");
}

void wifiIconAnimation() {
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("Connecting to WiFi...");
    LCD.setCursor(19, 0);
    LCD.write(byte(259));
    delay(200);
    LCD.setCursor(19, 0);
    LCD.write(byte(260));
    delay(200);
    LCD.setCursor(19, 0);
    LCD.write(byte(261));
    delay(200);
  } else {
    LCD.setCursor(19, 0);
    LCD.write(byte(261));
  }
}

void wifiIcons() {
  byte wifi1[8] = { B00000, B00000, B00000, B00000, B00000, B00100, B00000, B00000 };
  byte wifi2[8] = { B00000, B00000, B00100, B01010, B00000, B00100, B00000, B00000 };
  byte wifi3[8] = { B01110, B10001, B00100, B01010, B00000, B00100, B00000, B00000 };

  LCD.createChar(259, wifi1);
  LCD.createChar(260, wifi2);
  LCD.createChar(261, wifi3);
}

void termostatStateIcons() {
  byte stateHigh[] = { B00000,  B00100,  B01010,  B10001,  B00100,  B01010, B10001,  B00000 };
  byte stateLow[] = { B00000, B10001,  B01010,  B00100,  B10001,  B01010,  B00100,  B00000 };
  byte termometrIcon[8] = {B00100, B01010, B01010, B01110, B01110, B11111, B11111, B01110};

  LCD.createChar(262, stateHigh);
  LCD.createChar(263, stateLow);
  LCD.createChar(256, termometrIcon);
}

void clockIcons() {
  byte clockChar1[] = { B00111, B01000, B10010, B10010, B10011, B10000, B01000, B00111};
  byte clockChar2[] = {B10000, B01000, B00100, B00100, B10100, B00100, B01000, B10000};

  LCD.createChar(257, clockChar1);
  LCD.createChar(258, clockChar2);
}

// END ------------------- LCD -------------------

// START ------------------- RELAY -------------------

void changeRelayState() {
  if (RECEIVER.decodedIRData.command == 162) {
    relayState = !relayState;
    if (timerIsOn) {
      pauseTheTimer();
    }
  }
}

void printRelayState() {
  LCD.setCursor(17, 1);
  if (relayState) {
    digitalWrite(RELAY_PIN, HIGH);
    LCD.print(" ON");
  } else {
    digitalWrite(RELAY_PIN, LOW);
    LCD.print("OFF");
  }
}


void setRelayState() {
  if (relayState) {
    digitalWrite(RELAY_PIN, HIGH);
  } else {
    digitalWrite(RELAY_PIN, LOW);
  }
}

void setupRelayServer() {
  server.on("/setup", HTTP_POST, [](AsyncWebServerRequest * request) {
    String deviceIdParamName = "deviceId";
    String timerParamName = "timer";
    if (request->hasParam(deviceIdParamName)) {
      deviceId = request->getParam(deviceIdParamName)->value();
    }

    if (request->hasParam(timerParamName)) {
      timerInS = request->getParam(timerParamName)->value().toInt();
    }

    String response = R"(
      {
        "data": {
          "type": "relay",
          "timer": {
            "seconds": "%TIMER%"
          },
          "state": "%RELAY_STATUS"%
          "deviceId": "%DEVICE_ID%"
        }
      }
    )";

    response.replace("%TIMER%", String(timerInS));
    response.replace("%RELAY_STATUS%", relayState ? "ON" : "OFF");
    response.replace("%DEVICE_ID%", deviceId);

    request->send(200, "application/json", response);
  });
}

void toggleRelay() {
  server.on("/relay/toggle", HTTP_POST, [](AsyncWebServerRequest * request) {
    String response = R"(
      {
        "data": {
          "type": "relay",
          "deviceId": "%DEVICE_ID%",
          "state": "%RELAY_STATUS%"
          %TIMER_SECTION%
        }
      }
    )";

    String timerSection = R"(,
          "timer": {
            "timerInSeconds": "%TIMER_IN_SECONDS%"
            "timeLeft": "%TIME_LEFT%"
          }
    )";

    String timerParamName = "timer";
    if (request->hasParam(timerParamName) && request->getParam(timerParamName)->value() == "true") {
      response.replace("%TIMER_SECTION%", timerSection);
      startTheTimer();
    }

    relayState = !relayState;
    response.replace("%RELAY_STATUS%", relayState ? "ON" : "OFF");
    response.replace("%DEVICE_ID%", deviceId);
    response.replace("%TIMER%", String(timerInS));

    request->send(200, "application/json", response);
  });
}

// END ------------------- RELAY -------------------

// START ------------------- TIMER -------------------

void highlightTimerIndex() {
  if (!timerIsOn) {
    if (currentIndex < 0) currentIndex = 5;
    if (currentIndex > 5) currentIndex = 0;

    int cursorPosition = 8 + currentIndex;
    if (currentIndex > 1) cursorPosition++;
    if (currentIndex > 3) cursorPosition++;

    LCD.setCursor(cursorPosition, 1);
    LCD.print(" ");
    delay(100);
    LCD.setCursor(cursorPosition, 1);
    LCD.print(buffer[currentIndex]);
    delay(100);
  }
}

void startTimer() {
  unsigned long timeout = timerInS * 1000;
  unsigned long time = millis() - prevMillis;
  if (timerIsOn) {
    if (time >= timeout) {
      pauseTheTimer();
      strncpy(buffer, "000000", sizeof buffer);
      currentIndex = 5;
    }
    updateTimer();
  }
}

void setTimer() {
  if (currentIndex < 0) currentIndex = 5;
  if (currentIndex > 5) currentIndex = 0;
  if (!timerIsOn) {
    switch (RECEIVER.decodedIRData.command) {
      case 104:
        buffer[currentIndex] = '0';
        currentIndex--;
        break;
      case 48:
        buffer[currentIndex] = '1';
        currentIndex--;
        break;
      case 24:
        buffer[currentIndex] = '2';
        currentIndex--;
        break;
      case 122:
        buffer[currentIndex] = '3';
        currentIndex--;
        break;
      case 16:
        buffer[currentIndex] = '4';
        currentIndex--;
        break;
      case 56:
        buffer[currentIndex] = '5';
        currentIndex--;
        break;
      case 90:
        buffer[currentIndex] = '6';
        currentIndex--;
        break;
      case 66:
        buffer[currentIndex] = '7';
        currentIndex--;
        break;
      case 74:
        buffer[currentIndex] = '8';
        currentIndex--;
        break;
      case 82:
        buffer[currentIndex] = '9';
        currentIndex--;
        break;
      case 176:
        buffer[currentIndex] = '0';
        currentIndex++;
        break;
      case 144:
        currentIndex++;
        break;
      case 224:
        currentIndex--;
        break;
    }
    printTimer();
  }
  if (RECEIVER.decodedIRData.command == 168) {
    if (timerIsOn) {
      pauseTheTimer();
      printTimer();
    } else {
      int timerHours = String(buffer).substring(0, 2).toInt();
      int timerMinutes = String(buffer).substring(2, 4).toInt();
      int timerSeconds = String(buffer).substring(4, 6).toInt();

      timerInS = timerSeconds + timerMinutes * 60 + timerHours * 60 * 60;
      prevMillis = millis();
      startTheTimer();
    }
  }
}

void pauseTheTimer() {
  timerIsOn = false;
  relayState = false;
}

void startTheTimer() {
  timerIsOn = true;
  relayState = true;
}

void printTimer() {
  LCD.setCursor(0, 1);
  LCD.print("TIMER   ");

  String timerHours = String(buffer).substring(0, 2);
  String timerMinutes = String(buffer).substring(2, 4);
  if (timerMinutes.toInt() > 59) {
    buffer[2] = '5';
    buffer[3] = '9';
    timerMinutes = 59;
  }
  String timerSeconds = String(buffer).substring(4, 6);
  if (timerSeconds.toInt() > 59) {
    buffer[4] = '5';
    buffer[5] = '9';
    timerSeconds = 59;
  }

  LCD.print(timerHours);
  LCD.print(":");
  LCD.print(timerMinutes);
  LCD.print(":");
  LCD.print(timerSeconds);
  LCD.print(" ");
}

void updateTimer() {
  unsigned long newTimerInS = timeLeftOnTimer();
  int hoursLeft = newTimerInS / 60 / 60;
  newTimerInS = newTimerInS - (hoursLeft * 60 * 60);
  int minutesLeft = newTimerInS / 60;
  newTimerInS = newTimerInS - (minutesLeft * 60);
  int secondsLeft = newTimerInS;

  String bufferString;
  if (hoursLeft < 10) bufferString += String("0");
  bufferString += String(hoursLeft);
  if (minutesLeft < 10) bufferString += String("0");
  bufferString += String(minutesLeft);
  if (secondsLeft < 10) bufferString += String("0");
  bufferString += String(secondsLeft);

  bufferString.toCharArray(buffer, 7);
}

unsigned long timeLeftOnTimer() {
  unsigned long timeElapsed = (millis() - prevMillis) / 1000;
  return timerInS - timeElapsed;
}

// END ------------------- TIMER -------------------

// START ------------------- INIT -------------------

void initEncoder() {
  pinMode(ENCODER_CLK_PIN, INPUT);
  pinMode(ENCODER_DT_PIN, INPUT);

  attachInterrupt(digitalPinToInterrupt(ENCODER_CLK_PIN), readEncoder, FALLING);
}

void initOtherPins() {
  pinMode(TEMP_PIN, INPUT);
  pinMode(MESSURMENT_SWITCH_PIN, INPUT);
  pinMode(MODE_SWITCH_PIN, INPUT);
  pinMode(LED_1_PIN, OUTPUT);
  pinMode(SHOW_IP_BUTTON_PIN, INPUT_PULLUP);
}

void initLCD() {
  LCD.init();
  LCD.backlight();

  wifiIcons();
  termostatStateIcons();
  clockIcons();
}

void initRTC() {
  if (!RTC.begin()) {
    Serial.println("Couldn't find RTC");
    Serial.flush();
    abort();
  }
}

void initRelay() {
  pinMode(RELAY_PIN, OUTPUT);

  toggleRelay();
  setupRelayServer();

  RECEIVER.enableIRIn();

  Serial.begin(115200);
}

// END ------------------- INIT -------------------
$abcdeabcde151015202530fghijfghij
GND5VSDASCLSQWRTCDS1307+
NOCOMNCVCCGNDINLED1PWRRelay Module