// Shared parts of the macropad games: keys, scores, text, the game-over box.
// Keys: L = left (D4), M = middle (D3), R = right (D2), the same wiring as the PC firmware.
#pragma once

#include <EEPROM.h>
#include <U8g2lib.h>
#include <Wire.h>

enum Key : uint8_t { KEY_L, KEY_M, KEY_R, KEY_COUNT };

const uint8_t KEY_PINS[KEY_COUNT] = {4, 3, 2};
const uint8_t FRAME_MS = 33;                 // about 30 frames per second
const uint8_t RELEASE_FRAMES = 2;            // a key counts as released after 2 open frames (~66 ms): short contact dropouts are ignored
const uint16_t MENU_HOLD_MS = 1000;          // hold L and R together 1 s: back to the menu
const uint16_t OVER_PAUSE_MS = 700;          // after "game over" keys are ignored a moment, so the last press does not restart
const int HISCORE_ADDR = 32;                 // 2 bytes per game; the PC firmware uses address 0
const uint32_t I2C_TIMEOUT_US = 5000;        // one OLED transfer takes under 1 ms at 400 kHz

// OLED mounted upside down, as in the PC firmware. Buffer for a quarter of the screen (256 bytes):
// the picture is drawn in 4 strips, the whole 1 KB buffer would not leave enough RAM for the games.
U8G2_SSD1306_128X64_NONAME_2_HW_I2C oled(U8G2_R2, U8X8_PIN_NONE);

// The OLED gets the picture over I2C about 10 times a second (drawing a frame takes ~100 ms on the Nano).
// Sometimes a transfer fails on the wires (measured on Roman's board 13.11: about once in 5 s, at 100 kHz
// as well as at 400 kHz, mostly at the STOP after a transfer). The screen may then hold SDA low and wait for
// clocks that never come. Wire with no timeout waited for that bus forever: the picture froze, no key
// helped, only unplugging (games v1). Games v2 gave up after 5 ms and started the screen again with
// oled.begin(), but begin() switches the screen off and clears it: it blinked every few seconds.
// Now (v3): right after the failed transfer (not at the end of the frame) up to 9 clocks on SCL let the
// screen finish its byte, a STOP frees the bus, Wire starts again and the screen settings are sent once
// more WITHOUT "display off"; then the whole frame is painted again (loop()).
const uint8_t OLED_ADDR = 0x3C;
const uint8_t OLED_SETTINGS[] PROGMEM = {    // U8g2's SSD1306 start-up list without 0xAE (display off)
  0xD5, 0x80, 0xA8, 0x3F, 0xD3, 0x00, 0x40, 0x8D, 0x14, 0x20, 0x00, 0xA1, 0xC8,
  0xDA, 0x12, 0x81, 0xCF, 0xD9, 0xF1, 0xDB, 0x40, 0x2E, 0xA4, 0xA6, 0xAF,
};

uint16_t oled_recoveries = 0;                // how many bus failures were mended (answered to "i2c?")

bool oled_glitched = false;                  // a transfer failed during this frame

void oled_recover();

extern "C" uint8_t u8x8_byte_arduino_hw_i2c(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr);

// U8g2's own I2C sender, with a check after every transfer
uint8_t oled_byte_cb(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr) {
  uint8_t r = u8x8_byte_arduino_hw_i2c(u8x8, msg, arg_int, arg_ptr);

  if (msg == U8X8_MSG_BYTE_END_TRANSFER && Wire.getWireTimeoutFlag()) {
    oled_recover();
    oled_glitched = true;
  }

  return r;
}

void oled_start() {
  oled.getU8x8()->byte_cb = oled_byte_cb;
  Wire.setWireTimeout(I2C_TIMEOUT_US, true); // first, so even the screen's start-up cannot hang
  oled.begin();                              // also starts Wire
  oled.setBusClock(400000);
}

void i2c_line(uint8_t pin, bool low) {       // I2C lines are only pulled low or let go, never driven high
  if (low) {
    digitalWrite(pin, LOW);
    pinMode(pin, OUTPUT);
  } else {
    pinMode(pin, INPUT_PULLUP);
  }

  delayMicroseconds(5);
}

void oled_recover() {
  oled_recoveries++;
  Wire.end();
  i2c_line(SDA, false);

  for (uint8_t i = 0; i < 9 && digitalRead(SDA) == LOW; i++) {
    i2c_line(SCL, true);
    i2c_line(SCL, false);
  }

  i2c_line(SCL, true);                       // STOP: SDA goes up while SCL is up
  i2c_line(SDA, true);
  i2c_line(SCL, false);
  i2c_line(SDA, false);
  Wire.begin();
  Wire.clearWireTimeoutFlag();
  Wire.beginTransmission(OLED_ADDR);
  Wire.write(0x00);                          // the bytes after this are commands

  for (uint8_t i = 0; i < sizeof OLED_SETTINGS; i++) {
    Wire.write(pgm_read_byte(&OLED_SETTINGS[i]));
  }

  Wire.endTransmission();
}

bool key_down[KEY_COUNT];
bool key_tap[KEY_COUNT];                     // pressed in this frame
uint8_t key_open[KEY_COUNT];
uint16_t key_held_ms[KEY_COUNT];

bool wait_release = false;                   // after starting a game or opening the menu: keys count again only after all are let go
bool game_over = false;
uint32_t over_at = 0;
uint16_t score = 0;
uint16_t best = 0;

void read_keys() {
  if (wait_release) {
    bool any = false;

    for (uint8_t k = 0; k < KEY_COUNT; k++) {
      any |= digitalRead(KEY_PINS[k]) == LOW;
      key_down[k] = false;
      key_tap[k] = false;
      key_open[k] = 0;
    }

    wait_release = any;
    return;
  }

  for (uint8_t k = 0; k < KEY_COUNT; k++) {
    bool closed = digitalRead(KEY_PINS[k]) == LOW;
    key_tap[k] = false;

    if (closed) {
      key_open[k] = 0;

      if (!key_down[k]) {
        key_down[k] = true;
        key_tap[k] = true;
        key_held_ms[k] = 0;
      } else {
        key_held_ms[k] += FRAME_MS;
      }
    } else if (key_down[k] && ++key_open[k] >= RELEASE_FRAMES) {
      key_down[k] = false;
    } else if (key_down[k]) {
      key_held_ms[k] += FRAME_MS;
    }
  }
}

bool any_tap() {
  return key_tap[KEY_L] || key_tap[KEY_M] || key_tap[KEY_R];
}

uint16_t hiscore(uint8_t game) {
  uint16_t v = EEPROM.read(HISCORE_ADDR + game * 2) | (EEPROM.read(HISCORE_ADDR + game * 2 + 1) << 8);
  return v == 0xFFFF ? 0 : v;
}

void save_hiscore(uint8_t game, uint16_t v) {
  EEPROM.update(HISCORE_ADDR + game * 2, v & 0xFF);
  EEPROM.update(HISCORE_ADDR + game * 2 + 1, v >> 8);
}

void text_center(const char *s, int16_t y) {
  oled.drawStr((128 - oled.getStrWidth(s)) / 2, y, s);
}

void text_right(const char *s, int16_t x, int16_t y) {
  oled.drawStr(x - oled.getStrWidth(s), y, s);
}

void draw_number(uint16_t v, int16_t x, int16_t y, bool right) {
  char s[8];
  utoa(v, s, 10);

  if (right) {
    text_right(s, x, y);
  } else {
    oled.drawStr(x, y, s);
  }
}

// U8g2 on the Nano takes 0..255 coordinates: a box partly off the screen (a pipe leaving on the left,
// a car coming in at the top) is cut to the visible part here, otherwise it would vanish or jump.
void box(int16_t x, int16_t y, int16_t w, int16_t h) {
  if (x < 0) {
    w += x;
    x = 0;
  }

  if (y < 0) {
    h += y;
    y = 0;
  }

  if (x + w > 128) {
    w = 128 - x;
  }

  if (y + h > 64) {
    h = 64 - y;
  }

  if (w > 0 && h > 0) {
    oled.drawBox(x, y, w, h);
  }
}

bool hit(int16_t ax, int16_t ay, int16_t aw, int16_t ah, int16_t bx, int16_t by, int16_t bw, int16_t bh) {
  return ax < bx + bw && bx < ax + aw && ay < by + bh && by < ay + ah;
}

void end_game() {
  game_over = true;
  over_at = millis();
}

void draw_game_over() {
  char s[20];
  oled.setDrawColor(0);
  oled.drawBox(14, 14, 100, 38);
  oled.setDrawColor(1);
  oled.drawFrame(14, 14, 100, 38);
  oled.setFont(u8g2_font_7x14B_tr);
  text_center(score > best ? "NEW RECORD!" : "GAME OVER", 28);
  oled.setFont(u8g2_font_5x7_tr);
  strcpy(s, "score ");
  utoa(score, s + 6, 10);
  text_center(s, 38);
  text_center("M: again  L: menu", 48);
}
