/**
 * Copyright (C) 2015 Bicom Systems Ltd, (bicomsystems.com)
 *
 * Author: Seudin Kasumovic (seudin.kasumovic@gmail.com)
 *
 * This file is part of Kamailio, a free SIP server.
 *
 * Kamailio 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
 *
 * Kamailio 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
 *
 */

#include "../../core/mem/mem.h"
#define HANDLE_IO_INLINE
#include "../../core/io_wait.h"
#include "../../core/pt.h"  /* process_count */

#ifndef USE_TCP
#error	"USE_TCP must be enabled for this module"
#endif

#include "../../core/pass_fd.h"

#include "erlang_mod.h"
#include "erl_helpers.h"
#include "cnode.h"
#include "epmd.h"
#include "worker.h"
#include "handle_emsg.h"

#define IO_LISTEN_TIMEOUT	10
#define CONNECT_TIMEOUT	500 /* ms */

static io_wait_h erl_io_h;

cnode_handler_t *enode = NULL;
csockfd_handler_t *csocket_handler = NULL;
erlang_pid *cnode_reply_to_pid = NULL;

/**
 * @brief Initialize Kamailio as C node by active connect as client.
 */
int cnode_connect_to(cnode_handler_t *phandler, ei_cnode *ec, const str *nodename )
{
	struct sockaddr addr = { 0 };
	socklen_t addrlen = sizeof(struct sockaddr);
	int port;

	int rfd;
	int ai_error;
	ip_addr_t ip;

	phandler->request.buff = NULL;
	phandler->response.buff = NULL;
	phandler->sockfd = -1;
	phandler->destroy_f = NULL;

	LM_DBG("connecting to Erlang node %.*s ...\n", STR_FMT(nodename));

	if ((rfd = ei_connect_tmo(ec, nodename->s, CONNECT_TIMEOUT)) < 0)
	{
		switch (erl_errno)
		{
		case EHOSTUNREACH:
			LM_ERR("remote node %.*s is unreachable.\n", STR_FMT(nodename));
			break;
		case ENOMEM:
			LM_ERR("%s.\n", strerror(erl_errno));
			break;
		case EIO:
			LM_ERR("%s. %s.\n", strerror(erl_errno), strerror(errno));
			break;
		default:
			LM_ERR("%s.\n",strerror(erl_errno));
			break;
		}
		LM_ERR("failed erl_errno=%d %s.\n",erl_errno,strerror(erl_errno));
		return -1;
	}

	phandler->ec = *ec;
	phandler->handle_f = handle_cnode;
	phandler->wait_tmo_f = wait_cnode_tmo;
	phandler->destroy_f = destroy_cnode;

	erl_set_nonblock(rfd);

	phandler->sockfd = rfd;

	/* get addr on socket */
	if ((ai_error = getpeername(rfd, &addr, &addrlen)))
	{
		LM_ERR("%s\n",strerror(errno));
	}
	else
	{
		sockaddr2ip_addr(&ip,&addr);
		port = sockaddr_port(&addr);

		LM_DBG("... connected to %.*s %s:%u\n", STR_FMT(nodename), ip_addr2strz(&ip), port);
	}

	strncpy(phandler->conn.nodename, nodename->s, nodename->len);
	phandler->conn.nodename[nodename->len] = 0;

	/* for #Pid */
	phandler->ec.self.num = phandler->sockfd;
	phandler->new = NULL;

	if (ei_x_new(&phandler->request))
	{
		LOG(L_ERR,"failed to allocate ei_x_buff: %s.\n", strerror(erl_errno));
		return -1;
	}

	if (ei_x_new_with_version(&phandler->response))
	{
		LOG(L_ERR,"failed to allocate ei_x_buff: %s.\n", strerror(erl_errno));
		return -1;
	}

	enode = phandler;
	return 0;
}

int destroy_cnode(handler_common_t *phandler_t)
{
	cnode_handler_t *phandler = (cnode_handler_t*)phandler_t;

	if(phandler->request.buff) ei_x_free(&phandler->request);
	if(phandler->response.buff) ei_x_free(&phandler->response);

	erl_close_socket(phandler->sockfd);

	return 0;
}

int init_cnode_sockets(int cnode_id)
{
	char buff[EI_MAXALIVELEN+1];
	str alivename;
	ei_cnode ec;
	int poll_method;
	handler_common_t *phandler;

	/* generate cnode name */
	alivename.s = buff;
	if (no_cnodes > 1) {
		alivename.len = snprintf(buff,EI_MAXALIVELEN,"%.*s%d",STR_FMT(&cnode_alivename),cnode_id);
	} else {
		alivename = cnode_alivename;
	}

	if (alivename.len > EI_MAXALIVELEN) {
		LM_ERR("alive name %.*s too long max allowed size is %d\n",STR_FMT(&cnode_alivename), EI_MAXALIVELEN);
		return -1;
	}

	/* init Erlang connection */
	if (erl_init_ec(&ec,&alivename,&cnode_host,&cookie)) {
		LM_CRIT("failed to initialize ei_cnode\n");
		return -1;
	}

	poll_method=choose_poll_method();

	LM_DBG("using %s as the I/O watch method (auto detected)\n", poll_method_name(poll_method));

	if (init_io_wait(&erl_io_h,get_max_open_fds(),poll_method))
	{
		LM_CRIT("init_io_wait failed\n");
		return -1;
	}

	phandler = (handler_common_t*)pkg_malloc(sizeof(csockfd_handler_t));
	if (!phandler) {
		LM_ERR("not enough memory\n");
		return -1;
	}

	io_handler_ins(phandler);

	if (csockfd_init((csockfd_handler_t*)phandler, &ec)) {
		return -1;
	}

	if (io_watch_add(&erl_io_h,phandler->sockfd,POLLIN,ERL_CSOCKFD_H,phandler)) {
		LM_CRIT("io_watch_add failed\n");
		return -1;
	}

	phandler = (handler_common_t*)pkg_malloc(sizeof(cnode_handler_t));
	if (!phandler) {
		LM_CRIT("not enough memory\n");
		return -1;
	}

	io_handler_ins(phandler);

	/* connect to remote Erlang node */
	if (cnode_connect_to((cnode_handler_t*)phandler,&ec, erlang_nodename.s?&erlang_nodename:&erlang_node_sname)) {
		/* continue even failed to connect, connection can be established
		 * from Erlang side too */
		io_handler_del(phandler);
	} else if (io_watch_add(&erl_io_h,phandler->sockfd,POLLIN,ERL_CNODE_H,phandler)){
		LM_CRIT("io_watch_add failed\n");
		return -1;
	}

	phandler = (handler_common_t*)pkg_malloc(sizeof(epmd_handler_t));
	if (!phandler) {
		LM_CRIT("not enough memory\n");
		return -1;
	}

	io_handler_ins(phandler);

	/* start epmd handler - publish Kamailo C node */
	if (epmd_init((epmd_handler_t*)phandler, &alivename) < 0 ) {
		return -1;
	}

	if (io_watch_add(&erl_io_h,phandler->sockfd,POLLIN,ERL_EPMD_H,phandler)){
		LM_CRIT("io_watch_add failed\n");
		return -1;
	}

	return 0;
}

void cnode_main_loop(int cnode_id)
{
	char _cnode_name[MAXNODELEN];
	str *enode_name;
	int r;

	if (snprintf(_cnode_name, MAXNODELEN, "%.*s%d@%.*s", STR_FMT(&cnode_alivename), cnode_id+1,STR_FMT(&cnode_host)) >= MAXNODELEN)
	{
		LM_CRIT("the node name <%.*s%d@%.*s> is too large max length allowed is %d\n",
				STR_FMT(&cnode_alivename), cnode_id+1, STR_FMT(&cnode_host), MAXNODELEN-1);
		return;
	}

	erl_init_common();

	if (init_cnode_sockets(cnode_id)) {
		io_handlers_delete();
		erl_free_common();
		return;
	}

	enode_name = erlang_nodename.s?&erlang_nodename:&erlang_node_sname;

	/* main loop */
	switch(erl_io_h.poll_method){
		case POLL_POLL:
			while(1){
				r = io_wait_loop_poll(&erl_io_h, IO_LISTEN_TIMEOUT, 0);
				if(!r && enode_connect()) {
					LM_ERR("failed reconnect to %.*s\n",STR_FMT(enode_name));
				}
			}
			break;
#ifdef HAVE_SELECT
		case POLL_SELECT:
			while(1){
				r = io_wait_loop_select(&erl_io_h, IO_LISTEN_TIMEOUT, 0);
				if(!r && enode_connect()) {
					LM_ERR("failed reconnect to %.*s\n",STR_FMT(enode_name));
				}
			}
			break;
#endif
#ifdef HAVE_SIGIO_RT
		case POLL_SIGIO_RT:
			while(1){
				r = io_wait_loop_sigio_rt(&erl_io_h, IO_LISTEN_TIMEOUT);
				if(!r && enode_connect()) {
					LM_ERR("failed reconnect to %.*s\n",STR_FMT(enode_name));
				}
			}
			break;
#endif
#ifdef HAVE_EPOLL
			case POLL_EPOLL_LT:
				while(1){
					r = io_wait_loop_epoll(&erl_io_h, IO_LISTEN_TIMEOUT, 0);
					if(!r && enode_connect()) {
						LM_ERR("failed reconnect to %.*s\n",STR_FMT(enode_name));
					}
				}
				break;
			case POLL_EPOLL_ET:
				while(1){
					r = io_wait_loop_epoll(&erl_io_h, IO_LISTEN_TIMEOUT, 1);
					if(!r && enode_connect()) {
						LM_ERR("failed reconnect to %.*s\n",STR_FMT(enode_name));
					}
				}
				break;
#endif
#ifdef HAVE_KQUEUE
			case POLL_KQUEUE:
				while(1){
					r = io_wait_loop_kqueue(&erl_io_h, IO_LISTEN_TIMEOUT, 0);
					if(!r && enode_connect()) {
						LM_ERR("failed reconnect to %.*s\n",STR_FMT(enode_name));
					}
				}
				break;
#endif
#ifdef HAVE_DEVPOLL
                case POLL_DEVPOLL:
                	while(1){
						r = io_wait_loop_devpoll(&erl_io_h, IO_LISTEN_TIMEOUT, 0);
						if(!r && enode_connect()) {
							LM_ERR("failed reconnect to %.*s\n",STR_FMT(enode_name));
						}
                	}
                	break;
#endif
                default:
                	LM_CRIT("BUG: io_listen_loop: no support for poll method "
                			" %s (%d)\n", poll_method_name(erl_io_h.poll_method),
							erl_io_h.poll_method);
                	goto error;
        }

error:
	LOG(L_CRIT, "ERROR: cnode_main_loop exiting ...\n");
}

/* generic handle io routine, it will call the appropriate
 *  handle_xxx() based on the fd_map type
 *
 * params:  fm  - pointer to a fd hash entry
 *          idx - index in the fd_array (or -1 if not known)
 * return: -1 on error
 *          0 on EAGAIN or when by some other way it is known that no more
 *            io events are queued on the fd (the receive buffer is empty).
 *            Usefull to detect when there are no more io events queued for
 *            sigio_rt, epoll_et, kqueue.
 *         >0 on successfully read from the fd (when there might be more io
 *            queued -- the receive buffer might still be non-empty)
 */
int handle_io(struct fd_map* fm, short events, int idx)
{
	int type;
	handler_common_t *phandler;

	phandler = (handler_common_t*)fm->data;

	if (phandler->handle_f(phandler)) {
		io_watch_del(&erl_io_h,fm->fd,idx,IO_FD_CLOSING);
		io_handler_del(phandler);
		if (fm->type == ERL_WORKER_H) {
			LM_CRIT("error on unix socket, not recoverable error -- aborting\n");
			abort();
		}
		return -1;
	}

	if (phandler->new) {
		switch (fm->type) {
		case ERL_CSOCKFD_H:
			type = ERL_WORKER_H;
			break;
		case ERL_EPMD_H:
			type = ERL_CNODE_H;
			break;
		default:
			LM_ERR("should not be here!\n");
			return -1;
		}
		LM_DBG("add new handler type=%d\n",type);
		if (io_watch_add(&erl_io_h,phandler->new->sockfd,POLLIN,type,(void*)phandler->new)) {
			LM_ERR("failed to add new handler\n");
			return -1;
		}
		io_handler_ins(phandler->new);
		phandler->new = NULL;
	}

	return 1;
}

/**
 * @brief Handle requests from remote Erlang node.
 */
int handle_cnode(handler_common_t *phandler)
{
	int got;
	cnode_handler_t *listener;
	erlang_msg emsg; /* Incoming message */
	ei_x_buff *request;
	ei_x_buff *response;
	int response_start_index;

	listener = (cnode_handler_t*) phandler;
	request = &listener->request;
	response = &listener->response;

	response_start_index = response->index; /* save */
	request->index = 0;

	listener->tick_tmo = 0; /* reset ERL_TICK timer for all events */

	memset((void*)&emsg,0,sizeof(erlang_msg));

	if ((got = ei_xreceive_msg_tmo(listener->sockfd, &emsg, request,
			CONNECT_TIMEOUT)) == ERL_TICK)
	{ /* ignore */
		LM_DBG("%s received ERL_TICK from <%s>.\n", listener->ec.thisnodename, listener->conn.nodename);
		return 0;
	}
	else if (got == ERL_ERROR)
	{
		switch (erl_errno)
		{
		case EAGAIN:
			/* or */
		case ETIMEDOUT:
			response->index = response_start_index; /* restore version point */
			return 0;
			break;
		case EIO:
			LM_ERR("I/O error while receiving message from %s or connection closed.\n", listener->conn.nodename);
			enode = NULL;
			return -1;
			break;
		case ENOMEM:
			LM_ERR("Failed to receive message from %s, not enough memory. Closing connection.\n", listener->conn.nodename);
			enode = NULL;
			return -1;
			break;
		default:
			LM_ERR("Close connection to %s. %s.\n", listener->conn.nodename, strerror(erl_errno));
			enode = NULL;
			return -1;
		}
	}
	else if (got == ERL_MSG)
	{
		response->index = response_start_index; /* restore version point */
		switch (emsg.msgtype)
		{
		case ERL_LINK:
			LM_WARN("ERL_LINK from <%s> -- discarding.\n", listener->conn.nodename);
			break;
		case ERL_REG_SEND:
			PRINT_DBG_REG_SEND(listener->conn.nodename,emsg.from, listener->ec.thisnodename, emsg.toname,request);
			handle_erlang_msg(listener, &emsg);
			break;
		case ERL_SEND:
			PRINT_DBG_SEND(listener->conn.nodename,emsg.to,request);
			handle_erlang_msg(listener, &emsg);
			break;
		case ERL_EXIT:
			LM_WARN("received ERL_EXIT from <%s> -- discarding.\n", listener->conn.nodename);
			break;
		case ERL_UNLINK:
			LM_WARN("ERL_UNLINK from <%s> -- discarding.\n", listener->conn.nodename);
			break;
		case ERL_NODE_LINK:
			LM_WARN("ERL_NODE_LINK from <%s> -- discarding.\n", listener->conn.nodename);
			break;
		case ERL_GROUP_LEADER:
			LM_WARN("ERL_GROUP_LEADER from <%s> -- discarding.\n", listener->conn.nodename);
			break;
		case ERL_EXIT2:
			LM_WARN("ERL_EXIT2 from <%s> -- discarding.\n", listener->conn.nodename);
			break;
		case ERL_PASS_THROUGH:
			LM_WARN("ERL_PASS_THROUGH from <%s> -- discarding.\n", listener->conn.nodename);
			break;
		default:
			LM_WARN("Received unknown msgtype <%ld> from <%s> -- discarding.\n", emsg.msgtype, listener->conn.nodename);
			break;
		}
	}
	else
	{
		LM_ERR("unknown return value from ei_xreceive_msg_tmo.\n");
	}

	response->index = response_start_index; /* restore to version point */

	return  0;
}

int wait_cnode_tmo(handler_common_t *phandler_t)
{
	return 0;
}

int csockfd_init(csockfd_handler_t *phandler, const ei_cnode *ec)
{
	phandler->handle_f = handle_csockfd;
	phandler->wait_tmo_f = NULL;
	phandler->sockfd = csockfd;
	phandler->ec = *ec;
	phandler->new = NULL;

	erl_set_nonblock(csockfd);

	csocket_handler = phandler;

	csockfd = 0;
	return 0;
}

int handle_csockfd(handler_common_t *phandler_t)
{
	csockfd_handler_t *phandler;
	int data[2];
	int fd = -1;

	phandler = (csockfd_handler_t*)phandler_t;

	if (receive_fd(phandler->sockfd,(void*)data,sizeof(data),&fd,0) == -1) {
		LM_ERR("failed to receive socket: %s\n",strerror(errno));
		return -1;
	}

	phandler->new = (handler_common_t*)pkg_malloc(sizeof(worker_handler_t));
	if(!phandler->new) {
		LM_ERR("not enough memory\n");
		return -1;
	}

	io_handler_ins(phandler->new);

	return worker_init((worker_handler_t*)phandler->new,fd,&phandler_t->ec);
}

/*
 * \brief Connect to Erlang node if not connected
 */
int enode_connect() {

	handler_common_t *phandler;

	if (!csocket_handler) {
		return -1;
	}

	if (enode) {
		return 0;
	}

	LM_DBG("not connected, trying to connect...\n");

	phandler = (handler_common_t*)pkg_malloc(sizeof(cnode_handler_t));

	if (!phandler) {
		LM_CRIT("not enough memory\n");
		return -1;
	}

	io_handler_ins(phandler);

	/* connect to remote Erlang node */
	if (cnode_connect_to((cnode_handler_t*)phandler,&csocket_handler->ec, erlang_nodename.s?&erlang_nodename:&erlang_node_sname)) {
		/* continue even failed to connect, connection can be established
		 * from Erlang side too */
		io_handler_del(phandler);
	} else if (io_watch_add(&erl_io_h,phandler->sockfd,POLLIN,ERL_CNODE_H,phandler)){
		LM_CRIT("io_watch_add failed\n");
		erl_close_socket(phandler->sockfd);
		io_handler_del(phandler);
		return -1;
	}

	return 0;
}
