#include "SessionChunk.h"


template<typename Measurement_T, typename Reader, typename Writer, uint32_t CHUNK_SIZE>
class MeasurementSession {
public:
    typedef SessionChunk<Measurement_T, CHUNK_SIZE> Chunk_T;

    MeasurementSession()
            : currentChunk(&chunks[0]),
              otherChunk(&chunks[1]) {}

    void init(uint32_t epochStartTime) {
        currentChunk = &chunks[0];
        otherChunk = &chunks[1];
        currentChunk->init(epochStartTime, 0);
        otherChunk->init(0, 0);
    }

    bool addPoint(Measurement_T measurement) {
        const bool successful = currentChunk->addPoint(measurement);
        if (!successful) {
            Serial.println("Starting session rotate");
            rotate();
            const bool secondInsertSuccess = currentChunk->addPoint(measurement);
            assert_msg(secondInsertSuccess, "Session: insertion after rotation failed");
            // TODO check that there is place for file - remove old files
        }
        return true;
    }

    void finalize() {
        if( otherChunkFilled() )
            saveChunkToFile(otherChunk);
        if( currentChunk->numMeasurements() > 0) {
            saveChunkToFile(currentChunk);
        }
        currentChunk->init(0, 0);
        otherChunk->init(0, 0);
    }

    uint32_t numMeasurements() const {
        return currentChunk->getStartIndex() + currentChunk->numMeasurements();
    }

    template<typename Encoder_T>
    void serialize(Encoder_T & encoder, uint32_t startIdx) const
    {
        const uint32_t lastIdx = currentChunk->getStartIndex() + currentChunk->numMeasurements();
        if( lastIdx <= startIdx) {
            encoder.template sendArray<Measurement_T>(nullptr, 0);
            return;
        }

        Chunk_T::sendHeader(encoder, currentChunk->getStartTime(), startIdx);
        encoder.template sendArrayHeader<Measurement_T>(lastIdx - startIdx);
        while(startIdx < lastIdx)
            startIdx = serializeChunk(encoder, startIdx);
        assert_msg(startIdx == lastIdx, "Not all data was sent");
    }

    uint32_t getStartTime() const {
        return currentChunk->getStartTime();
    }
private:
    void rotate() {
        if( otherChunkFilled() )
            saveChunkToFile(otherChunk);
        swapChunks();

        currentChunk->init(otherChunk->getStartTime(), otherChunk->getStartIndex() + CHUNK_SIZE);
    }

    bool otherChunkFilled() const {
        return otherChunk->numMeasurements() > 0;
    }

    void swapChunks() {
        Chunk_T *tmp = currentChunk;
        currentChunk = otherChunk;
        otherChunk = tmp;
    }

    void saveChunkToFile(Chunk_T *chunk) const {
        const uint32_t chunkNr = chunk->getStartIndex() / CHUNK_SIZE;
        const auto fileName = chunkFileName(chunkNr, chunk->getStartTime());
        Serial.print("Writing session to file ");
        Serial.println(fileName);
        Writer writer(fileName);
        chunk->serialize(writer.encoder());
    };

    template< typename Encoder_T>
    uint32_t serializeChunk(Encoder_T & encoder, uint32_t startIdx) const {
        assert_msg( startIdx < currentChunk->getStartIndex() + currentChunk->numMeasurements(),
                   "serializeChunk: invalid startIdx" );

        if( startIdx >= currentChunk->getStartIndex() ) {
            const auto localStartIdx = startIdx - currentChunk->getStartIndex();
            const auto numElements = currentChunk->numMeasurements() - localStartIdx;
            assert_msg(numElements <= currentChunk->numMeasurements(), "Internal problem in serializeChunk");
            encoder.sendArrayPartialContents( currentChunk->getDataPointer() + localStartIdx, numElements );
            return currentChunk->getStartIndex() + currentChunk->numMeasurements();
        } else if( startIdx >= otherChunk->getStartIndex() && otherChunkFilled() ) {
            encoder.sendArrayPartialContents( otherChunk->getDataPointer(), otherChunk->numMeasurements() );
            assert_msg( otherChunk->numMeasurements(), CHUNK_SIZE );
            return otherChunk->getStartIndex() + otherChunk->numMeasurements();
        } else {
            if( encoder.getSizeCountMode() ) {
                encoder.template sendArrayPartialContents<Measurement_T>(nullptr, CHUNK_SIZE);
            } else {
                const uint32_t chunkNr = startIdx / CHUNK_SIZE;
                const auto chunkFileNameStr = chunkFileName(chunkNr, currentChunk->getStartTime());
                Reader reader(chunkFileNameStr);
                reader.seek(Chunk_T::valueOffset());

                const uint32_t PART_SIZE = 32;
#ifndef ARDUINO
                static_assert((PART_SIZE < CHUNK_SIZE) && (CHUNK_SIZE % PART_SIZE == 0));
#endif
                Measurement_T buffer[PART_SIZE];
                for(uint32_t i = 0; i < CHUNK_SIZE; i += PART_SIZE)
                {
                    reader.readBytes((char*) buffer, sizeof(Measurement_T) * PART_SIZE);
                    encoder.template sendArrayPartialContents<Measurement_T>(buffer, PART_SIZE);
                }
            }
            return startIdx + CHUNK_SIZE;
        }
    }

    static String chunkFileName(uint32_t chunkNr, uint32_t startTime) {
        return("/dat/" + toString(startTime) + String("_") + toString(chunkNr));
    }

    Chunk_T chunks[2];
    Chunk_T *currentChunk;
    Chunk_T *otherChunk;
};
