// ***************************************************************************
// RollingBuffer_p.cpp (c) 2011 Derek Barnett
// Marth Lab, Department of Biology, Boston College
// ---------------------------------------------------------------------------
// Last modified: 8 December 2011 (DB)
// ---------------------------------------------------------------------------
// Provides a dynamic I/O FIFO byte queue, which removes bytes as they are
// read from the front of the buffer and grows to accept bytes being written
// to buffer end.
//
// implementation note: basically a 'smart' wrapper around 1..* ByteArrays
// ***************************************************************************

#include "api/internal/io/RollingBuffer_p.h"
using namespace BamTools;
using namespace BamTools::Internal;

#include <algorithm>
#include <climits>
#include <cstddef>
#include <cstring>
#include <string>

// ------------------------------
// RollingBuffer implementation
// ------------------------------

RollingBuffer::RollingBuffer(std::size_t growth)
    : m_bufferGrowth(growth)
{
    // buffer always contains at least 1 (maybe empty) byte array
    m_data.push_back(ByteArray());

    // set cleared state
    Clear();
}

std::size_t RollingBuffer::BlockSize() const
{

    // if only one byte array in buffer <- needed?
    if (m_tailBufferIndex == 0) {
        return m_tail - m_head;
    }

    // otherwise return remaining num bytes in first array
    const ByteArray& first = m_data.front();
    return first.Size() - m_head;
}

bool RollingBuffer::CanReadLine() const
{
    return IndexOf('\n') != std::string::npos;
}

void RollingBuffer::Chop(std::size_t n)
{

    // update buffer size
    if (n > m_totalBufferSize) {
        m_totalBufferSize = 0;
    } else {
        m_totalBufferSize -= n;
    }

    // loop until target case hit
    for (;;) {

        // if only one array, decrement tail
        if (m_tailBufferIndex == 0) {
            m_tail -= n;

            // if all data chopped
            if (m_tail <= m_head) {
                m_head = 0;
                m_tail = 0;
            }
            return;
        }

        // if there's room in last byte array to 'chop', just decrement tail
        if (n <= m_tail) {
            m_tail -= n;
            return;
        }

        // otherwise we're going to overlap our internal byte arrays
        // reduce our chop amount by the amount of data in the last byte array
        n -= m_tail;

        // remove last byte array & set tail to it's end
        m_data.pop_back();
        --m_tailBufferIndex;
        m_tail = m_data.at(m_tailBufferIndex).Size();
    }

    // if buffer is now empty, reset state & clear up memory
    if (IsEmpty()) {
        Clear();
    }
}

void RollingBuffer::Clear()
{

    // remove all byte arrays (except first)
    m_data.erase(m_data.begin() + 1, m_data.end());

    // clear out first byte array
    m_data[0].Resize(0);
    m_data[0].Squeeze();

    // reset index & size markers
    m_head = 0;
    m_tail = 0;
    m_tailBufferIndex = 0;
    m_totalBufferSize = 0;
}

void RollingBuffer::Free(std::size_t n)
{

    // update buffer size
    if (n > m_totalBufferSize) {
        m_totalBufferSize = 0;
    } else {
        m_totalBufferSize -= n;
    }

    // loop until target case hit
    for (;;) {

        const std::size_t blockSize = BlockSize();

        // if there's room in current array
        if (n < blockSize) {

            // shift 'head' over @n bytes
            m_head += n;

            // check for emptied, single byte array
            if (m_head == m_tail && m_tailBufferIndex == 0) {
                m_head = 0;
                m_tail = 0;
            }

            break;
        }

        // otherwise we need to check next byte array
        // first update amount to remove
        n -= blockSize;

        // special case - there was only 1 array
        if (m_data.size() == 1) {
            if (m_data.at(0).Size() != m_bufferGrowth) {
                m_data[0].Resize(m_bufferGrowth);
            }
            m_head = 0;
            m_tail = 0;
            m_tailBufferIndex = 0;
            break;
        }

        // otherwise, remove first array and move to next iteration
        m_data.pop_front();
        --m_tailBufferIndex;
        m_head = 0;
    }

    // if buffer is now empty, reset state & clear up memory
    if (IsEmpty()) {
        Clear();
    }
}

std::size_t RollingBuffer::IndexOf(char c) const
{

    // skip processing if empty buffer
    if (IsEmpty()) {
        return std::string::npos;
    }

    std::size_t index(0);

    // iterate over byte arrays
    const std::size_t numBuffers = m_data.size();
    for (std::size_t i = 0; i < numBuffers; ++i) {
        const ByteArray& current = m_data.at(i);

        // if on first array, use head; else 0
        const std::size_t start = ((i == 0) ? m_head : 0);

        // if on last array, set end; else use current byte array size
        const std::size_t end = ((i == m_tailBufferIndex) ? m_tail : current.Size());

        // look through this iteration's byte array for @c
        const char* p = current.ConstData() + start;
        for (std::size_t j = start; j < end; ++j) {
            if (*p++ == c) {
                return index;
            }
            ++index;
        }
    }

    // no match found
    return std::string::npos;
}

bool RollingBuffer::IsEmpty() const
{
    return (m_tailBufferIndex == 0) && (m_tail == 0);
}

std::size_t RollingBuffer::Read(char* dest, std::size_t max)
{

    std::size_t bytesToRead = std::min(Size(), max);
    std::size_t bytesReadSoFar = 0;

    while (bytesReadSoFar < bytesToRead) {
        const char* readPtr = ReadPointer();
        std::size_t blockBytes = std::min((bytesToRead - bytesReadSoFar), BlockSize());
        if (dest) {
            std::memcpy(dest + bytesReadSoFar, readPtr, blockBytes);
        }
        bytesReadSoFar += blockBytes;
        Free(blockBytes);
    }

    return bytesReadSoFar;
}

std::size_t RollingBuffer::ReadLine(char* dest, std::size_t max)
{

    // if we can't read line or if max is 0
    if (!CanReadLine() || max == 0) {
        return 0;
    }

    // otherwise, read until we hit newline
    std::size_t bytesReadSoFar = 0;
    bool finished = false;
    while (!finished) {

        const std::size_t index = IndexOf('\n');
        const char* readPtr = ReadPointer();
        std::size_t bytesToRead = std::min((index + 1) - bytesReadSoFar, BlockSize());
        bytesToRead = std::min(bytesToRead, (max - 1) - bytesReadSoFar);
        std::memcpy(dest + bytesReadSoFar, readPtr, bytesToRead);
        bytesReadSoFar += bytesToRead;
        Free(bytesToRead);

        if (!((bytesReadSoFar < index + 1) && (bytesReadSoFar < max - 1))) {
            finished = true;
        }
    }

    // null terminate 'dest' & return numBytesRead
    dest[bytesReadSoFar] = '\0';
    return bytesReadSoFar;
}

const char* RollingBuffer::ReadPointer() const
{

    // return null if empty buffer
    if (m_data.empty()) {
        return 0;
    }

    // otherwise return pointer to current position
    const ByteArray& first = m_data.front();
    return first.ConstData() + m_head;
}

char* RollingBuffer::Reserve(std::size_t n)
{

    // if empty buffer
    if (m_totalBufferSize == 0) {
        m_data[0].Resize(std::max(m_bufferGrowth, n));
        m_totalBufferSize += n;
        m_tail = n;
        return m_data[m_tailBufferIndex].Data();
    }

    // increment buffer's byte count
    m_totalBufferSize += n;

    // if buffer already contains enough space to fit @n more bytes
    if ((m_tail + n) <= m_data.at(m_tailBufferIndex).Size()) {

        // fetch write pointer at current 'tail', increment tail by @n & return
        char* ptr = m_data[m_tailBufferIndex].Data();  //+ m_tail;
        m_tail += n;
        return ptr;
    }

    // if last byte array isn't half full
    if (m_tail < m_data.at(m_tailBufferIndex).Size() / 2) {

        // we'll allow simple resize
        m_data[m_tailBufferIndex].Resize(m_tail + n);

        // fetch write pointer at current 'tail', increment tail by @n & return
        char* ptr = m_data[m_tailBufferIndex].Data();  //+ m_tail;
        m_tail += n;
        return ptr;
    }

    // otherwise, shrink last byte array to current used size
    m_data[m_tailBufferIndex].Resize(m_tail);

    // then append new byte array
    m_data.push_back(ByteArray());
    ++m_tailBufferIndex;
    m_data[m_tailBufferIndex].Resize(std::max(m_bufferGrowth, n));
    m_tail = n;

    // return write-able pointer on new array
    return m_data[m_tailBufferIndex].Data();
}

std::size_t RollingBuffer::Size() const
{
    return m_totalBufferSize;
}

void RollingBuffer::Write(const char* src, std::size_t n)
{
    char* writePtr = Reserve(n);
    std::memcpy(writePtr, src, n);
}
