// automated hand car-wash
#undef MY_HW
#ifdef MY_HW
const byte PinWater = 12;
const byte PinFoam  = 11;
const byte PinInp [] = { A1, A2, A3 };
enum { Water, Foam ,Coin_5, Coin_1, Coin_10};   // identifies pins
enum { OutOff = HIGH, OutOn = LOW };
enum { ButOff = HIGH, ButOn = LOW };      // depends on wiring
// ---------------------------------------------------------
#else
# include <LiquidCrystal_I2C.h>
# define I2C_ADDR    0x27
# define LCD_COLUMNS 16
# define LCD_LINES   2
LiquidCrystal_I2C lcd(I2C_ADDR, LCD_COLUMNS, LCD_LINES);
const byte PinWater = 9;
const byte PinFoam  = 10;
const byte PinInp [] = { 2, 3, 4, 5, 6 };
enum { Coin_1, Coin_5, Coin_10, Water, Foam };   // identifies pins
enum { OutOff = LOW,  OutOn = HIGH };      // depends on wiring
enum { ButOff = LOW,  ButOn = HIGH };     // depends on wiring
#endif
// -----------------------------------------------------------------------------
char s [90];
// -----------------------------------------------------------------------------
// common output routine, can also be used to output to LCD
void
disp (
    const char *s )
{
    Serial.println (s);
#ifndef MY_HW
    lcd.clear ();
    lcd.print (s);
#endif
}
// -----------------------------------------------------------------------------
// monitor all inputs, return index of input when pressed
//    or return NoInpt
const byte Ninp       = sizeof(PinInp);
const int NoInp       = -1;
byte inpState [Ninp];
int
chkInp ()
{
    for (int n = 0; n < Ninp; n++)  {
        byte inp = digitalRead (PinInp [n]);
#if 0
        sprintf (s, "inp %d %d", n, inp);
        disp (s);
        delay (1000);
#endif
        if (inpState [n] != inp)  {         // state change
            inpState [n] = inp;
            delay (10);                     // debounce
            if (ButOn == inp)               // pressed
                return n;
        }
    }
    return NoInp;
}
// -----------------------------------------------------------------------------
// add to the balance depending on coin and return balance
const int MinBalance = 20;
int balance;
int
updateBalance (
    int coin )
{
    switch (coin) {
    case Coin_1:
        balance += 1;
        break;
    case Coin_5:
        balance += 5;
        break;
    case Coin_10:
        balance += 10;
        break;
    default:                // ignore non-coin input
        return balance; 
        break;
    }
    sprintf (s, " balance %3d", balance);
    disp (s);
    return balance;
}
// -----------------------------------------------------------------------------
// state and event (inputs and tmr) state machine
enum { Off, Idle, WaterOn, FoamOn };
int state = Off;
// timers that track system on and water/foam usage times
struct Tmr {
    const unsigned long MsecPeriodMax;  // max period
    const char   *label;                // text label
    bool          active;
    unsigned long msec;
    unsigned long msecPeriod;           // remaining water/foam  period
}
tmr [] = {
    {  5000, "water"  },
    {  2000, "foam"   },
    { 30000, "system" },
};
const int Ntmr = sizeof(tmr)/sizeof(Tmr);
enum { T_Water, T_Foam, T_System };     // indices of timers
// -------------------------------------
void
loop (void)
{
    // -------------------------------------
    // handle timers
    unsigned long msec = millis ();
    for (int n = 0; n < Ntmr; n++)  {
        if (tmr [n].active)  {
            if (msec - tmr [n].msec >= tmr [n].msecPeriod) {
                tmr [n].active     = false;         // expired
                tmr [n].msecPeriod = 0;             // indicates time-out
            }
        }
    }
    // -------------------------------------
    // handle inputs and state
    int inp = chkInp ();
    // state machine containing checks for events
    switch (state)  {
    case Off:
        digitalWrite (PinWater, OutOff);
        digitalWrite (PinFoam,  OutOff);
        if (MinBalance <= updateBalance (inp))  {
            balance -= MinBalance;
            // reset timers & enable system timer
            for (int n = 0; n < Ntmr; n++)
                tmr [n].msecPeriod = tmr [n].MsecPeriodMax;
            tmr [T_System].active = true;
            tmr [T_System].msec   = msec;
            state    = Idle;
            disp ("System On");
        }
        break;
    // system on, water/foam off
    case Idle:
        if (0 == tmr [T_System].msecPeriod)  {
            state = Off;
            disp ("System time-out");
        }
        else if (Water == inp)  {
            if (0 < tmr [T_Water].msecPeriod)  {
                digitalWrite (PinWater, OutOn);
                tmr [T_Water].active = true;
                tmr [T_Water].msec   = msec;
                state = WaterOn;
                sprintf (s, "Water On - time remaining %lu",
                        tmr [T_Water].msecPeriod);
                disp (s);
            }
            else
                disp ("Water timed-out");
        }
        else if (Foam == inp && 0 < tmr [T_Foam].msecPeriod)  {
            if (0 < tmr [T_Foam].msecPeriod)  {
                digitalWrite (PinFoam, OutOn);
                tmr [T_Foam].active = true;
                tmr [T_Foam].msec   = msec;
                state = FoamOn;
                sprintf (s, "Foam On - time remaining %lu",
                        tmr [T_Foam].msecPeriod);
                disp (s);
            }
            else
                disp ("Foam timed-out");
        }
        break;
    // system on, foam on
    case FoamOn:
        if (0 == tmr [T_System].msecPeriod)  {      
            state = Idle;
            disp ("System time-out");
        }
        else if (0 == tmr [T_Foam].msecPeriod)  {
            digitalWrite (PinFoam, OutOff);
            state = Idle;
            disp ("Foam time-out");
        }
        else if (Foam == inp)  {
            digitalWrite (PinFoam, OutOff);
            tmr [T_Foam].active      = false;
            tmr [T_Foam].msecPeriod -= msec - tmr [T_Foam].msec;
            state = Idle;
            sprintf (s, "Foam Off - time remaining %lu",
                    tmr [T_Foam].msecPeriod);
            disp (s);
        }
        break;
    // system on, water on
    case WaterOn:
        if (0 == tmr [T_System].msecPeriod)  {
            state = Idle;
            disp ("System time-out");
        }
        else if (0 == tmr [T_Water].msecPeriod)  {
            digitalWrite (PinWater, OutOff);
            state = Idle;
            disp ("Water time-out");
        }
        else if (Water == inp)  {
            digitalWrite (PinWater, OutOff);
            tmr [T_Water].active      = false;
            tmr [T_Water].msecPeriod -= msec - tmr [T_Water].msec;
            state = Idle;
            sprintf (s, "Water Off - time remaining %lu",
                    tmr [T_Water].msecPeriod);
            disp (s);
        }
        break;
    }
}
// -----------------------------------------------------------------------------
void
setup (void)
{
    Serial.begin (9600);
    for (int n = 0; n < Ninp; n++)  {
#ifdef MyHW
        pinMode (PinInp [n], INPUT_PULLUP);
#else
        pinMode (PinInp [n], INPUT);
#endif
        inpState [n] = digitalRead  (PinInp [n]);
    }
    digitalWrite (PinWater, OutOff);
    digitalWrite (PinFoam,  OutOff);
    pinMode (PinWater, OUTPUT);
    pinMode (PinFoam,  OUTPUT);
#ifndef MY_HW
    lcd.init ();
#endif
    disp ("Ready");
}