/* 
 * tkTableEdit.c --
 *
 *	This module implements editing functions of a table widget.
 *
 * Copyright (c) 1998-2000 Jeffrey Hobbs
 *
 * See the file "license.terms" for information on usage and redistribution
 * of this file, and for a DISCLAIMER OF ALL WARRANTIES.
 *
 * RCS: @(#) $Id: tkTableEdit.c,v 1.1.1.1 2005/03/01 06:53:04 chenyu Exp $
 */

#include "tkTable.h"

static void	TableModifyRC _ANSI_ARGS_((register Table *tablePtr,
			int doRows, int movetag,
			Tcl_HashTable *tagTblPtr, Tcl_HashTable *dimTblPtr,
			int offset, int from, int to, int lo, int hi,
			int outOfBounds));

/* insert/delete subcommands */
static char *modCmdNames[] = {
    "active", "cols", "rows", (char *)NULL
};
enum modCmd {
    MOD_ACTIVE, MOD_COLS, MOD_ROWS
};

/* insert/delete row/col switches */
static char *rcCmdNames[] = {
    "-keeptitles",	"-holddimensions",	"-holdselection",
    "-holdtags",	"-holdwindows",	"--",
    (char *) NULL
};
enum rcCmd {
    OPT_TITLES,	OPT_DIMS,	OPT_SEL,
    OPT_TAGS,	OPT_WINS,	OPT_LAST
};

#define HOLD_TITLES	1<<0
#define HOLD_DIMS	1<<1
#define HOLD_TAGS	1<<2
#define HOLD_WINS	1<<3
#define HOLD_SEL	1<<4


/*
 *--------------------------------------------------------------
 *
 * Table_EditCmd --
 *	This procedure is invoked to process the insert/delete method
 *	that corresponds to a table widget managed by this module.
 *	See the user documentation for details on what it does.
 *
 * Results:
 *	A standard Tcl result.
 *
 * Side effects:
 *	See the user documentation.
 *
 *--------------------------------------------------------------
 */
int
Table_EditCmd(ClientData clientData, register Tcl_Interp *interp,
	      int objc, Tcl_Obj *CONST objv[])
{
    register Table *tablePtr = (Table *) clientData;
    int doInsert, cmdIndex, first, last;

    if (objc < 4) {
	Tcl_WrongNumArgs(interp, 2, objv,
			 "option ?switches? arg ?arg?");
	return TCL_ERROR;
    }
    if (Tcl_GetIndexFromObj(interp, objv[2], modCmdNames,
			    "option", 0, &cmdIndex) != TCL_OK) {
	return TCL_ERROR;
    }

    doInsert = (*(Tcl_GetString(objv[1])) == 'i');
    switch ((enum modCmd) cmdIndex) {
    case MOD_ACTIVE:
	if (doInsert) {
	    /* INSERT */
	    if (objc != 5) {
		Tcl_WrongNumArgs(interp, 3, objv, "index string");
		return TCL_ERROR;
	    }
	    if (TableGetIcursorObj(tablePtr, objv[3], &first) != TCL_OK) {
		return TCL_ERROR;
	    } else if ((tablePtr->flags & HAS_ACTIVE) &&
		       !(tablePtr->flags & ACTIVE_DISABLED) &&
		       tablePtr->state == STATE_NORMAL) {
		TableInsertChars(tablePtr, first, Tcl_GetString(objv[4]));
	    }
	} else {
	    /* DELETE */
	    if (objc > 5) {
		Tcl_WrongNumArgs(interp, 3, objv, "first ?last?");
		return TCL_ERROR;
	    }
	    if (TableGetIcursorObj(tablePtr, objv[3], &first) != TCL_OK) {
		return TCL_ERROR;
	    }
	    if (objc == 4) {
		last = first+1;
	    } else if (TableGetIcursorObj(tablePtr, objv[4],
					  &last) != TCL_OK) {
		return TCL_ERROR;
	    }
	    if ((last >= first) && (tablePtr->flags & HAS_ACTIVE) &&
		!(tablePtr->flags & ACTIVE_DISABLED) &&
		tablePtr->state == STATE_NORMAL) {
		TableDeleteChars(tablePtr, first, last-first);
	    }
	}
	break;	/* EDIT ACTIVE */

    case MOD_COLS:
    case MOD_ROWS: {
	/*
	 * ROW/COL INSERTION/DELETION
	 * FIX: This doesn't handle spans
	 */
	int i, lo, hi, argsLeft, offset, minkeyoff, doRows;
	int maxrow, maxcol, maxkey, minkey, flags, count, *dimPtr;
	Tcl_HashTable *tagTblPtr, *dimTblPtr;
	Tcl_HashSearch search;

	doRows	= (cmdIndex == MOD_ROWS);
	flags	= 0;
	for (i = 3; i < objc; i++) {
	    if (*(Tcl_GetString(objv[i])) != '-') {
		break;
	    }
	    if (Tcl_GetIndexFromObj(interp, objv[i], rcCmdNames,
				    "switch", 0, &cmdIndex) != TCL_OK) {
		return TCL_ERROR;
	    }
	    if (cmdIndex == OPT_LAST) {
		i++;
		break;
	    }
	    switch (cmdIndex) {
	    case OPT_TITLES:
		flags |= HOLD_TITLES;
		break;
	    case OPT_DIMS:
		flags |= HOLD_DIMS;
		break;
 	    case OPT_SEL:
 		flags |= HOLD_SEL;
 		break;
	    case OPT_TAGS:
		flags |= HOLD_TAGS;
		break;
	    case OPT_WINS:
		flags |= HOLD_WINS;
		break;
	    }
	}
	argsLeft = objc - i;
	if (argsLeft < 1 || argsLeft > 2) {
	    Tcl_WrongNumArgs(interp, 3, objv, "?switches? index ?count?");
	    return TCL_ERROR;
	}

	count	= 1;
	maxcol	= tablePtr->cols-1+tablePtr->colOffset;
	maxrow	= tablePtr->rows-1+tablePtr->rowOffset;
	if (strcmp(Tcl_GetString(objv[i]), "end") == 0) {
	    /* allow "end" to be specified as an index */
	    first = (doRows) ? maxrow : maxcol;
	} else if (Tcl_GetIntFromObj(interp, objv[i], &first) != TCL_OK) {
	    return TCL_ERROR;
	}
	if (argsLeft == 2 &&
	    Tcl_GetIntFromObj(interp, objv[++i], &count) != TCL_OK) {
	    return TCL_ERROR;
	}
	if (count == 0 || (tablePtr->state == STATE_DISABLED)) {
	    return TCL_OK;
	}

	if (doRows) {
	    maxkey	= maxrow;
	    minkey	= tablePtr->rowOffset;
	    minkeyoff	= tablePtr->rowOffset+tablePtr->titleRows;
	    offset	= tablePtr->rowOffset;
	    tagTblPtr	= tablePtr->rowStyles;
	    dimTblPtr	= tablePtr->rowHeights;
	    dimPtr	= &(tablePtr->rows);
	    lo		= tablePtr->colOffset
		+ ((flags & HOLD_TITLES) ? tablePtr->titleCols : 0);
	    hi		= maxcol;
	} else {
	    maxkey	= maxcol;
	    minkey	= tablePtr->colOffset;
	    minkeyoff	= tablePtr->colOffset+tablePtr->titleCols;
	    offset	= tablePtr->colOffset;
	    tagTblPtr	= tablePtr->colStyles;
	    dimTblPtr	= tablePtr->colWidths;
	    dimPtr	= &(tablePtr->cols);
	    lo		= tablePtr->rowOffset
		+ ((flags & HOLD_TITLES) ? tablePtr->titleRows : 0);
	    hi		= maxrow;
	}

	/* constrain the starting index */
	if (first > maxkey) {
	    first = maxkey;
	} else if (first < minkey) {
	    first = minkey;
	}
	if (doInsert) {
	    /* +count means insert after index,
	     * -count means insert before index */
	    if (count < 0) {
		count = -count;
	    } else {
		first++;
	    }
	    if ((flags & HOLD_TITLES) && (first < minkeyoff)) {
		count -= minkeyoff-first;
		if (count <= 0) {
		    return TCL_OK;
		}
		first = minkeyoff;
	    }
	    if (!(flags & HOLD_DIMS)) {
		maxkey += count;
		*dimPtr += count;
	    }
	    for (i = maxkey; i >= first; i--) {
		/* move row/col style && width/height here */
		TableModifyRC(tablePtr, doRows, flags, tagTblPtr, dimTblPtr,
			offset, i, i-count, lo, hi, ((i-count) < first));
	    }
	} else {
	    /* (index = i && count = 1) == (index = i && count = -1) */
	    if (count < 0) {
		/* if the count is negative, make sure that the col count will
		 * delete no greater than the original index */
		if (first+count < minkey) {
		    if (first-minkey < abs(count)) {
			/*
			 * In this case, the user is asking to delete more rows
			 * than exist before the minkey, so we have to shrink
			 * the count down to the existing rows up to index.
			 */
			count = first-minkey;
		    } else {
			count += first-minkey;
		    }
		    first = minkey;
		} else {
		    first += count;
		    count = -count;
		}
	    }
	    if ((flags & HOLD_TITLES) && (first <= minkeyoff)) {
		count -= minkeyoff-first;
		if (count <= 0) {
		    return TCL_OK;
		}
		first = minkeyoff;
	    }
	    if (count > maxkey-first+1) {
		count = maxkey-first+1;
	    }
	    if (!(flags & HOLD_DIMS)) {
		*dimPtr -= count;
	    }
	    for (i = first; i <= maxkey; i++) {
		TableModifyRC(tablePtr, doRows, flags, tagTblPtr, dimTblPtr,
			offset, i, i+count, lo, hi, ((i+count) > maxkey));
	    }
	}
	if (!(flags & HOLD_SEL) &&
		Tcl_FirstHashEntry(tablePtr->selCells, &search) != NULL) {
	    /* clear selection - forceful, but effective */
	    Tcl_DeleteHashTable(tablePtr->selCells);
	    Tcl_InitHashTable(tablePtr->selCells, TCL_STRING_KEYS);
	}

	/*
	 * Make sure that the modified dimension is actually legal
	 * after removing all that stuff.
	 */
	*dimPtr = MAX(1, *dimPtr);

	TableAdjustParams(tablePtr);
	/* change the geometry */
	TableGeometryRequest(tablePtr);
	/* FIX:
	 * This has to handle when the previous rows/cols resize because
	 * of the *stretchmode.  InvalidateAll does that, but could be
	 * more efficient.
	 */
	TableInvalidateAll(tablePtr, 0);
	break;
    }

    }
    return TCL_OK;
}

/*
 *----------------------------------------------------------------------
 *
 * TableDeleteChars --
 *	Remove one or more characters from an table widget.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Memory gets freed, the table gets modified and (eventually)
 *	redisplayed.
 *
 *----------------------------------------------------------------------
 */
void
TableDeleteChars(tablePtr, index, count)
    register Table *tablePtr;	/* Table widget to modify. */
    int index;			/* Index of first character to delete. */
    int count;			/* How many characters to delete. */
{
#ifdef TCL_UTF_MAX
    int byteIndex, byteCount, newByteCount, numBytes, numChars;
    char *new, *string;

    string = tablePtr->activeBuf;
    numBytes = strlen(string);
    numChars = Tcl_NumUtfChars(string, numBytes);
    if ((index + count) > numChars) {
	count = numChars - index;
    }
    if (count <= 0) {
	return;
    }

    byteIndex = Tcl_UtfAtIndex(string, index) - string;
    byteCount = Tcl_UtfAtIndex(string + byteIndex, count)
	- (string + byteIndex);

    newByteCount = numBytes + 1 - byteCount;
    new = (char *) ckalloc((unsigned) newByteCount);
    memcpy(new, string, (size_t) byteIndex);
    strcpy(new + byteIndex, string + byteIndex + byteCount);
#else
    int oldlen;
    char *new;

    /* this gets the length of the string, as well as ensuring that
     * the cursor isn't beyond the end char */
    TableGetIcursor(tablePtr, "end", &oldlen);

    if ((index+count) > oldlen)
	count = oldlen-index;
    if (count <= 0)
	return;

    new = (char *) ckalloc((unsigned)(oldlen-count+1));
    strncpy(new, tablePtr->activeBuf, (size_t) index);
    strcpy(new+index, tablePtr->activeBuf+index+count);
    /* make sure this string is null terminated */
    new[oldlen-count] = '\0';
#endif
    /* This prevents deletes on BREAK or validation error. */
    if (tablePtr->validate &&
	TableValidateChange(tablePtr, tablePtr->activeRow+tablePtr->rowOffset,
			    tablePtr->activeCol+tablePtr->colOffset,
			    tablePtr->activeBuf, new, index) != TCL_OK) {
	ckfree(new);
	return;
    }

    ckfree(tablePtr->activeBuf);
    tablePtr->activeBuf = new;

    /* mark the text as changed */
    tablePtr->flags |= TEXT_CHANGED;

    if (tablePtr->icursor >= index) {
	if (tablePtr->icursor >= (index+count)) {
	    tablePtr->icursor -= count;
	} else {
	    tablePtr->icursor = index;
	}
    }

    TableSetActiveIndex(tablePtr);

    TableRefresh(tablePtr, tablePtr->activeRow, tablePtr->activeCol, CELL);
}

/*
 *----------------------------------------------------------------------
 *
 * TableInsertChars --
 *	Add new characters to the active cell of a table widget.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	New information gets added to tablePtr; it will be redisplayed
 *	soon, but not necessarily immediately.
 *
 *----------------------------------------------------------------------
 */
void
TableInsertChars(tablePtr, index, value)
    register Table *tablePtr;	/* Table that is to get the new elements. */
    int index;			/* Add the new elements before this element. */
    char *value;		/* New characters to add (NULL-terminated
				 * string). */
{
#ifdef TCL_UTF_MAX
    int oldlen, byteIndex, byteCount;
    char *new, *string;

    byteCount = strlen(value);
    if (byteCount == 0) {
	return;
    }

    /* Is this an autoclear and this is the first update */
    /* Note that this clears without validating */
    if (tablePtr->autoClear && !(tablePtr->flags & TEXT_CHANGED)) {
	/* set the buffer to be empty */
	tablePtr->activeBuf = (char *)ckrealloc(tablePtr->activeBuf, 1);
	tablePtr->activeBuf[0] = '\0';
	/* the insert position now has to be 0 */
	index = 0;
	tablePtr->icursor = 0;
    }

    string = tablePtr->activeBuf;
    byteIndex = Tcl_UtfAtIndex(string, index) - string;

    oldlen = strlen(string);
    new = (char *) ckalloc((unsigned)(oldlen + byteCount + 1));
    memcpy(new, string, (size_t) byteIndex);
    strcpy(new + byteIndex, value);
    strcpy(new + byteIndex + byteCount, string + byteIndex);

    /* validate potential new active buffer */
    /* This prevents inserts on either BREAK or validation error. */
    if (tablePtr->validate &&
	TableValidateChange(tablePtr, tablePtr->activeRow+tablePtr->rowOffset,
			    tablePtr->activeCol+tablePtr->colOffset,
			    tablePtr->activeBuf, new, byteIndex) != TCL_OK) {
	ckfree(new);
	return;
    }

    /*
     * The following construction is used because inserting improperly
     * formed UTF-8 sequences between other improperly formed UTF-8
     * sequences could result in actually forming valid UTF-8 sequences;
     * the number of characters added may not be Tcl_NumUtfChars(string, -1),
     * because of context.  The actual number of characters added is how
     * many characters were are in the string now minus the number that
     * used to be there.
     */

    if (tablePtr->icursor >= index) {
	tablePtr->icursor += Tcl_NumUtfChars(new, oldlen+byteCount)
	    - Tcl_NumUtfChars(tablePtr->activeBuf, oldlen);
    }

    ckfree(string);
    tablePtr->activeBuf = new;

#else
    int oldlen, newlen;
    char *new;

    newlen = strlen(value);
    if (newlen == 0) return;

    /* Is this an autoclear and this is the first update */
    /* Note that this clears without validating */
    if (tablePtr->autoClear && !(tablePtr->flags & TEXT_CHANGED)) {
	/* set the buffer to be empty */
	tablePtr->activeBuf = (char *)ckrealloc(tablePtr->activeBuf, 1);
	tablePtr->activeBuf[0] = '\0';
	/* the insert position now has to be 0 */
	index = 0;
    }
    oldlen = strlen(tablePtr->activeBuf);
    /* get the buffer to at least the right length */
    new = (char *) ckalloc((unsigned)(oldlen+newlen+1));
    strncpy(new, tablePtr->activeBuf, (size_t) index);
    strcpy(new+index, value);
    strcpy(new+index+newlen, (tablePtr->activeBuf)+index);
    /* make sure this string is null terminated */
    new[oldlen+newlen] = '\0';

    /* validate potential new active buffer */
    /* This prevents inserts on either BREAK or validation error. */
    if (tablePtr->validate &&
	TableValidateChange(tablePtr, tablePtr->activeRow+tablePtr->rowOffset,
			    tablePtr->activeCol+tablePtr->colOffset,
			    tablePtr->activeBuf, new, index) != TCL_OK) {
	ckfree(new);
	return;
    }
    ckfree(tablePtr->activeBuf);
    tablePtr->activeBuf = new;

    if (tablePtr->icursor >= index) {
	tablePtr->icursor += newlen;
    }
#endif

    /* mark the text as changed */
    tablePtr->flags |= TEXT_CHANGED;

    TableSetActiveIndex(tablePtr);

    TableRefresh(tablePtr, tablePtr->activeRow, tablePtr->activeCol, CELL);
}

/*
 *----------------------------------------------------------------------
 *
 * TableModifyRC --
 *	Helper function that does the core work of moving rows/cols
 *	and associated tags.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Moves cell data and possibly tag data
 *
 *----------------------------------------------------------------------
 */
static void
TableModifyRC(tablePtr, doRows, flags, tagTblPtr, dimTblPtr,
	      offset, from, to, lo, hi, outOfBounds)
    Table *tablePtr;	/* Information about text widget. */
    int doRows;		/* rows (1) or cols (0) */
    int flags;		/* flags indicating what to move */
    Tcl_HashTable *tagTblPtr, *dimTblPtr; /* Pointers to the row/col tags
					   * and width/height tags */
    int offset;		/* appropriate offset */
    int from, to;	/* the from and to row/col */
    int lo, hi;		/* the lo and hi col/row */
    int outOfBounds;	/* the boundary check for shifting items */
{
    int j, new;
    char buf[INDEX_BUFSIZE], buf1[INDEX_BUFSIZE];
    Tcl_HashEntry *entryPtr, *newPtr;
    TableEmbWindow *ewPtr;

    /*
     * move row/col style && width/height here
     * If -holdtags is specified, we don't move the user-set widths/heights
     * of the absolute rows/columns, otherwise we enter here to move the
     * dimensions appropriately
     */
    if (!(flags & HOLD_TAGS)) {
	entryPtr = Tcl_FindHashEntry(tagTblPtr, (char *)from);
	if (entryPtr != NULL) {
	    Tcl_DeleteHashEntry(entryPtr);
	}
	entryPtr = Tcl_FindHashEntry(dimTblPtr, (char *)from-offset);
	if (entryPtr != NULL) {
	    Tcl_DeleteHashEntry(entryPtr);
	}
	if (!outOfBounds) {
	    entryPtr = Tcl_FindHashEntry(tagTblPtr, (char *)to);
	    if (entryPtr != NULL) {
		newPtr = Tcl_CreateHashEntry(tagTblPtr, (char *)from, &new);
		Tcl_SetHashValue(newPtr, Tcl_GetHashValue(entryPtr));
		Tcl_DeleteHashEntry(entryPtr);
	    }
	    entryPtr = Tcl_FindHashEntry(dimTblPtr, (char *)to-offset);
	    if (entryPtr != NULL) {
		newPtr = Tcl_CreateHashEntry(dimTblPtr, (char *)from-offset,
			&new);
		Tcl_SetHashValue(newPtr, Tcl_GetHashValue(entryPtr));
		Tcl_DeleteHashEntry(entryPtr);
	    }
	}
    }
    for (j = lo; j <= hi; j++) {
	if (doRows /* rows */) {
	    TableMakeArrayIndex(from, j, buf);
	    TableMakeArrayIndex(to, j, buf1);
	    TableMoveCellValue(tablePtr, to, j, buf1, from, j, buf,
		    outOfBounds);
	} else {
	    TableMakeArrayIndex(j, from, buf);
	    TableMakeArrayIndex(j, to, buf1);
	    TableMoveCellValue(tablePtr, j, to, buf1, j, from, buf,
		    outOfBounds);
	}
	/*
	 * If -holdselection is specified, we leave the selected cells in the
	 * absolute cell values, otherwise we enter here to move the
	 * selection appropriately
	 */
	if (!(flags & HOLD_SEL)) {
	    entryPtr = Tcl_FindHashEntry(tablePtr->selCells, buf);
	    if (entryPtr != NULL) {
		Tcl_DeleteHashEntry(entryPtr);
	    }
	    if (!outOfBounds) {
		entryPtr = Tcl_FindHashEntry(tablePtr->selCells, buf1);
		if (entryPtr != NULL) {
		    Tcl_CreateHashEntry(tablePtr->selCells, buf, &new);
		    Tcl_DeleteHashEntry(entryPtr);
		}
	    }
	}
	/*
	 * If -holdtags is specified, we leave the tags in the
	 * absolute cell values, otherwise we enter here to move the
	 * tags appropriately
	 */
	if (!(flags & HOLD_TAGS)) {
	    entryPtr = Tcl_FindHashEntry(tablePtr->cellStyles, buf);
	    if (entryPtr != NULL) {
		Tcl_DeleteHashEntry(entryPtr);
	    }
	    if (!outOfBounds) {
		entryPtr = Tcl_FindHashEntry(tablePtr->cellStyles, buf1);
		if (entryPtr != NULL) {
		    newPtr = Tcl_CreateHashEntry(tablePtr->cellStyles, buf,
			    &new);
		    Tcl_SetHashValue(newPtr, Tcl_GetHashValue(entryPtr));
		    Tcl_DeleteHashEntry(entryPtr);
		}
	    }
	}
	/*
	 * If -holdwindows is specified, we leave the windows in the
	 * absolute cell values, otherwise we enter here to move the
	 * windows appropriately
	 */
	if (!(flags & HOLD_WINS)) {
	    /*
	     * Delete whatever window might be in our destination
	     */
	    Table_WinDelete(tablePtr, buf);
	    if (!outOfBounds) {
		/*
		 * buf1 is where the window is
		 * buf is where we want it to be
		 *
		 * This is an adaptation of Table_WinMove, which we can't
		 * use because we are intermediately fiddling with boundaries
		 */
		entryPtr = Tcl_FindHashEntry(tablePtr->winTable, buf1);
		if (entryPtr != NULL) {
		    /*
		     * If there was a window in our source,
		     * get the window pointer to move it
		     */
		    ewPtr = (TableEmbWindow *) Tcl_GetHashValue(entryPtr);
		    /* and free the old hash table entry */
		    Tcl_DeleteHashEntry(entryPtr);

		    entryPtr = Tcl_CreateHashEntry(tablePtr->winTable, buf,
			    &new);
		    /*
		     * We needn't check if a window was in buf, since the
		     * Table_WinDelete above should guarantee that no window
		     * is there.  Just set the new entry's value.
		     */
		    Tcl_SetHashValue(entryPtr, (ClientData) ewPtr);
		    ewPtr->hPtr = entryPtr;
		}
	    }
	}
    }
}
