/*
 * Copyright Amazon.com, Inc. or its affiliates. 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.
 * A copy of the License is located at
 *
 *     http://aws.amazon.com/apache2.0/
 *
 * or in the "license" file accompanying this file. This file 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.
 */

#ifndef ALEXA_CLIENT_SDK_ADSL_INCLUDE_ADSL_DIRECTIVEROUTER_H_
#define ALEXA_CLIENT_SDK_ADSL_INCLUDE_ADSL_DIRECTIVEROUTER_H_

#include <map>
#include <set>
#include <unordered_map>

#include <AVSCommon/AVS/CapabilityTag.h>
#include <AVSCommon/AVS/DirectiveHandlerConfiguration.h>
#include <AVSCommon/AVS/HandlerAndPolicy.h>
#include <AVSCommon/Utils/RequiresShutdown.h>
#include <AVSCommon/Utils/Metrics/MetricEventBuilder.h>
#include <AVSCommon/Utils/Metrics/MetricRecorderInterface.h>

namespace alexaClientSDK {
namespace adsl {
/**
 * Class to maintain a mapping from @c NamespaceAndName to @c HandlerAndPolicy, and to invoke
 * @c DirectiveHandlerInterface methods on the @c DirectiveHandler registered for a given @c AVSDirective.
 */
class DirectiveRouter : public avsCommon::utils::RequiresShutdown {
public:
    /// Constructor.
    /// @param metricRecorder The metric recorder.
    DirectiveRouter(std::shared_ptr<avsCommon::utils::metrics::MetricRecorderInterface> metricRecorder = nullptr);

    /**
     * Add mappings from handler's directives to @c BlockingPolicy values, gotten through the
     * handler's getConfiguration() method. If a mapping for any of the specified directives already
     * exists the entire call is refused.
     *
     * @param handler The handler to add.
     * @return Whether the handler was added.
     */
    bool addDirectiveHandler(std::shared_ptr<avsCommon::sdkInterfaces::DirectiveHandlerInterface> handler);

    /**
     * Remove the specified mappings from @c NamespaceAndName values to @c BlockingPolicy values, gotten through the
     * handler's getConfiguration() method. If any of the specified mappings do not match an existing mapping, the
     * entire operation is refused.
     *
     * @param handler The handler to remove.
     * @return Whether the configuration was removed.
     */
    bool removeDirectiveHandler(std::shared_ptr<avsCommon::sdkInterfaces::DirectiveHandlerInterface> handler);

    /**
     * Invoke @c handleDirectiveImmediately() on the handler registered for the given @c AVSDirective.
     *
     * @param directive The directive to be handled immediately.
     * @return Whether or not the handler was invoked.
     */
    bool handleDirectiveImmediately(std::shared_ptr<avsCommon::avs::AVSDirective> directive);

    /**
     * Invoke @c preHandleDirective() on the handler registered for the given @c AVSDirective.
     *
     * @param directive The directive to be preHandled.
     * @param result A result object to receive notification about the completion (or failure) of handling
     * the @c AVSDirective.
     * @return Whether or not the handler was invoked.
     */
    bool preHandleDirective(
        std::shared_ptr<avsCommon::avs::AVSDirective> directive,
        std::unique_ptr<avsCommon::sdkInterfaces::DirectiveHandlerResultInterface> result);

    /**
     * Invoke @c handleDirective() on the handler registered for the given @c AVSDirective.
     *
     * @param directive The directive to be handled.
     * @return @c true if the the registered handler returned @c true.  @c false if there was no registered handler
     * or the registered handler returned @c false (indicating that the directive was not recognized.
     */
    bool handleDirective(const std::shared_ptr<avsCommon::avs::AVSDirective>& directive);

    /**
     * Invoke cancelDirective() on the handler registered for the given @c AVSDirective.
     *
     * @param directive The directive to be handled.
     * @return Whether or not the handler was invoked.
     */
    bool cancelDirective(std::shared_ptr<avsCommon::avs::AVSDirective> directive);

    /**
     * Get the policy associated with the given directive.
     *
     * @param directive The directive for which the policy is required.
     * @return The corresponding @c BlockingPolicy value for the directive.
     */
    avsCommon::avs::BlockingPolicy getPolicy(const std::shared_ptr<avsCommon::avs::AVSDirective>& directive);

private:
    void doShutdown() override;

    /**
     * The lifecycle of instances of this class are used to set-up and tear-down around a call to a
     * @c DirectiveHandlerInterface method.  In particular, while instantiated it increments the reference count of
     * uses of the handler and releases m_mutex via a @c std::unique_lock so the mutex is not held during the call.
     * When destroyed m_mutex is re-acquired and the reference count for the handler is decremented.  When the
     * reference count goes to zero, the handler's @c onDeregistered() method in invoked.
     */
    class HandlerCallScope {
    public:
        /**
         * Constructor.
         * @note This constructor must only be called by threads that have acquired @c m_mutex.  When this constructor
         * exits @c m_mutex will be unlocked.
         *
         * @param lock The @c std::unique_lock to use to release and re-acquire @c m_mutex.
         * @param router The @c DirectiveRouter instance the will make the call.
         * @param handler The @c DirectiveHandlerInterface instance to call.
         */
        HandlerCallScope(
            std::unique_lock<std::mutex>& lock,
            DirectiveRouter* router,
            std::shared_ptr<avsCommon::sdkInterfaces::DirectiveHandlerInterface> handler);

        /**
         * Destructor.
         * @note This destructor must be called with @c m_mutex released.  When this destructor exits @c m_mutex will
         * have been re-acquired.
         */
        ~HandlerCallScope();

    private:
        /// The lock used to release and re-acquire @c m_mutex.
        std::unique_lock<std::mutex>& m_lock;

        /// The @c DirectiveRouter instance the will make the call.
        DirectiveRouter* m_router;

        /// The @c DirectiveHandlerInterface instance to call.
        std::shared_ptr<avsCommon::sdkInterfaces::DirectiveHandlerInterface> m_handler;
    };

    /**
     * Look up the configured @c HandlerAndPolicy value for the specified @c AVSDirective.
     * @note The calling thread must have already acquired @c m_mutex.
     *
     * @param directive The directive to look up a value for.
     * @return The corresponding @c HandlerAndPolicy value for the specified directive.
     */
    avsCommon::avs::HandlerAndPolicy getHandlerAndPolicyLocked(
        const std::shared_ptr<avsCommon::avs::AVSDirective>& directive);

    /**
     * Get the @c DirectiveHandler for this directive.
     * @note The calling thread must have already acquired @c m_mutex.
     *
     * @param directive The @c AVSDirective for which we're looking for handler.
     * @return The directive handler for success. @c nullptr in failure.
     */
    std::shared_ptr<avsCommon::sdkInterfaces::DirectiveHandlerInterface> getHandlerLocked(
        std::shared_ptr<avsCommon::avs::AVSDirective> directive);

    /**
     * Increment the reference count for the specified handler.
     *
     * @param handler The handler for which the reference count should be incremented.
     */
    void incrementHandlerReferenceCountLocked(
        std::shared_ptr<avsCommon::sdkInterfaces::DirectiveHandlerInterface> handler);

    /**
     * Decrement the reference count for the specified handler.  If the reference count goes to zero, call
     * the handlers onDeregistered() method.
     *
     * @param lock The @c std::unique_lock used to release and re-acquire @c m_mutex if @c onDeregistered() is called.
     * @param handler The @c DirectiveHandlerInterface instance whose reference count is to be decremented.
     */
    void decrementHandlerReferenceCountLocked(
        std::unique_lock<std::mutex>& lock,
        std::shared_ptr<avsCommon::sdkInterfaces::DirectiveHandlerInterface> handler);

    /**
     * Remove the specified mappings from @c NamespaceAndName values to @c BlockingPolicy values, gotten through the
     * handler's getConfiguration() method. If any of the specified mappings do not match an existing mapping, the
     * entire operation is refused.  This function should be called while holding m_mutex.
     *
     * @param handler The handler to remove.
     * @return @c true if @c handler was removed successfully, else @c false indicates an error occurred.
     */
    bool removeDirectiveHandlerLocked(std::shared_ptr<avsCommon::sdkInterfaces::DirectiveHandlerInterface> handler);

    /// The metric recorder.
    std::shared_ptr<avsCommon::utils::metrics::MetricRecorderInterface> m_metricRecorder;

    /// A mutex used to serialize access to @c m_configuration and @c m_handlerReferenceCounts.
    std::mutex m_mutex;

    /// Mapping from @c CapabilityMessageIdentifier to @c PolicyAndHandler.
    std::unordered_map<avsCommon::avs::CapabilityTag, avsCommon::avs::HandlerAndPolicy> m_configuration;

    /**
     * Instances of DirectiveHandlerInterface may receive calls after @c removeDirectiveHandlers() because
     * @ removeDirectiveHandlers() does not wait for any outstanding calls to complete.  To provide notification
     * that no more calls will be received, a reference count is maintained for each directive handler.  These
     * counts are incremented when a handler is added to @c m_configuration or when a call to a handler is in
     * progress.  These counts are decremented when the handler is removed from @c m_configuration or a call to
     * a handler returns.  When these count goes to zero the handler's @c onDeRegistered() method is invoked,
     * indicating that the handler will no longer be called (unless, of course, it is re-registered).
     */
    std::unordered_map<std::shared_ptr<avsCommon::sdkInterfaces::DirectiveHandlerInterface>, int>
        m_handlerReferenceCounts;

    /**
     * Submit metrics related to the given directive.
     *
     * @param metricEventBuilder The metric event builder to be used.
     * @param directive The given directive.
     */
    void submitMetric(
        avsCommon::utils::metrics::MetricEventBuilder& metricEventBuilder,
        const std::shared_ptr<avsCommon::avs::AVSDirective>& directive);
};

}  // namespace adsl
}  // namespace alexaClientSDK

#endif  // ALEXA_CLIENT_SDK_ADSL_INCLUDE_ADSL_DIRECTIVEROUTER_H_
