/*
   react on "alarm"
   increase warnings by time
   use one common logic to blink a LED, to honk a horn, to beep an buzzer, to blink a character on the LCD
   https://forum.arduino.cc/t/faster-digitalwrite/1097554/55
   2023-03-05 by noiasca
   don't deleted: will be used on HP
*/

#include <Wire.h>
#include <LiquidCrystal_I2C.h>

LiquidCrystal_I2C lcd(0x27, 20, 4);

const uint8_t alarmStartPin = A0; // "an external event"
const uint8_t alarmStopPin = A1;  // "the right code"
const uint8_t ignitionPin = 3;    // GPIO to kill the ignition
uint32_t alarmPreviousMillis = 0;
uint32_t alarmState = 0;

// a generic blink class to toggle a blinking effect on an elemet
class Blink  {
  protected :
    uint32_t previousMillis = 0; // time management for "blinking"
    uint16_t interval = 500;     // default interval
    uint8_t state = 0;           // 0 off, 1 OnPulsephase, 2 offPulsePhase
    bool preStop = false;        // flag to stop after next active phase

  public:
    Buzzer () {}

    virtual void hwWrite(uint8_t level);  // a low level hwWrite must be implemented in the derived class

    void setInterval(uint16_t interval) {
      this->interval = interval;
    }

    void off() {
      hwWrite(LOW);
      state = 0;
    }

    //let the last beep run out
    void offSmooth() {
      if (state == 2)
        off();
      else
        preStop = true;
    }

    void on() {
      hwWrite(HIGH);
      previousMillis = millis();
      state = 1;
      preStop = false;
    }

    void on(uint16_t interval) {
      setInterval(interval);
      on();
    } 

    void update(uint32_t currentMillis = millis()) {
      if (state && currentMillis - previousMillis > interval)  {
        previousMillis = currentMillis;
        if (state == 1) {
          hwWrite(LOW);                        // switch off - abstracted from the hardware
          if (preStop) off(); else state = 2;  
        }
        else {
          hwWrite(HIGH);                       // switch on - abstracted from the hardware
          state = 1;
        }
      }
    }
};

class DiscretePin : public Blink {
  protected:
    const uint8_t pin;  // the GPIO to be switched on/off 
  public:
    DiscretePin(uint8_t pin) : pin(pin) {}

    void begin() {                  // discrete output pins need a pinMode in setup()
      pinMode(pin, OUTPUT);
    }

    void hwWrite(uint8_t level) override {
      digitalWrite(pin, level);     // just a simple pass through for the digitalWrite 
    }
};

class Buzzer : public Blink {
  protected :
    const uint8_t pin;            // the GPIO for a buzzer
    const uint16_t freq;          // the frequency to beep
  public:
    Buzzer (uint8_t pin, uint16_t freq = 600) : pin(pin), freq(freq) {}

    void hwWrite(uint8_t level) override {
      if (level == LOW)
        noTone(pin);
      else
        tone(pin, freq);
    }
};

// an indicator on the display
// for usual you would also handover the reference to a LCD object, but lets keep it simple
class Indicator : public Blink {
  protected :
    const uint16_t col;          // on which column
    const uint8_t row;           // on which row
    char onChar = '-';           // a sign/char to be shown
    const char offChar = ' ';    // a sign/char if not shown (idle)
  public:
    Indicator (uint8_t col, uint8_t row, char onChar) : col(col), row(row), onChar(onChar) {}

    // output one character to the LCD on the designated place
    void hwWrite(uint8_t level) override {
      lcd.setCursor(col, row); // col, row
      if (level == LOW)
        lcd.write(offChar);
      else
        lcd.write(onChar);
    }

    // modifiy the on character
    void setOnChar(char onChar) {
      this->onChar = onChar;
    }
};

DiscretePin alarmLed(2);              // pin
DiscretePin horn(8);                  // pin
Buzzer buzzer(7);                     // pin, frequeny
Indicator alarmIndicator(15, 1, 'S'); // col, row, character

// just to show a "parallel" task
void updateDisplay(uint32_t currentMillis = millis()) {
  static uint32_t previousMillis = 0;
  if (currentMillis - previousMillis > 100) {
    previousMillis = currentMillis;
    lcd.setCursor(0, 0);
    lcd.print((millis() / 1000.0), 1);
  }
}

// ask the key pad if the right code was entered
bool disarmAlarm () {
  if (digitalRead(alarmStopPin) == LOW) return true;
  return false;
}

// action when alarm should be stopped
void alarmStop() {
  alarmLed.off();                 // optional offSmooth();
  buzzer.offSmooth();
  horn.off();                     // optional offSmooth();
  alarmIndicator.off();
  digitalWrite(ignitionPin, LOW); // release ignition killer
}

void alarmFSM(uint32_t currentMillis = millis()) {
  switch (alarmState) {
    case 0 :
      if (digitalRead(alarmStartPin) == LOW) {
        alarmState++;
        Serial.print(F("silent alarmState=")); Serial.println(alarmState);
        alarmPreviousMillis = currentMillis;
        alarmLed.on(1000);
        alarmIndicator.setOnChar('s');
        alarmIndicator.on(400);
      }
      break;
    case 1: // 0..15
      if (disarmAlarm()) {
        Serial.print(F("alarmOff")); Serial.println(alarmState);
        alarmStop();
        alarmState = 0;
      }
      if (currentMillis - alarmPreviousMillis > 15000UL) {
        alarmState++;
        Serial.print(F("warn alarmState=")); Serial.println(alarmState);
        alarmLed.on(500);
        buzzer.on(1000);
        alarmIndicator.setOnChar('w');
      }
      break;
    case 2: // 15..25
      if (disarmAlarm()) {
        Serial.print(F("alarmOff")); Serial.println(alarmState);
        alarmStop();
        alarmState = 0;
      }
      if (currentMillis - alarmPreviousMillis > 25000UL) {
        alarmState++;
        Serial.print(F("urgent alarmState=")); Serial.println(alarmState);
        alarmLed.on(300);
        buzzer.on(500);
        alarmIndicator.setOnChar('u');
      }
      break;
    case 3: // 25..30
      if (disarmAlarm()) {
        Serial.print(F("alarmOff in ")); Serial.println(alarmState);
        alarmStop();
        alarmState = 0;
      }
      if (currentMillis - alarmPreviousMillis > 30000UL) {
        alarmState++;
        Serial.print(F("cut off alarmState=")); Serial.println(alarmState);
        alarmLed.on(200);
        buzzer.offSmooth();
        horn.on();                       // instead
        alarmIndicator.setOnChar('x');
        digitalWrite(ignitionPin, HIGH); // ignition kill
      }
      break;
    case 4: // after
      if (disarmAlarm()) {
        Serial.print(F("alarmOff in ")); Serial.println(alarmState);
        alarmStop();
        alarmState = 0;
      }
      break;
  }
}

void setup() {
  Serial.begin(115200);
  Serial.println(F("Ready"));
  lcd.init();
  lcd.backlight();
  lcd.setCursor(9, 0);     // col, row
  lcd.print(("Seconds"));
  alarmLed.begin();
  horn.begin();
  pinMode(ignitionPin, OUTPUT);
  pinMode(alarmStartPin, INPUT_PULLUP);
  pinMode(alarmStopPin, INPUT_PULLUP);
  //Serial.println(millis()); // that's just because wokwi is different to a real arduino.
}

void loop() {
  uint32_t currentMillis = millis();  // instead of multiple calls of millis() we use the same timestamp for one loop iteration
  alarmFSM(currentMillis);
  updateDisplay(currentMillis);
  // each object needs a tick/timeslice to be able to check if there is something to do
  alarmLed.update(currentMillis);
  buzzer.update(currentMillis);
  horn.update(currentMillis);
  alarmIndicator.update(currentMillis);
}
// eof