/**
 ** EmacsText.m
 **
 ** A text subclass which provides emacs key bindings
 ** and "colored" program text. 
 ** 
 **  Modified by sl to be an emacs text only
 **/

#import "MiscEmacsText.h"

#import <stdlib.h>
#import <strings.h>
#import <appkit/appkit.h>
#import <defaults/defaults.h>
#import <libc.h>
#import <ctype.h>


#define ESC    (27)
#define CTRL_SPACE (0)
#define CTRL_A (1)
#define CTRL_B (2)
#define CTRL_D (4)
#define CTRL_E (5)
#define CTRL_K (11)
#define CTRL_L (12)
#define CTRL_N (14)
#define CTRL_O (15)
#define CTRL_P (16)
#define CTRL_S (19)
#define CTRL_T (20)
#define CTRL_V (22)
#define CTRL_W (23)
#define CTRL_Y (25)


/**
 ** Fragments of the following procedures were borrowed from
 ** Lee boynton's LispExample program, then hacked to bits.
 **/

static char killedText[10000] ; // buffer for "killed" text...
// since it is statically allocated, this thing could blow up
// if > 10000 chars are written to the kill buffer.  If I had
// I'd make this grow as needed....

static BOOL useEmacsBindings = 1 ; 

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

BOOL evalGets(NXStream *aStream, char * aBuf)
{ // Procedure to get the next line (or remainder of
  // stream) from a stream. The newline is discarded.  
  // If newline is present, returns YES, else if instead
  // the end of stream is found, returns NO (but aBuf may
  // still contain data).
  int c,i = 0;
  c = NXGetc(aStream) ;
  do
  { if(c == -1) // end of stream
    { aBuf[i] = '\0' ;
      return NO ;
    }
    else if(c == '\n')
    { aBuf[i] = '\0' ;
      return YES ;
    }
    else
    { aBuf[i++] = (char) c ;
      c = NXGetc(aStream) ;
    } 
  } while(1) ;
}

static char getPrevious(NXStream *s)
{ // NXBGetc - a text stream macro to get the previous character.
  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)) )


int findMatchingOpenParen(id text, char thisChar, char thatChar, int curPos)
{ int count = 1;
  char ch;
  NXStream *s = [text stream];
  NXSeek(s, curPos, NX_FROMSTART);
  while ((ch = NXBGetc(s)) != EOF)
  { if(ch == thatChar && !(--count))
      return NXTell(s);
    else if (ch == thisChar)
      count++;
  }
  return -1;
}


int
findMatchingCloseParen(id text, char thisChar, char thatChar, int curPos)
{ int count = 1;
  char ch;
  NXStream *s = [text stream];
  NXSeek(s, curPos, NX_FROMSTART);
  while ((ch = NXGetc(s)) != EOF)
  { if(ch == thatChar && !(--count))
      return NXTell(s);
    else if (ch == thisChar)
      count++;
  }
  return -1;
}

void highlightChar(id text, int charPosition)
{ NXSelPt selStart, selEnd;
  [text getSel:&selStart :&selEnd];
  [text setSel:charPosition :charPosition+1];
  [[text window] flushWindow];
  NXPing();
  [[text window] disableFlushWindow];
  [text setSel:selStart.cp :selEnd.cp];
  [[text window] reenableFlushWindow];
}


@implementation MiscEmacsText:Text


- awakeFromNib ;
{ // Do some setup work...I am my own delegate
  // [self setDelegate: self] ;

	NXAtom textTypes[5];
	
	textTypes[0] = NXRTFPboardType;
	textTypes[1] = NXAsciiPboardType;
	textTypes[2] = NULL;
  
	[self registerForDraggedTypes:textTypes count:2];
	return self;
}


#define MARK_UNSET -1

- keyDown:(NXEvent *)theEvent
{ // provide some emacs-style key bindings. We make
  int i ;
  static unsigned short val = '\0', lastChar = '\0';
  static BOOL firstKey = TRUE, insertChar;
  static long mark = MARK_UNSET ; 
  NXSelPt selStart, selEnd;

  if(!useEmacsBindings)
    return [super keyDown: theEvent] ;

  if(firstKey) // make sure killedText is empty
  {
		killedText[0] = '\0' ;
    firstKey = FALSE ;
  }
  [self getSel:&selStart :&selEnd];

	lastChar = val;        // val was set in "previous" call of keyDown..
	val = theEvent->data.key.charCode + (theEvent->data.key.charSet << 8);
	insertChar = YES ;
	switch ((unsigned char) val)
	{
	case 0xa3:  // ALT-<
		{ [self setSel: 0 :0] ;
			[self scrollSelToVisible] ;
			insertChar = NO ;
		}
		break ;
	case 0xb3: // ALT->
		{ i = [self textLength] ; --i ;
			[self setSel: i :i] ;
			[self scrollSelToVisible] ;
			insertChar = NO ;   
		}
		break ;
	case 0xd6: // ALT-v
		{ // move up one screenful
			NXPoint scrollPoint = {0.0,0.0} ;
			NXRect  clipRect ;
			[self convertPoint: &scrollPoint fromView: [superview superview]] ;
			// get the clipview's bounds
			[superview getBounds: &clipRect] ;
			scrollPoint.y -= clipRect.size.height ; // scroll up
			[self scrollPoint: &scrollPoint] ;  
			insertChar = NO ;
		}
		break ;    
	case 0xc8: // ALT-w
		{ int subStringLength ;
			if(mark == MARK_UNSET)
				break ; 
			[self getSel:&selStart :&selEnd];
			if(selStart.cp < mark)
				[self getSubstring: killedText
				start: selStart.cp length: subStringLength = mark - selStart.cp] ;
			else
				[self getSubstring: killedText
				start: mark length: subStringLength = selStart.cp - mark] ;
			killedText[subStringLength] = '\0' ;
			insertChar = NO ;
		}
		break ;
	case CTRL_SPACE: // set mark
		[self getSel:&selStart :&selEnd];
		mark = selStart.cp ;
		insertChar = NO ;
		break ;
	case CTRL_A: // beginning of line
		[self getSel:&selStart :&selEnd];
		i = [self lineFromPosition:selStart.cp] ;
		i = [self positionFromLine: i] ;
		[self setSel:i :i];
		insertChar = NO ;
		break ;
	case CTRL_E: // end of line
		[self getSel:&selStart :&selEnd];
		i = [self lineFromPosition: selStart.cp] ;
		i = [self positionFromLine: i + 1] ; // beginning of next line
		if(i == -1) // then there is no next line...
			i = [self textLength] + 1 ;
		[self setSel:i -1 :i - 1];
		insertChar = NO ;
		break ;
	case CTRL_B:// backwards one char
		[self getSel:&selStart :&selEnd];
		if(selStart.cp > 1)
			[self setSel:selStart.cp - 1 :selStart.cp -1];
		else
			NXBeep();
		insertChar = NO ;
		break ;
	case CTRL_D: // delete char under cursor
		[self getSel:&selStart :&selEnd];
		[self setSel:selEnd.cp :selEnd.cp+1];
		[self replaceSel:""];
		[self setSel:selStart.cp :selStart.cp] ;
		insertChar = NO ;
		break ;
	case CTRL_K:
	// clear to end of line, place killed text in killedText 
	// if line is empty, then delete the newline
	{ int currentEnd = 0 ;
		[self getSel:&selStart :&selEnd];
		i = [self lineFromPosition: selStart.cp] ;
		i = [self positionFromLine: i + 1];
		// i is index of beginning of next line
		
		if(i == -1) // then there is no next line...
		{ i = [self textLength] ;
		}
		else // there is another line
		{ if(i - selStart.cp > 1) // if line is not empty,
			i-- ;  // don't include the newline.
		}
		// successive CTRL_K's append to killedText ; any other char
		// interrupts the sequence
		if(lastChar == CTRL_K)
			currentEnd = strlen(killedText) ;
		[self getSubstring:  &killedText[currentEnd]
			start:selStart.cp length:i- selStart.cp] ;
		killedText[i - selStart.cp + currentEnd] = '\0' ;
		[self setSel:selStart.cp :i];
		[self replaceSel:""];
		[self setSel:selStart.cp :selStart.cp] ;
		insertChar = NO ;
		break ;
	}
	case CTRL_O: // "open" a line...i.e. add blank line before current
		[self getSel:&selStart :&selEnd];
		[self setSel: selStart.cp :selStart.cp] ;
		[self replaceSel: "\n"] ;            
		[self setSel: selStart.cp :selStart.cp] ;
		insertChar = NO ;
		break ;
	case CTRL_N: // move directly up or down one line
	case CTRL_P: // move directly up one line...fake up arrow
		{ NXEvent fakeEvent ;
			bcopy((char *) theEvent,(char *) &fakeEvent,sizeof(NXEvent)) ;
			fakeEvent.data.key.charCode = val == CTRL_N ? 175 :173 ;
			fakeEvent.data.key.charSet = 1 ;
			fakeEvent.flags = NX_NUMERICPADMASK ;
			[super keyDown: &fakeEvent] ;
			insertChar = NO ;
			}
		break ;
	case CTRL_V: // move down one screenful
		{  NXPoint scrollPoint = {0.0,0.0} ;
			NXRect  clipRect ;

			[self convertPoint: &scrollPoint fromView: [superview superview]] ;
			// get the clipview's bounds
			[superview getBounds: &clipRect] ;
			scrollPoint.y += clipRect.size.height ; // scroll down
			[self scrollPoint: &scrollPoint] ;  
			insertChar = NO ;
			break ;    
		}
	case CTRL_W: // delete text from point to mark ; add to killedText
		if(mark == MARK_UNSET)
			break ;
		[self getSel:&selStart :&selEnd];
		if(selStart.cp < mark)
		{  [self getSubstring: killedText
				start: selStart.cp length:mark - selStart.cp] ;
				[self setSel: selStart.cp :mark] ;
		}
		else
		{ [self getSubstring: killedText
				start: mark length: selStart.cp - mark] ;
			[self setSel: mark :selStart.cp] ;
		} 
		[self replaceSel: ""] ;          
		insertChar = NO ;
		break ;
	case CTRL_Y: // yank killedText 
		[self getSel:&selStart :&selEnd];
		[self setSel:selStart.cp :selStart.cp] ;
		[self replaceSel: killedText] ;
		insertChar = NO ;
		break ;
	default:
		break;
	}
	if(insertChar)
		[super keyDown: theEvent] ;
  return self ;
}


- mouseDown: (NXEvent *) anEvent ;
{ // augment double-click behavior to
  // check for bracketed structures and quoted
  // items. augment command-click behaviour to
  // select between backquotes.
  NXSelPt start, end ;
  char openers[] = "{([<\"'/`" ;
  char closers[] = "})]>\"'/`" ;
  char c,d, *place ;
  int otherPos ;
  NXStream *aStream ;
  if(anEvent->data.mouse.click == 1)
    [super mouseDown: anEvent] ;
  else if(anEvent->data.mouse.click == 2)
  { aStream = [self stream] ;
    [self getSel: &start :&end] ;
    if(start.cp > 0)
    { NXSeek(aStream,start.cp - 1,NX_FROMSTART) ;
      c = NXGetc(aStream) ;
      if(place = index(closers,c)) // we have right side of a structure
      {  d = openers[(int) place - (int) closers] ;// grab matching char
         if( (otherPos = findMatchingOpenParen(self, c, d, start.cp -1))
           != -1)
        { if(c == '`') // put selection "inside" ` ... ` pair
            [self setSel: otherPos + 1 :start.cp - 1] ;
          else
            [self setSel: otherPos :start.cp] ;
          return self ;
        }
        else
          [super mouseDown: anEvent] ;
      }
      else
      { NXSeek(aStream,start.cp,NX_FROMSTART) ;
        c = NXGetc(aStream) ;
        if(place = index(openers,c)) // we have left side of a structure
        { d = closers[(int) place - (int) openers] ;
          if( (otherPos = findMatchingCloseParen(self, c, d, start.cp +1))
           != -1)
          { if(c == '`') // put selection "inside" ` ... ` pair
             [self setSel: start.cp +1 :otherPos-1] ;
           else
             [self setSel: start.cp :otherPos] ;
          }
        }
        else
          [super mouseDown: anEvent] ;
      }
    }
  }
  else // deal with triple, quadruple, etc.  clicks here...
    [super mouseDown: anEvent] ; 
  if((anEvent->data.mouse.click == 1) &&
     (anEvent->flags & NX_COMMANDMASK))
  { // command click == search back for backquote (or beginning of text)
    // forward for backquote (or end ot text), then select. 
    int from, to ;
    aStream = [self stream] ;
    [self getSel: &start :&end] ;
    if(start.cp > 0)
    { from = start.cp - 1 ;
      NXSeek(aStream,from,NX_FROMSTART) ;
      while(from >= 1 && ((c = NXBGetc(aStream)) != '`'))
        from-- ;
      to = start.cp ;
      NXSeek(aStream,to,NX_FROMSTART) ;
      while(!NXAtEOS(aStream) && ((c = NXGetc(aStream)) != '`'))
        to++ ;
      [self setSel: from :to] ;
    }
  }
  return self ;
}

/*
- textDidGetKeys: sender isEmpty: (BOOL) flag
{ [window setDocEdited: YES] ;     
  return self ;
}
*/

- useEmacsBindings: (BOOL) YESorNO ;
{ useEmacsBindings = YESorNO ;
  return self ;
}

@end
