/*
Copyright (c) 2017-2020,
Battelle Memorial Institute; Lawrence Livermore National Security, LLC; Alliance for Sustainable
Energy, LLC.  See the top-level NOTICE for additional details. All rights reserved.
SPDX-License-Identifier: BSD-3-Clause
*/
#pragma once

#include "MessageFederate.hpp"

#include <memory>
#include <string>
#include <type_traits>
#include <utility>

namespace helics {
/** class to manage an endpoint */
class HELICS_CXX_EXPORT Endpoint {
  private:
    MessageFederate* fed{nullptr};  //!< the MessageFederate to interact with
    interface_handle handle;  //!< the id as generated by the Federate
    int referenceIndex{-1};  //!< an index used for callback lookup
    void* dataReference{nullptr};  //!< pointer to a piece of containing data
    std::string targetDest;  //!< a predefined target destination
    std::string actualName;  //!< the name of the endpoint
    bool disableAssign{false};  //!< disable assignment for the object
  public:
    /** default constructor*/
    Endpoint() = default;
    /**/
    // constructor used by messageFederateManager
    Endpoint(MessageFederate* mfed, const std::string& name, interface_handle id, void* data):
        fed(mfed), handle(id), dataReference(data), actualName(name)
    {
    }

    Endpoint(MessageFederate* mFed,
             const std::string& name,
             const std::string& type = std::string()):
        Endpoint(mFed->registerEndpoint(name, type))
    {
    }

    template<class FedPtr>
    Endpoint(FedPtr& mFed, const std::string& name, const std::string& type = std::string()):
        Endpoint(mFed->registerEndpoint(name, type))
    {
        static_assert(
            std::is_base_of<MessageFederate, std::remove_reference_t<decltype(*mFed)>>::value,
            "first argument must be a pointer to a MessageFederate");
    }
    /**constructor to build an endpoint object
    @param locality visibility of the endpoint either global or local
    @param mFed  the MessageFederate to use
    @param name the name of the endpoint
    @param type a named type associated with the endpoint
    */
    Endpoint(interface_visibility locality,
             MessageFederate* mFed,
             const std::string& name,
             const std::string& type = std::string());
    /**constructor to build an endpoint object
    @param locality visibility of the endpoint either global or local
    @param mFed  the MessageFederate to use
    @param name the name of the endpoint
    @param type a named type associated with the endpoint
    */
    template<class FedPtr>
    Endpoint(interface_visibility locality,
             FedPtr& mFed,
             const std::string& name,
             const std::string& type = std::string()):
        Endpoint(locality, std::addressof(*mFed), name, type)
    {
        static_assert(
            std::is_base_of<MessageFederate, std::remove_reference_t<decltype(*mFed)>>::value,
            "second argument must be a pointer to a MessageFederate");
    }
    /** check if the Endpoint links to a valid operation*/
    bool isValid() const { return handle.isValid(); }
    bool operator==(const Endpoint& ept) const { return (handle == ept.handle); }
    bool operator!=(const Endpoint& ept) const { return (handle != ept.handle); }
    bool operator<(const Endpoint& ept) const { return (handle < ept.handle); }
    /** send a data block and length
    @param dest string name of the destination
    @param data pointer to data location
    @param data_size the length of the data
    */
    void send(const std::string& dest, const char* data, size_t data_size) const
    {
        fed->sendMessage(*this, dest, data_view(data, data_size));
    }

    /** subscribe the endpoint to a particular publication*/
    void subscribe(const std::string& key) { fed->subscribe(*this, key); }
    /** send a data block and length
    @param dest string name of the destination
    @param data pointer to data location
    @param data_size the length of the data
    @param sendTime the time to send the message
    */
    void send(const std::string& dest, const char* data, size_t data_size, Time sendTime) const
    {
        fed->sendMessage(*this, dest, data_view(data, data_size), sendTime);
    }
    /** send a data block and length
    @param data pointer to data location
    @param data_size the length of the data
    @param sendTime the time to send the message
    */
    void send(const char* data, size_t data_size, Time sendTime) const
    {
        fed->sendMessage(*this, targetDest, data_view{data, data_size}, sendTime);
    }
    /** send a data_view
    @details a data view can convert from many different formats so this function should
    be catching many of the common use cases
    @param dest string name of the destination
    @param data the information to send
    */
    void send(const std::string& dest, const data_view& data) const
    {
        fed->sendMessage(*this, dest, data);
    }
    /** send a data_view
    @details a data view can convert from many different formats so this function should
    be catching many of the common use cases
    @param dest string name of the destination
    @param data data representation to send
    @param sendTime  the time the message should be sent
    */
    void send(const std::string& dest, const data_view& data, Time sendTime) const
    {
        fed->sendMessage(*this, dest, data, sendTime);
    }
    /** send a data block and length to the target destination
    @param data pointer to data location
    @param data_size the length of the data
    */
    void send(const char* data, size_t data_size) const
    {
        fed->sendMessage(*this, targetDest, data_view{data, data_size});
    }
    /** send a data_view to the target destination
    @details a data view can convert from many different formats so this function should
    be catching many of the common use cases
    @param data the information to send
    */
    void send(const data_view& data) const { fed->sendMessage(*this, targetDest, data); }
    /** send a data_view to the specified target destination
    @details a data view can convert from many different formats so this function should
    be catching many of the common use cases
    @param data a representation to send
    @param sendTime  the time the message should be sent
    */
    void send(const data_view& data, Time sendTime) const
    {
        fed->sendMessage(*this, targetDest, data, sendTime);
    }
    /** send a pointer to a message object*/
    void send(std::unique_ptr<Message> mess) const
    {
        if (mess->dest.empty()) {
            mess->dest = targetDest;
        }
        fed->sendMessage(*this, std::move(mess));
    }
    /** send a message object
    @details this is to send a pre-built message
    @param mess a reference to an actual message object
    */
    void send(const Message& mess) const { send(std::make_unique<Message>(mess)); }
    /** get an available message if there is no message the returned object is empty*/
    auto getMessage() const { return fed->getMessage(*this); }
    /** check if there is a message available*/
    bool hasMessage() const { return fed->hasMessage(*this); }
    /** check if there is a message available*/
    auto pendingMessages() const { return fed->pendingMessages(*this); }
    /** register a callback for an update notification
    @details the callback is called in the just before the time request function returns
    @param callback a function with signature void(endpoint_id_t, Time)
    time is the time the value was updated  This callback is a notification callback and doesn't
    return the value
    */
    void setCallback(const std::function<void(const Endpoint&, Time)>& callback)
    {
        fed->setMessageNotificationCallback(*this, callback);
    }

    /** add a named filter to an endpoint for all message coming from the endpoint*/
    void addSourceFilter(const std::string& filterName) { fed->addSourceFilter(*this, filterName); }
    /** add a named filter to an endpoint for all message going to the endpoint*/
    void addDestinationFilter(const std::string& filterName)
    {
        fed->addDestinationFilter(*this, filterName);
    }
    /** set a target destination for unspecified messages*/
    void setDefaultDestination(std::string target) { targetDest = std::move(target); }
    /** get the target destination for the endpoint*/
    const std::string& getDefaultDestination() const { return targetDest; }
    /** get the name of the endpoint*/
    const std::string& getName() const { return actualName; }
    /** get the name of the endpoint*/
    const std::string& getKey() const { return fed->getInterfaceName(handle); }
    /** get the specified type of the endpoint*/
    const std::string& getType() const { return fed->getExtractionType(*this); }
    /** get the actual endpoint id for the fed*/
    interface_handle getHandle() const { return handle; }
    /** implicit conversion operator for extracting the handle*/
    operator interface_handle() const { return handle; }
    /** get the interface information field of the publication*/
    const std::string& getInfo() const { return fed->getInfo(handle); }
    /** set the interface information field of the publication*/
    void setInfo(const std::string& info) { fed->setInfo(handle, info); }
    void setOption(int32_t option, int32_t value)
    {
        fed->setInterfaceOption(handle, option, value);
    }

    /** get the current value of a flag for the handle*/
    int32_t getOption(int32_t option) const { return fed->getInterfaceOption(handle, option); }
    /** close the endpoint from receiving more messages*/
    void close() { fed->closeInterface(handle); }

  private:
    friend class MessageFederateManager;
};
}  // namespace helics
