/*
  ESP32 FreeRTOS multi-core sketch
  Core 0: WiFi + WebServer + LCD + Proximity + Conveyor
  Core 1: Button + Flow aggregation + Pump control
*/

#include <ArduinoJson.h>
#include <WiFi.h>
#include <WebServer.h>
#include <Preferences.h>
#include <LiquidCrystal_I2C.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"

// ========== HARDWARE PINS ==========
#define RELAY_PIN 13        // Pump / valve relay (activated by button/pump task)
#define BUTTON_PIN 19       // Physical start button (INPUT_PULLUP)
#define FLOW_SENSOR_PIN 35  // Flow sensor (ISR increments pulse count)
#define CONVEYOR_PIN 32     // Conveyor relay (ON/OFF via proximity on core0)
#define PROX_PIN 33         // Proximity sensor (INPUT_PULLUP; LOW = detected)
#define I2C_ADDR 0x27       // LCD I2C address

// ========== GLOBALS & PERFS ==========
LiquidCrystal_I2C lcd(I2C_ADDR, 16, 2);
Preferences prefs;
WebServer server(80);

const char* ssid = "ESP32_Auto";
const char* password = "12345678";
IPAddress local_IP(192, 168, 10, 1);
IPAddress gateway(192, 168, 10, 1);
IPAddress subnet(255, 255, 255, 0);

#define PREF_NAMESPACE "esp32auto"
#define KEY_MODE "mode"
#define KEY_TIME "time"
#define KEY_LITER "liter"
#define KEY_PPL "ppl"
#define KEY_CONV "conv"

// defaults
volatile int modeSetting = 1;
volatile unsigned long mode1Seconds = 5;
volatile float mode2Liters = 10.0;
volatile float pulsesPerLiter = 12; // our value 71.5125
volatile unsigned long conveyorSeconds = 3;

// flow variables
volatile unsigned long pulseCount = 0;  // incremented in ISR
float totalLiters = 0.0;                // aggregated once a second (protected by mutex)
unsigned long lastFlowCalc = 0;

// mutex to protect totalLiters
SemaphoreHandle_t xLitMutex = NULL;

// pump/sequence flags (Core1)
volatile bool pumpRunning = false;

// conveyor state (Core0)
volatile bool conveyorRunning = false;
unsigned long conveyorStartMillis = 0;
unsigned long conveyorDurationMs = 0;

// button debounce state (Core1)
const unsigned long BUTTON_DEBOUNCE_MS = 50;
unsigned long lastButtonDebounceMillis = 0;
int lastButtonStable = HIGH;

// proximity debounce (Core0)
const unsigned long PROX_DEBOUNCE_MS = 50;
unsigned long lastProxDebounceMillis = 0;
int lastProxStable = HIGH;

// HTML page (same UI as earlier)
const char* htmlPage = R"rawliteral(
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Brine Automation Dashboard</title>
  <style>
    body { font-family: Arial, sans-serif; background: #f4f7f9; margin: 0; padding: 0; }
    .container { max-width: 480px; width: 90%; margin: 40px auto; background: #fff; padding: 20px; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
    h2 { text-align: center; color: #0077cc; margin-bottom: 20px; }
    label { font-weight: bold; display: block; margin-top: 10px; }
    input, select, button { width: 100%; padding: 10px; margin-top: 6px; font-size: 16px; border-radius: 6px; border: 1px solid #ccc; box-sizing: border-box; }
    button { background: #0077cc; color: white; border: none; margin-top: 15px; cursor: pointer; transition: 0.3s; }
    button:hover { background: #005fa3; }
    #msg { text-align: center; margin-top: 10px; font-weight: bold; color: green; }
  </style>
</head>
<body>
  <div class="container">
    <h2>ESP32 Automation</h2>
    <p>Current Mode: <span id="curMode">Loading...</span></p>
    
    <label>Select Mode:</label>
    <select id="mode">
      <option value="1">Mode 1 - Time</option>
      <option value="2">Mode 2 - Volume</option>
      <option value="3">Mode 3 - Disabled</option>
    </select>

    <div id="mode1Box">
      <label>Seconds:</label>
      <input id="time" type="number" min="1" value="5">
    </div>

    <div id="mode2Box" style="display:none;">
      <label>Liters:</label>
      <input id="lit" type="number" min="1" value="10">
    </div>

    <label>Conveyor Time (sec):</label>
    <input id="conv" type="number" min="1" value="3">

    <button onclick="save()">Save</button>
    <p id="msg"></p>
  </div>

<script>
async function loadStatus(){
  let r = await fetch('/status');
  if(r.ok){
    let d = await r.json();
    document.getElementById('curMode').innerText = d.mode;
    document.getElementById('mode').value = d.mode;
    document.getElementById('time').value = d.time;
    document.getElementById('lit').value = d.liter;
    document.getElementById('conv').value = d.conv;
    switchModeUI();
  }
}

async function save(){
  let data = {
    mode: parseInt(document.getElementById('mode').value),
    time: parseInt(document.getElementById('time').value),
    liter: parseFloat(document.getElementById('lit').value),
    conv: parseInt(document.getElementById('conv').value)
  };
  let r = await fetch('/save', {
    method:'POST',
    headers:{'Content-Type':'application/json'},
    body:JSON.stringify(data)
  });
  if(r.ok){
    document.getElementById('msg').innerText = '✅ Settings Saved!';
    loadStatus();
  } else {
    document.getElementById('msg').innerText = '❌ Save failed';
  }
}

document.getElementById('mode').addEventListener('change', switchModeUI);
function switchModeUI(){
  let m = document.getElementById('mode').value;
  document.getElementById('mode1Box').style.display = (m=='1')?'block':'none';
  document.getElementById('mode2Box').style.display = (m=='2')?'block':'none';
}

loadStatus();
</script>
</body>
</html>
)rawliteral";

// ========== PREFERENCES ==========

void saveSettings() {
  prefs.begin(PREF_NAMESPACE, false);
  prefs.putInt(KEY_MODE, (int)modeSetting);
  prefs.putULong(KEY_TIME, mode1Seconds);
  prefs.putFloat(KEY_LITER, mode2Liters);
  prefs.putFloat(KEY_PPL, pulsesPerLiter);
  prefs.putULong(KEY_CONV, conveyorSeconds);
  prefs.end();
  Serial.printf("Saved settings: mode=%d time=%lu liter=%.2f conv=%lu ppl=%.2f\n",
                (int)modeSetting, mode1Seconds, mode2Liters, conveyorSeconds, pulsesPerLiter);
}

void loadSettings() {
  prefs.begin(PREF_NAMESPACE, true);
  modeSetting = prefs.getInt(KEY_MODE, 1);
  mode1Seconds = prefs.getULong(KEY_TIME, 5);
  mode2Liters = prefs.getFloat(KEY_LITER, 10.0);
  pulsesPerLiter = prefs.getFloat(KEY_PPL, 71.5125);
  conveyorSeconds = prefs.getULong(KEY_CONV, 3);
  prefs.end();
  Serial.printf("Loaded settings: mode=%d time=%lu liter=%.2f conv=%lu ppl=%.2f\n",
                (int)modeSetting, mode1Seconds, mode2Liters, conveyorSeconds, pulsesPerLiter);
}

// ========== FLOW ISR & AGGREGATION ==========
void IRAM_ATTR flowISR() {
  pulseCount++;
}

// Called periodically on Core1 to aggregate pulses into liters into totalLiters
void aggregateFlow() {
  unsigned long now = millis();
  if (now - lastFlowCalc >= 1000UL) {  // every 1 second
    noInterrupts();
    unsigned long pulses = pulseCount;
    pulseCount = 0;
    interrupts();

    float litersThisSec = (float)pulses / pulsesPerLiter;

    // protect totalLiters
    if (xLitMutex != NULL && xSemaphoreTake(xLitMutex, (TickType_t)10) == pdTRUE) {
      totalLiters += litersThisSec;
      xSemaphoreGive(xLitMutex);
    } else {
      // fallback (shouldn't happen often)
      totalLiters += litersThisSec;
    }

    Serial.printf("Flow agg: pulses=%lu +%.4f L total=%.4f\n", pulses, litersThisSec, totalLiters);
    lastFlowCalc = now;
  }
}

// ========== WEB HANDLERS ==========
void handleRoot() {
  server.send(200, "text/html", htmlPage);
}

void handleStatus() {
  float safeLit = 0.0;
  if (xLitMutex != NULL && xSemaphoreTake(xLitMutex, (TickType_t)10) == pdTRUE) {
    safeLit = totalLiters;
    xSemaphoreGive(xLitMutex);
  } else {
    safeLit = totalLiters;
  }
  String json = "{\"mode\":" + String((int)modeSetting)
                + ",\"time\":" + String(mode1Seconds)
                + ",\"liter\":" + String(mode2Liters)
                + ",\"conv\":" + String(conveyorSeconds)
                + ",\"flow\":" + String(safeLit)
                + "}";
  server.send(200, "application/json", json);
}

void handleSave() {
  String body = server.arg("plain");
  DynamicJsonDocument doc(512);
  DeserializationError err = deserializeJson(doc, body);
  if (err) {
    server.send(400, "application/json", "{\"ok\":false, \"error\":\"bad json\"}");
    return;
  }
  if (doc.containsKey("mode")) modeSetting = doc["mode"].as<int>();
  if (doc.containsKey("time")) mode1Seconds = doc["time"].as<unsigned long>();
  if (doc.containsKey("liter")) mode2Liters = doc["liter"].as<float>();
  if (doc.containsKey("conv")) conveyorSeconds = doc["conv"].as<unsigned long>();
  saveSettings();
  server.send(200, "application/json", "{\"ok\":true}");
}

// ========== TASKS ==========

// Core0: Web + LCD + Proximity & Conveyor
void TaskCore0(void* pv) {
  (void)pv;
  Serial.println("TaskCore0 starting on core " + String(xPortGetCoreID()));

  // setup AP and server
  WiFi.softAP(ssid, password);
  WiFi.softAPConfig(local_IP, gateway, subnet);
  Serial.print("AP IP: ");
  Serial.println(WiFi.softAPIP());

#if defined(WEB_SERVER_ENABLE_CORS)
  server.enableCORS(true);
#endif
  server.on("/", handleRoot);
  server.on("/status", handleStatus);
  server.on("/save", HTTP_POST, handleSave);
  server.begin();
  Serial.println("Web server started (Core0).");

  unsigned long lastLCD = 0;
  const unsigned long LCD_INTERVAL = 700;

  for (;;) {
    // handle web clients frequently
    server.handleClient();

    // Proximity debounce & control conveyor (INPUT_PULLUP; LOW = detected)
    int prox = digitalRead(PROX_PIN);
    if (prox != lastProxStable) {
      lastProxDebounceMillis = millis();
      lastProxStable = prox;
    } else {
      if ((millis() - lastProxDebounceMillis) > PROX_DEBOUNCE_MS) {
        // stable
        if (prox == LOW) {  // detection
          if (!conveyorRunning) {
            conveyorRunning = true;
            conveyorStartMillis = millis();
            conveyorDurationMs = conveyorSeconds * 1000UL;
            digitalWrite(CONVEYOR_PIN, HIGH);
            Serial.printf("Proximity detected -> Conveyor ON for %lu ms\n", conveyorDurationMs);
          }
        }
        // If prox is HIGH we do nothing special: conveyor will auto-stop after duration
      }
    }

    // conveyor timing (non-blocking)
    if (conveyorRunning) {
      if (millis() - conveyorStartMillis >= conveyorDurationMs) {
        conveyorRunning = false;
        digitalWrite(CONVEYOR_PIN, LOW);
        Serial.println("Conveyor auto-stopped (Core0).");
      }
    }

    // LCD update: read totalLiters safely
    if (millis() - lastLCD >= LCD_INTERVAL) {
      float safeLit = 0.0;
      if (xLitMutex != NULL && xSemaphoreTake(xLitMutex, (TickType_t)10) == pdTRUE) {
        safeLit = totalLiters;
        xSemaphoreGive(xLitMutex);
      } else {
        safeLit = totalLiters;
      }

      // Update LCD content
      lcd.clear();
      switch (modeSetting) {
        case 1:
          lcd.print("Mode1: Time ");
          lcd.setCursor(0, 1);
          lcd.print("Time:");
          lcd.print(mode1Seconds);
          lcd.print("s ");
          break;
        case 2:
          lcd.print("Mode2: Volume");
          lcd.setCursor(0, 1);
          lcd.print(safeLit, 2);
          lcd.print("/");
          lcd.print(mode2Liters);
          lcd.print("L");
          break;
        default:
          lcd.print("Mode3 Disabled");
          lcd.setCursor(0, 1);
          lcd.print("Conv:");
          lcd.print(conveyorSeconds);
          break;
      }

      lastLCD = millis();
    }

    vTaskDelay(pdMS_TO_TICKS(40));  // yield
  }
}

// Core1: Flow aggregation and Pump control + Button handling
TaskHandle_t pumpTaskHandle = NULL;

void PumpTask(void* pv) {
  (void)pv;
  Serial.println("PumpTask starting on core " + String(xPortGetCoreID()));

  for (;;) {
    // Wait to be notified by the Button task when a start is requested
    // ulTaskNotifyTake blocks until a notification arrives (or timeout)
    uint32_t notified = ulTaskNotifyTake(pdTRUE, portMAX_DELAY);  // clear on receive
    if (notified > 0) {
      // If mode disabled ignore
      if (modeSetting == 3) {
        Serial.println("PumpTask: mode disabled -> ignoring start");
        continue;
      }

      if (pumpRunning) {
        Serial.println("PumpTask: pump already running -> ignoring");
        continue;
      }

      // Start pump according to mode
      if (modeSetting == 1) {
        // Time based
        pumpRunning = true;
        digitalWrite(RELAY_PIN, HIGH);
        Serial.printf("PumpTask: time-based run for %lu s\n", mode1Seconds);
        unsigned long start = millis();
        while (millis() - start < mode1Seconds * 1000UL) {
          aggregateFlow();  // keep aggregating flow during pump
          vTaskDelay(pdMS_TO_TICKS(200));
        }
        digitalWrite(RELAY_PIN, LOW);
        pumpRunning = false;
        Serial.println("PumpTask: time run finished.");
      } else if (modeSetting == 2) {
        // Volume based
        pumpRunning = true;
        // Reset flow counters safely
        if (xLitMutex != NULL && xSemaphoreTake(xLitMutex, (TickType_t)10) == pdTRUE) {
          totalLiters = 0.0;
          xSemaphoreGive(xLitMutex);
        } else {
          totalLiters = 0.0;
        }
        noInterrupts();
        pulseCount = 0;
        interrupts();

        digitalWrite(RELAY_PIN, HIGH);
        unsigned long startMillis = millis();
        Serial.printf("PumpTask: volume-based target %.2f L\n", mode2Liters);
        while (true) {
          aggregateFlow();
          float safeLit = 0.0;
          if (xLitMutex != NULL && xSemaphoreTake(xLitMutex, (TickType_t)10) == pdTRUE) {
            safeLit = totalLiters;
            xSemaphoreGive(xLitMutex);
          } else {
            safeLit = totalLiters;
          }

          if (safeLit >= mode2Liters) {
            Serial.printf("PumpTask: target reached %.3f L\n", safeLit);
            break;
          }
          // safety timeout (10 min)
          if (millis() - startMillis > 10UL * 60UL * 1000UL) {
            Serial.println("PumpTask: timeout waiting for volume -> abort");
            break;
          }
          vTaskDelay(pdMS_TO_TICKS(250));
        }
        digitalWrite(RELAY_PIN, LOW);
        pumpRunning = false;
        Serial.println("PumpTask: volume run finished.");
      }  // modes
    }
  }
}

void ButtonTask(void* pv) {
  (void)pv;
  Serial.println("ButtonTask starting on core " + String(xPortGetCoreID()));
  int lastStable = HIGH;
  lastButtonStable = HIGH;
  for (;;) {
    int b = digitalRead(BUTTON_PIN);
    if (b != lastStable) {
      lastButtonDebounceMillis = millis();
      lastStable = b;
    } else {
      if ((millis() - lastButtonDebounceMillis) > BUTTON_DEBOUNCE_MS) {
        // falling edge detection
        static int prevState = HIGH;
        if (b == LOW && prevState == HIGH) {
          Serial.println("ButtonTask: button pressed -> notify pump");
          // Notify (unblock) PumpTask
          if (pumpTaskHandle != NULL) {
            xTaskNotifyGive(pumpTaskHandle);
          }
        }
        prevState = b;
      }
    }

    // also aggregate flow here regularly
    aggregateFlow();

    vTaskDelay(pdMS_TO_TICKS(40));
  }
}

// ========== SETUP & LOOP ==========
void setup() {
  Serial.begin(115200);
  delay(50);

  // pins
  pinMode(RELAY_PIN, OUTPUT);
  pinMode(CONVEYOR_PIN, OUTPUT);
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  pinMode(FLOW_SENSOR_PIN, INPUT_PULLUP);
  pinMode(PROX_PIN, INPUT_PULLUP);

  digitalWrite(RELAY_PIN, LOW);
  digitalWrite(CONVEYOR_PIN, LOW);

  attachInterrupt(digitalPinToInterrupt(FLOW_SENSOR_PIN), flowISR, RISING);

  // lcd
  lcd.init();
  lcd.backlight();

  // mutex
  xLitMutex = xSemaphoreCreateMutex();

  // load settings
  loadSettings();

  // create tasks pinned to cores
  BaseType_t r0 = xTaskCreatePinnedToCore(TaskCore0, "TaskCore0", 8192, NULL, 1, NULL, 0);              // Core 0
  BaseType_t rPump = xTaskCreatePinnedToCore(PumpTask, "PumpTask", 8192, NULL, 2, &pumpTaskHandle, 1);  // Core 1
  BaseType_t rButton = xTaskCreatePinnedToCore(ButtonTask, "ButtonTask", 4096, NULL, 2, NULL, 1);       // Core 1

  if (r0 != pdPASS || rPump != pdPASS || rButton != pdPASS) {
    Serial.println("Task creation failed!");
  } else {
    Serial.println("Tasks created successfully.");
  }
}

void loop() {
  // Empty - tasks do the work
  vTaskDelay(pdMS_TO_TICKS(1000));
}
