/*
 * Copyright (c) 2004-2009, Luiz Otavio O Souza <loos.br@gmail.com>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

static const char rcsid[] = "$Id: mysql.c 112 2009-03-15 17:30:28Z loos-br $";

#include <sys/types.h>
#include <sys/stat.h>

#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <dirent.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "io.h"
#include "fmt.h"
#include "mysql.h"
#include "return.h"

#define print_bar(a)    (a[strlen(a) - 1] == '/') ? "" : "/"

int
tablecmp(struct table__ *t1, struct table__ *t2) {
    if (t1 == NULL ||
	t2 == NULL ||
	t1->name.len == 0 ||
	t2->name.len == 0) {
	return 1;
    }
    return (strcmp((char *)t1->name.s, (char *)t2->name.s));
}

RB_GENERATE(tables, table__, table_, tablecmp)

MYSQL_ROW
db_fetch_row(MYSQL_RES *res) {
    return(mysql_fetch_row(res));
}

u_int64_t
db_count(MYSQL_RES *res) {
    if(res == NULL)
	return 0;
    return(mysql_num_rows(res));
}

u_int64_t
db_affected_rows(MYSQL *mysql) {
    if(mysql == NULL)
	return 0;
    return(mysql_affected_rows(mysql));
}

u_int64_t
db_last_id(MYSQL *mysql) {
    if(mysql == NULL)
	return 0;
    return(mysql_insert_id(mysql));
}

MYSQL_RES *
db_free(MYSQL_RES *res) {
    if(res == NULL)
	return((MYSQL_RES *)NULL);

    mysql_free_result(res);

    return((MYSQL_RES *)NULL);
}

char *
db_escape(mysql_ *mysql, char *from) {
 char	*rtrn;
 size_t	len;

    if(from == NULL)
	return(NULL);

    len = strlen(from);

    rtrn = (char *)malloc(sizeof(char) * len * 2 + 1);
    if(rtrn == NULL)
	return(NULL);

    mysql_real_escape_string(&mysql->mysql, rtrn, from, len);

    return(rtrn);
}

MYSQL_RES *
db_query(mysql_ *mysql, string *query) {
 MYSQL_RES *res;

    if(query == NULL || query->s == NULL || *query->s == 0)
	return((MYSQL_RES *)NULL);

    mysql->info("SQL: (%s)\n", query);
    if (mysql_real_query(&mysql->mysql, (char *)query->s, query->len) != 0) {
	mysql->debug("SQL ERROR: (%s) MySQL: (%S)\n", query, db_error(mysql));
	return((MYSQL_RES *)NULL);
    }
    res = mysql_store_result(&mysql->mysql);

    return(res);
}

int
db_query2(mysql_ *mysql, string *query) {
    if (query == NULL || query->s == NULL || *query->s == 0)
	return(RFAIL);

    mysql->info("SQL: (%s)\n", query);
    if (mysql_real_query(&mysql->mysql, (char *)query->s, query->len) != 0) {
	mysql->debug("SQL ERROR: (%s) MySQL: (%S)\n", query, db_error(mysql));
	return(RFAIL);
    }

    return(ROK);
}

int
db_create_all(mysql_ *mysql) {
 struct table__	*table;
 string		sql;

    str_zero(&sql);
    RB_FOREACH(table, tables, &mysql->table_head) {

	if (table->exist == 1) continue;
	if (table->sql.len == 0 || table->name.len == 0) continue;

	sql.len = fmt_printf(NULL, (char *)table->sql.s, table->name.s);
	if (str_ready(&sql, sql.len + 1) == 0) exit(51);
	sql.len = fmt_printf(sql.s, (char *)table->sql.s, table->name.s);
	if (db_query2(mysql, &sql) != ROK &&
		mysql_errno(&mysql->mysql) != 0) {
		str_free(&sql);
		return(RFAIL);
	}
	str_free(&sql);
    }

    return(ROK);
}

int
db_check(mysql_ *mysql) {
 struct table__	find;
 struct table__	*table;
 MYSQL_RES	*res;
 MYSQL_ROW	row;
 string		sql;

    /* check tables */
    str_zero(&sql);
    sql.s = (unsigned char *)"SHOW TABLES";
    sql.len = strlen((char *)sql.s);
    if ((res = db_query(mysql, &sql)) == NULL)
	return(RFAIL);

    while ((row = db_fetch_row(res))) {
	if (row[0] == NULL) continue;
	str_zero(&find.name);
	find.name.len = strlen(row[0]);
	find.name.s   = (unsigned char *)row[0];
	table = RB_FIND(tables, &mysql->table_head, &find);
	if (table != NULL)
	    table->exist = 1;
    }
    res = db_free(res);

    /* create missing tables */
    return(db_create_all(mysql));
}

char *
db_error(mysql_ *mysql) {
 char *p;

    p = (char *)mysql_error(&mysql->mysql);
    if (p && *p)
	return(p);
    else
	return("");
}

int
db_init(mysql_ *mysql, void (*debug)(const char *, ...),
		       void (*info)(const char *, ...)) {
    my_bool	reconnect = 1;

    mysql->debug = debug;
    mysql->info  = info;

    if (!mysql->host.s || !mysql->user.s || !mysql->pass.s || !mysql->db.s) {

	if (!mysql->host.s) {
	    if (str_copys(&mysql->host, (unsigned char *)DEFAULT_MYSQL_HOST) == 0)
		exit(51);
	}

	if (!mysql->user.s) {
	    if (str_copys(&mysql->user, (unsigned char *)DEFAULT_MYSQL_USER) == 0)
		exit(51);
	}

	if (!mysql->pass.s) {
	    if (str_copys(&mysql->pass, (unsigned char *)DEFAULT_MYSQL_PASS) == 0)
		exit(51);
	}

	if (!mysql->db.s) {
	    if (str_copys(&mysql->db, (unsigned char *)DEFAULT_MYSQL_DB) == 0)
		exit(51);
	}

	if (mysql->port == 0)
	    mysql->port = DEFAULT_MYSQL_PORT;

	mysql->debug("check your mysql config !!! using defaults !!!\n"
		"    host: (%s)\n"
		"    port: (%d)\n"
		"    user: (%s)\n"
		"    pass: (%S)\n"
		"    db  : (%s)\n",
		&mysql->host, mysql->port, &mysql->user,
		mysql->pass.s ? "*********" : (char *)mysql->pass.s,
		&mysql->db);
    }

    /* init && connect */
    if (mysql_init(&mysql->mysql) == NULL) {
	mysql->debug("SQL INIT: (%S)\n", db_error(mysql));
	return(RFAIL);
    }

    if (!mysql_real_connect(&mysql->mysql,
		(*mysql->host.s == '/') ? NULL : (char *)mysql->host.s,
		(char *)mysql->user.s, (char *)mysql->pass.s,
		(char *)mysql->db.s, mysql->port,
		(*mysql->host.s == '/') ? (char *)mysql->host.s : NULL, 0)) {
	mysql->debug("SQL INIT: (%S)\n", db_error(mysql));
	return(RFAIL);
    }

    /* erase the database password from memory ?!?? Safety ? */
    memset(&mysql->pass.s, 0, mysql->pass.len);
    mysql->connected = 1;

    /* set auto-reconnect */
    mysql_options(&mysql->mysql, MYSQL_OPT_RECONNECT, &reconnect);

    /* check if db is ok */
    return(db_check(mysql));
}

void
db_close(mysql_ *mysql) {
    if (mysql->connected)
	mysql_close(&mysql->mysql);
    /* Free memory */
    mysql_server_end();
    mysql->connected = 0;
}

int
db_get_conf_key(char **key, char **buf) {
 register char		*p;

    p    = *buf;
    *key = *buf;
    while (*p && *p != '|' && *p != '\n' && *p != '\r') p++;
    if (*p == 0)
	return(ROK);

    *p++ = 0;
    *buf = p;

    return(ROK);
}

void
db_table_free(struct table__ *table) {
    if (table == NULL) return;
    str_free(&table->name);
    str_free(&table->sql);
    table->exist = 0;
    table->size  = 0;
}

int
db_read_table_sql(const char *file, long filelen, size_t size,
		  struct table__ **table_) {
 struct table__	*table;
 size_t		len;
 int		fd;

    if (file == NULL || filelen == 0)
	return(RFAIL);

    *table_ = (struct table__ *)malloc(sizeof(struct table__));
    if (*table_ == NULL) exit(51);
    table = *table_;
    str_zero(&table->name);
    str_zero(&table->sql);
    table->size  = size;
    table->exist = 0;

    if (str_ready(&table->sql, table->size + 1) == 0) exit(51);
    if (str_copy(&table->name, (unsigned char *)file, filelen) == 0) exit(51);

    /* open file and read read */
    fd = open((char *)table->name.s, O_RDONLY);
    if (fd == -1) {
	io_printf(1, "cannot open table file [%S] [%S]\n", table->name.s,
		  strerror(errno));
	db_table_free(table);
	free(*table_);
	return(RFAIL);
    }
    while (table->sql.len < size) {
	len = io_read(fd, (char *)table->sql.s + table->sql.len,
				  table->sql.a - table->sql.len);
	if (len == -1) {
	    io_printf(1, "cannot read table file [%S] [%S]\n", table->name.s,
		      strerror(errno));
	    db_table_free(table);
	    free(*table_);
	    return(RFAIL);
	}
	if (len == 0) break;
	table->sql.len += len;
    }
    close(fd);
    
    return(ROK);
}

int
db_read_tables(mysql_ *mysql, const char *configdir) {
 struct table__ *table = NULL;
 struct table__ *tmp = NULL;
 struct dirent	*de;
 struct stat	st;
 DIR		*dir;

    /* RB_INIT */
    RB_INIT(&mysql->table_head);

    if (chdir(MYSQLTABLE) == -1) {
        io_printf(1, "cannot chdir to mysql tables directory: [%S%Stables] [%S]\n",
		  configdir, print_bar(configdir), strerror(errno));
        return(RFAIL);
    }
    dir = opendir(".");
    if (dir == NULL)
	return(RFAIL);
    while ((de = readdir(dir)) != NULL) {
	if (de->d_name == NULL ||
	    strlen(de->d_name) == 0 ||
	    strcmp(de->d_name, ".") == 0 ||
	    strcmp(de->d_name, "..") == 0) {
	    continue;
	}

	memset(&st, 0, sizeof(st));
	if (stat(de->d_name, &st) == -1) {
	    io_printf(1, "cannot stat table file: [%S]: [%S]\n", de->d_name,
		      strerror(errno));
	    continue;
	}

	if (st.st_size == 0) {
	    io_printf(1, "table file with zero size: [%S]. ignored.\n", de->d_name);
	    continue;
	}

	if (db_read_table_sql(de->d_name, strlen(de->d_name), st.st_size, &table) == ROK) {
	    tmp = RB_INSERT(tables, &mysql->table_head, table);
	    if (tmp) {
		db_table_free(table);
		io_printf(1, "duplicated file: [%S]. ignored.\n", de->d_name);
	    }
	}
    }
    closedir(dir);
    return(ROK);
}

int
db_read_conf(mysql_ *mysql, const char *configdir) {
 struct stat		sb;
 FILE			*fp;
 char			buf[1024];
 char			*label = "";
 char			*key;
 char			*p;
 int			curdir = -1;

    memset(mysql, 0, sizeof(mysql_));

    /* save the current dir */
    curdir = open(".", O_RDONLY);
    if (curdir == -1) {
	io_printf(1, "cannot open curdir: [%S]\n", strerror(errno));
	goto fail;
    }

    /* chdir to config dir */
    if (chdir(configdir) == -1) {
	io_printf(1, "cannot chdir to mysql configuration directory: [%S]: [%S]\n",
		  configdir, strerror(errno));
	goto fail;
    }

    /* check access to config */
    memset(&sb, 0, sizeof(sb));
    if (stat(MYSQLCONF, &sb) == -1) {
	io_printf(1, "cannot stat configuration file: [%S%Sconf]: [%S]\n",
		  configdir, print_bar(configdir), strerror(errno));
	goto fail;
    }

    if (sb.st_mode & S_IRWXO)
	io_printf(STDERR, "PUBLIC ACCESS on %S%Sconf should be removed !!!\n",
		  "ie: chmod 400 %S%Sconf\n", configdir, print_bar(configdir),
		  configdir, print_bar(configdir));

    fp = fopen(MYSQLCONF, "r");
    if (fp == (FILE *)0) {
	io_printf(1, "cannot open configuration file: [%S%Sconf]: [%S]\n",
		   configdir, print_bar(configdir), strerror(errno));
	goto fail;
    }

    p = (char *)0;
    while (fgets(buf, sizeof(buf), fp)) {

	p = buf;

	/* skip spaces and tabs */
	while (p && *p && (*p == ' ' || *p == '\t')) ++p;
	if (!*p)
	    continue;

	if (*p == '\n' || *p == '\r' || *p == '#') {
	    p = (char *)0;
	    continue;
	}

	/* break on first valid line */
	break;
    }
    fclose(fp);

    /* check for buffer */
    if (!p || !*p) {
	io_printf(1, "zero configuration data (no content) on file: [%S%Sconf]\n",
		     configdir, print_bar(configdir));
	goto fail;
    }

    /* host */
    if (db_get_conf_key(&key, &p) == RFAIL) {
	label = "o host";
	goto field_fail;
    }
    if (str_copys(&mysql->host, (unsigned char *)key) == 0) exit(51);

    /* port */
    if (db_get_conf_key(&key, &p) == RFAIL) {
	label = "a porta";
	goto field_fail;
    }
    mysql->port = atoi(key);

    /* user */
    if (db_get_conf_key(&key, &p) == RFAIL) {
	label = "the user";
	goto field_fail;
    }
    if (str_copys(&mysql->user, (unsigned char *)key) == 0) exit(51);

    /* pass */
    if (db_get_conf_key(&key, &p) == RFAIL) {
	label = "the password";
	goto field_fail;
    }
    if (str_copys(&mysql->pass, (unsigned char *)key) == 0) exit(51);

    /* db */
    if (db_get_conf_key(&key, &p) == RFAIL) {
	label = "the database";
	goto field_fail;
    }
    if (str_copys(&mysql->db, (unsigned char *)key) == 0) exit(51);

    if (db_read_tables(mysql, configdir) == RFAIL)
	goto fail;

    /* check for tables */
    /* if (RB_EMPTY(&mysql->table_head))
     *	io_printf(1, "nenhuma tabela foi carregada !\n");
     */

    fchdir(curdir);
    close(curdir);
    return(ROK);

field_fail:
    io_printf(1, "cannot read %S: [%S%Sconf]\n"
		 "should be: host|port|user|pass|db\n",
		 label, configdir, print_bar(configdir));
fail:
    fchdir(curdir);
    close(curdir);
    return(RFAIL);
}

/* remove tables from tree and memory */
void free_sqltable_tree(mysql_ *mysql) {

	struct table__ *table;
	struct table__ *next;

	 for (table = RB_MIN(tables, &mysql->table_head); table != NULL; table = next) {
		next = RB_NEXT(tables, &mysql->table_head, table);
		RB_REMOVE(tables, &mysql->table_head, table);
		db_table_free(table);
		free(table);
    }

}
