/* Copyright 2013 Akira Ohta (akohta001@gmail.com)
    This file is part of ntch.

    The ntch 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 3 of the License, or
    (at your option) any later version.

    The ntch 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 ntch.  If not, see <http://www.gnu.org/licenses/>.
    
*/
#include <stdio.h> 
#include <stdlib.h> 
#include <string.h>
#include <assert.h>
#include <limits.h>
#include <sqlite3.h>
#include <openssl/sha.h>
#include <wchar.h>
#include <iconv.h>

#include "env.h"
#include "utils/nt_std_t.h" 
#include "utils/db.h" 
#include "utils/text.h" 
#include "utils/nt_conv_char.h" 
#include "usr/usr_db_t.h" 

#define NT_USR_DB_CHK_SUM (978428)

typedef struct tag_ctx_usr_db_t *ctx_usr_db_tp;
typedef struct tag_ctx_usr_db_t{
	nt_usr_db_handle_t handle;
	char db_path[PATH_MAX+1];
	sqlite3 *dbp;
}ctx_usr_db_t;

static const char *NT_SQL_VALIDATE_USR_TBL =
	"SELECT name FROM sqlite_master WHERE type=\'table\'"
	"AND name=\'usr_log\'";

static const char *NT_SQL_CREATE_USR_TBL =
	"CREATE TABLE usr_log (id INTEGER PRIMARY KEY AUTOINCREMENT, "
	"rec_type INTEGER NOT NULL, res_no INTEGER NOT NULL, "
	"board_name TEXT NOT NULL, dat_name TEXT NOT NULL, "
	"date_time REAL)";

static const char *NT_SQL_QUERY_READ_CNT_LIST = 
	"SELECT dat_name, res_no FROM usr_log WHERE "
	"board_name = :board_name AND rec_type = 1 ";
	
static const char *NT_SQL_QUERY_READ_CNT = 
	"SELECT res_no FROM usr_log WHERE board_name = :board_name "
	"AND dat_name = :dat_name AND rec_type = 1 ";
	
static const char *NT_SQL_UPDATE_READ_CNT = 
	"UPDATE usr_log SET res_no = :res_no, "
	"date_time = julianday(\'now\') WHERE  "
	"rec_type = 1 AND board_name = :board_name AND "
	"dat_name = :dat_name ";

static const char *NT_SQL_INSERT_READ_CNT = 
	"INSERT INTO usr_log (rec_type, res_no, board_name, "
	"dat_name, date_time) "
	"VALUES (1, :res_no, :board_name, :dat_name, "
	"julianday(\'now\') )"; 

static const char *NT_SQL_DELETE_BOARD_LOG =
	"DELETE FROM usr_log WHERE "
	"board_name = :board_name ";

static const char *NT_SQL_DELETE_THREAD_LOG =
	"DELETE FROM usr_log WHERE "
	"board_name = :board_name AND dat_name = :dat_name ";

static BOOL usr_tbl_create(sqlite3 *dbp);
static int usr_tbl_validate(sqlite3 *dbp);

static nt_usr_db_thread_data_tp
	thread_data_alloc(int read_count, const wchar_t *dat_name);
static BOOL nt_remove_thread_data(nt_link_tp *thread_data,
		const wchar_t *dat_name);

/* 
 * Initialize the Sqlite library.
 * This needs to be called when an application starts.
 */
nt_usr_db_handle nt_usr_db_init_lib(const char *db_path)
{
	ctx_usr_db_tp ctxp;
	int ret;

	assert(db_path);
	if(SQLITE_OK != sqlite3_config(SQLITE_CONFIG_SERIALIZED))
		return NULL;
	
	if(SQLITE_OK != sqlite3_initialize())
		return NULL;

	ctxp = malloc(sizeof(ctx_usr_db_t));
	if(!ctxp)
		return NULL;

	strcpy(ctxp->db_path, db_path);

	ctxp->dbp = NULL;
	ctxp->handle.chk_sum = NT_USR_DB_CHK_SUM;

	if(!nt_usr_db_open(&ctxp->handle)){
		nt_usr_db_finish_lib(&ctxp->handle);
		return NULL;
	}
	
	ret = usr_tbl_validate(ctxp->dbp);
	if(ret == -1){
		nt_usr_db_finish_lib(&ctxp->handle);
		return NULL;
	}
	if(ret == 1){
		if(!usr_tbl_create(ctxp->dbp)){
			nt_usr_db_close(&ctxp->handle);
			nt_usr_db_finish_lib(&ctxp->handle);
			return NULL;
		}
	}
	nt_usr_db_close(&ctxp->handle);
	return &ctxp->handle;
}

/* 
 * Shutdown the Sqlite library and
 * clear up all resources.
 */
void nt_usr_db_finish_lib(nt_usr_db_handle db_handle)
{
	assert(db_handle);
	assert(db_handle->chk_sum == NT_USR_DB_CHK_SUM);
	ctx_usr_db_tp ctxp = 
		(ctx_usr_db_tp)db_handle;
	free(ctxp);
	sqlite3_shutdown();
}

BOOL nt_usr_db_open(nt_usr_db_handle db_handle)
{
	int rc;
	ctx_usr_db_tp ctxp;

	assert(db_handle);
	assert(db_handle->chk_sum == NT_USR_DB_CHK_SUM);

	ctxp = (ctx_usr_db_tp)db_handle;
	rc = sqlite3_open_v2(ctxp->db_path, &ctxp->dbp,
			SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL);
	if(rc !=  SQLITE_OK){
		fputs(sqlite3_errmsg(ctxp->dbp), stderr);
		sqlite3_close(ctxp->dbp);
		ctxp->dbp = NULL;
		return FALSE;
	}
	return TRUE;
}

void nt_usr_db_close(nt_usr_db_handle db_handle)
{
	assert(db_handle);
	ctx_usr_db_tp ctxp = 
		(ctx_usr_db_tp)db_handle;
	sqlite3_close(ctxp->dbp);
	ctxp->dbp = NULL;
}

int nt_usr_db_get_read_count_by_dat_name(
	nt_link_tp thread_data_list, const wchar_t *dat_name)
{
	nt_link_tp linkp;
	nt_usr_db_thread_data_tp datap;
	assert(dat_name);
	if(!thread_data_list)
		return -1;
	linkp = thread_data_list;
	do{
		datap = (nt_usr_db_thread_data_tp)
			linkp->data;
		if(0 == wcscmp(datap->dat_name, dat_name)){
			return datap->read_count;
		}
		linkp = linkp->next;
	}while(linkp != thread_data_list);

	return -1;
}


nt_link_tp nt_usr_db_query_read_count_list(nt_usr_db_handle db_handle,
	const wchar_t *board_name)
{
	nt_link_tp result_linkp;
	nt_link_tp linkp;
	nt_usr_db_thread_data_tp thread_datap;
	const unsigned char *dat_name;
	int res_no;
	int len;
	int rc , idx;
	ctx_usr_db_tp ctxp;
	char board_nm_buf[256];
	wchar_t dat_nm_buf[64];
	sqlite3_stmt *stmt;
	iconv_t icd;
	BOOL result;

	assert(db_handle);
	assert(db_handle->chk_sum == NT_USR_DB_CHK_SUM);
	assert(board_name);

	result_linkp = NULL;
	stmt = NULL;
	icd = (iconv_t)-1;
	result = FALSE;

	ctxp = (ctx_usr_db_tp)db_handle;

	len = wcstombs(board_nm_buf, board_name, sizeof(board_nm_buf));
	if(len <= 0)
		return NULL;

	if(!nt_usr_db_open(db_handle)){
		return NULL;
	}

	rc = sqlite3_prepare_v2(ctxp->dbp,
			NT_SQL_QUERY_READ_CNT_LIST, -1,
			&stmt, NULL);
	if(rc != SQLITE_OK){
		fputs(sqlite3_errmsg(ctxp->dbp), stderr);
		nt_usr_db_close(db_handle);
		return NULL;
	}
	idx = sqlite3_bind_parameter_index(stmt, ":board_name");
	if(idx == 0 ||
		SQLITE_OK != sqlite3_bind_text(stmt, idx, 
			board_nm_buf, -1, SQLITE_STATIC)){
		goto ERROR_TRAP;
	}

	icd = iconv_open("wchar_t", "UTF-8");
	if(icd == (iconv_t)-1){
	    perror("e");
		goto ERROR_TRAP;
	}

	while(SQLITE_ROW == (rc = sqlite3_step(stmt))){
		res_no = sqlite3_column_int(stmt, 1);
		dat_name = sqlite3_column_text(stmt, 0);
		if(dat_name == NULL)
			goto ERROR_TRAP;

		if(!nt_conv_local2wc(icd, (char*)dat_name, 
					dat_nm_buf, sizeof(dat_nm_buf)))
			goto ERROR_TRAP;
		thread_datap =
			thread_data_alloc(res_no, dat_nm_buf);
		if(!thread_datap)
			goto ERROR_TRAP;
		linkp = nt_link_add_data(result_linkp, thread_datap);
		if(!linkp){
			nt_usr_db_thread_data_free(thread_datap);
			goto ERROR_TRAP;
		}
		if(!result_linkp)
			result_linkp = linkp;

	}
	if(SQLITE_DONE == rc){
		result = TRUE;
	}
ERROR_TRAP:
	if(icd != (iconv_t)-1)
		iconv_close(icd);
	if(stmt)
		sqlite3_finalize(stmt);
	nt_usr_db_close(db_handle);
	if(!result && result_linkp){
		nt_all_link_free(result_linkp, 
				nt_usr_db_thread_data_free);
		result_linkp = NULL;
	}
	return result_linkp;
}

int nt_usr_db_update_read_count(nt_usr_db_handle db_handle,
	const wchar_t *board_name, const wchar_t *dat_name, 
	int new_read_cnt)
{
	int len, result;
	int rc , idx;
	ctx_usr_db_tp ctxp;
	char board_nm_buf[256];
	char dat_nm_buf[256];
	sqlite3_stmt *stmt;

	assert(db_handle);
	assert(db_handle->chk_sum == NT_USR_DB_CHK_SUM);
	assert(board_name && dat_name);

	result = -1;

	ctxp = (ctx_usr_db_tp)db_handle;

	len = wcstombs(board_nm_buf, board_name, sizeof(board_nm_buf));
	if(len <= 0)
		return -1;
	len = wcstombs(dat_nm_buf, dat_name, sizeof(dat_nm_buf));
	if(len <= 0)
		return -1;

	if(!nt_usr_db_open(db_handle)){
		return -1;
	}

	rc = sqlite3_prepare_v2(ctxp->dbp,
			NT_SQL_QUERY_READ_CNT, -1,
			&stmt, NULL);
	if(rc != SQLITE_OK){
		fputs(sqlite3_errmsg(ctxp->dbp), stderr);
		nt_usr_db_close(db_handle);
		return -1;
	}
	idx = sqlite3_bind_parameter_index(stmt, ":board_name");
	if(idx == 0 ||
		SQLITE_OK != sqlite3_bind_text(stmt, idx, 
			board_nm_buf, -1, SQLITE_STATIC)){
		goto ERROR_TRAP;
	}
	idx = sqlite3_bind_parameter_index(stmt, ":dat_name");
	if(idx == 0 ||
		SQLITE_OK != sqlite3_bind_text(stmt, idx, 
			dat_nm_buf, -1, SQLITE_STATIC)){
		goto ERROR_TRAP;
	}
	rc = sqlite3_step(stmt);
	if(rc == SQLITE_ROW){
		result = sqlite3_column_int(stmt, 0);
		sqlite3_finalize(stmt);
		rc = sqlite3_prepare_v2(ctxp->dbp,
				NT_SQL_UPDATE_READ_CNT, -1,
				&stmt, NULL);
	}else if(rc == SQLITE_DONE){
		sqlite3_finalize(stmt);
		rc = sqlite3_prepare_v2(ctxp->dbp,
				NT_SQL_INSERT_READ_CNT, -1,
				&stmt, NULL);
	}else{
		fputs(sqlite3_errmsg(ctxp->dbp), stderr);
		goto ERROR_TRAP;
	}

	if(rc != SQLITE_OK){
		fputs(sqlite3_errmsg(ctxp->dbp), stderr);
		goto ERROR_TRAP;
	}
	
	idx = sqlite3_bind_parameter_index(stmt, ":board_name");
	if(idx == 0 ||
		SQLITE_OK != sqlite3_bind_text(stmt, idx, 
			board_nm_buf, -1, SQLITE_STATIC)){
		goto ERROR_TRAP;
	}
	idx = sqlite3_bind_parameter_index(stmt, ":dat_name");
	if(idx == 0 ||
		SQLITE_OK != sqlite3_bind_text(stmt, idx, 
			dat_nm_buf, -1, SQLITE_STATIC)){
		goto ERROR_TRAP;
	}
	idx = sqlite3_bind_parameter_index(stmt, ":res_no");
	if(idx == 0 ||
		SQLITE_OK != sqlite3_bind_int(stmt, idx, new_read_cnt)){
		goto ERROR_TRAP;
	}
	rc = sqlite3_step(stmt);
	if(rc != SQLITE_DONE){
		goto ERROR_TRAP;
	}
	if(result < 0)
		result = 0;
ERROR_TRAP:
	sqlite3_finalize(stmt);
	nt_usr_db_close(db_handle);
	return result;
}

BOOL nt_usr_db_delete_board_log(nt_usr_db_handle db_handle,
	const wchar_t *board_name)
{
	int len;
	int rc , idx;
	ctx_usr_db_tp ctxp;
	char board_nm_buf[256];
	sqlite3_stmt *stmt;
	BOOL result;

	assert(db_handle);
	assert(db_handle->chk_sum == NT_USR_DB_CHK_SUM);
	assert(board_name);

	stmt = NULL;
	result = FALSE;

	ctxp = (ctx_usr_db_tp)db_handle;

	len = wcstombs(board_nm_buf, board_name, sizeof(board_nm_buf));
	if(len <= 0)
		return FALSE;

	if(!nt_usr_db_open(db_handle)){
		return FALSE;
	}

	rc = sqlite3_prepare_v2(ctxp->dbp,
			NT_SQL_DELETE_BOARD_LOG, -1,
			&stmt, NULL);
	if(rc != SQLITE_OK){
		fputs(sqlite3_errmsg(ctxp->dbp), stderr);
		goto ERROR_TRAP;
	}
	idx = sqlite3_bind_parameter_index(stmt, ":board_name");
	if(idx == 0 ||
		SQLITE_OK != sqlite3_bind_text(stmt, idx, 
			board_nm_buf, -1, SQLITE_STATIC)){
		goto ERROR_TRAP;
	}

	rc = sqlite3_step(stmt);
	if(rc != SQLITE_DONE){
		goto ERROR_TRAP;
	}
	result = TRUE;
ERROR_TRAP:
	if(stmt)
	    sqlite3_finalize(stmt);
	nt_usr_db_close(db_handle);
	return result;
}

BOOL nt_usr_db_delete_thread_log(nt_usr_db_handle db_handle,
	const wchar_t *board_name, const wchar_t *dat_name,
	nt_link_tp *thread_data_list)
{
	int len;
	int rc , idx;
	ctx_usr_db_tp ctxp;
	char board_nm_buf[256];
	char dat_nm_buf[64];
	sqlite3_stmt *stmt;
	BOOL result;

	assert(db_handle);
	assert(db_handle->chk_sum == NT_USR_DB_CHK_SUM);
	assert(board_name);
	assert(dat_name);

	stmt = NULL;
	result = FALSE;

	ctxp = (ctx_usr_db_tp)db_handle;

	len = wcstombs(board_nm_buf, board_name, sizeof(board_nm_buf));
	if(len <= 0)
		return FALSE;

	len = wcstombs(dat_nm_buf, dat_name, sizeof(dat_nm_buf));
	if(len <= 0)
		return FALSE;

	if(!nt_usr_db_open(db_handle)){
		return FALSE;
	}

	rc = sqlite3_prepare_v2(ctxp->dbp,
			NT_SQL_DELETE_THREAD_LOG, -1,
			&stmt, NULL);
	if(rc != SQLITE_OK){
		fputs(sqlite3_errmsg(ctxp->dbp), stderr);
		goto ERROR_TRAP;
	}
	idx = sqlite3_bind_parameter_index(stmt, ":board_name");
	if(idx == 0 ||
		SQLITE_OK != sqlite3_bind_text(stmt, idx, 
			board_nm_buf, -1, SQLITE_STATIC)){
		goto ERROR_TRAP;
	}
	idx = sqlite3_bind_parameter_index(stmt, ":dat_name");
	if(idx == 0 ||
		SQLITE_OK != sqlite3_bind_text(stmt, idx, 
			dat_nm_buf, -1, SQLITE_STATIC)){
		goto ERROR_TRAP;
	}


	rc = sqlite3_step(stmt);
	if(rc != SQLITE_DONE){
		goto ERROR_TRAP;
	}
	if(thread_data_list &&  *thread_data_list &&
			!nt_remove_thread_data(thread_data_list, dat_name)){
		goto ERROR_TRAP;
	}
	result = TRUE;
ERROR_TRAP:
	if(stmt)
	    sqlite3_finalize(stmt);
	nt_usr_db_close(db_handle);
	return result;
}

static BOOL nt_remove_thread_data(nt_link_tp *threadpp,
		const wchar_t *dat_name)
{
	nt_link_tp threadp;
	nt_link_tp linkp, nextp;
	nt_usr_db_thread_data_tp datap;


	threadp = *threadpp;

	linkp = threadp->next;
	while(linkp != threadp){
		datap = (nt_usr_db_thread_data_tp)
			linkp->data;
		if(!datap){
			linkp = linkp->next;
			continue;
		}
		if(0 != wcscmp(dat_name, datap->dat_name)){
			linkp = linkp->next;
			continue;
		}
		nextp = linkp->next;
		nt_link_remove2(threadp, linkp);
		nt_usr_db_thread_data_free(datap);
		free(linkp);
		linkp = nextp;
	}
	datap = (nt_usr_db_thread_data_tp)
		linkp->data;
	if(datap && 0 == wcscmp(dat_name, datap->dat_name)){
		*threadpp = nt_link_remove2(threadp, linkp);
		nt_usr_db_thread_data_free(datap);
		free(linkp);
	}
	return  TRUE;
}


/*
 * name: usr_tbl_validate
 * function: validate usr table
 * param: dbp sqlite3 object pointer
 * return: -1 error 0. success. 1 no usr table record found.
 */
static int usr_tbl_validate(sqlite3 *dbp)
{
	sqlite3_stmt *stmt;
	int rc, ret;

	stmt = NULL;

	rc = sqlite3_prepare_v2(dbp,
			NT_SQL_VALIDATE_USR_TBL, -1,
			&stmt, NULL);
	if(rc != SQLITE_OK){
		fputs(sqlite3_errmsg(dbp), stderr);
		return -1;
	}
	rc = sqlite3_step(stmt);
	if(rc == SQLITE_ROW){
		ret = 0;
	}else if(rc == SQLITE_DONE){
		ret = 1;
	}else{
		fputs(sqlite3_errmsg(dbp), stderr);
		ret = -1;
	}
	sqlite3_finalize(stmt);
	return ret;
}

static BOOL usr_tbl_create(sqlite3 *dbp)
{
	sqlite3_stmt *stmt;
	int rc;

	stmt = NULL;

	rc = sqlite3_prepare_v2(dbp,
			NT_SQL_CREATE_USR_TBL, -1,
			&stmt, NULL);
	if(rc != SQLITE_OK){
		fputs(sqlite3_errmsg(dbp), stderr);
		return FALSE;
	}
	rc = sqlite3_step(stmt);
	if(rc != SQLITE_DONE){
		fputs(sqlite3_errmsg(dbp), stderr);
		return FALSE;
	}
	sqlite3_finalize(stmt);
	return TRUE;
}


static nt_usr_db_thread_data_tp
	thread_data_alloc(int read_count, const wchar_t *dat_name)
{
	nt_usr_db_thread_data_tp datap;
	wchar_t *wc;

	assert(dat_name);
	assert(read_count >= 0);

	datap = malloc(sizeof(nt_usr_db_thread_data_t));
	if(!datap)
		return NULL;
	wc = nt_w_str_clone(dat_name);
	if(!wc){
		free(datap);
		return NULL;
	}
	datap->dat_name = wc;
	datap->read_count = read_count;
	return datap;
}
void nt_usr_db_thread_data_free(void *ptr)
{
	assert(ptr);
	nt_usr_db_thread_data_tp datap =
		(nt_usr_db_thread_data_tp)ptr;
	free(datap->dat_name);
	free(ptr);
}
