/**
 * MIT Emergency Ventilator Controller
 * 
 * MIT License:
 * 
 * Copyright (c) 2020 MIT
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

/**
 * e-vent.ino
 * Main Arduino file.
 */

#include "LiquidCrystal.h"
#include "src/thirdparty/RoboClaw/RoboClaw.h"
#include "cpp_utils.h"  // Redefines macros min, max, abs, etc. into proper functions,
                        // should be included after third-party code, before E-Vent includes
#include "Alarms.h"
#include "Buttons.h"
#include "Constants.h"
#include "Display.h"
#include "Input.h"
#include "Logging.h"
#include "Pressure.h"


using namespace input;
using namespace utils;


/////////////////////
// Initialize Vars //
/////////////////////

// Cycle parameters
unsigned long cycleCount = 0;
float tCycleTimer;     // Absolute time (s) at start of each breathing cycle
float tIn;             // Calculated time (s) since tCycleTimer for end of IN_STATE
float tHoldIn;         // Calculated time (s) since tCycleTimer for end of HOLD_IN_STATE
float tEx;             // Calculated time (s) since tCycleTimer for end of EX_STATE
float tPeriod;         // Calculated time (s) since tCycleTimer for end of cycle
float tPeriodActual;   // Actual time (s) since tCycleTimer at end of cycle (for logging)
float tLoopTimer;      // Absolute time (s) at start of each control loop iteration
float tLoopBuffer;     // Amount of time (s) left at end of each loop

// States
States state;
bool enteringState;
float tStateTimer;

// Roboclaw
RoboClaw roboclaw(&Serial3, 10000);
int motorCurrent, motorPosition = 0;

// LCD Screen
LiquidCrystal lcd(LCD_RS_PIN, LCD_EN_PIN, LCD_D4_PIN, dLCD_D5_PIN, LCD_D6_PIN, LCD_D7_PIN);
display::Display displ(&lcd, AC_MIN);

// Alarms
alarms::AlarmManager alarm(BEEPER_PIN, SNOOZE_PIN, LED_ALARM_PIN, &displ, &cycleCount);

// Pressure
Pressure pressureReader(PRESS_SENSE_PIN);

// Buttons
buttons::PressHoldButton offButton(OFF_PIN, 2000);
buttons::DebouncedButton confirmButton(CONFIRM_PIN);

// Logger
logging::Logger logger(true/*Serial*/, false/*SD*/, false/*labels*/, ",\t"/*delim*/);

// Knobs
struct Knobs {
  int volume();  // Tidal volume
  int bpm();     // Respiratory rate
  float ie();    // Inhale/exhale ratio
  float ac();    // Assist control trigger sensitivity
  SafeKnob<int> volume_ = SafeKnob<int>(&displ, display::VOLUME, CONFIRM_PIN, &alarm, VOL_RES);
  SafeKnob<int> bpm_ = SafeKnob<int>(&displ, display::BPM, CONFIRM_PIN, &alarm, BPM_RES);
  SafeKnob<float> ie_ = SafeKnob<float>(&displ, display::IE_RATIO, CONFIRM_PIN, &alarm, IE_RES);
  SafeKnob<float> ac_ = SafeKnob<float>(&displ, display::AC_TRIGGER, CONFIRM_PIN, &alarm, AC_RES);
  void begin();
  void update();
} knobs;

// Assist control
bool patientTriggered = false;


///////////////////////
// Declare Functions //
///////////////////////

// Set the current state in the state machine
void setState(States newState);

// Calculates the waveform parameters from the user inputs
void calculateWaveform();

// Check for errors and take appropriate action
void handleErrors();

// Set up logger variables
void setupLogger();


///////////////////
////// Setup //////
///////////////////

void setup() {
  Serial.begin(SERIAL_BAUD_RATE);
  while(!Serial);

  if (DEBUG) {
    setState(DEBUG_STATE);
  } else {
    setState(PREHOME_STATE);  // Initial state
  }

  // Wait for the roboclaw to boot up
  delay(1000);
  
  //Initialize
  pinMode(HOME_PIN, INPUT_PULLUP);  // Pull up the limit switch
  setupLogger();
  alarm.begin();
  displ.begin();
  offButton.begin();
  confirmButton.begin();
  knobs.begin();
  tCycleTimer = now();

  roboclaw.begin(ROBOCLAW_BAUD);
  roboclaw.SetM1MaxCurrent(ROBOCLAW_ADDR, ROBOCLAW_MAX_CURRENT);
  roboclaw.SetM1VelocityPID(ROBOCLAW_ADDR, VKP, VKI, VKD, QPPS);
  roboclaw.SetM1PositionPID(ROBOCLAW_ADDR, PKP, PKI, PKD, KI_MAX, DEADZONE, MIN_POS, MAX_POS);
  roboclaw.SetEncM1(ROBOCLAW_ADDR, 0);  // Zero the encoder
}

//////////////////
////// Loop //////
//////////////////

void loop() {
  if (DEBUG) {
    if (Serial.available() > 0) {
      setState((States) Serial.parseInt());
      while(Serial.available() > 0) Serial.read();
    }
  }

  // All States
  tLoopTimer = now();  // Start the loop timer
  logger.update();
  knobs.update();
  calculateWaveform();
  readEncoder(roboclaw, motorPosition);  // TODO handle invalid reading
  readMotorCurrent(roboclaw, motorCurrent);
  pressureReader.read();
  handleErrors();
  alarm.update();
  displ.update();
  offButton.update();

  if (offButton.wasHeld()) {
    goToPositionByDur(roboclaw, BAG_CLEAR_POS, motorPosition, MAX_EX_DURATION);
    setState(OFF_STATE);
    alarm.allOff();
  }
  
  // State Machine
  switch (state) {

    case DEBUG_STATE:
      // Stop motor
      roboclaw.ForwardM1(ROBOCLAW_ADDR, 0);
      break;

    case OFF_STATE: 
      alarm.turningOFF(now() - tStateTimer < TURNING_OFF_DURATION);
      if (confirmButton.is_LOW()) {
        setState(PREHOME_STATE);
        alarm.turningOFF(false);
      }
      break;
  
    case IN_STATE:
      if (enteringState) {
        enteringState = false;
        const float tNow = now();
        tPeriodActual = tNow - tCycleTimer;
        tCycleTimer = tNow;  // The cycle begins at the start of inspiration
        goToPositionByDur(roboclaw, volume2ticks(knobs.volume()), motorPosition, tIn);
        cycleCount++;
      }

      if (now() - tCycleTimer > tIn) {
        setState(HOLD_IN_STATE);
      }
      break;
  
    case HOLD_IN_STATE:
      if (enteringState) {
        enteringState = false;
      }
      if (now() - tCycleTimer > tHoldIn) {
        pressureReader.set_plateau();
        setState(EX_STATE);
      }
      break;
  
    case EX_STATE:
      if (enteringState) {
        enteringState = false;
        goToPositionByDur(roboclaw, BAG_CLEAR_POS, motorPosition, tEx - (now() - tCycleTimer));
      }

      if (abs(motorPosition - BAG_CLEAR_POS) < BAG_CLEAR_TOL) {
        setState(PEEP_PAUSE_STATE);
      }
      break;

    case PEEP_PAUSE_STATE:
      if (enteringState) {
        enteringState = false;
      }
      
      if (now() - tCycleTimer > tEx + MIN_PEEP_PAUSE) {
        pressureReader.set_peep();
        
        setState(HOLD_EX_STATE);
      }
      break;

    case HOLD_EX_STATE:
      if (enteringState) {
        enteringState = false;
      }

      // Check if patient triggers inhale
      patientTriggered = pressureReader.get() < (pressureReader.peep() - knobs.ac()) 
          && knobs.ac() > AC_MIN;

      if (patientTriggered || now() - tCycleTimer > tPeriod) {
        if (!patientTriggered) pressureReader.set_peep();  // Set peep again if time triggered
        pressureReader.set_peak_and_reset();
        displ.writePeakP(round(pressureReader.peak()));
        displ.writePEEP(round(pressureReader.peep()));
        displ.writePlateauP(round(pressureReader.plateau()));
        setState(IN_STATE);
      }
      break;

    case PREHOME_STATE:
      if (enteringState) {
        enteringState = false;
        roboclaw.BackwardM1(ROBOCLAW_ADDR, HOMING_VOLTS);
      }

      if (homeSwitchPressed()) {
        setState(HOMING_STATE);
      }
      break;

    case HOMING_STATE:
      if (enteringState) {
        enteringState = false;
        roboclaw.ForwardM1(ROBOCLAW_ADDR, HOMING_VOLTS);
      }
      
      if (!homeSwitchPressed()) {
        roboclaw.ForwardM1(ROBOCLAW_ADDR, 0);
        delay(HOMING_PAUSE * 1000);  // Wait for things to settle
        roboclaw.SetEncM1(ROBOCLAW_ADDR, 0);  // Zero the encoder
        setState(IN_STATE);
      }
      break;
  }

  // Add a delay if there's still time in the loop period
  tLoopBuffer = max(0, tLoopTimer + LOOP_PERIOD - now());
  delay(tLoopBuffer*1000.0);
}


/////////////////
// Definitions //
/////////////////

void Knobs::begin() {
  volume_.begin(&readVolume);
  bpm_.begin(&readBpm);
  ie_.begin(&readIeRatio);
  ac_.begin(&readAc);
}

void Knobs::update() {
  volume_.update();
  bpm_.update();
  ie_.update();
  ac_.update();
}

inline int Knobs::volume() { return volume_.read(); }
inline int Knobs::bpm() { return bpm_.read(); }
inline float Knobs::ie() { return ie_.read(); }
inline float Knobs::ac() { return ac_.read(); }

void setState(States newState) {
  enteringState = true;
  state = newState;
  tStateTimer = now();
}

void calculateWaveform() {
  tPeriod = 60.0 / knobs.bpm();  // seconds in each breathing cycle period
  tHoldIn = tPeriod / (1 + knobs.ie());
  tIn = tHoldIn - HOLD_IN_DURATION;
  tEx = min(tHoldIn + MAX_EX_DURATION, tPeriod - MIN_PEEP_PAUSE);
}

void handleErrors() {
  // Pressure alarms
  const bool over_pressure = pressureReader.get() >= MAX_PRESSURE;
  alarm.highPressure(over_pressure);
  if (over_pressure) setState(EX_STATE);

  // These pressure alarms only make sense after homing 
  if (enteringState && state == IN_STATE) {
    alarm.badPlateau(pressureReader.peak() - pressureReader.plateau() > MAX_RESIST_PRESSURE);
    alarm.lowPressure(pressureReader.plateau() < MIN_PLATEAU_PRESSURE);
    alarm.noTidalPres(pressureReader.peak() - pressureReader.peep() < MIN_TIDAL_PRESSURE);
  }

  // Check if desired volume was reached
  if (enteringState && state == EX_STATE) {
    alarm.unmetVolume(knobs.volume() - ticks2volume(motorPosition) > VOLUME_ERROR_THRESH);
  }

  // Check if maximum motor current was exceeded
  if (motorCurrent >= MAX_MOTOR_CURRENT) {
    setState(EX_STATE);
    alarm.overCurrent(true);
  } else {
    alarm.overCurrent(false);
  }

  // Check if we've gotten stuck in EX_STATE (mechanical cycle didn't finsih)
  alarm.mechanicalFailure(state == EX_STATE && now() - tCycleTimer > tPeriod + MECHANICAL_TIMEOUT);
}

void setupLogger() {
  logger.addVar("Time", &tLoopTimer);
  logger.addVar("CycleStart", &tCycleTimer);
  logger.addVar("State", (int*)&state);
  logger.addVar("Pos", &motorPosition, 3);
  logger.addVar("Pressure", &pressureReader.get(), 6);
  // logger.addVar("Period", &tPeriodActual);
  // logger.addVar("tLoopBuffer", &tLoopBuffer, 6, 4);
  // logger.addVar("Current", &motorCurrent, 3);
  // logger.addVar("Peep", &pressureReader.peep(), 6);
  // logger.addVar("HighPresAlarm", &alarm.getHighPressure());
  // begin called after all variables added to include them all in the header
  logger.begin(&Serial, SD_SELECT);
}