//////////////////////////////////////////////////////////////////////////////
/// @file main.cpp
/// @author Kai R. ()
/// @brief Exercise program for learning the "Mediator Design Pattern" using
///        the example of a traffic light control system with two traffic lights.
///
/// @date 2024-03-04
/// @version 0.9
///
//////////////////////////////////////////////////////////////////////////////

#include <Arduino.h>
#include <Streaming.h>
Print &cout {Serial};

//////////////////////////////////////////////////////////////////////////////
/// @brief Timerclass for millis()
///
//////////////////////////////////////////////////////////////////////////////
class Timer {
public:
  void start() { timeStamp = millis(); }
  bool operator()(const uint32_t duration) { return (millis() - timeStamp >= duration) ? true : false; }

private:
  uint32_t timeStamp {0};
};

namespace Millis {
using Millis = decltype(millis());
constexpr Millis operator"" _ms(unsigned long long t) { return t; }
constexpr Millis operator"" _sec(unsigned long long t) { return t * 1000_ms; }
constexpr Millis operator"" _min(unsigned long long t) { return t * 60_sec; }
constexpr Millis operator"" _sec(double long  t) { return t * 1000_ms; }
constexpr Millis operator"" _min(double long t) { return t * 60_sec; }
}  // namespace Millis

namespace TL {
constexpr uint8_t MAX_STATES {5};
constexpr uint8_t BIT_MASK {0x4};

//////////////////////////////////////////////////////////////////////////////
/// @brief Structure for saving the data required for traffic light control
///
//////////////////////////////////////////////////////////////////////////////
struct Data {
  const uint32_t duration[MAX_STATES];
  const uint8_t ledPin[3];
  const uint8_t mask[MAX_STATES];
};

enum class Role : uint8_t { primary, secondary };

enum class Phase : uint8_t { red, redWait, redYellow, green, yellow, wait };   // Definition of TLPhase
Phase &operator++(Phase &phase) {
  return phase = (phase > Phase::green) ? Phase::red : static_cast<Phase>(static_cast<uint8_t>(phase) + 1);
};

class Device;   // forward declaration

//////////////////////////////////////////////////////////////////////////////
/// @brief Base class for a mediator that enables communication
///        between the two traffic light classes. (Interface)
///
//////////////////////////////////////////////////////////////////////////////
class Mediator {
public:
  virtual void notify(Device *sender, uint8_t id) const = 0;
};

//////////////////////////////////////////////////////////////////////////////
/// @brief Base class for the traffic light class. It is used to register
///        at the mediator.
///
//////////////////////////////////////////////////////////////////////////////
class DeviceBase {
public:
  DeviceBase(Mediator *mediator = nullptr, uint8_t id = 0) : mediator(mediator), id(id) {}
  void setMediator(Mediator *mediator, uint8_t id_) {
    this->mediator = mediator;
    id = id_;
  }

protected:
  Mediator *mediator;
  uint8_t id;
};

//////////////////////////////////////////////////////////////////////////////
/// @brief The traffic light class required for LED control
///
//////////////////////////////////////////////////////////////////////////////
class Device : public DeviceBase {
public:
  Device(Timer &timer_, const Data &data_, Role role_) : timer {timer_}, data {data_}, role {role_} {}

  void begin(Phase p = Phase::redWait) {
    pNow = p;
    for (auto p : data.ledPin) { pinMode(p, OUTPUT); }
    if (role == Role::primary) { isBlocked = false; }
  }
  void switchLight();
  void check() {
    if (timer(data.duration[idx]) == true) { pNow = pNext; }
  }
  Phase phase() const { return pNow; }
  void block() { isBlocked = true; }
  void release() { isBlocked = false; }

private:
  Timer &timer;
  const Data &data;
  Role role;
  Phase pNow;
  Phase pNext;
  uint8_t idx;
  bool isBlocked {true};
};

void Device::switchLight() {
  if (isBlocked == false) {
    timer.start();
    idx = static_cast<uint8_t>(pNow);
    if (pNow == Phase::red) { mediator->notify(this, id); }   // Message to mediator
    pNext = ++pNow;
    pNow = Phase::wait;
    uint8_t i {0};
    for (auto p : data.ledPin) {
      ((data.mask[idx] << i) & BIT_MASK) ? digitalWrite(p, HIGH) : digitalWrite(p, LOW);
      ++i;
    }
  }
}

//////////////////////////////////////////////////////////////////////////////
/// @brief The mediator class, which enables communication between the two
///        traffic light classes.
///
//////////////////////////////////////////////////////////////////////////////
class DeviceMediator : public Mediator {
private:
  Device *device[2];

public:
  DeviceMediator(Device *dev0, Device *dev1) : device {dev0, dev1} {
    device[0]->setMediator(this, 0);
    device[1]->setMediator(this, 1);
  }

  //////////////////////////////////////////////////////////////////////////////
  /// @brief This controls when a traffic light can end its red phase (release()).
  ///        This happens reciprocally. When the program starts, the traffic light
  ///        with ID 1 is released first. It goes from the red phase to green
  ///        and then back to red. Then the traffic light with ID 0 is released
  ///        and the process is repeated.
  ///
  ///        In the redWait phase, both traffic lights remain red
  ///        for a while before the switching process starts at the
  ///        released traffic light.
  ///
  ///        If a red phase is not released, the traffic light remains permanently red.
  ///
  /// @param sender
  /// @param id
  //////////////////////////////////////////////////////////////////////////////
  void notify(Device *sender, uint8_t id) const override {
    switch (id) {
      case 0:
        cout << "unblock Traffic Light 1 (top), block Traffic Light 0 (bottom)\n";
        device[0]->block();   //
        device[1]->release();
        break;
      case 1:
        cout << "unblock Traffic Light 0 (bottom), block Traffic Light 1 (top)\n";
        device[0]->release();
        device[1]->block();
        break;
    }
  }
};
}   // namespace TL

//
// Global variables, objects
//
using namespace Millis;
Timer timer[2];   // Create timer objecte for the traffic lights

// Sequence: red, redWait, redYellow, green, yellow  (MAX_PHASE)
// Field1 -> Length of the light phase in milliseconds
// Field2 -> Pins to which the LEDs are connected
// Field3 -> Bit pattern for switching the LEDs
const TL::Data tlData[2] {
    {{15_sec, 5_sec, 1.5_sec, 15_sec, 3_sec}, {6, 5, 4},  {4, 4, 6, 1, 2}},
    {{15_sec, 5_sec, 1.5_sec, 15_sec, 3_sec}, {10, 9, 8}, {4, 4, 6, 1, 2}}
};

// Create traffic light objects. One traffic light must be the primary (only one!)
TL::Device tLightA {timer[0], tlData[0], TL::Role::primary};
TL::Device tLightB {timer[1], tlData[1], TL::Role::secondary};
// Create Mediator, register traffic lights
TL::DeviceMediator devMed {&tLightA, &tLightB};

//
// Function(s)
//

//////////////////////////////////////////////////////////////////////////////
/// @brief Status machine to control the traffic light circuit
///
/// @param tl traffic light object
//////////////////////////////////////////////////////////////////////////////
void fstm(TL::Device &tl) {
  switch (tl.phase()) {
    case TL::Phase::red: [[fallthrough]];
    case TL::Phase::redWait: [[fallthrough]];
    case TL::Phase::redYellow: [[fallthrough]];
    case TL::Phase::green: [[fallthrough]];
    case TL::Phase::yellow: tl.switchLight(); break;
    case TL::Phase::wait: tl.check(); break;
  }
}

//
// Main Program
//
void setup() {
  Serial.begin(115200);
  tLightA.begin(TL::Phase::red);
  tLightB.begin(TL::Phase::redWait);
}

void loop() {
  fstm(tLightA);
  fstm(tLightB);
}
red red-wait red-yellow yellow green
15 sec 5 sec 1,5 sec 3 sec 15 sec