/*
 * Copyright (C) 2009, 2013 Henning Westerholt
 * Copyright (C) 2013 Charles Chance, sipcentric.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, see <http://www.gnu.org/licenses/>.
 */

/*!
 * \file
 * \brief memcached module
 */

#include "memcached.h"
#include "mcd_var.h"

#include "../../core/sr_module.h"
#include "../../core/mem/mem.h"
#include "../../core/dprint.h"
#include "../../core/ut.h"
#include "../../core/str.h"


MODULE_VERSION


/*! server string */
char *mcd_srv_str = "localhost:11211";
/*! cache (default) expire time in seconds */
unsigned int mcd_expire = 0;
/*! cache storage mode, set or add */
unsigned int mcd_mode = 0;
/*! server timeout in ms*/
unsigned int mcd_timeout = 5000;
/*! Internal or system memory manager, default is system */
unsigned int mcd_memory = 0;
/*! stringify all values retrieved from memcached, default false */
unsigned int mcd_stringify = 0;
/*! memcached handle */
struct memcached_st *memcached_h;
/*! memcached server list */
struct memcached_server_st *servers;


static int mod_init(void);

static void mod_destroy(void);


/*!
 * Exported pseudo-variables
 */
static pv_export_t mod_pvs[] = {
		{{"mct", sizeof("mct") - 1}, PVT_OTHER, pv_get_mcd_value,
				pv_set_mcd_value, pv_parse_mcd_name, 0, 0, 0},
		{{"mcinc", sizeof("mcinc") - 1}, PVT_OTHER, pv_get_mcd_value,
				pv_inc_mcd_value, pv_parse_mcd_name, 0, 0, 0},
		{{"mcdec", sizeof("mcdec") - 1}, PVT_OTHER, pv_get_mcd_value,
				pv_dec_mcd_value, pv_parse_mcd_name, 0, 0, 0},
		{{"mctex", sizeof("mctex") - 1}, PVT_OTHER, pv_get_null,
				pv_set_mcd_expire, pv_parse_mcd_name, 0, 0, 0},
		{{0, 0}, 0, 0, 0, 0, 0, 0, 0}};


/*!
 * Exported parameters
 */
static param_export_t params[] = {{"servers", PARAM_STRING, &mcd_srv_str},
		{"expire", INT_PARAM, &mcd_expire},
		{"timeout", INT_PARAM, &mcd_timeout}, {"mode", INT_PARAM, &mcd_mode},
		{"memory", INT_PARAM, &mcd_memory},
		{"stringify", INT_PARAM, &mcd_stringify}, {0, 0, 0}};


/*!
 * Module interface
 */
struct module_exports exports = {"memcached", DEFAULT_DLFLAGS, 0, params, 0,
		mod_pvs, 0, mod_init, 0, mod_destroy};


/*!
 * \brief Wrapper functions around our internal memory management for libmemcached (version >= 0.38) callback
 * \param ptr pointer to memcache handle
 * \param mem freed memory
 * \param context context for callbacks
 * \note pkg_free does not allow NULL pointer as standard free, therefore we check it here
 * \see pkg_free
 */
static inline void mcd_free(memcached_st *ptr, void *mem, void *context)
{
	if(mem)
		pkg_free(mem);
}

/*!
 * \brief Wrapper functions around our internal memory management for libmemcached (version < 0.38) callback
 * \param ptr pointer to memcache handle
 * \param mem freed memory
 * \note pkg_free does not allow NULL pointer as standard free, therefore we check it here
 * \see pkg_free
 */
static inline void mcd_free_compat(memcached_st *ptr, void *mem)
{
	if(mem)
		pkg_free(mem);
}


/*!
 * \brief Wrapper functions around our internal memory management for libmemcached (version >= 0.38) callback
 * \param ptr pointer to memcache handle
 * \param size allocated size
 * \param context context for callbacks
 * \return allocated memory, or NULL on failure
 * \see pkg_malloc
 */
static inline void *mcd_malloc(
		memcached_st *ptr, const size_t size, void *context)
{
	return pkg_malloc(size);
}

/*!
 * \brief Wrapper functions around our internal memory management for libmemcached (version < 0.38) callback
 * \param ptr pointer to memcache handle
 * \param size allocated size
 * \return allocated memory, or NULL on failure
 * \see pkg_malloc
 */
static inline void *mcd_malloc_compat(memcached_st *ptr, const size_t size)
{
	return pkg_malloc(size);
}


/*!
 * \brief Wrapper functions around our internal memory management for libmemcached (version >= 0.38) callback
 * \param ptr pointer to memcache handle
 * \param mem pointer to allocated memory
 * \param size new size of memory area
 * \param context context for callbacks
 * \return allocated memory, or NULL on failure
 * \see pkg_realloc
 */
static inline void *mcd_realloc(
		memcached_st *ptr, void *mem, const size_t size, void *context)
{
	return pkg_realloc(mem, size);
}

/*!
 * \brief Wrapper functions around our internal memory management for libmemcached (version < 0.38) callback
 * \param ptr pointer to memcache handle
 * \param mem pointer to allocated memory
 * \param size new size of memory area
 * \return allocated memory, or NULL on failure
 * \see pkg_realloc
 */
static inline void *mcd_realloc_compat(
		memcached_st *ptr, void *mem, const size_t size)
{
	return pkg_realloc(mem, size);
}


/*!
 * \brief Wrapper functions around our internal memory management for libmemcached (version >= 0.38) callback
 * \param ptr pointer to memcache handle
 * \param nelem number of elements
 * \param elsize size of elements
 * \param context context for callbacks
 * \return pointer to allocated memory, or NULL on failure
 * \see pkg_malloc
 * \todo this is not optimal, use internal calloc implemention which is not exported yet
 */
static inline void *mcd_calloc(
		memcached_st *ptr, size_t nelem, const size_t elsize, void *context)
{
	void *tmp = NULL;
	tmp = pkg_malloc(nelem * elsize);
	if(tmp != NULL) {
		memset(tmp, 0, nelem * elsize);
	}
	return tmp;
}

/*!
 * \brief Wrapper functions around our internal memory management for libmemcached (version < 0.38) callback
 * \param ptr pointer to memcache handle
 * \param nelem number of elements
 * \param elsize size of elements
 * \return pointer to allocated memory, or NULL on failure
 * \see pkg_malloc
 * \todo this is not optimal, use internal calloc implemention which is not exported yet
 */
static inline void *mcd_calloc_compat(
		memcached_st *ptr, size_t nelem, const size_t elsize)
{
	void *tmp = NULL;
	tmp = pkg_malloc(nelem * elsize);
	if(tmp != NULL) {
		memset(tmp, 0, nelem * elsize);
	}
	return tmp;
}


#if 0
/**
 * \brief Callback to check if we could connect successfully to a server
 * \param ptr memcached handler
 * \param my_server server instance
 * \param context context for callback
 * \return MEMCACHED_SUCCESS on success, MEMCACHED_CONNECTION_FAILURE on failure
 * \todo FIXME
 */
static inline memcached_server_fn mcd_check_connection(const memcached_st *ptr, memcached_server_instance_st my_server, void *context) {
	if (my_server->fd < 0) {
		return MEMCACHED_CONNECTION_FAILURE;
	}
	return MEMCACHED_SUCCESS;
}
#endif

/*!
 * \brief Module initialization function
 * \return 0 on success, -1 on failure
 */
static int mod_init(void)
{
	char *server, *port;
	unsigned int len = 0;
	memcached_return rc;
	struct memcached_server_st *svt;

	if((port = strchr(mcd_srv_str, ':')) != NULL) {
		port = port + 1;
		len = strlen(mcd_srv_str) - strlen(port) - 1;
	} else {
		LM_DBG("no port definition, using default port\n");
		port = "11211";
		len = strlen(mcd_srv_str);
	}

	server = pkg_malloc(len + 1);
	if(server == NULL) {
		PKG_MEM_ERROR;
		return -1;
	}

	strncpy(server, mcd_srv_str, len);
	server[len] = '\0';

	memcached_h = memcached_create(NULL);
	if(memcached_h == NULL) {
		LM_ERR("could not create memcached structure\n");
		pkg_free(server);
		return -1;
	}
	LM_DBG("allocated new server handle at %p", memcached_h);

	if(mcd_memory == 1) {
		LM_INFO("Use internal kamailio memory manager for memcached client "
				"library\n");

#if LIBMEMCACHED_VERSION_HEX >= 0x00038000
		rc = memcached_set_memory_allocators(memcached_h,
				(memcached_malloc_fn)mcd_malloc, (memcached_free_fn)mcd_free,
				(memcached_realloc_fn)mcd_realloc,
				(memcached_calloc_fn)mcd_calloc, NULL);
#else
		rc = memcached_set_memory_allocators(memcached_h,
				(memcached_malloc_function)mcd_malloc_compat,
				(memcached_free_function)mcd_free_compat,
				(memcached_realloc_function)mcd_realloc_compat,
				(memcached_calloc_function)mcd_calloc_compat);
#endif

		if(rc == MEMCACHED_SUCCESS) {
			LM_DBG("memory manager callbacks set\n");
		} else {
			LM_ERR("memory manager callbacks not set, returned %s.\n",
					memcached_strerror(memcached_h, rc));
			pkg_free(server);
			return -1;
		}
	} else {
		LM_INFO("Use system memory manager for memcached client library\n");
	}

	svt = memcached_server_list_append(servers, server, atoi(port), &rc);
	if(svt == NULL) {
		LM_ERR("failed to append server\n");
		if(servers) {
			memcached_server_list_free(servers);
			servers = NULL;
		}
		pkg_free(server);
		return -1;
	}

	servers = svt;
	if(memcached_behavior_set(
			   memcached_h, MEMCACHED_BEHAVIOR_CONNECT_TIMEOUT, mcd_timeout)
			!= MEMCACHED_SUCCESS) {
		LM_ERR("could not set server connection timeout\n");
		pkg_free(server);
		return -1;
	}
	rc = memcached_server_push(memcached_h, servers);
	if(rc == MEMCACHED_SUCCESS) {
		LM_DBG("added server list to structure\n");
	} else {
		LM_ERR("attempt to add server list to structure returned %s.\n",
				memcached_strerror(memcached_h, rc));
		pkg_free(server);
		return -1;
	}

	pkg_free(server);

	/** \todo FIXME logic to handle connection errors on startup
	memcached_server_cursor(memcached_h, (const memcached_server_fn*) &mcd_check_connection, NULL, 1);
	*/

	LM_INFO("libmemcached version is %s\n", memcached_lib_version());
	return 0;
}


/*!
 * \brief Module shutdown function
 */
static void mod_destroy(void)
{
	if(servers != NULL)
		memcached_server_list_free(servers);

	/* Crash on shutdown with internal memory manager, even if we disable the mm callbacks */
	if(mcd_memory != 1 && memcached_h != NULL)
		memcached_free(memcached_h);
}
