/**
 *  Request protocol:
 *  register, [contactIP], [contactPort], [measurementType], [transition], [startTime], [deltaTime], [duration]
 *  
 *  measurementType: {soilmoisture, waterlevel} although it does not matter which is used at the moment, both will return both
 *  transition: {rising, falling, any}
 **/

#include <WiFiS3.h>
#include "arduinoSecrets.h"

#define DEBUG 1

// Arduino pins
#define LED_PIN 12
#define PUMP_PIN 13
#define WATER_PIN A0
#define SOIL_PIN A1

// Constants
#define wateringTime 1000
#define dryThreshold 300
#define waterLowThreshold 500

// FSM States
#define INIT 1
#define GET_REQUEST 2
#define PARSE_REQUEST 3
#define SENSE 4
#define THINK 5
#define ACT 6
#define SEND_RESPONSE 7
#define DONE 8

// Messages
#define MSG_NONE 1
#define MSG_REGISTER 2

#define BAUD_RATE 9600
#define CONNECT_ATTEMPTS 5
#define CONNECT_RETRY_DELAY 5000
#define NUM_TOKENS 8

// Event types (not used yet)
#define NONE 0
#define RISING 1
#define FALLING 2
#define ANY 3

// Measurements
#define MEAS_SOILMOISTURE 1
#define MEAS_WATERLEVEL 2

#define MSG_DELIMITERS " ,\n"
#define MSG_BUFFLEN 1026

int state = INIT;

// WiFi credentials
char ssid[] = SECRET_SSID;
char passwd[] = SECRET_PASS;

int status = WL_IDLE_STATUS;
int attempts = 0;
byte mac[6];
IPAddress ip;
char messageBuffer[MSG_BUFFLEN];

// Registration information
IPAddress contactIP;
int contactPort;
int contactMeasurement;
int contactEvent;
unsigned long contactStart;
unsigned long contactDelta;
unsigned long contactDuration;

float sensorVal = 0.0;
unsigned long eventStartTime = 0;
unsigned long eventStopTime = 0;
unsigned long lastFireTime = 0;

bool activeRegistration = false;
bool readyToFire = false;
int notificationCount = 0;

// Time
unsigned long beginOfTime = 0;
unsigned long currentTime = 0;
unsigned long oldTime = 0;

int evt = 0;
String valStr = "";

WiFiServer server(80);
WiFiClient connectionSocket = server.available();
WiFiClient notifier;

String msgFromClient = "";
char *tokens[NUM_TOKENS];

// Sensor variables
int soilMoisture = 0;
int waterLevel = 0;
bool waterLow = false;
bool soilDry = false;

bool clientRequest = false;

void setup() {
  Serial.begin(BAUD_RATE);
 
  // Initialize hardware pins
  pinMode(PUMP_PIN, OUTPUT);
  pinMode(LED_PIN, OUTPUT);
  pinMode(SOIL_PIN, INPUT);
  pinMode(WATER_PIN, INPUT);
  digitalWrite(PUMP_PIN, HIGH);
 
  while(!Serial) {
    ;
  }
  while((status != WL_CONNECTED) && (attempts < CONNECT_ATTEMPTS)) {
    Serial.print("Attempting to connect to WPA2 network, SSID: ");
    Serial.println(ssid);
   
    status = WiFi.begin(ssid, passwd);
    attempts++;
    delay(CONNECT_RETRY_DELAY);
   
    if (status == WL_CONNECTED) {
      Serial.println("Connected");
    } else {
      Serial.println("Couldn't get a wifi connection");
    }
  }
  server.begin();
  printWifiStatus();
  beginOfTime = millis();
}

void printWifiStatus() {
  Serial.print("SSID: ");
  Serial.println(WiFi.SSID());
 
  ip = WiFi.localIP();
  Serial.print("IP Address: ");
  Serial.println(ip);
 
  long rssi = WiFi.RSSI();
  Serial.print("signal strength (RSSI):");
  Serial.print(rssi);
  Serial.println(" dBm");
}

void fetchLine(WiFiClient connectionSocket) {
  Serial.println("fetchLine");
  Serial.print("received from client: ");
 
  bool lineDone = false;
  msgFromClient = "";
 
  while(connectionSocket.connected() && (!lineDone)) {
    if (connectionSocket.available() && (!lineDone)) {
      char c = connectionSocket.read();
      Serial.write(c);
     
      if (c != '\n') {
        msgFromClient += c;
      } else {
        lineDone = true;
        break;
      }
    }
  }
}

void parseLine(String msg) {
  int index = 0;
  char *ptr = NULL;
 
  if (DEBUG) {
    Serial.println("parseLine");
  }
 
  msg.toCharArray(messageBuffer, MSG_BUFFLEN);
  ptr = strtok(messageBuffer, MSG_DELIMITERS);
 
  while(ptr != NULL) {
    if (DEBUG) {
      Serial.print("token: ");
      Serial.println(ptr);
    }
   
    tokens[index] = ptr;
    index++;
    ptr = strtok(NULL, MSG_DELIMITERS);
  }
}

int processMessage(char** toks) {
  int msgType = MSG_NONE;
  int index = 0;
 
  String command(toks[index++]);
  String ipStr(toks[index++]);
  String port(toks[index++]);
  String measurement(toks[index++]);
  String eventStr(toks[index++]);
  String start(toks[index++]);
  String delta(toks[index++]);
  String durationStr(toks[index]);
 
  if (command.equals("register")) {
    if (DEBUG) {
      Serial.print("processMessage: command= ");
      Serial.println(command);
    }
   
    if (!contactIP.fromString(ipStr.c_str())) {
      return MSG_NONE;
    }
   
    contactPort = port.toInt();
    if (measurement.equals("soilmoisture")) {
      contactMeasurement = MEAS_SOILMOISTURE;
    } else if (measurement.equals("waterlevel")) {
      contactMeasurement = MEAS_WATERLEVEL;
    }
   
    if (eventStr.equals("rising")) {
      contactEvent = RISING;
    } else if (eventStr.equals("falling")) {
      contactEvent = FALLING;
    } else if (eventStr.equals("any")) {
      contactEvent = ANY;
    } else {
      contactEvent = NONE;
    }
   
    contactStart = (unsigned long) start.toDouble();
    contactDelta = (unsigned long) delta.toDouble();
    contactDuration = (unsigned long) durationStr.toDouble();
   
    eventStartTime = beginOfTime + contactStart;
    eventStopTime = beginOfTime + contactStart + contactDuration;
   
    msgType = MSG_REGISTER;
  }
 
  if (DEBUG) {
    Serial.print("processMessage: msgType= ");
    Serial.println(msgType);
  }
 
  return msgType;
}

float getSoilMoisture() {
  return soilMoisture;
}

float getWaterLevel() {
  return waterLevel;
}

void sensorMeasure() {
  if (DEBUG) {
    Serial.println("sensorMeasure:: reading sensors");
  }
  soilMoisture = analogRead(SOIL_PIN);
  waterLevel = analogRead(WATER_PIN);
 
  if (DEBUG) {
    Serial.print("Soil Moisture: ");
    Serial.println(soilMoisture);
    Serial.print("Water Level: ");
    Serial.println(waterLevel);
  }
}

void printStateVars() {
  if (DEBUG) {
    Serial.print("clientRequest= ");
    Serial.println(clientRequest);
    Serial.print("activeRegistration= ");
    Serial.println(activeRegistration);
    Serial.print("readyToFire= ");
    Serial.println(readyToFire);
  }
}

int checkEvent() {
  int theEvent = 0;
  float oldVal = sensorVal;
 
  if (contactMeasurement == MEAS_SOILMOISTURE) {
    sensorVal = getSoilMoisture();
  } else if (contactMeasurement == MEAS_WATERLEVEL) {
    sensorVal = getWaterLevel();
  }
 
  if (DEBUG) {
    Serial.print("checkEvent: oldVal= ");
    Serial.println(oldVal);
    Serial.print("checkEvent: sensorVal= ");
    Serial.println(sensorVal);
  }
 
  if (sensorVal > oldVal) {
    theEvent = RISING;
    if (DEBUG)
      Serial.println("checkEvent: set event to RISING");
  } else if (sensorVal < oldVal) {
    theEvent = FALLING;
    if (DEBUG)
      Serial.println("checkEvent: set event to FALLING");
  } else if ((sensorVal == oldVal) || (contactEvent == ANY)) {
    theEvent = ANY;
    if (DEBUG)
      Serial.println("checkEvent: set event to ANY");
  } else {
    theEvent = NONE;
    if (DEBUG)
      Serial.println("checkEvent: set event to NONE");
  }
 
  return theEvent;
}

bool isWaterLow(int waterLevel) {
  if (waterLevel < waterLowThreshold) {
    return true;
  }
  return false;
}

bool isSoilDry(int soilMoisture) {
  if (soilMoisture < dryThreshold) {
    return true;
  }
  return false;
}

void water() {
  Serial.println("Watering");
  digitalWrite(PUMP_PIN, LOW);
  delay(wateringTime);
  Serial.println("Done Watering");
  digitalWrite(PUMP_PIN, HIGH);
}

void lightOn() {
  digitalWrite(LED_PIN, HIGH);
}

void lightOff() {
  digitalWrite(LED_PIN, LOW);
}

void loop() {
  while (state != DONE) {
    switch (state) {
      case INIT:
        if (DEBUG) {
          Serial.println();
          Serial.println("INIT");
        }
        printStateVars();
        sensorMeasure();
        state = GET_REQUEST;
        break;

      case GET_REQUEST:
        if (DEBUG) {
          Serial.println();
          Serial.println("GET_REQUEST");
        }
        printStateVars();
       
        Serial.print("The IP Address is: ");
        Serial.println(ip);
       
        connectionSocket = server.available();
       
        if (connectionSocket && (!clientRequest)) {
          if (connectionSocket.available()) {
            fetchLine(connectionSocket);
            clientRequest = true;
            state = PARSE_REQUEST;
          } else {
            clientRequest = false;
            state = SENSE;
          }
        } else {
          state = SENSE;
        }
        break;

      case PARSE_REQUEST:
        if (DEBUG) {
          Serial.println();
          Serial.println("PARSE_REQUEST");
        }
       
        printStateVars();
       
        if (connectionSocket) {
          parseLine(msgFromClient);
         
          if (processMessage(tokens) == MSG_REGISTER) {
            notificationCount = 0;
            activeRegistration = true;
            connectionSocket.println("registered");
          } else {
            connectionSocket.println("unknown");
          }
         
          connectionSocket.stop();
        }
       
        state = SENSE;
        break;

      case SENSE:
        if (DEBUG) {
          Serial.println();
          Serial.println("SENSE");
        }
       
        printStateVars();
        sensorMeasure();
        state = THINK;
        break;

      case THINK:
        if (DEBUG) {
          Serial.println();
          Serial.println("THINK");
        }
       
        printStateVars();
       
        soilDry = isSoilDry(soilMoisture);
        waterLow = isWaterLow(waterLevel);

        evt = checkEvent();
        oldTime = currentTime;
        currentTime = millis();
       
        Serial.print("currentTime= ");
        Serial.println(currentTime);
       
        if (DEBUG) {
          Serial.print("eventStartTime= ");
          Serial.println(eventStartTime);
          Serial.print("eventStopTime= ");
          Serial.println(eventStopTime);
          Serial.print("lastFireTime + contactDelta= ");
          Serial.println(lastFireTime + contactDelta);
        }
       
        Serial.print("clientRequest= ");
        Serial.println(clientRequest);
       
        if (clientRequest) {
          if ((currentTime > eventStartTime) && (currentTime < eventStopTime)) {
            Serial.println("event fire within active period");
            activeRegistration = true;
           
            if (currentTime >= (lastFireTime + contactDelta)) {
              Serial.println("signaling event fire currentTime >= lastFireTime + contactDelta");
              readyToFire = true;
            } else {
              Serial.println("signaling hold event fire");
              readyToFire = false;
            }
          } else if (currentTime < eventStartTime) {
            activeRegistration = true;
            readyToFire = false;
          } else {
            clientRequest = false;
            activeRegistration = false;
            readyToFire = false;
            notificationCount = 0;
          }
         
          state = ACT;
        } else {
          state = GET_REQUEST;
        }
       
        break;

      case ACT:
        if (DEBUG) {
          Serial.println();
          Serial.println("ACT");
        }
       
        if (waterLow) {
          Serial.println("Water Low - Light On");
          lightOn();
        } else {
          lightOff();
          if (soilDry) {
            Serial.println("Soil Dry - Watering");
            water();
          }
        }
       
        if (clientRequest && activeRegistration && readyToFire) {
         
          // Convert to percentages
          float soilPercent = (soilMoisture / 1023.0) * 100.0;
          float waterPercent = (waterLevel / 1023.0) * 100.0;
         
          // Build message with both readings
          valStr = "soilmoisture,waterlevel,";
          valStr.concat(soilPercent);
          valStr.concat(",");
          valStr.concat(waterPercent);
         
          if (DEBUG) {
            Serial.print("Preparing to send both values: ");
            Serial.println(valStr);
          }
         
          state = SEND_RESPONSE;
        } else {
          state = SENSE;
        }
       
        break;

      case SEND_RESPONSE:
        if (DEBUG) {
          Serial.println();
          Serial.println("SEND_RESPONSE");
          Serial.print("activeRegistration= ");
          Serial.println(activeRegistration);
        }
       
        printStateVars();
       
        if (activeRegistration && readyToFire && clientRequest) {
          Serial.println("connecting to notification server...");
          if (notifier.connect(contactIP, contactPort)) {
            Serial.println("connected");
            notifier.print(valStr.c_str());
            Serial.print("sent message: ");
            Serial.println(valStr.c_str());
            notifier.stop();
            lastFireTime = millis();
            notificationCount++;
          } else {
            Serial.print("unable to connect to ");
            Serial.print(contactIP);
            Serial.print(":");
            Serial.println(contactPort);
            notifier.stop();
          }
        }
       
        state = SENSE;
        break;

      case DONE:
        if (DEBUG)
          Serial.println("DONE");
        break;
    }
  }
 
  state = GET_REQUEST;
}
