/*	This file is part of the XText package (version 0.8)
	Mike Dixon, April 1992
	
	Copyright (c) 1992 Xerox Corporation.  All rights reserved.

	Use and copying of this software and preparation of derivative works based
	upon this software are permitted.  This software is made available AS IS,
	and Xerox Corporation makes no warranty about the software or its
	performance.
*/

#import "XText.h"
#import <appkit/Application.h>
#import <appkit/ClipView.h>
#import <appkit/publicWraps.h>
#import <stdio.h>

/*	Some of this code is based on other emacs-like Text classes by
	Julie Zelenski, Lee Boynton, and Glen Diener.

	There's some ugly hair in here; the Text object is not very well
	designed to support this kind of stuff.  No doubt this will all be
	fixed by NextStep 9.0 ...
*/

/*	We use an undocumented (but very useful) method on ClipView; declare it
	here to stop the compiler from complaining.
*/

@interface ClipView(undocumented)
- _scrollTo:(NXPoint *)origin;
@end

@implementation XText

- initFrame:(const NXRect *)frameRect text:(const char *)theText
	alignment:(int)mode
{
	// i don't understand why the compiler whines without the (char *) here
	[super initFrame:frameRect text:(char *)theText alignment:mode];
	posHint = -1;
	xHintPos = -1;
	return self;
}

- goto:(int)pos end:(int)end mode:(int)mode
{
	int start;
	
	switch(mode) {

	case 0:		// move
		[self setSel:pos :pos];
		[self scrollSelToVisible];
		posHint = -1;
		break;

	case 1:		// delete
	case 2:		// cut
		if (pos != end) {
			start = pos;
			if (start > end)
				{ start = end; end = pos; }
			[self disableAutodisplay];
			[self setSel:start :end];
			if (mode == 1)
				[self delete:self];
			else
				[self cut:self];
		}
		posHint = -1;
		break;

	case 3:		// select
		start = pos;
		if (start > end)
			{ start = end; end = pos; }
		// The Text object can't even extend the selection without flashing,
		// unless we disable autodisplay
		if (sp0.cp != spN.cp)
			[self disableAutodisplay];
		[self setSel:start :end];
		posHint = pos;
		break;
	}
	xHintPos = -1;
	return self;
}

- moveChar:(int)cnt mode:(int)mode
{
	int pos, end;
	int max = [self textLength];
	
	if (sp0.cp == posHint) {
		pos = sp0.cp + cnt;
		end = spN.cp;
	} else {
		pos = spN.cp + cnt;
		end = sp0.cp;
	}
	if (pos < 0)
		pos = 0;
	else if (pos > max)
		pos = max;
	return [self goto:pos end:end mode:mode];
}

/* NXBGetc - a text stream macro to get the previous character. */

typedef struct {
    id text;
    NXTextBlock *block;
} textInfo;

static char getPrevious(NXStream *s)
{
    textInfo *info = (textInfo *) s->info;
    NXTextBlock *block = info->block->prior;

    if (!block)
		return EOF;
    s->buf_base = block->text;
    s->buf_ptr = s->buf_base + block->chars;
    s->offset -= block->chars;
    info->block = block;
    return *(--s->buf_ptr);
}

#define NXBGetc(s) \
    (((s)->buf_base == (s)->buf_ptr) ? getPrevious(s) : \
									   *(--((s)->buf_ptr)) )

- moveWord:(int)cnt mode:(int)mode
{
    NXStream *s = [self stream];
	char c;
	int i, pos, end;
	unsigned char digit_cat = charCategoryTable['0'];
	unsigned char alpha_cat = charCategoryTable['a'];
	unsigned char c_cat;
	BOOL inWord = NO;

	if (cnt == 0)
		return self;
	if (sp0.cp == posHint) {
		pos = sp0.cp;
		end = spN.cp;
	} else {
		pos = spN.cp;
		end = sp0.cp;
	}
	NXSeek(s, pos, NX_FROMSTART);
	i = (cnt<0 ? -cnt : cnt);
	while (1) {
		c = (cnt<0 ? NXBGetc(s) : NXGetc(s));
		if (c == EOF) break;
		c_cat = charCategoryTable[c];
		if (c_cat==alpha_cat || c_cat==digit_cat)
			inWord = YES;
		else if (inWord) {
			--i;
			if (i > 0)
				inWord = NO;
			else
				break;
		}
	}
	pos = NXTell(s);
	if (c != EOF)
		pos += (cnt<0 ? 1 : -1);
	return [self goto:pos end:end mode:mode];
}

/*  line is from an NXSelPt; returns the length of the line.
	too bad this requires grunging in Text's data structures */

#define LINE_LENGTH(line) \
	(self->theBreaks->breaks[(line)/sizeof(NXLineDesc)] & 0x3fff)

- moveLine:(int)cnt mode:(int)mode
{
	int pos, end, x, dir;

	if (sp0.cp == posHint) {
		pos = sp0.cp;
		end = spN.cp;
	} else {
		pos = spN.cp;
		end = sp0.cp;
	}
	if (mode != 0)
		[self disableAutodisplay];
	// collapse and normalize the selection
	[self setSel:pos :pos];
	x = (sp0.cp == xHintPos ? xHint : (sp0.cp - sp0.c1st));
	
	if (cnt < 0) {
		dir = NX_UP;
		cnt = -cnt;
	} else {
		dir = NX_DOWN;
	}
	for (; cnt > 0; --cnt)
		[self moveCaret: dir];

	pos = LINE_LENGTH(sp0.line)-1;
	if (x < pos)
		pos = x;
	pos += sp0.c1st;
	[self goto:pos end:end mode:mode];
	xHintPos = pos;
	xHint = x;
	return self;
}

- lineBegin:(int)mode
{
	int pos, end;

	if (sp0.cp == posHint) {
		pos = sp0.c1st;
		end = spN.cp;
	} else {
		pos = spN.c1st;
		// Text is inconsistent about what line it thinks we're on
		if (spN.cp == (spN.c1st + LINE_LENGTH(spN.line)))
			pos = spN.cp;
		end = sp0.cp;
	}
	return [self goto:pos end:end mode:mode];
}

- lineEnd:(int)mode
{
	NXSelPt *sp;
	int pos, end;

	if (sp0.cp == posHint) {
		sp = &sp0;
		end = spN.cp;
	} else {
		// need to correct for TBD
		sp = &spN;
		end = sp0.cp;
	}
	pos = sp->c1st + LINE_LENGTH(sp->line) - 1;
	if (pos < sp->cp) {
		// Text is being flakey again; we really want to be on the next line
		// this is pretty gross
		pos = sp->line;
		if (theBreaks->breaks[pos/sizeof(NXLineDesc)] < 0)
			pos += sizeof(NXHeightChange);
		else
			pos += sizeof(NXLineDesc);
		pos = sp->cp + LINE_LENGTH(pos) - 1;
	}
	if ((pos == sp->cp) && (mode != 0))
		++pos;
	return [self goto:pos end:end mode:mode];
}

- docBegin:(int)mode
{
	return [self goto:0
				 end:(sp0.cp == posHint ? spN.cp : sp0.cp)
				 mode:mode];
}

- docEnd:(int)mode
{
	return [self goto:[self textLength]
				 end:(sp0.cp == posHint ? spN.cp : sp0.cp)
				 mode:mode];
}

- collapseSel:(int)dir
{
	int pos;

	if ((dir < 0) || ((dir == 0) && (sp0.cp == posHint)))
		pos = sp0.cp;
	else
		pos = spN.cp;
	return [self goto:pos end:pos mode:0];
}

- transChars
{
	int pos = sp0.cp;
	char buf[2], temp;

	if (pos == spN.cp) {
		if (pos == (sp0.c1st + LINE_LENGTH(sp0.line) - 1))
			--pos;
		if (pos > 0)
			if ([self getSubstring:buf start:pos-1 length:2] == 2) {
				temp = buf[1]; buf[1] = buf[0]; buf[0] = temp;
				[self disableAutodisplay];
				[self setSel:pos-1 :pos+1];
				[self replaceSel:buf length:2];
				return self;
			}
	}
	NXBeep();
	return self;
}

- openLine
{
	int pos = sp0.cp;

	// don't do anything if there's a non-empty selection
	if (pos == spN.cp) {
		[self replaceSel:"\n"];
		[self setSel:pos :pos];
	} else
		NXBeep();
	return self;
}

- scroll:(int)pages :(int)lines
{
	NXRect r;

	// if our superview isn't a ClipView, we can't scroll
	if ([superview respondsTo:@selector(_scrollTo:)]) {
		[superview getBounds:&r];
		r.origin.y += pages*r.size.height + lines*[self lineHeight];
		[superview _scrollTo:&r.origin];
	} else
		NXBeep();
	return self;
}

- scrollIfRO:(int)pages :(int)lines
{
	if (![self isEditable])
		return [self scroll:pages :lines];
	else
		return nil;
}

- insertChar:(NXEvent *)event
{
	char c;

	c = event->data.key.charCode;
	[self replaceSel:&c length:1];
	return self;
}

- insertNextChar
{
	static id action = nil;

	if (!action)
		action = [[XTEventMsgAction allocFromZone:[NXApp zone]]
							initSel:@selector(insertChar:)];
	nextAction = action;
	return self;
}

@end

/*	A (not very elegant) table format for storing the initial emacs bindings.
	Unused args are indicated by the magic value 99.
*/

typedef struct {
	SEL   *sel;
	short arg1;
	short arg2;
	keyCode key;
} tbl_entry;

/*	For these and other key codes, refer to
	/NextLibrary/Documentation/NextDev/Summaries/06_KeyInfo/KeyInfo.rtfd
*/

const tbl_entry emacs_base[] = {
{&@selector(moveChar:mode:), -1,  0, 0x351},  // ctrl-b	   move back char
{&@selector(moveChar:mode:), -1,  1, 0x401},  // ctrl-h	   delete back char
{&@selector(moveChar:mode:), -1,  3, 0x353},  // ctrl-B	   select back char
{&@selector(moveChar:mode:),  1,  0, 0x3c1},  // ctrl-f	   move fwd char
{&@selector(moveChar:mode:),  1,  1, 0x3b1},  // ctrl-d	   delete fwd char
{&@selector(moveChar:mode:),  1,  3, 0x3c3},  // ctrl-F	   select fwd char
{&@selector(moveWord:mode:), -1,  0, 0x354},  // alt-b	   move back word
{&@selector(moveWord:mode:), -1,  1, 0x1b4},  // alt-del   delete back word
{0,							  0,  0, 0x404},  // alt-h			(ditto)
{&@selector(moveWord:mode:), -1,  3, 0x356},  // alt-B	   select back word
{&@selector(moveWord:mode:),  1,  0, 0x3c4},  // alt-f	   move fwd word
{&@selector(moveWord:mode:),  1,  1, 0x3b4},  // alt-d	   delete fwd word
{&@selector(moveWord:mode:),  1,  3, 0x3c6},  // alt-F	   select fwd word
{&@selector(moveLine:mode:), -1,  0, 0x081},  // ctrl-p	   move back line
{&@selector(moveLine:mode:), -1,  3, 0x083},  // ctrl-P	   select back line
{&@selector(moveLine:mode:),  1,  0, 0x371},  // ctrl-n	   move fwd line
{&@selector(moveLine:mode:),  1,  3, 0x373},  // ctrl-N	   select fwd line
{&@selector(lineBegin:),	  0, 99, 0x391},  // ctrl-a	   move to line begin
{&@selector(lineBegin:),	  3, 99, 0x393},  // ctrl-A	   select to line bgn
{&@selector(lineEnd:),		  0, 99, 0x441},  // ctrl-e	   move to line end
{&@selector(lineEnd:),		  1, 99, 0x3e1},  // ctrl-k	   delete to line end
{&@selector(lineEnd:),		  3, 99, 0x443},  // ctrl-E	   select to line end
{&@selector(docBegin:),		  0, 99, 0x2e6},  // alt-<	   move to doc begin
{&@selector(docEnd:),		  0, 99, 0x2f6},  // alt->	   move to doc begin
{&@selector(collapseSel:),	  0, 99, 0x381},  // ctrl-spc  collapse selection
{&@selector(transChars),	 99, 99, 0x481},  // ctrl-t    transpose chars
{&@selector(setNextAction:),  0, 99, 0x421},  // ctrl-q	   quote next key
{&@selector(insertNextChar), 99, 99, 0x425},  // ctrl-alt-q   really quote key
{&@selector(openLine),		 99, 99, 0x071},  // ctrl-o	   open line
{&@selector(scroll::),		  1, -1, 0x341},  // ctrl-v	   scroll fwd page
{0,							  0,  0, 0x0f6},  // alt-shft-down	(ditto)
{&@selector(scroll::),		 -1,  1, 0x344},  // alt-v	   scroll back page
{0,							  0,  0, 0x166},  // alt-shft-up	(ditto)
{&@selector(scroll::),		  0,  4, 0x343},  // ctrl-V	   scroll fwd 4 lines
{&@selector(scroll::),		  0, -4, 0x346},  // alt-V	   scroll back 4 lines
{&@selector(scroll::),	  -9999,  0, 0x165},  // alt-ctrl-up	scroll to start
{&@selector(scroll::),	   9999,  0, 0x0f5},  // alt-ctrl-down  scroll to end
{&@selector(scrollIfRO::),	  1, -1, 0x380},  // space	   scroll fwd pg if RO
{&@selector(scrollIfRO::),	 -1,  1, 0x1b0},  // del	   scroll back pg if RO
{&@selector(scrollIfRO::),	  0,  4, 0x382},  // shift-sp  scroll fwd 4 lines
{&@selector(scrollIfRO::),	  0, -4, 0x1b2},  // shift-del scroll back 4 lines
{&@selector(scrollSelToVisible),
							 99, 99, 0x2d1},  // ctrl-l	   scroll to selection
{0, 0, 0}
};

void initbase_emacs(actionTbl actions, NXZone *zone)
{
	keyCode i;
	const tbl_entry *e;
	XTAction *a = [XTAction undefinedAction];

	// make all non-command control & alt combinations invoke "unboundKey"
	for (i=0; i<KEY_CODES; i+=16) {
		actions[i+1] = actions[i+3] = actions[i+4] = actions[i+5]
			= actions[i+6] = actions[i+7] = a;
	}

	// ... except for ctrl-i (a handy substitute for tab)
	actions[6*16 + 1] = nil;

	// and then install the emacs key bindings
	for (e=emacs_base; (e->key != 0); ++e) {
		if (e->sel == 0) {}
			// same action as previous binding
		else if (e->arg1 == 99)
			a = [[XTMsg0Action allocFromZone:zone] initSel:*(e->sel)];
		else if (e->arg2 == 99)
			a = [[XTMsg1Action allocFromZone:zone]
					initSel:*(e->sel) arg:e->arg1];
		else
			a = [[XTMsg2Action allocFromZone:zone]
					initSel:*(e->sel) arg:e->arg1 arg:e->arg2];
		actions[e->key] = a;
	}
}
