/*
 *
 *    Copyright (c) 2020-2021 Project CHIP Authors
 *    Copyright (c) 2016-2017 Nest Labs, Inc.
 *    All rights reserved.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

/**
 *    @file
 *      This file implements the circular buffer for TLV
 *      elements. When used as the backing store for the TLVReader and
 *      TLVWriter, those classes will work with the wraparound of data
 *      within the buffer.  Additionally, the TLVWriter will be able
 *      to continually add top-level TLV elements by evicting
 *      pre-existing elements.
 */
#include <lib/core/TLVCircularBuffer.h>

#include <lib/core/CHIPError.h>
#include <lib/core/TLVReader.h>
#include <lib/core/TLVTags.h>
#include <lib/core/TLVWriter.h>
#include <lib/support/BufferWriter.h>
#include <lib/support/CodeUtils.h>

#include <stdint.h>

namespace chip {
namespace TLV {

using namespace chip::Encoding;

/**
 * @brief
 *   TLVCircularBuffer constructor
 *
 * @param[in] inBuffer       A pointer to the backing store for the queue
 *
 * @param[in] inBufferLength Length, in bytes, of the backing store
 *
 * @param[in] inHead         Initial point for the head.  The @a inHead pointer is must fall within the backing store for the
 * circular buffer, i.e. within @a inBuffer and &(@a inBuffer[@a inBufferLength])
 */
TLVCircularBuffer::TLVCircularBuffer(uint8_t * inBuffer, uint32_t inBufferLength, uint8_t * inHead)
{
    mQueue       = inBuffer;
    mQueueSize   = inBufferLength;
    mQueueLength = 0;
    mQueueHead   = inHead;

    mProcessEvictedElement = nullptr;
    mAppData               = nullptr;

    // use common as opposed to unspecified, s.t. the reader that
    // skips over the elements does not complain about implicit
    // profile tags.
    mImplicitProfileId = kCommonProfileId;
}

/**
 * @brief
 *   TLVCircularBuffer constructor
 *
 * @param[in] inBuffer       A pointer to the backing store for the queue
 *
 * @param[in] inBufferLength Length, in bytes, of the backing store
 */
TLVCircularBuffer::TLVCircularBuffer(uint8_t * inBuffer, uint32_t inBufferLength)
{
    Init(inBuffer, inBufferLength);
}

/**
 * @brief
 *   TLVCircularBuffer Init function
 *
 * @param[in] inBuffer       A pointer to the backing store for the queue
 *
 * @param[in] inBufferLength Length, in bytes, of the backing store
 */
void TLVCircularBuffer::Init(uint8_t * inBuffer, uint32_t inBufferLength)
{
    mQueue       = inBuffer;
    mQueueSize   = inBufferLength;
    mQueueLength = 0;
    mQueueHead   = mQueue;

    mProcessEvictedElement = nullptr;
    mAppData               = nullptr;

    // use common as opposed to unspecified, s.t. the reader that
    // skips over the elements does not complain about implicit
    // profile tags.
    mImplicitProfileId = kCommonProfileId;
}

/**
 * @brief
 *   Evicts the oldest top-level TLV element in the TLVCircularBuffer
 *
 * This function removes the oldest top level TLV element in the
 * buffer.  The function will call the callback registered at
 * #mProcessEvictedElement to process the element prior to removal.
 * If the callback returns anything but #CHIP_NO_ERROR, the element
 * is not removed.  Similarly, if any other error occurs -- no
 * elements within the buffer, etc -- the underlying
 * #TLVCircularBuffer remains unchanged.
 *
 *  @retval #CHIP_NO_ERROR On success.
 *
 *  @retval other          On any other error returned either by the callback
 *                         or by the TLVReader.
 *
 */
CHIP_ERROR TLVCircularBuffer::EvictHead()
{
    CircularTLVReader reader;
    uint8_t * newHead;
    uint32_t newLen;

    // find the boundaries of an event to throw away
    reader.Init(*this);
    reader.ImplicitProfileId = mImplicitProfileId;

    // position the reader on the first element
    ReturnErrorOnFailure(reader.Next());

    // skip to the next element
    ReturnErrorOnFailure(reader.Skip());

    // record the state of the queue post-call
    newLen  = mQueueLength - (reader.GetLengthRead());
    newHead = const_cast<uint8_t *>(reader.GetReadPoint());

    // if a custom handler is installed, give it a chance to
    // process the element before we evict it from the buffer.
    if (mProcessEvictedElement != nullptr)
    {
        // Reinitialize the reader
        reader.Init(*this);
        reader.ImplicitProfileId = mImplicitProfileId;

        ReturnErrorOnFailure(mProcessEvictedElement(*this, mAppData, reader));
    }

    // update queue state
    mQueueLength = newLen;
    mQueueHead   = newHead;

    return CHIP_NO_ERROR;
}

/**
 * @brief
 *  Implements TLVBackingStore::OnInit(TLVWriter) for circular buffers.
 */
CHIP_ERROR TLVCircularBuffer::OnInit(TLVWriter & writer, uint8_t *& bufStart, uint32_t & bufLen)
{
    return GetNewBuffer(writer, bufStart, bufLen);
}

/**
 * @brief
 *   Get additional space for the TLVWriter.  In actuality, the
 *   function evicts an element from the circular buffer, and adjusts
 *   the head of this buffer queue
 *
 * @param[in,out] ioWriter  TLVWriter calling this function
 *
 * @param[out] outBufStart The pointer to the new buffer
 *
 * @param[out] outBufLen   The available length for writing
 *
 * @retval #CHIP_NO_ERROR On success.
 *
 * @retval other           If the function was unable to elide a complete
 *                         top-level TLV element.
 */

CHIP_ERROR TLVCircularBuffer::GetNewBuffer(TLVWriter & ioWriter, uint8_t *& outBufStart, uint32_t & outBufLen)
{
    if (mQueueLength >= mQueueSize)
    {
        // Queue is out of space, need to evict an element
        ReturnErrorOnFailure(EvictHead());
    }

    GetCurrentWritableBuffer(outBufStart, outBufLen);
    return CHIP_NO_ERROR;
}

void TLVCircularBuffer::GetCurrentWritableBuffer(uint8_t *& outBufStart, uint32_t & outBufLen) const
{
    uint8_t * tail = QueueTail();

    // set the output values, returned buffer must be contiguous
    outBufStart = tail;

    if (tail >= mQueueHead)
    {
        outBufLen = mQueueSize - static_cast<uint32_t>(tail - mQueue);
    }
    else
    {
        outBufLen = static_cast<uint32_t>(mQueueHead - tail);
    }
}

/**
 * @brief
 *   FinalizeBuffer adjust the `TLVCircularBuffer` state on
 *   completion of output from the TLVWriter.  This function affects
 *   the position of the queue tail.
 *
 * @param[in,out] ioWriter TLVWriter calling this function
 *
 * @param[in] inBufStart pointer to the start of data (from `TLVWriter`
 *                       perspective)
 *
 * @param[in] inBufLen   length of data in the buffer pointed to by
 *                       `inbufStart`
 *
 * @retval #CHIP_NO_ERROR Unconditionally.
 */

CHIP_ERROR TLVCircularBuffer::FinalizeBuffer(TLVWriter & ioWriter, uint8_t * inBufStart, uint32_t inBufLen)
{
    CHIP_ERROR err = CHIP_NO_ERROR;
    uint8_t * tail = inBufStart + inBufLen;
    if (inBufLen)
    {
        if (tail <= mQueueHead)
        {
            mQueueLength = mQueueSize - static_cast<uint32_t>(mQueueHead - tail);
        }
        else
        {
            mQueueLength = static_cast<uint32_t>(tail - mQueueHead);
        }
    }
    return err;
}

/**
 * @brief
 *  Implements TLVBackingStore::OnInit(TVLReader) for circular buffers.
 */
CHIP_ERROR TLVCircularBuffer::OnInit(TLVReader & reader, const uint8_t *& bufStart, uint32_t & bufLen)
{
    return GetNextBuffer(reader, bufStart, bufLen);
}

/**
 * @brief
 *   Get additional space for the TLVReader.
 *
 *  The storage provided by the TLVCircularBuffer may be
 *  wraparound within the buffer.  This function provides us with an
 *  ability to match the buffering of the circular buffer to the
 *  TLVReader constraints.  The reader will read at most `mQueueSize`
 *  bytes from the buffer.
 *
 * @param[in] ioReader         TLVReader calling this function.
 *
 * @param[in,out] outBufStart  The reference to the data buffer.  On
 *                             return, it is set to a value within this
 *                             buffer.
 *
 * @param[out] outBufLen       On return, set to the number of continuous
 *                             bytes that could be read out of the buffer.
 *
 * @retval #CHIP_NO_ERROR      Succeeds unconditionally.
 */
CHIP_ERROR TLVCircularBuffer::GetNextBuffer(TLVReader & ioReader, const uint8_t *& outBufStart, uint32_t & outBufLen)
{
    CHIP_ERROR err              = CHIP_NO_ERROR;
    uint8_t * tail              = QueueTail();
    const uint8_t * readerStart = outBufStart;

    if (readerStart == nullptr)
    {
        outBufStart = mQueueHead;

        if (outBufStart == mQueue + mQueueSize)
        {
            outBufStart = mQueue;
        }
    }
    else if (readerStart >= (mQueue + mQueueSize))
    {
        outBufStart = mQueue;
    }
    else
    {
        outBufLen = 0;
        return err;
    }

    if ((mQueueLength != 0) && (tail <= outBufStart))
    {
        // the buffer is non-empty and data wraps around the end
        // point.  The returned buffer conceptually spans from
        // outBufStart until the end of the underlying storage buffer
        // (i.e. mQueue+mQueueSize).  This case tail == outBufStart
        // indicates that the buffer is completely full
        outBufLen = mQueueSize - static_cast<uint32_t>(outBufStart - mQueue);
        if ((tail == outBufStart) && (readerStart != nullptr))
            outBufLen = 0;
    }
    else
    {
        // the buffer length is the distance between head and tail;
        // tail is either strictly larger or the buffer is empty
        outBufLen = static_cast<uint32_t>(tail - outBufStart);
    }
    return err;
}

} // namespace TLV
} // namespace chip
