/*
 * This file is distributed as part of the MariaDB Corporation MaxScale.  It 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,
 * version 2.
 *
 * 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, write to the Free Software Foundation, Inc., 51
 * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Copyright MariaDB Corporation Ab 2013-2014
 */

/**
 * @file session.c  - A representation of the session within the gateway.
 *
 * @verbatim
 * Revision History
 *
 * Date		Who			Description
 * 17/06/13	Mark Riddoch		Initial implementation
 * 02/09/13	Massimiliano Pinto	Added session refcounter
 * 29/05/14	Mark Riddoch		Addition of filter mechanism
 *
 * @endverbatim
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <session.h>
#include <service.h>
#include <router.h>
#include <dcb.h>
#include <spinlock.h>
#include <atomic.h>
#include <skygw_utils.h>
#include <log_manager.h>

/** Defined in log_manager.cc */
extern int            lm_enabled_logfiles_bitmask;
extern size_t         log_ses_count[];
extern __thread log_info_t tls_log_info;

/** Global session id; updated safely by holding session_spin */
static size_t session_id;

static SPINLOCK	session_spin = SPINLOCK_INIT;
static SESSION	*allSessions = NULL;


static int session_setup_filters(SESSION *session);

/**
 * Allocate a new session for a new client of the specified service.
 *
 * Create the link to the router session by calling the newSession
 * entry point of the router using the router instance of the
 * service this session is part of.
 *
 * @param service	The service this connection was established by
 * @param client_dcb	The client side DCB
 * @return		The newly created session or NULL if an error occured
 */
SESSION *
session_alloc(SERVICE *service, DCB *client_dcb)
{
        SESSION 	*session;

        session = (SESSION *)calloc(1, sizeof(SESSION));
        ss_info_dassert(session != NULL,
                        "Allocating memory for session failed.");
        
        if (session == NULL) 
	{
                LOGIF(LE, (skygw_log_write_flush(
                        LOGFILE_ERROR,
                        "Error : Failed to allocate memory for "
                        "session object due error %d, %s.",
                        errno,
                        strerror(errno))));
		goto return_session;
        }
#if defined(SS_DEBUG)
        session->ses_chk_top = CHK_NUM_SESSION;
        session->ses_chk_tail = CHK_NUM_SESSION;
#endif
        spinlock_init(&session->ses_lock);
        /*<
         * Prevent backend threads from accessing before session is completely
         * initialized.
         */
        spinlock_acquire(&session->ses_lock);
        session->service = service;
	session->client = client_dcb;
	session->n_filters = 0;
	memset(&session->stats, 0, sizeof(SESSION_STATS));
	session->stats.connect = time(0);
	session->state = SESSION_STATE_ALLOC;
        /*<
	 * Associate the session to the client DCB and set the reference count on
	 * the session to indicate that there is a single reference to the
         * session. There is no need to protect this or use atomic add as the
         * session has not been made available to the other threads at this
         * point.
         */
        session->data = client_dcb->data;
	client_dcb->session = session;
	session->refcount = 1;
        /*<
         * This indicates that session is ready to be shared with backend
         * DCBs. Note that this doesn't mean that router is initialized yet!
         */
        session->state = SESSION_STATE_READY;
        
        /*< Release session lock */
        spinlock_release(&session->ses_lock);

	/*
	 * Only create a router session if we are not the listening 
	 * DCB or an internal DCB. Creating a router session may create a connection to a
	 * backend server, depending upon the router module implementation
	 * and should be avoided for the listener session
	 *
	 * Router session creation may create other DCBs that link to the
	 * session, therefore it is important that the session lock is
         * relinquished beforethe router call.
	 */
	if (client_dcb->state != DCB_STATE_LISTENING && 
                client_dcb->dcb_role != DCB_ROLE_INTERNAL)
	{
		session->router_session =
                    service->router->newSession(service->router_instance,
                                                session);

                if (session->router_session == NULL) 
		{
                        /**
                         * Inform other threads that session is closing.
                         */
                        session->state = SESSION_STATE_STOPPING;
                        /*<
                         * Decrease refcount, set dcb's session pointer NULL
                         * and set session pointer to NULL.
                         */
                        session_free(session);
                        client_dcb->session = NULL;
                        session = NULL;
                        LOGIF(LE, (skygw_log_write_flush(
                                LOGFILE_ERROR,
                                "Error : Failed to create %s session.",
                                service->name)));
                        
                        goto return_session;
                }
		/*
		 * Pending filter chain being setup set the head of the chain to
		 * be the router. As filters are inserted the current head will
		 * be pushed to the filter and the head updated.
		 *
		 * NB This dictates that filters are created starting at the end
		 * of the chain nearest the router working back to the client
		 * protocol end of the chain.
		 */
		session->head.instance = service->router_instance;
		session->head.session = session->router_session;

		session->head.routeQuery = (void *)(service->router->routeQuery);

		session->tail.instance = session;
		session->tail.session = session;
		session->tail.clientReply = session_reply;

		if (service->n_filters > 0)
		{
			if (!session_setup_filters(session))
			{
				/**
				 * Inform other threads that session is closing.
				 */
				session->state = SESSION_STATE_STOPPING;
				/*<
				 * Decrease refcount, set dcb's session pointer NULL
				 * and set session pointer to NULL.
				 */
				session_free(session);
				client_dcb->session = NULL;
				session = NULL;
				LOGIF(LE, (skygw_log_write(
					LOGFILE_ERROR,
					"Error : Setting up filters failed. "
					"Terminating session %s.",
					service->name)));
				goto return_session;
			}
		}
        }

        spinlock_acquire(&session->ses_lock);
                
        if (session->state != SESSION_STATE_READY)
        {
		spinlock_release(&session->ses_lock);
		session_free(session);
                client_dcb->session = NULL;
                session = NULL;
                LOGIF(LE, (skygw_log_write_flush(
                        LOGFILE_ERROR,
                        "Error : Failed to create %s session.",
                        service->name)));
                spinlock_release(&session_spin);
        }
        else
        {
                session->state = SESSION_STATE_ROUTER_READY;
		spinlock_release(&session->ses_lock);		
		spinlock_acquire(&session_spin);
		/** Assign a session id and increase */
		session->ses_id = ++session_id; 
		session->next = allSessions;
                allSessions = session;
                spinlock_release(&session_spin);
                
		if (session->client->user == NULL)
		{
			LOGIF(LT, (skygw_log_write(
				LOGFILE_TRACE,
				"Started session [%lu] for %s service ",
				session->ses_id,
				service->name)));
		}
		else
		{
			LOGIF(LT, (skygw_log_write(
				LOGFILE_TRACE,
				"Started %s client session [%lu] for '%s' from %s",
				service->name,
				session->ses_id,
				session->client->user,
				session->client->remote)));			
		}
		atomic_add(&service->stats.n_sessions, 1);
                atomic_add(&service->stats.n_current, 1);
                CHK_SESSION(session);
        }        
return_session:
	return session;
}

/**
 * Enable specified logging for the current session and increase logger 
 * counter.
 * Generic logging setting has precedence over session-specific setting.
 * 
 * @param ses	session 
 * @param id	logfile identifier
 */
void session_enable_log(
	SESSION*     ses,
	logfile_id_t id)
{
	ses->ses_enabled_logs |= id;
	atomic_add((int *)&log_ses_count[id], 1);
}

/**
 * Disable specified logging for the current session and decrease logger
 * counter.
 * Generic logging setting has precedence over session-specific setting.
 * 
 * @param ses	session
 * @param id	logfile identifier
 */
void session_disable_log(
	SESSION* ses, 
	logfile_id_t id)
{
	if (ses->ses_enabled_logs & id)
	{
		ses->ses_enabled_logs &= ~id;
		atomic_add((int *)&log_ses_count[id], -1);
	}
}

/**
 * Link a session to a DCB.
 *
 * @param session	The session to link with the dcb
 * @param dcb		The DCB to be linked
 * @return		True if the session was sucessfully linked to the DCB
 */
bool
session_link_dcb(SESSION *session, DCB *dcb)
{
	spinlock_acquire(&session->ses_lock);
        ss_info_dassert(session->state != SESSION_STATE_FREE,
            "If session->state is SESSION_STATE_FREE then this attempt to "
            "access freed memory block.");
	if (session->state == SESSION_STATE_FREE)
	{
		spinlock_release(&session->ses_lock);
		return false;
	}
	atomic_add(&session->refcount, 1);
	dcb->session = session;
	spinlock_release(&session->ses_lock);
	return true;
}

int session_unlink_dcb(
        SESSION* session,
        DCB*     dcb)
{
        int nlink;
        
        CHK_SESSION(session);
        
	spinlock_acquire(&session->ses_lock);
        ss_dassert(session->refcount > 0);
        /*<
         * Remove dcb from session's router_client_session.
         */
        nlink = atomic_add(&session->refcount, -1);
        nlink -= 1;

        if (nlink == 0)
	{
                session->state = SESSION_STATE_FREE;
        }

        if (dcb != NULL)
        {
                 dcb->session = NULL;
        }
        spinlock_release(&session->ses_lock);
        
        return nlink;
}

/**
 * Deallocate the specified session
 *
 * @param session	The session to deallocate
 */
bool session_free(
        SESSION *session)
{
        bool    succp = false;
        SESSION *ptr;
        int     nlink;
	int	i;

        CHK_SESSION(session);

        /*<
         * Remove one reference. If there are no references left,
         * free session.
         */
        nlink = session_unlink_dcb(session, NULL);

        if (nlink != 0) {
                ss_dassert(nlink > 0);
                goto return_succp;
        }
        
	/* First of all remove from the linked list */
	spinlock_acquire(&session_spin);
	if (allSessions == session)
	{
		allSessions = session->next;
	}
	else
	{
		ptr = allSessions;
		while (ptr && ptr->next != session)
		{
			ptr = ptr->next;
		}
		if (ptr)
			ptr->next = session->next;
	}
	spinlock_release(&session_spin);
	atomic_add(&session->service->stats.n_current, -1);

	/* Free router_session and session */
        if (session->router_session) {
                session->service->router->freeSession(
                        session->service->router_instance,
                        session->router_session);
        }
	if (session->n_filters)
	{
		for (i = 0; i < session->n_filters; i++)
		{
			if (session->filters[i].filter)
				session->filters[i].filter->obj->closeSession(
					session->filters[i].instance,
					session->filters[i].session);
		}
		for (i = 0; i < session->n_filters; i++)
		{
			if (session->filters[i].filter)
				session->filters[i].filter->obj->freeSession(
					session->filters[i].instance,
					session->filters[i].session);
		}
		free(session->filters);
	}
	
	LOGIF(LT, (skygw_log_write(
		LOGFILE_TRACE,
		"Stopped %s client session [%lu]",
		session->service->name,
		session->ses_id)));
	
	/** Disable trace and decrease trace logger counter */
	session_disable_log(session, LT);
	
	free(session);
        succp = true;
        
return_succp :
        return succp;
}

/**
 * Check to see if a session is valid, i.e. in the list of all sessions
 *
 * @param session	Session to check
 * @return		1 if the session is valid otherwise 0
 */
int
session_isvalid(SESSION *session)
{
SESSION		*ptr;
int		rval = 0;

	spinlock_acquire(&session_spin);
	ptr = allSessions;
	while (ptr)
	{
		if (ptr == session)
		{
			rval = 1;
			break;
		}
		ptr = ptr->next;
	}
	spinlock_release(&session_spin);

	return rval;
}

/**
 * Print details of an individual session
 *
 * @param session	Session to print
 */
void
printSession(SESSION *session)
{
struct tm	result;
char		timebuf[40];

	printf("Session %p\n", session);
	printf("\tState:    	%s\n", session_state(session->state));
	printf("\tService:	%s (%p)\n", session->service->name, session->service);
	printf("\tClient DCB:	%p\n", session->client);
	printf("\tConnected:	%s",
		asctime_r(localtime_r(&session->stats.connect, &result), timebuf));
}

/**
 * Print all sessions
 *
 * Designed to be called within a debugger session in order
 * to display all active sessions within the gateway
 */
void
printAllSessions()
{
SESSION	*ptr;

	spinlock_acquire(&session_spin);
	ptr = allSessions;
	while (ptr)
	{
		printSession(ptr);
		ptr = ptr->next;
	}
	spinlock_release(&session_spin);
}


/**
 * Check sessions
 *
 * Designed to be called within a debugger session in order
 * to display information regarding "interesting" sessions
 */
void
CheckSessions()
{
SESSION	*ptr;
int	noclients = 0;
int	norouter = 0;

	spinlock_acquire(&session_spin);
	ptr = allSessions;
	while (ptr)
	{
		if (ptr->state != SESSION_STATE_LISTENER ||
				ptr->state != SESSION_STATE_LISTENER_STOPPED)
		{
			if (ptr->client == NULL && ptr->refcount)
			{
				if (noclients == 0)
				{
					printf("Sessions without a client DCB.\n");
					printf("==============================\n");
				}
				printSession(ptr);
				noclients++;
			}
		}
		ptr = ptr->next;
	}
	spinlock_release(&session_spin);
	if (noclients)
		printf("%d Sessions have no clients\n", noclients);
	spinlock_acquire(&session_spin);
	ptr = allSessions;
	while (ptr)
	{
		if (ptr->state != SESSION_STATE_LISTENER ||
				ptr->state != SESSION_STATE_LISTENER_STOPPED)
		{
			if (ptr->router_session == NULL && ptr->refcount)
			{
				if (norouter == 0)
				{
					printf("Sessions without a router session.\n");
					printf("==================================\n");
				}
				printSession(ptr);
				norouter++;
			}
		}
		ptr = ptr->next;
	}
	spinlock_release(&session_spin);
	if (norouter)
		printf("%d Sessions have no router session\n", norouter);
}

/**
 * Print all sessions to a DCB
 *
 * Designed to be called within a debugger session in order
 * to display all active sessions within the gateway
 *
 * @param dcb	The DCB to print to
 */
void
dprintAllSessions(DCB *dcb)
{
struct tm	result;
char		timebuf[40];
SESSION		*ptr;

	spinlock_acquire(&session_spin);
	ptr = allSessions;
	while (ptr)
	{
		dcb_printf(dcb, "Session %d (%p)\n",ptr->ses_id, ptr);
		dcb_printf(dcb, "\tState:    		%s\n", session_state(ptr->state));
		dcb_printf(dcb, "\tService:		%s (%p)\n", ptr->service->name, ptr->service);
		dcb_printf(dcb, "\tClient DCB:		%p\n", ptr->client);
		if (ptr->client && ptr->client->remote)
			dcb_printf(dcb, "\tClient Address:		%s\n", ptr->client->remote);
		dcb_printf(dcb, "\tConnected:		%s",
			asctime_r(localtime_r(&ptr->stats.connect, &result), timebuf));
		ptr = ptr->next;
	}
	spinlock_release(&session_spin);
}

/**
 * Print a particular session to a DCB
 *
 * Designed to be called within a debugger session in order
 * to display all active sessions within the gateway
 *
 * @param dcb	The DCB to print to
 * @param ptr	The session to print
 */
void
dprintSession(DCB *dcb, SESSION *ptr)
{
struct tm	result;
char		buf[30];
int		i;

	dcb_printf(dcb, "Session %d (%p)\n",ptr->ses_id, ptr);
	dcb_printf(dcb, "\tState:    		%s\n", session_state(ptr->state));
	dcb_printf(dcb, "\tService:		%s (%p)\n", ptr->service->name, ptr->service);
	dcb_printf(dcb, "\tClient DCB:		%p\n", ptr->client);
	if (ptr->client && ptr->client->remote)
		dcb_printf(dcb, "\tClient Address:		%s\n", ptr->client->remote);
	dcb_printf(dcb, "\tConnected:		%s",
			asctime_r(localtime_r(&ptr->stats.connect, &result), buf));
	if (ptr->n_filters)
	{
		for (i = 0; i < ptr->n_filters; i++)
		{
			dcb_printf(dcb, "\tFilter: %s\n",
					ptr->filters[i].filter->name);
			ptr->filters[i].filter->obj->diagnostics(
					ptr->filters[i].instance,
					ptr->filters[i].session,
					dcb);
		}
	}
}

/**
 * List all sessions in tabular form to a DCB
 *
 * Designed to be called within a debugger session in order
 * to display all active sessions within the gateway
 *
 * @param dcb	The DCB to print to
 */
void
dListSessions(DCB *dcb)
{
SESSION	*ptr;

	spinlock_acquire(&session_spin);
	ptr = allSessions;
	if (ptr)
	{
		dcb_printf(dcb, "Sessions.\n");
		dcb_printf(dcb, "-----------------+-----------------+----------------+--------------------------\n");
		dcb_printf(dcb, "Session          | Client          | Service        | State\n");
		dcb_printf(dcb, "-----------------+-----------------+----------------+--------------------------\n");
	}
	while (ptr)
	{
		dcb_printf(dcb, "%-16p | %-15s | %-14s | %s\n", ptr,
			((ptr->client && ptr->client->remote)
				? ptr->client->remote : ""),
			(ptr->service && ptr->service->name ? ptr->service->name
				: ""),
			session_state(ptr->state));
		ptr = ptr->next;
	}
	if (allSessions)
		dcb_printf(dcb, "-----------------+-----------------+----------------+--------------------------\n\n");
	spinlock_release(&session_spin);
}

/**
 * Convert a session state to a string representation
 *
 * @param state		The session state
 * @return A string representation of the session state
 */
char *
session_state(int state)
{
	switch (state)
	{
	case SESSION_STATE_ALLOC:
		return "Session Allocated";
	case SESSION_STATE_READY:
		return "Session Ready";
	case SESSION_STATE_ROUTER_READY:
		return "Session ready for routing";
	case SESSION_STATE_LISTENER:
		return "Listener Session";
	case SESSION_STATE_LISTENER_STOPPED:
		return "Stopped Listener Session";
	default:
		return "Invalid State";
	}
}

SESSION* get_session_by_router_ses(
        void* rses)
{
        SESSION* ses = allSessions;
        
        while (ses->router_session != rses && ses->next != NULL)
                ses = ses->next;
        
        if (ses->router_session != rses)
        {
                ses = NULL;
        }
        return ses;
}


/**
 * Create the filter chain for this session.
 *
 * Filters must be setup in reverse order, starting with the last
 * filter in the chain and working back towards the client connection
 * Each filter is passed the current session head of the filter chain
 * this head becomes the destination for the filter. The newly created
 * filter becomes the new head of the filter chain.
 *
 * @param	session		The session that requires the chain
 * @return	0 if filter creation fails
 */
static int
session_setup_filters(SESSION *session)
{
SERVICE		*service = session->service;
DOWNSTREAM 	*head;
UPSTREAM	*tail;
int		i;

	if ((session->filters = calloc(service->n_filters,
				sizeof(SESSION_FILTER))) == NULL)
	{
                LOGIF(LE, (skygw_log_write_flush(
			LOGFILE_ERROR,
			"Insufficient memory to allocate session filter "
			"tracking.\n")));
			return 0;
	}
	session->n_filters = service->n_filters;
	for (i = service->n_filters - 1; i >= 0; i--)
	{
		if (service->filters[i] == NULL)
		{
                	LOGIF(LE, (skygw_log_write_flush(
				LOGFILE_ERROR,
				"Service '%s' contians an unresolved filter.\n",
					service->name)));
			return 0;
		}
		if ((head = filterApply(service->filters[i], session,
						&session->head)) == NULL)
		{
                	LOGIF(LE, (skygw_log_write_flush(
				LOGFILE_ERROR,
				"Error : Failed to create filter '%s' for "
				"service '%s'.\n",
				service->filters[i]->name,
				service->name)));
			return 0;
		}
		session->filters[i].filter = service->filters[i];
		session->filters[i].session = head->session;
		session->filters[i].instance = head->instance;
		session->head = *head;
                free(head);
	}

	for (i = 0; i < service->n_filters; i++)
	{
		if ((tail = filterUpstream(service->filters[i],
				session->filters[i].session,
						&session->tail)) == NULL)
		{
                	LOGIF(LE, (skygw_log_write_flush(
				LOGFILE_ERROR,
				"Failed to create filter '%s' for service '%s'.\n",
					service->filters[i]->name,
					service->name)));
			return 0;
		}

		/*
		 * filterUpstream may simply return the 3 parameter if
		 * the filter has no upstream entry point. So no need
		 * to copy the contents or free tail in this case.
		 */
		if (tail != &session->tail)
		{
			session->tail = *tail;
			free(tail);
		}
	}

	return 1;
}

/**
 * Entry point for the final element int he upstream filter, i.e. the writing
 * of the data to the client.
 *
 * @param	instance	The "instance" data
 * @param	session		The session
 * @param	data		The buffer chain to write
 */
int
session_reply(void *instance, void *session, GWBUF *data)
{
SESSION		*the_session = (SESSION *)session;

	return the_session->client->func.write(the_session->client, data);
}

/**
 * Return the client connection address or name
 *
 * @param session	The session whose client address to return
 */
char *
session_get_remote(SESSION *session)
{
	if (session && session->client)
		return session->client->remote;
	return NULL;
}

bool session_route_query (
        SESSION* ses,
        GWBUF*   buf)
{
        bool succp;
        
        if (ses->head.routeQuery == NULL || 
                ses->head.instance == NULL || 
                ses->head.session == NULL)
        {
                succp = false;
                goto return_succp;
        }

        if (ses->head.routeQuery(ses->head.instance, ses->head.session, buf) == 1)
        {
                succp = true;
        }
        else
        {       
                succp = false;
        }
return_succp:
        return succp;
}


/**
 * Return the username of the user connected to the client side of the
 * session.
 *
 * @param session		The session pointer.
 * @return	The user name or NULL if it can not be determined.
 */
char *
session_getUser(SESSION *session)
{
	return (session && session->client) ? session->client->user : NULL;
}
/**
 * Return the pointer to the list of all sessions.
 * @return Pointer to the list of all sessions.
 */
SESSION *get_all_sessions()
{
	return allSessions;
}
