/*
    HKSyntaxDefinition.h

    Implementation of the HKSyntaxDefinition class for the HighlighterKit
    framework.

    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 "HKSyntaxDefinition.h"

#import <Foundation/NSArray.h>
#import <Foundation/NSBundle.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSFileManager.h>
#import <Foundation/NSPathUtilities.h>
#import <Foundation/NSScanner.h>
#import <Foundation/NSValue.h>

#import <AppKit/NSColor.h>

static NSDictionary *
ParseSyntaxGraphics (NSDictionary * specification)
{
  NSMutableDictionary * dict = [NSMutableDictionary dictionary];
  NSString * value;

  value = [specification objectForKey: @"ForegroundColor"];
  if (value != nil)
    {
      float r, g, b, a;
      NSScanner * scanner = [NSScanner scannerWithString: value];

      if ([scanner scanFloat: &r] && [scanner scanFloat: &g] &&
          [scanner scanFloat: &b])
        {
          if ([scanner scanFloat: &a] == NO)
            {
              a = 1.0;
            }

          [dict setObject: [NSColor colorWithCalibratedRed: r
                                                     green: g
                                                      blue: b
                                                     alpha: a]
                   forKey: @"ForegroundColor"];
        }
      else
        {
          NSLog(_(@"Invalid ForegroundColor specification \"%@\" found: "
                  @"the correct format is \"r g b [a]\" where each component"
                  @"is a real number in the range of 0.0 thru 1.0 inclusive, "
                  @"specifying the red, green, blue and alpha (optional) "
                  @"components of the desired color."), value);
        }
    }

  value = [specification objectForKey: @"BackgroundColor"];
  if (value != nil)
    {
      float r, g, b, a;
      NSScanner * scanner = [NSScanner scannerWithString: value];

      if ([scanner scanFloat: &r] && [scanner scanFloat: &g] &&
          [scanner scanFloat: &b])
        {
          if ([scanner scanFloat: &a] == NO)
            {
              a = 1.0;
            }

          [dict setObject: [NSColor colorWithCalibratedRed: r
                                                     green: g
                                                      blue: b
                                                     alpha: a]
                   forKey: @"BackgroundColor"];
        }
      else
        {
          NSLog(_(@"Invalid BackgroundColor specification \"%@\" found: "
                  @"the correct format is \"r g b [a]\" where each component"
                  @"is a real number in the range of 0.0 thru 1.0 inclusive, "
                  @"specifying the red, green, blue and alpha (optional) "
                  @"components of the desired color."), value);
        }
    }

  value = [specification objectForKey: @"Bold"];
  if (value != nil)
    {
      [dict setObject: [NSNumber numberWithBool: [value boolValue]]
               forKey: @"Bold"];
    }

  value = [specification objectForKey: @"Italic"];
  if (value != nil)
    {
      [dict setObject: [NSNumber numberWithBool: [value boolValue]]
               forKey: @"Italic"];
    }

  return [[dict copy] autorelease];
}

static void
MarkTextPatternBeginningCharacters (HKTextPattern * pattern,
                                    char * buffer, unsigned int bufSize)
{
  unichar * chars = HKPermissibleCharactersAtPatternBeginning (pattern);

  if (chars == (unichar *) -1)
    {
      memset(buffer, 1, 128);
    }
  else if (chars != NULL)
    {
      unsigned int i;
      unichar c;

      for (i = 0; (c = chars[i]) != 0; i++)
        {
          if (c < bufSize)
            {
              buffer[c] = 1;
            }
        }

      free(chars);
    }
}

static NSMutableDictionary * syntaxDefinitions = nil;

@implementation HKSyntaxDefinition

+ (void) initialize
{
  if (self == [HKSyntaxDefinition class])
    {
      syntaxDefinitions = [NSMutableDictionary new];
    }
}

/**
 * Locates a syntax file of the apropriate type and returns it's path.
 *
 * This method is used when creating syntax definitions of a certain
 * type. Syntax files can be placed in a variety of places in the file
 * system so that this method will find them:
 *
 * - in one of the system's domains in the 'Library/SyntaxDefinitions'
 *   subdirectory (or any of it's subdirectories)
 * - in the application's main bundle as a resource
 * - in this framework's bundle as a resource
 *
 * The syntax file is matched by looking at the file's extension - it must
 * say "syntax". Then the `type' argument is matched against the file's
 * name (in a case insensitive manner) - they must match. After that a
 * path to the file is returned.
 *
 * @param type The syntax file type which to look for.
 *
 * @return If found, a path to the syntax file, or `nil' if not.
 */
+ (NSString *) findSyntaxFileForType: (NSString *) type
{
  NSEnumerator * e;
  NSString * directory;
  NSFileManager * fm = [NSFileManager defaultManager];

  // make the search case insensitive
  type = [type lowercaseString];

  // search all domains for the matching .syntax file
  e = [NSSearchPathForDirectoriesInDomains (NSLibraryDirectory,
    NSAllDomainsMask, YES) objectEnumerator];
  while ((directory = [e nextObject]) != nil)
    {
      NSString * subpath = [directory stringByAppendingPathComponent:
        @"SyntaxDefinitions"];
      BOOL dir;

      if ([fm fileExistsAtPath: subpath isDirectory: &dir] && dir)
        {
          NSDirectoryEnumerator * de = [fm enumeratorAtPath: subpath];
          NSString * origPath;

          while ((origPath = [de nextObject]) != nil)
            {
              NSString * path = [origPath lowercaseString];
              if ([[path pathExtension] isEqualToString: @"syntax"] &&
                [[[path lastPathComponent] stringByDeletingPathExtension]
                  isEqualToString: type] &&
                [[[de fileAttributes] fileType] isEqualToString:
                  NSFileTypeRegular])
                {
                  return origPath;
                }
            }
        }
    }

  // try to look in the main app bundle
  NSString * file;

  e = [[[NSBundle mainBundle]
    pathsForResourcesOfType: @"syntax" inDirectory: nil]
    objectEnumerator];
  while ((file = [e nextObject]) != nil)
    {
      if ([[[[file lowercaseString] lastPathComponent]
        stringByDeletingPathExtension] isEqualToString: type])
        {
          return file;
        }
    }

  // try to look in our bundle as well
  e = [[[NSBundle bundleForClass: [HKSyntaxDefinition class]]
    pathsForResourcesOfType: @"syntax" inDirectory: nil]
    objectEnumerator];
  while ((file = [e nextObject]) != nil)
    {
      if ([[[[file lowercaseString] lastPathComponent]
        stringByDeletingPathExtension] isEqualToString: type])
        {
          return file;
        }
    }

  // not found
  return nil;
}

/**
 * Retrieves a syntax definition of the specified type.
 *
 * It is better if you use this method instead of construct syntax
 * definition objects by hand, since this method uses the standardized
 * search algorithms for syntax files and also caches the resulting
 * syntax definition (since syntax definitions are, by nature, immutable),
 * making later queries faster.
 *
 * @param type The syntax file type from which to construct the definition.
 *
 * @return The syntax definition, if it was found. If it wasn't, then `nil'.
 */
+ (HKSyntaxDefinition *) syntaxDefinitionForType: (NSString *) type
{
  HKSyntaxDefinition * def;

  def = [syntaxDefinitions objectForKey: type];
  if (def == nil)
    {
      NSString * file = [self findSyntaxFileForType: type];
      NSDictionary * contents = [NSDictionary
        dictionaryWithContentsOfFile: file];

      if (contents != nil && [contents objectForKey: @"Contexts"] != nil)
        {
          def = [[[HKSyntaxDefinition alloc]
            initWithContextList: [contents objectForKey: @"Contexts"]]
            autorelease];

          if (def != nil)
            {
              [syntaxDefinitions setObject: def forKey: type];
            }

          return def;
        }
      else
        {
          return nil;
        }
    }
  else
    {
      return def;
    }
}

- initWithContextList: (NSArray *) contexts
{
  if ((self = [self init]) != nil)
    {
      unsigned int i, n;
      NSMutableArray * contextGraphicsTmp = [NSMutableArray array],
                     * keywordGraphicsTmp = [NSMutableArray array];

      // compile the syntax definition
      for (i = 0, n = [contexts count]; i < n; i++)
        {
          unsigned int j, keywordCount, skipCount;
          NSDictionary * context = [contexts objectAtIndex: i];
          NSArray * ctxtKeywords, * skips;
          NSMutableArray * contextKeywordsGraphicsTmp;

          // context beginning/ending missing?
          if (([context objectForKey: @"Beginning"] == nil ||
               [context objectForKey: @"Ending"] == nil) &&
              i > 0)
            {
              NSLog(_(@"Syntax compilation error: context %i missing "
                      @"beginning or ending symbol."));

              [self release];
              return nil;
            }

          // process context beginnings/endings
          if (i > 0)
            {
              contextBeginnings = realloc(contextBeginnings, i *
                sizeof(HKTextPattern *));
              contextBeginnings[i - 1] = HKCompileTextPattern([context
                objectForKey: @"Beginning"]);

              MarkTextPatternBeginningCharacters(contextBeginnings[i - 1],
                contextBeginningChars, sizeof(contextBeginningChars));

              contextEndings = realloc(contextEndings, i *
                sizeof(HKTextPattern *));
              contextEndings[i - 1] = HKCompileTextPattern([context
                objectForKey: @"Ending"]);
            }

          // process context skips
          contextSkipChars = realloc(contextSkipChars, (i + 1) *
            sizeof(char *));
          contextSkipChars[i] = calloc(128, sizeof(char));
          contextSkips = realloc(contextSkips, sizeof(HKTextPattern **) *
            (i + 1));
          contextSkips[i] = NULL;
          skips = [context objectForKey: @"ContextSkips"];
          for (j = 0, skipCount = [skips count]; j < skipCount; j++)
            {
              NSString * skip = [skips objectAtIndex: j];

              contextSkips[i] = realloc(contextSkips[i], (j + 1) *
                sizeof(HKTextPattern *));
              contextSkips[i][j] = HKCompileTextPattern(skip);
              MarkTextPatternBeginningCharacters(contextSkips[i][j],
                contextSkipChars[i], 128);
            }
          contextSkips[i] = realloc(contextSkips[i], (j + 1) *
            sizeof(HKTextPattern *));
          contextSkips[i][j] = NULL;

          // process context graphics
          [contextGraphicsTmp addObject: ParseSyntaxGraphics(context)];

          keywords = realloc(keywords, (i + 1) * sizeof(HKTextPattern **));
          keywords[i] = NULL;

          ctxtKeywords = [context objectForKey: @"Keywords"];
          contextKeywordsGraphicsTmp = [NSMutableArray arrayWithCapacity:
            [ctxtKeywords count]];

          // run through all keywords in the context
          for (j = 0, keywordCount = [ctxtKeywords count];
               j < keywordCount;
               j++)
            {
              NSDictionary * keyword = [ctxtKeywords objectAtIndex: j];
              NSString * keywordString = [keyword objectForKey: @"Pattern"];
              HKTextPattern * pattern;

              if (keywordString == nil)
                {
                  NSLog(_(@"Missing keyword pattern declaration "
                          @"in context %i keyword %i. Ignoring all the "
                          @"remaining of the keywords in this context."),
                          i, j);
                  break;
                }
              pattern = HKCompileTextPattern(keywordString);
              if (pattern == NULL)
                {
                  break;
                }

              keywords[i] = realloc(keywords[i],
                                    (j + 1) * sizeof(HKTextPattern *));
              keywords[i][j] = pattern;

              [contextKeywordsGraphicsTmp addObject:
                ParseSyntaxGraphics(keyword)];
            }

          // append a trailing NULL to terminate the list
          keywords[i] = realloc(keywords[i], (j + 1) * sizeof(HKTextPattern *));
          keywords[i][j] = NULL;

          [keywordGraphicsTmp addObject: [[contextKeywordsGraphicsTmp
            copy] autorelease]];
        }

      // terminate the keywords array by appending a trailing NULL pointer
      keywords = realloc(keywords, (i + 1) * sizeof(HKTextPattern **));
      keywords[i] = NULL;

      // begining and ending arrays don't include the default context!
      // Thus it is indexed by 'i' not 'i + 1'
      contextBeginnings = realloc(contextBeginnings, i *
        sizeof(HKTextPattern **));
      contextBeginnings[i - 1] = NULL;
      contextEndings = realloc(contextEndings, i * sizeof(HKTextPattern **));
      contextEndings[i - 1] = NULL;

      contextSkipChars = realloc(contextSkipChars, (i + 1) * sizeof(char *));
      contextSkipChars[i] = NULL;

      ASSIGNCOPY (contextGraphics, contextGraphicsTmp);
      ASSIGNCOPY (keywordGraphics, keywordGraphicsTmp);

      return self;
    }
  else
    {
      return nil;
    }
}

- (void) dealloc
{
  HKTextPattern * pattern;
  unsigned int i;
  HKTextPattern ** patternList;
  char * buf;

  // free context beginnings
  for (i = 0; (pattern = contextBeginnings[i]) != NULL; i++)
    {
      HKFreeTextPattern (pattern);
    }
  free (contextBeginnings);

  // free context endings
  for (i = 0; (pattern = contextEndings[i]) != NULL; i++)
    {
      HKFreeTextPattern (pattern);
    }
  free (contextEndings);

  // free context skip characters
  for (i = 0; (buf = contextSkipChars[i]) != NULL; i++)
    {
      free (buf);
    }
  free (contextSkipChars);

  // free context skips
  for (i = 0; (patternList = contextSkips[i]) != NULL; i++)
    {
      unsigned int j;

      for (j = 0; (pattern = patternList[j]) != NULL; j++)
        {
          HKFreeTextPattern (pattern);
        }
      free (patternList);
    }
  free (contextSkips);

  // free keywords
  for (i = 0; (patternList = keywords[i]) != NULL; i++)
    {
      unsigned int j;

      for (j = 0; (pattern = patternList[j]) != NULL; j++)
        {
          HKFreeTextPattern (pattern);
        }

      free (patternList);
    }
  free (keywords);

  TEST_RELEASE (contextGraphics);
  TEST_RELEASE (keywordGraphics);

  [super dealloc];
}

/**
 * Returns a NULL pointer terminated list of context beginning symbols.
 */
- (HKTextPattern **) contextBeginnings
{
  return contextBeginnings;
}

- (const char *) contextBeginningCharacters
{
  return contextBeginningChars;
}

- (unsigned int) numberOfContextBeginningCharacters
{
  return sizeof(contextBeginningChars);
}

- (const char *) contextSkipCharactersForContext: (unsigned int) ctxt
{
  return contextSkipChars[ctxt];
}

- (unsigned int) numberOfContextSkipCharactersForContext: (unsigned int) ctxt
{
  return 128;
}

/**
 * Returns the context ending symbol for the context identified by `ctxt'.
 */
- (HKTextPattern *) contextEndingForContext: (unsigned int) ctxt
{
  return contextEndings[ctxt];
}

- (HKTextPattern **) contextSkipsForContext: (unsigned int) ctxt
{
  return contextSkips[ctxt];
}

- (NSColor *) foregroundColorForContext: (unsigned int) context
{
  return [[contextGraphics
    objectAtIndex: context]
    objectForKey: @"ForegroundColor"];
}

- (NSColor *) backgroundColorForContext: (unsigned int) context
{
  return [[contextGraphics
    objectAtIndex: context]
    objectForKey: @"BackgroundColor"];
}

- (BOOL) isItalicFontForContext: (unsigned int) context
{
  return [[[contextGraphics
    objectAtIndex: context]
    objectForKey: @"Italic"]
    boolValue];
}

- (BOOL) isBoldFontForContext: (unsigned int) context
{
  return [[[contextGraphics
    objectAtIndex: context]
    objectForKey: @"Bold"]
    boolValue];
}

/**
 * Returns a NULL pointer terminated list of text patterns representing
 * keywords to be matched inside context `context'.
 */
- (HKTextPattern **) keywordsInContext: (unsigned int) context
{
  return keywords[context];
}

/**
 * Returns the color with which the keyword identified by `keyword'
 * in `contextName' should be colored. The argument `keyword' is the
 * index of the keyword in the array returned by -keywordsInContext:.
 */
- (NSColor *) foregroundColorForKeyword: (unsigned int) keyword
                              inContext: (unsigned int) context
{
  return [[[keywordGraphics
    objectAtIndex: context]
    objectAtIndex: keyword]
    objectForKey: @"ForegroundColor"];
}

- (NSColor *) backgroundColorForKeyword: (unsigned int) keyword
                              inContext: (unsigned int) context
{
  return [[[keywordGraphics
    objectAtIndex: context]
    objectAtIndex: keyword]
    objectForKey: @"BackgroundColor"];
}
- (BOOL) isItalicFontForKeyword: (unsigned int) keyword
                      inContext: (unsigned int) context
{
  return [[[[keywordGraphics
    objectAtIndex: context]
    objectAtIndex: keyword]
    objectForKey: @"Italic"]
    boolValue];
}

/**
 * Returns YES if the font with which the keyword identified by `keyword'
 * should be bold and NO if it should be normal weigth.
 */
- (BOOL) isBoldFontForKeyword: (unsigned int) keyword
                    inContext: (unsigned int) context
{
  return [[[[keywordGraphics
    objectAtIndex: context]
    objectAtIndex: keyword]
    objectForKey: @"Bold"]
    boolValue];
}

@end
