/* GtkSQL -- an interactive graphical query tool for PostgreSQL
 * Copyright (C) 1998  Lionel ULMER
 *
 * MySQL "driver" (C) 1998 Des Herriott (des@ops.netcom.net.uk)
 *
 * This program 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.
 *
 * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

#include <stdlib.h>
#include <string.h>

#include <mysql.h>

#include "common.h"
#include "status.h"

/* You need to have a DBConnection first to cast it to a (DBConnection *) */
typedef struct {
  DBConnection public;

  /* Private */
  MYSQL *conn;

  char *dbname;
  
  DBTableDef *table_def;
  int table_num;
} DBConnection_MYSQL;

/*
 * A nasty hack: if a MySQL query succeeded, but returned a NULL result 
 * (e.g. after an UPDATE), we return the address of <dummy_res> from 
 * DBexecute_query().  This is so we can distinguish a successful query
 * which returned nothing from a failed query in DBget_query_status().
 */
MYSQL_RES dummy_res;

/* TODO - need a correct keyword list for MySQL */
static char *SQLkeywords[] = {
  "ABORT", "AND", "ALTER", "ASC", "BEGIN", "CLUSTER", "CLOSE", "COMMIT",
  "COPY", "CREATE", "AGGREGATE", "DATABASE", "FUNCTION", "INDEX", "OPERATOR",
  "RULE", "SEQUENCE", "TABLE", "TRIGGER", "TYPE", "USER", "VIEW", "DECLARE",
  "DELETE", "DROP", "END", "EXPLAIN", "FETCH", "GRANT", "INSERT", "LISTEN",
  "LOAD", "LOCK", "MOVE", "NOTIFY", "RESET", "REVOKE", "ROLLBACK", "SET",
  "SHOW", "UPDATE", "VACUUM", "DESC", "FROM", "GROUP BY", "LIKE", "OR",
  "ORDER BY", "SELECT", "TABLE", "TRANSACTION", "USER", "WHERE", "WORK",
  "INTO", "GROUP BY", "UNION", "USING", "ALL", "SUM", "MAX", "MIN", "MEAN",
  "COUNT", "DISTINCT", NULL
};

struct callback_data {
  void (*error_dialog)(char *, char *);
  
  GtkWidget *basename;
  GtkWidget *basehost;
  GtkWidget *baseport;
  GtkWidget *baselogin;
  GtkWidget *basepass; 
};

static int DBdisconnect(DBConnection *c) {
  MYSQL *co = ((DBConnection_MYSQL *) c)->conn;
  DBConnection_MYSQL *conn = (DBConnection_MYSQL *) c;

  /* End connection */
  mysql_close(co);

  free(conn->dbname);   
  
  /* Clear Keywords memory */
  RemoveAllKeywords(c);

  /* Free structure memory */
  free((DBConnection_MYSQL *) c);            

  return 0;
}

static char *DBget_error_message(DBConnection *c) {
  MYSQL *co = ((DBConnection_MYSQL *) c)->conn;  

  return mysql_error(co);
}

static char *DBget_db_name(DBConnection *c) {
  DBConnection_MYSQL *conn = (DBConnection_MYSQL *) c;
  return conn->dbname;
}

static void *DBexecute_query(DBConnection *c, char *query) {
  MYSQL *co = ((DBConnection_MYSQL *) c)->conn; 
  MYSQL_RES *res;

  if (mysql_query(co, query))
    return NULL;

  res = mysql_store_result(co);

  if (res == NULL && mysql_num_fields(co))
    return NULL;
  else if (res == NULL)
    return &dummy_res;

  return res;
}

static int DBget_query_status(DBConnection *c, void *result) {
  MYSQL *co = ((DBConnection_MYSQL *) c)->conn; 
  MYSQL_RES *res = result;
  char *err;
  
  err = mysql_error(co);
  if (err[0]) {
    return DB_QUERY_ERROR;
  } else if (res == &dummy_res) {
    return DB_QUERY_COMMAND_OK;
  } else if (mysql_num_rows(res) == 0) {
    return DB_QUERY_EMPTY;
  } else {
    return DB_QUERY_RECORDS_OK;
  }
}

static int DBget_record_number(DBConnection *c, void *result) {
  MYSQL_RES *res = result;

  return mysql_num_rows(res);
}

static int DBget_field_number(DBConnection *c, void *result) {
  MYSQL_RES *res = result;

  return mysql_num_fields(res);
}

static char *DBget_field_name(DBConnection *c, void *result, int field) {
  MYSQL_RES *res = result;
  MYSQL_FIELD *f;

  mysql_field_seek(res, field);
  if ((f = mysql_fetch_field(res)) == NULL)
    return NULL;
  return f->name;
}

static char *DBget_field_value(DBConnection *c, void *result,
			       int record, int field) {
  MYSQL_RES *res = result;
  MYSQL_ROW row;

  mysql_data_seek(res, record);
  if ((row = mysql_fetch_row(res)) == NULL) {
    return NULL;
  }
  return (char *)row[field];
}

static void DBclear_query(DBConnection *c, void *result) {
  MYSQL_RES *res = result;

  mysql_free_result(res);
}


static int add_field_def(DBConnection_MYSQL *c, DBTableDef *tbf) { 
  MYSQL *co = ((DBConnection_MYSQL *) c)->conn; 
  MYSQL_RES *table_info;
  char query[256]; /* FIXME */
  char type_str[64]; /* FIXME */
  int i;
  char *fname;
  char *rtype, *rnotnull, *rhasdef; 
  
  sprintf(query, "describe %s", tbf->name);

  if (mysql_query(co, query))
    return -1;
  if ((table_info = mysql_store_result(co)) == NULL)
    return -1;

  tbf->field_num = mysql_num_rows(table_info);
  tbf->field_def = (DBFieldDef *)
    malloc(tbf->field_num * sizeof(DBFieldDef));

  for (i = 0; i < tbf->field_num; i++) {
    MYSQL_ROW row;

    mysql_data_seek(table_info, i);
    row = mysql_fetch_row(table_info);

    fname = row[0];
    AddKeyword((DBConnection *) c, fname, KEYWORD_FIELD); 

    rtype = row[1];
    rnotnull = row[2];
    rhasdef = row[4];
  
    strcpy(type_str, rtype);

    if (strcmp(rnotnull, "YES") == 0)
      strcat(type_str, " not null");

    if (rhasdef) {
      strcat(type_str, " default ");
      strcat(type_str, rhasdef);
    }

    tbf->field_def[i].name = g_strdup(fname);
    tbf->field_def[i].type =  g_strdup(type_str);
    tbf->field_def[i].length = g_strdup("??");
  }

  return 0;
}

static int DBget_table_def(DBConnection *c) {
  DBConnection_MYSQL *conn = (DBConnection_MYSQL *) c;
  char query[] = "show tables";
  MYSQL_RES *res;

  if (conn->table_def != NULL) {
    fprintf(stderr, "Internal error (memory leak)...\n");
    return -1;
  }

  if (mysql_query(conn->conn, query)) {
    return -1;
  }
  if ((res = mysql_store_result(conn->conn)) == NULL) {
    return -1;
  } else {
    int i;
    int tables_in_list = mysql_num_rows(res);

    /* Allocating table array */
    conn->table_num = tables_in_list;
    conn->table_def = (DBTableDef *)
      malloc(tables_in_list * sizeof(DBTableDef)); 
 
    /* Fill the table */
    for (i = 0; i < tables_in_list; i++) {
      char *table_name;
      MYSQL_ROW row;

      mysql_data_seek(res, i);
      row = mysql_fetch_row(res);
      table_name = row[0];

      /* Table name */
      conn->table_def[i].name = g_strdup(table_name);

      /* Table fields */
      if (add_field_def(conn, conn->table_def + i) < 0) {
        conn->table_def = NULL;
        return -1;        
      }

      /* Keywords */
      AddKeyword(c, table_name, KEYWORD_TABLE);
    } /* for */

    mysql_free_result(res);
  }
  return 0;
}

/* Non PostgreSQL dependant */
static void DBfree_table_def(DBConnection *c) {
  DBConnection_MYSQL *conn = (DBConnection_MYSQL *) c;

  if (conn->table_def == NULL) {
    fprintf(stderr, "Internal error (freeing a NULL connection)...\n");
    return ;
  } else {
    DBFieldDef *dbf;
    int i, j;

    for (j = 0; j < conn->table_num; j++) {
      int fnum = (conn->table_def)[j].field_num;
      
      free(conn->table_def[j].name);
      
      dbf = (conn->table_def)[j].field_def;
      for (i = 0; i < fnum ; i++) {
	free(dbf[i].name);
	free(dbf[i].type);
      }
      free(dbf);
    }
    free(conn->table_def);
    conn->table_def = NULL;

    /* Removing Keywords */
    RemoveKeywords(c, KEYWORD_TABLE);
    RemoveKeywords(c, KEYWORD_FIELD); 
  }
}

/* Non PostgreSQL dependant */
static int DBget_table_num(DBConnection *c) {
  DBConnection_MYSQL *conn = (DBConnection_MYSQL *) c;

  return conn->table_num;
}

/* Non PostgreSQL dependant */
static DBTableDef *DBget_table(DBConnection *c, int table) {
  DBConnection_MYSQL *conn = (DBConnection_MYSQL *) c;

  if (table > conn->table_num) {
    fprintf(stderr, "Internal error (table number too big)...\n");
    return NULL;
  }

  if (conn->table_def == NULL) {
    fprintf(stderr, "Internal error (searching a NULL table)...\n");
    return NULL;
  } else {
    return conn->table_def + table;
  }
}

static int connect_callback(void *data,
			    DBConnection **ret,
			    GList **base_list,
			    char *name) {
  struct callback_data *cb_data = (struct callback_data *) data;
  char *bname, *bhost, *bport, *blogin, *bpass;
  
  /* Gets the results */
  bname = gtk_entry_get_text(GTK_ENTRY(cb_data->basename));
  bhost = gtk_entry_get_text(GTK_ENTRY(cb_data->basehost));
  if ((bhost != NULL) && (strlen(bhost) == 0))
    bhost = NULL;
  bport = gtk_entry_get_text(GTK_ENTRY(cb_data->baseport));
  if ((bport != NULL) && (strlen(bport) == 0))
    bport = NULL;
  blogin = gtk_entry_get_text(GTK_ENTRY(cb_data->baselogin));
  if ((blogin != NULL) && (strlen(blogin) == 0))
    blogin = NULL;
  bpass = gtk_entry_get_text(GTK_ENTRY(cb_data->basepass));
  if ((bpass != NULL) && (strlen(bpass) == 0))
    bpass = NULL; 
  
  /* Tests the results */
  if ((bname == NULL) || (strlen(bname) == 0)) {
    cb_data->error_dialog("Connection Error",
			  "You need to provide a database name");

    return CONNECT_ERROR;
  } else {
    char buf[256];  /* TODO: check for overflow */
    MYSQL *my_conn;
    DBConnection_MYSQL *ret_my;
    int i;

    /* All OK - set things up */
    status_push("Connecting to database...");
    
    if ((my_conn = malloc(sizeof(MYSQL))) == NULL) {
      return CONNECT_ERROR;
    }
    if (!mysql_connect(my_conn, bhost, blogin, bpass)) {
      sprintf(buf, "Database connection error : \n %s", mysql_error(my_conn));
      cb_data->error_dialog("Connection Error", buf);
      status_pop();
      return CONNECT_ERROR;
    }
    if (mysql_select_db(my_conn, bname)) {
      sprintf(buf, "Database connection error : \n %s", mysql_error(my_conn));
      cb_data->error_dialog("Connection Error", buf);
      status_pop();
      return CONNECT_ERROR;
    }

    /* We use calloc => Table def is null */ 
    ret_my = (DBConnection_MYSQL *) calloc(1, sizeof(DBConnection_MYSQL));
    /* The connection (private) */
    ret_my->conn = my_conn;
    /* The database name (private) */
    ret_my->dbname = g_strdup(bname);
    
    /* The functions (public) */
    ret_my->public.DBdisconnect = DBdisconnect;
    ret_my->public.DBget_error_message = DBget_error_message;
    ret_my->public.DBget_db_name = DBget_db_name;
    ret_my->public.DBexecute_query = DBexecute_query;
    ret_my->public.DBget_query_status = DBget_query_status;
    ret_my->public.DBget_record_number = DBget_record_number;
    ret_my->public.DBget_field_number = DBget_field_number;
    ret_my->public.DBget_field_name = DBget_field_name;
    ret_my->public.DBget_field_value = DBget_field_value;
    ret_my->public.DBclear_query = DBclear_query;         

    /* The keywords (public) */
    ret_my->public.keylist = NULL;
    i = 0;
    while (SQLkeywords[i] != NULL)
      AddKeyword((DBConnection *) ret_my, SQLkeywords[i++], KEYWORD_SQL);

    /* The tables functions (public) */
    ret_my->public.DBget_table_def = DBget_table_def;
    ret_my->public.DBfree_table_def = DBfree_table_def;
    ret_my->public.DBget_table_num = DBget_table_num;
    ret_my->public.DBget_table = DBget_table;    

    *ret = (DBConnection *) ret_my;

    status_pop();
  }
  
  return CONNECT_OK;
}

static GtkWidget *AddEntryLine(char *labeltext, char *entrydefault,
			       GtkWidget *table, int height, int pass,
			       DBConnector **ret_param,
			       void (*ok_callback)(GtkWidget *, gpointer)) {
  GtkWidget *label, *entry;

  label = gtk_label_new(labeltext);
  gtk_table_attach_defaults(GTK_TABLE(table), label, 0, 1, height, height + 1);
  gtk_widget_show(label);
  
  entry = gtk_entry_new();
  gtk_entry_set_text(GTK_ENTRY(entry), entrydefault);
  if (pass)
    gtk_entry_set_visibility (GTK_ENTRY(entry), FALSE);
  gtk_table_attach_defaults(GTK_TABLE(table), entry, 1, 2, height, height + 1);
  gtk_signal_connect(GTK_OBJECT(entry), "activate",
		     GTK_SIGNAL_FUNC(ok_callback), ret_param); 
  
  gtk_widget_show(entry);
  
  return entry;
}

DBConnector *register_MySQL(GtkWidget **notebook_pane,
			    GtkWidget **notebook_label,
			    void (*error_dialog)(char *, char *),
			    DBConnector **ret_param,
			    void (*ok_callback)(GtkWidget *, gpointer)) {
  DBConnector *ret;
  struct callback_data *cb_data;
  GtkWidget *table;
  GtkWidget *basename, *basehost, *baseport, *baselogin, *basepass;

  /* Prepares all the structures */
  cb_data = (struct callback_data *) malloc(sizeof(struct callback_data));
  if (cb_data == NULL)
    return NULL;
  cb_data->error_dialog = error_dialog;
  
  ret = (DBConnector *) malloc(sizeof(DBConnector));
  if (ret == NULL)
    return NULL;
  ret->data = (void *) cb_data;
  ret->connect_callback = connect_callback;
  
  /* Creates the notebook pane and label */
  /* The connection pane */    
  table = gtk_table_new(5, 2, FALSE);
  gtk_container_border_width(GTK_CONTAINER(table), 6);
  basename = AddEntryLine("Database Name : ", "", table, 0, 0,
			  ret_param, ok_callback);
  basehost = AddEntryLine("Database Host : ", "", table, 1, 0,
			  ret_param, ok_callback);
  baseport = AddEntryLine("Database Port : ", "", table, 2, 0,
			  ret_param, ok_callback);
  baselogin = AddEntryLine("Database Login : ", "", table, 3, 0,
			   ret_param, ok_callback);
  basepass = AddEntryLine("Database Password : ", "", table, 4, 1,
			  ret_param, ok_callback);
  gtk_widget_show(table);
  *notebook_pane = table;

  /* The label */
  *notebook_label = gtk_label_new("MySQL");
  
  /* Creates the data-structure */
  cb_data->basename = basename;
  cb_data->basehost = basehost;
  cb_data->baseport = baseport;
  cb_data->baselogin = baselogin;
  cb_data->basepass = basepass;
  
  return ret;
}
