/*
    EditorTextView.m

    Implementation of the EditorTextView class for the
    ProjectManager application.

    Copyright (C) 2005, 2006  Saso Kiselkov

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*/

#import "EditorTextView.h"

#import <Foundation/NSKeyedArchiver.h>
#import <Foundation/NSIndexSet.h>
#import <Foundation/NSString.h>
#import <Foundation/NSUserDefaults.h>

#import <AppKit/NSColor.h>
#import <AppKit/NSCursor.h>
#import <AppKit/NSEvent.h>
#import <AppKit/NSFont.h>
#import <AppKit/NSLayoutManager.h>
#import <AppKit/NSTextStorage.h>
#import <AppKit/NSWindow.h>
#import <AppKit/PSOperators.h>

#import <HighlighterKit/HighlighterKit.h>

#import <ctype.h>

#import "SourceEditorDocument.h"

// the maximum number of highlighted characters at the same time
#define MAX_HIGHLIGHTED_INDEXES 2

static inline float
my_abs(float aValue)
{
  if (aValue >= 0)
    {
      return aValue;
    }
  else
    {
      return -aValue;
    }
}

/**
 * Computes the indenting offset of the last line before the passed
 * start offset containg text in the passed string, e.g.
 *
 * ComputeIndentingOffset(@"  Hello World", 12) = 2
 * ComputeIndentingOffset(@"    Try this one out\n"
 *                        @"      ", 27) = 4
 *
 * @argument string The string in which to do the computation.
 * @argument start The start offset from which to start looking backwards.
 * @return The ammount of spaces the last line containing text is offset
 *      from it's start.
 */
static int ComputeIndentingOffset(NSString * string, unsigned int start)
{
  SEL sel = @selector(characterAtIndex:);
  unichar (* charAtIndex)(NSString *, SEL, unsigned int) =
    (unichar (*)(NSString *, SEL, unsigned int))
    [string methodForSelector: sel];
  unichar c;
  int firstCharOffset = -1;
  int offset;
  int startOffsetFromLineStart = -1;

  for (offset = start - 1; offset >= 0; offset--)
    {
      c = charAtIndex(string, sel, offset);

      if (c == '\n')
        {
          if (startOffsetFromLineStart < 0)
            {
              startOffsetFromLineStart = start - offset - 1;
            }

          if (firstCharOffset >= 0)
            {
              firstCharOffset = firstCharOffset - offset - 1;
              break;
            }
        }
      else if (!isspace(c))
        {
          firstCharOffset = offset;
        }
    }

  if (firstCharOffset >= 0)
    {
      // if the indenting of the current line is lower than the indenting
      // of the previous actual line, we return the lower indenting
      if (startOffsetFromLineStart >= 0 &&
        startOffsetFromLineStart < firstCharOffset)
        {
          return startOffsetFromLineStart;
        }
      // otherwise we return the actual indenting, so that any excess
      // space is trimmed and the lines are aligned according the last
      // indenting level
      else
        {
          return firstCharOffset;
        }
    }
   else
    {
      return 0;
    }
}

@interface EditorTextView (Private)

- (void) insertSpaceFillAlignedAtTabsOfSize: (unsigned int) tabSize;

- (void) updateDisplayOnInsertionPointCrosshairs;
- (void) updateDisplayOnGuide: (EditorGuide *) aGuide;

- (EditorGuide *) guideHitByPoint: (NSPoint) p;

@end

@implementation EditorTextView (Private)

/**
 * Makes the receiver insert as many spaces at the current insertion
 * location as are required to reach the nearest tab-character boundary.
 *
 * @argument tabSize Specifies how many spaces represent one tab.
 */
- (void) insertSpaceFillAlignedAtTabsOfSize: (unsigned int) tabSize
{
  char buf[tabSize];
  NSString * string = [self string];
  unsigned int lineLength;
  SEL sel = @selector(characterAtIndex:);
  unichar (* charAtIndex)(NSString*, SEL, unsigned int) =
    (unichar (*)(NSString*, SEL, unsigned int))
    [string methodForSelector: sel];
  int i;
  int skip;

  // computes the length of the current line
  for (i = [self selectedRange].location - 1, lineLength = 0;
       i >= 0;
       i--, lineLength++)
    {
      if (charAtIndex(string, sel, i) == '\n')
        {
          break;
        }
    }

  skip = tabSize - (lineLength % tabSize);
  if (skip == 0)
    {
      skip = tabSize;
    }

  memset(buf, ' ', skip);
  [super insertText: [NSString stringWithCString: buf length: skip]];
}

/**
 * Makes us perform -setNeedsDisplayInRect: on a minimal space that
 * only includes the insertion point crosshairs.
 */
- (void) updateDisplayOnInsertionPointCrosshairs
{
  NSRect visible = [self visibleRect];

  if (drawsColumnIndicator)
    {
      [self setNeedsDisplayInRect:
        NSMakeRect(NSMinX(_insertionPointRect), NSMinY(visible), 1,
        visible.size.height)];
    }

  [self setNeedsDisplayInRect:
    NSMakeRect(NSMinX(visible), NSMaxY(_insertionPointRect) + 1,
    visible.size.width, 1)];
}

/**
 * Makes us perform -setNeedsDisplayInRect: on a minimal space that
 * only includes the guide `aGuide'.
 */
- (void) updateDisplayOnGuide: (EditorGuide *) aGuide
{
  NSRect visible = [self visibleRect];
  float offset = [aGuide offset];

  if ([aGuide isHorizontal])
    {
      if (offset >= NSMinY(visible) && offset <= NSMaxY(visible))
        {
          [self setNeedsDisplayInRect:
            NSMakeRect(visible.origin.x, offset,
                       visible.size.width, 1)];
        }
    }
  else
    {
      if (offset >= NSMinX(visible) && offset <= NSMaxX(visible))
        {
          [self setNeedsDisplayInRect:
            NSMakeRect(offset, visible.origin.y,
                       1, visible.size.height)];
        }
    }
}

/**
 * Returns the guide hit by point `p'. If no guide is hit, `nil' is
 * returned instead.
 */
- (EditorGuide *) guideHitByPoint: (NSPoint) p
{
  NSEnumerator * e = [guides objectEnumerator];
  EditorGuide * guide;

  while ((guide = [e nextObject]) != nil)
    {
      float offset1 = [guide offset],
            offset2 = [guide isHorizontal] ? p.y : p.x;

      // we accept a vicinity of 2 points around the line
      if (my_abs(offset1 - offset2) <= 2)
        {
          return guide;
        }
    }

  return nil;
}

@end

@implementation EditorTextView

- (void) dealloc
{
  TEST_RELEASE(crosshairColor);

  TEST_RELEASE(highlighter);

  TEST_RELEASE(guides);

  [super dealloc];
}

- (void) awakeFromNib
{
  NSData * data;
  NSUserDefaults * df = [NSUserDefaults standardUserDefaults];

  drawCrosshairs = [df boolForKey: @"DrawCrosshairs"];
  if (drawCrosshairs)
    {
      if ((data = [df dataForKey: @"CrosshairColor"]) == nil ||
        (crosshairColor = [NSKeyedUnarchiver unarchiveObjectWithData:
        data]) == nil)
        {
          crosshairColor = [NSColor lightGrayColor];
        }
      [crosshairColor retain];
    }

  guides = [NSMutableArray new];
}

- (void) drawRect: (NSRect) r
{
  NSLayoutManager * layoutManager;
  NSTextContainer * textContainer;
  NSEnumerator * e;
  EditorGuide * guide;
  NSRange drawnRange;
  unsigned int highlightIndexes[MAX_HIGHLIGHTED_INDEXES];
  unsigned int i, nIndexes;

  if (drawingRecursionProtectionActive)
    {
      return;
    }

  drawingRecursionProtectionActive = YES;

  layoutManager = [self layoutManager];
  textContainer = [self textContainer];
  drawnRange = [layoutManager glyphRangeForBoundingRect: r
                                        inTextContainer: textContainer];

  [highlighter highlightRange: drawnRange];

  [super drawRect: r];

  // draw any guidelines
  e = [guides objectEnumerator];
  while ((guide = [e nextObject]) != nil)
    {
      [guide drawRect: r];
    }

  // then draw the crosshairs
  if (drawCrosshairs && !NSEqualRects(_insertionPointRect, NSZeroRect))
    {
      NSRect visible = [self visibleRect];

      [crosshairColor set];
      // reset dashing, as that might have been changed by the guidelines
      PSsetdash(NULL, 0, 0.0);

      if (drawsColumnIndicator)
        {
          PSmoveto(NSMinX(_insertionPointRect), NSMinY(visible));
          PSrlineto(0, NSMinY(_insertionPointRect) - NSMinY(visible));
          PSstroke();

          PSmoveto(NSMinX(_insertionPointRect), NSMaxY(_insertionPointRect));
          PSrlineto(0, NSMaxY(visible) - NSMaxY(_insertionPointRect));
          PSstroke();
        }

      PSmoveto(NSMinX(visible), NSMaxY(_insertionPointRect) + 1);
      PSrlineto(visible.size.width, 0);
      PSstroke();
    }

  // now draw the highlighted indexes
  nIndexes = [[editorDocument highlightedCharacterIndexes]
      getIndexes: highlightIndexes
        maxCount: MAX_HIGHLIGHTED_INDEXES
    inIndexRange: NULL];
  [[editorDocument highlightColor] set];
  if (nIndexes > 0)
    {
      NSRange r;
      NSRect boundingRect;

      r = NSMakeRange (highlightIndexes[0], 1);
      boundingRect = [layoutManager
        boundingRectForGlyphRange: r inTextContainer: textContainer];
      // widen the line
      PSsetlinewidth (2.0);
      PSmoveto (NSMidX (boundingRect), NSMinY (boundingRect));
      PSrlineto (-NSWidth (boundingRect) / 2, 0);
      PSrlineto (0, NSHeight (boundingRect));
      PSrlineto (NSWidth (boundingRect) / 2, 0);
      PSstroke ();
    }

  if (nIndexes > 1)
    {
      NSRange r;
      NSRect boundingRect;

      r = NSMakeRange (highlightIndexes[1], 1);
      boundingRect = [layoutManager
        boundingRectForGlyphRange: r inTextContainer: textContainer];
      PSmoveto (NSMidX (boundingRect), NSMinY (boundingRect));
      PSrlineto (NSWidth (boundingRect) / 2, 0);
      PSrlineto (0, NSHeight (boundingRect));
      PSrlineto (-NSWidth (boundingRect) / 2, 0);
      PSstroke ();
    }

  drawingRecursionProtectionActive = NO;
}

- (void) createSyntaxHighlighterForFileType: (NSString *) fileType
{
  ASSIGN (highlighter, [[[HKSyntaxHighlighter alloc]
    initWithHighlighterType: fileType
                textStorage: [self textStorage]
           defaultTextColor: [editorDocument textColor]]
    autorelease]);
}

- (void) insertText: text
{
  if ([text isKindOfClass: [NSString class]])
    {
      NSString * string = text;

      if ([string isEqualToString: @"\n"])
        {
          if ([[NSUserDefaults standardUserDefaults]
            boolForKey: @"ReturnDoesAutoindent"])
            {
              int offset = ComputeIndentingOffset([self string],
                [self selectedRange].location);
              char * buf;

              buf = (char *) malloc((offset + 2) * sizeof(unichar));
              buf[0] = '\n';
              memset(&buf[1], ' ', offset);
              buf[offset+1] = '\0';

              [super insertText: [NSString stringWithCString: buf]];
              free(buf);
            }
          else
            {
              [super insertText: text];
            }
        }
      else if ([string isEqualToString: @"\t"])
        {
          switch ([[NSUserDefaults standardUserDefaults]
            integerForKey: @"TabConversion"])
            {
            case 0:  // no conversion
              [super insertText: text];
              break;
            case 1:  // 2 spaces
              [super insertText: @"  "];
              break;
            case 2:  // 4 spaces
              [super insertText: @"    "];
              break;
            case 3:  // 8 spaces
              [super insertText: @"        "];
              break;
            case 4:  // aligned to tab boundaries of 2 spaces long tabs
              [self insertSpaceFillAlignedAtTabsOfSize: 2];
              break;
            case 5:  // aligned to tab boundaries of 4 spaces long tabs
              [self insertSpaceFillAlignedAtTabsOfSize: 4];
              break;
            case 6:  // aligned to tab boundaries of 8 spaces long tabs
              [self insertSpaceFillAlignedAtTabsOfSize: 8];
              break; 
            }
        }
      else
        {
          [super insertText: text];
        }
    }
  else
    {
      [super insertText: text];
    }
}

/* This extra change tracking is required in order to inform the document
 * that the text is changing _before_ it actually changes. This is required
 * so that the document can un-highlight any highlit characters before the
 * change occurs and after the change recompute any new highlighting.
 */
- (void) keyDown: (NSEvent *) ev
{
  if (drawCrosshairs)
    [self updateDisplayOnInsertionPointCrosshairs];

  [editorDocument editorTextViewWillPressKey: self];

  // allow the user to insert the tab character directly when holding
  // down the Shift key
  if ([[ev characters] isEqualToString: @"\t"] &&
      [ev modifierFlags] & NSShiftKeyMask)
    {
      [super insertText: @"\t"];
    }
  // otherwise take the standard path
  else
    {
      [super keyDown: ev];
    }

  [editorDocument editorTextViewDidPressKey: self];

  if (drawCrosshairs)
    [self updateDisplayOnInsertionPointCrosshairs];
}

- (void) paste: sender
{
  if (drawCrosshairs)
    [self updateDisplayOnInsertionPointCrosshairs];

  [editorDocument editorTextViewWillPressKey: self];
  [super paste: sender];
  [editorDocument editorTextViewDidPressKey: self];

  if (drawCrosshairs)
    [self updateDisplayOnInsertionPointCrosshairs];
}

- (void) mouseDown: (NSEvent *) ev
{
  EditorGuide * guide;

  guide = [self guideHitByPoint: [self convertPoint: [ev locationInWindow]
                                           fromView: nil]];
  if (guide != nil)
    {
      [self beginDraggingGuide: guide];
    }
  else
    {
      if (drawCrosshairs)
        [self updateDisplayOnInsertionPointCrosshairs];

      [super mouseDown: ev];

      if (drawCrosshairs)
        [self updateDisplayOnInsertionPointCrosshairs];
    }
}

- (void) setDrawsColumnIndicationGuideline: (BOOL) flag
{
  drawsColumnIndicator = flag;
  [self updateDisplayOnInsertionPointCrosshairs];
}

- (BOOL) drawsColumnIndicationGuideline
{
  return drawsColumnIndicator;
}

- (NSRect) selectionRect
{
  return _insertionPointRect;
}

/**
 * Creates a new guide and start dragging it. If `aStyle' isn't
 * -1, the guide's style is set to aStyle.
 */
- (void) createAndBeginDraggingNewGuide: (BOOL) isHorizontal
                              withStyle: (EditorGuideStyle) aStyle
{
  EditorGuide * guide;

  guide = [[EditorGuide new] autorelease];

  [guide setHorizontal: isHorizontal];

  if (aStyle != AnyGuideStyle)
    {
      [guide setGuideStyle: aStyle];
    }

  [guides addObject: guide];

  [self beginDraggingGuide: guide];
}

/**
 * Invoked when a guide is to be dragged.
 */
- (void) beginDraggingGuide: (EditorGuide *) aGuide
{
  NSEvent * ev;
  BOOL isHorizontal = [aGuide isHorizontal];
  NSCursor * cursor;

  // change the cursor
  if (isHorizontal)
    {
      cursor = [NSCursor resizeUpDownCursor];
    }
  else
    {
      cursor = [NSCursor resizeLeftRightCursor];
    }
  [cursor push];

  [aGuide setSelected: YES];
  [self updateDisplayOnGuide: aGuide];

  // start tracking the mouse
  while ([(ev = [[self window] nextEventMatchingMask: NSAnyEventMask])
    type] != NSLeftMouseUp)
    {
      if ([ev type] == NSLeftMouseDragged)
        {
          NSPoint p = [self convertPoint: [ev locationInWindow] fromView: nil];

          // now update the guide's position
          if (isHorizontal)
            {
              [self updateDisplayOnGuide: aGuide];
              [aGuide setOffset: p.y];
              [self updateDisplayOnGuide: aGuide];
            }
          else
            {
              [self updateDisplayOnGuide: aGuide];
              [aGuide setOffset: p.x];
              [self updateDisplayOnGuide: aGuide];
            }
        }
    }

  [cursor pop];


  [aGuide setSelected: NO];
  [self updateDisplayOnGuide: aGuide];

  {
    NSRect visible = [self visibleRect];
    float offset = [aGuide offset];

    if ([aGuide isHorizontal])
      {
        if (offset < NSMinY(visible) ||
            offset > NSMaxY(visible))
          {
            [guides removeObject: aGuide];
          }
      }
    else
      {
        if (offset < NSMinX(visible) ||
            offset > NSMaxX(visible))
          {
            [guides removeObject: aGuide];
          }
      }
  }
}

/**
 * Makes the text view mark it's highlighted character positions as
 * needing redisplay.
 */
- (void) setNeedsDisplayInHighlightedCharacters: (BOOL) flag
{
  if (flag)
    {
      unsigned int highlightIndexes[MAX_HIGHLIGHTED_INDEXES];
      unsigned int i, nIndexes;
      NSLayoutManager * layoutManager = [self layoutManager];
      NSTextContainer * textContainer = [self textContainer];

      nIndexes = [[editorDocument highlightedCharacterIndexes]
          getIndexes: highlightIndexes
            maxCount: MAX_HIGHLIGHTED_INDEXES
        inIndexRange: NULL];
      for (i = 0; i < nIndexes; i++)
        {
          NSRange r = NSMakeRange (highlightIndexes[i], 1);
          NSRect boundingRect = NSInsetRect ([layoutManager
            boundingRectForGlyphRange: r inTextContainer: textContainer], -1, -1);

          [self setNeedsDisplayInRect: boundingRect];
        } 
    }
}

@end
