// -------------------------------------------------------------------------------------
//  FileTable
//  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 "FileTable.h"

// -------------------------------------------------------------------------------------
@implementation FileTable

// -------------------------------------------------------------------------------------

/* count items in an unparsed table */
static int dt_countList(char *list)
{
	int		c;
	char	*r = list;
	
	/* make sure there is at least one entry */
	if (!r) return 0;
	while (isspace(*r)) r++;
	if (!*r) return 0;
	
	/* count remaining entries */
	for (c = 0, r = list; *r;) {
		c++;
		while (*r && !isspace(*r)) r++;	// skip value
		while (isspace(*r)) r++;		// skip white
	}
	
	return c;
}

/* count items in a parsed table */
static int dt_countTable(char **tbl)
{
	int	c;
	if (!tbl) return 0;		// no entries counted
	for (c = 0; tbl[c]; c++);
	return c;
}

/* parse a string table */
static char **dt_parseTable(char *list)
{
	int		n, tblSize;
	char	**theTbl, *r;

	/* skip whitespace in list */
	if (!list) return (char**)NULL;
	while (isspace(*list)) list++;
	if (!*list) return (char**)NULL;

	/* count items in table */
	tblSize = sizeof(char*) * (dt_countList(list) + 1); // include terminator
	theTbl = (char**)malloc(tblSize);
	memset(theTbl, 0, tblSize);
	
	/* fill table */
	for (n = 0, r = list; *r;) {
		theTbl[n++] = r;
		while (*r && !isspace(*r)) r++;
		while (isspace(*r)) *r++ = 0;
	}
	
	/* return the table */
	return theTbl;

}

// -------------------------------------------------------------------------------------

/* return access timestamp */
- (time_t)timestamp
{
	struct stat		st;
	if (stat((char*)tableHandle->access, &st) < 0) {
		NXLogError("Unable to stat file %s", tableHandle->access);
		return 0;
	}
	return st.st_mtime;
}

// -------------------------------------------------------------------------------------

- _addSimpleColumn:(char*)title :(char)cType :(float)size
{
	int				t;
	char			*types = CDT_TYPES;
	dataColumn_t	*dc = (dataColumn_t*)malloc(sizeof(dataColumn_t));
	memset(dc, 0, sizeof(dataColumn_t));
	for (t = 0; types[t] && (types[t] != cType); t++);
	dc->index        = [tableHandle->columnId count];
	dc->type         = types[t]? t : CDT_UNKNOWN;
	dc->keyTag       = NXCopyStringBuffer(title);
	dc->title        = (char*)nil;
	dc->size         = size;
	dc->minSize      = 0.0;
	dc->displayOrder = dc->index;
	dc->alignment    = NX_CENTERED;
	dc->isEditable   = dc->index? YES : NO;
	dc->isHidden     = (dc->displayOrder >= 0)? NO : YES;
	dc->nilValue     = (char*)nil;
	[self addColumnInfo:dc];
	return self;
}

/* load table column info */
- readTableColumns
{
	char		buf[2048];
	BOOL		cLoaded = NO, vLoaded = NO;
	FILE		*fh;
	
	/* open table */
	if (!(fh = fopen(tableHandle->access, "r"))) {
		NXLogError("Unable to open file %s", tableHandle->access);
		return (id)nil;
	}

	/* find/read column information */	
	while (fgets(buf, sizeof(buf), fh) && (*buf == '#')) {
		
		/* view information */
		if (!vLoaded && !strncmp(buf, "#!t", 3)) {
			char viewName[128];
			NXSize s = { 0.0, 0.0 };
			sscanf(buf + 4, "%s %f %f", viewName, &s.width, &s.height);
			tableHandle->viewSize = s;
			vLoaded = YES;
			continue;
		}
		
		/* column information */
		if (!cLoaded && (!strncmp(buf, "#!c\t", 4) || !strncmp(buf, "#\t", 2))) {
			int key, numCols;
			char **tbl, *b = buf + 1;
			for (;*b && (*b != ' ') && (*b != '\t'); b++);
			for (;(*b == ' ') || (*b == '\t'); b++);
			if (!(tbl = dt_parseTable(b))) continue;
			numCols = dt_countTable(tbl);
			for (key = 0; key < numCols; key++) {
				float size = 0.0;
				char *title = tbl[key], *p = index(title, ':'), ctype[4];
				if (p) { *p++ = 0; sscanf(p, "%c:%f", ctype, &size); }
				[self _addSimpleColumn:title :*ctype :size];
			}
			free(tbl);
			cLoaded = YES;
			continue;
		}
		
	}
	
	/* close table */
	fclose(fh);
	
	return self;
}

// -------------------------------------------------------------------------------------

/* load table */
- readTableData
{
	FILE	*fh;
	u_int	r;
	char	buf[4096];
	int		count = [tableHandle->columnId count];
	
	/* open table */
	if (!tableHandle->access) return (id)nil;
	if (!(fh = fopen((char*)tableHandle->access, "r"))) {
		NXLogError("Unable to open file %s", tableHandle->access);
		return (id)nil;
	}
	
	/* read table data */
	for (r = 0; fgets(buf, sizeof(buf), fh);) {
		int c, tlen;
		id rowId;
		char **tbl;
		if (!*buf || (*buf == '#') || (*buf == '\t') || (*buf == ' ')) continue;
		if (!(tbl = dt_parseTable(buf))) continue;
		rowId = [[List alloc] initCount:count];
		[tableHandle->dataId insertObject:rowId at:r];
		for (tlen = dt_countTable(tbl), c = 0; c < count; c++) {
			char *val = (c < tlen)? tbl[c] : (char*)nil;
			dataEntry_t *de = entryNEW;
			de->value = (char*)[self copyStringValue:val forColumn:c];
			de->isValid = -1;
			[rowId insertObject:(id)de at:c];
		}
		free(tbl);
		r++;
	}
	
	/* close table */
	fclose(fh);
	
	return self;
}

// -------------------------------------------------------------------------------------

- writeTable
{
	int			r, c;
	FILE		*fh;
	struct stat	st;
	
	/* make backup copy */
	if (stat(tableHandle->access, &st) >= 0) {
		char bku[MAXPATHLEN + 1];
		sprintf(bku, "%s~", tableHandle->access);
		if (rename(tableHandle->access, bku)) {
			NXLogError("Unable to rename file %s to %s", tableHandle->access, bku);
			return (id)nil;
		}
	}
	
	/* open file for writing */
	if (!(fh = fopen(tableHandle->access, "w+"))) {
		NXLogError("Unable to open file %s", tableHandle->access);
		return (id)nil;
	}

	/* view header */
	fprintf(fh, "#!t %s %.0f %.0f\n", tableHandle->name,
		tableHandle->viewSize.width, tableHandle->viewSize.height);
	
	/* column headers */
	fprintf(fh, "#!c\t");
	for (c = 0; c < [tableHandle->columnId count]; c++) {
		dataColumn_t *ci = [self columnInfoAt:c];
		fprintf(fh, "%s:%c:%.0f\t", ci->keyTag, *(CDT_TYPES + ci->type), ci->size);
	}
	fprintf(fh, "\n");
	
	/* data */
	for (r = 0; r < [tableHandle->dataId count]; r++) {
		id	rowId = [tableHandle->dataId objectAt:r];
		for (c = 0; c < [rowId count]; c++) {
			dataColumn_t *ci = [self columnInfoAt:c];
			dataEntry_t *de = entryPTR(rowId, c);
			if (c) fprintf(fh, "\t");
			fprintf(fh, "%s", (*de->value?de->value:(ci->nilValue?ci->nilValue:"*")));
		}
		fprintf(fh, "\n");
	}
	
	/* close file */
	fclose(fh);
	
	/* re-stat file to get modified date */
	if (stat((char*)tableHandle->access, &st) >= 0) tableHandle->date = st.st_mtime;
	else NXLogError("Unable to stat file %s", tableHandle->access);
	
	return self;
}

// -------------------------------------------------------------------------------------

/* create value for column */
- (const char*)copyStringValue:(const char*)value forColumn:(int)index
{
	char			*vcopy, buff[64];
	dataColumn_t	*dc = [self columnInfoAt:index];
	
	/* check arguments */
	if (!dc) return (char*)nil;
	
	/* special return types */
	switch (dc->type) {
		case CDT_BOOLEAN:
			return NXCopyStringBuffer(((value && strcmp(value,"NO"))?"YES":"NO"));
			break;
		case CDT_INTEGER:
			sprintf(buff, "%d", (value?atoi(value):0));
			return NXCopyStringBuffer(buff);
			break;
		case CDT_STRING:
		default:
			if (!value) return NXCopyStringBuffer("");
			for (vcopy = (char*)value; isspace(*vcopy); vcopy++); // skip white space
			vcopy = strcpy((char*)malloc(strlen(vcopy) + 1), vcopy);
			{ char *b,*v; for (b=v=vcopy;*v;v++) if(!isspace(*v)){if(b!=v)*b=*v;b++;} *b=0; }
			if (dc->nilValue && !strcmp(dc->nilValue, vcopy)) *vcopy = 0;
			return (char*)realloc(vcopy, strlen(vcopy) + 1);
			break;
	}

	/* control should not reach here */
	return NXCopyStringBuffer("");
}

// -------------------------------------------------------------------------------------

/* validate specific value (intended for subclass implementation) */
- (BOOL)verifyValue:(const char*)value dataType:(int)dataType
{
	if (!value) return NO;
	switch (dataType) {
		case CDT_STRING:
		case CDT_INTEGER:
		default:
			return YES;
	}
	return YES;
}

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