// -------------------------------------------------------------------------------------
//  DataTable
//  This software is without warranty of any kind.  Use at your own risk.
// -------------------------------------------------------------------------------------

#import <objc/objc.h>
#import <appkit/appkit.h>
#import <mach/mach.h>
#import <dbkit/dbkit.h>
#import <libc.h>
#import <stdio.h>
#import <string.h>
#import <ctype.h>
#import "DataTable.h"

// -------------------------------------------------------------------------------------
// misc
#define	KEY_COLUMN				0

// -------------------------------------------------------------------------------------
// string macros
#define	freeString(S)			{ if (S) { free(S); S = (char*)nil; } }

// -------------------------------------------------------------------------------------
// table macros
#define	dataROWS				tableHandle->dataId
#define	columnINFO				tableHandle->columnId
#define	orderedINFO				tableHandle->reorderId

// -------------------------------------------------------------------------------------
@interface DataTable(private)
- _resetDisplayedColumnOrdering;
@end

// -------------------------------------------------------------------------------------
@implementation DataTable
// -------------------------------------------------------------------------------------

// -------------------------------------------------------------------------------------
// initialize connection with data table 

/* open table */
- initFromFile:(const char*)fileName
{
	char	*p = fileName? rindex((char*)fileName, '/') : (char*)nil;

	/* init super */
	[self init];
	nullIndex = (id)-1;
	delegate = (id)nil;
	isModified = NO;
	
	/* initialize table handle */
	tableHandle = (dataTable_t*)malloc(sizeof(dataTable_t));
	memset(tableHandle, 0, sizeof(dataTable_t));
	tableHandle->name = NXCopyStringBuffer(p? (p+1) : "Unknown"); 
	tableHandle->access = fileName? NXCopyStringBuffer(fileName) : (char*)nil;
	tableHandle->viewSize.width = tableHandle->viewSize.height = 0.0;
	tableHandle->columnId = [[[List alloc] initCount:1] empty];
	tableHandle->reorderId = (id)nil;
	tableHandle->dataId = [[List allocFromZone:[self zone]] init];
	tableHandle->date = [self timestamp];
		
	/* read column-info and data */
	if (![self readTableColumns] || ![self readTableData])
		NXLogError("Unable to load column/data for file %s", (fileName?fileName:"?"));
	
	/* return */
	return self;
	
}

// -------------------------------------------------------------------------------------
// set attributes

/* set delegate */
- setDelegate:aDelegate
{
	delegate = aDelegate;
	return self;
}

/* return current delegate */
- delegate
{
	return delegate;
}

/* null index */
- setNullIndex:index
{
	nullIndex = index;
	return self;
}

/* set number of rows */
- setRows:(u_int)rowCount
{
	return [self notImplemented:_cmd];
}

/* set number of columns */
- setColumns:(u_int)colCount
{
	return [self notImplemented:_cmd];
}

/* set display order for column */
- setDisplayOrder:(int)order andWidth:(float)width forColumnIndex:(int)column
{
	BOOL			didEdit = NO;
	dataColumn_t	*ci = [self columnInfoAt:column];
	if (!ci) return (id)nil;
	if (ci->displayOrder != order) {
		ci->displayOrder = order;
		ci->isHidden = (order >= 0)? NO : YES;
		didEdit = YES;
		if (tableHandle->reorderId) {
			[tableHandle->reorderId free];
			tableHandle->reorderId = (id)nil;
		}
	}
	if ((width >= 0.0) && (ci->size != width)) {
		ci->size = width;
		didEdit = YES;
	}
	if (didEdit) [self setDocEdited:YES];
	return self;
}

// -------------------------------------------------------------------------------------
// freeing resources

/* free columns */
- _freeColumnInfo
{
	int	c;
	for (c = 0; c < [columnINFO count]; c++) {
		dataColumn_t *ci = [self columnInfoAt:c];
		freeString(ci->keyTag);
		freeString(ci->title);
		freeString(ci->nilValue);
		free(ci);
	}
	[columnINFO free];
	columnINFO = (id)nil;
	if (orderedINFO) {
		[orderedINFO free];
		orderedINFO = (id)nil;
	}
	return self;
}

/* free a single row */
- _freeRow:rowId
{
	int	c;
	for (c = 0; c < [rowId count]; c++) {
		dataEntry_t	*de = entryPTR(rowId, c);
		freeString(de->value);
		free(de);
	}
	return self;
}

/* free data */
- _freeData
{
	int	r;
	for (r = 0; r < [dataROWS count]; r++) [self _freeRow:[dataROWS objectAt:r]];
	[dataROWS freeObjects];
	[dataROWS free];
	dataROWS = (id)nil;
	return self;
}

/* free */
- free
{

	/* free table */
	if (tableHandle) {
		freeString(tableHandle->name);
		freeString(tableHandle->access);
		[self _freeData];
		[self _freeColumnInfo];
		free(tableHandle);
	}

	/* free object */
	return [super free];
	
}

// -------------------------------------------------------------------------------------
// View size (handle by superclass, delegate, or other object)

/* set size */
- setViewSize:(NXSize*)size
{
	tableHandle->viewSize = *size;
	return self;
}

/* get size */
- (const NXSize*)viewSize
{
	if ((tableHandle->viewSize.width > 0.0) && (tableHandle->viewSize.height > 0.0))
		return &(tableHandle->viewSize);
	return (NXSize*)nil;
}

// -------------------------------------------------------------------------------------
// saving the table

/* check last modified date */
- (BOOL)tableHasChanged
{
	struct stat		st;
	
	/* stat file */
	if (!tableHandle->access) return NO;
	if (stat((char*)tableHandle->access, &st) < 0) {
		NXLogError("Unable to stat file %s", tableHandle->access);
		return YES;
	}

	/* return file changed status */
	return tableHandle->date == st.st_mtime? NO : YES;
	
}

/* indicate table has been edited */
- setDocEdited:(BOOL)flag
{
	isModified = flag;
	if (delegate && (delegate != self) && [delegate respondsTo:@selector(setDocEdited:)])
		[delegate setDocEdited:flag];
	return self;
}

/* commit/save table */
- commitTable
{

	/* make sure we have a file name */
	if (!tableHandle->access) {
		// load table name */
	}
	
	/* write entire table */
	if ([self writeTable]) [self setDocEdited:NO];
	
	return self;
}

// -------------------------------------------------------------------------------------
// column info

/* return column info pointer */
+ (dataColumn_t*)_column:infoId infoAt:(int)n
{
	if (!infoId || (n < 0) || (n >= [infoId count])) return (dataColumn_t*)nil;
	return (dataColumn_t*)[infoId objectAt:n];
}

/* get column info by index */
- (dataColumn_t*)columnInfoAt:(u_int)col
{
	return [[self class] _column:columnINFO infoAt:col];
}

/* return column number for matching keyTag */
- (int)indexForColumnName:(const char*)name
{
	int	c;
	if (!name || !columnINFO) return -1;
	for (c = 0; c < [columnINFO count]; c++) {
		dataColumn_t *ci = [self columnInfoAt:c];
		if (!strcmp(name, ci->keyTag)) return c;
	}
	return -1;
}

/* reset displayed column ordering */
- _resetDisplayedColumnOrdering
{
	int	c, cnt;

	/* empty reorder table */
	if (!orderedINFO) orderedINFO = [[List alloc] initCount:1];
	[orderedINFO empty];
	
	/* add visible column headers to new list */
	for (c = 0, cnt = [columnINFO count]; c < cnt; c++) {
		dataColumn_t *ci = [self columnInfoAt:c];
		if (!ci || (ci->displayOrder < 0)) continue;
		[orderedINFO addObject:(id)ci];
	}

	/* sort reorder table by display order (since table is small, use simple-sort) */
	for (c = 0, cnt = [orderedINFO count]; c < cnt; c++) {
		int n;
		dataColumn_t *ci = (dataColumn_t*)[orderedINFO objectAt:c];
		for (n = c + 1; n < cnt; n++) {
			dataColumn_t *ni = (dataColumn_t*)[orderedINFO objectAt:n];
			if (ci->displayOrder > ni->displayOrder) {
				[orderedINFO replaceObjectAt:c with:(id)ni];
				[orderedINFO replaceObjectAt:n with:(id)ci];
				ci = ni;
			}
		}
	}
	
	return self;
}

/* get column info by order */
- (dataColumn_t*)orderedColumnInfoAt:(u_int)ord
{
	if (!orderedINFO) [self _resetDisplayedColumnOrdering];
	return [[self class] _column:orderedINFO infoAt:ord];
}

/* add column info */
- addColumnInfo:(dataColumn_t*)dc
{
	if (dc->type == CDT_UNKNOWN) dc->type = CDT_STRING;
	if (!dc->title) dc->title = NXCopyStringBuffer(dc->keyTag);
	if (dc->minSize <= 0.0) dc->minSize = dc->size;
	[columnINFO addObject:(id)dc];
	return self;
}

// -------------------------------------------------------------------------------------
// load text file table data

/* duplicate row */
- newRowName:(const char*)rowN copyFromRow:(int)rowX
{
	int		c, rcnt = [dataROWS count], ccnt = [columnINFO count];
	id		rowId, newRow;
	if ((rowX < 0) || (rowX > rcnt)) rowX = rcnt;
	rowId = rowX < rcnt? [dataROWS objectAt:rowX] : (id)nil;
	newRow = [[[List alloc] initCount:0] empty];
	if (![dataROWS insertObject:newRow at:MIN(rowX + 1, rcnt)]) {
		NXLogError("Unable to add new row %s into table", rowN);
		[newRow free];
		return (id)nil;
	}
	for (c = 0; c < ccnt; c++) {
		dataEntry_t *ne = entryNEW, *oe = rowId? entryPTR(rowId, c) : (dataEntry_t*)nil;
		ne->value = (char*)[self copyStringValue:(c?(oe?oe->value:""):rowN) forColumn:c];
		ne->isValid = -1;
		[newRow insertObject:(id)ne at:c];
	}
	[self setDocEdited:YES];
	return newRow;
}

/* delete row */
- deleteRowAt:(int)rowX
{
	id		rowId;
	if ((rowX < 0) || (rowX >= [dataROWS count])) return (id)nil;
	if (!(rowId = [dataROWS removeObjectAt:rowX])) return (id)nil;
	[self _freeRow:rowId];
	[self setDocEdited:YES];
	return self;
}

/* remove all table entries */
- empty
{
	return self;
}

/* fill/refill table */
- reset
{
	return self;
}

// -------------------------------------------------------------------------------------
// sorting

/* sort comparison */
- (int)sortCompare:(dataColumn_t*)dc values:(const char*)val1:(const char*)val2
{
	if (dc->type == CDT_INTEGER) return atoi(val1) - atoi(val2);
	return strcasecmp(val1, val2);
}

/* sort data table by column */
#define	STRCOMP(C,T,R1,R2)	[self sortCompare:T values:entryVALUE(R1,C):entryVALUE(R2,C)]
//#define	STRCOMP(C,T,R1,R2)	strcasecmp(entryVALUE(R1,C),entryVALUE(R2,C))
- sortTableByColumn:(int)pri :(int)sec
{
	int				r, cnt;
	dataColumn_t	*dcp, *dcs;
	
	/* check sort keys */
	if (pri < 0) {
		NXLogError("Invalid primary sort column %d", pri);
		return (id)nil;
	}
	if (pri == KEY_COLUMN) sec = -1;
	
	/* get column type */
	dcp = [self columnInfoAt:pri];	
	dcs = (sec >= 0)? [self columnInfoAt:sec] : (dataColumn_t*)nil;	
	
	/* sort */
	for (r = 0, cnt = [dataROWS count]; r < cnt; r++) {
		int n;
		id rowR = [dataROWS objectAt:r];
		for (n = r + 1; n < cnt; n++) {
			id rowN = [dataROWS objectAt:n];
			int cmp = STRCOMP(pri,dcp,rowR,rowN);
			if ((cmp > 0) || ((cmp == 0) && dcs && (STRCOMP(sec,dcs,rowR,rowN) > 0))) {
				[dataROWS replaceObjectAt:r with:rowN];
				[dataROWS replaceObjectAt:n with:rowR];
				rowR = rowN;
			}
		}
	}
	
	return self;
}
#undef STRCOMP

/* sort data table by column */
- sortTableByColumnName:(const char*)priName :(const char*)secName
{
	int		pri = [self indexForColumnName:priName];
	int		sec = secName? [self indexForColumnName:secName] : -1;
	
	/* check sort keys */
	if (pri < 0) {
		NXLogError("Invalid primary sort key %s", (priName?priName:"?"));
		return (id)nil;
	}
	if (secName && (sec < 0)) {
		NXLogError("Invalid secondary sort key %s", secName);
		return (id)nil;
	}
	
	/* sort */
	return [self sortTableByColumn:pri:sec];
	
}

// -------------------------------------------------------------------------------------
// validate entries

/* validate specific entry */
- (BOOL)_validateEntry:(dataEntry_t*)de forColumn:(u_int)column
{
	if (!de) return NO;
	if (de->isValid < 0) {
		dataColumn_t *dc = [self columnInfoAt:column];
		if (dc->nilValue&&(!*de->value||!strcmp(de->value,dc->nilValue))) de->isValid = YES;
		else de->isValid = [self verifyValue:de->value dataType:dc->type]? 1 : 0;
	}
	return de->isValid;
}

/* validate entries until an error is found */
- (BOOL)hasVerificationErrors
{
	int		r, rcnt = [dataROWS count];
	for (r = 0; r < rcnt; r++) {
		id rowId = [dataROWS objectAt:r];
		int c, ccnt = [rowId count];
		for (c = 0; c < ccnt; c ++) {
			dataEntry_t *de = entryPTR(rowId,c);
			if (![self _validateEntry:de forColumn:c]) return YES;
		}
	}
	return NO;
}

/* validate entry at specific location */
- (BOOL)verifyValueAt:(u_int)row :(u_int)column
{
	return [self _validateEntry:[self entryAtIndex:row:column] forColumn:column];
}

// -------------------------------------------------------------------------------------
// table access

/* find row for specified name */
- (int)indexForRowName:(const char*)rowN exactMatch:(BOOL)exact
{
	int		r;
	if (!rowN) return -1; // not found
	for (r = 0; r < [dataROWS count]; r++) {
		id rowId = [dataROWS objectAt:r];
		char *rn = (char*)entryVALUE(rowId,0);
		if (exact) { if (!strcmp(rn, rowN)) return r; }
		else       { if (!strstr(rn, rowN)) return r; }
	}
	return -1;
}

/* return specified table entry (by index) */
- (dataEntry_t*)entryAtIndex:(u_int)rowX :(u_int)colX
{
	id	rowId = [dataROWS objectAt:rowX];
	if (!rowId || (colX > [rowId count])) return (dataEntry_t*)nil;
	return entryPTR(rowId,colX);
}

/* return specified table entry (by name) */
- (const char*)valueForRowName:(const char*)rowN columnName:(const char*)colN
{
	int	wc = [self indexForColumnName:colN];
	if (wc >= 0) {
		int r = [self indexForRowName:rowN exactMatch:YES];
		if (r >= 0) return entryVALUE([dataROWS objectAt:r], wc);
	}
	return (char*)nil;
}

/* return specified table entry (by index) */
- (const char*)valueAtIndex:(u_int)rowX :(u_int)colX
{
	dataEntry_t		*de = [self entryAtIndex:rowX:colX];
	return de? de->value : (char*)nil;
}

/* return string constant at location */
- (const char*)valueFor:(u_int)rowIndex :(u_int)columnIndex
{
	return [self valueAtIndex:rowIndex:columnIndex];
}

/* set indexed table entry */
- setValue:(const char*)value atIndex:(u_int)rowX :(u_int)colX
{
	dataEntry_t		*de = [self entryAtIndex:rowX:colX];
	if (!de) return (id)nil;
	freeString(de->value);
	de->value = (char*)[self copyStringValue:value forColumn:colX];
	de->isValid = -1;
	[self setDocEdited:YES];
	return self;
}

/* set value for specified row/column name */
- setValue:(const char*)value forRowName:(const char*)rowN columnName:(const char*)colN
{
	int	wc = [self indexForColumnName:colN];
	if (wc >= 0) {
		int wr = [self indexForRowName:rowN exactMatch:YES];
		if (wr >= 0) return [self setValue:value atIndex:wr:wc];
	}
	return (id)nil;
}

/* change table value (note: index is independent of actual column position) */
- setValueFor:rowIndex :colIndex from:aValue
{
	return [self setValue:[aValue stringValue] atIndex:(u_int)rowIndex:(u_int)colIndex];
}

/* change table value (note: index is independent of actual column position) */
- setValueFor:colIndex at:(u_int)rowPosition from:aValue
{
	return [self setValue:[aValue stringValue] atIndex:rowPosition:(u_int)colIndex];
}

/* get table value */
- getValueFor:rowIndex :columnIndex into:aValue
{
	return [self getValueFor:columnIndex at:(u_int)rowIndex into:aValue];
}

/* get table value */
- getValueFor:index at:(u_int)aPosition into:aValue
{
	if (index == nullIndex) { [aValue setNull]; return self; }
	[aValue setStringValue:(char*)[self valueFor:aPosition :(u_int)index]];
	return self;
}

/* return specified table entry (by name) */
- (int)scanForValue:(const char*)value inColumnName:(const char*)colN
	startingAtRow:(int)rowX backwards:(BOOL)back
{
	if (value && colN) {
		int wc = [self indexForColumnName:colN];
		if (wc >= 0) {
			int r = rowX < 0? [dataROWS count] : rowX % [dataROWS count];
			if (rowX < 0) r = [dataROWS count];
			else r = rowX % [dataROWS count];
			for (;;) {
				id rowId = [dataROWS objectAt:r];
				if (strstr(entryVALUE(rowId, wc), value)) return r;
				r = (r + 1) % [dataROWS count];
				if (r == rowX) break;
			}
		}
	}
	return -1;
}

// -------------------------------------------------------------------------------------
// return table attributes

/* return name of table */
- (const char*)tableName
{
	return tableHandle->name;
}

/* return title of table */
- (const char*)tableTitle
{
	return tableHandle->name;
}

/* return access(path) of table */
- (const char*)tableAccess
{
	return tableHandle->access;
}

/* return number of visible columns in table */
- (u_int)visibleColumnCount
{
	if (!orderedINFO) [self _resetDisplayedColumnOrdering];
	return [orderedINFO count];
}

/* return number of visible columns */
- (u_int)columnCount
{
	return [self visibleColumnCount];
}

/* return number of visible columns in table */
- (u_int)actualColumnCount
{
	return [columnINFO count];
}

/* return number of rows in table */
- (u_int)rowCount
{
	return [dataROWS count];
}

// -------------------------------------------------------------------------------------
// DBTableView notification methods

/* user selection changed */
- tableViewDidChangeSelection:aTableView
{
	return self;
}

/* user move column */
- tableView:aTableView movedColumnFrom:(u_int)oldpos to:(u_int)newpos
{
	[self setDocEdited:YES];
	return self;
}

/* selection will change */
- (BOOL)tableViewWillChangeSelection:aTableView
{
	return YES;	// allow selection change
}

// -------------------------------------------------------------------------------------
// subclass methods

- (time_t)timestamp
{
	return (time_t)0;
}

- readTableColumns
{
	return [self subclassResponsibility:_cmd];
}

- readTableData
{
	return [self subclassResponsibility:_cmd];
}

- writeTable
{
	return [self subclassResponsibility:_cmd];
}

- (const char*)copyStringValue:(const char*)value forColumn:(int)index
{
	return NXCopyStringBuffer(value? value : "");
}

- (BOOL)verifyValue:(const char*)value dataType:(int)dataType
{
	return value? YES : NO;
}

// -------------------------------------------------------------------------------------
@end 
