/*
  xdrv_23_zigbee_4a_eeprom.ino - zigbee support for Tasmota - nano filesystem for EEPROM, with anti-weavering

  Copyright (C) 2021  Theo Arends and Stephan Hadinger

  This program is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#ifdef USE_ZIGBEE

// #define Z_EEPROM_DEBUG


// const static uint32_t ZIGB_NAME1 = 0x3167697A; // 'zig1' little endian
const static uint32_t ZIGB_NAME2 = 0x3267697A; // 'zig2' little endian, v2
const static uint32_t ZIGB_NAME4 = 0x3467697A; // 'zig4' little endian, v2
const static uint32_t ZIGB_DATA2 = 0x32746164; // 'dat2' little endian, v2
extern FS *dfsp;
extern "C" uint32_t _FS_end;
// Is it ok to write to bank 0x402FF000
bool flash_valid(void) {
  return (((uint32_t)&_FS_end) > 0x40280000) && (((uint32_t)&_FS_end) < 0x402FF000);
}

void hydrateSingleDevice(const SBuffer & buf_d, uint32_t version);

#ifdef USE_ZIGBEE_EEPROM
// The EEPROM is 64KB in size with individually writable bytes.
// They are conveniently organized in pages of 128 bytes to accelerate
// data transfer, but unlike flash memory, you don't need to erase an entire page.
// The chip spec says it supports over 2 million writes per byte.

// EEPROM LAYOUT:
// ==============
// 64KB EEPROM is divided in 256 block of 256 bytes.
// The internal page size is 128 bytes, so we're grouping 2 pages in one block
// The advantage is that any pointer to a block is a single byte
//
// Block 0, 1 and 255 are reserved.
//
// BLock 0 contains the directory of files
// Block 1 contains the linked list of blocks for each file
// Block 255 contains the bitmap of block and ageing information

// File structure
// Each file has :
//  - a name of 4 chars (no extension) that conveniently fit in uint32_t.
//  - a length in bytes, encoded with 16 bits (uint16_t)
//  - 1 byte indicating the first block of the file
//  - 1 byte reserved
//
// Then blocks are a linked-list of content. The next block is indicated in Block 1
//
// Note: the linked list could cause a circular reference loop and potentially an infinite loop.
// This is why the content lenght is used to check that the block count does not exceed
// the content length hence cannot cause an infinite loop.
// Any pointer to blocks 1 or 255 is considered invalid and means a corruption of the file system.

// Signature entry:
// - 4 bytes of signature, currently 'Tasm'. Any other entry indicates that the EEPROM was not formatted
// - 1 byte version number, currently 0x00
// - all other bytes (5..7) are reserved and filled with 0s

// DIRECTORY
// =========
// Block 0 is the directory. There is no support for folders.
// Each file entry is 8 bytes.
// First entry is a signature marker and version
// Entries 1..30 are for files
// Entry 31 (last entry) is reserved and filled with 0s

// BITMAP
// ==========
// Block 255:
// Each byte represents a block, remember there are 256 blocks in total
// Each byte is set as follows:
// bit 7 - block is used (1) or free (0) - note that blocks 0, 1 and 255 are always used
// bit 6 - block is damaged - not implemented yet but may be useful
// bit 0..5 - generation number for anti-weavering
//
// Caveat: this bitmap system may lead to wasted blocked marked as used but actually unused
// Periodical garbage collection and sanity checks can occur, for ex at boot.
//
// If the generation number overflows, all blocks start at generation `0`
// meaning that the entire bitmap block is overwritten.

// Version 0:
// Many features are not yet implemented.
// We start with hardcoded values:
//  - the two entries for files 'zig2' and 'dat2' are predefined
//  - the starting block for each file is fixed.
//    'Zig2' uses 32 blocks (8kb max) - starting at block 32
//    'Dat2' uses 32 blocks (8kb max) - starting at block 64
//  - the bitmap marks those blocks as used
//  - version number only uses first entry that doesn't get re-written
//  - only file size actually changes

/*********************************************************************************************\
 *
 * Constants
 *
\*********************************************************************************************/
const size_t ZFS_BLOCK_SIZE      = 256;
const size_t ZFS_ENTRY_SIZE      = 8;    // each entry is 32 bytes
const size_t ZFS_ENTRIES         = 30;
const uint32_t ZFS_SIGNATURE     = 0x6D736154;    // 'Tasm'


/*********************************************************************************************\
 * Specific to v2 (limited support)
\*********************************************************************************************/
const size_t ZFS_FILE_BLOCKS     = 31;    // 31 blocks

/*********************************************************************************************\
 * ZFS_File_Entry
\*********************************************************************************************/
class ZFS_File_Entry {
public:
  uint32_t        name;       // file name representing 4 chars, 0x00000000 means empty entry
  uint16_t        length;     // length of file in bytes
  uint8_t         blk_start;
  uint8_t         reserved;  // reserved for future use

  ZFS_File_Entry() :
    name(0),
    length(0),
    blk_start(0),
    reserved(0)
    {}

  inline static bool validIdx(uint8_t blk_start) { return ((blk_start != 0x00) && (blk_start != 0x01) && (blk_start != 0xFF)); };
  static uint16_t getAddress(uint8_t entry_idx);
  void read(uint8_t entry_idx);
  void write(uint8_t entry_idx) const ;
};

/*********************************************************************************************\
 * ZFS_File_Entry
\*********************************************************************************************/
class ZFS_Root_Entry {
public:
  uint32_t      signature;    // Signature that the block is correctly formatted
  uint8_t       version;      // version of file system structure
  uint8_t       reserved[3];

  ZFS_Root_Entry() :
    signature(ZFS_SIGNATURE),    // 'Tasm'
    version(0),
    reserved{}
    {};
};

/*********************************************************************************************\
 * ZFS_File_Entry
\*********************************************************************************************/
class ZFS_Dir_Block {
public:
  ZFS_Root_Entry    b0;               // signature entry
  ZFS_File_Entry    e[ZFS_ENTRIES];   // 7 entries for files
  ZFS_File_Entry    reserved;         // reserved for future use

  void format(void);                  // prepare default values for formatting
};

/*********************************************************************************************\
 * ZFS_Bitmap at block 0xFF
\*********************************************************************************************/
// Individual block
union ZFS_Bitmap_Entry {
  uint8_t   raw;
  struct {
    uint8_t   gen : 6;
    bool      damaged : 1;
    bool      used : 1;
  };
};

class ZFS_Bitmap {
public:
  ZFS_Bitmap_Entry         block[ZFS_BLOCK_SIZE];

  void format(void);
};

/*********************************************************************************************\
 * ZFS_Map, linked list of blocks, at block 1
\*********************************************************************************************/
class ZFS_Map {
public:
  uint8_t                  next_blk[ZFS_BLOCK_SIZE];

  void format(void);
};

/*********************************************************************************************\
 *
 * Formatting implementations
 *
\*********************************************************************************************/

void ZFS_Dir_Block::format(void) {
  // entry 0 - 'zig2'
  e[0].name = ZIGB_NAME2;
  e[0].length = 0;
  e[0].blk_start = 2;             // start at block 2 to 32
  // entry 1 - 'dat2'
  e[1].name = ZIGB_DATA2;
  e[1].length = 0;
  e[1].blk_start = 2 + 31;        // start at block 33 to 63
}

void ZFS_Bitmap::format(void) {
  ZFS_Bitmap_Entry val_used;
  val_used.gen = 0;
  val_used.damaged = false;
  val_used.used = true;
  // block 0, 1, 255
  // block[0x00] = val_used;    // already in loop
  // block[0x01] = val_used;
  block[0xFF] = val_used;
  // reserve block 32->63 for file 0 and 64->95 for file 1
  for (uint32_t i = 0; i < 64; i++) {
    block[i] = val_used;
  }
}

void ZFS_Map::format(void) {
  // map a linear linked list for v1
  for (uint32_t i = 2; i < ZFS_BLOCK_SIZE - 2; i++) {
    next_blk[i] = i+1;
  }
}

/*********************************************************************************************\
 *
 * Writing a file
 *
\*********************************************************************************************/

class ZFS_Write_File {
public:
  // file info
  uint32_t  name;
  uint16_t  cursor;
  uint16_t  length;
  uint8_t   blk_start;      // if 0x00 then file does not exist
  uint8_t   entry_idx;      // entry number in the directory

  ZFS_Write_File(void) : name(0), cursor(0), length(0), blk_start(0) {}
  void init(uint32_t _name) {
    name = _name;
    findOrCreate();
  }

  inline bool valid(void) const { return blk_start != 0; }       // does the file exist?

  int32_t addBytes(void* buffer, size_t buffer_len);
  int32_t close(void);

protected:
  void findOrCreate(void);
};


/*********************************************************************************************\
 *
 * Check that the EEPROM is formatted
 *
\*********************************************************************************************/

// Main class for the Zigbee filesystem
class ZFS {
public:

  static void initOrFormat(void);     // <0 means error
  static void format(void);           // format EEPROM

  static int32_t getLength(uint32_t name);
  static bool findFileEntry(uint32_t name, ZFS_File_Entry & entry, uint8_t * entry_idx);
  static void erase(void);              // erase EEPROM

  // read file
  static int32_t readBytes(uint32_t name, void* buffer, size_t buffer_len, uint16_t start, uint16_t len);
};

/*********************************************************************************************\
 *
 * Check that the EEPROM is formatted
 *
\*********************************************************************************************/

bool ZFS::findFileEntry(uint32_t name, ZFS_File_Entry & entry, uint8_t * _entry_idx) {
  if (!zigbee.eeprom_ready) { return false; }
  for (uint32_t entry_idx = 0; entry_idx < ZFS_ENTRIES; entry_idx++) {
    // read entry from EEPROM
    uint16_t  entry_addr = 0x0000 + sizeof(ZFS_Root_Entry) + sizeof(ZFS_File_Entry) * entry_idx;
    zigbee.eeprom.readBytes(entry_addr, sizeof(ZFS_File_Entry), (byte*)&entry);
#ifdef Z_EEPROM_DEBUG
    // {
    //   char hex_char[(sizeof(ZFS_File_Entry) * 2) + 2];
    //   AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "Read entry %d at address 0x%04X contains %*_H"), entry_idx, entry_addr, sizeof(entry), &entry);
    // }
#endif
    if (entry.name == name) {
      if (_entry_idx) { *_entry_idx = entry_idx; }
      return true;
    }
  }
  return false;
}

int32_t ZFS::getLength(uint32_t name) {
  ZFS_File_Entry entry;
  if (ZFS::findFileEntry(name, entry, nullptr)) {
    return entry.length;
  }
  return -1;
}

void ZFS::erase(void) {
  if (!zigbee.eeprom_present) { return; }
  uint32_t zero = 0;
  zigbee.eeprom.writeBytes(0x0000, sizeof(zero), (byte*)&zero);
}

/*********************************************************************************************\
 *
 * Reading a file
 *
\*********************************************************************************************/
int32_t ZFS::readBytes(uint32_t name, void* buffer, size_t buffer_len, uint16_t read_start, uint16_t read_len) {
  if (!zigbee.eeprom_ready) { return -1; }
#ifdef Z_EEPROM_DEBUG
    // AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "readBytes name=%08X, buffer_len=%d, read_start=0x%04X, read_len=%d"), name, buffer_len, read_start, read_len);
#endif
  if (name == 0x00000000) { return -1; }
  if (buffer_len == 0) { return 0; }

  // look for file
  ZFS_File_Entry entry;
  uint8_t entry_idx;
  if (!findFileEntry(name, entry, &entry_idx)) { return -1; }   // file not found

  if (read_start >= entry.length) { return 0; }    // start of read is beyond end of file, return nothing
  uint16_t max_read_len = entry.length - read_start;   // we know it's > 0
  if (read_len > max_read_len) { read_len = max_read_len; }
  if (read_len > buffer_len) { read_len = buffer_len; }
  // we know read_len is the correct max value now

  // compute the start block for the file
  // V1 it's the first one
  uint8_t  blk = entry.blk_start;

  zigbee.eeprom.readBytes((blk << 8) + read_start, read_len, (byte*) buffer);
  return read_len;
}

/*********************************************************************************************\
 *
 * Check that the EEPROM is formatted
 *
\*********************************************************************************************/

void ZFS::initOrFormat(void) {
  if (!zigbee.eeprom_present) { return; }

#ifdef Z_EEPROM_DEBUG
  // AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "sizeof(ZFS_Bitmap)=%d sizeof(ZFS_File_Entry)=%d sizeof(ZFS_Root_Entry)=%d sizeof(ZFS_Dir_Block)=%d"), sizeof(ZFS_Bitmap), sizeof(ZFS_File_Entry), sizeof(ZFS_Root_Entry), sizeof(ZFS_Dir_Block));
  {
    byte map[256];
    char hex_char[(256 * 2) + 2];
    zigbee.eeprom.readBytes(0x0000, 256, map);
    AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "BLK 00 %s"), ToHex_P(map, sizeof(map), hex_char, sizeof(hex_char)));
    // zigbee.eeprom.readBytes(0x0100, 256, map);
    // AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "BLK 01 %s"), ToHex_P(map, sizeof(map), hex_char, sizeof(hex_char)));
    zigbee.eeprom.readBytes(0x0200, 256, map);
    AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "BLK 02 %s"), ToHex_P(map, sizeof(map), hex_char, sizeof(hex_char)));
    zigbee.eeprom.readBytes(0x2100, 256, map);
    AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "BLK 21 %s"), ToHex_P(map, sizeof(map), hex_char, sizeof(hex_char)));
    // zigbee.eeprom.readBytes(0xFF00, 256, map);
    // AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "BLK FF %s"), ToHex_P(map, sizeof(map), hex_char, sizeof(hex_char)));
  }
#endif

  ZFS_Dir_Block * dir = new ZFS_Dir_Block();
  zigbee.eeprom.readBytes(0, sizeof(ZFS_Dir_Block), (byte*) dir);

  if (dir->b0.signature == ZFS_SIGNATURE) {
    // Good
    AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_ZIGBEE "EEPROM signature 0x%08X is correct"), dir->b0.signature);
  } else {
    AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_ZIGBEE "EEPROM signature 0x%08X is incorrect, formatting"), dir->b0.signature);
    format();
  }
  delete dir;

  zigbee.eeprom_ready = true;
}

//
// Format EEPROM
//
void ZFS::format(void) {
  AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "Formatting EEPROM"));

  // First write the bitmap
  ZFS_Bitmap * bitmap = new ZFS_Bitmap();
  bitmap->format();
  zigbee.eeprom.writeBytes(0xFF00, 256, (byte*) bitmap);
  delete bitmap;

  // Map
  ZFS_Map * map = new ZFS_Map();
  map->format();
  zigbee.eeprom.writeBytes(0x0100, 256, (byte*) map);
  delete map;

  // Dir
  ZFS_Dir_Block * dir = new ZFS_Dir_Block();
  dir->format();
  zigbee.eeprom.writeBytes(0x0000, 256, (byte*) dir);
  delete dir;
}

uint16_t ZFS_File_Entry::getAddress(uint8_t entry_idx) {
  return sizeof(ZFS_Root_Entry) + sizeof(ZFS_File_Entry) * entry_idx;
}

void ZFS_File_Entry::read(uint8_t entry_idx) {
  if (!zigbee.eeprom_ready) { return; }
  zigbee.eeprom.readBytes(getAddress(entry_idx), sizeof(ZFS_File_Entry), (byte*)this);
}

void ZFS_Write_File::findOrCreate(void) {
  ZFS_File_Entry entry;

  if (ZFS::findFileEntry(name, entry, &entry_idx)) {
    blk_start = entry.blk_start;
  }
};

int32_t ZFS_Write_File::addBytes(void* buffer, size_t buffer_len) {
  if (!zigbee.eeprom_ready) { return -1; }
  if ((buffer == nullptr) || (buffer_len == 0)) { return 0; }
  if (length + buffer_len > ZFS_FILE_BLOCKS * 256) { return -1; }   // exceeded max size

// #ifdef Z_EEPROM_DEBUG
//   AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "eeprom.writeBytes address=0x%04X, len=%d"), (blk_start << 8) + length, buffer_len);
// #endif
  zigbee.eeprom.writeBytes((blk_start << 8) + length, buffer_len, (byte*)buffer);
  length += buffer_len;
  return length;
}

int32_t ZFS_Write_File::close(void) {
  if (!zigbee.eeprom_ready) { return -1; }
  // write the final length
  uint16_t address = ZFS_File_Entry::getAddress(entry_idx);
  zigbee.eeprom.writeBytes(address + sizeof(name), 2, (byte*)&length);
  return length;
}


#endif // USE_ZIGBEE_EEPROM


/*********************************************************************************************\
 *
 * Generic for Reading a file
 *
 * Can work in 3 modes:
 * - if passed a filename, use the ZFS for EEPROM nano-fs
 * - if passed a File* object, use this object
 * - if passed a buffer, read from a binary buffer in RAM
\*********************************************************************************************/

class Univ_Read_File {
public:
  // file info
  uint16_t  len = 0;
  uint16_t  cursor = 0;
  bool      is_valid = false;

  Univ_Read_File(void) {}

  // == EEPROM ================================================
#ifdef USE_ZIGBEE_EEPROM
  uint32_t  eeprom_name = 0;
  ZFS_File_Entry entry;
  // uint16_t  length;
  // uint8_t   blk_start;      // if 0x00 then file does not exist
  uint8_t   entry_idx;      // entry number in the directory

  void init(uint32_t _name) {
    eeprom_name = _name;
    if (ZFS::findFileEntry(eeprom_name, entry, &entry_idx)) {
      len = ZFS::getLength(eeprom_name);
      is_valid = (len > 0);
    }
  }
#endif // USE_ZIGBEE_EEPROM

  // == File ================================================
#ifdef USE_UFILESYS
  File * file = nullptr;

  void init(File * _file) {
    file = _file;
    is_valid = (bool) *file;
    len = file->size();
  }
#endif

#ifdef ESP8266
  // == Buffer ================================================
  // binary buffer
  const uint8_t * buffer = nullptr;
  void init(const uint8_t * buf, size_t buflen) {
    buffer = buf;
    len = buflen;
    is_valid = (buffer != nullptr) && (len > 0);
  }
#endif // ESP8266
  

  // ==================================================
  inline bool valid(void) const { return is_valid; }       // does the file exist?

  int32_t readBytes(uint8_t* buf, size_t buflen);
  void close(void);
};

void Univ_Read_File::close(void) {
#ifdef USE_UFILESYS
  if (file != nullptr) {
    file->close();
  }
#endif // USE_UFILESYS
  // don't do anything for ZFS read of buffer
}

int32_t Univ_Read_File::readBytes(uint8_t* buf, size_t btr) {
  if (!is_valid) { return -1; }
#ifdef USE_UFILESYS
  if (file != nullptr) {
    return file->read(buf, btr);
  }
#endif // USE_UFILESYS
#ifdef USE_ZIGBEE_EEPROM
  if (eeprom_name != 0) {
    int32_t bytes_read = ZFS::readBytes(eeprom_name, buf, btr, cursor, btr);
    if (bytes_read < 0) { return -1; }
    cursor += bytes_read;
    return bytes_read;
  }
#endif // USE_ZIGBEE_EEPROM

#ifdef ESP8266
  // binary buffer
  if (buffer != nullptr)  {
    if (btr > len - cursor) { btr = len - cursor; }
    memcpy_P(buf, buffer + cursor, btr);
    cursor += btr;
    return btr;
  }
#endif // ESP8266

  return -1;
}


/*********************************************************************************************\
 *
 * Generic for Writing a file
 *
 * Can work in 3 modes:
 * - if passed a filename, use the ZFS for EEPROM nano-fs
 * - if passed a File* object, use this object
 * - if passed a buffer, write to a binary buffer in RAM
\*********************************************************************************************/

class Univ_Write_File {
public:
  // file info
  bool      is_valid = false;

  Univ_Write_File(void) {}

  // == EEPROM ================================================
#ifdef USE_ZIGBEE_EEPROM
  ZFS_Write_File eeprom_file;

  void init(uint32_t _name) {
    eeprom_file.init(_name);
    is_valid = eeprom_file.valid();
  }
#endif // USE_ZIGBEE_EEPROM

  // == File ================================================
#ifdef USE_UFILESYS
  File * file = nullptr;

  void init(File * _file) {
    file = _file;
    is_valid = (bool) *file;
  }
#endif

#ifdef ESP8266
  // == Buffer ================================================
  // binary buffer
  size_t buflen = 0;
  uint8_t * buffer = nullptr;
  uint16_t  cursor = 0;
  void init(uint8_t * buf, size_t _buflen) {
    buffer = buf;
    buflen = _buflen;
    is_valid = (buffer != nullptr) && (buflen > 0);
  }
#endif // ESP8266

  // ==================================================
  inline bool valid(void) const { return is_valid; }       // does the file exist?

  int32_t writeBytes(uint8_t* buf, size_t buflen);
  int32_t getCursor(void);
  void close(void);
};

void Univ_Write_File::close(void) {
#ifdef USE_UFILESYS
  if (file != nullptr) {
    file->close();
  }
#endif // USE_UFILESYS
#ifdef USE_ZIGBEE_EEPROM
  if (eeprom_file.valid()) {
    eeprom_file.close();
  }
#endif // USE_ZIGBEE_EEPROM
  // binary buffer doesn't need a close
}

int32_t Univ_Write_File::getCursor(void) {
  if (!is_valid) { return -1; }

#ifdef USE_UFILESYS
  if (file != nullptr) {
    return file->position();
  }
#endif // USE_UFILESYS
#ifdef USE_ZIGBEE_EEPROM
  if (eeprom_file.valid()) {
    return eeprom_file.length;
  }
#endif // USE_ZIGBEE_EEPROM

#ifdef ESP8266
  if (buffer != nullptr) {
    return cursor;
  }
#endif // ESP8266

  return -1;
}

int32_t Univ_Write_File::writeBytes(uint8_t* buf, size_t btw) {
  if (!is_valid) { return -1; }

#ifdef USE_UFILESYS
  if (file != nullptr) {
    return file->write(buf, btw);
  }
#endif // USE_UFILESYS
#ifdef USE_ZIGBEE_EEPROM
  if (eeprom_file.valid()) {
    uint16_t length_before = eeprom_file.length;
    eeprom_file.addBytes(buf, btw);
    return eeprom_file.length - length_before;    // compute the increase in size
  }
#endif // USE_ZIGBEE_EEPROM

#ifdef ESP8266
  if (buffer != nullptr) {
    // binary buffer
    if (btw > buflen - cursor) { btw = buflen - cursor; }
    memcpy_P(buffer + cursor, buf, btw);
    cursor += btw;
    return btw;
  }
#endif // ESP8266

  return -1;
}


#endif // USE_ZIGBEE
