#include <digitalWriteFast.h>

////////////////////////////////////////////////////
//  Global Definitions
////////////////////////////////////////////////////

//
// Class for a debounced push button input.
//
class Button {
public:
  Button(const uint8_t pin, bool as = LOW) : pin {pin}, activeState {as} {}

  void begin() {
    buttonState = !activeState;
    lastButtonState = activeState;
    pinMode(pin, (activeState) ? INPUT : INPUT_PULLUP);
  }

  bool tick() {
    bool reading = digitalReadFast(pin);
    if (reading != lastButtonState) { lastDebounceTime_ms = millis(); }

    if ((millis() - lastDebounceTime_ms) > debounceDelay_ms) {
      if (reading != buttonState) {
        buttonState = reading;
        if (buttonState == HIGH) { return true; }   // Button is pressed even after the debounce time has expired
      }
    }
    lastButtonState = reading;
    return false;
  }

private:
  const uint8_t pin;
  bool activeState;
  bool buttonState {HIGH};
  bool lastButtonState {LOW};
  uint32_t lastDebounceTime_ms {0};
  uint32_t debounceDelay_ms {50};
};

//
// Timer for non-blocking delays
//
class Timer {
public:
  void start() { timeStamp = millis(); }

  bool operator()(const unsigned long duration) const { return (millis() - timeStamp >= duration) ? true : false; }

private:
  unsigned long timeStamp {0};
};

//
// LED Pin Data
//
struct Led {
  const uint8_t pinNr;
};

//
// A dice class.
//
class Dice {
public:
  template <size_t MAX> Dice(const Led (&pLeds)[MAX]) : pLeds {pLeds}, ledCount {MAX} {}

  void begin() {
    for (uint8_t i = 0; i < ledCount; ++i) { pinMode(pLeds[i].pinNr, OUTPUT); }
  }
  bool roll();
  void rollReset() { rollCounter = 0; }
  void lightLeds(uint8_t pips) {
    for (uint8_t i = 0; i < NIBBLE; ++i) { digitalWriteFast(pLeds[i].pinNr, ((BITPATTERN[pips] >> i) & 0x01)); }
  }
  uint8_t getRollPips() const { return pips; }
  uint8_t getBitPattern() const { return BITPATTERN[pips]; }

private:
  uint8_t getRandom(uint8_t, uint8_t);

  const Led *pLeds;
  const uint8_t ledCount;
  uint8_t pips;

  Timer delay_ms;
  uint8_t rollCounter {0};
  uint8_t rollIterations {20};
  uint16_t wait_ms;
  bool isWaiting {true};

  static constexpr uint8_t NIBBLE {4};
  static constexpr uint8_t MIN_PIPS {1};
  static constexpr uint8_t MAX_PIPS {6};
  static const uint8_t BITPATTERN[7];
  // static constexpr uint8_t BITPATTERN[7] {0x00, 0x01, 0x02, 0x03, 0x06, 0x07, 0x0E}; // Version > C++11 necessary
};

bool Dice::roll() {
  if (rollCounter > rollIterations) { rollCounter = rollIterations; }
  if (isWaiting) {
    if (delay_ms(wait_ms)) {
      pips = random(MIN_PIPS, MAX_PIPS + 1);
      lightLeds(pips);
      isWaiting = false;
      rollCounter++;
    }
  } else {
    delay_ms.start();
    wait_ms = random(1, 200);
    isWaiting = true;
  }
  return (rollCounter == rollIterations) ? true : false;
}

//
// Create a random number.
//
uint8_t Dice::getRandom(uint8_t from, uint8_t to) {
  uint16_t seed = analogRead(7);
  seed += analogRead(1);
  seed *= analogRead(2);
  seed *= analogRead(3);
  seed *= analogRead(4);
  randomSeed(seed);
  return random(from, to);
}

// Bit sequences - sequence in which the pins must be controlled
// in order to display the corresponding number of pips via LED.
// 0   7 L 6 L 5 L 4 L    0000 All off
// 1 = 7 L 6 L 5 L 4 H    0001
// 2 = 7 L 6 L 5 H 4 L    0010
// 3 = 7 L 6 L 5 H 4 H    0011
// 4 = 7 L 6 H 5 H 4 L    0110
// 5 = 7 L 6 H 5 H 4 H    0111
// 6 = 7 H 6 H 5 H 4 L    1110
//                                   0     1     2     3     4     5     6
const uint8_t Dice::BITPATTERN[7] {0x00, 0x01, 0x02, 0x03, 0x06, 0x07, 0x0E};

////////////////////////////////////////////////////
//  Global literals / variables
////////////////////////////////////////////////////

// The order with which the pin numbers are initialized in LedArray
// must be the reverse of the order of the bit sequences.
constexpr Led ledArray_d0[] {{4}, {5}, {6}, {7}};     // Pin numbers to which the LEDs are connected.
constexpr Led ledArray_d1[] {{8}, {9}, {10}, {11}};   // Pin numbers to which the LEDs are connected.

Button btn {2};
Dice dices[] {{ledArray_d0}, {ledArray_d1}};

////////////////////////////////////////////////////
//  Functions
////////////////////////////////////////////////////

//
// Auxiliary function for displaying bit sequences.
//
void printBits(uint8_t value) {
  for (uint8_t i = 0; i < 4; ++i) { Serial.print(((value << i) & 0x08) ? 1 : 0); }
}

//
// Does the dice roll.
//
template <size_t MAX> void rollAllDices(Dice (&dice)[MAX]) {
  bool isFinished {false};
  while (!isFinished) {
    for (uint8_t i = 0; i < MAX; i++) { isFinished = dice[i].roll(); }
  }
  for (uint8_t i = 0; i < MAX; i++) {
    // Serial.print("Dice pips: ");
    // printBits(dice[i].getBitPattern()); Serial.print(" "); 
    // Serial.println(dice[i].getRollPips());
    dice[i].rollReset();
  }
}

////////////////////////////////////////////////////
//  Main program
////////////////////////////////////////////////////

void setup() {
  Serial.begin(115200);
  btn.begin();
  for (auto &dice : dices) { dice.begin(); }

  // Do animation
  rollAllDices(dices);
  // light off leds
  for (auto &dice : dices) { dice.lightLeds(0); }
}

void loop() {
  if (btn.tick()) { rollAllDices(dices); }
}