// -*- C++ -*-
/*!
 * @file   RedisPubSub.cpp
 * @brief  Redis-based Publisher/Subscriber utility for J-PARC/MLF
 * @brief  RedisPubSub is a Redis-based Publisher/Subscriber utility for
           J-PARC/MLF. RedisPubSub is an utility with Publisher/Subscirber
           model based on Redis. The Redis is an open source (BSD licensed),
           in-memory data structure store, used as a database, cache and
           message broker.
           The RedisPubSub was created without using smart pointer, but
           the smart pointer technique was adopted by the recommendation from
           Hosoya-san and then the RedisPubSub was renewed.
 * @date   25-OCT-2017
 * @author Yoshiji Yasu (Yoshiji.Yasu@kek.jp)
 *
 */

#include "RedisPubSub.hh"

#include <iostream>
#include <csignal>
#include <cerrno>
//using std::cout;
//using std::cerr;
//using std::endl;

RedisPublisher::RedisPublisher()
  : m_host("localhost"), m_port(6379)
    //  : m_host("localhost"), m_port(6379), m_context(0)
{
}

RedisPublisher::RedisPublisher(const std::string& host, int32_t port)
  : m_host(host) , m_port(port)
    //  : m_host(host) , m_port(port), m_context(0)
{
}

RedisPublisher::~RedisPublisher()
{
}

int32_t RedisPublisher::init()
{
  // cntxt will be deleted in this scope and then cntxt will be copied to
  // m_context only when the connection is successfully completed.
  std::shared_ptr<redisContext> cntxt(
    redisConnect(m_host.c_str(), m_port),
      RedisContextDeleter()
  );
  if (!cntxt) {
    std::cerr << "RedisPublisher::init(): can't allocate redis context" << std::endl;
    return -1;
  }
  if (cntxt->err) {
    std::cerr << "RedisPublisher::init(): " << cntxt->errstr << std::endl;
    return -2;
  }
  m_context = cntxt;
  return 0;
}

int32_t RedisPublisher::publish(const std::string& key_name,
                                const char* src_buf,
                                int32_t size)
{
  if (m_context == 0) { // no context
    return -1;
  }
  std::unique_ptr<redisReply, RedisReplyObjectDeleter> reply(
    static_cast<redisReply*>(
      redisCommand(m_context.get(),
                   "PUBLISH %s %b",
                   key_name.c_str(),
                   src_buf,
                   size)
                  )
  );
  if (!reply) {
    std::cerr << "RedisPublisher::publish(): redisCommand was not executed correctly." << std::endl;
    return -2;
  } else if (reply->type == REDIS_REPLY_ERROR) {
    std::cerr << "RedisPublisher::publish(): redisCommand error, " <<reply->str << std::endl;
    return -3;
  }
  return reply->integer; // number of clients that received the message.
}

RedisSubscriber::RedisSubscriber() : m_host("localhost"), m_port(6379)
{
}

RedisSubscriber::RedisSubscriber(const std::string& host, int32_t port)
    : m_host(host), m_port(port)
{
}

void RedisSubscriber::setHost(const std::string& host, int32_t port)
{
  m_host = host;
  m_port = port;
}

RedisSubscriber::~RedisSubscriber()
{
}

RedisSyncSubscriber::RedisSyncSubscriber() : RedisSubscriber()
{
}

RedisSyncSubscriber::RedisSyncSubscriber(const std::string& host, int32_t port)
  : RedisSubscriber(host, port)
{
}

RedisSyncSubscriber::~RedisSyncSubscriber()
{
}

int32_t RedisSyncSubscriber::init()
{
  std::shared_ptr<redisContext> cntxt(
    redisConnect(m_host.c_str(), m_port),
      RedisContextDeleter()
  );
  if (!cntxt) {
    std::cerr << "RedisPublisher::init(): can't allocate redis context" << std::endl;
    return -1;
  }
  if (cntxt->err) {
    std::cerr << "RedisSyncSubscriber::init(): " << cntxt->errstr << std::endl;
    return -2;
  }
  m_context = cntxt;
  return 0;
}

int32_t RedisSyncSubscriber::subscribe(const std::string& key_name)
{
  if (m_context == 0) { // no context
    return -1;
  }
  std::unique_ptr<redisReply, RedisReplyObjectDeleter> reply(
    static_cast<redisReply*>(
      redisCommand(m_context.get(), "SUBSCRIBE %s", key_name.c_str()))
  );
  if (!reply) {
    std::cerr << "RedisSyncSubscriber::subscribe(): redisCommand was not executed correctly." << std::endl;
    return -2;
  } else if (reply->type == REDIS_REPLY_ERROR) {
    std::cerr << "RedisSyncSubscriber::subscribe(): redisCommand error, " << reply->str << std::endl;
    return -3;
  }
  return 0;
}

int32_t RedisSyncSubscriber::unsubscribe(const std::string& key_name)
{
  if (m_context == 0) { // no context
    return -1;
  }

  std::unique_ptr<redisReply, RedisReplyObjectDeleter> reply(
    static_cast<redisReply*>(
      redisCommand(m_context.get(), "UNSUBSCRIBE %s", key_name.c_str()))
  );
  if (!reply) {
    std::cerr << "RedisSyncSubscriber::unsubscribe(): redisCommand was not executed correctly." << std::endl;
    return -2;
  }
  else if (reply->type == REDIS_REPLY_ERROR) {
    std::cerr << "RedisSyncSubscriber::unsubscribe(): redisCommand error, " << reply->str << std::endl;
    return -3;
  }
  return 0;
}

int32_t RedisSyncSubscriber::get(std::string& key_name,
                             char* target_buf,
                             int32_t& size,
                             int32_t timeout)
{
  if (m_context == 0) { // no context
    return -1;
  }
  // set timeout
  struct timeval tv = { timeout / 1000, (timeout % 1000) * 1000 };
  if (redisSetTimeout(m_context.get(), tv) == REDIS_ERR) {
    std::cerr << "RedisSyncSubscriber::get(): redisSetTimeout() error ";
    std::cout << m_context->errstr << std::endl;
    return -2;
  }
  redisReply* unmanaged_reply = 0;
  int32_t status = redisGetReply(m_context.get(),
                                 reinterpret_cast<void**>(&unmanaged_reply));
  //                             static_cast<void**>(&unmanaged_reply));
  std::unique_ptr<redisReply, RedisReplyObjectDeleter> reply;
  if (unmanaged_reply) { //!< replace the object
    reply.reset(unmanaged_reply);
  }
  if (status == REDIS_ERR) {
    if (m_context->err == REDIS_ERR_IO && errno == EAGAIN) { // timeout
      m_context->err = 0;
      return 1;
    }
    std::cerr << "RedisSyncSubscriber::get(): " << m_context->errstr << std::endl;
    return -4;
  } else if (status == REDIS_OK) {
    if (!reply) {
      std::cerr << "RedisSyncSubscriber::get(): context of reply was null." << std::endl;
      return -3;
    }
    std::string result(reply->element[1]->str,
                       reply->element[1]->len);
    key_name = result;
    std::memcpy(target_buf,
                reply->element[2]->str,
                reply->element[2]->len);
    size = reply->element[2]->len;
  }
  return 0;
}

int32_t RedisSyncSubscriber::getChannels(std::string name, std::vector<std::string>& vec)
{
  if (m_context == 0) { // no context
    return -1;
  }
  std::unique_ptr <redisReply, RedisReplyObjectDeleter> reply(
    static_cast<redisReply*>(
      redisCommand(m_context.get(),
                   "PUBSUB CHANNELS %s",
                   name.c_str()
      )
    )
  );
  if (!reply) {
    std::cerr << "RedisSyncSubcriber::getChannels(): redisCommand was not executed correctly." << std::endl;
    return -2;
  } else if (reply->type == REDIS_REPLY_ERROR) {
    std::cerr << "RedisSyncSubcriber::getChannels(): redisCommand error, " <<reply->str << std::endl;
    return -3;
  }
  for (uint32_t i=0; i< reply->elements; i++) {
    std::string s(reply->element[i]->str, reply->element[i]->len);
    vec.push_back(s);
    //std::cout << v[i] << std::endl;
  }
  return 0;
}
