/*
 *  Copyright (C) 2006-2009, Simon Hyde
 *  http://www.mvpmc.org/
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2.1 of the License, or (at your option) any later version.
 *
 *  This library 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
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#include <sys/types.h>
#include <string.h>
#include <inttypes.h>
#include <stdio.h>
#include <cmyth_local.h>

#define CMYTH_UINT32_STRLEN ((sizeof(uint32_t)*3)+1)
#define CMYTH_INT32_STRLEN (CMYTH_UINT32_STRLEN+1)
#define CMYTH_INT64_STRLEN 24

/**
 * Hold in-progress query
 */
struct cmyth_mysql_query_s
{
    char * buf;
    const char * source;
    const char * source_pos;
    int buf_size, buf_used, source_len;
    cmyth_database_t db;
};


/**
 * Internal only! Registered as callback to de-allocate actual buffer if
 * the query is de-allocated
 * \param p pointer to the query data structure
 */
static void
query_destroy(void *p)
{
    cmyth_mysql_query_t * query = (cmyth_mysql_query_t *)p;
    if(query->buf != NULL)
    {
	ref_release(query->buf);
	query->buf = NULL;
	query->buf_size = 0;
    }
    if(query->db != NULL)
    {
	ref_release(query->db);
	query->db = NULL;
    }
}

/**
 * Allocate a dynamic query string to have parameters added to it
 * \param db database connection object
 * \param query_string Query string with ? placemarks for all dynamic
 * 			parameters, this is NOT copied and must therefore
 * 			remain valid for the life of the query.
 */
cmyth_mysql_query_t *
cmyth_mysql_query_create(cmyth_database_t db, const char * query_string)
{
    cmyth_mysql_query_t * out;
    out = ref_alloc(sizeof(*out));
    if(out != NULL)
    {
	ref_set_destroy(out,query_destroy);
	out->source = out->source_pos = query_string;
	out->source_len = strlen(out->source);
	out->buf_size = out->source_len *2;
	out->buf_used = 0;
	out->db = ref_hold(db);
	out->buf = ref_alloc(out->buf_size);
	if(out->buf == NULL)
	{
	    ref_release(out);
	    out = NULL;
	}
	else
	{
	    out->buf[0] = '\0';
	}

    }
    return out;
}

void
cmyth_mysql_query_reset(cmyth_mysql_query_t *query)
{
    query->buf_used = 0;
    query->source_pos = query->source;
}

static int
query_buffer_check_len(cmyth_mysql_query_t *query, int len)
{
    if(len + query->buf_used >= query->buf_size)
    {
	/* Increase buffer size by len or out->source_len, whichever
	 * is bigger
	 */
	if(query->source_len > len)
	    query->buf_size += query->source_len;
	else
	    query->buf_size += len;
	query->buf = ref_realloc(query->buf,query->buf_size);
	if(query->buf == NULL)
	{
	    cmyth_mysql_query_reset(query);
	    return -1;
	}
    }
    return 0;
}

static int
query_buffer_add(cmyth_mysql_query_t *query, const char *buf,int len)
{
    int ret = query_buffer_check_len(query,len);
    if(ret < 0)
	return ret;
    memcpy(query->buf + query->buf_used,buf,len);
    query->buf_used +=len;
    query->buf[query->buf_used] = '\0';
    return len;
}

static inline int
query_buffer_add_str(cmyth_mysql_query_t *query, const char *str)
{
    return query_buffer_add(query,str,strlen(str));
}

static int
query_buffer_add_escape_str(cmyth_mysql_query_t *query, const char *str)
{
    int ret;
    int srclen = strlen(str);
    MYSQL * mysql;
    unsigned long destlen;
    /*According to the mysql C API refrence, there must be sourcelen*2 +1
     * characters of space in the destination buffer
     */
    ret = query_buffer_check_len(query,srclen*2 +1);
    if(ret < 0)
	return ret;
    mysql = cmyth_db_get_connection(query->db);
    if(mysql == NULL)
	return -1;
    destlen = mysql_real_escape_string(mysql, query->buf + query->buf_used,
					str, srclen);
    query->buf_used += destlen;
    /* MySQL claims it null terminates, but do so anyway just in case we've
     * done something stupid
     */
    query->buf[query->buf_used] = '\0';
    return destlen;
}

static int
query_begin_next_param(cmyth_mysql_query_t *query)
{
    int len,ret;
    const char * endpos = strchr(query->source_pos,(int)'?');
    /*No more parameter insertion points left!*/
    if(endpos == NULL)
	return -1;
    len = endpos - query->source_pos;
    ret = query_buffer_add(query,query->source_pos,len);
    query->source_pos = endpos + 1;
    return ret;
}

static inline int
query_buffer_add_int32(cmyth_mysql_query_t * query, int32_t param)
{
    char buf[CMYTH_INT32_STRLEN];
    sprintf(buf,"%"PRId32,param);
    return query_buffer_add_str(query,buf);
}

static inline int
query_buffer_add_uint32(cmyth_mysql_query_t * query, uint32_t param)
{
    char buf[CMYTH_UINT32_STRLEN];
    sprintf(buf,"%"PRIu32,param);
    return query_buffer_add_str(query,buf);
}

static inline int
  query_buffer_add_int64(cmyth_mysql_query_t * query, int64_t param)
{
  char buf[CMYTH_INT64_STRLEN];
  sprintf(buf,"%"PRId64,param);
  return query_buffer_add_str(query,buf);
}

/**
 * Add a int32_t integer parameter
 * \param query the query object
 * \param param the integer to add
 */
int
cmyth_mysql_query_param_int32(cmyth_mysql_query_t * query, int32_t param)
{
    int ret;
    ret = query_begin_next_param(query);
    if(ret < 0)
	return ret;
    return query_buffer_add_int32(query,param);
}

/**
 * Add an uint32_t integer parameter
 * \param query the query object
 * \param param the integer to add
 */
int
cmyth_mysql_query_param_uint32(cmyth_mysql_query_t * query, uint32_t param)
{
    int ret;
    ret = query_begin_next_param(query);
    if(ret < 0)
	return ret;
    return query_buffer_add_uint32(query,param);
}

/**
 * Add a int64_t parameter
 * \param query the query object
 * \param param the integer to add
 */
int
cmyth_mysql_query_param_int64(cmyth_mysql_query_t * query, int64_t param)
{
    int ret;
    ret = query_begin_next_param(query);
    if(ret < 0)
	return ret;
    return query_buffer_add_int64(query,param);
}

/**
 * Add an integer parameter
 * \param query the query object
 * \param param the integer to add
 */
int
cmyth_mysql_query_param_int(cmyth_mysql_query_t * query, int param)
{
    return cmyth_mysql_query_param_int32(query,(int32_t)param);
}

/**
 * Add an unsigned integer parameter
 * \param query the query object
 * \param param the integer to add
 */
int
cmyth_mysql_query_param_uint(cmyth_mysql_query_t * query, unsigned int param)
{
    return cmyth_mysql_query_param_uint32(query,(uint32_t)param);
}

/**
 * Add, and convert a unixtime to mysql date/timestamp
 * \param query the query object
 * \param param the time to add
 * \param tz_utc flag to convert local time to UTC time ( 1 = true | 0 = false )
 */
int
cmyth_mysql_query_param_unixtime(cmyth_mysql_query_t * query, time_t param, int tz_utc)
{
    int ret;
    ret = query_begin_next_param(query);
    if(ret < 0)
	return ret;
    if (tz_utc == 1)
        ret = query_buffer_add_str(query, "CONVERT_TZ(FROM_UNIXTIME(");
    else
        ret = query_buffer_add_str(query, "FROM_UNIXTIME(");
    if(ret < 0)
	return ret;
    ret = query_buffer_add_int32(query,(int32_t)param);
    if(ret < 0)
	return ret;
    if (tz_utc == 1)

        return query_buffer_add_str(query, "),'SYSTEM','UTC')");
    else
        return query_buffer_add_str(query, ")");
}


/**
 * Add (including adding quotes), and escape a string parameter.
 * \param query the query object
 * \param param the string to add
 */
int
cmyth_mysql_query_param_str(cmyth_mysql_query_t * query, const char *param)
{
    int ret;
    ret = query_begin_next_param(query);
    if(ret < 0)
	return ret;
    if(param == NULL)
	return query_buffer_add_str(query,"NULL");
    ret = query_buffer_add_str(query,"'");
    if(ret < 0)
	return ret;
    ret = query_buffer_add_escape_str(query,param);
    if(ret < 0)
	return ret;
    return query_buffer_add_str(query,"'");
}

/**
 * Get the completed query string
 * \return If all fields haven't been filled, or there is some other failure
 * 	this will return NULL, otherwise a string is returned. The returned
 * 	string must be released by the caller using ref_release().
 */
char *
cmyth_mysql_query_string(cmyth_mysql_query_t * query)
{
    if(strchr(query->source_pos, (int)'?') != NULL)
    {
	return NULL;/*Still more parameters to be added*/
    }
    if(query_buffer_add_str(query,query->source_pos) < 0)
	return NULL;
    /*Point source_pos to the '\0' at the end of the string so this can
     * be called multiple times
     */
    query->source_pos = query->source + query->source_len;
    return ref_hold(query->buf);
}


MYSQL_RES *
cmyth_mysql_query_result(cmyth_mysql_query_t * query)
{
    MYSQL_RES * retval = NULL;
    int ret;
    char * query_str;
    MYSQL *mysql = cmyth_db_get_connection(query->db);
    if(mysql == NULL)
	return NULL;
    query_str = cmyth_mysql_query_string(query);
    if(query_str == NULL)
	return NULL;
    ret = mysql_query(mysql,query_str);
    ref_release(query_str);
    if(ret != 0)
    {
	 cmyth_dbg(CMYTH_DBG_ERROR, "%s: mysql_query(%s) Failed: %s\n",
				__FUNCTION__, query_str, mysql_error(mysql));
	 return NULL;
    }
    retval = mysql_store_result(mysql);
    if(retval == NULL)
    {
	 cmyth_dbg(CMYTH_DBG_ERROR, "%s: mysql_use_result Failed: %s\n",
				__FUNCTION__, query_str, mysql_error(mysql));
    }
    return retval;
}

int
cmyth_mysql_query(cmyth_mysql_query_t * query)
{
	int ret;
	char * query_str;
	MYSQL *mysql = cmyth_db_get_connection(query->db);
	if(mysql == NULL)
		return -1;
	query_str = cmyth_mysql_query_string(query);
	if(query_str == NULL)
		return -1;
	ret = mysql_query(mysql,query_str);
	ref_release(query_str);
	if(ret != 0)
	{
		cmyth_dbg(CMYTH_DBG_ERROR, "%s: mysql_query(%s) Failed: %s\n",
				__FUNCTION__, query_str, mysql_error(mysql));
		return -1;
	}
	return 0;
}
