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

// TODO(#32628): Remove the CHIPCore.h header when the esp32 build is correctly fixed
#include <lib/core/CHIPCore.h>

#include <app/ReadHandler.h>
#include <app/icd/server/ICDStateObserver.h>
#include <lib/core/CHIPError.h>
#include <system/SystemClock.h>

namespace chip {
namespace app {
namespace reporting {

// Forward declaration of TestReportScheduler to allow it to be friend with ReportScheduler
class TestReportScheduler;

class TimerContext
{
public:
    virtual ~TimerContext() {}
    virtual void TimerFired() = 0;
};

/**
 * @class ReportScheduler
 *
 * @brief This class is responsible for scheduling Engine runs based on the reporting intervals of the ReadHandlers.
 *
 *
 * This class holds a pool of ReadHandlerNodes that are used to keep track of the minimum and maximum timestamps for a report to be
 * emitted based on the reporting intervals of the ReadHandlers associated with the node.
 *
 * The ReportScheduler also holds a TimerDelegate pointer that is used to start and cancel timers for the ReadHandlers depending
 * on the reporting logic of the Scheduler.
 *
 * It inherits the ReadHandler::Observer class to be notified of reportability changes in the ReadHandlers.
 * It inherits the ICDStateObserver class to allow the implementation to generate reports based on the changes in ICD devices state,
 * such as going from idle to active mode and vice-versa.
 *
 * @note The logic for how and when to schedule reports is implemented in the subclasses of ReportScheduler, such as
 * ReportSchedulerImpl and SyncronizedReportSchedulerImpl.
 */
class ReportScheduler : public ReadHandler::Observer, public ICDStateObserver
{
public:
    using Timestamp = System::Clock::Timestamp;

    /// @brief This class acts as an interface between the report scheduler and the system timer to reduce dependencies on the
    /// system layer.
    class TimerDelegate
    {
    public:
        virtual ~TimerDelegate() {}
        /// @brief Start a timer for a given context. The report scheduler must always cancel an existing timer for a context (using
        /// CancelTimer) before starting a new one for that context.
        /// @param context context to pass to the timer callback.
        /// @param aTimeout time in milliseconds before the timer expires
        virtual CHIP_ERROR StartTimer(TimerContext * context, System::Clock::Timeout aTimeout) = 0;
        /// @brief Cancel a timer for a given context
        /// @param context used to identify the timer to cancel
        virtual void CancelTimer(TimerContext * context)   = 0;
        virtual bool IsTimerActive(TimerContext * context) = 0;
        virtual Timestamp GetCurrentMonotonicTimestamp()   = 0;
    };

    /**
     * @class ReadHandlerNode
     *
     * @brief This class is responsible for determining when a ReadHandler is reportable depending on the monotonic timestamp of
     *  the system and the intervals of the ReadHandler. It inherits the TimerContext class to allow it to be used as a context for
     *  a TimerDelegate so that the TimerDelegate can call the TimerFired method when the timer expires.
     *
     *  Three conditions that can prevent the ReadHandler from being reportable:
     *  1: The ReadHandler is not in the CanStartReporting state:
     *      This condition can be resolved by setting the CanStartReporting flag on the ReadHandler
     *
     *  2: The minimal interval since the last report has not elapsed
     *      This condition can be resolved after enough time has passed since the last report or by setting the EngineRunScheduled
     *      flag
     *
     *  3: The maximal interval since the last report has not elapsed and the ReadHandler is not dirty:
     *      This condition can be resolved after enough time has passed since the last report to reach the max interval, by the
     *      ReadHandler becoming dirty or by setting the CanBeSynced flag and having another ReadHandler needing to report.
     *
     *  Once the 3 conditions are met, the ReadHandler is considered reportable.
     *
     *  Flags:
     *
     *  CanBeSynced: Mechanism to allow the ReadHandler to emit a report if another readHandler is ReportableNow.
     *  This flag is currently only used by the SynchronizedReportScheduler to allow firing reports of ReadHandlers at the same
     *  time.
     *
     *  EngineRunScheduled: Mechanism to ensure that the reporting engine will see the ReadHandler as reportable if a timer fires.
     *  This flag is used to confirm that the next report timer has fired for a ReadHandler, thus allowing reporting when timers
     *  fire earlier than the minimal timestamp due to mechanisms such as NTP clock adjustments.
     *
     */
    class ReadHandlerNode : public TimerContext
    {
    public:
        enum class ReadHandlerNodeFlags : uint8_t
        {
            // Flag to indicate if the engine run is already scheduled so the scheduler can ignore
            // it when calculating the next run time
            EngineRunScheduled = (1 << 0),
            // Flag to allow the read handler to be synced with other handlers that have an earlier max timestamp
            CanBeSynced = (1 << 1),
        };

        ReadHandlerNode(ReadHandler * aReadHandler, ReportScheduler * aScheduler, const Timestamp & now) : mScheduler(aScheduler)
        {
            VerifyOrDie(aReadHandler != nullptr);
            VerifyOrDie(aScheduler != nullptr);

            mReadHandler = aReadHandler;
            SetIntervalTimeStamps(aReadHandler, now);
        }
        ReadHandler * GetReadHandler() const { return mReadHandler; }

        /// @brief Check if the Node is reportable now, meaning its readhandler was made reportable by attribute dirtying and
        /// handler state, and minimal time interval since the last report has elapsed, or the maximal time interval since the last
        /// report has elapsed.
        /// @note If a handler has been flagged as scheduled for an engine run, it will be reported regardless of the timestamps.
        /// This is done to guarantee that the reporting engine will see the handler as reportable if a timer fires, even if it
        /// fires early.
        /// @param now current time to use for the check, the user must ensure to provide a valid time for this to be reliable
        bool IsReportableNow(const Timestamp & now) const
        {
            return (mReadHandler->CanStartReporting() &&
                    ((now >= mMinTimestamp && (mReadHandler->IsDirty() || now >= mMaxTimestamp || CanBeSynced())) ||
                     IsEngineRunScheduled()));
        }

        bool CanStartReporting() const { return mReadHandler->CanStartReporting(); }
        bool IsChunkedReport() const { return mReadHandler->IsChunkedReport(); }
        bool IsEngineRunScheduled() const { return mFlags.Has(ReadHandlerNodeFlags::EngineRunScheduled); }
        void SetEngineRunScheduled(bool aEngineRunScheduled)
        {
            mFlags.Set(ReadHandlerNodeFlags::EngineRunScheduled, aEngineRunScheduled);
        }
        bool CanBeSynced() const { return mFlags.Has(ReadHandlerNodeFlags::CanBeSynced); }
        void SetCanBeSynced(bool aCanBeSynced) { mFlags.Set(ReadHandlerNodeFlags::CanBeSynced, aCanBeSynced); }

        /// @brief Set the interval timestamps for the node based on the read handler reporting intervals
        /// @param aReadHandler read handler to get the intervals from
        /// @param now current time to calculate the mMin and mMax timestamps, the user must ensure to provide a valid time for this
        /// to be reliable
        void SetIntervalTimeStamps(ReadHandler * aReadHandler, const Timestamp & now)
        {
            uint16_t minInterval, maxInterval;
            aReadHandler->GetReportingIntervals(minInterval, maxInterval);
            mMinTimestamp = now + System::Clock::Seconds16(minInterval);
            mMaxTimestamp = now + System::Clock::Seconds16(maxInterval);
        }

        void TimerFired() override
        {
            SetEngineRunScheduled(true);
            mScheduler->ReportTimerCallback();
        }

        System::Clock::Timestamp GetMinTimestamp() const { return mMinTimestamp; }
        System::Clock::Timestamp GetMaxTimestamp() const { return mMaxTimestamp; }

    private:
        ReadHandler * mReadHandler;
        ReportScheduler * mScheduler;
        Timestamp mMinTimestamp;
        Timestamp mMaxTimestamp;

        BitFlags<ReadHandlerNodeFlags> mFlags;
    };

    ReportScheduler(TimerDelegate * aTimerDelegate) : mTimerDelegate(aTimerDelegate) {}

    virtual ~ReportScheduler() = default;

    virtual void ReportTimerCallback() = 0;

    /// @brief Check whether a ReadHandler is reportable right now, taking into account its minimum and maximum intervals.
    /// @param aReadHandler read handler to check
    bool IsReportableNow(ReadHandler * aReadHandler)
    {
        // Update the now timestamp to ensure external calls to IsReportableNow are always comparing to the current time
        Timestamp now          = mTimerDelegate->GetCurrentMonotonicTimestamp();
        ReadHandlerNode * node = FindReadHandlerNode(aReadHandler);
        return (nullptr != node) ? node->IsReportableNow(now) : false;
    }

    /// @brief Check if a ReadHandler is reportable without considering the timing
    bool IsReadHandlerReportable(ReadHandler * aReadHandler) const
    {
        return (nullptr != aReadHandler) ? aReadHandler->ShouldStartReporting() : false;
    }
    /// @brief Sets the ForceDirty flag of a ReadHandler
    void HandlerForceDirtyState(ReadHandler * aReadHandler) { aReadHandler->ForceDirtyState(); }

    /// @brief Get the number of ReadHandlers registered in the scheduler's node pool
    size_t GetNumReadHandlers() const { return mNodesPool.Allocated(); }

#if CONFIG_BUILD_FOR_HOST_UNIT_TEST
    Timestamp GetMinTimestampForHandler(const ReadHandler * aReadHandler)
    {
        ReadHandlerNode * node = FindReadHandlerNode(aReadHandler);
        return node->GetMinTimestamp();
    }
    Timestamp GetMaxTimestampForHandler(const ReadHandler * aReadHandler)
    {
        ReadHandlerNode * node = FindReadHandlerNode(aReadHandler);
        return node->GetMaxTimestamp();
    }
    ReadHandlerNode * GetReadHandlerNode(const ReadHandler * aReadHandler) { return FindReadHandlerNode(aReadHandler); }
#endif // CONFIG_BUILD_FOR_HOST_UNIT_TEST

protected:
    friend class chip::app::reporting::TestReportScheduler;

    /// @brief Find the ReadHandlerNode for a given ReadHandler pointer
    /// @param [in] aReadHandler ReadHandler pointer to look for in the ReadHandler nodes list
    /// @return Node Address if the node was found, nullptr otherwise
    ReadHandlerNode * FindReadHandlerNode(const ReadHandler * aReadHandler)
    {
        ReadHandlerNode * foundNode = nullptr;
        mNodesPool.ForEachActiveObject([&foundNode, aReadHandler](ReadHandlerNode * node) {
            if (node->GetReadHandler() == aReadHandler)
            {
                foundNode = node;
                return Loop::Break;
            }

            return Loop::Continue;
        });
        return foundNode;
    }

    ObjectPool<ReadHandlerNode, CHIP_IM_MAX_NUM_READS + CHIP_IM_MAX_NUM_SUBSCRIPTIONS> mNodesPool;
    TimerDelegate * mTimerDelegate;
};
}; // namespace reporting
}; // namespace app
}; // namespace chip
