// -----------------------------------------------------------------------------------
// non-volatile storage for 24XX series EEPROMS (often 4KB AT24C32 on DS3231 RTC modules with I2C address 0x57)
// read & write caching version of driver uses 2.5K RAM at 2KB, 5K at 4KB, etc.

#pragma once

#ifndef NV_ENDURANCE
  #define NV_ENDURANCE LOW
#endif

#include <Wire.h>

// I2C EEPROM Address
#if !defined(I2C_EEPROM_ADDRESS)
  #define I2C_EEPROM_ADDRESS 0x57
#endif

// Time to wait after write page is requested, in milliseconds
#if !defined(EEPROM_WRITE_WAIT)
  #define EEPROM_WRITE_WAIT 10UL
#endif

#if !defined(E2END)
  #define E2END 4095
#endif
#define CACHE_SIZE ((E2END+1)/8)

#define MSB(i) (i >> 8)
#define LSB(i) (i & 0xFF)

class nvs {
  public:    
    bool init() {
      HAL_Wire.begin();
      HAL_Wire.setClock(HAL_WIRE_CLOCK);
      _eeprom_addr = I2C_EEPROM_ADDRESS;

      // mark entire read cache as dirty
      for (int i=0; i < CACHE_SIZE; i++) cacheReadState[i]=255;
      // mark entire write cache as clean
      for (int i=0; i < CACHE_SIZE; i++) cacheWriteState[i]=0;

      HAL_Wire.beginTransmission(I2C_EEPROM_ADDRESS);
      bool error = HAL_Wire.endTransmission();
      return !error;
    }

    // move data to/from the cache
    void poll() {
      static int i=E2END;
      uint8_t j;
      int dirtyW, dirtyR;

      // just exit if waiting for an EEPROM write to finish
      if (!ee_ready()) return;

      // check 20 byte chunks of cache for data that needs processing so < about 2s to check the entire cache
      for (int j=0; j < 20; j++) {
        i++; if (i > E2END) i=0;
        dirtyW=bitRead(cacheWriteState[i/8],i%8);
        dirtyR=bitRead(cacheReadState[i/8],i%8);
        if (dirtyW || dirtyR) break;
      }

      // write data as required
      if (dirtyW) {
        j=cache[i];        // get data from cache
        ee_write(i,j);     // store in EEPROM
        #ifdef NV_VALIDATE
          uint8_t k;
          ee_read(i,&k);
          if (j!=k) {
            //Serial.print("Write addr "); Serial.print(i); Serial.print(" value "); Serial.print(j); Serial.println(" FAILED");
            //Serial.println("Restarting Wire");
            HAL_Wire.end();
            HAL_Wire.begin();
            HAL_Wire.setClock(HAL_WIRE_CLOCK);
            ee_write(i,j);
            //ee_read(i,&k);
            //if (j!=k) Serial.println("Second attempt FAILED"); else Serial.println("Second attempt SUCCESS");
          }
        #endif
        bitWrite(cacheWriteState[i/8],i%8,0); // clean
      } else {
        // read data as required
        if (dirtyR) {
          ee_read(i,&j); // get data from EEPROM
          cache[i]=j;      // store in cache
          bitWrite(cacheReadState[i/8],i%8,0); // clean
        }
      }
    }

    bool committed() {
      int dirtyPool=0;

      // check 20 byte chunks of cache for data that needs processing so < about 2s to check the entire cache
      for (int j=0; j < E2END; j++) {
        dirtyPool=bitRead(cacheWriteState[j/8],j%8);
        if (dirtyPool) break;
      }

      return !dirtyPool;
    }

    uint8_t read(int i) {
      int dirty=bitRead(cacheReadState[i/8],i%8);
      if (dirty) {
        uint8_t j;
        ee_read(i,&j);
        
        // store and mark as clean
        cache[i]=j;
        bitWrite(cacheReadState[i/8],i%8,0);

        return j;
      } else return cache[i];
    }

    void update(int i, byte j) {
      uint8_t k;
      k=read(i);
      if (j != k) {
        // store
        cache[i]=j;

        // mark as write as dirty (needs to be written)
        bitWrite(cacheWriteState[i/8],i%8,1);

        // mark read as clean (so we don't overwrite the cache)
        bitWrite(cacheReadState[i/8],i%8,0);
      }
    }

    void write(int i, byte j) {
      update(i,j);
    }

    // write int numbers into EEPROM at position i (2 bytes)
    void writeInt(int i, int j) {
      uint8_t *k = (uint8_t*)&j;
      update(i + 0, *k); k++;
      update(i + 1, *k);
    }

    // read int numbers from EEPROM at position i (2 bytes)
    int readInt(int i) {
      uint16_t j;
      uint8_t *k = (uint8_t*)&j;
      *k = read(i + 0); k++;
      *k = read(i + 1);
      return j;
    }

    // write 4 byte variable into EEPROM at position i (4 bytes)
    void writeQuad(int i, byte *v) {
      update(i + 0, *v); v++;
      update(i + 1, *v); v++;
      update(i + 2, *v); v++;
      update(i + 3, *v);
    }

    // read 4 byte variable from EEPROM at position i (4 bytes)
    void readQuad(int i, byte *v) {
      *v = read(i + 0); v++;
      *v = read(i + 1); v++;
      *v = read(i + 2); v++;
      *v = read(i + 3);
    }

    // write String into EEPROM at position i (16 bytes)
    void writeString(int i, char l[]) {
      for (int l1 = 0; l1 < 16; l1++) {
        update(i + l1, *l); l++;
      }
    }

    // read String from EEPROM at position i (16 bytes)
    void readString(int i, char l[]) {
      for (int l1 = 0; l1 < 16; l1++) {
        *l = read(i + l1); l++;
      }
    }

    // write 4 byte float into EEPROM at position i (4 bytes)
    void writeFloat(int i, float f) {
      writeQuad(i, (byte*)&f);
    }

    // read 4 byte float from EEPROM at position i (4 bytes)
    float readFloat(int i) {
      float f;
      readQuad(i, (byte*)&f);
      return f;
    }

    // write 4 byte long into EEPROM at position i (4 bytes)
    void writeLong(int i, long l) {
      writeQuad(i, (byte*)&l);
    }

    // read 4 byte long from EEPROM at position i (4 bytes)
    long readLong(int i) {
      long l;
      readQuad(i, (byte*)&l);
      return l;
    }

    // read count bytes from EEPROM starting at position i
    void readBytes(uint16_t i, byte *v, uint8_t count) {
      for (int j=0; j < count; j++) { *v = read(i + j); v++; }
    }

    // write count bytes to EEPROM starting at position i
    void writeBytes(uint16_t i, byte *v, uint8_t count) {
      for (int j=0; j < count; j++) { write(i + j,*v); v++; }
    }

private:
  // Address of the I2C EEPROM
  uint8_t _eeprom_addr;
  uint32_t nextOpMs=0;
  uint8_t cache[E2END+1];
  uint8_t cacheReadState[CACHE_SIZE];
  uint8_t cacheWriteState[CACHE_SIZE];

  bool ee_ready() {
    return (int32_t)(millis()-nextOpMs) >= 0;
  }

  void ee_write(int offset, byte data) {
    while (!ee_ready()) {}
    
    HAL_Wire.beginTransmission(_eeprom_addr);
    HAL_Wire.write(MSB(offset));
    HAL_Wire.write(LSB(offset));
    HAL_Wire.write(data);
    HAL_Wire.endTransmission();
    nextOpMs=millis()+EEPROM_WRITE_WAIT;
  }

  void ee_read(int offset, byte *data) {
    while (!ee_ready()) {}
    
    HAL_Wire.beginTransmission(_eeprom_addr);
    HAL_Wire.write(MSB(offset));
    HAL_Wire.write(LSB(offset));
    HAL_Wire.endTransmission();

    HAL_Wire.requestFrom(_eeprom_addr, (uint8_t)1);
    if (HAL_Wire.available()) {
      *data = HAL_Wire.read();
    }
  }
};

nvs nv;
