/*
 * Copyright (C) 2018 Freie Universität Berlin
 *
 * This file is subject to the terms and conditions of the GNU Lesser
 * General Public License v2.1. See the file LICENSE in the top level
 * directory for more details.
 */

/**
 * @defgroup    net_asymcute MQTT-SN Client (Asymcute)
 * @ingroup     net
 * @brief       Asymcute is an asynchronous MQTT-SN implementation
 *
 * # About
 * `Asymcute` is a asynchronous MQTT-SN client implementation, aiming at
 * providing the user a high degree of flexibility. It provides a flexible
 * interface that allows users to issue any number of concurrent requests to
 * one or more different gateways simultaneously.
 *
 * # Implementation state
 *
 * Implemented features:
 * - Connecting to multiple gateways simultaneously
 * - Registration of topic names
 * - Publishing of data (QoS 0 and QoS 1)
 * - Subscription to topics
 * - Pre-defined topic IDs as well as short and normal topic names
 *
 * Missing features:
 * - Gateway discovery process not implemented
 * - Last will feature not implemented
 * - No support for QoS level 2
 * - No support for wildcard characters in topic names when subscribing
 * - Actual granted QoS level on subscription is ignored
 *
 * @{
 * @file
 * @brief       Asymcute MQTT-SN interface definition
 *
 * @author      Hauke Petersen <hauke.petersen@fu-berlin.de>
 */

#ifndef NET_ASYMCUTE_H
#define NET_ASYMCUTE_H

#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>

#include "assert.h"
#include "event/timeout.h"
#include "event/callback.h"
#include "net/mqttsn.h"
#include "net/sock/udp.h"
#include "net/sock/util.h"

#ifdef __cplusplus
extern "C" {
#endif

#ifndef ASYMCUTE_BUFSIZE
/**
 * @brief   Default buffer size used for receive and request buffers
 */
#define ASYMCUTE_BUFSIZE            (128U)
#endif

#ifndef ASYMCUTE_HANDLER_PRIO
/**
 * @brief   Default priority for Asymcute's handler thread
 */
#define ASYMCUTE_HANDLER_PRIO       (THREAD_PRIORITY_MAIN - 2)
#endif

#ifndef ASYMCUTE_HANDLER_STACKSIZE
/**
 * @brief   Default stack size for Asymcute's handler thread
 */
#define ASYMCUTE_HANDLER_STACKSIZE  (THREAD_STACKSIZE_DEFAULT)
#endif

#ifndef ASYMCUTE_LISTENER_PRIO
/**
 * @brief   Default priority for an Asymcute listener thread
 *
 * @note    Must be of higher priority than @ref ASYMCUTE_HANDLER_PRIO
 */
#define ASYMCUTE_LISTENER_PRIO      (THREAD_PRIORITY_MAIN - 3)
#endif

#ifndef ASYMCUTE_LISTENER_STACKSIZE
/**
 * @brief   Default stack size for an Asymcute listener thread
 */
#define ASYMCUTE_LISTENER_STACKSIZE (THREAD_STACKSIZE_DEFAULT)
#endif

#ifndef ASYMCUTE_TOPIC_MAXLEN
/**
 * @brief   Maximum topic length
 *
 * @note    Must be less than (256 - 8) AND less than (ASYMCUTE_BUFSIZE - 8).
 */
#define ASYMCUTE_TOPIC_MAXLEN       (32U)
#endif

#ifndef ASYMCUTE_KEEPALIVE
/**
 * @brief   Keep alive interval [in s] communicated to the gateway
 *
 * For the default value, see spec v1.2, section 7.2 -> T_WAIT: > 5 min
 */
#define ASYMCUTE_KEEPALIVE          (360)       /* -> 6 min*/
#endif

#ifndef ASYMCUTE_KEEPALIVE_PING
/**
 * @brief   Interval to use for sending periodic ping messages
 *
 * The default behavior of this implementation is to send ping messages as soon
 * as three quarters of the keep alive interval have passed.
 *
 * @note    Must be less than ASYMCUTE_KEEPALIVE
 */
#define ASYMCUTE_KEEPALIVE_PING     ((ASYMCUTE_KEEPALIVE / 4) * 3)
#endif

#ifndef ASYMCUTE_T_RETRY
/**
 * @brief   Resend interval [in seconds]
 *
 * For the default value, see spec v1.2, section 7.2 -> T_RETRY: 10 to 15 sec
 */
#define ASYMCUTE_T_RETRY            (10U)       /* -> 10 sec */
#endif

#ifndef ASYMCUTE_N_RETRY
/**
 * @brief   Number of retransmissions until requests time out
 *
 * For the default value, see spec v1.2, section 7.2 -> N_RETRY: 3-5
 */
#define ASYMCUTE_N_RETRY            (3U)
#endif

/**
 * @brief   Return values used by public Asymcute functions
 */
enum {
    ASYMCUTE_OK         =  0,       /**< all is good */
    ASYMCUTE_OVERFLOW   = -1,       /**< error: insufficient buffer space */
    ASYMCUTE_GWERR      = -2,       /**< error: bad gateway connection state */
    ASYMCUTE_NOTSUP     = -3,       /**< error: feature not supported */
    ASYMCUTE_BUSY       = -4,       /**< error: context already in use */
    ASYMCUTE_REGERR     = -5,       /**< error: registration invalid */
    ASYMCUTE_SUBERR     = -6,       /**< error: subscription invalid */
};

/**
 * @brief   Possible event types passed to the event callback
 */
enum {
    ASYMCUTE_TIMEOUT,               /**< request timed out */
    ASYMCUTE_CANCELED,              /**< request was canceled */
    ASYMCUTE_REJECTED,              /**< request was rejected */
    ASYMCUTE_CONNECTED,             /**< connected to gateway */
    ASYMCUTE_DISCONNECTED,          /**< connection got disconnected */
    ASYMCUTE_REGISTERED,            /**< topic was registered */
    ASYMCUTE_PUBLISHED,             /**< data was published */
    ASYMCUTE_SUBSCRIBED,            /**< client was subscribed to topic */
    ASYMCUTE_UNSUBSCRIBED,          /**< client was unsubscribed from topic */
};

/**
 * @brief   Forward type declaration for connections contexts
 */
typedef struct asymcute_con asymcute_con_t;

/**
 * @brief   Forward type declaration for request contexts
 */
typedef struct asymcute_req asymcute_req_t;

/**
 * @brief   Forward type declaration for subscription contexts
 */
typedef struct asymcute_sub asymcute_sub_t;

/**
 * @brief   Forward type declaration for topic definitions
 */
typedef struct asymcute_topic asymcute_topic_t;

/**
 * @brief   Forward type declaration for last will definitions
 */
typedef struct asymcute_will asymcute_will_t;

/**
 * @brief   Event callback used for communicating connection and request related
 *          events to the user
 *
 * @param[in] req       pointer to the request context that triggered the event,
 *                      may be NULL of unsolicited events
 * @param[in] evt_type  type of the event
 */
typedef void(*asymcute_evt_cb_t)(asymcute_req_t *req, unsigned evt_type);

/**
 * @brief   Callback triggered on events for active subscriptions
 *
 * @param[in] sub       pointer to subscription context triggering this event
 * @param[in] evt_type  type of the event
 * @param[in] data      incoming data for PUBLISHED events, may be NULL
 * @param[in] len       length of @p data in bytes
 * @param[in] arg       user supplied argument
 */
typedef void(*asymcute_sub_cb_t)(const asymcute_sub_t *sub, unsigned evt_type,
                                 const void *data, size_t len, void *arg);

/**
 * @brief   Context specific timeout callback, only used internally
 *
 * @internal
 *
 * @param[in] con       connection context for this timeout
 * @param[in] req       request that timed out
 *
 * @return  Event type to communicate to the user
 */
typedef unsigned(*asymcute_to_cb_t)(asymcute_con_t *con, asymcute_req_t *req);

/**
 * @brief   Asymcute request context
 */
struct asymcute_req {
    mutex_t lock;                   /**< synchronization lock */
    struct asymcute_req *next;      /**< the requests list entry */
    asymcute_con_t *con;            /**< connection the request is using */
    asymcute_to_cb_t cb;            /**< internally used callback */
    void *arg;                      /**< internally used additional state */
    event_callback_t to_evt;        /**< timeout event */
    event_timeout_t to_timer;       /**< timeout timer */
    uint8_t data[ASYMCUTE_BUFSIZE]; /**< buffer holding the request's data */
    size_t data_len;                /**< length of the request packet in byte */
    uint16_t msg_id;                /**< used message id for this request */
    uint8_t retry_cnt;              /**< retransmission counter */
};

/**
 * @brief   Asymcute connection context
 */
struct asymcute_con {
    mutex_t lock;                       /**< synchronization lock */
    sock_udp_t sock;                    /**< socket used by a connections */
    sock_udp_ep_t server_ep;            /**< the gateway's UDP endpoint */
    asymcute_req_t *pending;            /**< list holding pending requests */
    asymcute_sub_t *subscriptions;      /**< list holding active subscriptions */
    asymcute_evt_cb_t user_cb;          /**< event callback provided by user */
    event_callback_t keepalive_evt;     /**< keep alive event */
    event_timeout_t keepalive_timer;    /**< keep alive timer */
    uint16_t last_id;                   /**< last used message ID for this
                                         *   connection */
    uint8_t keepalive_retry_cnt;        /**< keep alive transmission counter */
    uint8_t state;                      /**< connection state */
    uint8_t rxbuf[ASYMCUTE_BUFSIZE];    /**< connection specific receive buf */
    char cli_id[MQTTSN_CLI_ID_MAXLEN + 1];  /**< buffer to store client ID */
};

/**
 * @brief   Data-structure for holding topics and their registration status
 */
struct asymcute_topic {
    asymcute_con_t *con;        /**< connection used for registration */
    char name[ASYMCUTE_TOPIC_MAXLEN + 1];   /**< topic string (ACSII only) */
    uint8_t flags;              /**< normal, short, or pre-defined */
    uint16_t id;                /**< topic id */
};

/**
 * @brief   Data-structure holding the state of subscriptions
 */
struct asymcute_sub {
    asymcute_sub_t *next;       /**< the subscriptions list entry */
    asymcute_topic_t *topic;    /**< topic we subscribe to */
    asymcute_sub_cb_t cb;       /**< called on incoming data */
    void *arg;                  /**< user supplied callback argument */
};

/**
 * @brief   Data structure for defining a last will
 */
struct asymcute_will {
    const char *topic;          /**< last will topic */
    void *msg;                  /**< last will message content */
    size_t msg_len;             /**< length of last will message content */
};

/**
 * @brief   Check if a given request context is currently used
 *
 * @param[in] req       request context to check
 *
 * @return  true if context is currently used
 * @return  false if context is not used
 */
static inline bool asymcute_req_in_use(const asymcute_req_t *req)
{
    assert(req);
    return (req->con != NULL);
}

/**
 * @brief   Check if a given subscription is currently active
 *
 * @param[in] sub       subscription context to check
 *
 * @return  true if subscription is active
 * @return  false if subscription is not active
 */
static inline bool asymcute_sub_active(const asymcute_sub_t *sub)
{
    assert(sub);
    return (sub->topic != NULL);
}

/**
 * @brief   Reset the given topic
 *
 * @warning Make sure that the given topic is not used by any subscription or
 *          last will when calling this function
 *
 * @param[out] topic    topic to reset
 */
static inline void asymcute_topic_reset(asymcute_topic_t *topic)
{
    assert(topic);
    memset(topic, 0, sizeof(asymcute_topic_t));
}

/**
 * @brief   Check if a given topic is currently registered with a gateway
 *
 * @param[in] topic     topic to check
 *
 * @return  true if topic is registered
 * @return  false if topic is not registered
 */
static inline bool asymcute_topic_is_reg(const asymcute_topic_t *topic)
{
    assert(topic);
    return (topic->con != NULL);
}

/**
 * @brief   Check if a given topic is initialized
 *
 * @param[in] topic     topic to check
 *
 * @return  true if topic is initialized
 * @return  false if topic is not initialized
 */
static inline bool asymcute_topic_is_init(const asymcute_topic_t *topic)
{
    assert(topic);
    return (topic->name[0] != '\0');
}

/**
 * @brief   Compare two given topics and check if they are equal
 *
 * @param[in] a     topic A
 * @param[in] b     topic B
 *
 * @return  true if both topics are equal
 * @return  false if topics differ
 */
static inline bool asymcute_topic_equal(const asymcute_topic_t *a,
                                        const asymcute_topic_t *b)
{
    assert(a);
    assert(b);

    return ((a->flags == b->flags) && (a->id == b->id));
}

/**
 * @brief   Initialize the given topic
 *
 * @param[out] topic        topic to initialize
 * @param[in] topic_name    topic name (ASCII), may be NULL if topic should use
 *                          a pre-defined topic ID
 * @param[in] topic_id      pre-defined topic ID, or don't care if @p topic_name
 *                          is given
 *
 * @return  ASYMCUTE_OK on success
 * @return  ASYMCUTE_REGERR if topic is already registered
 * @return  ASYMCUTE_OVERFLOW if topic name does not fit into buffer or if pre-
 *          defined topic ID is invalid
 */
int asymcute_topic_init(asymcute_topic_t *topic, const char *topic_name,
                        uint16_t topic_id);

/**
 * @brief   Start a listener thread
 *
 * @note    Must have higher priority then the handler thread (defined by
 *          @ref ASYMCUTE_HANDLER_PRIO)
 *
 * @param[in] con       connection context to use for this connection
 * @param[in] stack     stack used to run the listener thread
 * @param[in] stacksize size of @p stack in bytes
 * @param[in] priority  priority of the listener thread created by this function
 * @param[in] callback  user callback for notification about connection related
 *                      events
 *
 * @return  ASYMCUTE_OK on success
 * @return  ASYMCUTE_BUSY if connection context is already in use
 */
int asymcute_listener_run(asymcute_con_t *con, char *stack, size_t stacksize,
                          char priority, asymcute_evt_cb_t callback);

/**
 * @brief   Start the global Asymcute handler thread for processing timeouts and
 *          keep alive events
 *
 * This function is typically called during system initialization.
 */
void asymcute_handler_run(void);

/**
 * @brief   Check if the given connection context is connected to a gateway
 *
 * @param[in] con       connection to check
 *
 * @return  true if context is connected
 * @return  false if not connected
 */
bool asymcute_is_connected(const asymcute_con_t *con);

/**
 * @brief   Connect to the given MQTT-SN gateway
 *
 * @param[in,out] con   connection to use
 * @param[in,out] req   request context to use for CONNECT procedure
 * @param[in] server    UDP endpoint of the target gateway
 * @param[in] cli_id    client ID to register with the gateway
 * @param[in] clean     set `true` to start a clean session
 * @param[in] will      last will (currently not implemented)
 *
 * @return  ASYMCUTE_OK if CONNECT message has been sent
 * @return  ASYMCUTE_NOTSUP if last will was given (temporary until implemented)
 * @return  ASYMCUTE_OVERFLOW if @p cli_id is larger than ASYMCUTE_ID_MAXLEN
 * @return  ASYMCUTE_GWERR if the connection is not in idle state
 * @return  ASYMCUTE_BUSY if the given request context is already in use
 */
int asymcute_connect(asymcute_con_t *con, asymcute_req_t *req,
                     sock_udp_ep_t *server, const char *cli_id, bool clean,
                     asymcute_will_t *will);

/**
 * @brief   Close the given connection
 *
 * @param[in,out] con   connection to close
 * @param[in,out] req   request context to use for DISCONNECT procedure
 *
 * @return  ASYMCUTE_OK if DISCONNECT message has been sent
 * @return  ASYMCUTE_GWERR if connection context is not connected
 * @return  ASYMCUTE_BUSY if the given request context is already in use
 */
int asymcute_disconnect(asymcute_con_t *con, asymcute_req_t *req);

/**
 * @brief   Register a given topic with the connected gateway
 *
 * @param[in] con       connection to use
 * @param[in,out] req   request context to use for REGISTER procedure
 * @param[in,out] topic topic to register
 *
 * @return  ASYMCUTE_OK if REGISTER message has been sent
 * @return  ASYMCUTE_REGERR if topic is already registered
 * @return  ASYMCUTE_GWERR if not connected to a gateway
 * @return  ASYMCUTE_BUSY if the given request context is already in use
 */
int asymcute_register(asymcute_con_t *con, asymcute_req_t *req,
                      asymcute_topic_t *topic);

/**
 * @brief   Publish the given data to the given topic
 *
 * @param[in] con       connection to use
 * @param[in,out] req   request context used for PUBLISH procedure
 * @param[in] topic     publish data to this topic
 * @param[in] data      actual payload to send
 * @param[in] data_len  size of @p data in bytes
 * @param[in] flags     additional flags (QoS level, DUP, and RETAIN)
 *
 * @return  ASYMCUTE_OK if PUBLISH message has been sent
 * @return  ASYMCUTE_NOTSUP if unsupported flags have been set
 * @return  ASYMCUTE_OVERFLOW if data does not fit into transmit buffer
 * @return  ASYMCUTE_REGERR if given topic is not registered
 * @return  ASYMCUTE_GWERR if not connected to a gateway
 * @return  ASYMCUTE_BUSY if the given request context is already in use
 */
int asymcute_publish(asymcute_con_t *con, asymcute_req_t *req,
                     const asymcute_topic_t *topic,
                     const void *data, size_t data_len, uint8_t flags);

/**
 * @brief   Subscribe to a given topic
 *
 * @param[in] con       connection to use
 * @param[in,out] req   request context used for SUBSCRIBE procedure
 * @param[out] sub      subscription context to store subscription state
 * @param[in,out] topic topic to subscribe to, must be initialized (see
 *                      asymcute_topic_init())
 * @param[in] callback  user callback triggered on events for this subscription
 * @param[in] arg       user supplied argument passed to the event callback
 * @param[in] flags     additional flags (QoS level and DUP)
 *
 * @return  ASYMCUTE_OK if SUBSCRIBE message has been sent
 * @return  ASYMCUTE_NOTSUP if invalid or unsupported flags have been set
 * @return  ASYMCUTE_REGERR if topic is not initialized
 * @return  ASYMCUTE_GWERR if not connected to a gateway
 * @return  ASYMCUTE_SUBERR if already subscribed to the given topic
 * @return  ASYMCUTE_BUSY if the given request context is already in use
 */
int asymcute_subscribe(asymcute_con_t *con, asymcute_req_t *req,
                       asymcute_sub_t *sub, asymcute_topic_t *topic,
                       asymcute_sub_cb_t callback, void *arg, uint8_t flags);

/**
 * @brief   Cancel an active subscription
 *
 * @param[in] con       connection to use
 * @param[in,out] req   request context used for UNSUBSCRIBE procedure
 * @param[in,out] sub   subscription to cancel
 *
 * @return  ASYMCUTE_OK if UNSUBSCRIBE message has been sent
 * @return  ASYMCUTE_SUBERR if subscription is not currently active
 * @return  ASYMCUTE_GWERR if not connected to a gateway
 * @return  ASYMCUTE_BUSY if the given request context is already in use
 */
int asymcute_unsubscribe(asymcute_con_t *con, asymcute_req_t *req,
                         asymcute_sub_t *sub);

#ifdef __cplusplus
}
#endif

#endif /* NET_ASYMCUTE_H */
/** @} */