#include <DHT.h>
// #include <SoftwareSerial.h>
#include <LiquidCrystal.h>  // LCD display library
#include <SPI.h>            // Serial Peripheral Interface library
#include <SD.h>             // SD memory card library (SPI is required)

// Digital PINs
#define LCD_DISPLAY_TX_PIN           2 
#define DHTPIN_1 20     // what pin we're connected to
#define DHTTYPE_1 DHT22   // DHT 22  (AM2302)
#define DHTPIN_2 21     // what pin we're connected to
#define DHTTYPE_2 DHT22   // DHT 22  (AM2302)

// SD card
#define CS_PIN 10

// LCD
// LiquidCrystal lcd(19, 18, 17, 16, 15, 14);
LiquidCrystal lcd(18, 19, 14, 15, 16, 17);

#define actuatorPositionPin 3  // High - close
#define actuatorActionPin 2  // Low - moving
#define valvePositionPin 5 // High -open
#define valveActionPin 4 // Low - moving

#define LCD_CONNECTED false // set to to true when LCD is hooked up

const int upPin = 6;                            // Pin # connected to up arrow button
const int downPin = 7;                          // Pin # connected to down arrow button
const int selectPin = 8;                        // Pin # connected to select / start button

DHT dht1(DHTPIN_1, DHTTYPE_1);
DHT dht2(DHTPIN_2, DHTTYPE_2);

const int numDhts = 2;
const int numSoil = 4;

const long readInterval = 2000;
const unsigned long actuatorMoveTime = 10000;
const unsigned long valveMoveTime = 10000;

bool actuatorState = false; // false-closed, true-open
bool actuatorMoving = true; // false-idle, true-moving
bool valveState = false; // false-closed, true-open
bool valveMoving = true; // false-idle, true-moving
bool waterLowState = false; // false-closed, true-open (instant action)

const int MoistAirValue = 925;
const int MoistWaterValue = 300;

// temperature schedule
const float openTemp = 30.0; // SD value
const float closeTemp = 25.0; // SD value
const int controlTempSensorID = 0; // Indoor temp sensor ID 

// watering schedule
int waterFillRatio[5] = {20, 40, 60, 80, 100}; // in %, SD value
int waterTempRange[5] = {10, 15, 20, 25, 30}; // first value is lowest threshold to start watering, SD value
const int waterCycleSeconds = 60; //SD
const float minWaterMoist = 30; //SD
const float maxWaterMoist = 90; //SD

// timers and counters
unsigned long previousMillis = 0;
unsigned long actuatorTimeCounter = 0;
unsigned long valveTimeCounter = 0;
unsigned long previousCmdStart = 0; // water
// other
int screenNum = 1;
bool editMode = false;
int menuChoice = 0;

// sensor values, parsed and normalized
float airHum[numDhts];
float temp[numDhts];
float soilMoist[numSoil];


struct Command{
  bool actuatorCmdReceived; // true - command to move actuator
  bool actuatorCmd; // true - open
  int actuatorMode; // 0-CLOS, 1-OPEN, 2-AUTO
  int actuatorMenuChoice; // same as above, just holds cursor position
  bool waterHighCmdReceived;
  bool waterHighCmd;
  int waterHighMode;
  int waterHighMenuChoice;
  bool waterLowCmdReceived;
  bool waterLowCmd;
  int waterLowMode;
  int waterLowMenuChoice;
};

Command cmd;

void setup() {
  Serial.begin(115200); 
  lcd.begin(20,4);  
  dht1.begin();
  dht2.begin();

  cmd.actuatorCmdReceived = false;
  cmd.actuatorMode = 2;
  cmd.actuatorMenuChoice = 2;
  cmd.waterHighCmdReceived = false;
  cmd.waterHighMode = 2;
  cmd.waterHighMenuChoice = 2;
  cmd.waterLowCmdReceived = false;
  cmd.waterLowMode = 2;
  cmd.waterLowMenuChoice = 2;
  
  lcd.clear();
  P("Welcome!");
  lcd.setCursor(0, 2);
  P("Greenhouse Control.");
  lcd.setCursor(0, 3);
  P("version 1.0");

  lcd.clear();
  // lcd.print(MOSI); //11
  // lcd.print(MISO);  //13
  // lcd.print(SCK); //12
  // lcd.print(SS); //10

  //Setup SD card
  if (SD.begin(CS_PIN) == false) {
    lcd.clear();
    lcd.print(F("       ERROR:"));
    lcd.setCursor(0, 2);
    lcd.print(F("Can't setup SD card"));
    lcd.setCursor(0, 3);
    lcd.print(F("System was shut down"));
    
  } else {
    lcd.clear();
    lcd.print(F("SD card OK"));
    if (readConfig(SD, "/sd.txt")) {
      lcd.print(F("read OK"));
    } else {
      lcd.print(F("read Fail"));
    }
    lcd.setCursor(0, 1);
    for (int i=0; i<=4; i++) {
      lcd.print(waterFillRatio[i]);
      lcd.print(" ");
    }
    lcd.setCursor(0, 2);
    for (int j=0; j<=4; j++) {
      lcd.print(waterTempRange[j]);
      lcd.print(" ");
    }
  }
}

bool readConfig(fs::FS &fs, const char * path){
  int col = 1;          // Column number (of text file).  First column is one.
  int row = 1;          // Row number (of text file).  First row is one.
  char tempChar;        // Temporary character holder (read one at a time from file)
  char tempLine[21];    // Temporary character array holder
  int tempLoc = 0;      // Current location of next character to place in tempLine array
  // Clear the arrays
  memset(waterFillRatio, 0, sizeof(waterFillRatio));
  memset(waterTempRange, 0, sizeof(waterTempRange));
  File file = fs.open(path);
  if(!file){
    return false;
  }
  while(file.available() > 0){
    // Read a single character
    tempChar = file.read();
    if (tempChar == 13) { // Carriage return: Read another char (it is always a line feed / 10).  Add null to end.
      file.read();
      tempLine[tempLoc] = '\0';
    }
    else if (tempChar == 44) {  // Comma: Add null to end.
      tempLine[tempLoc] = '\0';      
    }
    else if (tempLoc <= 19) {   // Add it to the temp line array
      tempLine[tempLoc] = tempChar;
      tempLoc = tempLoc + 1; 
    }
    if (row == 1 && tempChar == 44) {
      waterFillRatio[col - 1] = atoi(tempLine);
    }
    if (row == 2 && tempChar == 44) {
      waterTempRange[col - 1] = atoi(tempLine);
    }
    if (tempChar == 10) {  // End of line.  Reset everything and goto next line
      memset(tempLine, 0, 21);
      tempLoc = 0;
      row = row + 1;
      col = 1;
    }

    if (tempChar == 44) {  // Comma.  Reset everything and goto 1st column
      memset(tempLine, 0, 21);
      tempLoc = 0;
      col = col + 1;
    }
    
  }
  file.close();
  return true;
}

void dataReaders()
{
  airHum[0] = dht1.readHumidity();
  temp[0]= dht1.readTemperature();
  airHum[1] = dht2.readHumidity();
  temp[1]= dht2.readTemperature();

  int a0 = analogRead(A0);
  int a1 = analogRead(A1);
  int a2 = analogRead(A2);
  int a3 = analogRead(A3);
  soilMoist[0] = map(a0, MoistAirValue, MoistWaterValue, 0, 100);
  soilMoist[1] = map(a1, MoistAirValue, MoistWaterValue, 0, 100);
  soilMoist[2] = map(a2, MoistAirValue, MoistWaterValue, 0, 100);
  soilMoist[3] = map(a3, MoistAirValue, MoistWaterValue, 0, 100); 
}

void controlActuators(Command *cmd)
{  
  unsigned long currentMillis = millis();
  // done moving
  if ((currentMillis - actuatorTimeCounter >= actuatorMoveTime) && actuatorMoving == true) {    
    actuatorTimeCounter = currentMillis;
    actuatorMoving = false;
    digitalWrite(actuatorActionPin, HIGH);
    Serial.println("Done actuator moving");
  }
  // start opening
  if (actuatorState == false && cmd->actuatorCmd == true && cmd->actuatorCmdReceived == true) {
    actuatorState = true; // open it
    actuatorMoving = true;
    digitalWrite(actuatorPositionPin, LOW);
    digitalWrite(actuatorActionPin, LOW);
    cmd->actuatorCmdReceived = false;
    actuatorTimeCounter = currentMillis;
    Serial.println("Starting opening actuators");
  // start closing  
  } else if (actuatorState == true && cmd->actuatorCmd == false && cmd->actuatorCmdReceived == true) {
    actuatorState = false; // close it
    actuatorMoving = true;
    digitalWrite(actuatorPositionPin, HIGH);
    digitalWrite(actuatorActionPin, LOW);
    actuatorTimeCounter = currentMillis;
    Serial.println("Starting closing actuators");
    cmd->actuatorCmdReceived = false;
  } 
}

void controlValve(Command *cmd)
{
  unsigned long currentMillis = millis();
  if ((currentMillis - valveTimeCounter >= valveMoveTime) && valveMoving == true) {    
    valveTimeCounter = currentMillis;
    valveMoving = false;
    digitalWrite(valveActionPin, HIGH);
    Serial.println("Done valve moving");
  }
  if (valveState == false && cmd->waterHighCmd == true && cmd->waterHighCmdReceived) {
    valveState = true; // open it
    valveMoving = true;
    digitalWrite(valvePositionPin, LOW);
    digitalWrite(valveActionPin, LOW);
    valveTimeCounter = currentMillis;
    cmd->waterHighCmdReceived = false;
    Serial.println("Starting opening valve");
  } else if (valveState == true && cmd->waterHighCmd == false && cmd->waterHighCmdReceived) {
    valveState = false; // close it
    valveMoving = true;
    digitalWrite(valvePositionPin, HIGH);
    digitalWrite(valveActionPin, LOW);
    valveTimeCounter = currentMillis;
    cmd->waterHighCmdReceived = false;
    Serial.println("Starting closing valve");
  } 
}

void setCommand(Command *cmd)
{
  unsigned long currentMillis = millis();
  // actuators
  if (cmd->actuatorMode == 2) {
    if (cmd->actuatorCmd == true && temp[controlTempSensorID] < closeTemp ) { // if open and below lower, close
      cmd->actuatorCmd = false;
      cmd->actuatorCmdReceived = true;
    } else if (cmd->actuatorCmd == false && temp[controlTempSensorID] > openTemp) { // if closed and above high, open
      cmd->actuatorCmd = true;
      cmd->actuatorCmdReceived = true;
    } 
    // actuator manual overrides
  } else if (cmd->actuatorMode == 0) {
    cmd->actuatorCmd = false;
    cmd->actuatorCmdReceived = true;
  } else if (cmd->actuatorMode == 1) {
    cmd->actuatorCmd = true;
    cmd->actuatorCmdReceived = true;
  }
  // watering
  // manual modes
  if (cmd->waterHighMode == 0) {
    cmd->waterHighCmd = false;
    cmd->waterHighCmdReceived = true;
    return;
  }
  if (cmd->waterHighMode == 1) {
    cmd->waterHighCmd = true;
    cmd->waterHighCmdReceived = true;
    return;
  }
  // automode:
  // check the hard limits
  if (soilMoist[0] < minWaterMoist) {
    cmd->waterHighCmd = true;
    cmd->waterHighCmdReceived = true;
    return;
  }
  if (soilMoist[0] > maxWaterMoist) {
    cmd->waterHighCmd = false;
    cmd->waterHighCmdReceived = true;
    return;
  }
  // min temp to water
  if (temp[0] < waterTempRange[0]) {
    cmd->waterHighCmd = false;
    cmd->waterHighCmdReceived = true;
    return;
  }

  // find the temp range
  for (int i= 0; i<=sizeof(waterTempRange); i++) {
    if (temp[0] < waterTempRange[i+1]) {
      //float ratio =  waterTempRange[i]; 
      if (currentMillis - previousCmdStart >= waterCycleSeconds) {
        previousCmdStart = currentMillis;        
      }
      if ((currentMillis - previousCmdStart)*100/waterCycleSeconds <  waterTempRange[i]) { // offset / cycle < ratio
        cmd->waterHighCmd = true;
      } else {
        cmd->waterHighCmd = false;
      }
      cmd->waterHighCmdReceived = true;
      return;      
    }
  }  
  // handle what happens here?
}

String printSign(float number) {
  String sign = "+";
  if (number < 0) {
    sign = "";
  }
  return sign;  
}

String printOpenClosed(bool state, bool move) {
  if (state == true && move == false) {
    return "OPEN";
  } else if (state == false && move == false) {
    return "CLOSED";
  } else if (state == true && move == true) {
    return "OPENING";
  } else {
    return "CLOSING";
  }
}

String printMoveProgress(unsigned long elapsedT, unsigned long totalT) {
  unsigned long progress = elapsedT*100/totalT;
  return String(progress) + "%";
}

void printMenu(int currentSelected) {
  String cursor = "*";
  if (editMode == true) {
    cursor = "?";
  }
  if (currentSelected == 0) {
    P(cursor + "CLOS OPEN AUTO");
  } else if (currentSelected == 1) {
    P("CLOS " + cursor + "OPEN AUTO");
  } else if (currentSelected == 2) {
    P("CLOS OPEN " + cursor + "AUTO");
  }
}

// generic string print 
void P(String data) {
    lcd.print(data);
}

// generic float print with decimal places
void PN(float value, int precision) {
    lcd.print(value, precision);
}

// returns number of chars occupied by value and precision
int NumChars(float value, int precision) {
  int d = 0;
  if (value <= -100 ) {
    d = 4;    
  } else if (value <= 10) {
    d = 3;
  } else if (value < 0) {
    d = 2;
  } else if (value < 10) {
    d = 1;
  } else if (value < 100) {
    d = 2;
  } else {
    d = 3;
  }
  if (precision == 0) {
    return d;
  } else {
    return d+precision+1;
  }
}

void updateLCD() {
  lcd.clear();
  // indoor sensors
  if (screenNum == 1) {
    P("Inside:");
    lcd.setCursor(0, 1);
    P("Temp: ");
    lcd.setCursor(18-NumChars(temp[0], 1), 1);
    P(printSign(temp[0]));
    PN(temp[0], 1);
    P("C");
    lcd.setCursor(0, 2);
    P("Humidity:");
    lcd.setCursor(18-NumChars(airHum[0], 0), 2);
    PN(airHum[0], 0);
    P("%");
    lcd.setCursor(0, 3);
    P("Soil: ");
    lcd.setCursor(18-NumChars(soilMoist[0], 0), 3);
    PN(soilMoist[0], 0);
    P("%");
  }
  // indoor water control
  if (screenNum == 2) {
    P("IN: Water: ");
    P(printOpenClosed(waterLowState, false));
    lcd.setCursor(0, 1);
    printMenu(cmd.waterLowMenuChoice);   
  }
  // indoor window control
  if (screenNum == 3) {
    P("IN: Window: ");
    lcd.setCursor(0, 1);
    P(printOpenClosed(actuatorState, actuatorMoving));
    P(" ");
    if (actuatorMoving) {
      P(printMoveProgress(millis()-actuatorTimeCounter, actuatorMoveTime));
    }   
    lcd.setCursor(0, 2);
    printMenu(cmd.actuatorMenuChoice); 
  }
  // outdoor sensors
  if (screenNum == 4) {
    P("OUT: ");
    P(printSign(temp[1]));
    PN(temp[1], 1);
    P(" ");
    PN(airHum[1], 0);
    P("%");
    lcd.setCursor(0, 1);
    P("Soil: ");
    PN(soilMoist[1], 0);
    P("%");
  }
  // outdoor water control
  if (screenNum == 5) {
    P("OUT: Water: ");
    lcd.setCursor(0, 1);
    P(printOpenClosed(valveState, valveMoving));
    P(" ");
    if (valveMoving) {
      P(printMoveProgress(millis()-valveTimeCounter, valveMoveTime));
    }
    lcd.setCursor(0, 2);
    printMenu(cmd.waterHighMenuChoice); 
  }
}

void btnBounce(int btnPin) {
  while (digitalRead(btnPin) == LOW);
    delay(200);
}


void loop() {
  unsigned long currentMillis = millis();
  if (currentMillis - previousMillis >= readInterval) {
      previousMillis = currentMillis;
      dataReaders();
      setCommand(&cmd);
      controlActuators(&cmd);
      controlValve(&cmd);
      updateLCD();
  }
  // Up arrow button
  if (digitalRead(upPin) == LOW) {
    if (screenNum > 1 && editMode == false) {
      screenNum = screenNum - 1;
    }
    if (editMode == true) {
      if (screenNum == 2) {
        cmd.waterLowMenuChoice--;
        if (cmd.waterLowMenuChoice < 0) {
          cmd.waterLowMenuChoice = 2;
        }
      } else if (screenNum == 3) {
        cmd.actuatorMenuChoice--;
        if (cmd.actuatorMenuChoice < 0) {
          cmd.actuatorMenuChoice = 2;
        }
      } else if (screenNum == 5) {
        cmd.waterHighMenuChoice--;
        if (cmd.waterHighMenuChoice < 0) {
          cmd.waterHighMenuChoice = 2;
        }
      }
    }    
    updateLCD();
    btnBounce(upPin); 
  }
  // Down arrow button
  if (digitalRead(downPin) == LOW) {
    if (screenNum < 5 && editMode == false) {
      screenNum = screenNum + 1;
    }
    if (editMode == true) {
      if (screenNum == 2) {
        cmd.waterLowMenuChoice++;
        if (cmd.waterLowMenuChoice >= 3) {
          cmd.waterLowMenuChoice = 0;
        }
      } else if (screenNum == 3) {
        cmd.actuatorMenuChoice++;
        if (cmd.actuatorMenuChoice >= 3) {
          cmd.actuatorMenuChoice = 0;
        }
      } else if (screenNum == 5) {
        cmd.waterHighMenuChoice++;
        if (cmd.waterHighMenuChoice >= 3) {
          cmd.waterHighMenuChoice = 0;
        }
      }
    }
    updateLCD();
    btnBounce(downPin); 
  }
  // Select button
  if (digitalRead(selectPin) == LOW) {
    if (screenNum == 2 || screenNum == 3 || screenNum == 5) {
      if (editMode == false) {
        editMode = true;
      } else {
        editMode = false;
        if (screenNum == 2) {
          cmd.waterLowMode = cmd.waterLowMenuChoice;
          cmd.waterLowCmdReceived = true;
        } else if (screenNum == 3) {
          cmd.actuatorMode = cmd.actuatorMenuChoice;
          cmd.actuatorCmdReceived = true;
        } else if(screenNum == 5) {
          cmd.waterHighMode = cmd.waterHighMenuChoice;
          cmd.waterHighCmdReceived = true;
        }
      }
      updateLCD();
      btnBounce(selectPin);   
    }
  }
}