/*
 * Torn.cpp / Torn.ino
 *
 * Code by: Gray Mack
 * License: Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0) 
 *          https://creativecommons.org/licenses/by-nc-sa/4.0/
 * Created: //2020
 * One Line Description: 
 * Board: 
 * Select Board: Arduino Mega or Mega 2560
 * Other Hardware:
 * Detailed Description:
 * 
 * Rev History:
 * 
 *	03/10/2011 initial code creation
 * 03/11/2011 dorkbot release, blinky lights, still some bugs
 * 03/15/2011 Some refactoring and cleanup, bug fixes, still single player
 * 03/16/2011 replace use of servo library with PWM servo driver due to conflict in wire library
 *            making servo very jittery
 *
 * 12/18/2020 Version 2.0 reboot
 * 12/29/2020 Completed pin layout and initial unit tests of buttons, joysticks, midi, led-displays
 * //2020 ...
 */

// ----[ configuration  ]------------------------------------
//#define CONFIGURATION_1 true //
#define CONSOLE_BAUD 9600
#define CONSOLE_START_MESSAGE "Torn V 2.0   by Gray Mack"

// ----[ included libraries ]------------------------------------
#include <Arduino.h>
#include <Keypad.h> // 3.0 2015-09-18 http://playground.arduino.cc/Code/Keypad  Authors: Mark Stanley, Alexander Brevig
#include "GridBalancer.h"
#include "LightTracer.h"
#include "ScoreKeeper.h"
#include "MidiSound.h"
// other includes used in other modules
//#include "TM1637Display_Gray.h" // Author: [email protected]. Modified for faster behavior
// potential faster tm1637 library https://github.com/sufzoli/TM16XX
// tm1637 datasheet http://j5d2v7d7.stackpathcdn.com/wp-content/uploads/2015/10/TM1637_datasheet.pdf
//#include <SPI.h>
//#include <Adafruit_GFX.h>



// vs1053 datasheet https://cdn-shop.adafruit.com/datasheets/vs1053.pdf
// fancier midi library https://github.com/razrotenberg/Midier
// see youtube Mike Rainbow DIY VS1053 MIDI sound module
// green board schematic https://www.compel.ru/infosheet/LCTECH/VS1053%20MP3%20Module%20Development%20Board%20%5BOn-board%20voice%20recording%5D

// ----[ pin definitions ]------------------------------------
#define PIN_CONSOLE_RX 0
#define PIN_CONSOLE_TX 1
#define PIN_COIN_BUTTON_LED 2
#define PIN_MUSIC_BUTTON_LED 3

#define PIN_MIDI_RESET 6
#define PIN_SEVEN_SEGMENT_CLOCK 8
#define PIN_SEVEN_SEGMENT_DATA 9
#define PIN_PIEZO_SPEAKER 12 // alternate audio
#define PIN_BUILT_IN_LED LED_BUILTIN // (D4 built in LED)
#define PIN_MIDI_OUT_TX2 16

#define PIN_TRACER_LED0  22  // -+
#define PIN_TRACER_LED1  23  //  |
#define PIN_TRACER_LED2  24  //  |
#define PIN_TRACER_LED3  25  //   } PORTA
#define PIN_TRACER_LED4  26  //  |
#define PIN_TRACER_LED5  27  //  |  
#define PIN_TRACER_LED6  28  //  |
#define PIN_TRACER_LED7  29  // / 
#define PIN_TRACER_LED15 30  // -+ 
#define PIN_TRACER_LED14 31  //  |
#define PIN_TRACER_LED13 32  //  |
#define PIN_TRACER_LED12 33  //   } PORTC (reverse order)
#define PIN_TRACER_LED11 34  //  |
#define PIN_TRACER_LED10 35  //  |
#define PIN_TRACER_LED9  36  //  |
#define PIN_TRACER_LED8  37  // / 
#define PIN_TRACER_FIRST PIN_TRACER_LED0
#define PIN_TRACER_LAST PIN_TRACER_LED8
#define PIN_BUTTONS_R0 38
#define PIN_BUTTONS_R1 39
#define PIN_BUTTONS_R2 40
#define PIN_BUTTONS_R3 41
#define PIN_BUTTONS_C0 42
#define PIN_BUTTONS_C1 43
#define PIN_BUTTONS_C2 44
#define PIN_BUTTONS_C3 45

#define PIN_MAX72XX_8x8_LEFT_CS 48
#define PIN_MAX72XX_8x8_RIGHT_CS 49
#define PIN_MAX72XX_8X8_MOSI 51
#define PIN_MAX72XX_8X8_SCLK 52

#define PIN_JOYSTICK_U 62 // A8
#define PIN_JOYSTICK_D 63 // A9
#define PIN_JOYSTICK_L 64 // A10
#define PIN_JOYSTICK_R 65 // A11
#define PIN_JOYSTICK_LEFT_COM 66 // A12
#define PIN_JOYSTICK_RIGHT_COM 67 // A13
#define PIN_SPECIAL_BUTTONS 68 // A14

// ----[ constants ]------------------------------------
const uint8_t MAX_BALANCERS = 2;
const uint8_t MAX_TRACERS = 5;

const uint8_t SECONDS_TO_WAIT_FOR_PLAYERS = 5;

const uint8_t SEVEN_SEGMENT_BRIGHTNESS = 7; // 0 - 7max  

const uint8_t PLAY_BUTTONS_ROWS = 4;
const uint8_t PLAY_BUTTONS_COLS = 4;
const char ButtonKeys[PLAY_BUTTONS_ROWS][PLAY_BUTTONS_COLS] = {
   {0,4,8,12},
   {1,5,9,13},
   {2,6,10,14},
   {3,7,11,15}
};
byte PlayButtonsRowPins[PLAY_BUTTONS_ROWS] = {PIN_BUTTONS_R0, PIN_BUTTONS_R1, PIN_BUTTONS_R2, PIN_BUTTONS_R3}; //connect to the row pinouts of the kpd
byte PlayButtonsColPins[PLAY_BUTTONS_COLS] = {PIN_BUTTONS_C0, PIN_BUTTONS_C1, PIN_BUTTONS_C2, PIN_BUTTONS_C3}; //connect to the column pinouts of the kpd

const uint8_t JOYSTICKS_ROWS = 3;
const uint8_t JOYSTICKS_COLS = 4;
const char JoystickKeys[JOYSTICKS_ROWS][JOYSTICKS_COLS] = {
   {'U','D','L','R'},
   {'u','d','l','r'},
   {'1','2','x','x'},
};
byte PlayJoystickRowPins[JOYSTICKS_ROWS] = {PIN_JOYSTICK_LEFT_COM, PIN_JOYSTICK_RIGHT_COM, PIN_SPECIAL_BUTTONS}; //connect to the row pinouts of the kpd
byte PlayJoystickColPins[JOYSTICKS_COLS] = {PIN_JOYSTICK_U, PIN_JOYSTICK_D, PIN_JOYSTICK_L, PIN_JOYSTICK_R}; //connect to the column pinouts of the kpd

// how long does each level last from level 1 - 3         10, 30, 10, 30
const unsigned int CumulativeTimeInEachLevelSeconds[] = { 10, 40, 50, 80, 0 };

// ----[ types ]-------------------------------------------
enum PlayLevels : int8_t {
   PL_PowerupSequence = -3,
   PL_AttractMode = -2,
   PL_InstrumentMode = -1,
   PL_WaitingForOtherPlayerToJoin = 0,
   PL_PlayBasicExtinguish = 1
};
PlayLevels operator++(PlayLevels f) { return PlayLevels(int(f)+1); }

// ----[ helper class ]------------------------------------

// ----[ global variables ]------------------------------------
bool GameOver;
bool TwoPlayer;
PlayLevels LevelOfPlay;
unsigned long StartTime;
int TotalScore;
LightTracer Tracers[MAX_TRACERS];
GridBalancer* Balancers[MAX_BALANCERS]; // = { GridBalancer(1), GridBalancer(2) };
Keypad PlayerButtons = Keypad(makeKeymap(ButtonKeys), PlayButtonsRowPins, PlayButtonsColPins, PLAY_BUTTONS_ROWS, PLAY_BUTTONS_COLS);
Keypad PlayerJoysticks = Keypad(makeKeymap(JoystickKeys), PlayJoystickRowPins, PlayJoystickColPins, JOYSTICKS_ROWS, JOYSTICKS_COLS);
ScoreKeeper Score(PIN_SEVEN_SEGMENT_CLOCK, PIN_SEVEN_SEGMENT_DATA, SEVEN_SEGMENT_BRIGHTNESS);
MidiSound Sound;
uint16_t TracerLightsPackedArray;

// ----[ prototypes ]------------------------------------
void PowerupSequence(void);
void BeginAttractMode(void);
void AttractMode(void);
void BeginSoundGizmoMode(void);
void SoundGizmoMode(void);
void BeginWaitForPlayerStart(void);
void WaitForPlayerStart(void);
void BeginGame(void);
void RunGame(void);
void HandleLevelOfPlayChange(void);
void ServiceSerialCommands(void);
void digitalWriteTracerLights(uint8_t light, uint8_t value);
void ShowTracerLights(void);
void DebugStatus(void);
void AnnounceLevelOfPlay(int newLevel);
void UnitTestButtons(void);

// ----[ code ]------------------------------------
void setup() 
{
   Serial.begin(CONSOLE_BAUD);
   Serial.println(CONSOLE_START_MESSAGE);

   pinMode(PIN_COIN_BUTTON_LED, OUTPUT);
   pinMode(PIN_MUSIC_BUTTON_LED, OUTPUT);
   for(int buttonLights=PIN_TRACER_FIRST; buttonLights<=PIN_TRACER_LAST; buttonLights++)
   {
      pinMode(buttonLights, OUTPUT);
   }

   pinMode(PIN_MAX72XX_8x8_LEFT_CS, OUTPUT);
   digitalWrite(PIN_MAX72XX_8x8_LEFT_CS, LOW);
   Balancers[0] = new GridBalancer(PIN_MAX72XX_8x8_LEFT_CS);
   pinMode(PIN_MAX72XX_8x8_RIGHT_CS, OUTPUT);
   digitalWrite(PIN_MAX72XX_8x8_RIGHT_CS, LOW);
   Balancers[1] = new GridBalancer(PIN_MAX72XX_8x8_RIGHT_CS);


   Sound.Init(PIN_MIDI_RESET, PIN_MIDI_OUT_TX2);

   LevelOfPlay = PL_PowerupSequence;
   PowerupSequence();
   Score.FourDash();
   BeginAttractMode();

   UnitTestButtons();
   //Balancers[0]->UnitTest1();
   //Sound.UnitTest1();
}

void loop() 
{
   ServiceSerialCommands();
   if(!GameOver)
      RunGame();
   else if(LevelOfPlay == PL_AttractMode)
      AttractMode();
   else if(LevelOfPlay == PL_InstrumentMode)
      SoundGizmoMode();
   else if(LevelOfPlay == PL_WaitingForOtherPlayerToJoin)
      WaitForPlayerStart();
   DebugStatus();
}

// ----[ function ]------------------------------------

// ----[ function ]------------------------------------
void PowerupSequence()
{
   Score.PowerupSequence();
   Balancers[0]->PowerupSequence();
   Balancers[1]->PowerupSequence();

   Serial.print(F("Button Lights PowerupSequence 1"));
   for(int buttonLights=0; buttonLights<16; buttonLights++)
   {
      digitalWriteTracerLights(buttonLights, 1);
      delay(100);
      digitalWriteTracerLights(buttonLights, 0);
   }
   
   Serial.println(F(", 2"));
   for(int lights=0; lights<16; lights++)
   {
      TracerLightsPackedArray = 0;
      bitSet(TracerLightsPackedArray, lights);
      ShowTracerLights();
      delay(100);
   }
   TracerLightsPackedArray = 0;
   ShowTracerLights();
   Sound.PowerupSequence();
}

// ----[ function ]------------------------------------
void BeginAttractMode()
{
   LevelOfPlay = PL_AttractMode;
   GameOver = true;
   TwoPlayer = false;
   TotalScore = 0;
}

// ----[ function ]------------------------------------
void AttractMode()
{
   static unsigned long LastAttractModeTimer = 0;
   const unsigned long AttractModeTimeInterval = 100;
   static uint8_t PulseButtonLight = 0;
   static int8_t PulseDirection = 4;
   if(millis() - LastAttractModeTimer > AttractModeTimeInterval)
   {
      LastAttractModeTimer = millis();
      PulseButtonLight += PulseDirection;
      if(PulseButtonLight > 250) PulseDirection = -abs(PulseDirection);
      if(PulseButtonLight < 5) PulseDirection = abs(PulseDirection);
      analogWrite(PIN_COIN_BUTTON_LED, PulseButtonLight);
   }
   // check for start button

   
   if(PlayerJoysticks.getKeys())
   {
      for(int j=0; j<LIST_MAX; j++)
      {
         if(PlayerJoysticks.key[j].kstate == PRESSED)
         {
            if(PlayerJoysticks.key[j].kchar == '1')
            {
               Serial.println(" Start Button Pushed");
               BeginWaitForPlayerStart();
            }
            else if(PlayerJoysticks.key[j].kchar == '1')
            {
               Serial.println(" Sound Button Pushed");
               BeginSoundGizmoMode();
            }
         }
      }
   }
}

// ----[ function ]------------------------------------
void BeginSoundGizmoMode()
{
   LevelOfPlay = PL_InstrumentMode;

}

// ----[ function ]------------------------------------
void SoundGizmoMode()
{
   //                     C#6  D6 D#6  E6  F6 F#6  G6 G#6  A6 A#6  B6              C6
   const uint8_t Notes[] { 84, 86, 87, 88, 89, 90, 91, 92, 93, 94, 96, 0, 0, 0, 0, 84 };
   uint8_t chan = 0;
   if(PlayerButtons.getKeys())
   {
      for(int b=0; b<LIST_MAX; b++) // scan the list
      {
         if(PlayerButtons.key[b].stateChanged) // keys that changed
         {
            char btn = PlayerButtons.key[b].kchar;
            char note = (btn < 16) ? Notes[(int8_t)btn] : 0;
            if(PlayerButtons.key[b].kstate == PRESSED)
            {
               Serial.print("Button "); Serial.println((int)btn);
               digitalWriteTracerLights(btn, 1);
               Sound.MidiNoteOn(chan, note, 100);
            }
            else if(PlayerButtons.key[b].kstate == RELEASED)
            {
               Sound.MidiNoteOff(chan, note, 100);
               digitalWriteTracerLights(btn, 0);
            }
         }
      }
   }
}


// ----[ function ]------------------------------------
void BeginWaitForPlayerStart()
{
   LevelOfPlay = PL_WaitingForOtherPlayerToJoin;
   Score.SetCountdown(SECONDS_TO_WAIT_FOR_PLAYERS);
   //StartTime = millis();
}

// ----[ function ]------------------------------------
void WaitForPlayerStart()
{
   int knockSecondsFromCountdown = 0;
   if(PlayerJoysticks.getKeys())
   {
      for(int j=0; j<LIST_MAX; j++)
      {
         if(PlayerJoysticks.key[j].kstate == PRESSED)
         {
            if(PlayerJoysticks.key[j].kchar == '1')
            {
               knockSecondsFromCountdown = 1;
            }
         }
      }
   }
   if(Score.CountDownClock(knockSecondsFromCountdown))
   {
      BeginGame();
   }
}


// ----[ function ]------------------------------------
void BeginGame()
{
   GameOver = false;
   LevelOfPlay = PL_PlayBasicExtinguish;
   StartTime = millis();
   TotalScore = 0;
   for(int t = 0; t < MAX_TRACERS; t++)
   {
      Tracers[t].KillTracer();
   }
}

// ----[ function ]------------------------------------
void RunGame()
{
   /*
   MoveTracers();
   TracerSounds();
   HandleButtons();
      ReadSticks();
   HandleBalancers();
   RefreshScore();
   
   */
   HandleLevelOfPlayChange();
   ServiceSerialCommands();
   
}

// ----[ function ]------------------------------------
void HandleLevelOfPlayChange()
{
   if(GameOver || LevelOfPlay < 0) return;
   unsigned long LevelDoneTimeSeconds = (unsigned long)(CumulativeTimeInEachLevelSeconds[LevelOfPlay]) * 1000UL;
   if((millis() - StartTime) < LevelDoneTimeSeconds) return;
   AnnounceLevelOfPlay(++LevelOfPlay);
}

// ----[ function ]------------------------------------
void ServiceSerialCommands()
{
   // listen for a command and execute the action.  TX1,RX1
   // perhaps a jumper will set the master.
   // Need to be able to announce AboutToStartGame/WaitingForPlayerToJoin->, <-AcceptPlayer2, StartGame->
}

// ----[ function ]------------------------------------
void digitalWriteTracerLights(uint8_t light, uint8_t value)
{
   if(light < 8)
   {
      digitalWrite(PIN_TRACER_LED0+light, value);
   }
   else if(light < 16)
   {
      digitalWrite(PIN_TRACER_LED8-(light-8), value);
   }
   else
   {
      Serial.println(F("digitalWriteTracerLights: Unexpected light"));
   }
}

// ----[ function ]------------------------------------
void ShowTracerLights()
{
   PORTA = TracerLightsPackedArray & 0xFF;
   PORTC = (TracerLightsPackedArray >> 8) & 0xFF;
}

// ----[ function ]------------------------------------
void DebugStatus()
{
   const int DEBUG_STATUS_EVERY_MS = 1000;
   static unsigned long StatusMillis = 0;
   if(millis() - StatusMillis < DEBUG_STATUS_EVERY_MS) return;

   StatusMillis = millis();
   Serial.print(" Level: ");
   switch(LevelOfPlay)
   {
      case PL_PowerupSequence:
         Serial.print("Powerup ");
         break;
      case PL_AttractMode:
         Serial.print("Attract ");
         break;
      case PL_InstrumentMode:
         Serial.print("SoundGiz");
         break;
      case PL_WaitingForOtherPlayerToJoin:
         Serial.print("WaitPlyr");
         break;
      default:
         Serial.print("       "); Serial.print(LevelOfPlay);
         break;
   }
   Serial.print(" Time: ");     Serial.print((millis() - StartTime)/1000);
   Serial.print(" Score: ");    Serial.println(TotalScore);
}

// ----[ function ]------------------------------------
void AnnounceLevelOfPlay(int newLevel)
{
   Serial.print("------------[ Level "); Serial.print(newLevel); Serial.println(" ]------------");
   // mp3 player announce or midi jingle?
}


// ----[ test function ]------------------------------------
void UnitTestButtons()
{
   Serial.println("UnitTestButtons");
   char currentLeftStick = ' ';
   char currentRightStick = ' ';
   while(1)
   {
      if(PlayerButtons.getKeys())
      {
         for(int b=0; b<LIST_MAX; b++) // scan the list
         {
            if(PlayerButtons.key[b].stateChanged) // keys that changed
            {
               if(PlayerButtons.key[b].kstate == PRESSED)
               {
                  int btn = PlayerButtons.key[b].kchar;
                  Serial.print("Button "); Serial.print(btn); Serial.print(" pushed "); Serial.println((millis()/10)%10000);
                  //digitalWriteTracerLights(btn, HIGH);
                  TracerLightsPackedArray = 0;
                  bitSet(TracerLightsPackedArray, btn);
                  ShowTracerLights();
                  delay(50);
                  //digitalWriteTracerLights(btn, LOW);
                  TracerLightsPackedArray = 0;
                  ShowTracerLights();
               }
            }
         }
      }
      if(PlayerJoysticks.getKeys())
      {
         for(int j=0; j<LIST_MAX; j++)
         {
            KeyState ks = PlayerJoysticks.key[j].kstate;
            char event = PlayerJoysticks.key[j].kchar;
            if(ks == PRESSED)
            {
               Serial.print("   Joy ");
               switch(event)
               {
               case 'U': case 'D': case 'L': case 'R':
                  Serial.print(" L-");
                  currentLeftStick = event;
               break;
               case 'u': case 'd': case 'l': case 'r':
                  Serial.print(" R-");
                  currentRightStick = event;
               break;
               default:
                  Serial.print(" P-");
               break;
               }
               Serial.println(event);
            }
            else if(ks == RELEASED)
            {
               switch(event)
               {
               case 'U': case 'D': case 'L': case 'R':
                  currentLeftStick = ' ';
               break;
               case 'u': case 'd': case 'l': case 'r':
                  currentRightStick = ' ';
               break;
               }
  
            }
         }
      }
      const unsigned long STICK_TIME = 250;
      static unsigned long StickLogMillis;
      if(millis() - StickLogMillis > STICK_TIME)
      {
         StickLogMillis = millis();
         if(currentLeftStick  != ' ') { Serial.print("        L-"); Serial.println(currentLeftStick); }
         if(currentRightStick != ' ') { Serial.print("        R-"); Serial.println(currentRightStick); }
      }
   }
}
4-Digit Display