/* GtkSQL -- an interactive graphical query tool for PostgreSQL
 * Copyright (C) 1998  Lionel ULMER
 *
 * 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 <libpq-fe.h>
#include <string.h>
#include <postgres.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 */
  PGconn *conn;
  
  DBTableDef *table_def;
  int table_num;
} DBConnection_PG;

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) {
  PGconn *co = ((DBConnection_PG *) c)->conn;

  /* End connection */
  PQfinish(co);

  /* Clear Keywords memory */
  RemoveAllKeywords(c);
  
  /* Free structure memory */
  free((DBConnection_PG *) c);
  
  return 0;
}

static char *DBget_error_message(DBConnection *c) {
  PGconn *co = ((DBConnection_PG *) c)->conn;

  return PQerrorMessage(co);
}
static char *DBget_db_name(DBConnection *c) {
  PGconn *co = ((DBConnection_PG *) c)->conn;

  return PQdb(co);
}

static void *DBexecute_query(DBConnection *c, char *query) {
  PGconn *co = ((DBConnection_PG *) c)->conn;

  return (void *) PQexec(co, query);
}
static int DBget_query_status(DBConnection *c, void *result) {
  if (result == NULL)
    return DB_QUERY_ERROR;
  
  switch (PQresultStatus((PGresult *) result)) {
  case PGRES_TUPLES_OK:
    return DB_QUERY_RECORDS_OK;
  case PGRES_COMMAND_OK:
    return DB_QUERY_COMMAND_OK;
  case PGRES_EMPTY_QUERY:
    return DB_QUERY_EMPTY;
  case PGRES_BAD_RESPONSE:
  case PGRES_NONFATAL_ERROR:
  case PGRES_FATAL_ERROR:
    return DB_QUERY_ERROR;
  default:
    return DB_QUERY_OTHER;
  }
}
static int DBget_record_number(DBConnection *c, void *result) {
  return PQntuples((PGresult *) result);
}
static int DBget_field_number(DBConnection *c, void *result) {
  return PQnfields((PGresult *) result);
}
static char *DBget_field_name(DBConnection *c, void *result, int field) {
  return PQfname((PGresult *) result, field);
}
static char *DBget_field_value(DBConnection *c, void *result,
			       int record, int field) {
  return PQgetvalue((PGresult *) result, record, field);
}
static void DBclear_query(DBConnection *c, void *result) {
  PQclear((PGresult *) result);
}

static int add_field_def(DBConnection_PG *c, DBTableDef *tbf) {
  PGconn *conn = c->conn;
  
  PGresult *table_info;
  char query[512];

  sprintf(query, "SELECT a.attnum, a.attname, t.typname, a.attlen,"\
                        "a.atttypmod, a.attnotnull, a.atthasdef"\
                 "FROM pg_class c, pg_attribute a, pg_type t"\
                 "WHERE c.relname = '%s' AND"\
                       "a.attnum > 0 AND"\
                       "a.attrelid = c.oid AND"\
                       "a.atttypid = t.oid"\
                 "ORDER BY attnum", tbf->name);
  
  table_info = PQexec(conn, query);
  if ((table_info == NULL) ||
      (PQresultStatus(table_info) != PGRES_TUPLES_OK)) {
    
    if (table_info != NULL)
      PQclear(table_info);
    
    return -1;
  } else {
    int i;

    tbf->field_num = PQntuples(table_info);
    tbf->field_def = (DBFieldDef *)
      malloc(tbf->field_num * sizeof(DBFieldDef));
    
    for (i = 0; i < tbf->field_num ; i++) {
      char *line[3];
      char type_str[64];
      char *rtype, *rnotnull, *rhasdef;
      int attlen, atttypmod;
      char buf[512];
      PGresult *table_info2;
      
      line[0] = PQgetvalue(table_info, i, 1);
      AddKeyword((DBConnection *) c, line[0], KEYWORD_FIELD);
      
      rtype = PQgetvalue(table_info, i, 2);
      attlen = atoi(PQgetvalue(table_info, i, 3));
      atttypmod = atoi(PQgetvalue(table_info, i, 4));
      rnotnull = PQgetvalue(table_info, i, 5);
      rhasdef = PQgetvalue(table_info, i, 6);
      
      strcpy(type_str, rtype);
      if (strcmp(rtype, "bpchar") == 0)
	strcpy(type_str, "char()");
      else if (strcmp(rtype, "varchar") == 0)
	strcpy(type_str, "varchar()");
      else if (rtype[0] == '_') {
	strcpy(type_str, rtype + 1);
	strcat(type_str, "[]");
      }
      
      if (rnotnull[0] == 't')
	strcat(type_str, " not null");
      if (rhasdef[0] == 't') {
	sprintf(buf, "SELECT d.adsrc"\
                     "FROM pg_attrdef d, pg_class c"\
                     "WHERE c.relname = '%s' AND"\
                           "c.oid = d.adrelid AND"\
                           "d.adnum = %s",
		tbf->name, PQgetvalue(table_info, i, 0));
	table_info2 = PQexec(conn, buf);
	if ((table_info2 == NULL) ||
	    (PQresultStatus(table_info2) != PGRES_TUPLES_OK)) {
	  if (table_info != NULL)
	    PQclear(table_info);
	  if (table_info2 != NULL)
	    PQclear(table_info2);
	  
	  return -1;
	}
	strcat(type_str, " default ");
	strcat(type_str, PQgetvalue(table_info2, 0, 0));
	PQclear(table_info2);
      }
      line[1] = type_str;
      
      if (strcmp(rtype, "text") == 0)
	sprintf(buf, "var");
      else if (strcmp(rtype, "bpchar") == 0 ||
	       strcmp(rtype, "varchar") == 0) {
	sprintf(buf, "%i", atttypmod != -1 ? atttypmod - VARHDRSZ : 0);
      } else {
	if (attlen > 0)
	  sprintf(buf, "%i", attlen);
	else
	  sprintf(buf, "var");
      }
      line[2] = buf;
      
      tbf->field_def[i].name = g_strdup(line[0]);
      tbf->field_def[i].type = g_strdup(line[1]);
      tbf->field_def[i].length = g_strdup(line[2]);
    }
    
    if (table_info != NULL)
      PQclear(table_info);
  }

  return 0;
}
static int DBget_table_def(DBConnection *c) {
  DBConnection_PG *conn = (DBConnection_PG *) c;

  if (conn->table_def != NULL) {
    fprintf(stderr, "Internal error (memory leak)...\n");
    return -1;
  } else {
    int i;
    PGresult *tables;
    char query[512];
    
    sprintf(query, "SELECT usename, relname, relkind, relhasrules"\
                   "FROM pg_class, pg_user"\
                   "WHERE relkind = 'r' AND"\
                         "relname !~ '^pg_' AND"\
                         "relname !~ '^xin[vx][0-9]+' AND"\
                         "usesysid = relowner"\
                   "ORDER BY relname");
  
    tables = PQexec(conn->conn, query);
    if ((tables == NULL) || (PQresultStatus(tables) != PGRES_TUPLES_OK)) {
      if (tables != NULL)
	PQclear(tables);
      return -1;
    } else {
      int tables_in_list = PQntuples(tables);

      /* Allocating table array */
      conn->table_num = tables_in_list;
      conn->table_def = (DBTableDef *)
	malloc(tables_in_list * sizeof(DBTableDef));

      /* Filling the table */
      for (i = 0; i < tables_in_list; i++) {
	char *table_name = PQgetvalue(tables, i, 1);

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

	/* Fields */
	if (add_field_def(conn, conn->table_def + i) < 0) {
	  /* TODO: correct leak !!! */

	  conn->table_def = NULL;
	  
	  return -1;
	}
	
	/* Keywords */
	AddKeyword(c, table_name, KEYWORD_TABLE);
      }
      if (tables != NULL)
	PQclear(tables);
    }
  }

  return 0;
}

/* Non PostgreSQL dependant */
static void DBfree_table_def(DBConnection *c) {
  DBConnection_PG *conn = (DBConnection_PG *) 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_PG *conn = (DBConnection_PG *) c;

  return conn->table_num;
}

/* Non PostgreSQL dependant */
static DBTableDef *DBget_table(DBConnection *c, int table) {
  DBConnection_PG *conn = (DBConnection_PG *) 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;
  }
}

/* Gets a list of databases on the given server */
static GList *get_base_list(char *host, char *port) {
  char *query = "SELECT datname FROM pg_database;";
  PGconn *sc;
  PGresult *base_list;
  GList *cbitems = NULL;
  int i;

  sc = PQsetdb(host, port, NULL, NULL, "template1");
  if (PQstatus(sc) == CONNECTION_BAD)
    return NULL;

  base_list = PQexec(sc, query);
  if ((base_list == NULL) || (PQresultStatus(base_list) != PGRES_TUPLES_OK))
    return NULL;
  
  for (i = 0; i < PQntuples(base_list); i++)
    cbitems = g_list_append(cbitems, g_strdup(PQgetvalue(base_list, i, 0)));
  
  return cbitems;
}

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; 

  if (name != NULL) {
    bname = name;
  }
  
  /* Tests the results */
  if ((bname == NULL) || (strlen(bname) == 0)) {
    if (name == NULL) {
      *base_list = get_base_list(bhost, bport);

      return CONNECT_LIST;
    } else {
      return CONNECT_ERROR;
    }
  } else {
    PGconn *pg_conn = NULL;
    char buf[256];
    
    status_push("Connecting to database...");
    
    /* Opens the connection to the database */
    pg_conn = PQsetdbLogin(bhost, bport, NULL, NULL, bname, blogin, bpass);
    if (PQstatus(pg_conn) == CONNECTION_BAD) {
      /* Bad connection */
      char *errmsg = PQerrorMessage(pg_conn);

      sprintf(buf, "Database connection error : \n %s ", errmsg);
    
      cb_data->error_dialog("Connection Error", buf);
      
      PQfinish(pg_conn);

      return CONNECT_ERROR;
    } else {
      DBConnection_PG *ret_pg;
      int i;

      /* We use calloc => Table def is null */
      ret_pg = (DBConnection_PG *) calloc(1, sizeof(DBConnection_PG));
      /* The connection (private) */
      ret_pg->conn = pg_conn;

      /* The functions (public) */
      ret_pg->public.DBdisconnect = DBdisconnect;
      ret_pg->public.DBget_error_message = DBget_error_message;
      ret_pg->public.DBget_db_name = DBget_db_name;
      ret_pg->public.DBexecute_query = DBexecute_query;
      ret_pg->public.DBget_query_status = DBget_query_status;
      ret_pg->public.DBget_record_number = DBget_record_number;
      ret_pg->public.DBget_field_number = DBget_field_number;
      ret_pg->public.DBget_field_name = DBget_field_name;
      ret_pg->public.DBget_field_value = DBget_field_value;
      ret_pg->public.DBclear_query = DBclear_query;
      
      /* The keywords (public) */
      ret_pg->public.keylist = NULL;
      i = 0;
      while (SQLkeywords[i] != NULL)
	AddKeyword((DBConnection *) ret_pg, SQLkeywords[i++], KEYWORD_SQL);

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

      return CONNECT_OK;
    }
    
    status_pop();
  }
  
  return CONNECT_ERROR;
}

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_PostgreSQL(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;
  ret->connect_named = NULL;
  
  /* 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("Base Name : ", "", table, 0, 0,
			  ret_param, ok_callback);
  basehost = AddEntryLine("Base Host : ", "", table, 1, 0,
			  ret_param, ok_callback);
  baseport = AddEntryLine("Base Port : ", "", table, 2, 0,
			  ret_param, ok_callback);
  baselogin = AddEntryLine("Base Login : ", "", table, 3, 0,
			   ret_param, ok_callback);
  basepass = AddEntryLine("Base Password : ", "", table, 4, 1,
			  ret_param, ok_callback);
  gtk_widget_show(table);
  *notebook_pane = table;

  /* The label */
  *notebook_label = gtk_label_new("PostgreSQL");
  
  /* 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;
}
