/*
 *
 *    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.
 */

#pragma once

#include <app/ChunkedWriteCallback.h>
#include <app/InteractionModelEngine.h>
#include <app/WriteClient.h>
#include <controller/CommandSenderAllocator.h>
#include <controller/TypedCommandCallback.h>
#include <functional>
#include <lib/core/Optional.h>

namespace chip {
namespace Controller {

namespace Internal {
// WriteCancelFn functions on WriteAttribute() are for internal use only.
typedef std::function<void()> WriteCancelFn;
} // namespace Internal

/*
 * An adapter callback that permits applications to provide std::function callbacks for success, error and on done.
 * This permits a slightly more flexible programming model that allows applications to pass in lambdas and bound member functions
 * as they see fit instead.
 *
 */

class WriteCallback final : public app::WriteClient::Callback
{
public:
    using OnSuccessCallbackType = std::function<void(const app::ConcreteAttributePath &)>;

    //
    // Callback to deliver any error that occurs during the write. This includes
    // errors global to the write as a whole (e.g timeout) as well as per-attribute
    // errors.
    //
    // In the latter case, path will be non-null. Otherwise, it shall be null.
    //
    using OnErrorCallbackType = std::function<void(const app::ConcreteAttributePath * path, CHIP_ERROR err)>;
    using OnDoneCallbackType  = std::function<void(app::WriteClient *)>;

    WriteCallback(OnSuccessCallbackType aOnSuccess, OnErrorCallbackType aOnError, OnDoneCallbackType aOnDone, bool aIsGroupWrite) :
        mOnSuccess(aOnSuccess), mOnError(aOnError), mOnDone(aOnDone), mIsGroupWrite(aIsGroupWrite), mCallback(this)
    {}

    app::WriteClient::Callback * GetChunkedCallback() { return &mCallback; }

    void OnResponse(const app::WriteClient * apWriteClient, const app::ConcreteDataAttributePath & aPath,
                    app::StatusIB status) override
    {
        if (mCalledCallback)
        {
            return;
        }
        mCalledCallback = true;

        if (status.IsSuccess())
        {
            mOnSuccess(aPath);
        }
        else
        {
            mOnError(&aPath, status.ToChipError());
        }
    }

    void OnError(const app::WriteClient * apWriteClient, CHIP_ERROR aError) override
    {
        if (mCalledCallback)
        {
            return;
        }
        mCalledCallback = true;

        mOnError(nullptr, aError);
    }

    void OnDone(app::WriteClient * apWriteClient) override
    {
        if (!mIsGroupWrite && !mCalledCallback)
        {
            // This can happen if the server sends a response with an empty
            // WriteResponses list.  Since we are not sending wildcard write
            // paths, that's not a valid response and we should treat it as an
            // error.  Use the error we would have gotten if we in fact expected
            // a nonempty list.
            OnError(apWriteClient, CHIP_END_OF_TLV);
        }

        if (mOnDone != nullptr)
        {
            mOnDone(apWriteClient);
        }

        chip::Platform::Delete(apWriteClient);
        // Always needs to be the last call
        chip::Platform::Delete(this);
    }

private:
    OnSuccessCallbackType mOnSuccess = nullptr;
    OnErrorCallbackType mOnError     = nullptr;
    OnDoneCallbackType mOnDone       = nullptr;

    bool mCalledCallback = false;
    bool mIsGroupWrite   = false;

    app::ChunkedWriteCallback mCallback;
};

/**
 * Functions for writing attributes.  We have lots of different AttributeInfo
 * but a fairly small set of types that get written.  So we want to keep the
 * template on AttributeInfo very small, and put all the work in the template
 * with a small number of instantiations (one per type).
 */
template <typename AttrType>
CHIP_ERROR WriteAttribute(const SessionHandle & sessionHandle, chip::EndpointId endpointId, ClusterId clusterId,
                          AttributeId attributeId, const AttrType & requestData, WriteCallback::OnSuccessCallbackType onSuccessCb,
                          WriteCallback::OnErrorCallbackType onErrorCb, const Optional<uint16_t> & aTimedWriteTimeoutMs,
                          WriteCallback::OnDoneCallbackType onDoneCb = nullptr,
                          const Optional<DataVersion> & aDataVersion = NullOptional,
                          Internal::WriteCancelFn * outCancelFn      = nullptr)
{
    auto callback = Platform::MakeUnique<WriteCallback>(onSuccessCb, onErrorCb, onDoneCb, sessionHandle->IsGroupSession());
    VerifyOrReturnError(callback != nullptr, CHIP_ERROR_NO_MEMORY);

    auto client = Platform::MakeUnique<app::WriteClient>(app::InteractionModelEngine::GetInstance()->GetExchangeManager(),
                                                         callback->GetChunkedCallback(), aTimedWriteTimeoutMs);
    VerifyOrReturnError(client != nullptr, CHIP_ERROR_NO_MEMORY);

    if (sessionHandle->IsGroupSession())
    {
        ReturnErrorOnFailure(client->EncodeAttribute(chip::app::AttributePathParams(clusterId, attributeId), requestData));
    }
    else
    {
        ReturnErrorOnFailure(
            client->EncodeAttribute(chip::app::AttributePathParams(endpointId, clusterId, attributeId), requestData, aDataVersion));
    }

    ReturnErrorOnFailure(client->SendWriteRequest(sessionHandle));

    // If requested by the caller, provide a way to cancel the write interaction.
    if (outCancelFn != nullptr)
    {
        *outCancelFn = [rawCallback = callback.get(), rawClient = client.get()]() {
            chip::Platform::Delete(rawClient);
            chip::Platform::Delete(rawCallback);
        };
    }

    // At this point the handle will ensure our callback's OnDone is always
    // called.
    client.release();
    callback.release();

    return CHIP_NO_ERROR;
}

template <typename AttributeInfo>
CHIP_ERROR WriteAttribute(const SessionHandle & sessionHandle, chip::EndpointId endpointId,
                          const typename AttributeInfo::Type & requestData, WriteCallback::OnSuccessCallbackType onSuccessCb,
                          WriteCallback::OnErrorCallbackType onErrorCb, const Optional<uint16_t> & aTimedWriteTimeoutMs,
                          WriteCallback::OnDoneCallbackType onDoneCb = nullptr,
                          const Optional<DataVersion> & aDataVersion = NullOptional)
{
    return WriteAttribute(sessionHandle, endpointId, AttributeInfo::GetClusterId(), AttributeInfo::GetAttributeId(), requestData,
                          onSuccessCb, onErrorCb, aTimedWriteTimeoutMs, onDoneCb, aDataVersion);
}

template <typename AttributeInfo>
CHIP_ERROR WriteAttribute(const SessionHandle & sessionHandle, chip::EndpointId endpointId,
                          const typename AttributeInfo::Type & requestData, WriteCallback::OnSuccessCallbackType onSuccessCb,
                          WriteCallback::OnErrorCallbackType onErrorCb, uint16_t aTimedWriteTimeoutMs,
                          WriteCallback::OnDoneCallbackType onDoneCb = nullptr,
                          const Optional<DataVersion> & aDataVersion = NullOptional)
{
    return WriteAttribute<AttributeInfo>(sessionHandle, endpointId, requestData, onSuccessCb, onErrorCb, onDoneCb,
                                         MakeOptional(aTimedWriteTimeoutMs), onDoneCb, aDataVersion);
}

template <typename AttributeInfo, typename std::enable_if_t<!AttributeInfo::MustUseTimedWrite(), int> = 0>
CHIP_ERROR WriteAttribute(const SessionHandle & sessionHandle, chip::EndpointId endpointId,
                          const typename AttributeInfo::Type & requestData, WriteCallback::OnSuccessCallbackType onSuccessCb,
                          WriteCallback::OnErrorCallbackType onErrorCb, WriteCallback::OnDoneCallbackType onDoneCb = nullptr,
                          const Optional<DataVersion> & aDataVersion = NullOptional)
{
    return WriteAttribute<AttributeInfo>(sessionHandle, endpointId, requestData, onSuccessCb, onErrorCb, NullOptional, onDoneCb,
                                         aDataVersion);
}

} // namespace Controller
} // namespace chip
