/*
 * Copyright (C) 2010 Voice System
 *
 * This file is part of opensips, a free SIP server.
 *
 * opensips 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
 *
 * opensips 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA
 *
 * History:
 * ---------
 *  Initial version 02-04-2010 (Anca Vamanu)
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/un.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <poll.h>
#include "../../dprint.h"
#include "../../ut.h"
#include "../../trim.h"
#include "../../resolve.h"
#include "../../pt.h"
#include "../../sr_module.h"
#include "../../reactor_defs.h"
#include "../../reactor_proc.h"
#include "../../io_wait.h"
#include "../../lib/list.h"

#include "rtpproxy.h"
#include "notification_process.h"

#define BUF_LEN				255

int *rtpp_notify_process_no;

#define IS_DIGIT(_c) ((_c) >= '0' && (_c) <= '9')

struct rtpp_notify {
	int fd;
	char *remaining;
	int remaining_len;
	int linked;
	union sockaddr_union addr;
	struct list_head list;
};
OSIPS_LIST_HEAD(rtpp_notify_fds);

static void free_rtpp_notify(int fd, struct rtpp_notify *notify)
{
	reactor_proc_del_fd(fd, -1, IO_FD_CLOSING);
	if (notify) {
		if (notify->linked)
			list_del(&notify->list);
		if (notify->remaining)
			pkg_free(notify->remaining);
		pkg_free(notify);
	}
	shutdown(fd, SHUT_RDWR);
	close(fd);
}

static int set_nonblocking(int fd)
{
	int flags;

	flags = fcntl(fd, F_GETFL);
	if (flags == -1) {
		LM_ERR("fcntl(%d, F_GETFL) failed: %s\n", fd, strerror(errno));
		return -1;
	}

	if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) {
		LM_ERR("fcntl(%d, F_SETFL) failed: %s\n", fd, strerror(errno));
		return -1;
	}

	return 0;
}

static int notification_handler(str *command)
{
	char cmd, *p;
	str param, token;
	unsigned int h_entry, h_id, is_callid = 0;
	struct rtpp_dtmf_event dtmf;
	str terminate_reason = str_init("RTPProxy Timeout");

	if (command->len < 1) {
		LM_ERR("no command received from RTPProxy!\n");
		return -1;
	}
	cmd = command->s[0];
	param.s = command->s + 1;
	param.len = command->len - 1;
	LM_DBG("Handling RTPProxy command %c %.*s\n", cmd, param.len, param.s);
	switch (cmd) {
		case 'I':
			/* we are not listening for timeout notifications */
			return 0;
		case 'T':
			LM_INFO("Timeout notification for %.*s\n", param.len, param.s);
			if (parse_dlg_did(&param, &h_entry, &h_id) < 0)
				return -1;
			if(run_dlg_api(&dlg_api, terminate_dlg, NULL, h_entry, h_id, &terminate_reason)< 0)
				LM_ERR("Failed to terminate dialog h_entry=[%u], h_id=[%u]\n", h_entry, h_id);
			return 0;
		case 'D':
			p = q_memchr(param.s, ' ' ,param.len);
			if (!p) {
				LM_ERR("could not determine the notification id in %.*s!\n",
						param.len, param.s);
				return -1;
			}
			token.s = param.s + 1;
			token.len = p - token.s;
			if (*param.s == 'c')
				is_callid = 1;

			param.s = p + 1;
			param.len -= token.len + 2;

			if (param.len < 0) {
				LM_ERR("could not get digit in param %.*s!\n", param.len, param.s);
				return -1;
			}

			memset(&dtmf, 0, sizeof dtmf);
			dtmf.is_callid = is_callid;
			dtmf.id = token;

			dtmf.digit = *param.s;

			param.s += 2;
			param.len -= 2;

			if (param.len > 0) {
				p = q_memchr(param.s, ' ', param.len);
				if (p) {
					token.s = param.s;
					token.len = p - param.s;
					if (str2int(&token, &dtmf.volume) < 0)
						dtmf.volume = 0;

					param.s = p + 1;
					param.len -= token.len + 1;
					if (param.len >= 0) {
						p = q_memchr(param.s, ' ', param.len);
						if (p) {
							/* we got both duration and stream */
							token.s = p + 1;
							token.len = param.len - (token.s - param.s);

							param.len -= token.len + 1;
							if (param.len >= 0 && str2int(&token, &dtmf.stream) < 0)
								dtmf.stream = 0;
						}
						if (str2int(&param, &dtmf.duration) < 0)
							dtmf.duration = 0;
					}
				}
			}
			LM_INFO("got event %c volume=%u duration=%u stream=%u for %.*s\n",
					dtmf.digit, dtmf.volume, dtmf.duration, dtmf.stream,
					dtmf.id.len, dtmf.id.s);
			rtpproxy_raise_dtmf_event(&dtmf);

			return 0;
		default:
			LM_WARN("Unhandled command %c param=%.*s\n", cmd, param.len, param.s);
			return 0;
	}
}

static int compare_sa(struct sockaddr *s1, struct sockaddr *s2)
{
	if (s1->sa_family != s2->sa_family)
		return 1;
	if (s1->sa_family == AF_INET &&
			memcmp(&((struct sockaddr_in *)s1)->sin_addr,
				&((struct sockaddr_in *)s2)->sin_addr, 4) != 0)
		return 2;
	if (s1->sa_family == AF_INET6 &&
			memcmp(&((struct sockaddr_in6 *)s1)->sin6_addr,
				&((struct sockaddr_in6 *)s2)->sin6_addr, 16) != 0)
		return 3;
	return 0;
}

static struct rtpp_node *rtpproxy_get_node(union sockaddr_union *rtpp_info)
{
	struct rtpp_set * rtpp_list;
	struct rtpp_node *crt_rtpp;

	if (!rtpp_set_list || !(*rtpp_set_list))
		return NULL;

	if (nh_lock)
		lock_start_read( nh_lock );
	for (rtpp_list = (*rtpp_set_list)->rset_first; rtpp_list != NULL;
			rtpp_list = rtpp_list->rset_next) {
		for(crt_rtpp = rtpp_list->rn_first; crt_rtpp != NULL;
						crt_rtpp = crt_rtpp->rn_next) {
			if (crt_rtpp->rn_umode == CM_UNIX || crt_rtpp->rn_umode == CM_CUNIX)
				continue;
			if (compare_sa(&crt_rtpp->addr.s, &rtpp_info->s) == 0) {
				if (nh_lock)
					lock_stop_read( nh_lock );
				return crt_rtpp;
			}
		}
	}
	if (nh_lock)
		lock_stop_read( nh_lock );
	return NULL;
}

static int rtpproxy_io_callback(int fd, void *fs, int was_timeout)
{
	struct rtpp_notify *notify = (struct rtpp_notify *)fs;
	char buffer[BUF_LEN+1];
	int len, left, offset, total, entered = 0;
	str command;
	char *p, *start, *sp, *end;

	profiling_proc_start( LEVEL_EXTRAPROCS, 1);

	if (notify && notify->remaining) {
		memcpy(buffer, notify->remaining, notify->remaining_len);
		offset = notify->remaining_len;
		pkg_free(notify->remaining);
		notify->remaining_len = 0;
		notify->remaining = NULL;
	} else {
		offset = 0;
	}

	for (;;) {
		if (offset == BUF_LEN) {
			LM_ERR("RTPProxy notification command too large [%.*s]\n",
					offset, buffer);
			free_rtpp_notify(fd, notify);
			goto err1;
		}

		do
			len = read(fd, buffer + offset, BUF_LEN - offset);
		while (len == -1 && errno == EINTR);

		if (len < 0) {
			if (errno == EAGAIN || errno == EWOULDBLOCK)
				break;

			LM_ERR("reading from socket failed: %s\n",strerror(errno));
			free_rtpp_notify(fd, notify);
			goto err1;
		}
		if (len == 0) {
			if (offset)
				LM_WARN("dropping partial RTPProxy notification [%.*s]\n",
						offset, buffer);
			LM_DBG("closing rtpproxy notify socket\n");
			free_rtpp_notify(fd, notify);
			goto done;
		}

		total = len + offset;
		buffer[total] = 0; // make it null terminated

		LM_DBG("Notification(s) received: [%.*s]\n", total, buffer);
		if (!entered) {
			profiling_proc_enter( LEVEL_EXTRAPROCS,
					ss_merge256("RTPP_CB ",buffer), 0 );
			entered = 1;
		}

		p = buffer;
		left = total;
		end = buffer + total;

		do {
			start = p;

			sp = q_memchr(p, '\n', left);
			if (sp == NULL)
				break;
			command.s = p;
			command.len = sp - p;
			/* skip the command */
			p = sp + 1;
			left -= (sp - start) + 1;

			if (notification_handler(&command) < 0) {
				LM_ERR("notification_handler failed\n");
				free_rtpp_notify(fd, notify);
				goto err1;
			}

			LM_DBG("Left to process: %d\n[%.*s]\n", left, left, p);

		} while (p < end);

		offset = end - p;
		if (offset) {
			LM_DBG("%d remaining data in buffer!\n", offset);
			memmove(buffer, p, offset);
		}
	}

	if (offset) {
		if (notify && (notify->remaining = pkg_malloc(offset)) != NULL) {
			notify->remaining_len = offset;
			memcpy(notify->remaining, buffer, offset);
		} else {
			LM_WARN("dropping remaining data [%.*s]\n", offset, buffer);
		}
	}

	if (entered)
		profiling_proc_exit( LEVEL_EXTRAPROCS, "RTPP_CB", 0 );

done:
	profiling_proc_end( LEVEL_EXTRAPROCS, 0 );
	return 0;
err1:
	if (entered)
		profiling_proc_exit( LEVEL_EXTRAPROCS, "RTPP_CB", -1 );
	profiling_proc_end( LEVEL_EXTRAPROCS, -1 );
	return -1;
}

static int rtpproxy_io_new_callback(int fd, void *fs, int was_timeout)
{
	int size;
	struct sockaddr_storage rtpp_info = {0};
	struct rtpp_node *node;
	struct rtpp_notify *notify;

	profiling_proc_start( LEVEL_EXTRAPROCS, 1);

	size = sizeof(rtpp_info);
	fd = accept(fd, (struct sockaddr *)&rtpp_info, (socklen_t *)&size);
	if(fd < 0) {
		LM_ERR("socket accept failed: %s(%d)\n", strerror(errno), errno);
		goto err;
	}

	profiling_proc_enter( LEVEL_EXTRAPROCS, "RTPP new CB", 0 );

	if (set_nonblocking(fd) < 0)
		goto err1;

	notify = pkg_malloc(sizeof *notify);
	if (!notify) {
		LM_ERR("could not allocate notify node\n");
		goto err1;
	}
	memset(notify, 0, sizeof *notify);
	notify->fd = fd;

	if (rtpp_notify_cfg.sock.rn_umode == CM_UNIX) {
		LM_DBG("trusting unix socket connection\n");
	} else {
		node = rtpproxy_get_node((union sockaddr_union *)&rtpp_info);
		if (!node) {
			LM_WARN("connection from unknown RTPProxy node");
			pkg_free(notify);
			goto err1;
		}
		memcpy(&notify->addr, &node->addr, sizeof(union sockaddr_union));
		list_add(&notify->list, &rtpp_notify_fds);
		notify->linked = 1;
	}

	if (reactor_proc_add_fd(fd, rtpproxy_io_callback, notify) < 0) {
		LM_CRIT("failed to add RTPProxy listen socket to reactor\n");
		if (notify->linked)
			list_del(&notify->list);
		pkg_free(notify);
		goto err1;
	}

	profiling_proc_exit( LEVEL_EXTRAPROCS, "RTPP new CB", 0 );
	profiling_proc_end( LEVEL_EXTRAPROCS, 0 );
	return 0;
err1:
	if (fd >= 0) {
		shutdown(fd, SHUT_RDWR);
		close(fd);
	}
	profiling_proc_exit( LEVEL_EXTRAPROCS, "RTPP new CB", -1 );
err:
	profiling_proc_end( LEVEL_EXTRAPROCS, -1 );
	return -1;
}

int init_rtpp_notify(void)
{
	rtpp_notify_process_no = shm_malloc(sizeof *rtpp_notify_process_no);
	if (!rtpp_notify_process_no) {
		LM_ERR("cannot allocate space for rtpp notify process number\n");
		return -1;
	}
	return 0;
}

static rtp_io_getrnsock_t
rtp_io_rnsock_f(void)
{
	static rtp_io_getrnsock_t _rtp_io_getrnsock = {0};
	if (_rtp_io_getrnsock == NULL)
		_rtp_io_getrnsock = (rtp_io_getrnsock_t)find_export("rtp_io_getrnsock", 0);
	return _rtp_io_getrnsock;
}

int fill_rtp_io_rnsock(void)
{

	rtp_io_getrnsock_t rtp_io_rnsock = rtp_io_rnsock_f();
	if (rtp_io_rnsock == NULL)
		return -1;
	if (rtp_io_rnsock(&rtpp_notify_cfg) != 0)
		return -1;
	return 0;
}

void notification_listener_process(int rank)
{
	struct sockaddr_un saddr_un;
	struct sockaddr_in saddr_in;
	char *p;
	str id;
	unsigned short port;
	struct sockaddr* saddr;
	int len, n;
	int optval = 1;
	int socket_fd = -1;
	str *rn_name = &rtpp_notify_cfg.name;
	struct rtpp_sock *rn_sock = &rtpp_notify_cfg.sock;

	if (rn_name->s == NULL) {
		if (fill_rtp_io_rnsock() != 0)
			goto serve;
	}

	*rtpp_notify_process_no = process_no;

	switch (rn_sock->rn_umode) {
	case CM_TCP:
		p = strrchr(rn_name->s, ':');
		if (!p) {
			LM_ERR("invalid udp address <%.*s>\n", rn_name->len, rn_name->s);
			return;
		}
		n = p - rn_name->s;
		rn_name->s[n] = 0;

		id.s = p+1;
		id.len = rn_name->len - n - 1;
		port= str2s(id.s, id.len, &n);
		if(n) {
			LM_ERR("Bad format for socket name. Expected ip:port\n");
			return;
		}
		/* skip here tcp part */
		rn_name->s += 4;
		memset(&saddr_in, 0, sizeof(saddr_in));
		saddr_in.sin_addr.s_addr = inet_addr(rn_name->s);
		saddr_in.sin_family = AF_INET;
		saddr_in.sin_port = htons(port);

		socket_fd = socket(AF_INET, SOCK_STREAM, 0);
		if (socket_fd == -1) {
			LM_ERR("can't create timeout socket\n");
			return;
		}
		saddr = (struct sockaddr*)&saddr_in;
		len = sizeof(saddr_in);
		LM_DBG("binding socket %d to %s:%d\n", socket_fd, rn_name->s, port);
		break;
	case CM_UNIX:
		/* create socket */
		socket_fd = socket(AF_UNIX, SOCK_STREAM, 0);
		if (socket_fd == -1) {
			LM_ERR("Failed to create unix socket\n");
			return;
		}

		memset(&saddr_un, 0, sizeof(struct sockaddr_un));
		saddr_un.sun_family = AF_LOCAL;
		strncpy(saddr_un.sun_path, rn_name->s,
				sizeof(saddr_un.sun_path) - 1);
		saddr = (struct sockaddr*)&saddr_un;
		len = sizeof(saddr_un);
		LM_DBG("binding unix socket %s\n", rn_name->s);
		break;
	case CM_RTPIO:
		socket_fd = rn_sock->fd;
		len = -1;
		saddr = NULL;
		LM_INFO("using rtp.io notification socket %d\n", socket_fd);
		break;
	default:
		abort();
	}

	if (rn_sock->rn_umode != CM_RTPIO) {
		if (setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, (void*)&optval,
				sizeof(optval)) == -1) {
			LM_ERR("setsockopt failed %s\n", strerror(errno));
			return;
		}

		if (bind(socket_fd, saddr, len) == -1) {
			LM_ERR("failed to bind to socket: %s\n", strerror(errno));
			return;
		}

		/* open socket for listening */
		if(listen(socket_fd, 10) == -1) {
			LM_ERR("socket listen failed: %s(%d)\n", strerror(errno), errno);
			close(socket_fd);
			return;
		}
	}

serve:
	if (reactor_proc_init("RTPProxy events") < 0) {
		LM_ERR("failed to init the RTPProxy events\n");
		return;
	}

	if (socket_fd != -1) {
		reactor_proc_cb_f cb_f = (rn_sock->rn_umode != CM_RTPIO) ? rtpproxy_io_new_callback :
		    rtpproxy_io_callback;
		if (rn_sock->rn_umode == CM_RTPIO && set_nonblocking(socket_fd) < 0)
			return;
		if (reactor_proc_add_fd(socket_fd, cb_f, NULL) < 0) {
			LM_CRIT("failed to add RTPProxy listen socket to reactor\n");
			return;
		}
	}

	reactor_proc_loop();
}

static void ipc_update_rtpp_notify(int sender, void *param)
{
	struct list_head *it, *safe;
	struct rtpp_notify *notify;

	LM_INFO("updating RTPProxy notify handlers!\n");
	/* go through each handler and see if there's a corresponding rtpp node */
	list_for_each_safe(it, safe, &rtpp_notify_fds) {
		notify = list_entry(it, struct rtpp_notify, list);
		if (rtpproxy_get_node(&notify->addr) == NULL) {
			reactor_del_reader(notify->fd, -1, IO_FD_CLOSING);
			list_del(&notify->list);
			shutdown(notify->fd, SHUT_RDWR);
			close(notify->fd);
			if (notify->remaining)
				pkg_free(notify->remaining);
			pkg_free(notify);
		}
	}
}

void update_rtpp_notify(void)
{
	if (!rtpp_notify_process_no) {
		LM_DBG("RTPProxy process not initialized\n");
		return;
	}
	if (ipc_send_rpc(*rtpp_notify_process_no, ipc_update_rtpp_notify, NULL) != 0)
		LM_ERR("could not send RTPProxy update to notify process!\n");
}

int rtpproxy_is_nproc(enum inp_op op)
{
	static int _rtpproxy_is_nproc = 0;
	const int lastval = _rtpproxy_is_nproc;

	if (op == NPROC_SET) {
		_rtpproxy_is_nproc = 1;
	}
	return lastval;
}
