/*
 *
 *    Copyright (c) 2021 Project CHIP Authors
 *    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.
 */

#include "lib/core/TLV.h"
#include "lib/core/TLVTags.h"
#include "lib/core/TLVTypes.h"
#include "protocols/interaction_model/Constants.h"
#include "system/SystemPacketBuffer.h"
#include "system/TLVPacketBufferBackingStore.h"
#include <app/BufferedReadCallback.h>
#include <app/InteractionModelEngine.h>
#include <lib/support/ScopedBuffer.h>

namespace chip {
namespace app {

void BufferedReadCallback::OnReportBegin()
{
    mCallback.OnReportBegin();
}

void BufferedReadCallback::OnReportEnd()
{
    CHIP_ERROR err = DispatchBufferedData(mBufferedPath, StatusIB(), true);
    if (err != CHIP_NO_ERROR)
    {
        mCallback.OnError(err);
        return;
    }

    mCallback.OnReportEnd();
}

CHIP_ERROR BufferedReadCallback::GenerateListTLV(TLV::ScopedBufferTLVReader & aReader)
{
    TLV::TLVType outerType;
    Platform::ScopedMemoryBuffer<uint8_t> backingBuffer;

    //
    // To generate the final reconstituted list, we need to allocate a contiguous
    // buffer than can hold the entirety of its contents. To do so, we need to figure out
    // how big a buffer to allocate. This requires walking the buffered list items and computing their TLV sizes,
    // summing them all up and adding a bit of slop to account for the TLV array the list elements will go into.
    //
    // The alternative was to use a PacketBufferTLVWriter backed by chained packet buffers to
    // write out the list - this would have removed the need for this first pass. However,
    // we cannot actually back a TLVReader with a chained buffer since that violates the ability
    // for us to create readers off-of readers. Each reader would assume exclusive ownership of the chained
    // buffer and mutate the state within TLVPacketBufferBackingStore, preventing shared use.
    //
    // To avoid that, a single contiguous buffer is the best likely approach for now.
    //
    size_t totalBufSize = 0;
    for (const auto & packetBuffer : mBufferedList)
    {
        totalBufSize += packetBuffer->TotalLength();
    }

    //
    // Size of the start container and end container are just 1 byte each, but, let's just be safe.
    //
    totalBufSize += 4;

    backingBuffer.Calloc(totalBufSize);
    VerifyOrReturnError(backingBuffer.Get() != nullptr, CHIP_ERROR_NO_MEMORY);

    TLV::ScopedBufferTLVWriter writer(std::move(backingBuffer), totalBufSize);
    ReturnErrorOnFailure(writer.StartContainer(TLV::AnonymousTag(), TLV::kTLVType_Array, outerType));

    for (auto & bufHandle : mBufferedList)
    {
        System::PacketBufferTLVReader reader;

        reader.Init(std::move(bufHandle));

        ReturnErrorOnFailure(reader.Next());
        ReturnErrorOnFailure(writer.CopyElement(TLV::AnonymousTag(), reader));
    }

    ReturnErrorOnFailure(writer.EndContainer(outerType));

    writer.Finalize(backingBuffer);

    aReader.Init(std::move(backingBuffer), totalBufSize);

    return CHIP_NO_ERROR;
}

CHIP_ERROR BufferedReadCallback::BufferListItem(TLV::TLVReader & reader)
{
    System::PacketBufferTLVWriter writer;
    System::PacketBufferHandle handle;

    //
    // We conservatively allocate a packet buffer as big as an IPv6 MTU (since we're buffering
    // data received over the wire, which should always fit within that).
    //
    // We could have snapshotted the reader at its current position, advanced it past the current element
    // and computed the delta in its read point to figure out the size of the element before allocating
    // our target buffer. However, the reader's current position is already set past the control octet
    // and the tag. Consequently, the computed size is always going to omit the sizes of these two parts of the
    // TLV element. Since the tag can vary in size, for now, let's just do the safe thing. In the future, if this is a problem,
    // we can improve this.
    //
    handle = System::PacketBufferHandle::New(chip::app::kMaxSecureSduLengthBytes);
    VerifyOrReturnError(!handle.IsNull(), CHIP_ERROR_NO_MEMORY);

    writer.Init(std::move(handle), false);

    ReturnErrorOnFailure(writer.CopyElement(TLV::AnonymousTag(), reader));
    ReturnErrorOnFailure(writer.Finalize(&handle));

    // Compact the buffer down to a more reasonably sized packet buffer
    // if we can.
    //
    handle.RightSize();

    mBufferedList.push_back(std::move(handle));

    return CHIP_NO_ERROR;
}

CHIP_ERROR BufferedReadCallback::BufferData(const ConcreteDataAttributePath & aPath, TLV::TLVReader * apData)
{

    if (aPath.mListOp == ConcreteDataAttributePath::ListOperation::ReplaceAll)
    {
        TLV::TLVType outerContainer;

        VerifyOrReturnError(apData->GetType() == TLV::kTLVType_Array, CHIP_ERROR_INVALID_TLV_ELEMENT);
        mBufferedList.clear();

        ReturnErrorOnFailure(apData->EnterContainer(outerContainer));

        CHIP_ERROR err;

        while ((err = apData->Next()) == CHIP_NO_ERROR)
        {
            ReturnErrorOnFailure(BufferListItem(*apData));
        }

        if (err == CHIP_END_OF_TLV)
        {
            err = CHIP_NO_ERROR;
        }

        ReturnErrorOnFailure(err);
        ReturnErrorOnFailure(apData->ExitContainer(outerContainer));
    }
    else if (aPath.mListOp == ConcreteDataAttributePath::ListOperation::AppendItem)
    {
        ReturnErrorOnFailure(BufferListItem(*apData));
    }

    return CHIP_NO_ERROR;
}

CHIP_ERROR BufferedReadCallback::DispatchBufferedData(const ConcreteAttributePath & aPath, const StatusIB & aStatusIB,
                                                      bool aEndOfReport)
{
    if (aPath == mBufferedPath)
    {
        //
        // If we encountered the same list again and it's not the last DataIB, then
        // we need to continue to buffer up this list's data, so return immediately without dispatching
        // the existing buffered up contents.
        //
        if (!aEndOfReport)
        {
            return CHIP_NO_ERROR;
        }

        //
        // If we had previously buffered up data for this list and now we have encountered
        // an error for this list, that error takes precedence and the buffered data is now
        // rendered invalid. Return immediately without dispatching the existing buffered up contents.
        //
        if (aStatusIB.mStatus != Protocols::InteractionModel::Status::Success)
        {
            return CHIP_NO_ERROR;
        }
    }

    if (!mBufferedPath.IsListOperation())
    {
        return CHIP_NO_ERROR;
    }

    StatusIB statusIB;
    TLV::ScopedBufferTLVReader reader;

    ReturnErrorOnFailure(GenerateListTLV(reader));

    //
    // Update the list operation to now reflect the delivery of the entire list
    // i.e a replace all operation.
    //
    mBufferedPath.mListOp = ConcreteDataAttributePath::ListOperation::ReplaceAll;

    //
    // Advance the reader forward to the list itself
    //
    ReturnErrorOnFailure(reader.Next());

    mCallback.OnAttributeData(mBufferedPath, &reader, statusIB);

    //
    // Clear out our buffered contents to free up allocated buffers, and reset the buffered path.
    //
    mBufferedList.clear();
    mBufferedPath = ConcreteDataAttributePath();
    return CHIP_NO_ERROR;
}

void BufferedReadCallback::OnAttributeData(const ConcreteDataAttributePath & aPath, TLV::TLVReader * apData,
                                           const StatusIB & aStatus)
{
    CHIP_ERROR err;

    //
    // First, let's dispatch to our registered callback any buffered up list data from previous calls.
    //
    err = DispatchBufferedData(aPath, aStatus);
    SuccessOrExit(err);

    //
    // We buffer up list data (only if the status was successful)
    //
    if (aPath.IsListOperation() && aStatus.mStatus == Protocols::InteractionModel::Status::Success)
    {
        err = BufferData(aPath, apData);
        SuccessOrExit(err);
    }
    else
    {
        mCallback.OnAttributeData(aPath, apData, aStatus);
    }

    //
    // Update our latched buffered path.
    //
    mBufferedPath = aPath;

exit:
    if (err != CHIP_NO_ERROR)
    {
        mCallback.OnError(err);
    }
}

} // namespace app
} // namespace chip
