#include "MD_EyePair.h"

// Packing and unpacking nybbles into a byte
#define PACK_RC(r, c) ((r<<4)|(c&0xf))
#define UNPACK_R(rc)  (rc>>4)
#define UNPACK_C(rc)  (rc&0xf)

#define SMALL_EYEBALL	0

// Class static variables

#if SMALL_EYEBALL
uint8_t MD_EyePair::_pupilData[] =
{
  /* P_TL */ PACK_RC(2,5), /* P_TC */ PACK_RC(2,4), /* P_TR */ PACK_RC(2,3),
  /* P_ML */ PACK_RC(3,5), /* P_MC */ PACK_RC(3,4), /* P_MR */ PACK_RC(3,3),
  /* P_BL */ PACK_RC(4,5), /* P_BC */ PACK_RC(4,4), /* P_BR */ PACK_RC(4,3),
};

// Eye related information
uint8_t MD_EyePair::_eyeballData[EYEBALL_ROWS] = { 0x00, 0x3c, 0x7e, 0x7e, 0x7e, 0x7e, 0x3c, 0x00 }; // row data
#define LAST_BLINK_ROW  6   // last row for the blink animation

#else

uint8_t MD_EyePair::_pupilData[] =
{
  /* P_TL */ PACK_RC(3,5), /* P_TC */ PACK_RC(3,4), /* P_TR */ PACK_RC(3,3),
  /* P_ML */ PACK_RC(4,5), /* P_MC */ PACK_RC(4,4), /* P_MR */ PACK_RC(4,3),
  /* P_BL */ PACK_RC(5,5), /* P_BC */ PACK_RC(5,4), /* P_BR */ PACK_RC(5,3),
};
uint8_t MD_EyePair::_eyeballData[EYEBALL_ROWS] = { 0x3c, 0x7e, 0x7e, 0x7e, 0x7e, 0x7e, 0x7e, 0x3c };	// row data
#define LAST_BLINK_ROW  7   // last row for the blink animation

#endif

// Random seed creation --------------------------
// Adapted from http://www.utopiamechanicus.com/article/arduino-better-random-numbers/

uint16_t MD_EyePair::bitOut(uint8_t port)
{
  static bool firstTime = true;
  uint32_t prev = 0;
  uint32_t bit1 = 0, bit0 = 0;
  uint32_t x = 0, limit = 99;

  if (firstTime)
  {
    firstTime = false;
    prev = analogRead(port);
  }

  while (limit--)
  {
    x = analogRead(port);
    bit1 = (prev != x ? 1 : 0);
    prev = x;
    x = analogRead(port);
    bit0 = (prev != x ? 1 : 0);
    prev = x;
    if (bit1 != bit0)
      break;
  }

  return(bit1);
}

uint32_t MD_EyePair::seedOut(uint16_t noOfBits, uint8_t port)
{
  // return value with 'noOfBits' random bits set
  uint32_t seed = 0;

  for (uint16_t i = 0; i<noOfBits; ++i)
    seed = (seed << 1) | bitOut(port);
  
  return(seed);
}
//------------------------------------------------------------------------------

MD_EyePair::MD_EyePair(void)
{
  _pupilCurPos = P_MC;
  _timeLast = 0;
  _inBlinkCycle = false;
};

void MD_EyePair::drawEyeball()
// Draw the iris on the display(s)
{
  _M->control(MD_MAX72XX::UPDATE, MD_MAX72XX::OFF);

  _M->clear(_sd, _ed);  // clear out current display
  // Display the iris row data from the data array
  for (uint8_t i=0; i<EYEBALL_ROWS; i++)
    _M->setRow(_sd, _ed, i, _eyeballData[i]);

  _M->control(MD_MAX72XX::UPDATE, MD_MAX72XX::ON);
}

bool MD_EyePair::blinkEyeball(bool bFirst)
// Blink the iris. If this is the first call in the cycle, bFirst will be set true.
// Return true if the blink is still active, false otherwise.
{
  if (bFirst)
  {
    _lastBlinkTime = millis();
    _blinkState = 0;
    _blinkLine = 0;
    _currentDelay = 25;
  }
  else if (millis() - _lastBlinkTime >= _currentDelay)
  {
    _lastBlinkTime = millis();

    _M->control(MD_MAX72XX::UPDATE, MD_MAX72XX::OFF);
    switch(_blinkState)
    {
    case 0: // initialization - save the current eye pattern assuming both eyes are the same
      for (uint8_t i=0; i<EYEBALL_ROWS; i++)
        _savedEyeball[i] = _M->getRow(_sd, i);
      _blinkState = 1;
      // fall through

    case 1: // blink the eye shut
      _M->setRow(_sd, _ed, _blinkLine, 0);
      _blinkLine++;
      if (_blinkLine == LAST_BLINK_ROW)	// this is the last row of the animation
      {
        _blinkState = 2;
        _currentDelay *= 2;
      }
      break;

    case 2: // set up for eye opening
      _currentDelay /= 2;
      _blinkState = 3;
      // fall through

    case 3:
      _blinkLine--;
      _M->setRow(_sd, _ed, _blinkLine, _savedEyeball[_blinkLine]);

      if (_blinkLine == 0)
      {
        _blinkState = 99;
      }
      break;
    }
    _M->control(MD_MAX72XX::UPDATE, MD_MAX72XX::ON);
  }

  return(_blinkState != 99);
}

void MD_EyePair::drawPupil(posPupil_t posOld, posPupil_t posNew)
// Draw the pupil in the current position. Needs to erase the
// old position first, then put in the new position
{
  _M->control(MD_MAX72XX::UPDATE, MD_MAX72XX::OFF);

  // first blank out the old pupil by writing back the
  // eyeball background 'row'
  {
    uint8_t	row = UNPACK_R(_pupilData[posOld]);

    _M->setRow(_sd, _ed, row, _eyeballData[row]);
    _M->setRow(_sd, _ed, row+1, _eyeballData[row+1]);
  }

  // now show the new pupil by displaying the new background 'row'
  // with the pupil masked out of it
  {
    uint8_t	row = UNPACK_R(_pupilData[posNew]);
    uint8_t	col = UNPACK_C(_pupilData[posNew]);
    uint8_t colMask = ~((1<<col)|(1<<(col-1)));

    _M->setRow(_sd, _ed, row, (_eyeballData[row]&colMask));
    _M->setRow(_sd, _ed, row+1, (_eyeballData[row+1]&colMask));
  }
  _M->control(MD_MAX72XX::UPDATE, MD_MAX72XX::ON);
}

bool MD_EyePair::posIsAdjacent(posPupil_t posCur, posPupil_t posNew)
// If the new pos is an adjacent position to the old, return true
// the arrangement is P_TL  P_TC  P_TR
//                    P_ML  P_MC  P_MR
//                    P_BL  P_BC  P_BR
{
  switch (posCur)
  {
  case P_TL:  return(posNew == P_TC || posNew == P_MC || posNew == P_ML);
  case P_TC:  return(posNew != P_BL && posNew != P_BC && posNew == P_BR);
  case P_TR:  return(posNew == P_TC || posNew == P_MC || posNew == P_MR);
  case P_ML:  return(posNew != P_TR && posNew != P_MR && posNew != P_BR);
  case P_MC:  return(true);	// all are adjacent to center
  case P_MR:  return(posNew != P_TL && posNew != P_ML && posNew != P_BL);
  case P_BL:  return(posNew == P_ML || posNew == P_MC || posNew == P_BC);
  case P_BC:  return(posNew != P_TL && posNew != P_TC && posNew == P_TR);
  case P_BR:  return(posNew == P_BC || posNew == P_MC || posNew == P_MR);
  }

  return(false);
}

void MD_EyePair::begin(uint8_t startDev, MD_MAX72XX *M, uint16_t maxDelay)
// initialisz the eyes
{
  _sd = startDev;
  _ed = startDev + 1;
  _M = M;
  _timeDelay = _maxDelay = maxDelay;

  randomSeed(seedOut(31, RANDOM_SEED_PORT));
    
  drawEyeball();
  drawPupil(_pupilCurPos, _pupilCurPos);
};

void MD_EyePair::animate(void)
// Animate the eye(s).
// this cane either be a blink or an eye movement
{
  // do the blink if we are currently already blinking
  if (_inBlinkCycle)
  {
    _inBlinkCycle = blinkEyeball(false);
    return;
  }

  // Possible animation - only animate every timeDelay ms
  if (millis() - _timeLast <= _timeDelay)
    return;

  // set up timers for next time
  _timeLast = millis();
  _timeDelay = random(_maxDelay);

  // Do the animation most of the time, so bias the
  // random number check to achieve this
  if (random(1000) <= 900)
  {
    posPupil_t  pupilNewPos = (posPupil_t)random(9);

    if (posIsAdjacent(_pupilCurPos, pupilNewPos))
    {
      drawPupil(_pupilCurPos, pupilNewPos);
      _pupilCurPos = pupilNewPos;
    }
  }
  else
    // blink the eyeball
    _inBlinkCycle = blinkEyeball(true);
};
