//  Mini Game Platform Library - Hardware Support functions
//  4 Button + 8 LED module + speaker out (Piezo)
//  STEM - Arduino, electronic and software ('C'), with Math 
//  see: https://www.instructables.com/member/RonM9
//  Jan 2021 Update
//

//#define DEBUG false
bool DEBUG = false;

// ----------------  Function prototypes (pre definitions, for the compiler)
void boop(int msecs, int delayms=0);
void bop(int, int);
void beep(int duration, int delayms=0);
void buZZ(int msecs, int delayms=0);
void clearDisp();
void scanBtns();
void btnsNow();  // gets an updated state without scanBtns's 'debounce' interference

//=================================================================
// Edit within this section if you device is not wire out as a
// standard "Mini Game Platform" see: instructables.com/....
// ----------------  hardware dependent section  ------------------
// constants won't change, unless the hardware changes.

// ---------- 8 LED configuration data
const int nleds=8;
#define ON_STATE 0
// defined as needed below: PSUDOVCC    // supplies +V to 8 LED module
// defined as needed below: PSUDOGND    // used by 4 Key module as needed

bool Loud = false;  // enables AC driven audio   
const int ledOnBrd=LED_BUILTIN;  // the number of the LED 'L' on MCU board

//=====================================================================================================
//=========== Below unComment with "/*****/" the section which applies & otherwise disable ( with: "/*****" )
/*****/
// ------------ open prototype STEM Educational platform (arduino Nano)
// ledOnBrd=ONBOARD_LED;
const int button[] = {16, 17, 18, 19};   // The four button input pins are oriented in this order
const int lites[] = {9, 8, 7, 6, 5, 4, 3, 2};   // the LED pin assignments
const int RedSet=4, YelSet=3, GrnSet=1;  // used by readySetGo() to cycle Red/Yellow/!GREEN
const byte AUDIO_OUT = 13;
const byte AUDIO_NEG = 12;
#define PSUDOGND 15  // used by 4 Key module as needed

/*****
// ------------ enclosed BlackBox version (also aka Lefthand version, all reversed right to left)
const int button[] = {19, 18, 17, 16};      //The four button input pins
const int lites[] = {2, 3, 4, 5, 6, 7, 8, 9};   //the LED pins
const int RedSet=1, YelSet=2, GrnSet=4;  // used by readySetGo() to cycle Red/Yellow/!GREEN
const byte AUDIO_OUT = 13;
const byte AUDIO_NEG = 12;
#define EXTRNVCC 10  // supplies +V to 8 LED module
/*****
// ------------ Pro Micro Open construction (analogs are unavailable, used for LEDs)
#define ProMicro
const int button[] = {2, 3, 4, 5};        //The four button input pins
const int lites[] = {21, 20, 19, 18, 15, 14, 16, 10};     //the LED pins
const int RedSet=4, YelSet=3, GrnSet=1;  // used by readySetGo() to cycle Red/Yellow/!GREEN
const byte AUDIO_OUT = 8;
const byte AUDIO_NEG = 9;
/*****
// ------------ Open SparkFun Pro Micro - RP2040 construct layout like Pro Micro version (analogs are unavailable, used for LEDs)
#define RP2040
const int button[] = {2, 3, 4, 5};        //The four button input pins
const int lites[] = {29, 28, 27, 26, 22, 20, 23, 21};     //the LED pins
const int RedSet=4, YelSet=3, GrnSet=1;  // used by readySetGo() to cycle Red/Yellow/!GREEN
const byte AUDIO_OUT = 8;
const byte AUDIO_NEG = 9;
/*****
// ------------ Enclosed ver SparkFun Pro Micro - RP2040 construct layout reverse of Open version
// in hind sight, no need to reverse wire order, PCV could have been flipped or use more flecible silicone wire 
#define RP2040
const int button[] = {22, 20, 23, 21};     //NOTE: button input pins need pullup Rs
const int lites[] = {2, 3, 4, 5, 6, 7, 8, 9};     //the LED pins
const int RedSet=4, YelSet=3, GrnSet=1;  // used by readySetGo() to cycle Red/Yellow/!GREEN
const byte AUDIO_OUT = A1;
const byte AUDIO_NEG = A0;
/*****
// ------------ Plug-N-Play (held together with just an alastic band) - WaveShare RP2040-Zero
//                          (could use a breadboard or solder them directly together)
#define RP2040
const int button[] = {27, 26, 15, 14};     //NOTE: button input pins need pullup Rs
const int lites[] = {1, 2, 3, 4, 5, 6, 7, 8};     //the LED pins
const int RedSet=4, YelSet=3, GrnSet=1;  // used by readySetGo() to cycle Red/Yellow/!GREEN
const byte AUDIO_OUT = 12;
const byte AUDIO_NEG = 10;
#define PSUDOVCC 0    // supplies +V to 8 LED module
#define PSUDOGND 28   // used by 4 Key module as needed
/*****/
//===========================================================================================

// ---------- Analog inputs used by some
#ifdef RP2040
//!!! input Keys need extrn wired pullups, as RP2040 does not have them
const byte ADC_a = A3;
byte ADC_b = A2;
#else
const byte ADC_a = A7;
byte ADC_b = A0;
#endif

bool debugPrt=false;

//=================================================================
// ----------  button config. data
#define PRESSED_STATE 0

// -----------------------
// 4-Button module support

bool btn1 = false;  // reflects the state of button #1
bool btn2 = false;
bool btn3 = false;
bool btn4 = false;
bool Btn=false;   // reflects Buttons collectively
bool btn[5];      // array of btn1-4 states
bool btns=false;  // true is multiple buttons are pressed
bool ESC = false;  // a psuedo button, true is btn4 is held down for 2 seconds
unsigned int escCnt;
int secsIdle=0;

bool btn1Changed = false; // true if btn1 just changed
bool btn2Changed = false;
bool btn3Changed = false;
bool btn4Changed = false;
bool BtnChanged = false;  // true if any button changes
//unsigned long lastChg;    // millis() when last button change occured
unsigned int lastChg;    // my 'msCnt' millisec cnt when last button change occured
byte  btnNum;     // btn number pressed 1-4, lower num takes precedence

static byte btnState = 0, currState, priorBSt;

#define btn1Pressed (btn1Changed && btn1)
#define btn2Pressed (btn2Changed && btn2)
#define btn3Pressed (btn3Changed && btn3)
#define btn4Pressed (btn4Changed && btn4)
#define BtnPressed (BtnChanged && Btn)

// -----------------------------------------------------------------
// 8 LED display bar support
unsigned int msCnt;   // pseudo MSecs, incremented every time 'refreshDisp()' is called
#define myMillis msCnt
unsigned int mySecs=0;

// NOTE! in the arrays below, elements [1]-[8] relate to LED 1-8 respectively
//                            [0] & [9] are for overflow protection
bool dim[8+2];  // set true which LEDs you want to be dim (~10% on)
bool lit[8+2];  //                 "                  lit (~33% on)
bool brt[8+2];  //                 "                  bright (100%)
bool arc[8+2];  //                 "           sparky variable brightness
int brightOne;  // sets indicate (1-8) LED Bright.  Useful as a cursor or sprite
int litLED;     //         "               Lit            "
int dimLED;
int flashLed;  // the number of the LED to Flash
int dimFlash;  // only works over otherwise off or dim LEDs
int blinkLed;
int spriteOne, spriteTwo;
bool onSet;   // if true LEDs set as is, upd disabled
byte levelOn[nleds+2]; // for lighting 0-16 Levels
bool glow[nleds+2]; // used to implement a cyclic brightness 'glow' lighting effect
bool glowEnable=false;
int dimIt;  // the led to be 'dim'd 50% of its otherwise brightness

//==================== Support functions ========================

// ----------------- setup the hardware for the Mini STEM/Game platform
void gPlatform_setup() {
  // ------------ setup LED pins as outputs
#ifdef PSUDOVCC
  pinMode(PSUDOVCC, OUTPUT);    // provide +v for LED bar module
  digitalWrite(PSUDOVCC, HIGH);
#endif
#ifdef EXTRNVCC
  pinMode(EXTRNVCC, INPUT);   // let it be externally driven, for my blackBox proj.
#endif
  for (int x = 0; x < nleds; x++)
  {
    pinMode(lites[x], OUTPUT);
    digitalWrite(lites[x], ON_STATE);  // on startup have LEDs momentarially On
  }

  // ------------ setup the four button module
  for (int x = 0; x < 4; x++)
  {
//    pinMode(button[x], INPUT);      // button pins are inputs
//    digitalWrite(button[x], HIGH);  // enable internal pullup; buttons start in high position; logic reversed
    pinMode(button[x], INPUT_PULLUP);
  }
#ifdef PSUDOGND
    if (PSUDOGND == ADC_b) ADC_b = ADC_a;  // work-around (RP2040-zero) as pin cannot do double duty
    pinMode(PSUDOGND, OUTPUT);    // provide Ground (-V) for the button module
    digitalWrite(PSUDOGND, LOW);
#endif

  // ------------ setup and init the Audio
  pinMode(AUDIO_OUT, OUTPUT);
  digitalWrite(AUDIO_OUT, LOW);
  pinMode(AUDIO_NEG, OUTPUT);
  digitalWrite(AUDIO_OUT, LOW);

  clearDisp();  // initialize display LEDs
  scanBtns();   //   "  button statuses
}

// ------------------------------------------------------------
//        Display Output Processing

//  ---------------------  Display Update  - Should to be called ~ every One-two Milli-Seconds
void refreshDisp() {
  bool on[13];
  int ledn, Lite;
  unsigned int ledTime;
  int i, n, t;
  bool flashTime, blinkTime;
  byte gs, gsCnt;
  static byte dimitCnt;
  static unsigned long lastMS;

  msCnt++;    
  if (DEBUG && msCnt%10000==0) {  // check on display refresh rate and service consistancy
    Serial.print(millis()-lastMS); Serial.print("  ");
    lastMS=millis();
  }
  if (msCnt%1000==0) {
    mySecs++;
    secsIdle++;
  }
  
  if (onSet) return;   // override: LEDs are set in fixed On pattern
  flashTime = ((msCnt%600) < 250);  // 200ms out of 500ms turn it On
  for (i=1; i<=nleds; i++) on[i]=0; // init working array

  // -------------- determine which Leds its time to illuminate
  ledTime = msCnt;
  if (ledTime%15 == 1) {                // 1/x cadence time to show 'dim'
    for (i=1; i<=nleds; i++) {on[i]=dim[i];}
    on[dimLED] = true;
    on[blinkLed] = true;
  } else if (ledTime%3 == 1) {          // ~27% (1/3-20%) (=4/15) of the time as 'lit'
    for (i=1; i<=nleds; i++) on[i]=lit[i];
    on[litLED] = true;
  }

  // -------- glow support
  if (glowEnable) {
    gsCnt=(ledTime/30)%31+1;  // cycles 1-31
    gs=(gsCnt>16)? 32-gsCnt : gsCnt;  // goes 1-16...15-1
    for (i=1; i<=nleds; i++) {
      if(glow[i] && gs>(ledTime%16)) on[i]=true;
    }
  }
  // -------- handle Brt & lighting levels
  for (i=1; i<=nleds; i++) {
    if (brt[i]) on[i]=true;
    if (levelOn[i]>(ledTime%16)) on[i]=true;  // provide for lighting of 0-16 levels
  }

  // implement a sparkly arc option
  for (i=1; i<=nleds; i++) if (arc[i]) {
    t=(ledTime/101)%4;
    if      (t==0 && (ledTime%15==0)) on[i]=true;
    else if (t==1 && (ledTime%3==0)) on[i]=true;  // spark
    else if (t==2 && (ledTime%11==0)) on[i]=true;
    else if (t==3 && (ledTime%7==0)) on[i]=true;
  }

  //if (flashTime) on[dimFlash] = (dim[dimFlash])?  0:(ledTime%15==0);  // for 200ms make it dim, if dim make it off
  if (flashTime) on[dimFlash] = (dim[dimFlash] || arc[dimFlash] || glow[dimFlash])?  0:(ledTime%15==0);  // for 200ms make it dim, if dim make it off
  on[brightOne] = true;
  on[spriteOne] = true;
  on[spriteTwo] = true;
  if (flashTime && flashLed) on[flashLed] = (brt[flashLed] || glow[flashLed])? false:true;

  if ((msCnt%250) < 80) on[blinkLed] = (dim[blinkLed] || dimLED);

  if (on[dimIt]) {
    if (dim[dimIt]) on[dimIt]=msCnt&1; // dim 1/2
    else if ((++dimitCnt%3)>0) on[dimIt]=false;  // 2/3 Dimming for any and all other LED lite level
  }
  
  // -------------- drive the resulting Leds of interest
  for (i=0; i<nleds; i++) {
    ledn = i+1;
    Lite = lites[i];
    digitalWrite(Lite, (on[ledn]==ON_STATE));
  }
}

// ------------------
void clearDisp() {
    onSet=false;
    litLED=0;
    dimLED=0;
    dimIt=0;
    brightOne = 0;
    flashLed = 0;
    dimFlash = 0;
    blinkLed=0;
    spriteOne = 0;
    spriteTwo = 0;
    for (int i=0; i<=nleds; i++) {
      dim[i] =false;
      lit[i]=false;
      brt[i]=false;
      arc[i]=false;
      glow[i]=false;
      levelOn[i]=0;
    }
    if (!onSet) refreshDisp();
}

// -----------------------
void refreshWait(int msec) {
  while (msec>0) {
    refreshDisp();
    delay(1);
    msec--;
  }
}


// ------------------------------------------------------------
//        Button Input Processing
// Optimized for games, minimizing button press detection time,
// while practically elliminating false state changes (due to button contact noise)
void scanBtns() {
//  static byte btnState = 0, currState, priorBSt;  Now public vars
  static byte lastState;
  bool b1,b2,b3,b4;
  byte i,k;
  unsigned int st; // state time
  static unsigned long lastChgMs;
  static byte sscnt=0;  // steady state cnt
  
  if (btn4) ESC = (msCnt-escCnt)>2000;   // generally 1000 msCnt = 1200 millis
  else ESC = false;

  if (secsIdle>4*60) { // every 4 mins.
    secsIdle=0;
    digitalWrite(ledOnBrd, HIGH);  beep(100,20); 
    digitalWrite(ledOnBrd, LOW);   beep(100); // say HEY! Don't forget me
  }

  BtnChanged = btn1Changed = btn2Changed = btn3Changed = btn4Changed = false;
  b1 = btn[1] = ! digitalRead(button[0]);
  b2 = btn[2] = ! digitalRead(button[1]);
  b3 = btn[3] = ! digitalRead(button[2]);
  b4 = btn[4] = ! digitalRead(button[3]);
  currState = int(b1) + 2*int(b2) + 4*int(b3) + 8*int(b4);  // combined button states
  if (currState != lastState) { // little debounce: a new state must repeat 2 scans in a row
                                // do not increase this, else sprite hit detection will sufer
    lastState=currState;
    if (currState==btnState) lastChgMs=millis(); // so set new begin timing
    sscnt=0;
    return;
  }
  sscnt++;
  
  st=millis()-lastChgMs;
  if (sscnt>14 && currState==0) st=99;  // set to fix false button state
  if ((st>14 || currState!=priorBSt) && currState != btnState) {  // the 'button state' has changed & not too soon
    ESC = false;
    lastChg = msCnt;  // was: millis();
    lastChgMs = millis();
    escCnt = msCnt;
    priorBSt = btnState;
    btnState = currState;
    BtnChanged=true;
    secsIdle=0;
    sscnt=0;

    Btn = b1 || b2 || b3 || b4;
    btns = (b1 + b2 + b3 + b4) > 1;
    if (debugPrt)  {Serial.print(" Btns:  ");  Serial.print(b1); Serial.print(b2); Serial.print(b3); Serial.print(b4); }
    if (debugPrt)  {Serial.print(" CurrState Btn State:  ");  Serial.println(currState);}
    btnNum =  (b1)? 1 : ((b2)? 2 : ((b3)? 3 : ((b4)? 4 : 0)));  // logical button # (1-4) (multiple presses not supported)
    Btn = b1 || b2 || b3 || b4;
    if(b1 != btn1) {btn1 = b1; btn1Changed = true;}
    if(b2 != btn2) {btn2 = b2; btn2Changed = true;}
    if(b3 != btn3) {btn3 = b3; btn3Changed = true;}
    if(b4 != btn4) {btn4 = b4; btn4Changed = true;}
  } else {
    if (currState!=btnState) {lastChgMs=millis();}  // update minimum Chg timing
  }
}

// -------------
void btnsNow() {  // gets an updated state without scanBtns's 'debounce' interference
  scanBtns();
  if (BtnChanged || (currState == btnState)) return;
  // else likly change in progress
  delayMicroseconds(100);
  scanBtns();
}

// ------------------------------------------------------------
//        Audio support

void beep(int msecs, int delayms){  // works OK with Buzzers, best with Speakers  (1KHz)
  for (int i=0; i<msecs; i++) {         // loop for so many 'msecs'
    digitalWrite ( AUDIO_OUT, HIGH); 
    delayMicroseconds(500);    
    digitalWrite ( AUDIO_OUT, LOW);
    delayMicroseconds(500);
    refreshDisp();  // just spent 1 ms, lets refreshDisp now
  }
  refreshWait(delayms);          // pause for delayms millisecs   
}

void bop(int msecs, int delayms=0){   // a lower frequency ~500Hz tone then does 'beep()'
  for (int i=0; i<((msecs+1)/2); i++) {         // loop for 'msecs' milliseconds
    digitalWrite ( AUDIO_OUT, HIGH); 
    if (Loud) digitalWrite ( AUDIO_NEG, LOW);
    refreshWait(1);
    digitalWrite ( AUDIO_OUT, LOW);
    if (Loud) digitalWrite ( AUDIO_NEG, HIGH);
    refreshWait(1);
  }
  if (Loud) digitalWrite ( AUDIO_NEG, LOW);
  refreshWait(delayms);          // pause for delayms millisecs
}

void boop(int msecs, int delayms){   // produces a lower frequency ~250Hz tone then does 'beep()'
  for (int i=0; i<((msecs+3)/4); i++) {         // loop for 'msecs' milliseconds
    digitalWrite ( AUDIO_OUT, HIGH); 
    refreshWait(2);
    digitalWrite ( AUDIO_OUT, LOW);
    refreshWait(2);
  }
  refreshWait(delayms);          // pause for delayms millisecs
}

void buZZ(int msecs, int delayms){   // produces a lower frequency tone then does 'beep()'
  for (int i=0; i<((msecs+5)/6); i++) {         // loop for 'msecs' milliseconds
    beep(2,1);
    boop(2);
  }
  refreshWait(delayms);          // pause for delayms millisecs
}
