/*
 *
 *    Copyright (c) 2020 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.
 */

/**
 *  @file
 *    This file contains definitions for a base Cluster class. This class will
 *    be derived by various ZCL clusters supported by CHIP. The objects of the
 *    ZCL cluster class will be used by Controller applications to interact with
 *    the CHIP device.
 */

#pragma once

#include "app/ConcreteCommandPath.h"
#include <app/AppConfig.h>
#include <app/DeviceProxy.h>
#include <controller/InvokeInteraction.h>
#include <controller/ReadInteraction.h>
#include <controller/WriteInteraction.h>
#include <lib/core/Optional.h>
#include <messaging/ExchangeMgr.h>
#include <system/SystemClock.h>

namespace chip {
namespace Controller {

template <typename T>
using CommandResponseSuccessCallback = void(void * context, const T & responseObject);
using CommandResponseFailureCallback = void(void * context, CHIP_ERROR err);
using CommandResponseDoneCallback    = void();
using WriteResponseSuccessCallback   = void (*)(void * context);
using WriteResponseFailureCallback   = void (*)(void * context, CHIP_ERROR err);
using WriteResponseDoneCallback      = void (*)(void * context);
template <typename T>
using ReadResponseSuccessCallback     = void (*)(void * context, T responseData);
using ReadResponseFailureCallback     = void (*)(void * context, CHIP_ERROR err);
using ReadDoneCallback                = void (*)(void * context);
using SubscriptionEstablishedCallback = void (*)(void * context, SubscriptionId subscriptionId);
using ResubscriptionAttemptCallback   = void (*)(void * context, CHIP_ERROR aError, uint32_t aNextResubscribeIntervalMsec);
using SubscriptionOnDoneCallback      = std::function<void(void)>;

class DLL_EXPORT ClusterBase
{
public:
    ClusterBase(Messaging::ExchangeManager & exchangeManager, const SessionHandle & session, EndpointId endpoint) :
        mExchangeManager(exchangeManager), mSession(session), mEndpoint(endpoint)
    {}

    virtual ~ClusterBase() {}

    // Temporary function to set command timeout before we move over to InvokeCommand
    // TODO: remove when we start using InvokeCommand everywhere
    void SetCommandTimeout(Optional<System::Clock::Timeout> timeout) { mTimeout = timeout; }

    /**
     * Returns the current command timeout set via SetCommandTimeout, or an
     * empty optional if no timeout has been set.
     */
    Optional<System::Clock::Timeout> GetCommandTimeout() { return mTimeout; }

    /*
     * This function permits sending an invoke request using cluster objects that represent the request and response data payloads.
     *
     * Success and Failure callbacks must be passed in through which the decoded response is provided as well as notification of any
     * failure.
     */
    template <typename RequestDataT>
    CHIP_ERROR InvokeCommand(const RequestDataT & requestData, void * context,
                             CommandResponseSuccessCallback<typename RequestDataT::ResponseType> successCb,
                             CommandResponseFailureCallback failureCb, const Optional<uint16_t> & timedInvokeTimeoutMs)
    {
        auto onSuccessCb = [context, successCb](const app::ConcreteCommandPath & aPath, const app::StatusIB & aStatus,
                                                const typename RequestDataT::ResponseType & responseData) {
            successCb(context, responseData);
        };

        auto onFailureCb = [context, failureCb](CHIP_ERROR aError) { failureCb(context, aError); };

        return InvokeCommandRequest(&mExchangeManager, mSession.Get().Value(), mEndpoint, requestData, onSuccessCb, onFailureCb,
                                    timedInvokeTimeoutMs, mTimeout);
    }

    template <typename RequestDataT>
    CHIP_ERROR InvokeCommand(const RequestDataT & requestData, void * context,
                             CommandResponseSuccessCallback<typename RequestDataT::ResponseType> successCb,
                             CommandResponseFailureCallback failureCb, uint16_t timedInvokeTimeoutMs)
    {
        return InvokeCommand(requestData, context, successCb, failureCb, MakeOptional(timedInvokeTimeoutMs));
    }

    template <typename RequestDataT, typename std::enable_if_t<!RequestDataT::MustUseTimedInvoke(), int> = 0>
    CHIP_ERROR InvokeCommand(const RequestDataT & requestData, void * context,
                             CommandResponseSuccessCallback<typename RequestDataT::ResponseType> successCb,
                             CommandResponseFailureCallback failureCb)
    {
        return InvokeCommand(requestData, context, successCb, failureCb, NullOptional);
    }

    /**
     * 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 AttrType & requestData, void * context, ClusterId clusterId, AttributeId attributeId,
                              WriteResponseSuccessCallback successCb, WriteResponseFailureCallback failureCb,
                              const Optional<uint16_t> & aTimedWriteTimeoutMs, WriteResponseDoneCallback doneCb = nullptr,
                              const Optional<DataVersion> & aDataVersion = NullOptional)
    {
        auto onSuccessCb = [context, successCb](const app::ConcreteAttributePath & aPath) {
            if (successCb != nullptr)
            {
                successCb(context);
            }
        };

        auto onFailureCb = [context, failureCb](const app::ConcreteAttributePath * aPath, CHIP_ERROR aError) {
            if (failureCb != nullptr)
            {
                failureCb(context, aError);
            }
        };

        auto onDoneCb = [context, doneCb](app::WriteClient * pWriteClient) {
            if (doneCb != nullptr)
            {
                doneCb(context);
            }
        };

        return chip::Controller::WriteAttribute<AttrType>(mSession.Get().Value(), mEndpoint, clusterId, attributeId, requestData,
                                                          onSuccessCb, onFailureCb, aTimedWriteTimeoutMs, onDoneCb, aDataVersion);
    }

    template <typename AttrType>
    CHIP_ERROR WriteAttribute(GroupId groupId, FabricIndex fabricIndex, const AttrType & requestData, void * context,
                              ClusterId clusterId, AttributeId attributeId, WriteResponseSuccessCallback successCb,
                              WriteResponseFailureCallback failureCb, const Optional<uint16_t> & aTimedWriteTimeoutMs,
                              WriteResponseDoneCallback doneCb = nullptr, const Optional<DataVersion> & aDataVersion = NullOptional)
    {

        auto onSuccessCb = [context, successCb](const app::ConcreteAttributePath & aPath) {
            if (successCb != nullptr)
            {
                successCb(context);
            }
        };

        auto onFailureCb = [context, failureCb](const app::ConcreteAttributePath * aPath, CHIP_ERROR aError) {
            if (failureCb != nullptr)
            {
                failureCb(context, aError);
            }
        };

        auto onDoneCb = [context, doneCb](app::WriteClient * pWriteClient) {
            if (doneCb != nullptr)
            {
                doneCb(context);
            }
        };

        Transport::OutgoingGroupSession groupSession(groupId, fabricIndex);
        return chip::Controller::WriteAttribute<AttrType>(SessionHandle(groupSession), 0 /*Unused for Group*/, clusterId,
                                                          attributeId, requestData, onSuccessCb, onFailureCb, aTimedWriteTimeoutMs,
                                                          onDoneCb, aDataVersion);
    }

    template <typename AttributeInfo>
    CHIP_ERROR WriteAttribute(GroupId groupId, FabricIndex fabricIndex, const typename AttributeInfo::Type & requestData,
                              void * context, WriteResponseSuccessCallback successCb, WriteResponseFailureCallback failureCb,
                              WriteResponseDoneCallback doneCb = nullptr, const Optional<DataVersion> & aDataVersion = NullOptional,
                              const Optional<uint16_t> & aTimedWriteTimeoutMs = NullOptional)
    {
        return WriteAttribute(groupId, fabricIndex, requestData, context, AttributeInfo::GetClusterId(),
                              AttributeInfo::GetAttributeId(), successCb, failureCb, aTimedWriteTimeoutMs, doneCb, aDataVersion);
    }

    template <typename AttributeInfo>
    CHIP_ERROR WriteAttribute(const typename AttributeInfo::Type & requestData, void * context,
                              WriteResponseSuccessCallback successCb, WriteResponseFailureCallback failureCb,
                              const Optional<uint16_t> & aTimedWriteTimeoutMs, WriteResponseDoneCallback doneCb = nullptr,
                              const Optional<DataVersion> & aDataVersion = NullOptional)
    {
        return WriteAttribute(requestData, context, AttributeInfo::GetClusterId(), AttributeInfo::GetAttributeId(), successCb,
                              failureCb, aTimedWriteTimeoutMs, doneCb, aDataVersion);
    }

    template <typename AttributeInfo>
    CHIP_ERROR WriteAttribute(const typename AttributeInfo::Type & requestData, void * context,
                              WriteResponseSuccessCallback successCb, WriteResponseFailureCallback failureCb,
                              uint16_t aTimedWriteTimeoutMs, WriteResponseDoneCallback doneCb = nullptr,
                              const Optional<DataVersion> & aDataVersion = NullOptional)
    {
        return WriteAttribute<AttributeInfo>(requestData, context, successCb, failureCb, MakeOptional(aTimedWriteTimeoutMs), doneCb,
                                             aDataVersion);
    }

    template <typename AttributeInfo, typename std::enable_if_t<!AttributeInfo::MustUseTimedWrite(), int> = 0>
    CHIP_ERROR WriteAttribute(const typename AttributeInfo::Type & requestData, void * context,
                              WriteResponseSuccessCallback successCb, WriteResponseFailureCallback failureCb,
                              WriteResponseDoneCallback doneCb = nullptr, const Optional<DataVersion> & aDataVersion = NullOptional)
    {
        return WriteAttribute<AttributeInfo>(requestData, context, successCb, failureCb, NullOptional, doneCb, aDataVersion);
    }

#if CHIP_CONFIG_ENABLE_READ_CLIENT
    /**
     * Read an attribute and get a type-safe callback with the attribute value.
     */
    template <typename AttributeInfo>
    CHIP_ERROR ReadAttribute(void * context, ReadResponseSuccessCallback<typename AttributeInfo::DecodableArgType> successCb,
                             ReadResponseFailureCallback failureCb, bool aIsFabricFiltered = true)
    {
        return ReadAttribute<typename AttributeInfo::DecodableType, typename AttributeInfo::DecodableArgType>(
            context, AttributeInfo::GetClusterId(), AttributeInfo::GetAttributeId(), successCb, failureCb, aIsFabricFiltered);
    }

    template <typename DecodableType, typename DecodableArgType>
    CHIP_ERROR ReadAttribute(void * context, ClusterId clusterId, AttributeId attributeId,
                             ReadResponseSuccessCallback<DecodableArgType> successCb, ReadResponseFailureCallback failureCb,
                             bool aIsFabricFiltered = true)
    {
        auto onSuccessCb = [context, successCb](const app::ConcreteAttributePath & aPath, const DecodableType & aData) {
            if (successCb != nullptr)
            {
                successCb(context, aData);
            }
        };

        auto onFailureCb = [context, failureCb](const app::ConcreteAttributePath * aPath, CHIP_ERROR aError) {
            if (failureCb != nullptr)
            {
                failureCb(context, aError);
            }
        };

        return Controller::ReadAttribute<DecodableType>(&mExchangeManager, mSession.Get().Value(), mEndpoint, clusterId,
                                                        attributeId, onSuccessCb, onFailureCb, aIsFabricFiltered);
    }

    /**
     * Subscribe to attribute and get a type-safe callback with the attribute
     * value when it changes.
     */
    template <typename AttributeInfo>
    CHIP_ERROR
    SubscribeAttribute(void * context, ReadResponseSuccessCallback<typename AttributeInfo::DecodableArgType> reportCb,
                       ReadResponseFailureCallback failureCb, uint16_t minIntervalFloorSeconds, uint16_t maxIntervalCeilingSeconds,
                       SubscriptionEstablishedCallback subscriptionEstablishedCb = nullptr,
                       ResubscriptionAttemptCallback resubscriptionAttemptCb = nullptr, bool aIsFabricFiltered = true,
                       bool aKeepPreviousSubscriptions = false, const Optional<DataVersion> & aDataVersion = NullOptional,
                       SubscriptionOnDoneCallback subscriptionDoneCb = nullptr)
    {
        return SubscribeAttribute<typename AttributeInfo::DecodableType, typename AttributeInfo::DecodableArgType>(
            context, AttributeInfo::GetClusterId(), AttributeInfo::GetAttributeId(), reportCb, failureCb, minIntervalFloorSeconds,
            maxIntervalCeilingSeconds, subscriptionEstablishedCb, resubscriptionAttemptCb, aIsFabricFiltered,
            aKeepPreviousSubscriptions, aDataVersion, subscriptionDoneCb);
    }

    template <typename DecodableType, typename DecodableArgType>
    CHIP_ERROR SubscribeAttribute(void * context, ClusterId clusterId, AttributeId attributeId,
                                  ReadResponseSuccessCallback<DecodableArgType> reportCb, ReadResponseFailureCallback failureCb,
                                  uint16_t minIntervalFloorSeconds, uint16_t maxIntervalCeilingSeconds,
                                  SubscriptionEstablishedCallback subscriptionEstablishedCb = nullptr,
                                  ResubscriptionAttemptCallback resubscriptionAttemptCb = nullptr, bool aIsFabricFiltered = true,
                                  bool aKeepPreviousSubscriptions               = false,
                                  const Optional<DataVersion> & aDataVersion    = NullOptional,
                                  SubscriptionOnDoneCallback subscriptionDoneCb = nullptr)
    {
        auto onReportCb = [context, reportCb](const app::ConcreteAttributePath & aPath, const DecodableType & aData) {
            if (reportCb != nullptr)
            {
                reportCb(context, aData);
            }
        };

        auto onFailureCb = [context, failureCb](const app::ConcreteAttributePath * aPath, CHIP_ERROR aError) {
            if (failureCb != nullptr)
            {
                failureCb(context, aError);
            }
        };

        auto onSubscriptionEstablishedCb = [context, subscriptionEstablishedCb](const app::ReadClient & readClient,
                                                                                SubscriptionId subscriptionId) {
            if (subscriptionEstablishedCb != nullptr)
            {
                subscriptionEstablishedCb(context, subscriptionId);
            }
        };

        auto onResubscriptionAttemptCb = [context, resubscriptionAttemptCb](const app::ReadClient & readClient, CHIP_ERROR aError,
                                                                            uint32_t aNextResubscribeIntervalMsec) {
            if (resubscriptionAttemptCb != nullptr)
            {
                resubscriptionAttemptCb(context, aError, aNextResubscribeIntervalMsec);
            }
        };

        return Controller::SubscribeAttribute<DecodableType>(
            &mExchangeManager, mSession.Get().Value(), mEndpoint, clusterId, attributeId, onReportCb, onFailureCb,
            minIntervalFloorSeconds, maxIntervalCeilingSeconds, onSubscriptionEstablishedCb, onResubscriptionAttemptCb,
            aIsFabricFiltered, aKeepPreviousSubscriptions, aDataVersion, subscriptionDoneCb);
    }

    /**
     * Read an event and get a type-safe callback with the event data.
     *
     * @param[in] successCb Used to deliver event data received through the Read interactions
     * @param[in] failureCb failureCb will be called when an error occurs *after* a successful call to ReadEvent.
     * @param[in] doneCb    OnDone will be called when ReadClient has finished all work for event retrieval, it is possible that
     * there is no event.
     */
    template <typename DecodableType>
    CHIP_ERROR ReadEvent(void * context, ReadResponseSuccessCallback<DecodableType> successCb,
                         ReadResponseFailureCallback failureCb, ReadDoneCallback doneCb)
    {
        auto onSuccessCb = [context, successCb](const app::EventHeader & aEventHeader, const DecodableType & aData) {
            if (successCb != nullptr)
            {
                successCb(context, aData);
            }
        };

        auto onFailureCb = [context, failureCb](const app::EventHeader * aEventHeader, CHIP_ERROR aError) {
            if (failureCb != nullptr)
            {
                failureCb(context, aError);
            }
        };

        auto onDoneCb = [context, doneCb](app::ReadClient * apReadClient) {
            if (doneCb != nullptr)
            {
                doneCb(context);
            }
        };
        return Controller::ReadEvent<DecodableType>(&mExchangeManager, mSession.Get().Value(), mEndpoint, onSuccessCb, onFailureCb,
                                                    onDoneCb);
    }

    template <typename DecodableType>
    CHIP_ERROR SubscribeEvent(void * context, ReadResponseSuccessCallback<DecodableType> reportCb,
                              ReadResponseFailureCallback failureCb, uint16_t minIntervalFloorSeconds,
                              uint16_t maxIntervalCeilingSeconds,
                              SubscriptionEstablishedCallback subscriptionEstablishedCb = nullptr,
                              ResubscriptionAttemptCallback resubscriptionAttemptCb     = nullptr,
                              bool aKeepPreviousSubscriptions = false, bool aIsUrgentEvent = false)
    {
        auto onReportCb = [context, reportCb](const app::EventHeader & aEventHeader, const DecodableType & aData) {
            if (reportCb != nullptr)
            {
                reportCb(context, aData);
            }
        };

        auto onFailureCb = [context, failureCb](const app::EventHeader * aEventHeader, CHIP_ERROR aError) {
            if (failureCb != nullptr)
            {
                failureCb(context, aError);
            }
        };

        auto onSubscriptionEstablishedCb = [context, subscriptionEstablishedCb](const app::ReadClient & readClient,
                                                                                SubscriptionId subscriptionId) {
            if (subscriptionEstablishedCb != nullptr)
            {
                subscriptionEstablishedCb(context, subscriptionId);
            }
        };

        auto onResubscriptionAttemptCb = [context, resubscriptionAttemptCb](const app::ReadClient & readClient, CHIP_ERROR aError,
                                                                            uint32_t aNextResubscribeIntervalMsec) {
            if (resubscriptionAttemptCb != nullptr)
            {
                resubscriptionAttemptCb(context, aError, aNextResubscribeIntervalMsec);
            }
        };

        return Controller::SubscribeEvent<DecodableType>(&mExchangeManager, mSession.Get().Value(), mEndpoint, onReportCb,
                                                         onFailureCb, minIntervalFloorSeconds, maxIntervalCeilingSeconds,
                                                         onSubscriptionEstablishedCb, onResubscriptionAttemptCb,
                                                         aKeepPreviousSubscriptions, aIsUrgentEvent);
    }
#endif // CHIP_CONFIG_ENABLE_READ_CLIENT

protected:
    Messaging::ExchangeManager & mExchangeManager;

    // Since cluster object is ephemeral, the session shall be valid during the entire lifespan, so we do not need to check the
    // session existence when using it. For java and objective-c binding, the cluster object is allocated in the heap, such that we
    // can't use SessionHandle here, in such case, the cluster object must be freed when the session is released.
    SessionHolder mSession;

    EndpointId mEndpoint;
    Optional<System::Clock::Timeout> mTimeout;
};

} // namespace Controller
} // namespace chip
