//
//  ICeCoffEETerminal.m
//  ICeCoffEE
//
//  Created by Nicholas Riley on Mon Jan 28 2002.
//  Copyright (c) 2002 Nicholas Riley. All rights reserved.
//

#import "ICeCoffEETerminal.h"
#import "ICeCoffEEScanner.h"
#import "ICeCoffEE.h"
#import <Carbon/Carbon.h>
#include <unistd.h>

static NSRange ICCF_zeroRange = { NSNotFound, 5 };

#define min(a,b)        (a < b ? a : b)
#define max(a,b)        (a > b ? a : b)

@interface TermStorage:NSObject
- (struct _FSelPt)selPt0;
- (struct _FSelPt)selPt1;
- (BOOL)hasSelection;
- (void)startSelectionAtLine:(unsigned int)row column:(unsigned short)column;
- (void)startSelectionAtLine:(unsigned int)row offset:(unsigned short)offset;
- (void)endSelectionAtLine:(unsigned int)row offset:(unsigned short)offset;
- (void)selectWhitespaceDelimitedTextAtLine:(unsigned int)line offset:(unsigned short)offset;
- (void)selectWordAtLine:(unsigned int)line offset:(unsigned short)offset;
- (NSString *)selectedString;
- (unsigned int)numberOfLines;
- (unsigned int)effectiveColumnsForLine:(unsigned int)line;
- (void)clearSelection;
- (BOOL)isSelected:(unsigned int)line :(unsigned int)column;
@end

@interface TermController:NSObject
- (TermStorage *)storage;
@end

@implementation ICECoffEETermSubviewSuper
// NSTextInput implementation
- (void)insertText:(id)aString {}
- (void)doCommandBySelector:(SEL)aSelector {}
- (void)setMarkedText:(id)aString selectedRange:(NSRange)selRange {}
- (void)unmarkText {}
- (BOOL)hasMarkedText { return NO; }
- (long)conversationIdentifier { return 0; }
- (NSAttributedString *)attributedSubstringFromRange:(NSRange)theRange { return nil; }
- (NSRange)markedRange { return ICCF_zeroRange; }
- (NSRange)selectedRange { return ICCF_zeroRange; }
- (NSRect)firstRectForCharacterRange:(NSRange)theRange { return NSZeroRect; }
- (unsigned int)characterIndexForPoint:(NSPoint)thePoint { return 0; }
- (NSArray*)validAttributesForMarkedText { return nil; }
// NSDraggingDestination
- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender { return NSDragOperationNone; }
// misc. other stuff
- (void)_optionClickEvent:(NSEvent *)event:(unsigned int)row:(unsigned short)column {}
- (void)setNeedsDisplay {}
// NSView methods (needed to avoid crash in super invocation)
- (void)mouseUp:(NSEvent *)e { [super mouseUp: e]; }
- (void)mouseDown:(NSEvent *)e { [super mouseDown: e]; }
@end

@interface ICeCoffEETerminalRange : NSObject
{
    TermStorage *storage;
    struct _FSelPt pt0;
    struct _FSelPt pt1;
    unsigned int width;
    unsigned int height;
}

+ (ICeCoffEETerminalRange *)rangeWithTerminal:(ICeCoffEETerminal *)terminal;
+ (ICeCoffEETerminalRange *)rangeWithTerminal:(ICeCoffEETerminal *)terminal pt0:(struct _FSelPt)selPt0 pt1:(struct _FSelPt)selPt1;

- (id)initWithTerminal:(ICeCoffEETerminal *)terminal;
- (id)initWithTerminal:(ICeCoffEETerminal *)terminal pt0:(struct _FSelPt)selPt0 pt1:(struct _FSelPt)selPt1;

- (struct _FSelPt)pt0;
- (struct _FSelPt)pt1;

- (NSString *)stringFromRange;

- (void)growBackwardByLength:(unsigned long)length;
- (void)growForwardByLength:(unsigned long)length;

- (void)shrinkBackByLength:(unsigned long)length;
- (void)shrinkFrontByLength:(unsigned long)length;

- (BOOL)rangeIsEmpty;

- (void)select;

@end

@implementation ICeCoffEETerminalRange

+ (ICeCoffEETerminalRange *)rangeWithTerminal:(ICeCoffEETerminal *)terminal;
{
    return [[[self alloc] initWithTerminal: terminal] autorelease];
}

+ (ICeCoffEETerminalRange *)rangeWithTerminal:(ICeCoffEETerminal *)terminal pt0:(struct _FSelPt)selPt0 pt1:(struct _FSelPt)selPt1;
{
    return [[[self alloc] initWithTerminal: terminal pt0: selPt0 pt1: selPt1] autorelease];
}

- (id)initWithTerminal:(ICeCoffEETerminal *)terminal;
{
    if ( (self = [self init]) != nil) {
        storage = [(TermController *)[terminal valueForKey: @"controller"] storage];
        pt0 = [storage selPt0];
        pt1 = [storage selPt1];
        width = [storage effectiveColumnsForLine: pt0.line];
        height = [storage numberOfLines];
    }
    return self;
}

- (id)initWithTerminal:(ICeCoffEETerminal *)terminal pt0:(struct _FSelPt)selPt0 pt1:(struct _FSelPt)selPt1;
{
    if ( (self = [self initWithTerminal: terminal]) != nil) {
        pt0 = selPt0;
        pt1 = selPt1;
    }
    return self;
}

- (struct _FSelPt)pt0;
{
    return pt0;
}

- (struct _FSelPt)pt1;
{
    return pt1;
}

- (NSString *)stringFromRange;
{
    struct _FSelPt oldPt0 = [storage selPt0], oldPt1 = [storage selPt1];
    NSString *str;

    [storage startSelectionAtLine: pt0.line offset: pt0.col];
    [storage endSelectionAtLine: pt1.line offset: pt1.col];

    str = [storage selectedString];

    [storage startSelectionAtLine: oldPt0.line offset: oldPt0.col];
    [storage endSelectionAtLine: oldPt1.line offset: oldPt1.col];

    return str;
}

- (struct _FSelPt)_moveBack:(struct _FSelPt)pt byLength:(unsigned long)length;
{
    unsigned int extraLines = length / width;
    if ((long)(pt.line - extraLines) < 0) {
        pt.line = 0;
        pt.col = 0;
    } else if (pt.line - extraLines == 0) {
        pt.line = 0;
        pt.col += width - (length % height);
        if (pt.col < width) pt.col = 0;
        else pt.col -= width;
    } else {
        pt.line -= extraLines;
        pt.col += width - (length % height);
        if (pt.col < width) pt.line--;
        else pt.col -= width;
    }
    return pt;
}

- (struct _FSelPt)_moveForward:(struct _FSelPt)pt byLength:(unsigned long)length;
{
    unsigned int extraLines = length / width;
    if (pt.line + extraLines >= height) {
        pt.line = height - 1;
        pt.col = width - 1;
    } else if (pt.line + extraLines == height - 1) {
        pt.line = height - 1;
        pt.col += length % height;
        if (pt.col >= width) pt.col = width - 1;
    } else {
        pt.line += extraLines;
        pt.col = pt.col + (length % height);
        if (pt.col > width) {
            pt.line++;
            pt.col -= width;
        }
    }
    return pt;
}

- (BOOL)rangeIsEmpty;
{
    return (pt1.line < pt0.line) || (pt1.line == pt0.line && pt1.col <= pt0.col);
}

- (void)growBackwardByLength:(unsigned long)length;
{
    pt0 = [self _moveBack: pt0 byLength: length];
}

- (void)growForwardByLength:(unsigned long)length;
{
    pt1 = [self _moveForward: pt1 byLength: length];
}

- (void)shrinkFrontByLength:(unsigned long)length;
{
    pt0 = [self _moveForward: pt0 byLength: length];
}

- (void)shrinkBackByLength:(unsigned long)length;
{
    pt1 = [self _moveBack: pt1 byLength: length];
}

- (void)select;
{
    [storage startSelectionAtLine: pt0.line offset: pt0.col];
    [storage endSelectionAtLine: pt1.line offset: pt1.col];
}

- (NSString *)description;
{
    return [NSString stringWithFormat: @"L%uC%hu - L%uC%hu: |%@|", pt0.line, pt0.col, pt1.line, pt1.col, [[self stringFromRange] stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]]];
}

@end

// TermSubview's NSTextInput implementation is essentially a noop: make one that does something
@implementation ICeCoffEETerminal // XXX normally in category (NSTextInput), but APE doesnÕt work if I do that

- (NSRange)selectedRange {
    TermStorage *storage = [(TermController *)[self valueForKey: @"controller"] storage];
    struct _FSelPt selPt0 = [storage selPt0], selPt1 = [storage selPt1];
    unsigned width = [storage effectiveColumnsForLine: selPt0.line];
    unsigned pt0 = selPt0.line * width + selPt0.col;
    ICLog(@"selPt0 %d x %d selPt1 %d x %d", selPt0.line, selPt0.col, selPt1.line, selPt1.col);
    return NSMakeRange(pt0, selPt1.line * width + selPt1.col - pt0);
}

// XXX string isn't padded: what we need is something like the ICeCoffEE 1.2.x method, adapted to Terminal 1.2, but we've chosen another way
- (NSAttributedString *)attributedSubstringFromRange:(NSRange)theRange;
{
    TermStorage *storage = [(TermController *)[self valueForKey: @"controller"] storage];
    struct _FSelPt oldPt0 = [storage selPt0], oldPt1 = [storage selPt1], realPt0, realPt1;
    unsigned pt1 = theRange.location + theRange.length;
    unsigned width = [storage effectiveColumnsForLine: 0];
    NSAttributedString *str;

    realPt0.line = theRange.location / width;
    realPt0.col = theRange.location % width;
    realPt1.line = pt1 / width;
    realPt1.col = pt1 % width;

    [storage startSelectionAtLine: realPt0.line offset: realPt0.col];
    [storage endSelectionAtLine: realPt1.line offset: realPt1.col];

    str = [[NSAttributedString alloc] initWithString: [storage selectedString]];

    NSAssert2([str length] == theRange.length, @"Substring has length %lu when we expected %lu", [str length], theRange.length);

    [storage startSelectionAtLine: oldPt0.line offset: oldPt0.col];
    [storage endSelectionAtLine: oldPt1.line offset: oldPt1.col];
    
    return [str autorelease];
}

static NSEvent *ICCF_downEvent;
static unsigned int ICCF_line;
static unsigned short ICCF_col;
static BOOL ICCF_optionClickRegistered;

- (void)setSelectedRange:(NSRange)charRange affinity:(NSSelectionAffinity)affinity stillSelecting:(BOOL)stillSelectingFlag;
{
    TermStorage *storage = [(TermController *)[self valueForKey: @"controller"] storage];
    unsigned width = [storage effectiveColumnsForLine: 0];
    unsigned pt1 = charRange.location + charRange.length;
    [storage startSelectionAtLine: charRange.location / width offset: charRange.location % width];
    [storage endSelectionAtLine: pt1 / width offset: pt1 % width];
    // [self refresh];
}

void ICCF_LaunchURLFromTerminal(ICeCoffEETerminal *self) {
    NSCharacterSet *urlLeftDelimiters = nil, *urlRightDelimiters = nil;

    ICeCoffEETerminalRange *termRange = nil, *selRange = nil;
    NSString *s;
    NSRange range, delimiterRange;

    NS_DURING

        TermStorage *storage = [(TermController *)[self valueForKey: @"controller"] storage];

        if ([storage hasSelection] && [storage isSelected: ICCF_line : ICCF_col]) {
            selRange = [ICeCoffEETerminalRange rangeWithTerminal: self];
        } else { // select something
	    // XXX test this next line, it may be what's causing a Terminal bug to exhibit itself
            [storage selectWordAtLine: ICCF_line offset: ICCF_col];
            selRange = [ICeCoffEETerminalRange rangeWithTerminal: self];
            NSCAssert(![selRange rangeIsEmpty], ICCF_LocalizedString(@"Sorry, ICeCoffEE was unable to find anything to select"));
        }

        // However, word selection does not capture even the approximate boundaries of a URL
        // (text to a space/line ending character); it'll stop at a period in the middle of a hostname.
        // So, we expand it as follows:

        ICCF_Delimiters(&urlLeftDelimiters, &urlRightDelimiters);

        termRange = [ICeCoffEETerminalRange rangeWithTerminal: self pt0: [selRange pt0] pt1: [selRange pt0]];
        [termRange growBackwardByLength: ICCF_MAX_URL_LEN]; // potentially too big
	
expandFront:
	s = [termRange stringFromRange];
        ICLog(@"front %@", termRange);
        delimiterRange = [s rangeOfCharacterFromSet: urlLeftDelimiters
                                            options: NSLiteralSearch | NSBackwardsSearch];
        if (delimiterRange.location == NSNotFound) {
            // extend to beginning of string (as much as possible)
            [selRange growBackwardByLength: [s length]];
        } else {
            NSCAssert(delimiterRange.length == 1, @"Internal error: delimiter matched range is not of length 1");
            [selRange growBackwardByLength: [s length] - delimiterRange.location - 1];
	    
	    // in url/(parens)stuff, handle clicking inside or after (parens).
	    if ([s characterAtIndex: delimiterRange.location] == '(') {
		s = [selRange stringFromRange];
		if ([s rangeOfString: @")"].location != NSNotFound ||
		    [s rangeOfCharacterFromSet: [NSCharacterSet characterSetWithCharactersInString: @"/."]].location == NSNotFound) {
		    [selRange growBackwardByLength: 1];
		    ICLog(@"expanding past (, now |%@|", selRange);
		    [termRange shrinkBackByLength: [[termRange stringFromRange] length] - delimiterRange.location];
		    goto expandFront;
		}
	    }
        }

        ICLog(@"parsed front %@", selRange);

        termRange = [ICeCoffEETerminalRange rangeWithTerminal: self pt0: [selRange pt1] pt1: [selRange pt1]];
        [termRange growForwardByLength: ICCF_MAX_URL_LEN]; // potentially too big
	
expandBack:
	s = [termRange stringFromRange];
        ICLog(@"back %@", termRange);
        delimiterRange = [s rangeOfCharacterFromSet: urlRightDelimiters
                                            options: NSLiteralSearch];
        if (delimiterRange.location == NSNotFound) {
            // extend to end of string
            [selRange growForwardByLength: [s length]];
        } else {
            NSCAssert(delimiterRange.length == 1, @"Internal error: delimiter matched range is not of length 1");
            [selRange growForwardByLength: delimiterRange.location];
	    
	    // grow URL past closing paren if we've seen an open paren
	    if ([s characterAtIndex: delimiterRange.location] == ')' &&
		[[selRange stringFromRange] rangeOfString: @"("].location != NSNotFound) {
		[selRange growForwardByLength: 1];
		ICLog(@"expanding past ), now |%@|", selRange);
		[termRange shrinkFrontByLength: delimiterRange.location + 1];
		goto expandBack;
	    }
        }

        ICCF_StartIC();

        s = [selRange stringFromRange];

        range = NSMakeRange(0, [s length]);

        ICCF_CheckRange(range);

        ICLog(@"parsed back %@", selRange);
        ICLog(@"range of string %@", NSStringFromRange(range));
        ICCF_ParseURL(s, &range);
        ICLog(@"parsed range %@ |%@|", NSStringFromRange(range), [s substringWithRange: range]);

        [selRange shrinkFrontByLength: range.location];
        [selRange shrinkBackByLength: [s length] - range.length - range.location];

        s = [selRange stringFromRange];
        ICLog(@"reconstituted URL %@", selRange);

        [selRange select];
        [self setNeedsDisplay];
        [[self superview] display];

        if (ICCF_LaunchURL(s, ICCF_KeyboardAction([NSApp currentEvent])) && ICCF_prefs.textBlinkEnabled) {
            int i;
            // Terminal flashes the selection one more time, so blink one fewer
            for (i = 1 ; i < ICCF_prefs.textBlinkCount ; i++) {
                [storage clearSelection];
                [self setNeedsDisplay];
                [[self superview] display];
                usleep(kICBlinkDelayUsecs);
                [selRange select];
                [self setNeedsDisplay];
                [[self superview] display];
                usleep(kICBlinkDelayUsecs);
            }
        }
    NS_HANDLER
        ICCF_HandleException(localException);
    NS_ENDHANDLER

    ICCF_StopIC();
}

- (void)_optionClickEvent:(NSEvent *)event:(unsigned int)row:(unsigned short)column;
{
    if (ICCF_downEvent != nil) {
        ICCF_line = row; // XXX are these lines or rows? check with wrapping
        ICCF_col = column;
        ICCF_optionClickRegistered = YES;
    } else {
        [super _optionClickEvent: event :row :column];
    }
}

- (void)mouseUp:(NSEvent *)upEvent;
{
    ICLog(@"ICeCoffEE Terminal up: %@", upEvent);
    [super mouseUp: upEvent];
    if (ICCF_downEvent != nil) {
        NSPoint downPt = [ICCF_downEvent locationInWindow];
        NSPoint upPt = [upEvent locationInWindow];
        [ICCF_downEvent release];
        ICCF_downEvent = nil;
        if (abs(downPt.x - upPt.x) <= kICHysteresisPixels && abs(downPt.y - upPt.y) <= kICHysteresisPixels) {
            if (ICCF_optionClickRegistered) {
                ICCF_optionClickRegistered = NO;
                ICLog(@"========= launching... %d x %d", ICCF_line, ICCF_col);
                ICCF_LaunchURLFromTerminal(self);
            }
        }
    }
}

- (void)mouseDown:(NSEvent *)downEvent;
{
    if (ICCF_enabled && ICCF_prefs.commandClickEnabled && ICCF_EventIsCommandMouseDown(downEvent)) {
        NSEvent *optionClickEvent = [NSEvent mouseEventWithType: NSLeftMouseDown location: [downEvent locationInWindow] modifierFlags: NSAlternateKeyMask timestamp: [downEvent timestamp] windowNumber: [downEvent windowNumber] context: [downEvent context] eventNumber: [downEvent eventNumber] clickCount: 1 pressure: 0];
        [ICCF_downEvent release];
        ICCF_downEvent = [downEvent retain];
        ICLog(@"ICeCoffEE Terminal constructed: %@", optionClickEvent);
        ICCF_optionClickRegistered = NO;
        [super mouseDown: optionClickEvent];
    } else {
        [super mouseDown: downEvent];
    }
}

// NSDraggingDestination
// -[TermSubview draggingUpdated:] just invokes draggingEntered...
// XXX Crashing on repeated drag snap-back can happen even without ICeCoffEE installed; don't bother to try to fix
- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender;
{
    if (!ICCF_prefs.terminalRequireOptionForSelfDrag || [sender draggingSource] != self || ([[NSApp currentEvent] modifierFlags] & NSAlternateKeyMask)) {
        [super draggingEntered: sender];
        // When doing non-self drags, this works around one bug in Terminal wherein the option key acts as a toggle, and it shouldn't (see Aqua HIG).  Unfortunately, this messes up drag feedback for self drags, but I don't know of any way to fix it.  Not that most Cocoa apps get it remotely right, anyway.
        return NSDragOperationCopy;
    }
    return NSDragOperationNone;
}

@end
