#ifndef IPC_MANAGER_H
#define IPC_MANAGER_H

#include <string>
#include <memory>
#include <thread>
#include <map>
#include <fstream>

#if __GNUG__
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-variable"
#endif
#include "asio.hpp"
#if __GNUG__
#pragma GCC diagnostic pop
#endif

#include "ipc_client.h"
#include "ipc_server.h"
#include "logger.h"

namespace ipc {

class Manager {
  public:
    explicit Manager(size_t thread_count = 1) {
      LOG_INFO("IPC Protocol Version %u.%u", static_cast<unsigned>(ipc::major_version), static_cast<unsigned>(ipc::minor_version));
      work_.reset( new asio::io_service::work(io_service_) );

      // Auto-select half of available threads
      if (thread_count == 0) {
        thread_count = std::thread::hardware_concurrency() / 2;
        if (thread_count == 0) {
          thread_count = 1;
        }
      }

      for (size_t i = 0; i < thread_count; ++i) {
        io_thread_pool_.emplace_back(std::thread( [&]{ io_service_.run(); } ));
      }
    }

    Manager(const std::string &db_fname, size_t thread_count = 1) : Manager(thread_count) {
      loadDB(db_fname);
    }

    ~Manager() {
      work_.reset(); //Allows io_service_.run() to finish
      //Work reset won't work if the handlers are chained (like they are in the header aquire/decode/aquire loop)
      //So it is also good to just stop the system here - the bad thing is it will happen by canceling all async_ functions
      io_service_.stop();
      for (auto &thread : io_thread_pool_) {
        if (thread.joinable()) {
          thread.join();
        }
      }
    }

    template<typename CustomSession>
    std::shared_ptr<Client<CustomSession> > createClient(const char* service, bool autostart = true) { 
      return createClient<CustomSession>(std::string(service), autostart);
    }

    template<typename CustomSession>
    std::shared_ptr<Client<CustomSession> > createClient(const std::string &service, bool autostart = true) {
      auto service_info = db_.find(service);
      if (service_info == db_.end()) {
        LOG_ERROR("Manager::create_client - Couldn't find service with name [%s]!", service.c_str());
        return std::shared_ptr<Client<CustomSession> >(nullptr); //Return nullptr if we cannot find the service
      }
      auto p = std::make_shared<Client<CustomSession>>(io_service_, service_info->second.ip, service_info->second.port);
      if (autostart == true) {
        p->start();
      }
      return p;
    }

    template<typename CustomSession>
    std::shared_ptr<Client<CustomSession> > createClient(const char* ip, const char* port, bool autostart = true) { 
      return createClient<CustomSession>(std::string(ip), std::string(port), autostart);
    }

    template<typename CustomSession>
    std::shared_ptr<Client<CustomSession> > createClient(const std::string &ip, const std::string& port, bool autostart = true) {
      auto p = std::make_shared<Client<CustomSession>>(io_service_, ip, port);
      if (autostart == true) {
        p->start();
      }
      return p;
    }

    template<typename CustomSession>
    std::shared_ptr<Server<CustomSession> > createServer(const char* service, bool autostart = true) { 
      return createServer<CustomSession>(std::string(service), autostart);
    }

    template<typename CustomSession>
    std::shared_ptr<Server<CustomSession> > createServer(const std::string &service, bool autostart = true) {
      auto service_info = db_.find(service);
      if (service_info == db_.end()) {
        LOG_ERROR("Manager::create_server - Couldn't find service with name [%s]!", service.c_str());
        return std::shared_ptr<Server<CustomSession> >(nullptr); //Return nullptr if we cannot find the service
      }
      auto p = std::make_shared<Server<CustomSession>>(io_service_, "", service_info->second.port);
      if (autostart == true) {
        p->start();
      }
      return p;
    }

    template<typename CustomSession>
    std::shared_ptr<Server<CustomSession> > createServer(const char* address, const char* port, bool autostart = true) { 
      return createServer<CustomSession>(std::string(address), std::string(port), autostart);
    }

    template<typename CustomSession>
    std::shared_ptr<Server<CustomSession> > createServer(const std::string &address, const std::string &port, bool autostart = true) {
      auto p = std::make_shared<Server<CustomSession>>(io_service_, address, port);
      if (autostart == true) {
        p->start();
      }
      return p;
    }

    void loadDB(const std::string &fname) {
      std::ifstream data(fname.c_str());
      if (data.is_open() == false) {
        LOG_ERROR("Manager::loadDB - The description file [%s] couldn't be opened!", fname.c_str());
        return;
      }

      std::string line;

      std::stringstream  lineStream(line);
      std::string        cell;

      unsigned int line_count = 0;

      while (data.eof() == false) {
        line_count++;
        std::getline(data,line);

        while (line[0] == '#') {  //Skip comment
          line_count++;
          std::getline(data,line);
        }

        lineStream.clear();
        lineStream.str(line);
        std::vector<std::string> fields;

        while (std::getline(lineStream,cell,' ')) {
          fields.emplace_back(cell);
        }

        if (fields.size() != 3) {
          LOG_ERROR("Manager::loadDB - The description file has more than three fields on line %u! Skipping!", line_count);
          continue;
        }

        DBEntry entry = {fields[0], fields[1], fields[2]};
        db_.emplace(fields[0], std::move(entry));
      }
    }

  private:
    struct DBEntry {
      std::string service_name;
      std::string ip;
      std::string port;
    };

    asio::io_service io_service_;
    std::unique_ptr<asio::io_service::work> work_;
    std::vector<std::thread> io_thread_pool_;
    std::map<std::string, DBEntry> db_;
};

}

#endif //IPC_MANAGER_H