
#include "Display.h"
#include <MD_Parola.h>
#include <MD_MAX72xx.h>
#include <SPI.h>
#include "MD_RobotEyes.h"
extern "C" {
#include "user_interface.h"
}

#define DISPLAY_CLK_PIN D5
#define DISPLAY_DATA_PIN D7
#define DISPLAY_CS_PIN D6
#define VERTICAL_BAR_STARTS_TOP false

#define HARDWARE_TYPE MD_MAX72XX::FC16_HW
#define MAX_DISPLAY_DEVICES 4

static MD_Parola P = MD_Parola(HARDWARE_TYPE, DISPLAY_DATA_PIN, DISPLAY_CLK_PIN, DISPLAY_CS_PIN, MAX_DISPLAY_DEVICES);
static MD_RobotEyes E;
static MD_MAX72XX *graphicDisplay = NULL;


void Display::begin() {
  int charCode;
#if VERTICAL_BAR_STARTS_TOP
  static uint8_t verticalBarFont[] = {
    1, 0x00, /* blank */
    1, 0x01, /* 1 dot */
    1, 0x03, /* 2 dots */
    1, 0x07,
    1, 0x0f,
    1, 0x1f,
    1, 0x3f,
    1, 0x7f,
    1, 0xff, /* vertical bar completely set */
  }; // columns from right to left, each byte is a single column
#else
  static uint8_t verticalBarFont[] = {
    1, 0x00, /* blank */
    1, 0x80, /* 1 dot */
    1, 0xc0, /* 2 dots */
    1, 0xe0,
    1, 0xf0,
    1, 0xf8,
    1, 0xfc,
    1, 0xfe,
    1, 0xff, /* vertical bar completely set */
  }; // columns from right to left, each byte is a single column
#endif
  static uint8_t newZero[] = {0x05, 0x3e, 0x41, 0x41, 0x41, 0x3e, 0x00};

  P.begin();
  // P.setZoneEffect(0, true, PA_FLIP_LR);
  graphicDisplay = P.getGraphicObject();
  E.begin(graphicDisplay, 1); // start at 2nd module, count starts with 0

  P.setIntensity(1);
  for (charCode=1; charCode<=9; ++charCode) {
    P.addChar(charCode, verticalBarFont+2*(charCode-1));
  }
  // replace the 0 characters, we do not like the "slash"
  P.addChar('0', newZero);

  char intro[] = {' ', 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x00};
  P.print(intro);
}

typedef struct
{
  char name[7];
  MD_RobotEyes::emotion_t e;
  uint16_t timePause;  // in milliseconds
} sampleItem_t;

static const sampleItem_t eSeq[] =
{
  { "Nutral", MD_RobotEyes::E_NEUTRAL, 1000 },
  { "Blink" , MD_RobotEyes::E_BLINK, 1000 },
  { "Wink"  , MD_RobotEyes::E_WINK, 1000 },
  { "Left"  , MD_RobotEyes::E_LOOK_L, 1000 },
  { "Right" , MD_RobotEyes::E_LOOK_R, 1000 },
  { "Up"    , MD_RobotEyes::E_LOOK_U, 1000 },
  { "Down"  , MD_RobotEyes::E_LOOK_D, 1000 },
  { "Angry" , MD_RobotEyes::E_ANGRY, 1000 },
  { "Sad"   , MD_RobotEyes::E_SAD, 1000 },
  { "Evil"  , MD_RobotEyes::E_EVIL, 1000 },
  { "Evil2" , MD_RobotEyes::E_EVIL2, 1000 },
  { "Squint", MD_RobotEyes::E_SQUINT, 1000 },
  { "Dead"  , MD_RobotEyes::E_DEAD, 1000 },
  { "ScanV" , MD_RobotEyes::E_SCAN_UD, 1000 },
  { "ScanH" , MD_RobotEyes::E_SCAN_LR, 1000 },
};
#define DISPLAY_ANIM_NAME false

void Display::loopEyeAnimation() {
  // show startup animation
  boolean animationFinished = false;
  static uint32_t timeStartDelay;
  static uint8_t index = ARRAY_SIZE(eSeq);
  static enum { S_IDLE, S_TEXT, S_ANIM, S_PAUSE } state = S_IDLE;

  animationFinished = E.runAnimation();

  switch (state)
  {
    case S_IDLE:
      index++;
      if (index >= ARRAY_SIZE(eSeq))
        index = 0;
      P.displayClear();
      #if DISPLAY_ANIM_NAME
        E.setText(eSeq[index].name);
      #endif
      state = S_TEXT;
      break;

    case S_TEXT: // wait for the text to finish
      if (animationFinished)  // text animation is finished
      {
        E.setAnimation(eSeq[index].e, true);
        state = S_ANIM;
      }
      break;

    case S_ANIM:  // checking animation is completed
      if (animationFinished)  // animation is finished
      {
        timeStartDelay = millis();
        state = S_PAUSE;
      }
      break;

    case S_PAUSE: // non blocking waiting for a period between animations
      if (millis() - timeStartDelay >= eSeq[index].timePause)
        state = S_IDLE;
      break;

      default:
      state = S_IDLE;
      break;
  }
}

void Display::loop() {
  char minuteProgressIndicator;
  char timeBuffer[10];

  minuteProgressIndicator = currentTime->getSeconds()/6.7 + 1; // char code 1-8 show vertical bar
  if (minuteProgressIndicator > 9) minuteProgressIndicator = 9;
  snprintf(timeBuffer, 10, "%c %2d:%02d", minuteProgressIndicator, currentTime->getHours(), currentTime->getMinutes());

  // standard procedure to display
  static uint32_t last_clock_refresh = 0;
  static uint32_t lastTimeClockNameShown = 0;
  static boolean showingClockName = false;

  if (showingClockName) {
    if (millis() - lastTimeClockNameShown > clockNameDurationTime_ms) {
      // stop showingClockName
      showingClockName = false;
    }
  } else {
    if ((millis() - lastTimeClockNameShown > displayClockNameEvery_ms)
        && (currentTime->getSeconds() < 60-doNotShowClockNameBeforeAndAfterMinuteChange_s)
        && (currentTime->getSeconds() > doNotShowClockNameBeforeAndAfterMinuteChange_s)) {
      // re-initialize, that fixes display problems due to electrical relais feedbacks
      reInitializeDisplay();
      P.setIntensity(2);
      P.print(currentTime->getClockName());
      lastTimeClockNameShown = millis();
      showingClockName = true;
    } else {
      // showing clock
      if (millis() - last_clock_refresh > displayRefresh_ms) {
        // re-initialize, that fixes display problems due to electrical relais feedbacks
        reInitializeDisplay();
        P.setIntensity(1);
        P.print(timeBuffer);
        last_clock_refresh = millis();
      }
    }
  }
}

void Display::reInitializeDisplay() {
  uint32_t free = system_get_free_heap_size();
  #define REINIT_AFTER_ms 5000
  #define AVOID_REINIT_BEFORE_AND_AFTER_FULLMINUTE_FOR_s 3

  if (last_reinit_ts == 0) last_reinit_ts = millis();

  if (millis() - last_reinit_ts > REINIT_AFTER_ms
      && currentTime->getSeconds() < 60 - AVOID_REINIT_BEFORE_AND_AFTER_FULLMINUTE_FOR_s
      && currentTime->getSeconds() > AVOID_REINIT_BEFORE_AND_AFTER_FULLMINUTE_FOR_s) {
    P.begin();
    Serial.print("reinit display, free="); Serial.println(free);
    last_reinit_ts = millis();
  }
}

void Display::print(const char *s) { P.print(s); }
void Display::print(const String s) { P.print(s); }
