// Use the LCD module display to create an mechanical pushwheel type display
// When numbers change they are scrolled up or down as if on a cylinder
//
// Increment or decrement numbers using the UP/DOWN keys on an LCD shield

// forum thread: https://forum.arduino.cc/index.php?topic=524021.msg3575861#new
//

#include <LiquidCrystal.h>

#define DEBUG 0

#if DEBUG
#define	PRINT(s, v)	{ Serial.print(F(s)); Serial.print(v); }
#define	PRINTX(s, v)	{ Serial.print(F(s)); Serial.print(v, HEX); }
#define PRINTS(s)   Serial.print(F(s));
#else
#define	PRINT(s, v)
#define PRINTS(s)
#endif

#define ARRAY_SIZE(a) (sizeof(a)/sizeof(a[0]))

// LCD definitions
// Pins
const uint8_t LCD_RS = 8;
const uint8_t LCD_ENA = 9;
const uint8_t LCD_D4 = 4;
const uint8_t LCD_D5 = LCD_D4 + 1;
const uint8_t LCD_D6 = LCD_D4 + 2;
const uint8_t LCD_D7 = LCD_D4 + 3;

LiquidCrystal lcd(LCD_RS, LCD_ENA, LCD_D4, LCD_D5, LCD_D6, LCD_D7);

// LCD Geometry
const uint8_t LCD_ROWS = 2;
const uint8_t LCD_COLS = 16;

// Display and animation parameters
const uint8_t ANIMATION_FRAME_TIME = 50;  // in milliseconds
const uint8_t DISP_R = 1;
const uint8_t DISP_C = 0;

// Structure to hold the data for each character to be displayed and animated.
// There can only be at most MAX_DIGITS displayed as the limit comes from
// the number of custom characters that can be defined for the number.
const uint8_t CHAR_ROWS = 8;
const uint8_t CHAR_COLS = 5;
const uint8_t MAX_DIGITS = 8;

typedef struct
{
  uint32_t timeLastFrame;     // time the last frame started animating
  uint8_t prev, curr;         // ASCII value for the character
  uint8_t index;              // animation progression index
  uint8_t charMap[CHAR_ROWS]; // generated custom char bitmap
} digitData_t;

digitData_t digits[MAX_DIGITS];

// User defined characters for digits. These should match the standard
// LCD ROM bitmaps for these digits.
uint8_t digitsMap[][CHAR_ROWS] =
{
  { 0x0e, 0x11, 0x13, 0x15, 0x19, 0x11, 0x0e, 0x00 }, // '0'
  { 0x04, 0x0c, 0x04, 0x04, 0x04, 0x04, 0x0e, 0x00 }, // '1'
  { 0x0e, 0x11, 0x01, 0x02, 0x04, 0x08, 0x1f, 0x00 }, // '2'
  { 0x1f, 0x02, 0x04, 0x02, 0x01, 0x11, 0x0e, 0x00 }, // '3'
  { 0x02, 0x06, 0x0a, 0x12, 0x1f, 0x02, 0x02, 0x00 }, // '4'
  { 0x1f, 0x10, 0x1e, 0x01, 0x01, 0x11, 0x0e, 0x00 }, // '5'
  { 0x06, 0x08, 0x10, 0x1e, 0x11, 0x11, 0x0e, 0x00 }, // '6'
  { 0x1f, 0x11, 0x01, 0x02, 0x04, 0x04, 0x04, 0x00 }, // '7'
  { 0x0e, 0x11, 0x11, 0x0e, 0x11, 0x11, 0x0e, 0x00 }, // '8'
  { 0x0e, 0x11, 0x11, 0x0f, 0x01, 0x02, 0x0c, 0x00 }, // '9'
};

// LCD Shield key definitions
const uint8_t KEY_ADC_PORT = A0;
const uint8_t KEY_NONE = 0;
const uint8_t KEY_RIGHT = 1;
const uint8_t KEY_UP = 2;
const uint8_t KEY_DOWN = 3;
const uint8_t KEY_LEFT = 4;
const uint8_t KEY_SELECT = 5;

typedef struct
{
  int   adcThreshold;
  uint8_t id;
} keyDef_t;

const keyDef_t keyDefTable[] =
{
  { 760, KEY_SELECT },
  { 535, KEY_LEFT },
  { 360, KEY_DOWN },
  { 160, KEY_UP },
  { 60, KEY_RIGHT }
};

uint8_t getKey(unsigned int input)
// The LCD shield has a voltage divider tree for a set of buttons.
// Convert ADC value passed in to key identifier using the table of value defined
// Return KEY_NONE if no key.
{
  static uint32_t lastCheck = 0;
  uint8_t  key = KEY_NONE;

  if (millis() - lastCheck > 200)
  {
    for (int k = 0; k < ARRAY_SIZE(keyDefTable); k++)
    {
      if (input < keyDefTable[k].adcThreshold)
        key = keyDefTable[k].id;  // Assume it will match this one
      else
        break;
    }
  }

  if (key != KEY_NONE)
    lastCheck = millis();

  return (key);
}

void updateDisplay(uint8_t r, uint8_t c)
// do the necessary to display current number scrolling anchored
// on the LHS at LCD (r, c) coordinates.
{
  lcd.noDisplay();
  PRINTS("\nuD ")
  // for each digit position, create the lcd custom character and
  // display the custom character, left to right.
  for (uint8_t i = 0; i < MAX_DIGITS; i++)
  {
    PRINT(".", digits[MAX_DIGITS - i - 1].curr);
    lcd.createChar(i, digits[MAX_DIGITS - i - 1].charMap);
    lcd.setCursor(c + i, r);
    lcd.write((uint8_t)i);
  }

  lcd.display();
}

boolean displayValue(uint32_t value)
// Display the required animated value on the LCD matrix and return true if
// an animation is current. Finite state machine will ignore new values while
// e=xisting animations are underway.
// Needs to be called repeatedly to ensure animations are completed smoothly.
{
  static enum { ST_INIT, ST_WAIT, ST_ANIM } state = ST_INIT;
  static uint32_t valueLast = 0;  // remember old value
  bool bUpdate = false;

  // finite state machine to control what we do
  switch (state)
  {
    case ST_INIT:	// Initialise the display - done once only on first call
      PRINTS("\nST_INIT");
      for (uint8_t i = 0; i < MAX_DIGITS; i++)
      {
        // separate digits
        digits[i].prev = digits[i].curr = value % 10;
        value = value / 10;
      }

      // Display the starting number
      for (uint8_t i = 0; i < MAX_DIGITS; i++)
        memcpy(digits[i].charMap, digitsMap[digits[i].curr], ARRAY_SIZE(digits[i].charMap));

      bUpdate = true;

      // Now we just wait for a change
      state = ST_WAIT;
      PRINTS("\nTo ST_WAIT");
      break;

    case ST_WAIT: // not animating - save new value digits and check if we need to animate
      //PRINTS("\nST_WAIT");
      if (valueLast != value)
      {
        state = ST_ANIM;  // a change has been found - we will be animating something

        for (int8_t i = 0; i < MAX_DIGITS; i++)
        {
          // separate digits
          digits[i].curr = value % 10;
          value = value / 10;

          // initialise animation parameters for this digit
          digits[i].index = 0;
          digits[i].timeLastFrame = 0;
        }
      }

      if (state == ST_WAIT) // no changes - keep waiting
        break;
    // else fall through as we need to animate from now

    case ST_ANIM: // currently animating a change
      // work out the new intermediate bitmap for each character
      for (uint8_t i = 0; i < MAX_DIGITS; i++)
      {
        if ((digits[i].prev != digits[i].curr) && // values are different ...
            (millis() - digits[i].timeLastFrame >= ANIMATION_FRAME_TIME)) // ... and timer has expired
        {
          PRINT("\nST_ANIM ", i);
          PRINT(" '", digits[i].prev);
          PRINT("'-'", digits[i].curr);
          PRINT("' idx ", digits[i].index);

          if (value > valueLast)
          {
            // scroll up
            // copy the bottom of the old digit from the index position and then the
            // top of new digit for the rest of the character
            PRINTS(" UP ");
            for (int8_t p = 0; p < CHAR_ROWS; p++)
            {
              if (p < CHAR_ROWS - digits[i].index)
                digits[i].charMap[p] = digitsMap[digits[i].prev][p + digits[i].index];
              else
                digits[i].charMap[p] = digitsMap[digits[i].curr][p - CHAR_ROWS + digits[i].index];
            }

            bUpdate = true;
          }
          else
          {
            // scroll down
            // copy the bottom of new digit up to the index position and then from
            // the start of the old digit for the rest of the character
            PRINTS(" DWN ");
            for (uint8_t p = 0; p < CHAR_ROWS; p++)
            {
              if (p < digits[i].index)
                digits[i].charMap[p] = digitsMap[digits[i].curr][p + CHAR_ROWS - digits[i].index];
              else
                digits[i].charMap[p] = digitsMap[digits[i].prev][p - digits[i].index];
            }

            bUpdate = true;
          }

          // set new parameters for next animation and check if we are done
          digits[i].index++;
          digits[i].timeLastFrame = millis();
          if (digits[i].index > CHAR_ROWS)
            digits[i].prev = digits[i].curr;  // done animating
        }
      }

      // are we done animating?
      {
        boolean allDone = true;

        for (uint8_t i = 0; allDone && (i < MAX_DIGITS); i++)
        {
          allDone = allDone && (digits[i].prev == digits[i].curr);
        }

        if (allDone)
        {
          valueLast = value;
          state = ST_WAIT;
        }
      }
      break;

    default:
      state = ST_INIT;
  }

  if (bUpdate) updateDisplay(DISP_R, DISP_C);

  return (state == ST_WAIT);   // animation has ended
}

void setup()
{
#if DEBUG
  Serial.begin(57600);
#endif // DEBUG
  PRINTS("\n[LCD PushWheel]")

  // initialise LCD display
  lcd.begin(LCD_COLS, LCD_ROWS);
  lcd.clear();
  lcd.noCursor();
  lcd.print("Pushwheel Demo");

  pinMode(KEY_ADC_PORT, INPUT);
};

void loop()
{
  static uint32_t value = 12345678;
  uint8_t k = getKey(analogRead(KEY_ADC_PORT));
  static bool upOs, dnOs, upSetup, dnSetup, upTrigger, dnTrigger;
  //
  // mods for pushbutton control
  //
  //if (k == KEY_UP) value++;
  // else if (k == KEY_DOWN) value--;

  upTrigger = ((digitalRead(3) == LOW) ? true : false);
  upOs = upTrigger && upSetup;
  upSetup = !upTrigger;
  if (upOs) value++;

  dnTrigger = (digitalRead(2) == LOW ? true : false);
  dnOs = dnTrigger && dnSetup;
  dnSetup = !dnTrigger;
  if (dnOs) value--;



  displayValue(value);
}