#include <Streaming.h>
#include <Keypad.h>
#include <LiquidCrystal_I2C.h>

Print &cout {Serial};

constexpr uint8_t ROWS = 3;
constexpr uint8_t COLS = 3;
constexpr char buttons[ROWS][COLS] = {
    {'0', '1', '2'},
    {'3', '4', '5'},
    {'6', '7', '8'}
};

uint8_t rowPins[ROWS] = {9, 8, 7};
uint8_t colPins[COLS] = {6, 5, 4};

constexpr uint8_t BOARD_COLS {3};
constexpr uint8_t BOARD_ROWS {BOARD_COLS};
constexpr uint8_t BOARD_SIZE {(BOARD_COLS + 2) * (BOARD_ROWS + 2)};
constexpr uint8_t BOARD_OFFSET {BOARD_COLS + 2};

// Index - Offsets to test whether three identical characters are in a row
//                      CHECK       ROW,     COL   DIAG. RIGHT       DIAG. LEFT
constexpr uint8_t DIRECTIONS[] {BOARD_OFFSET, 1, BOARD_OFFSET + 1, BOARD_OFFSET - 1};

constexpr char BD {'@'};   // Value: border of playing field
constexpr char FD {' '};   // Value: playing field
constexpr char BOARD_INIT[BOARD_SIZE] {BD, BD, BD, BD, BD, BD, FD, FD, FD, BD, BD, FD, FD,
                                       FD, BD, BD, FD, FD, FD, BD, BD, BD, BD, BD, BD};

char board[BOARD_SIZE];  // Playground
LiquidCrystal_I2C lcd(0x27, 20, 4); // I2C_ADDR, LCD_COLUMNS, LCD_LINES
Print &lcdOut {lcd};
Keypad keypad = Keypad(makeKeymap(buttons), rowPins, colPins, ROWS, COLS);

//////////////////////////////////////////////////////////////////////////////
/// BDbrief Sets the playing field to its initial values
///
/// BDtparam (&brd)[N]   The playing field as an array
//////////////////////////////////////////////////////////////////////////////
template <size_t N> void resetBoard(char (&brd)[N]) { memcpy(brd, BOARD_INIT, N); }

//////////////////////////////////////////////////////////////////////////////
/// @brief Display of the playing field  (serial console)
///
/// @tparam  BDtparam (&brd)[N]   The playing field as an array
//////////////////////////////////////////////////////////////////////////////
template <size_t N> void showBoard(char (&brd)[N]) {
  for (uint8_t i = BOARD_OFFSET; i < (N - BOARD_OFFSET); ++i) {
    if (brd[i] != BD) {
      cout << _FMT("[%,%,%]\n", brd[i], brd[i + 1], brd[i + 2]);
      i += 4;
    }
  }
}

//////////////////////////////////////////////////////////////////////////////
/// @brief Display of the playing field  (LCD/TFT)
/// 
/// @param disp              Reference to display object
/// @tparam char (&brd)[N]   The playing field as an array
//////////////////////////////////////////////////////////////////////////////
template <size_t N> void showBoard(LiquidCrystal_I2C &disp, char (&brd)[N]) {
  Print &lcdOut {disp};
  uint8_t row {0};
  for (uint8_t i = BOARD_OFFSET; i < (N - BOARD_OFFSET); ++i) {
    if (brd[i] != BD) {
      disp.setCursor(6, row);
      lcdOut << _FMT("[%,%,%]", brd[i], brd[i + 1], brd[i + 2]);
      i += 4;
      ++row;
    }
  }
}

//////////////////////////////////////////////////////////////////////////////
/// @brief Show which player has the turn
/// 
/// @param disp     Reference to display object
/// @param plChar   Character of the player
/// @param plIdx    Index represents the Playernumber - 1
//////////////////////////////////////////////////////////////////////////////
void showPlayer(LiquidCrystal_I2C &disp, const char plChar[], uint8_t plIdx) {
   Print &lcdOut {disp};
   disp.setCursor(0, 0);
   lcdOut << "P:" << plIdx+1;
   disp.setCursor(0, 1);
   lcdOut <<  F("(") << plChar[plIdx] << F(")");
}

//////////////////////////////////////////////////////////////////////////////
/// @brief Show the winner
/// 
/// @param disp   Reference to display object
/// @param plIdx  Index represents the Playernumber - 1
//////////////////////////////////////////////////////////////////////////////
void showWinner(LiquidCrystal_I2C &disp, uint8_t plIdx) {
  Print &lcdOut {disp};
  disp.setCursor(1,3);
  if (plIdx > 1) {
    lcdOut << F("No one has won %-)");
  } else {
    lcdOut << F("Player: ") << plIdx + 1 << F(" has won!");
  }
}

//////////////////////////////////////////////////////////////////////////////
/// @brief Deletes the fourth line of the display (status display)
/// 
/// @param disp   Reference to display object
//////////////////////////////////////////////////////////////////////////////
void clearStatusRow(LiquidCrystal_I2C &disp) {
  Print &lcdOut {disp};
  disp.setCursor(0,3);
  lcdOut << _PAD(20,' ');   // clear fourth row
}

//////////////////////////////////////////////////////////////////////////////
/// @brief Conversion from the simple offset (buttons) in the array
///        to the array from the playing field
///
/// @param pos
/// @return uint8_t
//////////////////////////////////////////////////////////////////////////////
uint8_t calcBoardOffset(uint8_t pos) {
  uint8_t col = pos % BOARD_COLS;
  uint8_t row = pos / BOARD_ROWS;
  return (BOARD_OFFSET + 1) + (row * BOARD_OFFSET) + col;
}

//////////////////////////////////////////////////////////////////////////////
/// @brief Implementation of a move. The player value is placed on the
///        corresponding field of the playing field (array).
///
/// @param brd      Array representing the playing field
/// @param pos      Position on the playing field (index 0 - 9)
/// @param value    Player value (-1 = player 1 or 1 = player 2)
/// @return true    Value was set
/// @return false   Value was not set
//////////////////////////////////////////////////////////////////////////////
bool move(char brd[], uint8_t pos, const char value) {
  bool isValueAssigned {false};
  if (pos > (BOARD_COLS * BOARD_ROWS - 1)) {
    cout << F("ERROR!\n");   // Index out of bounds
    return isValueAssigned;
  }

  uint8_t idx = calcBoardOffset(pos);
  // Only set player value if this field has not yet received one
  if (brd[idx] == FD) {
    brd[idx] = value;
    isValueAssigned = true;
  }
  return isValueAssigned;
}

//////////////////////////////////////////////////////////////////////////////
/// @brief Check whether there are three identical, consecutive values
///        horizontally, vertically or diagonally on the playing field.
///        https://www.youtube.com/watch?v=e5lKoL9G0N4
///
/// @param brd    Array representing the playing field
/// @param idx    Index of the last player value entered on the playing field
/// @return true  identical Values
/// @return false no identical values
//////////////////////////////////////////////////////////////////////////////
bool isRowComplete(char brd[], uint8_t idx) {
  char compare = brd[idx];
  uint8_t sum {1};

  for (auto &dirIdx : DIRECTIONS) {   // Get the index of the corresponding direction for comparison
    uint8_t sIdx = idx + dirIdx;
    sum = 1;
    while (brd[sIdx] == compare) {   // Compare to the right
      sIdx += dirIdx;
      sum += 1;
    }
    sIdx = idx - dirIdx;
    while (brd[sIdx] == compare) {   // Compare to the left
      sIdx -= dirIdx;
      sum += 1;
    }
    if (sum == 3) { return true; }
  }
  return false;
}

//////////////////////////////////////////////////////////////////////////////
/// @brief Check whether a button has been pressed
///
/// @tparam keypad      Key matrix object
/// @return int8_t      value (0-8) of the button pressed or -1 if none was pressed.
//////////////////////////////////////////////////////////////////////////////
int8_t checkButtons(Keypad &kp) {
  char key = kp.getKey();
  return (key) ? key-0x30 : -1;
}

void setup() {
  Serial.begin(115200);
  lcd.init();
  lcd.backlight();
  resetBoard(board);
  // cout << F("Player: 1 (X) has the turn\n");
  showPlayer(lcd,"X",0);
  showBoard(lcd, board);
}

void loop() {
  static bool restart {false};
  static char player[2] {'X', 'O'};
  static uint8_t pCounter[2] {0};
  static uint8_t pIdx {0};

  int8_t bVal = checkButtons(keypad);
  if (bVal > (-1)) {
    if (move(board, bVal, player[pIdx]) == true) {
      pCounter[pIdx]++;
      if (isRowComplete(board, calcBoardOffset(bVal))) {   // Three in a row?
        // cout << F("\nPlayer: ") << pIdx + 1 << F(" has won!\n");
        showWinner(lcd,pIdx);
        showBoard(lcd,board);
        restart = true;
      } else if (pCounter[0] + pCounter[1] == (BOARD_ROWS * BOARD_COLS)) {   // no and no remaining fields
        showWinner(lcd,2); // Index 2 (as marker) if no one has won.
        // cout << F("\nNo one has won\n");
        showBoard(lcd,board);
        restart = true;
      }

      if (restart == true) {
        pCounter[0] = 0;
        pCounter[1] = 0;
        resetBoard(board);
        restart = false;
        delay(5000);
        clearStatusRow(lcd);
      }

      pIdx = !pIdx;   // Swap Player
      // cout  << F("\nPlayer: ") << pIdx + 1 << F(" (") << player[pIdx] << F(") has the turn\n");
      showBoard(lcd,board);
      showPlayer(lcd,player,pIdx);
    }
  }
}