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

#import <appkit/appkit.h>
#import <dbkit/dbkit.h>
#import <objc/objc.h>
#import <libc.h>
#import <stdlib.h>
#import <string.h>
#import <c.h>
#import <ctype.h>
#import <sys/param.h>
#import <sys/types.h>
#import <sys/time.h>
#import <sys/dir.h>
#import "FileTable.h"
#import "BoolFormatter.h"
#import "EditFormatter.h"
#import "DataTableView.h"
#import "SimpleTableView.h"
#import "DataEditor.h"

// -------------------------------------------------------------------------------------
// DataEditor global vars
static id					instanceList = (id)nil;		// list of instantiated editors
static BOOL					shutDown = NO;				// becomes true when terminating

// -------------------------------------------------------------------------------------
// global preferences
static BOOL					prefShowTableGrid = YES;

// -------------------------------------------------------------------------------------
// NXRect abbreviations
#define	X					origin.x
#define	Y					origin.y
#define	W					size.width
#define	H					size.height

// -------------------------------------------------------------------------------------
// misc constants/macros
#define LOCALIZE(X)			NXLocalizedString((X), cpNIL, cpNIL)
#define	cpNIL				(char*)nil
#define	KEY_COLUMN			0

// -------------------------------------------------------------------------------------
// textWidth macro
#define textWidth(T)		({ \
								NXSize s; \
								[[[[Cell alloc] initTextCell:T] calcCellSize:&s] free]; \
								rint(s.width * 1.40); \
							})

// -------------------------------------------------------------------------------------
@implementation DataEditor
// -------------------------------------------------------------------------------------

/* return custom column formatter */
- _columnFormatterForType:(int)type
{
	id	fmtId = (id)nil;
	switch (type) {
		case CDT_BOOLEAN:
			fmtId = [[BoolFormatter alloc] init];
			[fmtId setIcon:""];
			[fmtId setAltIcon:"checkH"];
			break;
		case CDT_UNKNOWN:
		case CDT_STRING:
		case CDT_INTEGER:
		default:
			fmtId = [[EditFormatter alloc] init];
			break;
	}
	return fmtId;
}

/* return vector column for identifier */
- (int)_vectorColumn:(int)col
{
	int	c;
	for (c = 0; c < [dataTableView columnCount]; c++) {
		id <DBTableVectors> v = [dataTableView columnAt:c];
		if ([v identifier] == (id)col) return c;
	}
	return -1;
}

/* add column at view order */
- _addColumnAt:(int)i
{
	id					fmtId;
	id <DBTableVectors>	vectId;
	dataColumn_t		*ci = [dataTable orderedColumnInfoAt:i];
	
	/* column not found, or is hidden */
	if (!ci || ci->isHidden) return self;
	
	/* add column to TableView */
	fmtId = [self _columnFormatterForType:ci->type];
	[dataTableView addColumn:(id)ci->index withFormatter:fmtId andTitle:ci->title at:i];
	
	/* set column attributes */
	vectId = [dataTableView columnAt:i];
	[vectId setTitle:ci->title];
	[vectId setTitleAlignment:NX_CENTERED];
	[vectId sizeTo:((ci->size > 0.0)? ci->size : textWidth(ci->title))];
	[vectId setMinSize:ci->minSize];
	[vectId setContentAlignment:ci->alignment];
	[vectId setEditable:ci->isEditable];
	[vectId setResizable:YES];
	[vectId setAutosizable:NO];
	
	return self;
}

/* reset column info */
- _resetColumnInfo
{
	int	c;

	/* update dataTable column header info */
	for (c = 0; c < [dataTable actualColumnCount]; c++) {
		int n = [self _vectorColumn:c];
		float size = n>=0? [[dataTableView columnAt:n] size] : 0.0;
		[dataTable setDisplayOrder:n andWidth:size forColumnIndex:c];
	}
	
	return self;
}

/* reload tableView data */
- _reloadData
{
	[rcdCount setIntValue:[dataTable rowCount]];
	[dataTableView reloadData:self];
	return self;
}

/* hide selected column */
- hideColumn:sender
{
	int		col = [dataTableView selectedColumn];
	if (col >= 0) {
		[dataTableView removeColumnAt:col];
		[self _resetColumnInfo];
		[self _reloadData];
	} else NXBeep();
	return self;
}

/* reset column info */
- unhideAllColumns:sender
{
	int	c, vcnt = [dataTable columnCount], acnt = [dataTable actualColumnCount];

	/* display display */
	[editorWindow disableFlushWindow];
	[editorWindow disableDisplay];
	
	/* unhide hidden columns */
	for (c = 0; (c < acnt) && (vcnt < acnt); c++) {
		dataColumn_t *ci = [dataTable columnInfoAt:c];
		if (ci && ci->isHidden) {
			[dataTable setDisplayOrder:vcnt andWidth:-1.0 forColumnIndex:c];
			[self _addColumnAt:vcnt++];
		}
	}
	
	/* reload data */
	[self _resetColumnInfo];
	[editorWindow reenableDisplay];
	[editorWindow reenableFlushWindow];
	[self _reloadData];
	
	return self;
}

// -------------------------------------------------------------------------------------
// DataEditor initialization

/* initialize window */
- _initWindowViews
{
	int		i, c;
	NXRect	rect;
	char	*docTitle;
	id		tblViewSav;

	/* disable window flushing */
	[editorWindow disableFlushWindow];
	[editorWindow disableDisplay];
	
	/* set document title */
	docTitle = (char*)[dataTable tableTitle];
	if (*docTitle == '/') [editorWindow setTitleAsFilename:docTitle];
	else [editorWindow setTitle:docTitle];
	
	/* init window header box */
	[headerBox setBorderType:NX_NOBORDER];
	[rcdCount setIntValue:[dataTable rowCount]];

	/* initialize dataTableBox & contents */
	[dataTableBox setBorderType:NX_NOBORDER];
	tblViewSav = dataTableView;
	[tblViewSav getFrame:&rect];
	dataTableView = [[TableView alloc] initFrame:&rect];
	[[tblViewSav superview] addSubview:dataTableView];
	[tblViewSav free];
	
	/* set tableview attributes */
	[dataTableView setMode:DB_LISTMODE];
	[dataTableView setTarget:self];
	[dataTableView setAction:@selector(selectTableCell1:)];
	[dataTableView setDoubleAction:@selector(selectTableCell2:)];
	[dataTableView setDelegate:self];
	[dataTableView allowVectorReordering:YES];
	[dataTableView allowVectorResizing:YES];
	[dataTableView setEditable:YES];
	[dataTableView setGridVisible:prefShowTableGrid];
	[dataTableView setHorizScrollerRequired:YES];
	
	/* add columns */
	for (c = [dataTable columnCount], i = 0; i < c; i++) [self _addColumnAt:i];
	[dataTableView setDataSource:dataTable];

	/* re-enable window flushing */
	[editorWindow reenableDisplay];
	[editorWindow reenableFlushWindow];
	[editorWindow display];

	return self;
}

/* place window */
- _placeWindow:(BOOL)moveWindow
{
	NXSize	scrSize;
	NXRect	rect;
	[NXApp getScreenSize:&scrSize];
	[editorWindow getFrame:&rect];
	if (moveWindow) {
		static int winCount = 0;
		rect.X = floor(scrSize.width  * 0.20) + ((float)winCount *  64.0);
		rect.Y = floor(scrSize.height * 0.85) + ((float)winCount * -24.0);
		if (rect.X > (scrSize.width  - 64.0)) rect.X = scrSize.width  - 64.0;
		if (rect.Y > (scrSize.height - 24.0)) rect.Y = scrSize.height - 24.0;
		rect.Y -= rect.H;
		winCount = (++winCount) % 5;
	}
	if ([dataTable viewSize]) rect.size = *[dataTable viewSize];
	[editorWindow placeWindow:&rect];
	[self windowDidResize:editorWindow];
	return self;
}

/* initialization */
- initFromFile:(const char*)fileName
{
	
	/* init super and add to instance list */
	[super init];
	if (!instanceList) instanceList = [[[List alloc] initCount:1] empty];
	[instanceList addObject:self];

	/* load nib */
	if (![NXApp loadNibSection:"DataEditor.nib" owner:self]) {
		NXLogError("Could not load nib file 'DataEditor.nib'");
		[NXApp delayedFree:self];
		return (id)nil;
	}
	
	/* open/fill table */
	dataTable = [[FileTable alloc] initFromFile:fileName];
	[dataTable setDelegate:self];
	[self sortByDisplayOrder];
	
	/* init/show window */
	[editorWindow setDelegate:self];
	[self _initWindowViews];
	[self _placeWindow:YES];
	[[editorWindow display] makeKeyAndOrderFront:(id)nil];
	
	/* return */
	return self;
}

/* normal free */
- free
{
	[instanceList removeObject:self];
	[dataTable free];
	return [super free];
}

/* close window (free) */
- closeWindow
{
	[editorWindow performClose:self];
	return self;
}

/* free all instances */
+ terminateAllEditors
{
	shutDown = YES;
	[[[instanceList copy] makeObjectsPerform:@selector(closeWindow)] free];
	return self;
}

// -------------------------------------------------------------------------------------
// key name

/* get a new key name */
- (const char*)getKeyName:(const char*)keyName
{
	char	title[256], *newName, *tableName = (char*)[dataTable tableTitle];
	
	/* init panel fields and show */
	sprintf(title, "%s: Key Name", (*tableName=='/'?"<file>":tableName));
	[dataNamePanel setTitle:title];
	[dataNameField setStringValue:(keyName?keyName:"")];
	[dataNameError setStringValue:""];
	[[[dataNamePanel center] display] makeKeyAndOrderFront:(id)nil];

	/* loop until successful */
	for (;;) {
	
		/* show panel */
		[NXApp runModalFor:dataNamePanel];
		newName = (char*)[dataNameField stringValue];
		if (dataNameFlag || !*newName || !strcmp(newName, keyName)) {
			newName = (char*)nil;
			break;
		}
	
		/* check for existance */
		if ([dataTable indexForRowName:newName exactMatch:YES] < 0) break;
		[dataNameError setStringValue:"Key name already exists. Please try again."];
		NXLogError("Key %s already exists", newName);
		NXBeep();
	
	}
	
	[dataNamePanel orderOut:(id)nil];
	return newName;
	
}

/* add key button selection */
- keyResponse:sender
{
	dataNameFlag = [sender tag];
	[NXApp abortModal];
	return self;
}

// -------------------------------------------------------------------------------------
// adding/duplicating rows

/* add a new row */
- addRow:sender
{
	char	*keyName;
	if (keyName = (char*)[self getKeyName:""]) {
		[dataTable newRowName:keyName copyFromRow:-1];
		[self resort:(id)nil];
	}
	return self;
}

/* duplicate selected row */
- duplicateRow:sender
{
	int		row = [dataTableView selectedRow];
	char	*keyName;
	if ((row >= 0) && (keyName = (char*)[self getKeyName:""])) {
		[dataTable newRowName:keyName copyFromRow:row];
		[self resort:(id)nil];
	}
	return self;
}

/* delete selected row */
- deleteRow:sender
{
	int		row = [dataTableView selectedRow];
	if (row >= 0) {
		char *value = (char*)[dataTable valueFor:row:KEY_COLUMN];
  		const char *ttl = LOCALIZE("Delete");
		const char *msg = LOCALIZE("%s: Delete Record Entry '%s'?");
  		const char *op1 = LOCALIZE("Delete");
		const char *op2 = LOCALIZE("Don't Delete");
		const char *op3 = cpNIL;
		int rtn = NXRunAlertPanel(ttl,msg,op1,op2,op3, [dataTable tableName], value);
		if (rtn == NX_ALERTDEFAULT) {
			[dataTable deleteRowAt:row];
			[self _reloadData];
		}
	} else NXBeep();
	return self;
}

// -------------------------------------------------------------------------------------
// table cell selection

/* single-click table cell selection */
- selectTableCell1:sender
{
	return self;
}

/* double-click table cell selection: edit record name field */
- selectTableCell2:sender
{
	char			*keyName;
	dataColumn_t	*ci;
	int				row = [dataTableView selectedCellRow];
	int				col = [dataTableView selectedCellColumn];
	
	/* check for proper row */
	if (row < 0) return self;
	
	/* check for proper column */
	ci = [dataTable orderedColumnInfoAt:col];
	if (!ci || (ci->index != KEY_COLUMN)) return self;

	/* get new key name */
	keyName = (char*)[self getKeyName:[dataTable valueFor:row:KEY_COLUMN]];
	if (!keyName) return self;
	
	/* change key name */
	[dataTable setValue:keyName atIndex:row:KEY_COLUMN];
	[dataTableView rowsChangedFrom:row to:row];

	return self;
}

// -------------------------------------------------------------------------------------
// sorting functions

/* sort by display order */
- sortByDisplayOrder
{
	dataColumn_t	*pci = [dataTable orderedColumnInfoAt:0];
	dataColumn_t	*sci = pci? [dataTable orderedColumnInfoAt:1] : (dataColumn_t*)nil;
	if (!pci || ![dataTable sortTableByColumn:pci->index :(sci?sci->index:-1)]) {
		NXLogError("Cannot sort by display order");
		return self;
	}
	return self;
}

/* sort */
- resort:sender
{
	[self sortByDisplayOrder];
	[self _reloadData];
	return self;
}

// -------------------------------------------------------------------------------------
// pasteboard functions

/* delete column/row */
- delete:sender
{
	int	row = [dataTableView selectedRow], col = [dataTableView selectedColumn];
	if ((row < 0) && (col >= 0)) [self hideColumn:(id)nil];
	else [self deleteRow:(id)nil];
	return self;
}

/* cut column/row */
- cut:sender
{
	// not supported
	NXBeep();
	return self;
}

/* copy column/row */
- copy:sender
{
	// not supported
	NXBeep();
	return self;
}

/* paste column/row */
- paste:sender
{
	// not supported
	NXBeep();
	return self;
}

// -------------------------------------------------------------------------------------
// save

/* set doc modified flag */
- setDocEdited:(BOOL)flag
{
	return [editorWindow setDocEdited:flag];
}

/* save contents of document */
- save:sender
{
	NXRect	rect;
	
	/* reset column info */
	[self _resetColumnInfo];
	
	/* check for existing errors */
	if ([dataTable hasVerificationErrors]) {
   		const char *ttl = LOCALIZE("Save");
		const char *msg = LOCALIZE("%s has errors");
  		const char *op1 = LOCALIZE("Save Anyway");
		const char *op2 = LOCALIZE("Don't Save");
  		const char *op3 = cpNIL;
		int rtn = NXRunAlertPanel(ttl,msg,op1,op2,op3, [dataTable tableName]);
		if (rtn != NX_ALERTDEFAULT) return (id)nil;  // Don't Save
	}

	/* check for table changes */
	if ([dataTable tableHasChanged]) {
   		const char *ttl = LOCALIZE("Save");
		const char *msg = LOCALIZE("%s has been edited by someone else");
  		const char *op1 = LOCALIZE("Overwrite");
		const char *op2 = LOCALIZE("Cancel");
  		const char *op3 = cpNIL;
		int rtn = NXRunAlertPanel(ttl,msg,op1,op2,op3, [dataTable tableName]);
		if (rtn != NX_ALERTDEFAULT) return (id)nil;  // Cancel
	}
	
	/* commit(save) table */
	[editorWindow getFrame:&rect];
	[dataTable setViewSize:&rect.size];
	[dataTable commitTable];
	
	return self;
	
}

/* save contents of document */
- saveAs:sender
{
	NXLogError("SaveAs not implemented...");
	NXBeep();
	return self;
}

/* revert to saved */
- revertToSaved:sender
{
	id	oldData;
	if ([editorWindow isDocEdited]) {
   		const char *ttl = LOCALIZE("Revert");
		const char *msg = LOCALIZE("Revert changes to %s?");
 		const char *op1 = LOCALIZE("Revert");
		const char *op2 = LOCALIZE("Cancel");
		const char *op3 = cpNIL;
		int rtn = NXRunAlertPanel(ttl,msg,op1,op2,op3, [dataTable tableName]);
		if (rtn != NX_ALERTDEFAULT) return (id)nil;	// Cancel
	}
	oldData = dataTable;
	dataTable = [[FileTable alloc] initFromFile:[oldData tableAccess]];
	[dataTable setDelegate:self];
	[self sortByDisplayOrder];
	[self _initWindowViews];
	[self _placeWindow:NO];
	[self setDocEdited:NO];
	[oldData free];
	return self;
}

/* close window */
- close:sender
{
	return [self closeWindow];
}

// -------------------------------------------------------------------------------------
// TableView delegate methods

/* user move column */
- tableView:aTableView movedColumnFrom:(u_int)oldpos to:(u_int)newpos
{
	[self _resetColumnInfo];
	if ((oldpos <= 1) || (newpos <= 1)) [self resort:(id)nil];
	[self setDocEdited:YES];
	return self;
}

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

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

// -------------------------------------------------------------------------------------
// window delegate methods

/* window is resizing */
- windowWillResize:windowId toSize:(NXSize*)newSize
{
	NXRect	fRect, cRect;

	/* set max width: account for ScrollView(TableView) */
	[[dataTableView docView] getFrame:&cRect];
	[[dataTableView class] getFrameSize:&fRect.size forContentSize:&cRect.size
		horizScroller:YES vertScroller:YES borderType:[dataTableView borderType]];
	
	/* set max width: account for Box (nil since Box has no border) */
	
	/* set max width: account for Window */
	cRect.size = fRect.size;
	cRect.X = cRect.Y = 0.0;
	[[windowId class] getFrameRect:&fRect forContentRect:&cRect style:[windowId style]];

	/* reset width */
	if (newSize->width > fRect.W) newSize->width = fRect.W;
	
	return self;
}

/* window is resizing */
- windowDidResize:windowId
{
	NXRect	winFrame, rect;
	
	/* get window size */
	[[windowId contentView] getFrame:&winFrame];

	/* reset header width */
	[headerBox getFrame:&rect];
	winFrame.H -= rect.H;
	rect.X = 0.0;
	rect.Y = winFrame.H;
	rect.W = winFrame.W;
	[headerBox setFrame:&rect];

	/* reset table box frame */
	[dataTableBox getFrame:&rect];
	rect.X = 0.0;
	rect.Y = 0.0;
	rect.size = winFrame.size;
	[dataTableBox setFrame:&rect];
	
	/* resize contents of table box */
	[[dataTableBox contentView] getFrame:&rect];
	[dataTableView setFrame:&rect];
	
	return self;
}

/* window will close */
- windowWillClose:windowId
{

	/* check for edited document */
	if ([editorWindow isDocEdited]) {
   		const char *ttl = LOCALIZE("Save");
		const char *msg = LOCALIZE("Save changes to %s?");
 		const char *op1 = LOCALIZE("Save");
		const char *op2 = LOCALIZE("Don't Save");
  		const char *op3 = shutDown? cpNIL : LOCALIZE("Cancel");
		int rtn = NXRunAlertPanel(ttl,msg,op1,op2,op3, [dataTable tableName]);
		if (!shutDown && (rtn == NX_ALERTOTHER)) return (id)nil;	// Cancel
		if (rtn == NX_ALERTDEFAULT) [self save:(id)nil];			// Save (else No)
	}
	
	/* free/close window */
	[instanceList removeObject:self];
	[NXApp delayedFree:self];
	return self;
	
}

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