/*
    SyntaxHighlighter.m

    Implementation of the SyntaxHighlighter class for the
    ProjectManager application.

    Copyright (C) 2005  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 "SyntaxHighlighter.h"

#import <Foundation/NSBundle.h>
#import <Foundation/NSString.h>
#import <Foundation/NSArray.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSNotification.h>
#import <Foundation/NSDebug.h>
#import <Foundation/NSAutoreleasePool.h>
#import <Foundation/NSCharacterSet.h>
#import <Foundation/NSValue.h>
#import <Foundation/NSNull.h>
#import <Foundation/NSException.h>
#import <Foundation/NSLock.h>

#import <AppKit/NSTextStorage.h>
#import <AppKit/NSAttributedString.h>

#import "PCEditorView.h"
#import "SyntaxDefinition.h"

static NSString * const KeywordsNotFixedAttributeName = @"KNF";
static NSString * const ContextAttributeName = @"C";

static inline BOOL
my_isspace(unichar c)
{
  if (c == ' ' || c == '\t' || c == '\f')
    {
      return YES;
    }
  else
    {
      return NO;
    }
}

/**
 * This function looks ahead and after `startRange' in `string' and
 * tries to return the range of a whitespace delimited word at the
 * specified range. E.g. string = @"abc def ghi" and startRange = {5, 1},
 * then {4, 3} is returned, because the word "def" lies within the range.
 * Please note that even when the range points to a whitespace area
 * (e.g. string = @"abc def" and startRange = {3, 1}), the lookup
 * will occur and not return `not found' (e.g. in the above example it
 * would return {0, 7}). When the range is also surrounded by whitespace
 * (e.g. @"   " and startRange = {1, 1}) the startRange itself is returned.
 */
static NSRange
RangeOfWordInString(NSString * string, NSRange startRange)
{
  SEL sel = @selector(characterAtIndex:);
  unichar (*characterAtIndex)(id, SEL, unsigned int) = 
    (unichar (*)(id, SEL, unsigned int)) [string methodForSelector: sel];
  NSInteger ahead, after;
  NSUInteger length = [string length];

  for (ahead = 1; ahead <= (NSInteger) startRange.location; ahead++)
    {
      if (my_isspace(characterAtIndex(string,
                                      sel,
                                      startRange.location - ahead)))
        {
          break;
        }
    }
  ahead--;

  for (after = 0; (after + NSMaxRange(startRange)) < length; after++)
    {
      if (my_isspace(characterAtIndex(string,
                                      sel,
                                      (after + NSMaxRange(startRange)))))
        {
          break;
        }
    }

  {
    NSUInteger start = startRange.location - ahead;
    NSUInteger length = startRange.length + ahead + after;

    if (start > 0)
      {
        start--;
        length++;
      }
    if (length + 1 < length)
      {
        length++;
      }

    return NSMakeRange(start, length);
  }
}

static inline BOOL
LocateString(NSString * str,
             unichar * buf,
             NSUInteger length,
             NSUInteger offset)
{
  NSUInteger i, n;

  for (i = 0, n = [str length]; i < n; i++)
    {
      if (i >= length)
        {
          return NO;
        }

      if (buf[i + offset] != [str characterAtIndex: i])
        {
          return NO;
        }
    }

  return YES;
}

@interface SyntaxHighlighter (Private)

- (void) fixUpContextsInRange: (NSRange) r;

- (void) fixUpKeywordsInRange: (NSRange) r;
- (void) lazilyFixUpKeywordsInRange: (NSRange) r;

- (void) assignGraphicalAttributesOfContext: (NSUInteger) context
                                    toRange: (NSRange) r;

- (void) assignGraphicalAttributesOfKeyword: (unsigned int) keyword
                                  inContext: (NSUInteger) context
                                    toRange: (NSRange) r;

- (NSUInteger) contextBeforeRange: (NSRange) r;
- (NSUInteger) contextAfterRange: (NSRange) r;
- (NSUInteger) contextAtEndOfRange: (NSRange) r;

- (void) beginEditingIfNeeded;
- (void) endEditingIfNeeded;

@end

@implementation SyntaxHighlighter (Private)

/**
 * Fixes up the contexts inside the text storage in range `r'. A context
 * is recognized by the "Context" attribute which holds the number of
 * the context. This method also applies graphical attributes of the
 * corresponding contexts to the context ranges.
 */
- (void) fixUpContextsInRange: (NSRange) r
{
  TextPattern ** beginnings = [syntax contextBeginnings];
  const char * beginningChars = [syntax contextBeginningCharacters];
  unsigned numBeginningChars = [syntax numberOfContextBeginningCharacters];

  NSUInteger i;
  unichar * string;
  unsigned int context;

  string = (unichar *) malloc(r.length * sizeof(unichar));
  [[textStorage string] getCharacters: string range: r];

  i = 0;
  context = [self contextBeforeRange: r];
  while (i < r.length)
    {
       // marks the beginning of the currently processed range
      unsigned int mark = i;

      // default context - look for beginning symbols
      if (context == 0)
        {
          unsigned int j = 0;
          TextPattern * pattern = NULL;
          NSRange ctxtRange;
          NSInteger l = 0;
          TextPattern ** skips = [syntax contextSkipsForContext: 0];
          const char * skipChars = [syntax contextSkipCharactersForContext: 0];
          unsigned int numSkipChars = [syntax
            numberOfContextSkipCharactersForContext: 0];

          for (;i < r.length; i++)
            {
              unichar c = string[i];

              // Optimize - look into the skip characters array if the
              // character could be the beginning of a skip sequence.
              // If not, don't perform skip sequence recognition at all.
              if (c < numSkipChars && skipChars[c])
                {
                  for (j = 0; (pattern = skips[j]) != NULL; j++)
                    {
                      l = CheckTextPatternPresenceInString(pattern,
                                                           string,
                                                           r.length,
                                                           i);
                      if (l > 0)
                        {
                          break;
                        }
                    }

                  if (l > 0)
                    {
                      i += l - 1;
                      continue;
                    }
                }

              // optimize - skip unneeded characters
              if (c < numBeginningChars && !beginningChars[c])
                {
                  continue;
                }

              for (j = 0; (pattern = beginnings[j]) != NULL; j++)
                {
                  l = CheckTextPatternPresenceInString(pattern, string,
                                                       r.length, i);
                  if (l > 0)
                    {
                      break;
                    }
                }

              if (l > 0)
                {
                  break;
                }
            }

          // non-default contexts begin with number 1, not zero
          j++;

          ctxtRange = NSMakeRange(r.location + mark, i - mark);
          if (ctxtRange.length > 0)
            {
              // add an attribute telling the context into the text storage
              [textStorage addAttribute: ContextAttributeName
                                  value: [NSNumber numberWithInt: 0]
                                  range: ctxtRange];
              [self assignGraphicalAttributesOfContext: 0 toRange: ctxtRange];
            }

          ctxtRange = NSMakeRange(r.location + i, l);
          if (ctxtRange.length > 0)
            {
              [textStorage addAttribute: ContextAttributeName
                                  value: [NSNumber numberWithInt: j]
                                  range: ctxtRange];
              [self assignGraphicalAttributesOfContext: j
                                               toRange: ctxtRange];
            }
          i += l;

          // switch to the found context again
          context = j;
        }
      // specific context - look for it's terminator, but skip it's
      // exceptions
      else
        {
          NSInteger l = 0;
          TextPattern * ending = [syntax contextEndingForContext: context - 1];
          NSRange ctxtRange;
          TextPattern ** skips = [syntax contextSkipsForContext: context];
          const char * skipChars = [syntax contextSkipCharactersForContext:
            context];
          NSUInteger numSkipChars = [syntax
            numberOfContextSkipCharactersForContext: context];

          for (;i < r.length; i++)
            {
              unichar c = string[i];

              if (c < numSkipChars && skipChars[c])
                {
                  unsigned int j;
                  TextPattern * pattern;

                  for (j = 0; (pattern = skips[j]) != NULL; j++)
                    {
                      l = CheckTextPatternPresenceInString(pattern, string,
                                                           r.length, i);
                      if (l > 0)
                        {
                          break;
                        }
                    }

                  if (l > 0)
                    {
                      i += l - 1;
                      continue;
                    }
                }

              l = CheckTextPatternPresenceInString(ending, string,
                                                   r.length, i);
              if (l > 0)
                {
                  break;
                }
            }

          ctxtRange = NSMakeRange(r.location + mark, i - mark);
          if (ctxtRange.length > 0)
            {
              // add an attribute telling the context into the
              // text storage
              [textStorage addAttribute: ContextAttributeName
                                  value: [NSNumber numberWithInteger: context]
                                  range: ctxtRange];
              [self assignGraphicalAttributesOfContext: context
                                               toRange: ctxtRange];
            }

          ctxtRange = NSMakeRange(r.location + i, l);
          if (ctxtRange.length > 0)
            {
              [textStorage addAttribute: ContextAttributeName
                                  value: [NSNumber numberWithInteger: 0]
                                  range: ctxtRange];
              [self assignGraphicalAttributesOfContext: context
                                               toRange: ctxtRange];
            }
          i += l;

          // switch to the default context again
          context = 0;
        }
    }

  free(string);
}

- (void) fixUpKeywordsInRange: (NSRange) r
{
  unichar * string;
  NSUInteger i;

  string = malloc(r.length * sizeof(unichar));
  [[textStorage string] getCharacters: string range: r];

  for (i = 0; i < r.length;)
    {
      NSRange contextRange;
      TextPattern ** patterns;
      NSInteger context;

      context = [[textStorage attribute: ContextAttributeName
                                atIndex: i + r.location
                         effectiveRange: &contextRange] integerValue];

      contextRange = NSIntersectionRange(r, contextRange);
      contextRange.location -= r.location;

      patterns = [syntax keywordsInContext: context];

      while (i < NSMaxRange(contextRange))
        {
          unichar c = string[i];
          NSUInteger l = 0;
          NSUInteger j;
          TextPattern * pattern;

          // skip whitespace - it can't start a keyword
          if (my_isspace(c) || c == '\r' || c == '\n')
            {
              i++;
              continue;
            }

          for (j = 0; (pattern = patterns[j]) != NULL; j++)
            {
              l = CheckTextPatternPresenceInString(pattern,
                                                   string,
                                                   r.length,
                                                   i);
              if (l > 0)
                {
                  break;
                }
            }

          // found a pattern?
          if (pattern != NULL)
            {
              NSRange keywordRange = NSMakeRange(i + r.location, l);

              [self assignGraphicalAttributesOfKeyword: j
                                             inContext: context
                                               toRange: keywordRange];
              i += l;
            }
          else
            {
              i++;
            }
        }
    }

  free(string);
}

- (void) lazilyFixUpKeywordsInRange: (NSRange) r
{
  NSUInteger i;
  BOOL localDidBeginEditing = NO;

  for (i = r.location; i < NSMaxRange(r);)
    {
      NSRange effectiveRange;

      // locate non-fixed areas and fix them up
      if ([textStorage attribute: KeywordsNotFixedAttributeName
                         atIndex: i
           longestEffectiveRange: &effectiveRange
                         inRange: r] != nil)
        {
          if (localDidBeginEditing == NO)
            {
              localDidBeginEditing = YES;
              [textStorage beginEditing];
            }
          effectiveRange = NSIntersectionRange(effectiveRange, r);
          [self fixUpKeywordsInRange: effectiveRange];
          [textStorage removeAttribute: KeywordsNotFixedAttributeName
                                 range: effectiveRange];
          i += effectiveRange.length;
        }

      // skip over fixed areas
      else
        {
          i += effectiveRange.length;
        }
    }

  if (localDidBeginEditing == YES)
    {
      [textStorage endEditing];
    }
}

- (void) assignGraphicalAttributesOfContext: (NSUInteger) ctxt
                                    toRange: (NSRange) r
{
  BOOL bold, italic;
  NSColor * color;

  color = [syntax foregroundColorForContext: ctxt];
  if (color != nil)
    {
      [textStorage addAttribute: NSForegroundColorAttributeName
                          value: color
                          range: r];
    }
  else
    {
      [textStorage removeAttribute: NSForegroundColorAttributeName range: r];
    }

  color = [syntax backgroundColorForContext: ctxt];
  if (color != nil)
    {
      [textStorage addAttribute: NSBackgroundColorAttributeName
                          value: color
                          range: r];
    }
  else
    {
      [textStorage removeAttribute: NSBackgroundColorAttributeName range: r];
    }

  bold = [syntax isBoldFontForContext: ctxt];
  italic = [syntax isItalicFontForContext: ctxt];
  if (bold && italic)
    {
      [textStorage addAttribute: NSFontAttributeName
                          value: boldItalicFont
                          range: r];
    }
  else if (bold)
    {
      [textStorage addAttribute: NSFontAttributeName
                          value: boldFont
                          range: r];
    }
  else if (italic)
    {
      [textStorage addAttribute: NSFontAttributeName
                          value: italicFont
                          range: r];
    }
  else
    {
      [textStorage addAttribute: NSFontAttributeName
                          value: normalFont
                          range: r];
    }
}

- (void) assignGraphicalAttributesOfKeyword: (NSUInteger) keyword
                                  inContext: (NSUInteger) context
                                    toRange: (NSRange) r
{
  BOOL bold, italic;
  NSColor * color;

  color = [syntax foregroundColorForKeyword: keyword inContext: context];
  if (color != nil)
    {
      [textStorage addAttribute: NSForegroundColorAttributeName
                          value: color
                          range: r];
    }
  else
    {
      color = [syntax foregroundColorForContext: context];

      if (color != nil)
        {
          [textStorage addAttribute: NSForegroundColorAttributeName
                              value: color
                              range: r];
        }
      else
        {
          [textStorage removeAttribute: NSForegroundColorAttributeName
                                 range: r];
        }
    }

  color = [syntax backgroundColorForKeyword: keyword inContext: context];
  if (color != nil)
    {
      [textStorage addAttribute: NSBackgroundColorAttributeName
                          value: color
                          range: r];
    }
  else
    {
      color = [syntax backgroundColorForContext: context];

      if (color != nil)
        {
          [textStorage addAttribute: NSBackgroundColorAttributeName
                              value: color
                              range: r];
        }
      else
        {
          [textStorage removeAttribute: NSBackgroundColorAttributeName
                                 range: r];
        }
    }

  bold = [syntax isBoldFontForKeyword: keyword inContext: context];
  italic = [syntax isItalicFontForKeyword: keyword inContext: context];
  if (bold && italic)
    {
      [textStorage addAttribute: NSFontAttributeName
                          value: boldItalicFont
                          range: r];
    }
  else if (bold)
    {
      [textStorage addAttribute: NSFontAttributeName
                          value: boldFont
                          range: r];
    }
  else if (italic)
    {
      [textStorage addAttribute: NSFontAttributeName
                          value: italicFont
                          range: r];
    }
  else
    {
      [textStorage addAttribute: NSFontAttributeName
                          value: normalFont
                          range: r];
    }
}

- (NSUInteger) contextBeforeRange: (NSRange) r
{
  NSRange tmp;

  if (r.location == 0)
    {
      return 0;
    }
  else
    {
      return [[textStorage attribute: ContextAttributeName
                             atIndex: r.location - 1
                      effectiveRange: &tmp] intValue];
    }
}

- (NSUInteger) contextAfterRange: (NSRange) r
{
  NSRange tmp;
  NSUInteger i;
  NSUInteger length;

  i = NSMaxRange(r);
  length = [textStorage length];

  if (length == 0)
    {
      return 0;
    }
  else if (i < length)
    {
      return [[textStorage attribute: ContextAttributeName
                             atIndex: i
                      effectiveRange: &tmp] intValue];
    }
  else
    {
      return 0;
    }
}

- (NSUInteger) contextAtEndOfRange: (NSRange) r
{
  NSRange tmp;
  NSInteger i = (int) NSMaxRange(r) - 1;

  if (i < 0)
    {
      return 0;
    }
  else
    {
      return [[textStorage attribute: ContextAttributeName
                             atIndex: i
                      effectiveRange: &tmp] integerValue];
    }
}

- (void) beginEditingIfNeeded
{
  if (didBeginEditing == NO)
    {
      didBeginEditing = YES;
      [textStorage beginEditing];
    }
}

- (void) endEditingIfNeeded
{
  if (didBeginEditing == YES)
    {
      didBeginEditing = NO;
      [textStorage endEditing];
    }
}

@end

@implementation SyntaxHighlighter

- initWithFileType:(NSString *)fileType
       textStorage:(NSTextStorage *)aStorage
{
  if ([self init])
    {
      NSRange r;

      ASSIGN(textStorage, aStorage);
      ASSIGN(syntax, [SyntaxDefinition
        syntaxDefinitionForFileType:fileType textStorage:textStorage]);

      // no syntax definition - no highlighting possible
      if (syntax == nil)
        {
          [self release];
          return nil;
        }

      // mark all of the text storage as requiring keyword fixing
      r = NSMakeRange(0, [textStorage length]);
      [textStorage addAttribute: KeywordsNotFixedAttributeName
                          value: [NSNull null]
                          range: r];

      [[NSNotificationCenter defaultCenter]
        addObserver: self
           selector: @selector(textStorageWillProcessEditing:)
               name: NSTextStorageWillProcessEditingNotification
             object: textStorage];

      ASSIGN(normalFont, [PCEditorView defaultEditorFont]);
      ASSIGN(boldFont, [PCEditorView defaultEditorBoldFont]);
      ASSIGN(italicFont, [PCEditorView defaultEditorItalicFont]);
      ASSIGN(boldItalicFont, [PCEditorView defaultEditorBoldItalicFont]);

      return self;
    }
  else
    {
      return nil;
    }
}

- (void) dealloc
{
  NSDebugLLog(@"SyntaxHighlighter", @"SyntaxHighlighter: dealloc");

  [[NSNotificationCenter defaultCenter] removeObserver: self];

  TEST_RELEASE(textStorage);
  TEST_RELEASE(syntax);
  TEST_RELEASE(normalFont);
  TEST_RELEASE(boldFont);
  TEST_RELEASE(italicFont);
  TEST_RELEASE(boldItalicFont);

  [super dealloc];
}

- (void) highlightRange: (NSRange) r
{
  if (delayedProcessedRange.length > 0)
    {
      [self beginEditingIfNeeded];
      [self fixUpContextsInRange: delayedProcessedRange];
      [self fixUpKeywordsInRange: delayedProcessedRange];

      if ([self contextAtEndOfRange: delayedProcessedRange] !=
          [self contextAfterRange: delayedProcessedRange])
        {
          NSRange invalidatedRange;

          lastProcessedContextIndex = NSMaxRange(delayedProcessedRange);

          invalidatedRange = NSMakeRange(NSMaxRange(delayedProcessedRange),
            [textStorage length] - NSMaxRange(delayedProcessedRange));
          [textStorage addAttribute: KeywordsNotFixedAttributeName
                              value: [NSNull null]
                              range: invalidatedRange];
        }
    }
  else
    {
      if (delayedProcessedRange.location > 0 &&
        [self contextBeforeRange: delayedProcessedRange] !=
        [self contextAfterRange: delayedProcessedRange])
        {
          NSRange invalidatedRange;

          lastProcessedContextIndex = NSMaxRange(delayedProcessedRange);

          [self beginEditingIfNeeded];

          invalidatedRange = NSMakeRange(NSMaxRange(delayedProcessedRange),
            [textStorage length] - NSMaxRange(delayedProcessedRange));
          [textStorage addAttribute: KeywordsNotFixedAttributeName
                              value: [NSNull null]
                              range: invalidatedRange];
        }
    }

  delayedProcessedRange = NSMakeRange(0, 0);

  r = RangeOfWordInString([textStorage string], r);

  // need to fixup contexts?
  if (NSMaxRange(r) > lastProcessedContextIndex)
    {
      NSRange fixupRange;

      fixupRange = NSMakeRange(lastProcessedContextIndex,
                               NSMaxRange(r) - lastProcessedContextIndex);

      [self beginEditingIfNeeded];
      [self fixUpContextsInRange: fixupRange];

      lastProcessedContextIndex = NSMaxRange(r);
    }

  [self lazilyFixUpKeywordsInRange: r];

  [self endEditingIfNeeded];
}

- (void) textStorageWillProcessEditing: (NSNotification *) notif
{
  if ([textStorage editedMask] & NSTextStorageEditedCharacters)
    {
      NSRange editedRange = [textStorage editedRange];

      delayedProcessedRange = RangeOfWordInString([textStorage string],
                                                  editedRange);

      if (lastProcessedContextIndex > editedRange.location)
        {
          lastProcessedContextIndex += [textStorage changeInLength];
        }
    }
}

@end
