/* MiscTreeBrowserMatrix
 *
 */

// IMPORTS

#import <math.h>
#import <dpsclient/wraps.h>
#import <appkit/publicWraps.h>	// NXBeep()
#import <appkit/nextstd.h>		// NXLogError()
#import <appkit/graphics.h>
#import <appkit/Application.h>
#import <appkit/TextField.h>
#import <appkit/TextFieldCell.h>
#import <appkit/Text.h>

#import <misckit/MiscTreeBrowser.h>
#import <misckit/MiscTreeBrowserCell.h>
#import <misckit/MiscTreeBrowserMatrix.h>
#import <misckit/MiscTreeBrowserProtocols.h>
#import <misckit/TB_images.h>

// DEFINES

#define RETURN	13
#define ESCAPE	27
#define DELETE	127

#define LEFTARROW	172
#define RIGHTARROW	174
#define UPARROW		173
#define DOWNARROW	175

#define NODE_INDENT_WIDTH	10.0


// IMPLEMENTATION

@implementation MiscTreeBrowserMatrix

// PRIVATE METHODS

- (int)_indexOfNode:aNode
/*
Returns index of cell containing aNode, or -1 if none does.
*/
{
	unsigned int row, cellCount = [self cellCount];

	for (row = 0; row < cellCount; row++)
	{
		id aCell = [self cellAt:row :0];

		if ([aCell node] == aNode)
		{
			return row;
		}
	}

	return -1;					// no cells contain aNode
}

- _cellForNode:aNode
{
	int idx = [self _indexOfNode:aNode];

	return (idx >= 0) ? [self cellAt:idx :0] : nil;
}

- _selectCell:aCell withState:(int)state
{
	int i, lim = [self cellCount];
	id returnCell = nil;

	[self lockFocus];
	for (i = 0; i < lim; i++)
	{
		id c = [self cellAt:i :0];
		int cState = 0;

		if (c == aCell)
		{
			cState = state;
			returnCell = aCell;
		}
		[cell setState:cState];
		if ([c isHighlighted] != cState)
		{
			[self highlightCellAt:i :0 lit:cState ? YES : NO];
		}
	}
	[self unlockFocus];

	return returnCell;
}

- _dragImage
/*"
Returns the (static) drag NXImage.
"*/
{
	static NXImage *dImage = nil;
	if (!dImage)
	{
		dImage = _MISC_TBDrag_tiff_fn();
	}

	return dImage;
}

- _treeBrowser
/*"
Returns our controlling MiscTreeBrowser 
"*/
{
	return [superview superview];
}

- _editField
/*"
Return the textfield used for editing cells.
"*/
{
	if (!editField)
	{
		NXRect aFrame = { 0.0, 0.0, 0.0, 0.0 };

		aFrame.size = cellSize;
		editField = [[TextField alloc] initFrame:&aFrame];
		if (font)
		{
			[editField setFont:font];
		}
		[editField setEditable:YES];
		[editField setBezeled:YES];
		[editField setTextDelegate:self];
	}

	return editField;
}

// cell dragging private methods

- _dragCell
/*"
Return cell dragged from, or nil of not dragging.
"*/
{
	return (dragCellRow >= 0) ? [self cellAt:dragCellRow :0] : nil;
}

- (BOOL)_isDraggingCell
/*"
Return YES if MiscTreeBrowserMatrix is dragging.
"*/
{
	return (dragCellRow >= 0) ? YES : NO;
}

- _clearDragging
/*"
Mark MiscTreeBrowserMatrix as no longer dragging.
"*/
{
	lastPoint.x = lastPoint.y = -1.0;
	dragCellRow = -1;
	toCellRow = -1;
	toIndentLevel = -1;

	return self;
}

- _setDragging:(int)rowNum
/*"
Mark MiscTreeBrowserMatrix as dragging; save rowNum of cell dragged from.
"*/
{
	dragCellRow = rowNum;
	
	return self;
}

- _toggleCellOpen
/*"
Toggle the selected cell open or closed.
"*/
{
	id node = [[self selectedCell] node];

	if ([[self _treeBrowser] nodeIsOpen:node])
	{
		[[self _treeBrowser] closeNode:node];
	}
	else
	{
		[[self _treeBrowser] openNode:node];
	}

	return self;
}

- _init
/*"
Initialise instance variables
"*/
{
	textCell = [[TextFieldCell alloc] init];
	nodeIndentWidth = NODE_INDENT_WIDTH;
	[self _clearDragging];

	return self;
}

// PUBLIC METHODS

- initFrame:(const NXRect *)frRect 
	   mode:(int)aMode
  cellClass:classId
	numRows:(int)r
	numCols:(int)c
/*"
Initialise a MiscTreeBrowserMatrix instance.  aMode should be MX_TRACKMODE to allow node dragging.
"*/
{
	[super initFrame:frRect
		   mode:aMode			// NX_TRACKMODE for dragging...
		   cellClass:classId
		   numRows:r numCols:c];
	[self _init];

	return self;
}

- initFrame:(const NXRect *)frameRect 
	   mode:(int)aMode
  prototype:aCell
	numRows:(int)r
	numCols:(int)c
/*"
Initialise a MiscTreeBrowserMatrix instance.  aMode should be MX_TRACKMODE to allow node dragging.
"*/
{
	[super initFrame:frameRect mode:aMode prototype:aCell numRows:r numCols:c];
	[self _init];

	return self;
}

- sharedTextCell
/*"
Returns the text cell to be used for writing the text in a cell.  Used by MiscTreeBrowserCell instances.
"*/
{
	return textCell;
}

- setFont:aFont
/*"
Set the matrix font to aFont.
"*/
{
	[[self _editField] setFont:aFont];
	[[self sharedTextCell] setFont:aFont];
	[super setFont:aFont];

	return self;
}

- sendDoubleAction
/*"
If the user double-clicks on a cell's title, edit the title, otherwise invoke %{-sendDoubleAction} on the cell.
"*/
{
	NXEvent *theEvent = [NXApp currentEvent];
	int row, col;
	NXRect cellFrame, titleFrame;
	NXPoint mousePt;
	id scell = [self selectedCell];

	mousePt = theEvent->location;
	[self convertPoint:&mousePt fromView:nil];
	[self getRow:&row andCol:&col ofCell:scell];
	[self getCellFrame:&cellFrame at:row :col];

								// get frames for checking pointer location
	titleFrame = cellFrame;
	[scell getTitleRect:&titleFrame inView:self];

	if ([self mouse:&mousePt inRect:&titleFrame])
	{							// double-click on text; edit it
		[self editCell:scell];
	}
	else
	{							// perform double-click action
		[scell sendDoubleAction];
	}

	return self;
}

- (NXColor)cellColor
{
	return [[self _treeBrowser] backgroundColor];
}

- (NXCoord)nodeIndentWidth
{
	return nodeIndentWidth;
}

// CELL SELECTION

- _selectCell:aCell
/*"
Select aCell.
"*/
{
	[super selectCell:aCell];

	return [self _selectCell:aCell withState:1];
}

- unselectCell:aCell
/*"
Unselect aCell and all others.
"*/
{
	[super selectCellAt:-1 :-1]; // unselect all

	return [self _selectCell:aCell withState:0];
}

- _selectCellAt:(int)row :(int)col
/*"
Select cell at row,col - deselect all others.  Returns self.
"*/
{
	id aCell = [self cellAt:row :col];

	[super selectCellAt:row : col];
	if (aCell)
	{
		[self _selectCell:aCell];
	}
	return self;
}

// EVENT HANDLING

- mouseDownAt:(const NXPoint *)startPoint forCell:(MiscTreeBrowserCell *)aCell
/*"
If already editing in another cell, end editing, and record startPoint.
"*/
{
	NXEvent *theEvent = [NXApp currentEvent];

	if (theEvent->data.mouse.click == 1)
	{
		// if we are already editing a (different) cell, end editing.
		if (editCell && (editCell != aCell))
		{
			id editor = [[editField window] getFieldEditor:NO for:editField];

			[self textDidEnd:editor endChar:NX_RETURN];
		}
								// record where clicked (to check if dragging)
		lastPoint = *startPoint;
	}

	return self;
}


// _dragToPoint:moveNode macros

#define DR_SETROWLEVEL(ROW,LEVEL) toCellRow = (ROW); toIndentLevel = (LEVEL)

#define DR_SUCCEED(LEVEL,PARENT,AFTER) \
	{ if (performMove) \
	   [[self _treeBrowser] addNode:dragNode toParent:(PARENT) after:(AFTER)];\
	  DR_SETROWLEVEL(row,LEVEL); return self; }

#define DR_FAIL(RETVAL)			return (RETVAL)


- _dragToPoint:(const NXPoint *)currentPoint moveNode:(BOOL)performMove
/*"
Given a cell from which a node is being dragged and the current drag point, determine which cell/node pair the node is being dragged to, and whether the node is to be added as a child of the node, or a child of one of its ancestors.  Translate this to an indent level, and store in the #int pointed to by pIndentLevel.  If performMove is YES, message the controlling MiscTreeBrowser to perform the move.
"*/
{
	int row, col;
	int aboveIndentLevel, belowIndentLevel, pointerIndentLevel;
	MiscTreeBrowserCell *overCell, *aboveCell, *belowCell;
	MiscTreeBrowserCell *fromCell = [self _dragCell];
	id<MiscTreeBrowserNodes> aboveNode, belowNode, dragNode, dragParent;
	NXRect cellFrame;
	NXCoord xOffset, yOffset;
	
	overCell = [self getRow:&row andCol:&col forPoint:currentPoint];
	if (!overCell || !fromCell)
	{							// not over a cell; exit here
		DR_FAIL(nil);
	}

	dragNode = [fromCell node];
	dragParent = [dragNode parent];

	if (performMove && dragParent && ![dragParent canRemoveChild:dragNode])
	{							// cannot remove dragged node from parent;
		NXBeep();				// if trying to move, complain
		performMove = NO;		// and do not attempt move
	}
								// get details of cell the pointer is in
	[self getCellFrame:&cellFrame at:row :col];
	yOffset =  currentPoint->y - cellFrame.origin.y;
	xOffset =  currentPoint->x - cellFrame.origin.x;
								// find which cell pointer is "between"
	if (row == dragCellRow)
	{
		NXCoord margin = cellFrame.size.height / 4.0;

		if ((yOffset >= margin) &&
			(yOffset <= (cellFrame.size.height - margin)))
		{						// pointer on home cell; do no more
			DR_SETROWLEVEL(row + 1,[overCell indentLevel]);

			return self;
		}
	}
	if (yOffset > (cellFrame.size.height / 2.0))
	{
		row++;
	}

	// row now indicates bottom row of the cells the pointer is "between"
	aboveCell = [self cellAt:(row - 1) :0];
	belowCell = [self cellAt:row :0];

	aboveNode = [aboveCell node];
	belowNode = [belowCell node];

	dragNode = [fromCell node];
	pointerIndentLevel = (int)(xOffset / [self nodeIndentWidth]);
	aboveIndentLevel = [aboveCell indentLevel];

	if ([aboveNode parent] == [belowNode parent])
	{							// nodes are siblings (possibly roots)
		id parent = [aboveNode parent];
		BOOL asChild = ([aboveNode canAddChild:dragNode]) ? YES : NO;
		BOOL asSibling = (!parent || [parent canAddChild:dragNode
											 after:aboveNode]) ? YES : NO;

		if (belowCell == fromCell)
		{						// between dragCell and one above
			asSibling = NO;
		}
		else if (aboveCell == fromCell)
		{						// between dragCell and one below
			asSibling = asChild  = NO;
		}

		if (asSibling && asChild)
		{	// can be either sib or child, use pointerIntentLevel to decide
			if (pointerIndentLevel > aboveIndentLevel)
			{
				asSibling = NO;
			}
			else
			{
				asChild = NO;
			}
		}

		// only one of asSibling and asChild is now YES
		if (asSibling)
		{
			DR_SUCCEED(aboveIndentLevel,parent,aboveNode);
		}
		else if (asChild)
		{
			DR_SUCCEED(aboveIndentLevel +1,aboveNode,MTB_LASTCHILD(aboveNode));
		}
	}
	else if ([belowNode parent] == aboveNode)
	{							// nodes are parent(above) / child(below)
		BOOL asChild = ([aboveNode canAddChild:dragNode]) ? YES : NO;

		if ((belowCell != fromCell) && (aboveCell != fromCell) && asChild)
		{
			DR_SUCCEED(aboveIndentLevel + 1,aboveNode,nil);
		}
	}
	else if (aboveIndentLevel > (belowIndentLevel = [belowCell indentLevel]))
	{							// above/below not related
		int indentLevel = (pointerIndentLevel < belowIndentLevel)
			? belowIndentLevel
			: pointerIndentLevel;

		if ((indentLevel > aboveIndentLevel) && (aboveNode != dragNode) &&
			[aboveNode canAddChild:dragNode])
		{
			DR_SUCCEED(aboveIndentLevel +1,aboveNode,MTB_LASTCHILD(aboveNode));
		}
		else
		{
			id sibling = [aboveCell nodeAtIndentLevel:pointerIndentLevel];
			id parent = sibling ? [sibling parent] : nil;

			if (!parent || [parent canAddChild:dragNode after:sibling])
			{
				DR_SUCCEED(indentLevel,parent,sibling);
			}
		}
	}
	else
	{
		NXLogError("Unknown drag state!\n");
	}

	DR_FAIL(nil);
}

- mouseDraggedAt:(const NXPoint *)currentPoint forCell:(MiscTreeBrowserCell *)fromCell
/*"
The user is dragging aCell, and the pointer is at currentPoint.  Work out what to draw, where...  To avoid unneccessary flicker during redraw, the image is only drawn if it has moved.  The point that the image was last drawn at is stored in %lastPoint.
"*/
{
	NXImage *dragImage = [self _dragImage];
	NXRect cellFrame;
	NXSize dragImageSize;
	NXPoint imagePoint;
	int oldIndentLevel, oldCellRow;

	if (![self _isDraggingCell])
	{
		int row, col;

		/* allow a few pixels "wobble" before starting drag */
		if ((fabs(currentPoint->x - lastPoint.x) +
			 fabs(currentPoint->y - lastPoint.y) <= 4.0))
		{
			return self;
		}
		// this is the start of dragging
		// get new dragCellRow values
		[self getRow:&row andCol:&col forPoint:&lastPoint];
		[self _setDragging:row];

		lastPoint.x = lastPoint.y = -1.0;
		PSsetinstance(YES);		// start instance drawing
	}
								// check if we have moved
	oldIndentLevel = toIndentLevel;
	oldCellRow = toCellRow;

	[self _dragToPoint:currentPoint moveNode:NO];

	if ((oldIndentLevel == toIndentLevel) && (oldCellRow == toCellRow))
	{							// not moved, no need to redraw
		return self;
	}
	if (toCellRow < 0)
	{							// pointer is not over a cell in the matrix
		PSnewinstance();		// so delete image and quit

		return self;
	}
								// calculate where to draw image
	[dragImage getSize:&dragImageSize];
	[self getCellFrame:&cellFrame at:toCellRow :0];
	imagePoint.x = cellFrame.origin.x +
		((float)toIndentLevel * [self nodeIndentWidth]);

	if ((toCellRow == dragCellRow + 1) &&
		(toIndentLevel == [fromCell indentLevel]))
	{
								// ...superimpose drag image in Cell button.
		imagePoint.y = cellFrame.origin.y -
			((cellFrame.size.height - dragImageSize.height) / 2.0);
	}
	else
	{							// calculate Y for image (on cell lower edge)
		imagePoint.y = cellFrame.origin.y + (dragImageSize.height / 2.0);

	}
								// instance-draw the node dragging image
	PSnewinstance();
	[dragImage composite:NX_SOVER toPoint:&imagePoint];

	return self;
}

- mouseUpAt:(const NXPoint *)upPoint forCell:(MiscTreeBrowserCell *)fromCell
/*"
Handles 'mouseUp' event; if dragging, switches off instance drawing.  Selects
cell clicked. If it was a double click, sends the DoubleAction... TBC
"*/
{
	NXEvent *theEvent = [NXApp currentEvent];

	if ([self _isDraggingCell])
	{							// zap drag image
		PSnewinstance();
		PSsetinstance(NO);
	}

	if (theEvent->data.mouse.click == 0)
	{							// drag'n'drop
		[self _dragToPoint:upPoint moveNode:YES];
	}
	else if (theEvent->data.mouse.click == 1)
	{							// single click
		NXRect cellFrame;
		int row, col;

		[self getRow:&row andCol:&col ofCell:fromCell];
		[self getCellFrame:&cellFrame at:row :col];
		[fromCell getOpenerRect:&cellFrame inView:self];
		cellFrame.origin.y -= cellFrame.size.height; // frame is flipped

		if ([self mouse:(NXPoint *)upPoint inRect:&cellFrame])
		{
			[self perform:@selector(_toggleCellOpen)
				  with:nil
				  afterDelay:1
				  cancelPrevious:YES];
		}
		[self _selectCell:fromCell];
	}
	else if (theEvent->data.mouse.click == 2)
	{							// double-click
		[self _selectCell:fromCell];
		[self sendDoubleAction];
	}

	[self _clearDragging];		// clean up dragging state (if any)

	return self;
}

- (BOOL)acceptsFirstResponder
/*"
Returns YES - matrix allows use of arrow keys etc.
"*/
{
	return YES;
}

- keyDown:(NXEvent *)theEvent
/*"
Does nothing, but eliminated "no responder" beeps
"*/
{
	return self;
}

- keyUp:(NXEvent *)theEvent
/*"
Handle keystrokes; ignore them if the user is editing a cell, otherwise:
Backspace - delete selected node, Return - create sibling to selected node,
Up/DownArrow - move selection up/down a cell. Left/RightArrow - move
out of/into node hierarchy.
"*/
{
	unsigned short keypress = theEvent->data.key.charCode;
	int sRow;
	id sCell;

	if (wasEditing)
	{							// ignore keystrokes whilst editing cell
		if (!editCell)
		{						// skip 'Return' KeyUp at end of edit
			wasEditing = NO;
		}
		return self;
	}

	switch (keypress)
	{
	case DELETE:
		
		[[self _treeBrowser] removeNodeAt:[self selectedRow]];
		break;

	case RETURN:
		{
			id scell = [self selectedCell];
			id node = [scell node];
			id newNode = [[[node class] alloc] init];
			id tb = [self _treeBrowser];
			id ncell;

			if ([scell isOpen] && ([node childCount] > 0))
			{
				[tb addNode:newNode toParent:node after:nil];
			}
			else
			{
				[tb addNode:newNode toParent:[node parent] after:node];
			}
			ncell = [self _cellForNode:newNode];
			[self _selectCell:ncell];
			[self editCell:ncell];
		}
		break;

	case UPARROW:
		// select next cell up, or last cell if at top or none selected
		sRow = [self selectedRow];
		sRow = ((sRow <= 0) ? numRows : sRow) - 1;
		[self _selectCellAt:sRow : 0];
		[self scrollCellToVisible:sRow :0];
		break;

	case DOWNARROW:
		// select next cell down, or first cell if at end or none selected
		sRow = [self selectedRow] + 1;
		sRow = (sRow >= numRows) ?  0 : sRow;
		[self _selectCellAt:sRow : 0];
		[self scrollCellToVisible:sRow :0];
		break;

	case LEFTARROW:
		// if cell is open, close it.  If it is not open, and selected cell is
		// not a root cell, select the parent cell
		sCell = [self selectedCell];
		if ([sCell isOpen])
		{
			[self _toggleCellOpen];
		}
		else
		{
			id parent = [[sCell node] parent];

			if (parent)
			{
				sRow = [self selectedRow] - 1;
				while ((sRow > 0) && ([[self cellAt:sRow :0] node] != parent))
				{
					sRow--;
				}
				[self _selectCellAt:sRow : 0];
				[self scrollCellToVisible:sRow :0];
			}
		}
		break;

	case RIGHTARROW:
		// if cell is not open, open it.  If it is open and has children,
		// select first child
		sCell = [self selectedCell];
		if ([sCell isOpen])
		{
			if (![sCell isLeaf])
			{
				sRow = [self selectedRow] + 1;
				[self _selectCellAt:sRow :0];
				[self scrollCellToVisible:sRow :0];
			}
		}
		else
		{
			[self _toggleCellOpen];
		}
		break;
	}

	return self;
}

// text (field editor) delegate methods

- textDidEnd:sender endChar:(unsigned short)reason
/*"
Ends editing in the editCell, removes it from the view and checks
whyEnd.  If Escape was pressed, abandon the edit, otherwise set the
selected cell stringValue to the editCell text.
"*/
{
	int row,  col;
	
	if (reason != ESCAPE)
	{
		[editCell setStringValue:[editField stringValue]];
	}
	[editField removeFromSuperview];

	[self getRow:&row andCol:&col ofCell:editCell];
	[editCell setEditing:NO];
	
	[self _selectCell:editCell];
	[self drawCellAt:(row - 1) :0];
	[self drawCellAt:(row + 1) :0];

	editCell = nil;
	[[self window] perform:@selector(makeFirstResponder:)
				   with:self
				   afterDelay:1
				   cancelPrevious:YES];

	return self;
}

- editCell:aCell
/*"
Start user editing of a cell; plonk the editcell on top of the aCell,
fill it with aCell title , select the text and make the edit cell
the first responder.  Returns self, or nil if there is some problem;
like aCell not existing.
"*/
{
	if ((aCell == [self selectedCell]) || ([self _selectCell:aCell] == aCell))
	{
		int row, col;
		NXRect textFrame;
		id editor = [self _editField];

		editCell = aCell;
		wasEditing = YES;

		[self getRow:&row andCol:&col ofCell:editCell];
		[self getCellFrame:&textFrame at:row :col];
		[editCell getTitleRect:&textFrame inView:self];
		[editCell setEditing:YES];
		[self drawCell:editCell];
		[editor setStringValue:[editCell stringValue]];
		textFrame.size.height += 5.0;
		textFrame.origin.y -= 3.0;
		textFrame.origin.x -= 2.0;
		[editor setFrame:&textFrame];
		[self addSubview:editor];

		[[self window] makeFirstResponder:editor];
		[editor display];
		[editor selectText:self];

		return self;
	}

	return nil;
}

- (BOOL)isEditing
/*"
Returns YES if the user is editing the title of a cell.
"*/
{
	return editCell ? YES : NO;
}

- endEditing
/*"
If the user is editing a cell, force an end to the editing
(accepting whatever changed have been made).  Return self.
"*/
{
	if ([self isEditing])
	{
		id editor = [[editField window] getFieldEditor:NO for:editField];
		id scell = [self selectedCell];

		[self textDidEnd:editor endChar:NX_RETURN];
		[self _selectCell:scell];
	}

	return self;
}

@end
