/*
 * Written by Masayuki Takigahira
 * Copyright 2012 Kyoto University and Honda Motor Co.,Ltd.
 * All rights reserved.
 * HARK was developed by researchers in Okuno Laboratory
 * at the Kyoto University and Honda Research Institute Japan Co.,Ltd.
 */

#ifndef HARK_MQTT_BASE_HPP
#define HARK_MQTT_BASE_HPP

#include <iostream>
#include <sstream>
#include <string>
#include <vector>
#include <queue>

#include <iomanip>

#include <cstring>

#include <mosquittopp.h>

namespace HARK_MQTT {
  class mqtt_publisher : public mosqpp::mosquittopp {
  private:
    std::string topic;
    std::string host;
    int port;
    int keepalive;
    int life;
    bool is_debug;
    std::string username;
    std::string password;
    std::string keyfile_password;
    bool mqtt_v31;
    bool connect_valid;

  static int pw_callback(char *buf, int size, int rwflag, void *userdata){
    //strncpy(buf, keyfile_password.c_str(), size); buf[size - 1] = 0;
    return (int) strlen(buf);
  }
  
  public:
    // Constructors [legacy compatible, MQTTv31 user/pass auth, TLS cert auth, full support(base)]
    mqtt_publisher(const char *_id, const std::string _topic, const std::string _host, int _port=1883,
        int _keepalive=60, bool _debug=false):
    mqtt_publisher(_id, _topic, _host, _port, "", "", _keepalive, true, _debug){ }
    mqtt_publisher(const char *_id, const std::string _topic, const std::string _host, int _port=1883,
        const std::string _username="", const std::string _password="",
        int _keepalive=60, bool _mqtt_v31=true, bool _debug=false):
    mqtt_publisher(_id, _topic, _host, _port, _username, _password, "", _keepalive, _mqtt_v31, _debug){ }
    mqtt_publisher(const char *_id, const std::string _topic, const std::string _host, int _port=8883,
        const std::string _username="", const std::string _password="", const std::string _cafile="",
        int _keepalive=60, bool _mqtt_v31=true, bool _debug=false):
    mqtt_publisher(_id, _topic, _host, _port, _username, _password, _cafile, "", "", "", false, false, "", _keepalive, _mqtt_v31, _debug){ }
    mqtt_publisher(const char * _id, const std::string _topic, const std::string _host, int _port=8883,
        const std::string _username="", const std::string _password="",
        const std::string _cafile="", const std::string _capath="",
        const std::string _certfile="", const std::string _keyfile="",
        bool insecure=true, bool cert_reqs=false, const std::string tls_version="",
        int _keepalive=60, bool _mqtt_v31=true, bool _debug=false):
    mosquittopp(_id, true){

      int ret, i;
      
      topic     = _topic;
      host      = _host;
      username  = _username;
      password  = _password;
      port      = _port;
      keepalive = _keepalive;
      mqtt_v31  = _mqtt_v31;
      is_debug  = _debug;
      std::string cafile = _cafile;
      std::string capath = _capath;
      std::string certfile = _certfile;
      std::string keyfile = _keyfile;
      
      if(is_debug){
        std::cerr << "Constructing mqtt_publisher." << std::endl;
        std::cerr << "lib_init()..." << std::endl;
      }
      mosqpp::lib_init();
      
      if(is_debug){
        std::cerr << "connect(host=" << host << ", port=" << port << ", keepalive=" << keepalive << ")... [default topic=" << topic << "]" << std::endl;
      }
      if(mqtt_v31 && username.length() > 0){
        if(password.length() > 0){
          username_pw_set(username.c_str(), password.c_str());
        }
        else{
          username_pw_set(username.c_str(), NULL);
        }
      }
      if((cafile.length() > 0) || (capath.length() > 0) || (certfile.length() > 0) || (keyfile.length() > 0)){
        ret = tls_set(
          (cafile.length() > 0) ? cafile.c_str() : NULL,
          (capath.length() > 0) ? capath.c_str() : NULL,
          (certfile.length() > 0) ? certfile.c_str() : NULL,
          (keyfile.length() > 0) ? keyfile.c_str() : NULL,
          pw_callback
        );
        if(is_debug){
          switch(ret){
          case MOSQ_ERR_SUCCESS:
            std::cerr << "Set TLS parameter(s)" << std::endl;
            break;
          case MOSQ_ERR_INVAL:
            std::cerr << "Cannot set TLS parameter(s), Please check certificate file path" << std::endl;
            break;
          case MOSQ_ERR_NOMEM:
            std::cerr << "Cannot set TLS parameter(s), Not enough memory" << std::endl;
            break;
          default:
            std::cerr << "Unknown error occured in tls_set()" << std::endl;
            break;
          }
        }
        if(insecure){
          ret = tls_insecure_set(true);
          switch(ret){
          case MOSQ_ERR_SUCCESS:
            std::cerr << "[CAUTION] was changed to disable hostname check features in server certificate, Don't use this feature other than test purpose" << std::endl;
            break;
          case MOSQ_ERR_INVAL:
            std::cerr << "Cannot change to disable hostname check features in server certificate" << std::endl;
            break;
          default:
            std::cerr << "Unknown error occured in tls_insecure_set()" << std::endl;
            break;
          }
        }
        if((tls_version.length() >= 5) && (strncmp(tls_version.c_str(), "tlsv1", 5) == 0)){
          if(tls_version.length() >= 7){
            if(strncmp(tls_version.c_str(), "tlsv1.2", 7) == 0){
              ret = tls_opts_set((cert_reqs > 0) ? 1 : 0, "tlsv1.2", NULL);
            }
            else if(strncmp(tls_version.c_str(), "tlsv1.1", 7) == 0){
              ret = tls_opts_set((cert_reqs > 0) ? 1 : 0, "tlsv1.1", NULL);
            }
            else if(strncmp(tls_version.c_str(), "tlsv1.0", 7) == 0){
              ret = tls_opts_set((cert_reqs > 0) ? 1 : 0, "tlsv1", NULL);
            }
            else{
              std::cerr << "Unsupported TLS version (" << tls_version << ")" << std::endl;
              ret = MOSQ_ERR_SUCCESS;
            }
          }
          else{
            if(tls_version.length() == 5){
              ret = tls_opts_set((cert_reqs > 0) ? 1 : 0, "tlsv1", NULL);
            }
            else{
              std::cerr << "Unsupported TLS version (" << tls_version << ")" << std::endl;
              ret = MOSQ_ERR_SUCCESS;
            }
          }
          switch(ret){
          case MOSQ_ERR_SUCCESS:
            std::cerr << "Set TLS version (" << tls_version << ")" << std::endl;
            break;
          case MOSQ_ERR_INVAL:
            std::cerr << "Cannot set TLS version (" << tls_version << ")" << std::endl;
            break;
          case MOSQ_ERR_NOMEM:
            std::cerr << "Cannot set TLS version, Not enough memory" << std::endl;
            break;
          default:
            break;
          }
        }
      }
      life = 0;
      i = 5;
      do{
        ret = connect(host.c_str(), port, keepalive);
        i--;
      } while(ret != MOSQ_ERR_SUCCESS && i >= 0);
      if(ret == MOSQ_ERR_SUCCESS){
        connect_valid = true;
        loop_start(); // replace from loop() to loop_start(). I think no problem on publisher.
        max_inflight_messages_set(0);
      }
      else{
        connect_valid = false;
      }
    }
    
    ~mqtt_publisher(){
      if(is_debug){
        std::cerr << "life:" << life << std::endl;
        do{
          life--;
        } while(life > 0);
        std::cerr << "life:" << life << std::endl;
      }
      else{
        life=0;
      }
      
      if(is_debug){
        std::cerr << "Destructing mqtt_publisher. " << std::endl;
        std::cerr << "disconnect() ..." << std::endl;
      }
      disconnect();
      
      if(is_debug){ std::cerr << "lib_cleanup()..." << std::endl; }
      mosqpp::lib_cleanup();
      
      if(is_debug){ std::cerr << "done." << std::endl; }
      max_inflight_messages_set(0);
    }
    
    // setter
    bool send_message(std::string _topic, std::string _message, int _retry, int _qos, int _count);
    bool send_message(std::string _message, int _qos, int _count); // for regacy implement compatibility
    
    bool send_message(char *_topic, int _topic_length, char *_message, int _message_length, int _retry, int _qos, int _count);
    bool send_message(char *_message, int _message_length, int _qos, int _count); // for regacy implement compatibility
    
    int get_life();
    /*
    void request_loop();
    */
    // callbacks
    void on_publish(int mid);
    void on_connect(int rc);
    void on_disconnect(int rc);
  }; // class mqtt_publisher end

  class mqtt_subscriber : public mosqpp::mosquittopp {
  private:
    std::string topic;
    std::string host;
    int port;
    int keepalive;
    bool is_debug;
    std::string username;
    std::string password;
    bool mqtt_v31;
    bool connect_valid;
    int life;
    
    std::queue<std::string> message_buffer;

  public:
    mqtt_subscriber(const char *_id, const std::string _topic, const std::string _host, int _port=1883,
        int _keepalive=60, bool _debug=false):
    mqtt_subscriber(_id, _topic, _host, _port, "", "", _keepalive, true, _debug){ }
    mqtt_subscriber(const char *_id, const std::string _topic, const std::string _host, int _port=1883,
        const std::string _username="", const std::string _password="",
        int _keepalive=60, bool _mqtt_v31=true, bool _debug=false):
    mqtt_subscriber(_id, _topic, _host, _port, _username, _password, "", _keepalive, true, _debug){ }
    mqtt_subscriber(const char *_id, const std::string _topic, const std::string _host, int _port=8883,
        const std::string _username="", const std::string _password="", const std::string _cafile="",
        int _keepalive=60, bool _mqtt_v31=true, bool _debug=false):
    mosquittopp(_id, true){
      int ret, i;
      
      topic     = _topic;
      host      = _host;
      username  = _username;
      password  = _password;
      port      = _port;
      keepalive = _keepalive;
      mqtt_v31  = _mqtt_v31;
      is_debug  = _debug;
      std::string cafile = _cafile;

      if(is_debug){
        std::cerr << "Constructing mqtt_subscriber. " << std::endl;
        std::cerr << "lib_init()..." << std::endl;
      }
      mosqpp::lib_init();
      
      if(is_debug){
        std::cerr << "connect(host=" << host << ", port=" << port << ", keepalive=" << keepalive << ")... [default topic=" << topic << "]" << std::endl;
      }
      if(mqtt_v31 && username != ""){
        if(password != ""){
          username_pw_set(username.c_str(), password.c_str());
        }
        else{
          username_pw_set(username.c_str(), NULL);
        }
      }
      if(cafile.length()){
        ret = tls_set(cafile.c_str());
        if(is_debug){
          switch(ret){
          case MOSQ_ERR_SUCCESS:
            std::cerr << "Set TLS parameter(s)" << std::endl;
            break;
          case MOSQ_ERR_INVAL:
            std::cerr << "Cannot set TLS parameter(s), Please check certificate file path" << std::endl;
            break;
          case MOSQ_ERR_NOMEM:
            std::cerr << "Cannot set TLS parameter(s), Not enough memory" << std::endl;
            break;
          default:
            std::cerr << "Unknown error occured in tls_set()" << std::endl;
            break;
          }
        }
      }
      life = 0;
      i = 5;
      do{
        ret = connect(host.c_str(), port, keepalive);
        i--;
      } while(ret != MOSQ_ERR_SUCCESS && i >= 0);
      if(ret == MOSQ_ERR_SUCCESS){
        connect_valid = true;
        loop_start(); // replace from loop() to loop_start(). I think no problem on subscriber.
        max_inflight_messages_set(0);
      }
      else{
        connect_valid = false;
      }
    }

    bool is_connected(){ return connect_valid; }

    bool has_data(){ return !message_buffer.empty(); }

    ~mqtt_subscriber(){
      if(is_debug){
        std::cerr << "Destructing mqtt_subscriber. " << std::endl;
      }
      loop_stop(true);
      mosqpp::lib_cleanup();
      int discarded_count=0; while(!message_buffer.empty()){ message_buffer.pop(); discarded_count++; } std::cerr << "discarded_count=" << discarded_count << std::endl; // buffer clear
    }
    
    void on_connect(int rc);
    void on_disconnect(int rc);
    
    // getter
    std::string receive_message();
    
    const char * receive_char_message();
    
    // callbacks
    void on_message(const struct mosquitto_message *msg);
    void on_subscribe(int mid, int qos_count, const int *granted_qos);
  }; // class mqtt_subscriber end
} // namespace HARK_MQTT end

#endif // HARK_MQTT_BASE_HPP

