/*
  xdrv_23_zigbee.ino - zigbee support for Tasmota

  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

// Ensure persistence of devices into Flash
//
// Structure:
// (from file info):
// uint16 - start address in Flash (offset)
// uint16 - length in bytes (makes sure parsing stops)
//
// First byte:
// 0x00 - Empty or V3 format
// 0x01-0xFE - Legacy format
// 0xFF - invalid
//
//
// V1 Legacy
// =========
// File structure:
// uint8 - number of devices, 0=none, 0xFF=invalid entry (probably Flash was erased)
//
// [Array of devices]
// [Offset = 2]
// uint8  - length of device record
// uint16 - short address
// uint64 - long IEEE address
// uint8  - number of endpoints
// [Array of endpoints]
// uint8  - endpoint number
// uint16 - profileID of the endpoint
// Array of uint8 - clusters In  codes, 0xFF end marker
// Array of uint8 - clusters Out codes, 0xFF end marker
//
// str    - ModelID (null terminated C string, 32 chars max)
// str    - Manuf   (null terminated C string, 32 chars max)
// str    - FriendlyName   (null terminated C string, 32 chars max)
// reserved for extensions
//  -- V2 --
// int8_t - zigbee profile of the device
//
// =======================
// v3 with version number
// File structure:
//
// uint8 - number of devices, 0=none, 0xFF=invalid entry (probably Flash was erased)
//
// [Array of devices]
// [Offset = 2]
// uint8  - length of device record
// uint16 - short address
// uint64 - long IEEE address
//
// str    - ModelID (null terminated C string, 32 chars max)
// str    - Manuf   (null terminated C string, 32 chars max)
// str    - FriendlyName   (null terminated C string, 32 chars max)
//
// [Array of endpoints]
// uint8  - endpoint number, 0xFF marks the end of endpoints
// uint8[] - list of configuration bytes, 0xFF marks the end
// i.e. 0xFF-0xFF marks the end of the array of endpoints
//
// =======================
// v4 which provides more extensibility
// File structure: (all values are little Endian)
//
// uint8 - number of devices, 0xFF indicates invalid file (or erased Flash?)
// [Array of devices, max to number of devices]
// [starts at offset = 3]
// uint16 - length of the device record, including the length field - allows to jump to next device

// [mandatory device data]
// uint16 - short address
// uint64 - long IEEE address
//
// str    - ModelID (null terminated C string, 32 chars max)
// str    - Manuf   (null terminated C string, 32 chars max)
// str    - FriendlyName   (null terminated C string, 32 chars max)
// 
// [Array of endpoints]
// uint8   - endpoint number, 0xFF marks the end of endpoints
// uint8   - length of the endpoint information, excuding length byte and endpoint number
// uint8[] - list of configuration bytes, 0xFF marks the end
// str     - (optional) FriendlyName for this endpoint (null terminated C string, 32 chars max), 0x00 if none
//
// [extended attributes]
// : any other data until `length of the device record` is reached
//


// Memory footprint
#ifdef ESP8266
const static uint16_t z_spi_start_sector = 0xFF;  // Force last bank of first MB
const static uint8_t* z_spi_start    = (uint8_t*) 0x402FF000;  // 0x402FF000
const static uint8_t* z_dev_start    = z_spi_start + 0x0800;   // 0x402FF800 - 2KB
const static size_t   z_spi_len      = 0x1000;   // 4kb blocks
const static size_t   z_block_offset = 0x0800;
const static size_t   z_block_len    = 0x0800;   // 2kb
#endif  // ESP8266
#ifdef ESP32
uint8_t* z_dev_start;
const static size_t   z_spi_len      = 0x1000;   // 4kb blocks
const static size_t   z_block_offset = 0x0000;   // No offset needed
const static size_t   z_block_len    = 0x1000;   // 4kb
#endif  // ESP32

// Each entry consumes 8 bytes
class Z_Flashentry {
public:
  uint32_t name;    // simple 4 letters name. Currently 'zig1', 'zig2'. 0xFFFFFFFF if not entry
  uint16_t len;     // len of object in bytes, 0xFFFF if no entry
  uint16_t start;   // address of start, 0xFFFF if empty, must be aligned on 128 bytes boundaries
};

class Z_Flashdirectory {
public:
  // 8 bytes header
  uint32_t magic;         // magic value 'Tsmt' to check that the block is initialized
  uint32_t clock;         // clock vector to discard entries that are made before this one. This should be incremented by 1 for each new entry (future anti-weavering)
  // entries, 14*8 = 112 bytes
  Z_Flashentry entries[14];
  uint32_t name;    // simple 4 letters name. Currently 'skey', 'crt ', 'crt1', 'crt2'
  uint16_t len;     // len of object
  uint16_t reserved; // align on 4 bytes boundary
  // link to next entry, none for now, but may be used for anti-weavering
  uint16_t next_dir;  // 0xFFFF if none
  uint16_t reserved1; // must be 0xFFFF
  uint32_t reserved2; // must be 0xFFFFFFFF
};

const static size_t   Z_MAX_FLASH = z_block_len - sizeof(Z_Flashentry);  // 2040

bool hibernateDeviceConfiguration(SBuffer & buf, const class Z_Data_Set & data, uint8_t endpoint) {
  bool found = false;
  for (auto & elt : data) {
    if (endpoint == elt.getEndpoint()) {
      buf.add8(elt.getConfigByte());
      found = true;
    }
  }
  return found;
}

/*********************************************************************************************\
 * hibernateDevice
 *
 * Transforms a single device into a SBuffer binary representation.
 * Only supports v2 (not the legacy old one long forgotten)
\*********************************************************************************************/
SBuffer hibernateDevice(const struct Z_Device &device) {
  SBuffer buf(256);

  buf.add16(0x0000);     // overall length, will be updated later
  buf.add16(device.shortaddr);
  buf.add64(device.longaddr);

  char *names[3] = { device.modelId, device.manufacturerId, device.friendlyName };

  for (uint32_t i=0; i<nitems(names); i++) {
    char *p = names[i];
    if (p) {
      size_t len = strlen(p);
      if (len > 32) { len = 32; }       // max 32 chars
      buf.addBuffer(p, len);
    }
    buf.add8(0x00);     // end of string marker
  }

  // check if we need to write fake endpoint 0x00
  buf.add8(0x00);
  uint32_t ep0_len_offset = buf.len();   // mark where to update the ep data length
  buf.add8(0x00);  
  if (hibernateDeviceConfiguration(buf, device.data, 0)) {
    buf.add8(0xFF); // end of configuration
    buf.add8(0x00); // empty ep friendly name, as it would duplicate the global friendly name
    // update the lenght of ep0_data_lenth
    buf.set8(ep0_len_offset, buf.len() - ep0_len_offset - 1);
  } else {
    buf.setLen(buf.len()-2);    // remove 2 bytes header
  }
  // scan endpoints
  for (uint32_t i=0; i<endpoints_max; i++) {
    uint8_t endpoint = device.endpoints[i];
    if (0x00 == endpoint) { break; }
    buf.add8(endpoint);
    uint32_t ep_len_offset = buf.len();   // mark where to update the ep data length
    buf.add8(0x00);  
    hibernateDeviceConfiguration(buf, device.data, endpoint);
    buf.add8(0xFF); // end of configuration
    const char * ep_name = device.ep_names.getEPName(endpoint);
    if (ep_name != nullptr) {
      size_t len = strlen(ep_name);
      if (len > 32) { len = 32; }       // max 32 chars
      buf.addBuffer(ep_name, len);
      buf.add8(0x00);     // end of string marker
    } else {
      buf.add8(0x00); // no endpoint name
    }
    // update the lenght of ep0_data_lenth
    buf.set8(ep_len_offset, buf.len() - ep_len_offset - 1);
  }
  buf.add8(0xFF); // end of endpoints

  // update overall length
  buf.set8(0, buf.len());

  return buf;
}


/*********************************************************************************************\
 * Write Devices in EEPROM/File/Flash
 * 
 * Updated to v4 format
 * 
 * Writes the preamble and all devices in the Univ_Write_File structure.
 * Does not close the file at the end.
 * Returns true if succesful.
 * In case of problem, the output file is left untouched
\*********************************************************************************************/
// EEPROM variant that writes one item at a time and is not limited to 2KB
bool hibernateDevices(Univ_Write_File & write_data);
bool hibernateDevices(Univ_Write_File & write_data) {
  // first prefix is number of devices
  size_t devices_size = zigbee_devices.devicesSize();
  if (devices_size > 250) { devices_size = 250; }         // arbitrarily limit to 250 devices in EEPROM instead of 32 in Flash
  uint8_t devices_size8 = devices_size;
  write_data.writeBytes((uint8_t*)&devices_size8, sizeof(devices_size8)); // write number of devices in file

  for (const auto & device : zigbee_devices.getDevices()) {
    const SBuffer buf = hibernateDevice(device);
    if (buf.len() > 0) {
      int32_t ret = write_data.writeBytes(buf.getBuffer(), buf.len());
      if (ret != buf.len()) {
        AddLog(LOG_LEVEL_ERROR, PSTR(D_LOG_ZIGBEE "Error writing Devices, written = %d, expected = %d"), ret, buf.len());
        return false;
      }
    }
  }
  return true;
}

// parse a single string from the saved data
// if something wrong happens, returns nullptr to ignore the string
// Index d is incremented to just after the string
const char * hydrateSingleString(const SBuffer & buf, uint32_t *d) {
  size_t s_len = buf.strlen(*d);
  const char * ptr = s_len ? buf.charptr(*d) : "";
  *d += s_len + 1;
  return ptr;
}

/*********************************************************************************************\
 * hydrateSingleDevice
 *
 * Transforms a binary representation to a Zigbee device
 * Supports only v2 and v4
\*********************************************************************************************/
void hydrateSingleDevice(const SBuffer & buf_d, uint32_t version) {
  uint32_t d = 0;   // index in device buffer
  uint16_t shortaddr = buf_d.get16(d);  d += 2;
  uint64_t longaddr  = buf_d.get64(d);  d += 8;
  size_t   buf_len = buf_d.len();
  Z_Device & device = zigbee_devices.updateDevice(shortaddr, longaddr);   // update device's addresses

  // ModelId
  device.setModelId(hydrateSingleString(buf_d, &d));

  // ManufID
  device.setManufId(hydrateSingleString(buf_d, &d));

  // FriendlyName
  device.setFriendlyName(hydrateSingleString(buf_d, &d));

  if (d >= buf_len) { return; }

  // Read per-endpoint information
  while (d < buf_len) {
    uint8_t ep = buf_d.get8(d++);
    if (0xFF == ep) { break; }        // ep 0xFF marks the end of the endpoints
    if (ep > 240 && ep != 0xF2) { ep = 0xFF; }      // ep == 0xFF means ignore
    device.addEndpoint(ep);           // it will ignore invalid endpoints
    
    if (version == 4) {
      if (d >= buf_len) { break; }    // end of buffer
      uint8_t ep_len = buf_d.get8(d++);
      if (d + ep_len > buf_len) { break; }    // buffer is too small to contain the announced data

      while (d < buf_len) {
        uint8_t config_type = buf_d.get8(d++);
        if (0xFF == config_type) { break; }                                // 0xFF marks the end of congiguration
        uint8_t config = config_type & 0x0F;
        Z_Data_Type type = (Z_Data_Type) (config_type >> 4);
        // set the configuration
        if (ep != 0xFF) {
          Z_Data & z_data = device.data.getByType(type, ep);
          if (&z_data != nullptr) {
            z_data.setConfig(config);
            Z_Data_Set::updateData(z_data);
          }
        }
      }
      // ability to have additional fields here
      // friendly name for ep
      if (d < buf_len) {
        device.setFriendlyEPName(ep, hydrateSingleString(buf_d, &d));
      }
      // additional information comes here
    } else {
      // version == 2
      while (d < buf_len) {
        uint8_t config_type = buf_d.get8(d++);
        if (0xFF == config_type) { break; }                                // 0xFF marks the end of congiguration
        uint8_t config = config_type & 0x0F;
        Z_Data_Type type = (Z_Data_Type) (config_type >> 4);
        // set the configuration
        if (ep != 0xFF) {
          Z_Data & z_data = device.data.getByType(type, ep);
          if (&z_data != nullptr) {
            z_data.setConfig(config);
            Z_Data_Set::updateData(z_data);
          }
        }
      }
    }
  }
}

/*********************************************************************************************\
 * loadZigbeeDevices
 *
 * Load device configuration from storage.
 * Order of storage for loading is: 1/ EEPROM 2/ File system 3/ Flash (ESP8266 only)
\*********************************************************************************************/
// dump = true, only dump to logs, don't actually load
bool loadZigbeeDevices(void) {
  Univ_Read_File f;   // universal reader
  const char * storage_class = PSTR("");
  uint32_t file_version = 4;        // currently supporting v3 and v4

#ifdef USE_UFILESYS
  File file;
  if (!f.valid() && dfsp) {
    file = dfsp->open(TASM_FILE_ZIGBEE, "r");
    if (!file) {
      file = dfsp->open(TASM_FILE_ZIGBEE_LEGACY_V2, "r");
      if (file)  { file_version = 2; }  // v2 found
    }
    if (file) {
      f.init(&file);
      storage_class = PSTR("File System");
    }
  }
#endif // USE_UFILESYS

#ifdef USE_ZIGBEE_EEPROM
  if (!f.valid() && zigbee.eeprom_ready) {
    f.init(ZIGB_NAME4);                   // try v4 first
    if (!f.valid()) {
      f.init(ZIGB_NAME2);                 // else try v2
      if (f.valid()) { file_version = 2; }  // v2 found
    }
    if (f.valid()) {
      storage_class = PSTR("EEPROM");
    }
  }
#endif // USE_ZIGBEE_EEPROM

#ifdef ESP8266
  if (!f.valid() && flash_valid()) {
    // Read binary data from Flash

    Z_Flashentry flashdata;
    memcpy_P(&flashdata, z_dev_start, sizeof(Z_Flashentry));
    // AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_ZIGBEE "z_dev_start %p"), z_dev_start);
    AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_ZIGBEE "Zigbee signature in Flash: %08X - %d"), flashdata.name, flashdata.len);

    // Check the signature
    if ( ((flashdata.name == ZIGB_NAME2) || (flashdata.name == ZIGB_NAME4))
        && (flashdata.len > 0)) {
      uint16_t buf_len = flashdata.len;
      if (flashdata.name == ZIGB_NAME2) { file_version = 2; }  // v2 found
      f.init(z_dev_start + sizeof(Z_Flashentry), buf_len);
      storage_class = PSTR("Flash");
    }
  }
#endif // ESP8266

  uint32_t file_len = 0;
  uint8_t num_devices = 0;
  if (f.valid()) {
    file_len = f.len;
    f.readBytes(&num_devices, sizeof(num_devices));
  }
  if ((file_len < 10) || (num_devices == 0x00) || (num_devices == 0xFF)) {             // No data
    AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "No Zigbee device information"));
    return false;
  }
  AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "Zigbee device information found in %s (%d devices - %d bytes)"), storage_class, num_devices, file_len);

  uint32_t k = 1;   // byte index in global buffer
  for (uint32_t i = 0; (i < num_devices) && (k < file_len); i++) {
    uint16_t dev_record_len = 0;
    size_t dev_record_len_bytes = file_version >= 4 ? 2 : 1;
    f.readBytes((uint8_t*)&dev_record_len, dev_record_len_bytes);   // starting with v4, length is 2 bytes
    if (dev_record_len == 0) {
      AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "Invalid device information, aborting"));
      zigbee_devices.clean();   // don't write back to Flash what we just loaded
      return false;
    }
    SBuffer buf(dev_record_len - dev_record_len_bytes);
    buf.setLen(dev_record_len - dev_record_len_bytes);
    buf.set8(0, dev_record_len - dev_record_len_bytes);    // push the first byte (len including this first byte)
    int32_t ret = f.readBytes(buf.buf(), dev_record_len - dev_record_len_bytes);
    if (ret != dev_record_len - dev_record_len_bytes) {
      AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "Invalid device information, aborting"));
      zigbee_devices.clean();   // don't write back to Flash what we just loaded
      return false;
    }

    hydrateSingleDevice(buf, file_version);

    // next iteration
    k += dev_record_len;
  }

  zigbee_devices.clean();   // don't write back to Flash what we just loaded
  return true;
}

/*********************************************************************************************\
 * saveZigbeeDevices
 *
 * Save device configuration from storage.
 * Order of storage for saving is: 1/ EEPROM 2/ File system 3/ Flash (ESP8266 only)
\*********************************************************************************************/
void saveZigbeeDevices(void) {
  Univ_Write_File f;
  const char * storage_class = PSTR("");

// TODO can we prioritize filesystem instead of eeprom?
#ifdef USE_ZIGBEE_EEPROM
  if (!f.valid() && zigbee.eeprom_ready) {
    f.init(ZIGB_NAME4);
    storage_class = PSTR("EEPROM");
  }
#endif

#ifdef USE_UFILESYS
  File file;
  if (!f.valid() && dfsp) {
    file = dfsp->open(TASM_FILE_ZIGBEE, "w");
    if (file) {
      f.init(&file);
      storage_class = PSTR("File System");
    }
  }
#endif

#if defined(ESP8266)
  uint8_t *sbuffer = nullptr;
  static const size_t max_flash_size = 2040;
  if (!f.valid() && flash_valid()) {
    sbuffer = (uint8_t*) malloc(max_flash_size);
    f.init(sbuffer, max_flash_size);
    storage_class = PSTR("Flash");
  }
#endif // defined(ESP8266)

  bool written = false;
  size_t buf_len = 0;
  if (f.valid()) {
    written = hibernateDevices(f);

    buf_len = f.getCursor();
    f.close();
    AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "Zigbee Devices Data saved in %s (%d bytes)"), storage_class, buf_len);
  }

#if defined(ESP8266)
  if (written && sbuffer != nullptr) {
    // first copy SPI buffer into ram
    uint8_t *spi_buffer = (uint8_t*) malloc(z_spi_len);
    if (!spi_buffer) {
      AddLog(LOG_LEVEL_ERROR, PSTR(D_LOG_ZIGBEE "Cannot allocate 4KB buffer"));
      free(sbuffer);
      return;
    }
    ESP.flashRead(z_spi_start_sector * SPI_FLASH_SEC_SIZE, (uint32_t*) spi_buffer, SPI_FLASH_SEC_SIZE);

    Z_Flashentry *flashdata = (Z_Flashentry*)(spi_buffer + z_block_offset);
    flashdata->name = ZIGB_NAME4;     // v4
    flashdata->len = buf_len;
    flashdata->start = 0;

    memcpy(spi_buffer + z_block_offset + sizeof(Z_Flashentry), sbuffer, buf_len);

    // buffer is now ready, write it back
    if (ESP.flashEraseSector(z_spi_start_sector)) {
      ESP.flashWrite(z_spi_start_sector * SPI_FLASH_SEC_SIZE, (uint32_t*) spi_buffer, SPI_FLASH_SEC_SIZE);
    }
    AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "Zigbee Devices Data store in Flash (0x%08X - %d bytes)"), z_dev_start, buf_len);
    free(spi_buffer);
    free(sbuffer);
  }
#endif // defined(ESP8266)

}


/*********************************************************************************************\
 * eraseZigbeeDevices
 *
 * Erase all storage locations: 1/ EEPROM, 2/ File system 3/ Flash (ESP8266 only if no filesystem)
\*********************************************************************************************/
// Erase the flash area containing the ZigbeeData
void eraseZigbeeDevices(void) {
  zigbee_devices.clean();     // avoid writing data to flash after erase
#ifdef USE_ZIGBEE_EEPROM
  ZFS_Erase();
#endif // USE_ZIGBEE_EEPROM

#if defined(ESP8266) && !defined(USE_UFILESYS)
  // first copy SPI buffer into ram
  uint8_t *spi_buffer = (uint8_t*) malloc(z_spi_len);
  if (!spi_buffer) {
    AddLog(LOG_LEVEL_ERROR, PSTR(D_LOG_ZIGBEE "Cannot allocate 4KB buffer"));
    return;
  }
  // copy the flash into RAM to make local change, and write back the whole buffer
  ESP.flashRead(z_spi_start_sector * SPI_FLASH_SEC_SIZE, (uint32_t*) spi_buffer, SPI_FLASH_SEC_SIZE);

  // Fill the Zigbee area with 0xFF
  memset(spi_buffer + z_block_offset, 0xFF, z_block_len);

  // buffer is now ready, write it back
  if (ESP.flashEraseSector(z_spi_start_sector)) {
    ESP.flashWrite(z_spi_start_sector * SPI_FLASH_SEC_SIZE, (uint32_t*) spi_buffer, SPI_FLASH_SEC_SIZE);
  }

  free(spi_buffer);
  AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "Zigbee Devices Data erased in %s"), PSTR("Flash"));
#endif  // defined(ESP8266) && !defined(USE_UFILESYS)

#ifdef USE_UFILESYS
  if (TfsDeleteFile(TASM_FILE_ZIGBEE)) {
    AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "Zigbee Devices Data erased"));
  }
#endif  // USE_UFILESYS
}

/*********************************************************************************************\
 * restoreDumpAllDevices
 *
 * Dump all devices in `ZbRestore <hex>` format ready to copy/paste
\*********************************************************************************************/
void restoreDumpAllDevices(void) {
  for (const auto & device : zigbee_devices.getDevices()) {
    const SBuffer buf = hibernateDevice(device);
    if (buf.len() > 0) {
      Response_P(PSTR("{\"" D_PRFX_ZB D_CMND_ZIGBEE_RESTORE "\":\"ZbRestore %_B\"}"), &buf);
      MqttPublishPrefixTopicRulesProcess_P(RESULT_OR_STAT, PSTR(D_PRFX_ZB D_CMND_ZIGBEE_DATA));
    }
  }
}

#endif // USE_ZIGBEE
