#ifndef EEPROM_h
#define EEPROM_h

#include <Arduino.h>
#include <inttypes.h>
#include "string.h"

#define EEPROM_OP_TIMEOUT 100000
#define EEPROM_PAGE_WORDS 32      // words number per page
#define EEPROM_PAGE_COUNT 8       // user EEPROM pages number
#define EEPROM_START_ADDR 0x1C00  // user EEPROM start address
#define EEPROM_WORD_SIZE  4       // word takes 4 bytes
#define EEPROM_PAGE_SIZE  ( EEPROM_PAGE_WORDS * EEPROM_WORD_SIZE )   // page takes 32*4 = 128 bytes
#define EEPROM_END        0x1FFF
#define EEPROM_LENGTH     (EEPROM_PAGE_SIZE * EEPROM_PAGE_COUNT)

void HAL_read(uint16_t addr, uint32_t * data);
void HAL_write(uint16_t addr, uint32_t * data);
void HAL_erase(uint16_t addr);
uint8_t read_byte( int idx );
void write_byte( int idx, uint8_t val );
void update_byte( int idx, uint8_t val );

struct EERef{

    EERef( const int index )
        : index( index )                 {}
    
    uint8_t operator*() const            { return read_byte( index ); }
    operator uint8_t() const             { return **this; }
    
    EERef &operator=( const EERef &ref ) { return *this = *ref; }
    EERef &operator=( uint8_t in )       { return write_byte( index, in ), *this;  }
    EERef &operator +=( uint8_t in )     { return *this = **this + in; }
    EERef &operator -=( uint8_t in )     { return *this = **this - in; }
    EERef &operator *=( uint8_t in )     { return *this = **this * in; }
    EERef &operator /=( uint8_t in )     { return *this = **this / in; }
    EERef &operator ^=( uint8_t in )     { return *this = **this ^ in; }
    EERef &operator %=( uint8_t in )     { return *this = **this % in; }
    EERef &operator &=( uint8_t in )     { return *this = **this & in; }
    EERef &operator |=( uint8_t in )     { return *this = **this | in; }
    EERef &operator <<=( uint8_t in )    { return *this = **this << in; }
    EERef &operator >>=( uint8_t in )    { return *this = **this >> in; }
    
    EERef &update( uint8_t in )          { return  in != *this ? *this = in : *this; }
    
    /** Prefix increment/decrement **/
    EERef& operator++()                  { return *this += 1; }
    EERef& operator--()                  { return *this -= 1; }
    
    /** Postfix increment/decrement **/
    uint8_t operator++ (int){ 
        uint8_t ret = **this;
        return ++(*this), ret;
    }

    uint8_t operator-- (int){ 
        uint8_t ret = **this;
        return --(*this), ret;
    }
    
    int index;
};

struct EEPtr{

    EEPtr( const int index )
        : index( index )                {}
        
    operator int() const                { return index; }
    EEPtr &operator=( int in )          { return index = in, *this; }
    
    bool operator!=( const EEPtr &ptr ) { return index != ptr.index; }
    EERef operator*()                   { return index; }
    
    EEPtr& operator++()                 { return ++index, *this; }
    EEPtr& operator--()                 { return --index, *this; }
    EEPtr operator++ (int)              { return index++; }
    EEPtr operator-- (int)              { return index--; }

    int index;
};

static inline uint32_t eeprom_reverse_word(uint32_t word)
{
    return ((word & 0x000000FFUL) << 24) |
           ((word & 0x0000FF00UL) << 8)  |
           ((word & 0x00FF0000UL) >> 8)  |
           ((word & 0xFF000000UL) >> 24);
}

struct EEPROMClass{

    //Basic user access methods.
    EERef operator[]( const int idx )    { return idx; }
    uint8_t read( int idx )              { return EERef( idx ); }
    void write( int idx, uint8_t val )   { (EERef( idx )) = val; }
    void update( int idx, uint8_t val )  { update_byte(idx, val); }
    
    void begin();
    EEPtr end()                          { return length(); } // Standards requires this to be the item after the last valid entry. The returned pointer is invalid.
    uint16_t length()                    { return (uint16_t)EEPROM_LENGTH; }
    
    template< typename T >
    const T &put(int idx, const T &data)
    {
        void* dataPointer = (void*)&data;
        // check if idx is valid
        if (idx < 0)
        {
            ErrorMsgHandler("EEPROM.put(): The eeprom cell address must be non-negative");
            return data;
        }
        if ((uint32_t)idx >= (uint32_t)EEPROM_LENGTH)
        {
            ErrorMsgHandler("EEPROM.put(): The address of the eeprom cell goes beyond the eeprom");
            return data;
        }
        if (sizeof(data) > ((uint32_t)EEPROM_LENGTH - (uint32_t)idx))
        {
            ErrorMsgHandler("EEPROM.put(): The data goes beyond the eeprom");
            return data;
        }

        uint32_t write_data_buf[EEPROM_PAGE_WORDS] = {};
        uint32_t old_data_buf[EEPROM_PAGE_WORDS];
        uint32_t dataSize = sizeof(data);   // writing data size
        uint32_t dataShift = 0;             // shift of the data writing start address
        uint32_t writeSize = dataSize;
        
        // calc start address of the desired page
        uint32_t addr = EEPROM_START_ADDR + (((uint32_t)idx) / EEPROM_PAGE_SIZE) * EEPROM_PAGE_SIZE;
        // address of the searched word in eeprom: EEPROM_START_ADDR + (uint32_t)idx
        uint32_t word_addr = EEPROM_START_ADDR + (((uint32_t)idx) / EEPROM_WORD_SIZE) * EEPROM_WORD_SIZE;
        // searched word index in the write_data_buf array: the word address in the eeprom minus the page beginning address 
        uint32_t word_idx  = (word_addr - addr) / EEPROM_WORD_SIZE;
        // page data start address
        uint32_t byte_addr = (uint32_t)idx % EEPROM_PAGE_SIZE;
        // byte number in a word
        uint32_t byte_idx  = ((uint32_t)idx) % EEPROM_WORD_SIZE;
        // read first page
        HAL_read((uint16_t)addr, write_data_buf);
        memcpy(old_data_buf, write_data_buf, sizeof(write_data_buf));
        // if data does not fit on the first page, then write down only what fits
        if (EEPROM_PAGE_SIZE - byte_addr < dataSize)
            writeSize = EEPROM_PAGE_SIZE - byte_addr;
        uint32_t lastWord = (writeSize + byte_idx - 1) / EEPROM_WORD_SIZE + word_idx;
        dataSize -= writeSize;
        // Convert affected words to byte order suitable for memcpy()
        for(uint8_t i = word_idx; i <= lastWord; i++)
        {
            write_data_buf[i] = eeprom_reverse_word(write_data_buf[i]);
        }
        // write data page by page, first separately write the first page
        memcpy((void *)((uint8_t *)write_data_buf + byte_addr), (void*)dataPointer, writeSize);
        // prepare words for writing
        for(uint8_t i = word_idx; i <= lastWord; i++)
        {
            write_data_buf[i] = eeprom_reverse_word(write_data_buf[i]);
        }
        if (memcmp(old_data_buf, write_data_buf, sizeof(write_data_buf)) != 0)
        {
            HAL_erase((uint16_t)addr);
            HAL_write((uint16_t)addr, write_data_buf);
        }
        // if there is data left after writing the first page, then write it page by page until it runs out
        while (dataSize > 0)
        {
            addr += EEPROM_PAGE_SIZE;
            HAL_read((uint16_t)addr, write_data_buf);
            memcpy(old_data_buf, write_data_buf, sizeof(write_data_buf));
            dataShift += writeSize;
            writeSize = dataSize;
            if (EEPROM_PAGE_SIZE < dataSize)
                writeSize = EEPROM_PAGE_SIZE;
            lastWord = (writeSize - 1) / EEPROM_WORD_SIZE;
            // Convert affected words to byte order suitable for memcpy()
            for(uint8_t i = 0; i <= lastWord; i++)
            {
                write_data_buf[i] = eeprom_reverse_word(write_data_buf[i]);
            }
            memcpy((void *)(write_data_buf), (void*)((uint8_t *)dataPointer + dataShift), writeSize);
            // prepare words for writing
            for(uint8_t i = 0; i <= lastWord; i++)
            {
                write_data_buf[i] = eeprom_reverse_word(write_data_buf[i]);
            }
            if (memcmp(old_data_buf, write_data_buf, sizeof(write_data_buf)) != 0)
            {
                HAL_erase((uint16_t)addr);
                HAL_write((uint16_t)addr, write_data_buf);
            }
            dataSize -= writeSize;
        }
        return data;
    }

    template< typename T >
    T &get(int idx, T &data)
    {
        void* dataPointer = (void*)&data;
        // check if idx is valid
        if (idx < 0)
        {
            ErrorMsgHandler("EEPROM.get(): The eeprom cell address must be non-negative");
            return data;
        }
        if ((uint32_t)idx >= (uint32_t)EEPROM_LENGTH)
        {
            ErrorMsgHandler("EEPROM.get(): The address of the eeprom cell goes beyond the eeprom");
            return data;
        }
        if (sizeof(data) > ((uint32_t)EEPROM_LENGTH - (uint32_t)idx))
        {
            ErrorMsgHandler("EEPROM.get(): The data goes beyond the eeprom");
            return data;
        }

        uint32_t read_data_buf[EEPROM_PAGE_WORDS] = {}; 
        uint32_t dataSize = sizeof(data);   // reading data size
        uint32_t dataShift = 0;             // shift of the data reading start address
        uint32_t readSize = dataSize;

        // calc start address of the desired page
        uint32_t addr = EEPROM_START_ADDR + (((uint32_t)idx) / EEPROM_PAGE_SIZE) * EEPROM_PAGE_SIZE;
        // address of the searched word in eeprom: EEPROM_START_ADDR + (uint32_t)idx
        uint32_t word_addr = EEPROM_START_ADDR + (((uint32_t)idx) / EEPROM_WORD_SIZE) * EEPROM_WORD_SIZE;
        // searched word index in the read_data_buf array: the word address in the eeprom minus the page beginning address 
        uint32_t word_idx  = (word_addr - addr) / EEPROM_WORD_SIZE;
        // page data start address
        uint32_t byte_addr = (uint32_t)idx % EEPROM_PAGE_SIZE;
        // byte number in a word
        uint32_t byte_idx  = ((uint32_t)idx) % EEPROM_WORD_SIZE;
        // read first page
        HAL_read((uint16_t)addr, read_data_buf);
        if (EEPROM_PAGE_SIZE - byte_addr < dataSize)
            readSize = EEPROM_PAGE_SIZE - byte_addr;
        uint32_t lastWord = (readSize + byte_idx - 1) / EEPROM_WORD_SIZE + word_idx;
        dataSize -= readSize;
        // prepare words
        for(uint8_t i = word_idx; i <= lastWord; i++)
        {
            read_data_buf[i] = eeprom_reverse_word(read_data_buf[i]);
        }
        // read data page by page, first separately read the first page
        memcpy((void *)dataPointer, (void*)((uint8_t *)read_data_buf + byte_addr), readSize);

        // if there is data left after reading the first page, then read it page by page until it runs out
        while (dataSize > 0)
        {
            addr += EEPROM_PAGE_SIZE;
            HAL_read((uint16_t)addr, read_data_buf);
            dataShift += readSize;
            readSize = dataSize;
            if (EEPROM_PAGE_SIZE < dataSize)
                readSize = EEPROM_PAGE_SIZE;
            lastWord = (readSize - 1) / EEPROM_WORD_SIZE;
            // prepare words
            for(uint8_t i = 0; i <= lastWord; i++)
            {
                read_data_buf[i] = eeprom_reverse_word(read_data_buf[i]);
            }
            memcpy((void *)((uint8_t *)dataPointer + dataShift), (void*)(read_data_buf), readSize);
            dataSize -= readSize;
        }
        return data;                     // Return passed object pointer with the read data
    }
};

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-variable"
static EEPROMClass EEPROM;
#pragma GCC diagnostic pop

#endif