/* (C) 2011 by Pablo Neira Ayuso <pablo@gnumonks.org>
 * (C) 2017 by Harald Welte <laforge@gnumonks.org>
 * All Rights Reserved.
 *
 * SPDX-License-Identifier: GPL-2.0+
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <time.h>
#include <sys/fcntl.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <arpa/inet.h>
#include <netinet/in.h>

#include <osmocom/core/linuxlist.h>
#include <osmocom/core/select.h>
#include <osmocom/core/utils.h>
#include <osmocom/gsm/tlv.h>
#include <osmocom/core/msgb.h>
#include <osmocom/core/logging.h>
#include <osmocom/core/talloc.h>
#include <osmocom/core/socket.h>

#include <osmocom/netif/datagram.h>

#define OSMO_DGRAM_CLI_F_RECONF	(1 << 0)

#define OSMO_DGRAM_RX_F_RECONF	(1 << 0)

/*! \addtogroup datagram
 *  @{
 */

/*! \file datagram.c
 *  \brief Osmocom datagram socket helpers
 */


/*
 * Client side.
 */

struct osmo_dgram_tx {
	struct osmo_fd			ofd;
	struct llist_head		tx_queue;
	char				*addr;
	uint16_t			port;
	char				*local_addr;
	uint16_t			local_port;
	int (*write_cb)(struct osmo_dgram_tx *conn);
	void				*data;
	unsigned int			flags;
};

/*! \brief Close an Osmocom Datagram Transmitter
 *  \param[in] conn Osmocom Datagram Transmitter to be closed
 *  We unregister the socket fd from the osmocom select() loop
 *  abstraction and close the socket */
void osmo_dgram_tx_close(struct osmo_dgram_tx *conn)
{
	if (conn->ofd.fd == -1)
		return;
	osmo_fd_unregister(&conn->ofd);
	close(conn->ofd.fd);
	conn->ofd.fd = -1;
}

static int osmo_dgram_tx_write(struct osmo_dgram_tx *conn)
{
	struct msgb *msg;
	struct llist_head *lh;
	int ret;

	LOGP(DLINP, LOGL_DEBUG, "sending data\n");

	if (llist_empty(&conn->tx_queue)) {
		osmo_fd_write_disable(&conn->ofd);
		return 0;
	}
	lh = conn->tx_queue.next;
	llist_del(lh);
	msg = llist_entry(lh, struct msgb, list);

	ret = send(conn->ofd.fd, msg->data, msg->len, 0);
	if (ret < 0) {
		LOGP(DLINP, LOGL_ERROR, "error to send (%s)\n",
			strerror(errno));
	}
	msgb_free(msg);
	return 0;
}

static int osmo_dgram_tx_fd_cb(struct osmo_fd *ofd, unsigned int what)
{
	struct osmo_dgram_tx *conn = ofd->data;

	if (what & OSMO_FD_WRITE) {
		LOGP(DLINP, LOGL_DEBUG, "write\n");
		osmo_dgram_tx_write(conn);
	}
	return 0;
}

/*! \brief Create an Osmocom datagram transmitter
 *  \param[in] ctx talloc context from which to allocate memory
 *  This function allocates a new \ref osmo_dgram_tx and initializes
 *  it with default values
 *  \returns Osmocom Datagram Transmitter; NULL on error */
struct osmo_dgram_tx *osmo_dgram_tx_create(void *ctx)
{
	struct osmo_dgram_tx *conn;

	conn = talloc_zero(ctx, struct osmo_dgram_tx);
	if (!conn)
		return NULL;

	osmo_fd_setup(&conn->ofd, -1, OSMO_FD_READ, osmo_dgram_tx_fd_cb, conn, 0);
	INIT_LLIST_HEAD(&conn->tx_queue);

	return conn;
}


/*! \brief Set the remote address to which we transmit
 *  \param[in] conn Datagram Transmitter to modify
 *  \param[in] addr Remote IP address */
void
osmo_dgram_tx_set_addr(struct osmo_dgram_tx *conn,
				const char *addr)
{
	osmo_talloc_replace_string(conn, &conn->addr, addr);
	conn->flags |= OSMO_DGRAM_CLI_F_RECONF;
}

/*! \brief Set the remote port to which we transmit
 *  \param[in] conn Datagram Transmitter to modify
 *  \param[in] port Remote Port Number */
void
osmo_dgram_tx_set_port(struct osmo_dgram_tx *conn,
				uint16_t port)
{
	conn->port = port;
	conn->flags |= OSMO_DGRAM_CLI_F_RECONF;
}

/*! \brief Set the local address from which we transmit
 *  \param[in] conn Datagram Transmitter to modify
 *  \param[in] addr Local IP address */
void
osmo_dgram_tx_set_local_addr(struct osmo_dgram_tx *conn, const char *addr)
{
	osmo_talloc_replace_string(conn, &conn->local_addr, addr);
	conn->flags |= OSMO_DGRAM_CLI_F_RECONF;
}

/*! \brief Set the local port from which we transmit
 *  \param[in] conn Datagram Transmitter to modify
 *  \param[in] port Local Port Number */
void
osmo_dgram_tx_set_local_port(struct osmo_dgram_tx *conn, uint16_t port)
{
	conn->local_port = port;
	conn->flags |= OSMO_DGRAM_CLI_F_RECONF;
}

/*! \brief Set application private data of the datagram transmitter
 *  \param[in] conn Datagram Transmitter to modify
 *  \param[in] data User-specific data (available in call-back functions) */
void
osmo_dgram_tx_set_data(struct osmo_dgram_tx *conn, void *data)
{
	conn->data = data;
}

/*! \brief Destroy a Osmocom datagram transmitter
 *  \param[in] conn Datagram Transmitter to destroy */
void osmo_dgram_tx_destroy(struct osmo_dgram_tx *conn)
{
	osmo_dgram_tx_close(conn);
	talloc_free(conn);
}

/*! \brief Open connection of an Osmocom datagram transmitter
 *  \param[in] conn Stream Client to connect
 *  \returns 0 on success; negative in case of error */
int osmo_dgram_tx_open(struct osmo_dgram_tx *conn)
{
	int ret;

	/* we are reconfiguring this socket, close existing first. */
	if ((conn->flags & OSMO_DGRAM_CLI_F_RECONF) && conn->ofd.fd >= 0)
		osmo_dgram_tx_close(conn);

	conn->flags &= ~OSMO_DGRAM_CLI_F_RECONF;

	ret = osmo_sock_init2(AF_INET, SOCK_DGRAM, IPPROTO_UDP,
			      conn->local_addr, conn->local_port, conn->addr, conn->port,
			      OSMO_SOCK_F_BIND|OSMO_SOCK_F_CONNECT|OSMO_SOCK_F_NONBLOCK);
	if (ret < 0)
		return ret;

	conn->ofd.fd = ret;
	if (osmo_fd_register(&conn->ofd) < 0) {
		close(ret);
		conn->ofd.fd = -1;
		return -EIO;
	}
	return 0;
}

/*! \brief Enqueue data to be sent via an Osmocom datagram transmitter
 *  \param[in] conn Datagram Transmitter through which we want to send
 *  \param[in] msg Message buffer to enqueue in transmit queue */
void osmo_dgram_tx_send(struct osmo_dgram_tx *conn,
				 struct msgb *msg)
{
	msgb_enqueue(&conn->tx_queue, msg);
	osmo_fd_write_enable(&conn->ofd);
}

/*
 * Server side.
 */

struct osmo_dgram_rx {
	struct osmo_fd		ofd;
	char			*addr;
	uint16_t		port;
	int (*cb)(struct osmo_dgram_rx *conn);
	void			*data;
	unsigned int		flags;
};

/*! \brief Receive data via Osmocom datagram receiver
 *  \param[in] conn Datagram Receiver from which to receive
 *  \param msg pre-allocate message buffer to which received data is appended
 *  \returns number of bytes read (might be 0 for empty UDP packet), negative on error. */
int osmo_dgram_rx_recv(struct osmo_dgram_rx *conn,
				struct msgb *msg)
{
	int ret;

	ret = recv(conn->ofd.fd, msg->data, msg->data_len, 0);
	if (ret < 0) {
		LOGP(DLINP, LOGL_ERROR, "error receiving data from tx: %s\n", strerror(errno));
		return ret;
	}
	msgb_put(msg, ret);
	LOGP(DLINP, LOGL_DEBUG, "received %d bytes from tx\n", ret);
	return ret;
}

static void osmo_dgram_rx_read(struct osmo_dgram_rx *conn)
{
	LOGP(DLINP, LOGL_DEBUG, "message received\n");

	if (conn->cb)
		conn->cb(conn);
}

static int osmo_dgram_rx_cb(struct osmo_fd *ofd, unsigned int what)
{
	struct osmo_dgram_rx *conn = ofd->data;

	LOGP(DLINP, LOGL_DEBUG, "read\n");
	if (what & OSMO_FD_READ)
		osmo_dgram_rx_read(conn);

	return 0;
}

/*! \brief Create an Osmocom datagram receiver
 *  \param[in] ctx talloc context from which to allocate memory
 *  This function allocates a new \ref osmo_dgram_rx and initializes
 *  it with default values
 *  \returns Datagram Receiver; NULL on error */
struct osmo_dgram_rx *osmo_dgram_rx_create(void *ctx)
{
	struct osmo_dgram_rx *conn;

	conn = talloc_zero(ctx, struct osmo_dgram_rx);
	if (!conn)
		return NULL;

	osmo_fd_setup(&conn->ofd, -1, OSMO_FD_READ, osmo_dgram_rx_cb, conn, 0);

	return conn;
}

/*! \brief Set the local address to which we bind
 *  \param[in] conn Datagram Receiver to modify
 *  \param[in] addr Local IP address */
void osmo_dgram_rx_set_addr(struct osmo_dgram_rx *conn,
				     const char *addr)
{
	osmo_talloc_replace_string(conn, &conn->addr, addr);
	conn->flags |= OSMO_DGRAM_RX_F_RECONF;
}

/*! \brief Set the local port to which we bind
 *  \param[in] conn Datagram Receiver to modify
 *  \param[in] port Local port number */
void osmo_dgram_rx_set_port(struct osmo_dgram_rx *conn,
				     uint16_t port)
{
	conn->port = port;
	conn->flags |= OSMO_DGRAM_RX_F_RECONF;
}

/*! \brief Set the read() call-back of the datagram receiver
 *  \param[in] conn Datagram Receiver to modify
 *  \param[in] read_cb Call-back function executed after read() */
void osmo_dgram_rx_set_read_cb(struct osmo_dgram_rx *conn,
	int (*read_cb)(struct osmo_dgram_rx *conn))
{
	conn->cb = read_cb;
}

/*! \brief Destroy the datagram receiver. Releases Memory.
 *  \param[in] conn Datagram Receiver */
void osmo_dgram_rx_destroy(struct osmo_dgram_rx *conn)
{
	osmo_dgram_rx_close(conn);
	talloc_free(conn);
}

/*! \brief Open the datagram receiver.  This actually initializes the
 *  underlying socket and binds it to the configured ip/port
 *  \param[in] conn Datagram Receiver to open */
int osmo_dgram_rx_open(struct osmo_dgram_rx *conn)
{
	int ret;

	/* we are reconfiguring this socket, close existing first. */
	if ((conn->flags & OSMO_DGRAM_RX_F_RECONF) && conn->ofd.fd >= 0)
		osmo_dgram_rx_close(conn);

	conn->flags &= ~OSMO_DGRAM_RX_F_RECONF;

	ret = osmo_sock_init(AF_INET, SOCK_DGRAM, IPPROTO_UDP,
			     conn->addr, conn->port, OSMO_SOCK_F_BIND);
	if (ret < 0)
		return ret;

	conn->ofd.fd = ret;
	if (osmo_fd_register(&conn->ofd) < 0) {
		close(ret);
		conn->ofd.fd = -1;
		return -EIO;
	}
	return 0;
}


/*! \brief Close the datagram receiver and unregister from select loop
 *  Does not destroy the datagram receiver, merely closes it!
 *  \param[in] conn Stream Server Link to close */
void osmo_dgram_rx_close(struct osmo_dgram_rx *conn)
{
	if (conn->ofd.fd == -1)
		return;
	osmo_fd_unregister(&conn->ofd);
	close(conn->ofd.fd);
	conn->ofd.fd = -1;
}

/*
 * Client+Server (bidirectional communications).
 */

struct osmo_dgram {
	struct osmo_dgram_rx	*rx;
	struct osmo_dgram_tx	*tx;
	int (*read_cb)(struct osmo_dgram *conn);
	void				*data;
};

static int
dgram_rx_cb(struct osmo_dgram_rx *rx)
{
	struct osmo_dgram *conn = rx->data;

	if (conn->read_cb)
		return conn->read_cb(conn);

	return 0;
}


/*! \brief Create an Osmocom datagram transceiver (bidirectional)
 *  \param[in] ctx talloc context from which to allocate memory
 *  This function allocates a new \ref osmo_dgram and initializes
 *  it with default values.  Internally, the Transceiver is based on a
 *  tuple of transmitter (\ref osmo_dgram_tx) and receiver (\ref osmo_dgram_rx)
 *  \returns Osmocom Datagram Transceiver; NULL on error */
struct osmo_dgram *osmo_dgram_create(void *ctx)
{
	struct osmo_dgram *conn;

	conn = talloc_zero(ctx, struct osmo_dgram);
	if (!conn)
		return NULL;

	conn->rx= osmo_dgram_rx_create(ctx);
	if (conn->rx == NULL)
		return NULL;

	osmo_dgram_rx_set_read_cb(conn->rx, dgram_rx_cb);
	conn->rx->data = conn;

	conn->tx = osmo_dgram_tx_create(ctx);
	if (conn->tx == NULL) {
		osmo_dgram_rx_destroy(conn->rx);
		return NULL;
	}

	return conn;
}

/*! \brief Destroy a Osmocom datagram transceiver
 *  \param[in] conn Datagram Transceiver to destroy */
void osmo_dgram_destroy(struct osmo_dgram *conn)
{
	osmo_dgram_rx_destroy(conn->rx);
	osmo_dgram_tx_destroy(conn->tx);
}

/*! \brief Set the local address to which we bind
 *  \param[in] conn Datagram Transceiver to modify
 *  \param[in] addr Local IP address */
void
osmo_dgram_set_local_addr(struct osmo_dgram *conn, const char *addr)
{
	osmo_dgram_rx_set_addr(conn->rx, addr);
}

/*! \brief Set the remote address to which we transmit/connect
 *  \param[in] conn Datagram Transceiver to modify
 *  \param[in] addr Remote IP address */
void
osmo_dgram_set_remote_addr(struct osmo_dgram *conn, const char *addr)
{
	osmo_dgram_tx_set_addr(conn->tx, addr);
}

/*! \brief Set the local port to which we bind
 *  \param[in] conn Datagram Transceiver to modify
 *  \param[in] port Local Port Number */
void
osmo_dgram_set_local_port(struct osmo_dgram *conn, uint16_t port)
{
	osmo_dgram_rx_set_port(conn->rx, port);
}

/*! \brief Set the remote port to which we transmit
 *  \param[in] conn Datagram Transceiver to modify
 *  \param[in] port Remote Port Number */
void
osmo_dgram_set_remote_port(struct osmo_dgram *conn, uint16_t port)
{
	osmo_dgram_tx_set_port(conn->tx, port);
}

/*! \brief Set the read() call-back of the datagram receiver
 *  \param[in] conn Datagram Receiver to modify
 *  \param[in] read_cb Call-back function executed after read() */
void osmo_dgram_set_read_cb(struct osmo_dgram *conn,
			    int (*read_cb)(struct osmo_dgram *conn))
{
	conn->read_cb = read_cb;
}

/*! \brief Set application private data of the datagram transmitter
 *  \param[in] conn Datagram Transmitter to modify
 *  \param[in] data User-specific data (available in call-back functions) */
void osmo_dgram_set_data(struct osmo_dgram *conn, void *data)
{
	conn->data = data;
}

/*! \brief Get application private data of the datagram transceiver
 *  \param[in] conn Datagram Transceiver
 *  \returns Application private data, as set by \ref osmo_dgram_set_data() */
void *osmo_dgram_get_data(struct osmo_dgram *conn)
{
	return conn->data;
}

/*! \brief Open the datagram transceiver.  This actually initializes the
 * underlying sockets and binds/connects them to the configured ips/ports
 *  \param[in] conn Datagram Transceiver to open */
int osmo_dgram_open(struct osmo_dgram *conn)
{
	int ret;

	ret = osmo_dgram_rx_open(conn->rx);
	if (ret < 0)
		return ret;

	ret = osmo_dgram_tx_open(conn->tx);
	if (ret < 0) {
		osmo_dgram_rx_close(conn->rx);
		return ret;
	}
	return ret;
}

/*! \brief Close an Osmocom Datagram Transceiver
 *  \param[in] conn Osmocom Datagram Transceiver to be closed
 *  We unregister the socket fds from the osmocom select() loop
 *  and close them. */
void osmo_dgram_close(struct osmo_dgram *conn)
{
	osmo_dgram_rx_close(conn->rx);
	osmo_dgram_tx_close(conn->tx);
}

/*! \brief Enqueue data to be sent via an Osmocom datagram transceiver
 *  \param[in] conn Datagram Transceiver through which we want to send
 *  \param[in] msg Message buffer to enqueue in transmit queue */
void osmo_dgram_send(struct osmo_dgram *conn, struct msgb *msg)
{
	osmo_dgram_tx_send(conn->tx, msg);
}

/*! \brief Receive data via Osmocom datagram transceiver
 *  \param[in] conn Datagram Transceiver from which to receive
 *  \param msg pre-allocate message buffer to which received data is appended
 *  \returns number of bytes read, negative on error. */
int osmo_dgram_recv(struct osmo_dgram *conn, struct msgb *msg)
{
	return osmo_dgram_rx_recv(conn->rx, msg);
}

/*! @} */
