/*
 * 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_client_sofia_agent_t mrcp_client_sofia_agent_t;
#define NUA_MAGIC_T mrcp_client_sofia_agent_t

typedef struct mrcp_client_sofia_session_t mrcp_client_sofia_session_t;
#define NUA_HMAGIC_T mrcp_client_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_client_signaling_agent.h"
#include "apt_producer_task.h"

struct mrcp_client_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_client_sofia_session_t {
	mrcp_signaling_channel_t *channel;
	su_home_t                *home;
	nua_handle_t             *nh;

	mrcp_status_t             initiating;
};

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_client_sofia_agent_t   *sofia_agent;
	nua_handle_t                *nh;
	mrcp_client_sofia_session_t *sofia_session;
	sip_t const                 *sip;
	tagi_t                      *tags;
};

#define MRCP_CLIENT_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_resource_discover(mrcp_signaling_agent_t *agent);
static mrcp_signaling_channel_t* mrcp_sofia_session_create(mrcp_signaling_agent_t *agent, apr_pool_t *pool);

static const mrcp_signaling_agent_method_set_t signaling_agent_method_set = {
	mrcp_sofia_resource_discover,
	mrcp_sofia_session_create,
};

static mrcp_status_t mrcp_sofia_session_offer(mrcp_signaling_channel_t *channel);
static mrcp_status_t mrcp_sofia_session_terminate(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 = {
	mrcp_sofia_session_offer,
	NULL, /*answer*/
	mrcp_sofia_session_terminate,
	mrcp_sofia_session_destroy
};



size_t mrcp_sdp_offer_generate_by_mrcp_descriptor(char *buffer, size_t size, mrcp_descriptor_t *descriptor);
mrcp_status_t mrcp_descriptor_generate_by_sdp_session(mrcp_descriptor_t *descriptor, sdp_session_t *sdp, apr_pool_t *pool);

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

static mrcp_status_t mrcp_client_sofia_on_call_ready(mrcp_sofia_event_t *sofia_event)
{
	const char *local_sdp_str = NULL, *remote_sdp_str = NULL;
	sdp_parser_t *parser = NULL;
	sdp_session_t *sdp = NULL;
	mrcp_signaling_channel_t *channel = sofia_event->sofia_session->channel;

	tl_gets(sofia_event->tags, 
			SOATAG_LOCAL_SDP_STR_REF(local_sdp_str),
			SOATAG_REMOTE_SDP_STR_REF(remote_sdp_str),
			TAG_END());

	if(remote_sdp_str) {
		apt_log(APT_PRIO_INFO,"Remote SDP\n[%s]\n", remote_sdp_str);
		parser = sdp_parse(sofia_event->sofia_session->home,remote_sdp_str,(int)strlen(remote_sdp_str),0);
		sdp = sdp_session(parser);
		
		mrcp_descriptor_generate_by_sdp_session(&channel->remote_descriptor,sdp,channel->pool);
	}

	sofia_event->sofia_session->initiating = MRCP_STATUS_FAILURE;
	channel->event_set->on_answer(channel);
	
	if(parser) {
		sdp_parser_free(parser);
	}
	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_ready:
			mrcp_client_sofia_on_call_ready(sofia_event);
			break;
		case nua_callstate_terminated:
			sofia_event->sofia_session->channel->event_set->on_terminate(sofia_event->sofia_session->channel);
			break;
	}
	return MRCP_STATUS_SUCCESS;
}

static mrcp_status_t mrcp_sofia_on_resource_discover(mrcp_sofia_event_t *sofia_event)
{
	const char *remote_sdp_str = NULL;

	tl_gets(sofia_event->tags, 
			SOATAG_REMOTE_SDP_STR_REF(remote_sdp_str),
			TAG_END());

	mrcp_descriptor_init(&sofia_event->sofia_agent->signaling_agent->server_capabilities);
	if(remote_sdp_str) {
		apt_log(APT_PRIO_INFO,"Remote SDP\n[%s]\n", remote_sdp_str);
	}

	sofia_event->sofia_agent->signaling_agent->event_set->on_resource_discover(sofia_event->sofia_agent->signaling_agent);
	
	if(sofia_event->nh) {
		nua_handle_destroy(sofia_event->nh);
		sofia_event->nh = NULL;
	}
	return MRCP_STATUS_SUCCESS;
}

static mrcp_signaling_channel_t* mrcp_sofia_session_create(mrcp_signaling_agent_t *agent, apr_pool_t *pool)
{
	mrcp_client_sofia_agent_t *sofia_agent = mrcp_client_signaling_agent_object_get(agent);
	mrcp_client_sofia_session_t *sofia_session = apr_palloc(pool,sizeof(mrcp_client_sofia_session_t));
	sofia_session->home = su_home_new(sizeof(*sofia_session->home));
	sofia_session->channel = mrcp_client_signaling_channel_create(agent,&signaling_channel_method_set,sofia_session,pool);
	sofia_session->initiating = MRCP_STATUS_FAILURE;

	sofia_session->nh = nua_handle(sofia_agent->nua,sofia_session,
				SIPTAG_TO_STR(agent->sip_to_str),
				SIPTAG_FROM_STR(agent->sip_from_str),
				SIPTAG_CONTACT_STR(agent->sip_contact_str),
				TAG_END());

	return sofia_session->channel;
}

static mrcp_status_t mrcp_sofia_session_destroy(mrcp_signaling_channel_t *channel)
{
	mrcp_client_sofia_session_t *sofia_session = mrcp_client_signaling_channel_object_get(channel);

	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;
		}
	}
	return MRCP_STATUS_SUCCESS;
}



static mrcp_status_t mrcp_sofia_session_offer(mrcp_signaling_channel_t *channel)
{
	char sdp_str[2048];
	char *local_sdp_str = NULL;
	mrcp_signaling_agent_t *agent = mrcp_client_signaling_channel_agent_get(channel);
	mrcp_client_sofia_agent_t *sofia_agent = mrcp_client_signaling_agent_object_get(agent);
	mrcp_client_sofia_session_t *sofia_session = mrcp_client_signaling_channel_object_get(channel);
	if(!sofia_agent || !sofia_session || !sofia_session->nh) {
		return MRCP_STATUS_FAILURE;
	}
	
	if(mrcp_sdp_offer_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);
	}

	sofia_session->initiating = MRCP_STATUS_SUCCESS;
	nua_invite(sofia_session->nh,
			   TAG_IF(local_sdp_str,SOATAG_USER_SDP_STR(local_sdp_str)),
			   TAG_END());

	return MRCP_STATUS_SUCCESS;
}

static mrcp_status_t mrcp_sofia_session_terminate(mrcp_signaling_channel_t *channel)
{
	mrcp_client_sofia_session_t *sofia_session = mrcp_client_signaling_channel_object_get(channel);
	if(!sofia_session || !sofia_session->nh) {
		return MRCP_STATUS_FAILURE;
	}

	if(sofia_session->initiating == MRCP_STATUS_SUCCESS) {
		nua_cancel(sofia_session->nh,TAG_END());
	}
	else {
		nua_bye(sofia_session->nh,TAG_END());
	}
	return MRCP_STATUS_SUCCESS;
}

static mrcp_status_t mrcp_sofia_resource_discover(mrcp_signaling_agent_t *agent)
{
	mrcp_client_sofia_agent_t *sofia_agent = mrcp_client_signaling_agent_object_get(agent);

	nua_handle_t *nh = nua_handle(sofia_agent->nua,NULL,
				SIPTAG_TO_STR(agent->sip_to_str),
				SIPTAG_FROM_STR(agent->sip_from_str),
				SIPTAG_CONTACT_STR(agent->sip_contact_str),
				TAG_END());
	
	nua_options(nh,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 && 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_r_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_client_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_client_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;
		mrcp_client_signaling_agent_signal(sofia_agent->signaling_agent,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_client_sofia_agent_t   *sofia_agent,
                                      nua_handle_t                *nh,
                                      mrcp_client_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_r_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;
		mrcp_client_signaling_agent_signal(sofia_agent->signaling_agent,task_msg);
	}
}

static void mrcp_sofia_main_loop(void *data)
{
	char sip_bind_url[128];
	mrcp_client_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->client_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_CLIENT_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_client_sofia_agent_t *sofia_agent = mrcp_sofia_agent_get(module);
	apt_log(APT_PRIO_INFO,"Open Sofia SIP Client 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_client_sofia_agent_t *sofia_agent = mrcp_sofia_agent_get(module);
	apt_log(APT_PRIO_INFO,"Close Sofia SIP Client 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_client_sofia_agent_t *sofia_agent, 
									const char *client_ip, unsigned short client_port, 
									const char *server_ip, unsigned short server_port,
									const char *resource_location)
{
	char sip_str[128];
	if(client_ip) {
		sofia_agent->signaling_agent->client_ip = apr_pstrdup(sofia_agent->pool,client_ip);
	}
	if(server_ip) {
		sofia_agent->signaling_agent->server_ip = apr_pstrdup(sofia_agent->pool,server_ip);
	}
	if(resource_location) {
		sofia_agent->signaling_agent->resource_location = apr_pstrdup(sofia_agent->pool,resource_location);
	}
	sofia_agent->signaling_agent->client_sip_port = client_port;
	sofia_agent->signaling_agent->server_sip_port = server_port;
	sprintf(sip_str,"sip:%s:%d",sofia_agent->signaling_agent->client_ip,sofia_agent->signaling_agent->client_sip_port);
	sofia_agent->signaling_agent->sip_contact_str = apr_pstrdup(sofia_agent->pool,sip_str);
	
	sprintf(sip_str,"sip:%s",sofia_agent->signaling_agent->client_ip);
	sofia_agent->signaling_agent->sip_from_str = apr_pstrdup(sofia_agent->pool,sip_str);

	if(sofia_agent->signaling_agent->resource_location && sofia_agent->signaling_agent->resource_location != '\0') {
		sprintf(sip_str,"sip:%s@%s:%d",
			sofia_agent->signaling_agent->resource_location,
			sofia_agent->signaling_agent->server_ip,
			sofia_agent->signaling_agent->server_sip_port);
	}
	else {
		sprintf(sip_str,"sip:%s:%d",sofia_agent->signaling_agent->server_ip,sofia_agent->signaling_agent->server_sip_port);
	}
	sofia_agent->signaling_agent->sip_to_str = apr_pstrdup(sofia_agent->pool,sip_str);
}


static mrcp_status_t mrcp_sofia_agent_destroy(mrcp_module_t *module)
{
	mrcp_client_sofia_agent_t *sofia_agent = mrcp_sofia_agent_get(module);
	apt_log(APT_PRIO_NOTICE,"Destroy Sofia SIP Client Agent\n");
	apt_producer_task_destroy(sofia_agent->producer_task);
	sofia_agent->pool = NULL;
	return MRCP_STATUS_SUCCESS;
}


mrcp_signaling_agent_t* mrcp_client_sofia_agent_create(const char *client_ip, unsigned short client_port, 
													   const char *server_ip, unsigned short server_port, 
													   const char *resource_location, apr_pool_t *pool)
{
	apt_task_msg_pool_t *msg_pool;
	mrcp_client_sofia_agent_t *sofia_agent = apr_palloc(pool,sizeof(mrcp_client_sofia_agent_t));
	mrcp_signaling_agent_t *agent = mrcp_client_signaling_agent_create(
		&signaling_agent_method_set,
		&module_method_set,
		sofia_agent,
		pool);
	apt_log(APT_PRIO_NOTICE,"Create Sofia SIP Client Agent %s:%hu -> %s:%hu\n",client_ip,client_port,server_ip,server_port);
	sofia_agent->pool = pool;
	sofia_agent->signaling_agent = agent;
	sofia_agent->root = NULL;
	sofia_agent->nua = NULL;
	mrcp_sofia_agent_config(sofia_agent,client_ip,client_port,server_ip,server_port,resource_location);

	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 sofia_agent->signaling_agent;
}
