// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC")
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

#ifndef PACKET_QUEUE_MGR_H
#define PACKET_QUEUE_MGR_H

#include <cc/data.h>
#include <cc/simple_parser.h>
#include <dhcp/packet_queue.h>
#include <exceptions/exceptions.h>
#include <boost/shared_ptr.hpp>
#include <functional>
#include <map>
#include <string>

namespace isc {
namespace dhcp {

/// @brief Invalid Queue type exception
///
/// Thrown when a packet queue manager doesn't recognize the type of the queue.
class InvalidQueueType : public Exception {
public:
    InvalidQueueType(const char* file, size_t line, const char* what) :
        isc::Exception(file, line, what) {}
};

/// @brief Packet Queue Managers (PQM).
///
/// Base class to manage the registry of packet queue implemenations
/// and the creation of and access to the current packet queue.
///
/// @tparam PacktQueueTypePtr Base type of packet queues managed by
/// the manager (e.g. PacketQueue4Ptr, PacketQueue6Ptr).
template<typename PacketQueueTypePtr>
class PacketQueueMgr {
public:
    /// @brief Defines the type of the packet queue factory function.
    ///
    /// Factory function returns a pointer to the instance of the packet
    /// queue created. 
    typedef std::function<PacketQueueTypePtr(data::ConstElementPtr)> Factory;

    /// @brief Constructor.
    PacketQueueMgr()
        : factories_(), packet_queue_() {
    }

    /// @brief Registers new queue factory function for a given queue type.
    ///
    /// The typical usage of this function is to make the PQM aware of a
    /// packet queue implementation. This implementation may exist
    /// in a hooks library. In such a case, this function should be called from
    /// the @c load function in this library. When the queue impl is registered,
    /// the server will use it when required by the configuration, i.e. a
    /// user specifies it in "queue-control:queue-type"
    ///
    /// If the given queue type has already been registered, perhaps
    /// by another hooks library, the PQM will refuse to register another
    /// of the same type.
    ///
    /// @param queue_type Queue type, e.g. "kea-ring4"
    /// @param factory Pointer to the queue factory function.
    ///
    /// @return true if the queue type has been successfully registered, false
    /// if the type already exists.
    bool registerPacketQueueFactory(const std::string& queue_type,
                                    Factory factory) {
        // Check if this backend has been already registered.
        if (factories_.count(queue_type)) {
            return (false);
        }

        // Register the new backend.
        factories_.insert(std::make_pair(queue_type, factory));
        return (true);
    }

    /// @brief Unregisters the queue factory function for a given type.
    ///
    /// This function is used to remove the factory function for a given type.
    /// Typically, it would be called when unloading the hook library which
    /// loaded the type, and thus called by the library's @c unload function.
    /// In addition to removing the factory, it will also destroy the current
    /// queue if it is of the same queue-type as the factory being removed.
    /// This avoids the nastiness that occurs when objecs are left in existence
    /// after their library is unloaded.
    ///
    /// @param queue_type queue type, e.g. "kea-ring4".
    ///
    /// @return false if no factory for the given type was unregistered, true
    /// if the factory was removed.
    bool unregisterPacketQueueFactory(const std::string& queue_type) {
        // Look for it.
        auto index = factories_.find(queue_type);

        // Not there so nothing to do.
        if (index == factories_.end()) {
            return (false);
        }

        // If the queue is of the type being unregistered, then remove it. We don't
        // a queue instance outliving its library.
        if ((packet_queue_) && (packet_queue_->getQueueType() == queue_type)) {
            packet_queue_.reset();
        }

        // Remove the factory.
        factories_.erase(index);

        return (true);
    }

    /// @brief Create an instance of a packet queue.
    ///
    /// Replace the current packet queue with a new one based on the
    /// given configuration parameters.  The set of parameters must
    /// contain at least "queue-type".  This value is used to locate
    /// the registered queue factory to invoke to create the new queue.
    ///
    /// The factory is passed the parameters verbatim for its use in
    /// creating the new queue.  Factories are expected to throw exceptions
    /// on creation failure. Note the existing queue is not altered or
    /// replaced unless the new queue is successfully created.
    ///
    /// @throw InvalidQueueParameter if parameters is not map that contains
    /// "queue-type", InvalidQueueType if the queue type requested is not
    /// supported.
    /// @throw Unexpected if the backend factory function returned NULL.
    void createPacketQueue(data::ConstElementPtr parameters) {
        if (!parameters) {
            isc_throw(Unexpected, "createPacketQueue - queue parameters is null");
        }

        // Get the database type to locate a factory function.
        std::string queue_type ;
        try {
            queue_type = data::SimpleParser::getString(parameters, "queue-type");
        } catch (std::exception& ex) {
            isc_throw(InvalidQueueParameter, "queue-type missing or invalid: " << ex.what());
        }

        // Look up the factory.
        auto index = factories_.find(queue_type);

        // Punt if there is no matching factory.
        if (index == factories_.end()) {
            isc_throw(InvalidQueueType, "The type of the packet queue: '" <<
                      queue_type << "' is not supported"); }

        // Call the factory to create the new queue.
        // Factories should throw InvalidQueueParameter if given
        // bad values in the control.
        auto new_queue = index->second(parameters);
        if (!new_queue) {
            isc_throw(Unexpected, "Packet queue " << queue_type <<
                      " factory returned NULL");
        }

        // Replace the existing queue with the new one.
        packet_queue_ = new_queue;
    }

    /// @brief Returns underlying packet queue.
    PacketQueueTypePtr getPacketQueue() const {
        return (packet_queue_);
    }

    /// @brief Destroys the current packet queue.
    /// Any queued packets will be discarded.
    void destroyPacketQueue() {
        packet_queue_.reset();
    }

protected:
    /// @brief A map holding registered backend factory functions.
    std::map<std::string, Factory> factories_;

    /// @brief the current queue_ ?
    PacketQueueTypePtr packet_queue_;
};

} // end of namespace isc::dhcp
} // end of namespace isc

#endif // PACKET_QUEUE_MGR_H
