#include <Arduino.h>
#include <Wire.h>
#include <stdarg.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include "angry.h"
#include "embarrassed.h"
#include "laugh.h"
#include "proud.h"
#include "relaxed.h"
#include "quiet.h"
#include "music.h"
#include "cheerful.h"
#include "playful.h"
#include "tears.h"
#include "affection.h"
#include "intense.h"
#include "outburst.h"
#include "yawn.h"
#include "goofy.h"
#include "surprised.h"
#include "defiant.h"
#include "state_titles.h"
#include "sad_wipe.h"
#include "sad_rain.h"
#include "grumpy_turn.h"
#include "grumpy_stomp.h"
#include "sweet_kiss.h"
#include "sweet_shy.h"
#include "cozy_story.h"
#include "lesson_timer.h"
#include "mobile_games.h"
#include "motion_sensor.h"
#include "orientation.h"
Orientation180 screenOrientation;
// CAD rotates the IMU +90 degrees about Z: sensor X points toward case +Y.
// Confirm this sign against the installed sensor's axis marking during commissioning.
constexpr float SCREEN_UP_SIGN = 1.0f;

Adafruit_SSD1306 display(128, 64, &Wire, -1, 400000, 400000);
// Keep only the emotional portion of mixed source clips, excluding happy endings.
const AnimatedGIF sleepyEyes = {9,128,64,affection_delays,affection_frames};
const AnimatedGIF crying = {44,128,64,tears_delays,tears_frames};
const AnimatedGIF downcast = {18,128,64,outburst_delays+37,outburst_frames+37};
const AnimatedGIF scowl = {33,128,64,proud_delays+35,proud_frames+35};
const AnimatedGIF protest = {45,128,64,intense_delays,intense_frames};
const AnimatedGIF hearts = {18,128,64,affection_delays+24,affection_frames+24};
const AnimatedGIF bashful = {44,128,64,embarrassed_delays,embarrassed_frames};
const AnimatedGIF kisses = {15,128,64,affection_delays+60,affection_frames+60};
enum SceneEffect { PLAIN, DREAM, DOZE, WAKE, STARTLE, RAIN, TISSUE, SIGH, PUDDLE, STEAM, TURN, GRUMBLE, STOMP, LETTER, HEARTFLIGHT, SHY, SPARKLE };
struct Scene {
  const AnimatedGIF* clip;
  SceneEffect effect;
  uint16_t duration;
};
struct CharacterState {
  const char* name;
  Scene scenes[6];
  uint8_t sceneCount;
  const AnimatedGIF* petReaction;
};
const CharacterState states[] = {
  {"Cozy", {{&cozy_story_gif,PLAIN,1}, {&relaxed_gif,PLAIN,1}, {&yawn_gif,PLAIN,1}, {&sleepyEyes,DOZE,10000}, {&sleepyEyes,DREAM,11000}}, 5, &sleepyEyes},
  {"Bouncy", {{&cheerful_gif,PLAIN,5000}, {&playful_gif,PLAIN,5000}, {&goofy_gif,PLAIN,5000}, {&laugh_gif,PLAIN,2100}, {&playful_gif,SPARKLE,5000}}, 5, &laugh_gif},
  {"Sad", {{&sad_wipe_gif,PLAIN,1}, {&sad_rain_gif,PLAIN,1}, {&downcast,PLAIN,2400}}, 3, &downcast},
  {"Grumpy", {{&grumpy_turn_gif,PLAIN,1}, {&grumpy_stomp_gif,PLAIN,1}, {&scowl,PLAIN,1}}, 3, &scowl},
  {"Sweetie", {{&sweet_kiss_gif,PLAIN,1}, {&hearts,PLAIN,2500}, {&sweet_shy_gif,PLAIN,1}, {&bashful,PLAIN,1}, {&kisses,PLAIN,1}}, 5, &hearts},
};
constexpr uint8_t STATE_COUNT = sizeof(states) / sizeof(states[0]);
const AnimatedGIF* active = states[0].scenes[0].clip;
uint32_t sceneStarted = 0;
bool sceneRunning = false;
uint32_t due, lastReport, buttonClicks;
uint32_t lastLoopUs = 0, maxLoopUs = 0, maxRenderUs = 0, renderedFrames = 0;
uint16_t frame = 0;
uint8_t outgoing[1024];
uint8_t transitionStep = 4;
bool windTransition = false;
uint32_t titleUntil = 0;
bool titleVisible = false;
constexpr uint8_t WIND_FRAMES = 14;
uint8_t selected = 0, sceneIndex = 0;
bool petting = false;

void logEvent(const char* format, ...) {
  char message[220];
  va_list args;
  va_start(args, format);
  int length = vsnprintf(message, sizeof(message), format, args);
  va_end(args);

  if (length > 0 && length < (int)sizeof(message) && Serial.availableForWrite() >= length + 16) {
    Serial.write((const uint8_t*)message, length);
  }

}

void selectClip(const AnimatedGIF* clip, uint32_t now, bool wind = false) {
  sceneRunning = false;
  windTransition = wind;

  if (wind) {
    titleVisible = true;
    titleUntil = now + 1400;
  }

  memcpy(outgoing, display.getBuffer(), sizeof(outgoing));
  transitionStep = 0;
  active = clip;
  frame = 0;
  due = now;
}

void drawStateTitle() {

  if (titleVisible && (int32_t)(titleUntil - millis()) > 0) {
    display.fillRect(0, 51, 128, 13, SSD1306_BLACK);
    display.drawBitmap(0, 51, stateTitles[selected], 128, 13, SSD1306_WHITE);
  } else {
    titleVisible = false;
  }

}

void drawWind(uint8_t step) {
  float t = (float)step / (WIND_FRAMES - 1);
  display.clearDisplay();
  // SSD1306 buffer is page-major; each byte holds eight vertical pixels.
  for (int y = 0; y < 64; ++y) {
    float lane = ((y / 4) * 7 % 11) / 10.0f;
    float progress = constrain((t - lane * .12f) / .64f, 0.0f, 1.0f);
    int shift = (int)(150 * progress * progress);
    int lift = (int)(progress * (lane - .5f) * 16);

    for (int x = 0; x < 128; ++x) {

      if (outgoing[x + (y / 8) * 128] & (1 << (y % 8))) {
        display.drawPixel(x + shift, y + lift, SSD1306_WHITE);
      }

    }

  }

  float reveal = constrain((t - .42f) / .58f, 0.0f, 1.0f);
  reveal = reveal * reveal * (3 - 2 * reveal);
  int incomingX = (int)(-128 * (1 - reveal));
  display.drawBitmap(incomingX, 0, active->frames[0], 128, 64, SSD1306_WHITE);

  if (step > 0 && step < WIND_FRAMES - 1) {
    for (int i = 0; i < 12; ++i) {
      int x = ((i * 37 + step * (9 + i % 4)) % 172) - 22;
      int y = 5 + (i * 17 % 54);
      display.drawFastHLine(x, y, 3 + i % 5, SSD1306_WHITE);

      if ((i + step) % 4 == 0) {
        display.drawFastVLine(x + 2, y - 2, 5, SSD1306_WHITE);
      }

    }

  }

  drawStateTitle();
  display.display();
}

void heart(int x, int y, int r) {
  display.fillCircle(x-r, y, r, SSD1306_WHITE);
  display.fillCircle(x+r, y, r, SSD1306_WHITE);
  display.fillTriangle(x-2*r, y, x+2*r, y, x, y+3*r, SSD1306_WHITE);
}

uint16_t frameDelay(uint16_t index) {
  uint16_t base = max((uint16_t)40, active->delays[index]);

  if (selected == 0) {
    SceneEffect effect = states[selected].scenes[sceneIndex].effect;

    if (!petting && (effect == WAKE || effect == STARTLE)) {
      return 50;
    }

    if (!petting && (effect == DREAM || effect == DOZE)) {
      return base * 5 / 2;
    }

  }

  return base;
}

uint32_t shakeUntil = 0;
bool shakeReaction = false;

void drawScene(uint32_t now) {

  if (shakeReaction && (int32_t)(shakeUntil-now)>0) {

    display.drawBitmap(0,0,surprised_gif.frames[(now/80)%surprised_gif.frame_count],128,64,SSD1306_WHITE);
    return;

  }

  shakeReaction = false;

  if (!sceneRunning) {
    sceneStarted = now;
    sceneRunning = true;
  }

  const Scene& scene = states[selected].scenes[sceneIndex];
  SceneEffect effect = petting ? PLAIN : scene.effect;
  uint32_t elapsed = now - sceneStarted;
  float t = min(1.0f, (float)elapsed / scene.duration);
  int x = 0, y = 0;

  if (effect == TURN) {
    x = t < .3f ? -(int)(t * 90) : (t < .7f ? -27 : -(int)((1-t)*90));
  }

  if (effect == STOMP && t > .2f && t < .8f) {
    y = (elapsed / 120 % 2) ? 3 : -2;
  }

  if (effect == DOZE) {
    y = (int)(6 * sinf(t * 3.141593f));
  }

  bool waking = effect == WAKE || effect == STARTLE;
  uint32_t wakeAt = effect == STARTLE ? 6500 : 5000;
  bool startled = waking && elapsed >= wakeAt && elapsed < wakeAt + 1100;
  const uint8_t* face = active->frames[frame];

  if (waking) {
    face = sleepyEyes.frames[(elapsed / 175) % sleepyEyes.frame_count];

    if (startled) {
      uint32_t jumpTime = elapsed - wakeAt;
      face = surprised_gif.frames[0];
      y = -(int)(sinf(min(1.0f, jumpTime / 450.0f) * 3.141593f) * (effect == STARTLE ? 9 : 4));
    } else if (elapsed >= wakeAt + 1100 && elapsed < wakeAt + 2300) {
      face = relaxed_gif.frames[min((uint32_t)17, (elapsed-wakeAt-1100)/100)];
    }

  }

  display.drawBitmap(x, y, face, 128, 64, SSD1306_WHITE);

  if (startled) {
    display.setTextSize(1);
    display.setTextColor(SSD1306_WHITE);
    display.setCursor(110, 8);
    display.print(effect == STARTLE ? "!!" : "!");
  }


  if (effect == DREAM || effect == DOZE || (waking && (elapsed < wakeAt || elapsed >= wakeAt + 2300))) {
    display.setTextSize(1);
    display.setTextColor(SSD1306_WHITE);
    for (int i=0; i<3; ++i) {
      int phase = (elapsed / 225 + i*13) % 42;
      display.setCursor(104+i*6, 35-phase);
      display.print("z");
    }
  }

  if (effect == RAIN) {
    for (int i=0;i<10;++i) {
      int yy=(elapsed/35+i*19)%76-8;
      int xx=6+i*13;

      if (t < .7f || yy > (t-.7f)*220) {
        display.drawLine(xx,yy,xx-2,yy+4,SSD1306_WHITE);
      }

    }
  }

  if (effect == TISSUE) {
    int xx = t < .5f ? (int)(t*180)-15 : (int)((1-t)*180)-15;
    int yy = 36 + (int)(3*sinf(t*25));
    display.fillRoundRect(xx,yy,23,17,3,SSD1306_WHITE);
    display.drawLine(xx+5,yy+4,xx+15,yy+12,SSD1306_BLACK);
  }

  if (effect == SIGH) {
    int travel=(int)(t*35);
    display.drawCircle(91+travel,43-travel/3,2+(int)(t*4),SSD1306_WHITE);
    display.drawCircle(87+travel,47-travel/3,1,SSD1306_WHITE);
  }

  if (effect == PUDDLE) {
    int radius=3+(int)(t*18);
    display.drawRoundRect(64-radius,57,2*radius,6,3,SSD1306_WHITE);
    int drop=(elapsed/70)%23;
    display.drawFastVLine(77,34+drop,3,SSD1306_WHITE);
  }

  if (effect == STEAM || effect == GRUMBLE) {
    for (int i=0;i<3;++i) {
      int xx=effect == STEAM ? 6+i*4 : 107+i*5;
      int yy=8+(elapsed/80+i*5)%16;
      display.drawLine(xx,yy,xx+3,yy-4,SSD1306_WHITE);
      display.drawLine(xx+3,yy-4,xx,yy-8,SSD1306_WHITE);
    }
  }

  if (effect == STOMP && t > .2f && t < .8f) {
    display.drawLine(9,59,22,54,SSD1306_WHITE);
    display.drawLine(106,54,119,59,SSD1306_WHITE);
  }

  if (effect == SHY) {
    for (int i=0;i<3;++i) {
      display.drawLine(19+i*4,40,17+i*4,44,SSD1306_WHITE);
      display.drawLine(101+i*4,40,99+i*4,44,SSD1306_WHITE);
    }
  }

  if (effect == HEARTFLIGHT) {
    heart(78+(int)(t*36), 45-(int)(t*35), 2+(int)(t*2));
  }

  if (effect == LETTER) {
    int yy=64-(int)(min(t*3,1.0f)*22);
    display.fillRect(46,yy,36,20,SSD1306_BLACK);
    display.drawRect(46,yy,36,20,SSD1306_WHITE);
    display.drawLine(46,yy,64,yy+10,SSD1306_WHITE);
    display.drawLine(64,yy+10,81,yy,SSD1306_WHITE);

    if (t > .4f) {
      heart(64, yy-(int)((t-.4f)*55), 3);
    }

  }

  if (effect == SPARKLE) {
    for (int i=0;i<6;++i) {
      int xx=7+i*22, yy=7+(i%2)*47;
      int radius=1+(elapsed/150+i)%3;
      display.drawFastHLine(xx-radius,yy,radius*2+1,SSD1306_WHITE);
      display.drawFastVLine(xx,yy-radius,radius*2+1,SSD1306_WHITE);
    }
  }

}

void setup() {
  Serial.begin(115200);
  Serial.setTxTimeoutMs(0);
  Wire.begin(3, 4);
  Wire.setTimeOut(50);
  SchoolTimer::begin();
  MotionSensor::begin();

  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C, true, false)) {
    logEvent("OLED allocation failed\n");
    while (true) { delay(100); }
  }

  display.clearDisplay();
  drawStateTitle();
  display.display();
}

void loop() {
  uint32_t loopUs = micros();

  if (lastLoopUs != 0) {
    maxLoopUs = max(maxLoopUs, loopUs - lastLoopUs);
  }

  lastLoopUs = loopUs;
  uint32_t now = millis();
  bool resumeCharacter=false;
  bool characterClick=false;

  bool timerOwns=SchoolTimer::tick(display, now, resumeCharacter, characterClick);
  bool shaken=MotionSensor::shake(now);

  if (MotionSensor::fresh && screenOrientation.update(now, SCREEN_UP_SIGN*MotionSensor::axesG[0], MotionSensor::axesG[1], MotionSensor::axesG[2])) {

    // Rotate panel addressing so direct framebuffer writes and game frames agree.
    display.ssd1306_command(SSD1306_SEGREMAP | (screenOrientation.flipped ? 0 : 1));
    display.ssd1306_command(screenOrientation.flipped ? SSD1306_COMSCANINC : SSD1306_COMSCANDEC);

  }

  if (SchoolTimer::mode==LessonClock::GAMES) {

    if (MobileGames::tick(display,now,digitalRead(5)==HIGH,SchoolTimer::priorityOverlay)) {

      SchoolTimer::mode=LessonClock::CHARACTER;
      SchoolTimer::button=LessonClock::Button{};
      SchoolTimer::covered=false;
      selectClip(states[selected].scenes[sceneIndex].clip,now);

    }

    delay(1);
    return;

  }

  if (timerOwns) {
    petting=false;
    delay(1);
    return;
  }

  if (resumeCharacter) {
    display.setTextSize(1);
    display.setTextWrap(true);
    selectClip(states[selected].scenes[sceneIndex].clip, now);
  }

  if (shaken && !windTransition) {

    shakeReaction=true;
    shakeUntil=now+1200;

  }

  if (characterClick) {
    selected = (selected + 1) % STATE_COUNT;
    sceneIndex = 0;
    petting = false;
    ++buttonClicks;
    selectClip(states[selected].scenes[sceneIndex].clip, now, true);
    logEvent("button click state=%s\n", states[selected].name);
  }

  if ((int32_t)(now - due) >= 0) {
    uint32_t renderStart = micros();

    if (windTransition && transitionStep < WIND_FRAMES) {
      drawWind(transitionStep++);
      due = now + 35;

      if (transitionStep == WIND_FRAMES) {
        windTransition = false;
        transitionStep = 4;
        frame = 1;
        due = now + frameDelay(0);
      }

    } else if (transitionStep < 4) {
      static const uint8_t masks[] = {16, 32, 16, 0};

      if (transitionStep < 2) {
        memcpy(display.getBuffer(), outgoing, sizeof(outgoing));
      } else {
        display.clearDisplay();
        display.drawBitmap(0, 0, active->frames[0], 128, 64, SSD1306_WHITE);
      }

      uint8_t mask = masks[transitionStep];
      display.fillRect(0, 0, 128, mask, SSD1306_BLACK);
      display.fillRect(0, 64 - mask, 128, mask, SSD1306_BLACK);
      drawStateTitle();
      display.display();
      ++transitionStep;
      due = now + 35;
    } else {
      display.clearDisplay();
      drawScene(now);
      drawStateTitle();
      display.display();
      due = now + frameDelay(frame);
      ++frame;

      if (frame >= active->frame_count) {
        frame = 0;

        if (!petting && now - sceneStarted >= states[selected].scenes[sceneIndex].duration) {
          sceneIndex = (sceneIndex + 1) % states[selected].sceneCount;
          selectClip(states[selected].scenes[sceneIndex].clip, due);
        }

      }

    }

    maxRenderUs = max(maxRenderUs, micros() - renderStart);
    ++renderedFrames;
  }

  if (now - lastReport >= 3000) {
    lastReport = now;
    Wire.beginTransmission(0x3C);
    logEvent("state=%s action=%s scene=%u frame=%u total=%lu clicks=%lu I2C=%u loopMaxUs=%lu renderMaxUs=%lu\n", states[selected].name, petting ? "pet" : "idle", sceneIndex, frame, renderedFrames, buttonClicks, Wire.endTransmission(), maxLoopUs, maxRenderUs);
  }

  delay(1);
}
