#include <Arduino.h>
#include <Preferences.h>
#include <U8g2lib.h>
#include <inttypes.h>
#include <stdio.h>
#include <string.h>
#include <time.h>

#include "TBeamDisplay.h"

namespace {

#ifndef BOARD_ID
#define BOARD_ID "CY"
#endif

#ifndef NODE_LABEL
#define NODE_LABEL "Cy"
#endif

#ifndef BUILD_EPOCH
#define BUILD_EPOCH 0
#endif

using tbeam::DisplayConfig;
using tbeam::DisplayFont;
using tbeam::TBeamDisplay;

static constexpr const char* kExerciseTitle = "Exercise 24";
static constexpr const char* kExerciseSubtitle = "NVS Persistence Demo";
static constexpr const char* kExerciseVersion = "Version 1";
static constexpr const char* kNamespace = "mag";
static constexpr const char* kBlobKey = "magcal_blob";
static constexpr const char* kKeyX = "mag_x";
static constexpr const char* kKeyY = "mag_y";
static constexpr const char* kKeyZ = "mag_z";
static constexpr const char* kKeyEpoch = "mag_epoch";
static constexpr uint32_t kSplashMs = 15000;
static constexpr uint32_t kInitMessageMs = 2000;

struct __attribute__((packed)) MagCal {
  int16_t x;
  int16_t y;
  int16_t z;
  uint64_t epoch;
  uint16_t version;
};

static_assert(sizeof(MagCal) == 16, "MagCal layout changed");

MagCal magcalibration = {-654, 1129, 464, BUILD_EPOCH, 1};

enum class ValueState : uint8_t {
  Missing = 0,
  NullValue,
  Valid,
};

struct DisplayField {
  ValueState state = ValueState::Missing;
  int16_t value = 0;
};

struct EpochField {
  ValueState state = ValueState::Missing;
  uint64_t value = 0;
};

struct CalibrationView {
  DisplayField x{};
  DisplayField y{};
  DisplayField z{};
  EpochField epoch{};
};

Preferences g_preferences;
TBeamDisplay g_display;
char g_displayLines[4][64] = {};
uint32_t g_scrollStartMs = 0;

bool formatEpochUtc(uint64_t epoch, char* out, size_t outSize) {
  if (out == nullptr || outSize == 0) {
    return false;
  }

  if (epoch > static_cast<uint64_t>(INT32_MAX)) {
    return false;
  }

  time_t raw = static_cast<time_t>(epoch);
  struct tm tmUtc;
  if (gmtime_r(&raw, &tmUtc) == nullptr) {
    return false;
  }

  return strftime(out, outSize, "%Y%m%d_%H%M%S", &tmUtc) > 0;
}

bool isInvalidCalibration(const MagCal& cal) {
  if (cal.version == 0 || cal.epoch == 0) {
    return true;
  }
  return cal.x == 0 && cal.y == 0 && cal.z == 0 && cal.epoch == 0 && cal.version == 0;
}

void assignMissing(CalibrationView& view) {
  view = CalibrationView{};
}

void assignNull(CalibrationView& view) {
  view.x.state = ValueState::NullValue;
  view.y.state = ValueState::NullValue;
  view.z.state = ValueState::NullValue;
  view.epoch.state = ValueState::NullValue;
}

void assignValid(const MagCal& cal, CalibrationView& view) {
  view.x.state = ValueState::Valid;
  view.x.value = cal.x;
  view.y.state = ValueState::Valid;
  view.y.value = cal.y;
  view.z.state = ValueState::Valid;
  view.z.value = cal.z;
  view.epoch.state = ValueState::Valid;
  view.epoch.value = cal.epoch;
}

void formatField(const DisplayField& field, char* out, size_t outSize) {
  if (field.state == ValueState::Missing) {
    strlcpy(out, "not found", outSize);
    return;
  }
  if (field.state == ValueState::NullValue) {
    strlcpy(out, "NULL", outSize);
    return;
  }
  snprintf(out, outSize, "%d", field.value);
}

void formatEpochField(const EpochField& field, char* out, size_t outSize) {
  if (field.state == ValueState::Missing) {
    strlcpy(out, "not found", outSize);
    return;
  }
  if (field.state == ValueState::NullValue) {
    strlcpy(out, "NULL", outSize);
    return;
  }
  if (!formatEpochUtc(field.value, out, outSize)) {
    strlcpy(out, "NULL", outSize);
  }
}

bool writeCalibration(const MagCal& cal) {
  const size_t written = g_preferences.putBytes(kBlobKey, &cal, sizeof(cal));
  if (written != sizeof(cal)) {
    return false;
  }

  const size_t xWritten = g_preferences.putShort(kKeyX, cal.x);
  const size_t yWritten = g_preferences.putShort(kKeyY, cal.y);
  const size_t zWritten = g_preferences.putShort(kKeyZ, cal.z);
  const size_t epochWritten = g_preferences.putULong64(kKeyEpoch, cal.epoch);
  return xWritten > 0 && yWritten > 0 && zWritten > 0 && epochWritten > 0;
}

CalibrationView loadCalibration(bool& initializedDefaults) {
  CalibrationView view;
  assignMissing(view);
  initializedDefaults = false;

  const size_t len = g_preferences.getBytesLength(kBlobKey);
  if (len == sizeof(MagCal)) {
    MagCal cal{};
    const size_t read = g_preferences.getBytes(kBlobKey, &cal, sizeof(cal));
    if (read == sizeof(cal)) {
      if (isInvalidCalibration(cal)) {
        assignNull(view);
      } else {
        assignValid(cal, view);
      }
      return view;
    }
  }

  if (writeCalibration(magcalibration)) {
    initializedDefaults = true;
    assignValid(magcalibration, view);
  }
  return view;
}

void showSplash() {
  if (!g_display.ready()) {
    return;
  }

  g_display.setFont(DisplayFont::NORMAL);
  g_display.showLines(kExerciseTitle, kExerciseSubtitle, kExerciseVersion);
  delay(kSplashMs);
}

void showMessage(const char* line1, const char* line2 = nullptr) {
  if (!g_display.ready()) {
    return;
  }

  g_display.setFont(DisplayFont::NORMAL);
  g_display.showLines(line1, line2);
}

void showCalibration(const CalibrationView& view) {
  char xValue[24];
  char yValue[24];
  char zValue[24];
  char epochValue[24];

  formatField(view.x, xValue, sizeof(xValue));
  formatField(view.y, yValue, sizeof(yValue));
  formatField(view.z, zValue, sizeof(zValue));
  formatEpochField(view.epoch, epochValue, sizeof(epochValue));

  snprintf(g_displayLines[0], sizeof(g_displayLines[0]), "magnet calibration.x = %s", xValue);
  snprintf(g_displayLines[1], sizeof(g_displayLines[1]), "magnet calibration.y = %s", yValue);
  snprintf(g_displayLines[2], sizeof(g_displayLines[2]), "magnet calibration.z = %s", zValue);
  snprintf(g_displayLines[3], sizeof(g_displayLines[3]), "magnet calibration.date = %s", epochValue);
  g_scrollStartMs = millis();
}

void renderCalibrationScreen() {
  if (!g_display.ready()) {
    return;
  }

  U8G2& oled = g_display.raw();
  oled.clearBuffer();
  oled.setFont(u8g2_font_4x6_tf);

  const uint32_t elapsed = millis() - g_scrollStartMs;
  const int16_t scrollStep = static_cast<int16_t>(elapsed / 175U);
  const uint8_t yPositions[4] = {8, 22, 36, 50};

  for (uint8_t i = 0; i < 4; ++i) {
    const int width = oled.getUTF8Width(g_displayLines[i]);
    int16_t x = 0;
    if (width > 128) {
      const int travel = width - 128 + 8;
      x = -static_cast<int16_t>(scrollStep % travel);
    }
    oled.drawUTF8(x, yPositions[i], g_displayLines[i]);
  }

  oled.sendBuffer();
}

void logCalibration(const CalibrationView& view) {
  char xValue[24];
  char yValue[24];
  char zValue[24];
  char epochValue[24];

  formatField(view.x, xValue, sizeof(xValue));
  formatField(view.y, yValue, sizeof(yValue));
  formatField(view.z, zValue, sizeof(zValue));
  formatEpochField(view.epoch, epochValue, sizeof(epochValue));

  Serial.printf("magnet calibration.x = %s\n", xValue);
  Serial.printf("magnet calibration.y = %s\n", yValue);
  Serial.printf("magnet calibration.z = %s\n", zValue);
  Serial.printf("magnet calibration.date = %s\n", epochValue);
}

}  // namespace

void setup() {
  Serial.begin(115200);
  delay(200);

  DisplayConfig displayConfig;
  displayConfig.powerSave = false;
  g_display.begin(displayConfig);

  showSplash();

  if (!g_preferences.begin(kNamespace, false)) {
    showMessage("NVS open failed", kNamespace);
    Serial.println("Failed to open Preferences namespace");
    return;
  }

  bool initializedDefaults = false;
  const CalibrationView view = loadCalibration(initializedDefaults);

  if (initializedDefaults) {
    showMessage("Calibration initialized");
    delay(kInitMessageMs);
  }

  showCalibration(view);
  renderCalibrationScreen();
  logCalibration(view);
}

void loop() {
  renderCalibrationScreen();
  delay(250);
}
