#include <Button_SL.hpp>
#include <Streaming.h>

Print &cout {Serial};
using namespace Btn;

///
/// Global Constants
///
constexpr uint8_t RGB_LED_PINS[] {A1, A2, A3};
constexpr uint8_t MATRIX_COLS {3};
constexpr uint8_t MATRIX_ROWS {MATRIX_COLS};
constexpr uint32_t BLINK_INTERVAL_MS {400};

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

private:
  uint32_t timeStamp {0};
};

///
/// Own Datatypes
///
enum class Status : uint8_t { none, col, row, save, reset, result, waitForReset};
enum ColorNum {off, red, green, blue, cyan, yellow, magenta, white};

struct PlayingField {
  const uint8_t pin;
  bool saved;
  uint8_t value;
};

///
/// Used Functions
///

//
// Button query
//
template <size_t N> Status checkButtons(ButtonSL (&btns)[N]) {
  for (uint8_t i = 0; i < N; ++i) {
    if (btns[i].tick() != ButtonState::notPressed) { return static_cast<Status>(i + 1); }
  }
  return Status::none;
}

//
// toggle LEDs
//
template <size_t N> void toggleLed(PlayingField (&data)[N], uint16_t idx) {
  if (!data[idx].saved && idx < N) { digitalWrite(data[idx].pin, !digitalRead(data[idx].pin)); }
}

//
// switch  LED off
//
template <size_t N> void switchLedOff(PlayingField (&data)[N], uint16_t idx) {
  if (!data[idx].saved && idx < N) { digitalWrite(data[idx].pin, LOW); }
}

//
// switch  RGB-LED (off / color)
//
template <size_t N> void setRGBLed(const uint8_t (&pin)[N], uint8_t colorNum) {
  uint32_t colorBits {0b00000000111101110011001010100000};
  uint8_t colors = (colorBits >> (colorNum * 3)) & 0b111;
  for (size_t i {0}; i < N; ++i) { digitalWrite(pin[i], colors << i & 0b100); }
}

//
// Movement on the game matrix. Calculate the right array index
//
template <size_t N> bool doMove(PlayingField (&data)[N], uint16_t &idx, uint8_t increment) {
  bool allFieldsSet {false};
  auto start = idx;
  do {
    idx += increment;
    if (increment > 1) {
      idx = (idx > (N - 1)) ? (idx - (N - 1)) % increment : idx;
    } else {
      idx = (idx > (N - 1)) ? 0 : idx;
    }
    if (idx == start) { allFieldsSet = true; }   // Abort condition
    if (!data[idx].saved) { break; }             // Exit loop if a free field is found
  } while (!allFieldsSet);
  return allFieldsSet;
}

//
// Check the result when the input has been completed
//
template <size_t N> bool checkResult(PlayingField (&result)[N]) {
  int16_t rowSum[MATRIX_ROWS] {0, 0, 0};
  int16_t colSum[MATRIX_COLS] {0, 0, 0};
  int16_t diagSum1 {0};
  int16_t diagSum2 {0};
  for (size_t row {0}; row < MATRIX_ROWS; ++row) {
    for (size_t col {0}; col < MATRIX_COLS; ++col) {
      uint8_t idx = row * MATRIX_ROWS + col;
      cout << result[idx].value << " ";
      rowSum[row] += result[idx].value;   
      colSum[col] += result[idx].value;  

      if (row == col) {
        diagSum1 += result[idx].value;   // Main diagonal
      }
      if (row + col == MATRIX_COLS - 1) {
        diagSum2 += result[idx].value;   // Secondary diagonal
      }
    }
  }
  cout << endl;
  // Ausgabe der Summen
  // cout << "Reihensummen: ";
  // for (size_t i {0}; i < MATRIX_ROWS; ++i) { cout << rowSum[i] << " "; }
  // cout << endl;
  // cout << "Spaltensummen: ";
  // for (size_t i {0}; i < MATRIX_COLS; ++i) { cout << colSum[i] << " "; }
  // cout << endl;
  // cout << "Hauptdiagonale: " << diagSum1 << endl;
  // cout << "Nebendiagonale: " << diagSum2 << endl;

  bool isRowEqual {true};
  bool isColEqual {true};

  int16_t compare = rowSum[0];
  for (size_t i = 1; i < MATRIX_ROWS; ++i) {
    if (rowSum[i] != compare) {
      isRowEqual = false;
      break;
    }
  }

  for (size_t i = 0; i < MATRIX_COLS; ++i) {
    if (colSum[i] != compare) {
      isRowEqual = false;
      break;
    }
  }
  return isRowEqual && isColEqual && (diagSum1 == compare) && (diagSum2 == compare);
}

//
// Clear Data
//
template <size_t N> void resetData(PlayingField (&data)[N]) {
  for (auto &d : data) {
    digitalWrite(d.pin, LOW);
    d.saved = false;
    d.value = 0;
  }
}

///
/// Global variables / objects
///
PlayingField pfData[] {
    {2,  false, 0},
    {3,  false, 0},
    {4,  false, 0},
    {5,  false, 0},
    {6,  false, 0},
    {7,  false, 0},
    {8,  false, 0},
    {9,  false, 0},
    {10, false, 0}
};

ButtonSL bArray[] {{11}, {12}, {13}, {A0}};
Timer waitBlink;

// | 8  | 1  | 6  |
// | 3  | 5  | 7  |
// | 4  | 9  | 2  |

///
/// Main Program
///
void setup() {
  Serial.begin(115200);
  for (auto &p : pfData) { pinMode(p.pin, OUTPUT); }
  for (auto &p : RGB_LED_PINS) { pinMode(p, OUTPUT); }
  for (auto &b : bArray) {
    b.begin();
    b.setDebounceTime_ms(40);
  }
  setRGBLed(RGB_LED_PINS, blue);
}

void fsm() {
  static Status status {Status::none};
  static uint16_t index {};       // index playfield array (leds)
  static uint16_t prevIndex {};   // previos index playfield array ( for moving  - switch previous led off )
  static uint8_t saveCounter {};  // Storage counter -> index for data array (result)
  static bool isSaving {false};   // needed to prevent saving errors

  switch (status) {
    case Status::none:
      status = checkButtons(bArray);
      break;
    case Status::col:
    case Status::row:
      switchLedOff(pfData, prevIndex);
      doMove(pfData, index, (status == Status::col) ? 1 : MATRIX_COLS);
      waitBlink.start();
      status = Status::none;
      break;
    case Status::save:
      pfData[index].saved = true;
      pfData[index].value = ++saveCounter;
      cout << "Saved Nr. " << saveCounter << " at Index: " << index << endl;
      digitalWrite(pfData[index].pin, HIGH);
      if (doMove(pfData, index, 1) == true) {  // true if all fields are set
        status = Status::result;
      } else {
        waitBlink.start();
        status = Status::none;
      }
      break;
    case Status::reset:
      resetData(pfData);
      setRGBLed(RGB_LED_PINS, blue);
      index = saveCounter = 0;
      isSaving = false;
      cout << endl << endl;
      status = Status::none;
      break;
    case Status::result:
      checkResult(pfData) ? setRGBLed(RGB_LED_PINS, green) : setRGBLed(RGB_LED_PINS, red);
      status =Status::waitForReset; 
      [[falltrough]]
    case Status::waitForReset:
      if (checkButtons(bArray) == Status::reset) { status = Status:: reset; }
      break;
    default:
      break;
  }
  if (waitBlink(BLINK_INTERVAL_MS)) {
    prevIndex = index;
    toggleLed(pfData, index);
    waitBlink.start();
  }
}

void loop() { fsm(); }
col
row
save
reset
blue:game active green: result is correct red: result is incorrect