#include "TBeamClock.h"

#ifndef OLED_SDA
#define OLED_SDA 17
#endif

#ifndef OLED_SCL
#define OLED_SCL 18
#endif

namespace tbeam
{

  TBeamClock::TBeamClock(TwoWire &wire) : wire_(wire) {}

  bool TBeamClock::begin(const ClockConfig &config)
  {
    config_ = config;
    clearError();

    if (config_.sda < 0)
    {
      config_.sda = OLED_SDA;
    }
    if (config_.scl < 0)
    {
      config_.scl = OLED_SCL;
    }

    if (config_.beginWire)
    {
      wire_.begin(config_.sda, config_.scl);
    }

    DateTime dt{};
    bool lowVoltage = false;
    ready_ = readRtc(dt, lowVoltage);
    if (!ready_)
    {
      setError("RTC read failed");
      valid_ = false;
      return false;
    }

    lowVoltage_ = lowVoltage;
    lastRtc_ = dt;
    valid_ = !lowVoltage && isValidDateTime(dt);
    lastEpoch_ = valid_ ? toEpochSeconds(dt) : 0;
    if (lowVoltage)
    {
      setError("RTC low-voltage flag set");
    }
    else if (!valid_)
    {
      setError("RTC date/time invalid");
    }
    return true;
  }

  void TBeamClock::update()
  {
    DateTime dt{};
    bool lowVoltage = false;
    if (!readRtc(dt, lowVoltage))
    {
      ready_ = false;
      valid_ = false;
      setError("RTC read failed");
      return;
    }

    ready_ = true;
    lowVoltage_ = lowVoltage;
    lastRtc_ = dt;
    valid_ = !lowVoltage && isValidDateTime(dt);
    lastEpoch_ = valid_ ? toEpochSeconds(dt) : 0;
    if (valid_)
    {
      clearError();
    }
    else if (lowVoltage)
    {
      setError("RTC low-voltage flag set");
    }
    else
    {
      setError("RTC date/time invalid");
    }
  }

  bool TBeamClock::readRtc(DateTime &out, bool &lowVoltageFlag) const
  {
    wire_.beginTransmission(config_.rtcAddress);
    wire_.write(0x02);
    if (wire_.endTransmission(false) != 0)
    {
      return false;
    }

    const uint8_t need = 7;
    const uint8_t got = wire_.requestFrom((int)config_.rtcAddress, (int)need);
    if (got != need)
    {
      return false;
    }

    const uint8_t sec = wire_.read();
    const uint8_t min = wire_.read();
    const uint8_t hour = wire_.read();
    const uint8_t day = wire_.read();
    const uint8_t weekday = wire_.read();
    const uint8_t month = wire_.read();
    const uint8_t year = wire_.read();

    lowVoltageFlag = (sec & 0x80U) != 0;
    out.second = fromBcd(sec & 0x7FU);
    out.minute = fromBcd(min & 0x7FU);
    out.hour = fromBcd(hour & 0x3FU);
    out.day = fromBcd(day & 0x3FU);
    out.weekday = fromBcd(weekday & 0x07U);
    out.month = fromBcd(month & 0x1FU);
    const uint8_t yy = fromBcd(year);
    out.year = (month & 0x80U) ? (1900U + yy) : (2000U + yy);
    return true;
  }

  bool TBeamClock::readValidRtc(DateTime &out, int64_t *epochOut) const
  {
    bool lowVoltage = false;
    if (!readRtc(out, lowVoltage) || lowVoltage || !isValidDateTime(out))
    {
      return false;
    }
    if (epochOut)
    {
      *epochOut = toEpochSeconds(out);
    }
    return true;
  }

  bool TBeamClock::writeRtc(const DateTime &dt) const
  {
    if (!isValidDateTime(dt))
    {
      return false;
    }

    wire_.beginTransmission(config_.rtcAddress);
    wire_.write(0x02);
    wire_.write(toBcd(dt.second) & 0x7FU);
    wire_.write(toBcd(dt.minute) & 0x7FU);
    wire_.write(toBcd(dt.hour) & 0x3FU);
    wire_.write(toBcd(dt.day) & 0x3FU);
    wire_.write(toBcd(dt.weekday) & 0x07U);

    uint8_t monthReg = toBcd(dt.month) & 0x1FU;
    if (dt.year < 2000U)
    {
      monthReg |= 0x80U;
    }
    wire_.write(monthReg);
    wire_.write(toBcd((uint8_t)(dt.year % 100U)));
    return wire_.endTransmission() == 0;
  }

  bool TBeamClock::isValidDateTime(const DateTime &dt)
  {
    if (dt.year < 2000U || dt.year > 2099U)
      return false;
    if (dt.month < 1U || dt.month > 12U)
      return false;
    if (dt.day < 1U || dt.day > daysInMonth(dt.year, dt.month))
      return false;
    if (dt.hour > 23U || dt.minute > 59U || dt.second > 59U)
      return false;
    return true;
  }

  int64_t TBeamClock::toEpochSeconds(const DateTime &dt)
  {
    const int64_t days = daysFromCivil((int)dt.year, dt.month, dt.day);
    return days * 86400LL + (int64_t)dt.hour * 3600LL + (int64_t)dt.minute * 60LL + (int64_t)dt.second;
  }

  bool TBeamClock::fromEpochSeconds(int64_t seconds, DateTime &out)
  {
    if (seconds < 0)
    {
      return false;
    }

    int64_t days = seconds / 86400LL;
    int64_t remainder = seconds % 86400LL;
    if (remainder < 0)
    {
      remainder += 86400LL;
      days -= 1;
    }

    out.hour = (uint8_t)(remainder / 3600LL);
    remainder %= 3600LL;
    out.minute = (uint8_t)(remainder / 60LL);
    out.second = (uint8_t)(remainder % 60LL);

    days += 719468;
    const int era = (days >= 0 ? days : days - 146096) / 146097;
    const unsigned doe = (unsigned)(days - era * 146097);
    const unsigned yoe = (doe - doe / 1460U + doe / 36524U - doe / 146096U) / 365U;
    int year = (int)yoe + era * 400;
    const unsigned doy = doe - (365U * yoe + yoe / 4U - yoe / 100U);
    const unsigned mp = (5U * doy + 2U) / 153U;
    const unsigned day = doy - (153U * mp + 2U) / 5U + 1U;
    const unsigned month = mp + (mp < 10U ? 3U : (unsigned)-9);
    year += (month <= 2U);

    out.year = (uint16_t)year;
    out.month = (uint8_t)month;
    out.day = (uint8_t)day;
    out.weekday = 0;
    return isValidDateTime(out);
  }

  void TBeamClock::formatIsoUtc(const DateTime &dt, char *out, size_t outSize)
  {
    snprintf(out,
             outSize,
             "%04u-%02u-%02uT%02u:%02u:%02uZ",
             (unsigned)dt.year,
             (unsigned)dt.month,
             (unsigned)dt.day,
             (unsigned)dt.hour,
             (unsigned)dt.minute,
             (unsigned)dt.second);
  }

  void TBeamClock::formatCompactUtc(const DateTime &dt, char *out, size_t outSize)
  {
    snprintf(out,
             outSize,
             "%04u%02u%02u_%02u%02u%02u",
             (unsigned)dt.year,
             (unsigned)dt.month,
             (unsigned)dt.day,
             (unsigned)dt.hour,
             (unsigned)dt.minute,
             (unsigned)dt.second);
  }

  void TBeamClock::makeRunId(const DateTime &dt, const char *boardId, char *out, size_t outSize)
  {
    snprintf(out,
             outSize,
             "%04u%02u%02u_%02u%02u%02u_%s",
             (unsigned)dt.year,
             (unsigned)dt.month,
             (unsigned)dt.day,
             (unsigned)dt.hour,
             (unsigned)dt.minute,
             (unsigned)dt.second,
             boardId ? boardId : "NODE");
  }

  bool TBeamClock::parseDateTime(const char *text, DateTime &out)
  {
    if (!text)
    {
      return false;
    }
    int y = 0;
    int mo = 0;
    int d = 0;
    int h = 0;
    int mi = 0;
    int s = 0;
    if (sscanf(text, "%d-%d-%d %d:%d:%d", &y, &mo, &d, &h, &mi, &s) != 6 &&
        sscanf(text, "%d-%d-%dT%d:%d:%d", &y, &mo, &d, &h, &mi, &s) != 6)
    {
      return false;
    }

    out.year = (uint16_t)y;
    out.month = (uint8_t)mo;
    out.day = (uint8_t)d;
    out.hour = (uint8_t)h;
    out.minute = (uint8_t)mi;
    out.second = (uint8_t)s;
    out.weekday = 0;
    return isValidDateTime(out);
  }

  uint8_t TBeamClock::toBcd(uint8_t value)
  {
    return (uint8_t)(((value / 10U) << 4U) | (value % 10U));
  }

  uint8_t TBeamClock::fromBcd(uint8_t value)
  {
    return (uint8_t)(((value >> 4U) * 10U) + (value & 0x0FU));
  }

  bool TBeamClock::isLeapYear(uint16_t year)
  {
    return ((year % 4U) == 0U && (year % 100U) != 0U) || ((year % 400U) == 0U);
  }

  uint8_t TBeamClock::daysInMonth(uint16_t year, uint8_t month)
  {
    static const uint8_t kDays[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
    if (month == 2U)
    {
      return (uint8_t)(isLeapYear(year) ? 29U : 28U);
    }
    if (month >= 1U && month <= 12U)
    {
      return kDays[month - 1U];
    }
    return 0;
  }

  int64_t TBeamClock::daysFromCivil(int year, unsigned month, unsigned day)
  {
    year -= (month <= 2U);
    const int era = (year >= 0 ? year : year - 399) / 400;
    const unsigned yoe = (unsigned)(year - era * 400);
    const unsigned doy = (153U * (month + (month > 2U ? (unsigned)-3 : 9U)) + 2U) / 5U + day - 1U;
    const unsigned doe = yoe * 365U + yoe / 4U - yoe / 100U + doy;
    return era * 146097 + (int)doe - 719468;
  }

  void TBeamClock::setError(const char *message) const
  {
    strlcpy(lastError_, message ? message : "", sizeof(lastError_));
  }

  void TBeamClock::clearError() const
  {
    lastError_[0] = '\0';
  }

} // namespace tbeam
