/*
 *
 *    Copyright (c) 2023 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/AttributeAccessInterface.h>
#include <app/CommandHandlerInterface.h>
#include <app/ConcreteAttributePath.h>
#include <app/ConcreteClusterPath.h>
#include <app/clusters/resource-monitoring-server/replacement-product-list-manager.h>
#include <app/clusters/resource-monitoring-server/resource-monitoring-cluster-objects.h>
#include <app/data-model/Nullable.h>
#include <app/util/basic-types.h>
#include <lib/core/CHIPError.h>
#include <lib/core/DataModelTypes.h>
#include <protocols/interaction_model/StatusCode.h>
#include <stdint.h>

namespace chip {
namespace app {
namespace Clusters {
namespace ResourceMonitoring {

// forward declarations
class Delegate;

class Instance : public CommandHandlerInterface, public AttributeAccessInterface
{

public:
    /**
     * Creates a resource monitoring cluster instance. The Init() method needs to be called for this instance to be registered and
     * called by the interaction model at the appropriate times.
     *
     * @param aDelegate A pointer to the delegate to be used by this server.
     * Note: the caller must ensure that the delegate lives throughout the instance's lifetime.
     * @param aEndpointId                       The endpoint on which this cluster exists. This must match the zap configuration.
     * @param aClusterId                        The ID of the ResourceMonitoring aliased cluster to be instantiated.
     * @param aFeatureMap                       The feature map of the cluster.
     * @param aDegradationDirection             The degradation direction of the cluster.
     * @param aResetConditionCommandSupported   Whether the ResetCondition command is supported by the cluster.
     */
    Instance(Delegate * aDelegate, EndpointId aEndpointId, ClusterId aClusterId, uint32_t aFeatureMap,
             ResourceMonitoring::Attributes::DegradationDirection::TypeInfo::Type aDegradationDirection,
             bool aResetConditionCommandSupported);

    ~Instance() override;

    // Not copyable or movable
    Instance(const Instance &)             = delete;
    Instance & operator=(const Instance &) = delete;
    Instance(Instance &&)                  = delete;
    Instance & operator=(Instance &&)      = delete;

    /**
     * Initialise the Resource Monitoring cluster.
     *
     * @die                                     If the cluster ID given is not a valid Resource Monitoring cluster ID.
     * @die                                     If the endpoint and cluster ID have not been enabled in zap.
     * @return CHIP_ERROR_INVALID_ARGUMENT      If the CommandHandler or Attribute Handler could not be registered.
     * @return CHIP_ERROR_INCORRECT_STATE       If the CommandHandler was already registered
     * @return CHIP_ERROR_INCORRECT_STATE       If the AttributeAccessInterfaceRegistry::Register fails.
     * @return CHIP_ERROR                       If the AppInit() method returned an error. This is application specific.
     *
     * @return CHIP_NO_ERROR                    If the cluster was initialised successfully.
     */
    CHIP_ERROR Init();

    /**
     * Checks if the given feature is supported by the cluster.
     * @param feature   The aFeature to check.
     *
     * @return true     If the feature is supported.
     * @return false    If the feature is not supported.
     */
    bool HasFeature(ResourceMonitoring::Feature aFeature) const;

    // Attribute setters
    Protocols::InteractionModel::Status UpdateCondition(uint8_t aNewCondition);
    Protocols::InteractionModel::Status UpdateChangeIndication(ChangeIndicationEnum aNewChangeIndication);
    Protocols::InteractionModel::Status UpdateInPlaceIndicator(bool aNewInPlaceIndicator);
    Protocols::InteractionModel::Status UpdateLastChangedTime(DataModel::Nullable<uint32_t> aNewLastChangedTime);

    void SetReplacementProductListManagerInstance(ReplacementProductListManager * instance);

    // Attribute getters
    uint8_t GetCondition() const;
    ChangeIndicationEnum GetChangeIndication() const;
    DegradationDirectionEnum GetDegradationDirection() const;
    bool GetInPlaceIndicator() const;
    DataModel::Nullable<uint32_t> GetLastChangedTime() const;

    EndpointId GetEndpointId() const { return mEndpointId; }
    ClusterId GetClusterId() const { return mClusterId; }

private:
    Delegate * mDelegate;

    EndpointId mEndpointId{};
    ClusterId mClusterId{};

    // attribute Data Store
    chip::Percent mCondition                       = 100;
    DegradationDirectionEnum mDegradationDirection = DegradationDirectionEnum::kDown;
    ChangeIndicationEnum mChangeIndication         = ChangeIndicationEnum::kOk;
    bool mInPlaceIndicator                         = true;
    DataModel::Nullable<uint32_t> mLastChangedTime;
    ReplacementProductListManager * mReplacementProductListManager = nullptr;

    uint32_t mFeatureMap;

    bool mResetConditionCommandSupported = false;

    ReplacementProductListManager * GetReplacementProductListManagerInstance();

    CHIP_ERROR ReadReplaceableProductList(AttributeValueEncoder & aEncoder);

    // CommandHandlerInterface
    void InvokeCommand(HandlerContext & ctx) override;
    CHIP_ERROR RetrieveAcceptedCommands(const ConcreteClusterPath & cluster,
                                        ReadOnlyBufferBuilder<DataModel::AcceptedCommandEntry> & builder) override;

    // AttributeAccessInterface
    CHIP_ERROR Read(const ConcreteReadAttributePath & aPath, AttributeValueEncoder & aEncoder) override;
    CHIP_ERROR Write(const ConcreteDataAttributePath & aPath, AttributeValueDecoder & aDecoder) override;

    template <typename RequestT, typename FuncT>
    void HandleCommand(HandlerContext & handlerContext, FuncT func);

    void LoadPersistentAttributes();

    /**
     * Internal method to handle the ResetCondition command.
     */
    void HandleResetCondition(HandlerContext & ctx,
                              const ResourceMonitoring::Commands::ResetCondition::DecodableType & commandData);
}; // class Instance

class Delegate
{

private:
    Instance * mInstance = nullptr;

protected:
    /**
     * @brief Provides access to the const Instance pointer.
     * This method is placed in the protected section because it must be called by classes derived from the delegate.
     *
     * @return A const pointer to the Instance object associated with this delegate.
     */
    const Instance * GetInstance() const { return mInstance; }

    /**
     * @brief Provides access to the Instance pointer.
     * This method is placed in the protected section because it must be called by classes derived from the delegate.
     *
     * @return A pointer to the Instance object associated with this delegate.
     */
    Instance * GetInstance() { return mInstance; }

public:
    Delegate()          = default;
    virtual ~Delegate() = default;

    // The following methods should be overridden by the SDK user to implement the business logic of their application

    /**
     * This init method will be called during Resource Monitoring Server initialization after the instance information has been
     * validated and the instance has been registered. This method should be overridden by the SDK user to initialize the
     * application logic.
     *
     * @return CHIP_NO_ERROR    If the application was initialized successfully. All other values will cause the initialization to
     * fail.
     */
    virtual CHIP_ERROR Init() = 0;

    /**
     * This method may be overwritten by the SDK User, if the default behaviour is not desired.
     * Preferably, the SDK User should implement the PreResetCondition() and PostResetCondition() methods instead.
     *
     * The cluster implementation will handle all of the resets needed by the spec.
     * - Update the Condition attribute according to the DegradationDirection (if supported)
     * - Update the ChangeIndicator attribute to kOk
     * - Update the LastChangedTime attribute (if supported)
     *
     * The return value will depend on the PreResetCondition() and PostResetCondition() method, if one of them does not return
     * Success, this method will return the failure as well.
     * @return Status::Success      If the command was handled successfully.
     * @return All Other            PreResetCondition() or PostResetCondition() failed, these are application specific.
     */
    virtual Protocols::InteractionModel::Status OnResetCondition();

    /**
     * This method may be overwritten by the SDK User, if the SDK User wants to do something before the reset.
     * If there are some internal states of the devices or some specific methods that must be called, that are needed for the reset
     * and that can fail, they should be done here and not in PostResetCondition().
     *
     * @return Status::Success      All good, the reset may proceed.
     * @return All Other            The reset should not proceed. The reset command will fail.
     */
    virtual Protocols::InteractionModel::Status PreResetCondition();

    /**
     * This method may be overwritten by the SDK User, if the SDK User wants to do something after the reset.
     * If this fails, the attributes will already be updated, so the SDK User should not do something here
     * that can fail and that will affect the state of the device. Do the checks in the PreResetCondition() method instead.
     *
     * @return Status::Success      All good
     * @return All Other            Something went wrong. The attributes will already be updated. But the reset command will report
     *                              the failure.
     */
    virtual Protocols::InteractionModel::Status PostResetCondition();

    /**
     * This method is used by the SDK to set the instance pointer. This is done during the instantiation of a Instance object.
     * @param aInstance A pointer to the Instance object related to this delegate object.
     * @note This method is for internal SDK use and should only be called by the `Instance` constructor and destructor.
     */
    void SetInstance(Instance * aInstance)
    {
        VerifyOrDie(mInstance == nullptr || aInstance == nullptr || mInstance == aInstance);
        mInstance = aInstance;
    }
};

} // namespace ResourceMonitoring
} // namespace Clusters
} // namespace app
} // namespace chip
