
/****************************************************************************
 *
 * MODULE:       v.reclass
 * AUTHOR(S):    R.L. Glenn, USDA, SCS, NHQ-CGIS (original contributor)
 *               GRASS 6 update: Radim Blazek <radim.blazek gmail.com>
 *               Glynn Clements <glynn gclements.plus.com>,
 *               Jachym Cepicky <jachym les-ejk.cz>, Markus Neteler <neteler itc.it>
 *               OGR support by Martin Landa <landa.martin gmail.com>
 * PURPOSE:      
 * COPYRIGHT:    (C) 1999-2009 by the GRASS Development Team
 *
 *               This program is free software under the GNU General
 *               Public License (>=v2). Read the file COPYING that
 *               comes with GRASS for details.
 *
 *****************************************************************************/

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

#include <grass/gis.h>
#include <grass/vector.h>
#include <grass/dbmi.h>
#include <grass/glocale.h>

#define KEY(x) (G_strcasecmp(key,x)==0)

int inpt(FILE * rulefd, char *buf);
int key_data(char *buf, char **k, char **d);
int reclass(struct Map_info *In, struct Map_info *Out, int type,
	    int field, dbCatValArray * cvarr, int optiond);

static int cmpcat(const void *pa, const void *pb)
{
    dbCatVal *p1 = (dbCatVal *) pa;
    dbCatVal *p2 = (dbCatVal *) pb;

    if (p1->cat < p2->cat)
	return -1;
    if (p1->cat > p2->cat)
	return 1;
    return 0;
}

int main(int argc, char *argv[])
{
    struct GModule *module;

    /*    struct Flag *d_flag; */
    struct Option *in_opt, *out_opt, *type_opt, *field_opt, *rules_opt,
	*col_opt;
    char *key, *data, buf[1024];
    int rclelem, type, field;
    struct Map_info In, Out;

    struct field_info *Fi;
    dbDriver *Driver;
    dbCatValArray cvarr;

    int i, n, ttype;

    G_gisinit(argv[0]);

    module = G_define_module();
    G_add_keyword(_("vector"));
    G_add_keyword(_("reclassification"));
    G_add_keyword(_("attributes"));
    module->description =
	_("Changes vector category values for an existing vector map "
	  "according to results of SQL queries or a value in attribute table column.");

    in_opt = G_define_standard_option(G_OPT_V_INPUT);

    field_opt = G_define_standard_option(G_OPT_V_FIELD);
    field_opt->guisection = _("Selection");

    type_opt = G_define_standard_option(G_OPT_V_TYPE);
    type_opt->options = "point,line,boundary,centroid";
    type_opt->answer = "point,line,boundary,centroid";
    type_opt->guisection = _("Selection");

    out_opt = G_define_standard_option(G_OPT_V_OUTPUT);
    
    col_opt = G_define_standard_option(G_OPT_DB_COLUMN);
    col_opt->label =
	_("The name of the column whose values are to be used as new categories");
    col_opt->description = _("The source for the new key column must be type integer or string");
    
    rules_opt = G_define_standard_option(G_OPT_F_INPUT);
    rules_opt->key = "rules";
    rules_opt->required = NO;
    rules_opt->description = _("Full path to the reclass rule file");

    if (G_parser(argc, argv))
	exit(EXIT_FAILURE);


    type = Vect_option_to_types(type_opt);

    if ((!(rules_opt->answer) && !(col_opt->answer)) ||
	(rules_opt->answer && col_opt->answer)) {
	G_fatal_error(_("Either '%s' or '%s' must be specified"),
		      rules_opt->key, col_opt->key);
    }

    Vect_check_input_output_name(in_opt->answer, out_opt->answer,
				 G_FATAL_EXIT);

    Vect_set_open_level(2);
    if (Vect_open_old2(&In, in_opt->answer, "", field_opt->answer) < 0)
	G_fatal_error(_("Unable to open vector map <%s>"), in_opt->answer);
    field = Vect_get_field_number(&In, field_opt->answer);

    if (Vect_open_new(&Out, out_opt->answer, Vect_is_3d(&In)) < 0)
	G_fatal_error(_("Unable to create vector map <%s>"), out_opt->answer);

    Vect_copy_head_data(&In, &Out);
    Vect_hist_copy(&In, &Out);
    Vect_hist_command(&Out);

    /* Read column values from database */
    db_CatValArray_init(&cvarr);

    Fi = Vect_get_field(&In, field);
    if (Fi == NULL)
      G_fatal_error(_("Database connection not defined for layer %s"),
		    field_opt->answer);

    Driver = db_start_driver_open_database(Fi->driver, Fi->database);
    if (Driver == NULL)
	G_fatal_error(_("Unable to open database <%s> by driver <%s>"),
		      Fi->database, Fi->driver);
    db_set_error_handler_driver(Driver);

    if (col_opt->answer) {
	int ctype;
	int nrec;

	/* Check column type */
	ctype = db_column_Ctype(Driver, Fi->table, col_opt->answer);

	if (ctype == -1) {
	    G_fatal_error(_("Column <%s> not found in table <%s>"),
			  col_opt->answer, Fi->table);
	}
	else if (ctype == DB_C_TYPE_INT) {
	    nrec =
		db_select_CatValArray(Driver, Fi->table, Fi->key,
				      col_opt->answer, NULL, &cvarr);
	    G_debug(3, "nrec = %d", nrec);
	}
	else if (ctype == DB_C_TYPE_STRING) {
	    int i, more, nrows, ncols, newval, len;
	    dbString stmt, stmt2;
	    dbString lastval;
	    dbCursor cursor;
	    dbColumn *column;
	    dbValue *value;
	    dbTable *table;
	    struct field_info *NewFi;
	    dbDriver *Driver2;
	    int foundnull;

	    db_init_string(&stmt);
	    db_init_string(&stmt2);
	    db_init_string(&lastval);

	    NewFi = Vect_default_field_info(&Out, field, NULL, GV_1TABLE);
	    Vect_map_add_dblink(&Out, field, NULL, NewFi->table, GV_KEY_COLUMN,
				NewFi->database, NewFi->driver);


	    /* Open output driver and database */
	    if (strcmp(Fi->driver, NewFi->driver) == 0
		&& strcmp(Fi->database,
		          Vect_subst_var(NewFi->database, &Out)) == 0) {
		G_debug(3, "Use the same driver");
		Driver2 = Driver;
	    }
	    else {
		Driver2 = db_start_driver_open_database(NewFi->driver,
							Vect_subst_var(NewFi->
								       database,
								       &Out));
		db_set_error_handler_driver(Driver2);
	    }

	    /* get string column length */
	    db_set_string(&stmt, Fi->table);
	    if (db_describe_table(Driver, &stmt, &table) != DB_OK) {
		G_fatal_error(_("Unable to describe table <%s>"), Fi->table);
	    }

	    column = NULL;

	    ncols = db_get_table_number_of_columns(table);
	    G_debug(3, "ncol = %d", ncols);

	    len = 0;
	    for (i = 0; i < ncols; i++) {
		column = db_get_table_column(table, i);
		if (G_strcasecmp(db_get_column_name(column), col_opt->answer) == 0) {
		    /* String column length */
		    len = db_get_column_length(column);
		    break;
		}
	    }
	    db_free_table(table);

	    /* Create table */
	    sprintf(buf, "create table %s (cat integer, %s varchar(%d))",
		    NewFi->table, col_opt->answer, len);

	    db_set_string(&stmt2, buf);

	    if (db_execute_immediate(Driver2, &stmt2) != DB_OK) {
		Vect_close(&Out);
		G_fatal_error("Unable to create table: '%s'",
			      db_get_string(&stmt2));
	    }
	    db_begin_transaction(Driver2);

	    /* select values */
	    sprintf(buf, "SELECT %s, %s FROM %s ORDER BY %s", Fi->key,
		    col_opt->answer, Fi->table, col_opt->answer);
	    db_set_string(&stmt, buf);

	    G_debug(3, "  SQL: %s", db_get_string(&stmt));

	    if (db_open_select_cursor(Driver, &stmt, &cursor, DB_SEQUENTIAL)
		!= DB_OK)
		G_fatal_error("Unable to open select cursor: '%s'",
			      db_get_string(&stmt));

	    nrows = db_get_num_rows(&cursor);

	    G_debug(3, "  %d rows selected", nrows);
	    if (nrows <= 0)
		G_fatal_error(_("No records selected from table <%s>"),
			      Fi->table);

	    db_CatValArray_alloc(&cvarr, nrows);

	    table = db_get_cursor_table(&cursor);

	    /* Check if key column is integer */
	    column = db_get_table_column(table, 0);
	    ctype = db_sqltype_to_Ctype(db_get_column_sqltype(column));
	    G_debug(3, "  key type = %d", type);

	    if (ctype != DB_C_TYPE_INT) {
		G_fatal_error(_("Key column type is not integer"));	/* shouldnot happen */
	    }

	    cvarr.ctype = DB_C_TYPE_INT;

	    newval = 0;
	    foundnull = 0;

	    /* fetch the data */
	    for (i = 0; i < nrows; i++) {
		int isnull;

		if (db_fetch(&cursor, DB_NEXT, &more) != DB_OK) {
		    G_fatal_error(_("Unable to fetch data from table <%s>"),
				  Fi->table);
		}

		column = db_get_table_column(table, 1);
		value = db_get_column_value(column);
		isnull = db_test_value_isnull(value);

		if (i == 0 || (!foundnull && isnull) ||
		    (!isnull && strcmp(db_get_value_string(value),
				       db_get_string(&lastval)) != 0)) {

		    if (!foundnull && isnull) {
		        foundnull = 1;
			db_set_value_string(value, "");
		    }

		    newval++;
		    db_set_string(&lastval, db_get_value_string(value));
		    G_debug(3, "  newval = %d string = %s", newval,
			    db_get_value_string(value));

		    db_set_string(&stmt2, db_get_value_string(value));
		    db_double_quote_string(&stmt2);
		    sprintf(buf, "insert into %s values (%d, '%s')",
			    NewFi->table, newval, db_get_string(&stmt2));

		    db_set_string(&stmt2, buf);

		    if (db_execute_immediate(Driver2, &stmt2) != DB_OK) {
			Vect_close(&Out);
			G_fatal_error(_("Cannot insert data: [%s]"),
				      db_get_string(&stmt2));
		    }
		}

		column = db_get_table_column(table, 0);	/* first column */
		value = db_get_column_value(column);
		cvarr.value[i].cat = db_get_value_int(value);

		cvarr.value[i].val.i = newval;

		G_debug(4, "  cat = %d newval = %d", cvarr.value[i].cat,
			newval);
	    }

	    cvarr.n_values = nrows;

	    db_close_cursor(&cursor);
	    db_free_string(&stmt);
	    db_free_string(&lastval);

	    db_commit_transaction(Driver2);

	    if (db_create_index2(Driver2, NewFi->table, NewFi->key) != DB_OK)
		G_warning(_("Unable to create index for table <%s>, key <%s>"),
			  NewFi->table, NewFi->key);

	    if (db_grant_on_table(Driver2, NewFi->table, DB_PRIV_SELECT,
				  DB_GROUP | DB_PUBLIC) != DB_OK) {
		G_fatal_error(_("Unable to grant privileges on table <%s>"),
			      NewFi->table);
	    }
	    if (Driver2 != Driver)
		db_close_database_shutdown_driver(Driver2);

	    qsort((void *)cvarr.value, nrows, sizeof(dbCatVal), cmpcat);
	}
	else {
	    G_fatal_error(_("Column type must be integer or string"));
	}

    }
    else {
	int cat;
	char *label, *where;
	FILE *rulefd;

	G_debug(2, "Reading rules");

	if ((rulefd = fopen(rules_opt->answer, "r")) == NULL)
	    G_fatal_error(_("Unable to open rule file <%s>"),
			  rules_opt->answer);

	db_CatValArray_alloc(&cvarr, Vect_get_num_lines(&In));

	cat = 0;
	where = label = NULL;
	while (inpt(rulefd, buf)) {
	    if (!key_data(buf, &key, &data))
		continue;

	    G_strip(data);
	    G_debug(3, "key = %s data = %s", key, data);

	    if (KEY("cat")) {
		if (cat > 0)
		    G_fatal_error(_("Category %d overwritten by '%s'"), cat,
				  data);
		cat = atoi(data);
		if (cat <= 0)
		    G_fatal_error(_("Category '%s' invalid"), data);
	    }
	    else if (KEY("label")) {
		if (label)
		    G_fatal_error(_("Label '%s' overwritten by '%s'"), label,
				  data);
		label = G_store(data);
	    }
	    else if (KEY("where")) {
		if (where)
		    G_fatal_error(_("Condition '%s' overwritten by '%s'"),
				  where, data);
		where = G_store(data);
	    }
	    else {
		G_fatal_error(_("Unknown rule option: '%s'"), key);
	    }

	    if (cat > 0 && where) {
		int i, over, *cats, ncats;
		dbCatVal *catval;

		G_debug(2, "cat = %d, where = '%s'", cat, where);
		if (!label)
		    label = where;

		ncats =
		    db_select_int(Driver, Fi->table, Fi->key, where, &cats);
		if (ncats == -1)
		    G_fatal_error(_("Cannot select values from database"));
		G_debug(3, "  ncats = %d", ncats);


		/* If the category already exists, overwrite it cvarr, set to 0 in cats
		 * and don't add second time */
		over = 0;
		for (i = 0; i < ncats; i++) {
		    if (db_CatValArray_get_value(&cvarr, cats[i], &catval) ==
			DB_OK) {
			catval->val.i = cat;
			cats[i] = 0;
			over++;
		    }
		}
		if (over > 0)
		    G_warning(_("%d previously set categories overwritten by new category %d"),
			      over, cat);

		for (i = 0; i < ncats; i++) {
		    if (cats[i] <= 0)
			continue;

		    if (cvarr.n_values == cvarr.alloc) {
			db_CatValArray_realloc(&cvarr,
					       (int)10 + cvarr.alloc / 3);
		    }
		    G_debug(3, "Add old cat %d", cats[i]);
		    cvarr.value[cvarr.n_values].cat = cats[i];
		    cvarr.value[cvarr.n_values].val.i = cat;
		    cvarr.n_values++;
		}

		db_CatValArray_sort(&cvarr);

		G_free(cats);
		cat = 0;
		where = label = NULL;
	    }
	}

	if (cat > 0 || where)
	    G_fatal_error(_("Incomplete rule"));
    }

    db_close_database_shutdown_driver(Driver);

    /* reclass vector map */
    rclelem = reclass(&In, &Out, type, field, &cvarr, 0);

    /* Copy tables */
    n = Vect_get_num_dblinks(&In);
    for (i = 0; i < Vect_get_num_dblinks(&In); i++) {
	Fi = Vect_get_dblink(&In, i);
	if (Fi->number == field) {
	    n--;
	    break;
	}
    }
    if (n > 1)
	ttype = GV_MTABLE;
    else
	ttype = GV_1TABLE;

    for (i = 0; i < Vect_get_num_dblinks(&In); i++) {
	Fi = Vect_get_dblink(&In, i);
	if (Fi->number == field) {
	    continue;
	}

	Vect_copy_table(&In, &Out, Fi->number, Fi->number, Fi->name, ttype);
    }

    Vect_close(&In);

    Vect_build(&Out);
    Vect_close(&Out);

    G_done_msg(_("%d features reclassed."), rclelem);

    exit(EXIT_SUCCESS);
}
