#ifndef IPC_CLIENT_H
#define IPC_CLIENT_H

#include <memory> //std::enable_shared_from_this
#include <string>

#include <chrono>
#include <thread>
#include <atomic>

#include "ipc_session_holder.h"
#include "ipc_session.h"
#include "logger.h"

namespace ipc {

template<typename CustomSession>
class Client : public SessionHolder, public std::enable_shared_from_this<Client<CustomSession>> {
  public:
    Client(asio::io_service& io_service, const std::string &ip, const std::string &port)
      : socket_(io_service), resolver_(io_service), query_(ip, port), ip_(ip), port_(port) {
      reconnect_running_ = false;
      stopped_ = true;
    }

     ~Client() {
      stop();
    }

  /**
    * @brief Is client running.
    * Checks if the client is running. It might have been stopped with stop().
    */
    inline bool isRunning() const {
      return !stopped_;
    }

  /**
    * @brief Start the client.
    * This is also required because shared_from_this() doesn't work in constructor.
    */
    void start() {
      if (stopped_ == true) {
        stopped_ = false;
        resolve();
      }
    }

  /**
    * @brief Stop the client.
    */
    void stop() {
      std::lock_guard<std::mutex> lock(shutdown_mutex_);
      if (stopped_ == false) {
        stopped_ = true;
        resolver_.cancel();
        // If connected
        if (session_) {
          session_->close();
        }
        // If disconnected
        if (reconnect_thread_.joinable()) {
          reconnect_thread_.join();
        }
      }
    }


  /**
    * @brief Send buffer to the server.
    * @param buff Buffer to send
    */
    void write(const Buffer &buff) {
      if (session_) {
        if (session_->connected() == true) { //If we lose connection, we also lose this message. Nothing to do for it now
          session_->write(buff);
        }
      }
    }

  /**
    * @brief Returns true if the client is connected.
    */
    inline bool connected() { return (session_ ? session_->connected() : false); }

  /**
    * @brief Returns the session.
    */
    inline std::shared_ptr<CustomSession> session() const {
      if (!session_) {
        LOG_ERROR("Returned session is nullptr! Connect to server or check connected() before using this!");
      }
      return session_; 
    }

  /**
    * @brief Subscribe to the server.
    * NOTE: If a subscribed client is stopped, and then started, it will not automatically resubscribe!
    * Put subscribe() in handleConnect() if auto-subscripton is required!
    */
    inline void subscribe() { if (session_) { session_->subscribe(); } }

  /**
    * @brief Unsubscribe from the server.
    */
    inline void unsubscribe() { if (session_) { session_->unsubscribe(); } }

  /**
    * @brief Returns true if subscribed to the server.
    */
    inline bool subscribed() { return (session_ ? session_->subscribed() : false); }

  private:
  /**
    * @brief Resolves the address (if url, then through DNS) via async call.
    */
    void resolve() {
      if (stopped_ == true) { return; }
      auto self(this->shared_from_this());
      resolver_.async_resolve(query_,
        [this,self](std::error_code ec, asio::ip::tcp::resolver::iterator endpoint_iterator) {
            if (!ec) {
              endpoint_iterator_ = endpoint_iterator;
              connect();
            } else {
              LOG_WARNING("IPC::Client::resolve() - host:port = %s:%s - Error = %s - Trying again!"
                          ,ip_.c_str(), port_.c_str(), ec.message().c_str());
              if (reconnect_running_ == false) {
                reconnect_running_ = true;
                if (reconnect_thread_.joinable()) {
                  reconnect_thread_.join();
                }
                reconnect_thread_ = std::thread([this] {
                  std::this_thread::sleep_for(std::chrono::milliseconds(500));
                  reconnect_running_ = false;
                  resolve();
                } );
              }
            }
          }
        );
    }

  /**
    * @brief Tries connecting to the server. Will try every 500ms on failure.
    */
    void connect() {
      if (stopped_ == true) { return; }
      asio::ip::tcp::endpoint endpoint = *endpoint_iterator_;
      auto self(this->shared_from_this());
      socket_.async_connect(endpoint,
        [this,self](std::error_code ec) {
            if (!ec) {
              // The connection was successful. Send the request.
              // NOTE(harijs): We could output endpoint_iterator_->endpoint() here which would show the resolved ip:port pair
              // Or we could pass it to session, so it could be printed there
              session_ = std::make_shared<CustomSession>(std::move(socket_), this, false);
              session_->start();
              return;
            }
            ++endpoint_iterator_;
            if (endpoint_iterator_ != asio::ip::tcp::resolver::iterator()) {
              // The connection failed. Try the next endpoint in the list.
              LOG_WARNING("IPC::Client::connect() - host:port = %s:%s - Error = Connection failed! Trying the next endpoint! Error = %s"
                          ,ip_.c_str(), port_.c_str(), ec.message().c_str());
              if (reconnect_running_ == false) {
                reconnect_running_ = true;
                if (reconnect_thread_.joinable()) {
                  reconnect_thread_.join();
                }
                reconnect_thread_ = std::thread([this] {
                  std::this_thread::sleep_for(std::chrono::milliseconds(500));
                  reconnect_running_ = false;
                  connect();   
                } );
              }
            } else {
              LOG_WARNING("IPC::Client::connect() - host:port = %s:%s - Error = %s"
                          ,ip_.c_str(), port_.c_str(), ec.message().c_str());
              if (reconnect_running_ == false) {
                reconnect_running_ = true;
                if (reconnect_thread_.joinable()) {
                  reconnect_thread_.join();
                }
                reconnect_thread_ = std::thread([this] {               
                  std::this_thread::sleep_for(std::chrono::milliseconds(500));
                  reconnect_running_ = false;
                  resolve();
                } );
              }
            }
          }
        );
    }

    asio::ip::tcp::socket socket_;
    asio::ip::tcp::resolver resolver_;
    asio::ip::tcp::resolver::iterator endpoint_iterator_;
    asio::ip::tcp::resolver::query query_;

    std::string ip_;
    std::string port_;
    std::shared_ptr<CustomSession> session_;

    std::thread reconnect_thread_;
    std::atomic<bool> reconnect_running_;
    std::atomic<bool> stopped_;

    std::mutex shutdown_mutex_;
};

}

#endif //IPC_CLIENT_H
