#ifndef CLIENT_H
#define CLIENT_H

#include "zmq_bazooka/sockets.h"
#include "zmq_bazooka/input_selector.h"
#include "bibun_messenging/sender.h"
#include "bibun_messenging/receiver.h"
#include "bibun_messenging/message.h"
#include "bibun_messenging/registry.h"
#include "bibun_util/exception.h"

#include <memory>
#include <functional>
#include <vector>
#include <string>
#include <chrono>
#include <queue>
#include <mutex>

namespace bibunm
{

///
/// \brief Receive and send messages on the GuBinBus.
///
class Client
{
   // Need to define how to copy this non-copyable atomic. The solution is not to copy but create a new atomic.
   struct TerminationData // TODO. Util this.
   {
      TerminationData();
      TerminationData(const TerminationData &);
      TerminationData &operator=(const TerminationData &);

      void terminate();

      bool &isTermed();
      std::mutex &termLock();
   private:
      bool _has_termed;
      std::mutex _has_termed_lock;
   };

   typedef std::function<void()> message_result_cb;
public:
   Client() = delete;

   ///
   /// \brief Create a client to recevie and send messages on the GuBinBus.
   /// \param id The name of our application. Unique on a device but common to the same application running on other
   ///    devices.
   /// \param node_pub_port The broadcaster port number of the GuBinBus node on your device.
   /// \param node_channeler_port The channeler port number of the GuBinBus node on your device.
   /// \param subscriptions What applications are you interested in. You can even subscribe to your self. This is a
   ///    list of subscriptions that you will receive messages for from Clients that chose them as their own id.
   ///
   Client(std::string id, int node_broadcaster_port, int node_channeler_port,
          std::vector<std::string> subscriptions = std::vector<std::string>());

   /// @name Move and copy semantics.
   /// We prevent copying because there are many members which require locking and such that do not lend to these
   /// semantics. A Client can be moved.
   /// @{

   Client(const Client &) = delete;
   Client &operator=(const Client &) = delete;

   Client(Client &&) = default;
   Client &operator=(Client &&) = default;

   /// @}

   /// @name Sending messages
   /// Sending messages on the GuBinBus framework. These functions take care of various options for ownership of
   /// message data and try to assist in managing the asynchronous nature of network IO.
   /// @{

   void send(char *data,
             size_t length,
             std::string type,
             std::list<std::string> routes = std::list<std::string>(),
             std::chrono::seconds valid_for = std::chrono::hours(1),
             message_result_cb callback = nullptr,
             Message::AckMode ack_mode = Message::AckMode::NONE);

   void send(Payload payload,
             std::list<std::string> routes = std::list<std::string>(),
             std::chrono::seconds valid_for = std::chrono::hours(1),
             message_result_cb callback = nullptr,
             Message::AckMode ack_mode = Message::AckMode::NONE);

   void send(Message message, message_result_cb callback = nullptr);

   /// @}

   ///
   /// \brief Hand thread over to BiBun until the program ends.
   /// \param error_callback Provide a function to handle errors in the client. There is not a whole lot you can do with
   ///   an error... maybe just log the error. For example: \code{.cpp}
   ///   Client client(/* construct as you see fit */);
   ///
   ///   // Provide a callback for when we receive messages.
   ///   client.onMessage([](Message *msg) {
   ///      cout << "Hey I received: " << msg->payload().getBuffer() << endl;
   ///   });
   ///
   ///   // Some hypothetical structure that holds things like registry lists. We can only receive registry messages
   ///   // in this thread so we must protect the stuff with mutex locks.
   ///   SharedData shared_data;
   ///
   ///   // Define what we do when we receive registry messages.
   ///   client.obtainRegistry([&shared_data](shared_ptr<Registry> registry) {
   ///      // We share ownership of registry. I won't delete it because you are going to send it to
   ///      // update_registry_list... but I would if you did not.
   ///      shared_data.update_registry_list(registry);
   ///   });
   ///
   ///   // In these threads, you can call client.send(/* args */); and you will receive registry updates... somehow...
   ///   // you will probably have to inspect shared_data in those other threads.
   ///   pass_client_to_other_threads(&client, &shared_data);
   ///
   ///   // Note. You have not received or sent anything. It is time... you have to give this thread up until the
   ///   // program ends. This will block until that time.
   ///   client.blockThreadForIO([&logger](bibunu::Exception &exp){
   ///      // You can also add some of your own context to the exception. You should make this sound like it has
   ///      // to do with your project. Example: Project V encountered a problem while trying to handle requests in
   ///      // BiBun.
   ///      exp.addContext("While trying to handle BiBun stuff");
   ///      // Log backtrace of how this error happened.
   ///      logger.error() << exp.generateMessage(); // generateMessage creates a detailed backtrace of the error.
   ///   });
   ///   \endcode
   /// \param recv_timeout How long to wait while polling for received requests. Too short and you will use all your
   ///   CPU cycles. Too long and you will schedule requests to be sent too slowly. Just use the default please.
   ///
   void blockThreadForIO(std::function<void(bibunu::Exception &exp)> error_callback = nullptr,
                         std::chrono::milliseconds recv_timeout = std::chrono::seconds(1));

   ///
   /// \brief If you can access client from another thread, this function will stop blockThreadForIO so that your
   /// program can end cleanly.
   ///
   void terminate();

   ///
   /// \brief Register a message callback to fire when you receive a message.
   ///
   /// The function fires in your IO thread (the thread you called blockThreadForIO in and where you created
   /// this Client). It is a good idea to try handle the function quickly. The IO thread is blocking while you wait.
   /// You can send an Ack with client.sendAck(*msg); if you captured your client in a lambda, functor or your client
   /// is available globally.
   /// \param message_callback A function that handles a unique_ptr to a Message. You use it like a normal pointer
   /// and it will "self-delete" at the end of the callback. If you need it for longer than that, you should release
   /// the unique_ptr (this gives you the Message* which you must delete) or move (using std::move) it into your
   /// own container/thread.
   ///
   void onMessage(std::function<void(std::unique_ptr<Message>)> message_callback);

   // Obtain a registry object describing all the nodes connected to the Bibun node on an interval basis. Call the
   // function without arguments to cancel this.
   // The GetRegistry request will be sent every interval seconds, as long a client thread keeps calling completeSends.

   ///
   /// \brief Register how we should handle registry messages.
   ///
   /// The registry message will contain the layout of the nodes connected to the node on your device. Don't call this
   /// function if you are at the "bottom" of the GuBinBus. You don't direct your messages to a particular node.
   /// \param registry_callback A function to handle the registry you receive. The payload should be a JSON blob
   /// describing nodes connected to the node on your device, and their children and so on.
   /// \param interval How often we should refresh the registry.
   ///
   void obtainRegistry(std::function<void(std::shared_ptr<Registry>)> registry_callback = nullptr,
      std::chrono::seconds interval = std::chrono::seconds(60)); // TODO. Is 60 seconds a good default.

   // Send an ack for the provided message.
   void sendAck(const Message &message);

   ///
   /// \brief Your applications identification.
   /// \return The string.
   ///
   const std::string &id() const;

   TerminationData &termData();

   virtual ~Client();
private:
   // Check for new requests. The timeout is how long we should block before continuing with no action.
   // The timeout is infinite by default. Return of true indicates that requests were read.
   bool check(std::chrono::milliseconds timeout = std::chrono::milliseconds(-1));

   void receive_on_listener(bzook::Socket*);
   void receive_on_dispenser(bzook::Socket*);
   void handle_request(std::unique_ptr<internal_objects::Request> request);

   // Send one inbox request for provided subscription.
   void send_inbox();

   bool send_one_msg();
   void send_all_msgs();

   std::chrono::system_clock::time_point _last_inbox;
   std::chrono::seconds _inbox_interval; // TODO. For now this is set to a default value of 60 seconds and on startup.
   // We must not inbox while we are in a message callback. This is to allow the client to ack the message, if we
   // send an inbox, we could receive the message immediately again. Although, reads and sends happen in one thread and
   // therefore this collision seems impossible. // TODO. Validate my suspicions that this is overkill.
   std::unique_ptr<std::mutex> _inbox_lock;

   struct MessageQueue
   {
      MessageQueue();
      MessageQueue(MessageQueue &&other);

      void push(Message &msg);

      bool performOperation(std::function<bool(const Message &msg)> operation);

   private:
      static bool determine_priority(const Message &lhs, const Message &rhs);

      std::mutex _lck;
      std::priority_queue<Message, std::deque<Message>, decltype(&determine_priority)> _msgs;
   };
   MessageQueue _message_queue;

   struct MsgResultCallbacks
   {
      MsgResultCallbacks();
      MsgResultCallbacks(MsgResultCallbacks &&other);

      void registerCallback(Message &msg, message_result_cb callback);

      void handleAck(internal_objects::Ack &ack);
   private:
      std::mutex _lck;
      std::map<std::string, message_result_cb> _msg_res_map;
   };
   MsgResultCallbacks _msg_results;

   std::unique_ptr<std::mutex> _rcv_lock;
   std::queue<Message> _rcv_msg_queue;

   std::function<void(std::unique_ptr<Message>)> _message_callback;

   // Registry callback and interval for GetRegistryRequests.
   std::function<void(std::shared_ptr<Registry>)> _registry_callback;
   std::chrono::seconds _get_registry_interval;
   std::chrono::system_clock::time_point _last_get_reg_send;
   void send_get_registry();

   bzook::Socket _listener;
   bibunm::Receiver _lstn_rcver;
   bzook::Socket _dispenser;
   bibunm::Receiver _disp_rcver;
   bibunm::Sender _disp_snder;

   bzook::InputSelector _input_selector;

   std::string _id;
   std::vector<std::string> _subscriptions;

   TerminationData _termination;
};

}

#endif // CLIENT_H
