/*
 * OpenMRCP - Open Source Media Resource Control Protocol Stack
 * Copyright (C) 2007, Cepstral LLC
 *
 * Version: MPL 1.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * Author(s):
 * Arsen Chaloyan <achaloyan@yahoo.com>
 *
 * Contributor(s):
 *
 */

typedef struct mrcp_server_sofia_agent_t mrcp_server_sofia_agent_t;
#define NUA_MAGIC_T mrcp_server_sofia_agent_t

typedef struct mrcp_server_sofia_session_t mrcp_server_sofia_session_t;
#define NUA_HMAGIC_T mrcp_server_sofia_session_t

#include <sofia-sip/su.h>
#ifdef _MSC_VER
#undef strncasecmp
#undef strcasecmp
#endif
#include <apr_general.h>
#include <sofia-sip/nua.h>
#include <sofia-sip/sip_status.h>
#include <sofia-sip/sdp.h>

#include "mrcp_server_signaling_agent.h"
#include "apt_producer_task.h"


struct mrcp_server_sofia_agent_t {
	mrcp_signaling_agent_t *signaling_agent;
	apt_producer_task_t    *producer_task;

	su_root_t              *root;
	nua_t                  *nua;

	apr_pool_t             *pool;
};

struct mrcp_server_sofia_session_t {
	mrcp_signaling_channel_t *channel;
	su_home_t                *home;
	nua_handle_t             *nh;
};

typedef enum {
	MRCP_SOFIA_EVENT_AGENT_PROCESS,
	MRCP_SOFIA_EVENT_AGENT_STARTED,
	MRCP_SOFIA_EVENT_AGENT_TERMINATED
} mrcp_sofia_event_id;


typedef struct mrcp_sofia_event_t mrcp_sofia_event_t;

struct mrcp_sofia_event_t {
	mrcp_sofia_event_id          event_id;

	nua_event_t                  nua_event;
	int                          status;
	char const                  *phrase;
	nua_t                       *nua;
	mrcp_server_sofia_agent_t   *sofia_agent;
	nua_handle_t                *nh;
	mrcp_server_sofia_session_t *sofia_session;
	sip_t const                 *sip;
	tagi_t                      *tags;
};

#define MRCP_SERVER_SOFIA_AGENT "OpenMRCP Sofia-SIP"


static mrcp_status_t mrcp_sofia_agent_destroy(mrcp_module_t *module);
static mrcp_module_state_t mrcp_sofia_agent_open(mrcp_module_t *module);
static mrcp_module_state_t mrcp_sofia_agent_close(mrcp_module_t *module);
static mrcp_status_t mrcp_sofia_agent_signal_handler(mrcp_module_t *module, apt_task_msg_t *msg);

static const mrcp_module_method_set_t module_method_set = {
	mrcp_sofia_agent_destroy,
	mrcp_sofia_agent_open,
	mrcp_sofia_agent_close,
	mrcp_sofia_agent_signal_handler
};

static mrcp_status_t mrcp_sofia_session_answer(mrcp_signaling_channel_t *channel);
static mrcp_status_t mrcp_sofia_session_destroy(mrcp_signaling_channel_t *channel);

static const mrcp_signaling_channel_method_set_t signaling_channel_method_set = {
	NULL, /*offer*/
	mrcp_sofia_session_answer,
	NULL, /*terminate*/
	mrcp_sofia_session_destroy
};


mrcp_status_t mrcp_descriptor_generate_by_sdp_offer(mrcp_descriptor_t *descriptor, sdp_session_t *sdp, apr_pool_t *pool);
size_t mrcp_sdp_answer_generate_by_mrcp_descriptor(char *buffer, size_t size, mrcp_descriptor_t *descriptor);
size_t mrcp_resource_discover_sdp_generate(char *buffer, size_t size, mrcp_descriptor_t *descriptor);

static APR_INLINE mrcp_server_sofia_agent_t* mrcp_sofia_agent_get(mrcp_module_t *module)
{
	return ((mrcp_signaling_agent_t*)module)->object;
}

static mrcp_server_sofia_session_t* mrcp_sofia_session_create(mrcp_server_sofia_agent_t *sofia_agent)
{
	mrcp_server_sofia_session_t *sofia_session;
	apr_pool_t *pool;
	const mrcp_signaling_agent_event_set_t *event_set = sofia_agent->signaling_agent->agent_event_set;
	mrcp_session_t *mrcp_session = event_set->on_session_create(sofia_agent->signaling_agent,NULL,&pool);
	if(!mrcp_session || !pool) {
		return NULL;
	}
	sofia_session = apr_palloc(pool,sizeof(*sofia_session));
	sofia_session->home = su_home_new(sizeof(*sofia_session->home));
	sofia_session->channel = mrcp_server_signaling_channel_create(
		sofia_agent->signaling_agent,
		&signaling_channel_method_set,
		sofia_session,
		pool);
	event_set->on_channel_attach(mrcp_session,sofia_session->channel);
	return sofia_session;
}

static mrcp_status_t mrcp_sofia_session_answer(mrcp_signaling_channel_t *channel)
{
	mrcp_server_sofia_session_t *sofia_session = channel->object;
	const char *local_sdp_str = NULL;
	char sdp_str[2048];

	if(!sofia_session || !sofia_session->nh) {
		return MRCP_STATUS_FAILURE;
	}

	if(mrcp_sdp_answer_generate_by_mrcp_descriptor(sdp_str,sizeof(sdp_str),&channel->local_descriptor) > 0) {
		local_sdp_str = sdp_str;
		apt_log(APT_PRIO_INFO,"Local SDP\n[%s]\n", local_sdp_str);
	}

	nua_respond(sofia_session->nh, SIP_200_OK, 
		        SIPTAG_CONTACT_STR(channel->agent->sip_contact_str),
				TAG_IF(local_sdp_str,SOATAG_USER_SDP_STR(local_sdp_str)),
			    NUTAG_AUTOANSWER(0),
				TAG_END());
	
	return MRCP_STATUS_SUCCESS;
}

static mrcp_status_t mrcp_sofia_session_destroy(mrcp_signaling_channel_t *channel)
{
	mrcp_server_sofia_session_t *sofia_session = channel->object;

	if(sofia_session) {
		if(sofia_session->nh) {
			nua_handle_bind(sofia_session->nh, NULL);
			nua_handle_destroy(sofia_session->nh);
		}
		if(sofia_session->home) {
			su_home_unref(sofia_session->home);
			sofia_session->home = NULL;
		}
		channel->object = NULL;
	}
	return MRCP_STATUS_SUCCESS;
}

static mrcp_status_t mrcp_sofia_on_call_received(mrcp_sofia_event_t *sofia_event)
{
	int offer_recv = 0, answer_recv = 0, offer_sent = 0, answer_sent = 0;
	const char *local_sdp_str = NULL, *remote_sdp_str = NULL;
	mrcp_server_sofia_session_t *sofia_session;
	sdp_parser_t *parser = NULL;
	sdp_session_t *sdp = NULL;

	tl_gets(sofia_event->tags, 
			NUTAG_OFFER_RECV_REF(offer_recv),
			NUTAG_ANSWER_RECV_REF(answer_recv),
			NUTAG_OFFER_SENT_REF(offer_sent),
			NUTAG_ANSWER_SENT_REF(answer_sent),
			SOATAG_LOCAL_SDP_STR_REF(local_sdp_str),
			SOATAG_REMOTE_SDP_STR_REF(remote_sdp_str),
			TAG_END());
	
	sofia_session = sofia_event->sofia_session;
	if(!sofia_session) {
		sofia_session = mrcp_sofia_session_create(sofia_event->sofia_agent);
		if(!sofia_session) {
			return MRCP_STATUS_FAILURE;
		}
		nua_handle_bind(sofia_event->nh, sofia_session);
		sofia_session->nh = sofia_event->nh;
	}

	if(!sofia_session->channel) {
		return MRCP_STATUS_FAILURE;
	}

	local_sdp_str = NULL;
	if(remote_sdp_str) {
		apt_log(APT_PRIO_INFO,"Remote SDP\n[%s]\n", remote_sdp_str);

		parser = sdp_parse(sofia_session->home,remote_sdp_str,(int)strlen(remote_sdp_str),0);
		sdp = sdp_session(parser);
		
		mrcp_descriptor_generate_by_sdp_offer(
							&sofia_session->channel->remote_descriptor,
							sdp,
							sofia_session->channel->pool);

		sdp_parser_free(parser);
	}

	return sofia_session->channel->event_set->on_offer(sofia_session->channel);
}

static mrcp_status_t mrcp_sofia_on_call_terminated(mrcp_sofia_event_t *sofia_event)
{
	mrcp_server_sofia_session_t *sofia_session = sofia_event->sofia_session;
	if(sofia_session) {
		sofia_session->channel->event_set->on_terminate(sofia_session->channel);
	}
	return MRCP_STATUS_SUCCESS;
}

static mrcp_status_t mrcp_sofia_on_state_changed(mrcp_sofia_event_t *sofia_event)
{
	int ss_state = nua_callstate_init;
	tl_gets(sofia_event->tags, 
			NUTAG_CALLSTATE_REF(ss_state),
			TAG_END()); 
	
	apt_log(APT_PRIO_NOTICE,"SIP Call State [%s]\n", nua_callstate_name(ss_state));

	switch(ss_state) {
		case nua_callstate_received:
			mrcp_sofia_on_call_received(sofia_event);
			break;
		case nua_callstate_terminated:
			mrcp_sofia_on_call_terminated(sofia_event);
			break;
	}
	return MRCP_STATUS_SUCCESS;
}

static mrcp_status_t mrcp_sofia_on_resource_discover(mrcp_sofia_event_t *sofia_event)
{
	char sdp_str[2048];
	char *local_sdp_str = NULL;

	apt_log(APT_PRIO_NOTICE,"SIP Options\n");

	if(mrcp_resource_discover_sdp_generate(sdp_str,sizeof(sdp_str),&sofia_event->sofia_agent->signaling_agent->capabilities) > 0) {
		local_sdp_str = sdp_str;
		apt_log(APT_PRIO_INFO,"Local SDP\n[%s]\n", local_sdp_str);
	}

	nua_respond(sofia_event->nh, SIP_200_OK, 
#ifdef NUA_OPTIONS_WITH_SDP
				NUTAG_WITH_CURRENT(sofia_event->nua),
#endif
				SIPTAG_CONTACT_STR(sofia_event->sofia_agent->signaling_agent->sip_contact_str),
				TAG_IF(local_sdp_str,SOATAG_USER_SDP_STR(local_sdp_str)),
				TAG_END());

	return MRCP_STATUS_SUCCESS;
}

static mrcp_status_t mrcp_sofia_agent_signal_handler(mrcp_module_t *module, apt_task_msg_t *msg)
{
	mrcp_sofia_event_t *sofia_event = (mrcp_sofia_event_t*)msg->data;
	if(!sofia_event) {
		return MRCP_STATUS_FAILURE;
	}

	if(sofia_event->event_id == MRCP_SOFIA_EVENT_AGENT_TERMINATED) {
		if(module->event_set->on_close) {
			module->event_set->on_close(module);
		}
		return MRCP_STATUS_SUCCESS;
	}

	if(!sofia_event->sofia_agent) {
		return MRCP_STATUS_FAILURE;
	}

	apt_log(APT_PRIO_DEBUG,"Process SIP Event [%s] Status %d %s\n", nua_event_name(sofia_event->nua_event), sofia_event->status, sofia_event->phrase);
	switch(sofia_event->nua_event) {
		case nua_i_state:
			mrcp_sofia_on_state_changed(sofia_event);
			break;
		case nua_i_options:
			mrcp_sofia_on_resource_discover(sofia_event);
			break;
		default:
			break;
	}

	return MRCP_STATUS_SUCCESS;
}

static void mrcp_sofia_agent_on_terminate_requested(void *data)
{
	mrcp_server_sofia_agent_t *sofia_agent = data;

	apt_log(APT_PRIO_DEBUG,"Sofia SIP Terminate Requested\n");
	while(!sofia_agent->nua) {
		/* wait for nua to start to be able to terminate */
		apt_log(APT_PRIO_DEBUG,"Wait for NUA to Start\n");
		apt_task_delay(500);
	}

	if(sofia_agent->nua) {
		apt_log(APT_PRIO_DEBUG,"Send Shutdown Signal to NUA\n");
		nua_shutdown(sofia_agent->nua);
	}
}

static void mrcp_sofia_agent_on_terminate_complete(void *data)
{
	mrcp_server_sofia_agent_t *sofia_agent = data;
	apt_task_msg_t *task_msg;
	task_msg = apt_producer_task_msg_get(sofia_agent->producer_task);
	if(task_msg) {
		mrcp_sofia_event_t *sofia_event = (mrcp_sofia_event_t*)task_msg->data;
		sofia_event->event_id = MRCP_SOFIA_EVENT_AGENT_TERMINATED;
		sofia_agent->signaling_agent->module.signal(
			&sofia_agent->signaling_agent->module,
			task_msg);
	}
}

/* This callback will be called by SIP stack to process incoming events */
static void mrcp_sofia_event_callback( nua_event_t                  nua_event,
									   int                          status,
									   char const                  *phrase,
									   nua_t                       *nua,
									   mrcp_server_sofia_agent_t   *sofia_agent,
									   nua_handle_t                *nh,
									   mrcp_server_sofia_session_t *sofia_session,
									   sip_t const                 *sip,
									   tagi_t                       tags[])
{
	mrcp_status_t mrcp_process = MRCP_STATUS_FAILURE;
	
	apt_log(APT_PRIO_INFO,"Recieve SIP Event [%s] Status %d %s\n",nua_event_name(nua_event),status,phrase);

	switch(nua_event) {
		case nua_i_state:
		case nua_i_options:
			mrcp_process = MRCP_STATUS_SUCCESS;
			break;
		case nua_r_shutdown:
			/* break main loop of sofia thread */
			su_root_break(sofia_agent->root);
			break;
		default: 
			break;
	}

	if(mrcp_process == MRCP_STATUS_SUCCESS) {
		apt_task_msg_t *task_msg = apt_producer_task_msg_get(sofia_agent->producer_task);
		mrcp_sofia_event_t *sofia_event = (mrcp_sofia_event_t*)task_msg->data;
		sofia_event->event_id = MRCP_SOFIA_EVENT_AGENT_PROCESS;
		
		sofia_event->nua_event = nua_event;
		sofia_event->status = status;
		sofia_event->phrase = phrase;
		sofia_event->nua = nua;
		sofia_event->sofia_agent = sofia_agent;
		sofia_event->nh = nh;
		sofia_event->sofia_session = sofia_session;
		sofia_event->sip = sip;
		sofia_event->tags = tags;
		sofia_agent->signaling_agent->module.signal(&sofia_agent->signaling_agent->module,
													task_msg);
	}
}

static void mrcp_sofia_main_loop(void *data)
{
	char sip_bind_url[128];
	mrcp_server_sofia_agent_t *sofia_agent = data;
	if(!sofia_agent) {
		return;
	}
	
	/* Initialize Sofia-SIP library and create event loop */
	su_init();
	sofia_agent->root = su_root_create(NULL);

	/* Create a user agent instance. The stack will call the 'event_callback()' 
	 * callback when events such as succesful registration to network, 
	 * an incoming call, etc, occur. 
	 */
	sprintf(sip_bind_url,"sip:%s:%d","0.0.0.0",sofia_agent->signaling_agent->sip_port);
	sofia_agent->nua = nua_create(sofia_agent->root, /* Event loop */
					mrcp_sofia_event_callback, /* Callback for processing events */
					sofia_agent, /* Additional data to pass to callback */
					NUTAG_URL(sip_bind_url), /* Address to bind to */
					TAG_END()); /* Last tag should always finish the sequence */
	if(sofia_agent->nua) {

		nua_set_params(sofia_agent->nua,
					   NUTAG_AUTOANSWER(0),
					   NUTAG_APPL_METHOD("OPTIONS"),
					   SIPTAG_USER_AGENT_STR(MRCP_SERVER_SOFIA_AGENT),
					   TAG_END());

		/* Run event loop */
		su_root_run(sofia_agent->root);
		
		/* Destroy allocated resources */
		nua_destroy(sofia_agent->nua);
	}
	su_root_destroy(sofia_agent->root);
	su_deinit();
}


static mrcp_module_state_t mrcp_sofia_agent_open(mrcp_module_t *module)
{
	mrcp_server_sofia_agent_t *sofia_agent = mrcp_sofia_agent_get(module);
	apt_log(APT_PRIO_INFO,"Open Sofia SIP Agent\n");
	apt_producer_task_start(sofia_agent->producer_task);
	return MODULE_STATE_OPEN_INPROGRESS;
}

static mrcp_module_state_t mrcp_sofia_agent_close(mrcp_module_t *module)
{
	mrcp_server_sofia_agent_t *sofia_agent = mrcp_sofia_agent_get(module);
	apt_log(APT_PRIO_INFO,"Close Sofia SIP Agent\n");
	apt_producer_task_terminate(sofia_agent->producer_task,MRCP_STATUS_FAILURE);
	return MODULE_STATE_CLOSE_INPROGRESS;
}


static void mrcp_sofia_agent_config(mrcp_server_sofia_agent_t *sofia_agent, const char *ip, unsigned short sip_port, unsigned short mrcp_port)
{
	char sip_contact_buf[128];
	if(ip) {
		sofia_agent->signaling_agent->capabilities.ip = apr_pstrdup(sofia_agent->pool,ip);
	}
	sofia_agent->signaling_agent->mrcp_port = mrcp_port;
	sofia_agent->signaling_agent->sip_port = sip_port;

	sprintf(sip_contact_buf,"sip:%s:%d",sofia_agent->signaling_agent->capabilities.ip,sofia_agent->signaling_agent->sip_port);
	sofia_agent->signaling_agent->sip_contact_str = apr_pstrdup(sofia_agent->pool,sip_contact_buf);
}

static mrcp_status_t mrcp_sofia_agent_destroy(mrcp_module_t *module)
{
	mrcp_server_sofia_agent_t *sofia_agent = mrcp_sofia_agent_get(module);

	apt_log(APT_PRIO_NOTICE,"Destroy Sofia SIP Agent\n");

	apt_producer_task_destroy(sofia_agent->producer_task);
	sofia_agent->pool = NULL;
	return MRCP_STATUS_SUCCESS;
}

mrcp_signaling_agent_t* mrcp_server_sofia_agent_create(const char *ip, unsigned short sip_port, unsigned short mrcp_port, 
													   const char *resource_location, apr_pool_t *pool)
{
	apt_task_msg_pool_t *msg_pool;
	mrcp_server_sofia_agent_t *sofia_agent = apr_palloc(pool,sizeof(mrcp_server_sofia_agent_t));
	mrcp_signaling_agent_t *agent = mrcp_server_signaling_agent_create(&module_method_set,sofia_agent,pool);
	apt_log(APT_PRIO_NOTICE,"Create Sofia SIP Agent %s:%hu\n",ip,sip_port);
	sofia_agent->pool = pool;
	sofia_agent->signaling_agent = agent;
	sofia_agent->root = NULL;
	sofia_agent->nua = NULL;
	mrcp_sofia_agent_config(sofia_agent,ip,sip_port,mrcp_port);

	msg_pool = apt_task_msg_pool_create_waitable_static(sizeof(mrcp_sofia_event_t),pool);
	sofia_agent->producer_task = apt_producer_task_create(sofia_agent,mrcp_sofia_main_loop,msg_pool,pool);
	if(sofia_agent->producer_task) {
		apt_task_t *task = apt_producer_task_get(sofia_agent->producer_task);
		apt_task_event_handler_set(task,TASK_STATE_TERMINATE_REQUESTED,sofia_agent,mrcp_sofia_agent_on_terminate_requested);
		apt_task_event_handler_set(task,TASK_STATE_TERMINATE_COMPLETED,sofia_agent,mrcp_sofia_agent_on_terminate_complete);
	}
	return agent;
}
