#include "Dtypes.h"
#include "SessionChunk.h"
#include "FilesystemAbstraction.h"
#include "Logger.h"


template <typename Measurement_T, uint32_t MAX_SIZE>
class SimpleMeasurementSession
{
public:
    using ChunkT = SessionChunk<Measurement_T, MAX_SIZE>;
    using MeasurementType = Measurement_T;
    // save interval in number of measurements (by default every minute)
    SimpleMeasurementSession(uint32_t saveInterval = 10 * 20)
        : chunk(nullptr), saveInterval_(saveInterval)
    {
    }

    ~SimpleMeasurementSession()
    {
        if (chunk != nullptr)
            free(chunk);
    }

    void init(uint32_t epochStartTime)
    {
        if (chunk == nullptr)
        {
            // psram allocation doesn't seem to work in constructor
            chunk = (ChunkT *)heap_caps_malloc(sizeof(ChunkT), MALLOC_CAP_SPIRAM);
            new (chunk) ChunkT(); // placement new to init chunk
        }
        chunk->init(epochStartTime, 0);
    }

    bool addPoint(Measurement_T measurement)
    {
        bool success = chunk->addPoint(measurement);
        if (success && (chunk->numMeasurements() % saveInterval_) == 0)
            saveToFileSystem();
        if (!success)
            LOG_WARNING("Failed to add point");
        return success;
    }

    void finalize()
    {
        if (numMeasurements() > 0)
            saveToFileSystem();
        chunk->init(0, 0);
    }

    uint32_t getStartTime() const
    {
        return chunk->getStartTime();
    }

    uint32_t numMeasurements() const
    {
        return chunk->numMeasurements();
    }

    template <typename Encoder_T>
    void serialize(Encoder_T &encoder, uint32_t startIdx) const
    {
        ChunkT::sendHeader(encoder, chunk->getStartTime(), startIdx);
        auto numElementsToSend = chunk->numMeasurements() - startIdx;
        encoder.sendArray(chunk->getDataPointer() + startIdx, numElementsToSend);
    }

    Measurement_T *getDataPointer()
    {
        return chunk->getDataPointer();
    }

private:
    void saveToFileSystem()
    {
        static const uint32_t arrayHeaderOffset = ChunkT::arrayHeaderOffset();
        const uint32_t numMeasurements = chunk->numMeasurements();

        // todo: check this! free doesn't mean that the file writing actually works ok
        // use error codes of write instead? anyway: test it!
        LOG_INFO("%ld saveToFileSystem start", millis());
        deleteUntilBytesFree(CONFIG_SESSION_MAX_SIZE);
        LOG_INFO("   %ld after deleteUntilBytesFree()", millis());

        String filename = String(CONFIG_DATA_PATH) + "/" + String(chunk->getStartTime());
        if (portablefs::exists(filename.c_str()))
        {
            auto file = portablefs::open(filename.c_str(), "r+");
            file.seek(0, SeekEnd);
            size_t existingMeasurements = (file.size() - ChunkT::valueOffset()) / sizeof(Measurement_T);
            size_t measurementsToWrite = numMeasurements - existingMeasurements;
            Measurement_T *startPtr = chunk->getDataPointer() + existingMeasurements;
            file.write((uint8_t *)(startPtr), measurementsToWrite * sizeof(Measurement_T));
            
            file.seek(arrayHeaderOffset);

            StreamingMsgPackEncoder<portablefs::File> encoder(&file);
            encoder.template sendArrayHeader<Measurement_T>(numMeasurements);
        }
        else
        {
            auto file = portablefs::open(filename.c_str(), "w");
            StreamingMsgPackEncoder<portablefs::File> encoder(&file);
            chunk->serialize(encoder);
        }
        LOG_INFO("   %ld saveToFileSystem done", millis());
    }

    void deleteUntilBytesFree(size_t requiredSpace)
    {
        auto freeBytes = portablefs::totalBytes() - portablefs::usedBytes();
        while (freeBytes < requiredSpace)
        {
            uint32_t nextSessionToDelete = uint32_t(-1);

            auto dir = portablefs::openDir(CONFIG_DATA_PATH);
            String filenameToDelete;
            while (dir.next())
            {
                if (dir.isFile())
                {
                    const auto fileName = dir.fileName();
                    const auto fileNameWithoutDir = fileName.substring(fileName.lastIndexOf("/") + 1);
                    uint32_t sessionId = fileNameWithoutDir.toInt();
                    if (sessionId < nextSessionToDelete)
                    {
                        nextSessionToDelete = sessionId;
                        filenameToDelete = dir.fileName();
                    }
                }
            }
            assert(nextSessionToDelete > 0);
            assert(nextSessionToDelete < uint32_t(-1));
            LOG_INFO("Removing old session %s to make space", filenameToDelete.c_str());
            portablefs::remove(filenameToDelete.c_str());
            auto newFreeBytes = portablefs::totalBytes() - portablefs::usedBytes();
            assert(newFreeBytes > freeBytes);
            freeBytes = newFreeBytes;
        }
    }

    ChunkT *chunk;
    uint32_t saveInterval_;
};
