/*
 Copyright (C) 2003-2004 Max Berger
 Copyright (C) 2004 OpenGroupware.org

 This file is part of versitSaxDriver, written for the OpenGroupware.org 
 project (OGo).
 
 OGo is free software; you can redistribute it and/or modify it under
 the terms of the GNU Lesser General Public License as published by the
 Free Software Foundation; either version 2, or (at your option) any
 later version.
 
 OGo 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 Lesser General Public
 License for more details.
 
 You should have received a copy of the GNU Lesser General Public
 License along with OGo; see the file COPYING.  If not, write to the
 Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
 02111-1307, USA.
*/
// $Id$

#include "VSSaxDriver.h"
#include "VSStringFormatter.h"
#include "common.h"

@implementation VSSaxDriver

static BOOL debugOn = NO;

static NSCharacterSet *dotCharSet = nil;
static NSCharacterSet *equalSignCharSet = nil;
static NSCharacterSet *commaCharSet = nil;
static NSCharacterSet *colonAndSemicolonCharSet = nil;
static NSCharacterSet *colonSemicolonAndDquoteCharSet = nil;
static NSCharacterSet *whitespaceCharSet = nil;

static VSStringFormatter *stringFormatter = nil;

+ (void)initialize {
  static BOOL didInit = NO;
  NSUserDefaults *ud;

  if(didInit)
    return;
  didInit = YES;

  ud      = [NSUserDefaults standardUserDefaults];
  debugOn = [ud boolForKey:@"OGoDebugVersitSaxDriver"];

  dotCharSet =
    [[NSCharacterSet characterSetWithCharactersInString:@"."] retain];
  equalSignCharSet =
    [[NSCharacterSet characterSetWithCharactersInString:@"="] retain];
  commaCharSet =
    [[NSCharacterSet characterSetWithCharactersInString:@","] retain];
  colonAndSemicolonCharSet =
    [[NSCharacterSet characterSetWithCharactersInString:@":;"] retain];
  colonSemicolonAndDquoteCharSet =
    [[NSCharacterSet characterSetWithCharactersInString:@":;\""] retain];
  whitespaceCharSet =
    [[NSCharacterSet whitespaceCharacterSet] retain];

  stringFormatter = [VSStringFormatter sharedFormatter];
}


- (id)init {
  if ((self = [super init])) {
    self->prefixURI         = @"";
    self->cardStack         = [[NSMutableArray alloc]      init];
    self->elementList       = [[NSMutableArray alloc]      init];
    self->attributeMapping  = [[NSMutableDictionary alloc] init];
    self->subItemMapping    = [[NSMutableDictionary alloc] init];
  }
  return self;
}

- (void)dealloc {
  [self->contentHandler    release];
  [self->prefixURI         release];
  [self->cardStack         release];
  [self->elementList       release];
  [self->attributeElements release];
  [self->elementMapping    release];
  [self->attributeMapping  release];
  [self->subItemMapping    release];
  [super dealloc];
}

/* accessors */

- (void)setFeature:(NSString *)_name to:(BOOL)_value {
}
- (BOOL)feature:(NSString *)_name {
  return NO;
}

- (void)setProperty:(NSString *)_name to:(id)_value {
}
- (id)property:(NSString *)_name {
  return nil;
}

/* handlers */

- (void)setContentHandler:(id<NSObject,SaxContentHandler>)_handler {
  ASSIGN(self->contentHandler,_handler);
}

- (void)setDTDHandler:(id<NSObject,SaxDTDHandler>)_handler {
  // FIXME
}

- (void)setErrorHandler:(id<NSObject,SaxErrorHandler>)_handler {
  // FIXME
}
- (void)setEntityResolver:(id<NSObject,SaxEntityResolver>)_handler {
  // FIXME
}

- (id<NSObject,SaxContentHandler>)contentHandler {
  return self->contentHandler;
}

- (id<NSObject,SaxDTDHandler>)dtdHandler {
  // FIXME
  return NULL;
}

- (id<NSObject,SaxErrorHandler>)errorHandler {
  // FIXME
  return NULL;
}
- (id<NSObject,SaxEntityResolver>)entityResolver {
  // FIXME
  return NULL;
}

- (void)setPrefixURI:(NSString *)_uri {
  ASSIGNCOPY(self->prefixURI, _uri);
}
- (NSString *)prefixURI {
  return self->prefixURI;
}

- (void)setAttributeElements:(NSSet *)_elements {
  ASSIGNCOPY(self->attributeElements, _elements);
}
- (NSSet *)attributeElements {
  return self->attributeElements;
}

- (void)setElementMapping:(NSDictionary *)_mapping {
  ASSIGNCOPY(self->elementMapping, _mapping);
}
- (NSDictionary *)elementMapping {
  return self->elementMapping;
}

- (void)setAttributeMapping:(NSDictionary *)_mapping {
  [self setAttributeMapping:_mapping forElement:@""];
}

- (void)setAttributeMapping:(NSDictionary *)_mapping 
  forElement:(NSString *)_element 
{
  if (!_element)
    _element = @"";
  [attributeMapping setObject:_mapping forKey:_element];
}

- (void)setSubItemMapping:(NSArray *)_mapping 
  forElement:(NSString *)_element 
{
  [subItemMapping setObject:_mapping forKey:_element];  
}



/* parsing */

- (NSString *)_mapTagName:(NSString *)_tagName {
  NSString *ret;
  NSRange  r;

  if ((ret = [self->elementMapping objectForKey:_tagName]) == nil) {
    //NSLog(@"Unknown Key: %@ in %@",_tagName,self->elementMapping);
    ret = _tagName;

    /* This is to allow parsing of vCards produced by Apple
       Addressbook. AFAIK the .dot notation is a non-standard
       extension */
    r = [_tagName rangeOfCharacterFromSet:dotCharSet];
    if (r.length > 0) {
      ret = [self _mapTagName:[_tagName substringFromIndex:(r.location + 1)]];
    }
  }
  return ret;
}

- (void)_addAttribute:(NSString *)_attribute
  value:(NSString *)_value 
  toAttrs:(SaxAttributes *)_attrs
{
  [_attrs addAttribute:_attribute
          uri:self->prefixURI 
          rawName:_attribute
          type:@"CDATA"
          value:_value];
}

- (void)_addAttribute:(NSString *)_attribute value:(NSString *)_value {
  NSArray *element = [cardStack lastObject];
  SaxAttributes *attrs = [element objectAtIndex:2];
  [self _addAttribute:_attribute value:_value toAttrs:attrs];
}

- (NSString *)_mapAttrName:(NSString *)_attrName forTag:(NSString *)_tagName {
  NSString *mappedName;

  mappedName = [[self->attributeMapping objectForKey:_tagName]
                                        objectForKey:_attrName];
  if (!mappedName) {
    mappedName = [[self->attributeMapping objectForKey:
                                          [self _mapTagName:_tagName]]
                                          objectForKey:_attrName];
  }
  if (!mappedName) {
    mappedName = [[self->attributeMapping objectForKey:@""]
                                          objectForKey:_attrName];
  }
  if (!mappedName)
    mappedName = _attrName;
  
  return mappedName;
}

- (void)_parseAttr:(NSString *)_attr 
  forTag:(NSString *)_tagName
  intoAttr:(NSString **)attr_
  intoValue:(NSString **)value_
{
  NSRange  r;
  NSString *attrName, *attrValue, *mappedName;
  
  r = [_attr rangeOfCharacterFromSet:equalSignCharSet];
  if (r.length > 0) {
    attrName  = [[_attr substringToIndex:r.location] uppercaseString];
    attrValue = [_attr substringFromIndex:(r.location + 1)];
  }
  else {
    attrName  = @"TYPE";
    attrValue = _attr;
  }
  
#if 0
  // ZNeK: what's this for?
  r = [attrValue rangeOfCharacterFromSet:commaCharSet];
  while (r.length > 0) {
    [attrValue replaceCharactersInRange:r withString:@" "];
    r = [attrValue rangeOfCharacterFromSet:commaCharSet];
  }
#endif

  mappedName = [self _mapAttrName:attrName forTag:_tagName];
  *attr_ = mappedName;
  *value_ = [stringFormatter stringByUnescapingRFC2445Text:attrValue];
}

- (id<NSObject,SaxAttributes>)_mapAttrs:(NSArray *)_attrs 
  forTag:(NSString *)_tagName 
{
  SaxAttributes       *retAttrs;
  NSEnumerator        *attrEnum;
  NSString            *curAttr, *mappedAttr, *mappedValue, *oldValue;
  NSMutableDictionary *attributes;

  if (!_attrs || [_attrs count] == 0)
    return nil;

  attributes = [[NSMutableDictionary alloc] init];
  retAttrs = [[[SaxAttributes alloc] init] autorelease];
  attrEnum = [_attrs objectEnumerator];
  while ((curAttr = [attrEnum nextObject])) {
    [self _parseAttr:curAttr
          forTag:_tagName
          intoAttr:&mappedAttr
          intoValue:&mappedValue];
    if ((oldValue = [attributes objectForKey:mappedAttr])) {
      NSString *val;
      /* ZNeK: duh! */
      val = [NSString stringWithFormat:@"%@ %@",oldValue, mappedValue];
      [attributes setObject:val forKey:mappedAttr];
    }
    else  
      [attributes setObject:mappedValue forKey:mappedAttr];
  }

  attrEnum = [attributes keyEnumerator];
  while ((curAttr = [attrEnum nextObject])) {
    [self _addAttribute:curAttr
          value:[attributes objectForKey:curAttr]
          toAttrs:retAttrs];
  }
  
  [attributes release];
  
  return retAttrs;
}

- (NSArray *)_beginTag:(NSString *)_tagName 
  withAttrs:(id<NSObject,SaxAttributes>)_attrs 
{
  NSArray *tag = [NSArray arrayWithObjects:@"BEGIN",_tagName,_attrs,NULL];
  [self->elementList addObject:tag];
  return tag;
}

- (void)_endTag:(NSString *)_tagName {
  [self->elementList addObject:
	 [NSArray arrayWithObjects:@"END",_tagName,NULL]];
}

- (void)_addSubItems:(NSArray *)_items withData:(NSString *)_content {
  NSEnumerator *itemEnum, *contentEnum;
  NSString *subTag;
  NSString *subContent;
  
  itemEnum    = [_items objectEnumerator];
  contentEnum = [[_content componentsSeparatedByString:@";"] objectEnumerator];
  
  while ((subTag=[itemEnum nextObject])) {
    subContent = [contentEnum nextObject];
    
    [self _beginTag:subTag withAttrs:nil];
    if ([subContent length]>0) 
      [self->elementList addObject:
        [NSArray arrayWithObjects:@"DATA", subContent, nil]];  
    [self _endTag:subTag];
  }
}

- (void)_dataTag:(NSString *)_tagName 
  withAttrs:(id<NSObject,SaxAttributes>)_attrs 
  andContent:(NSString *)_content 
{
  NSArray *subItems;

  _content = [stringFormatter stringByUnescapingRFC2445Text:_content];
  if ([self->attributeElements containsObject:_tagName]) {
    [self _addAttribute:_tagName value:_content];
  } 
  else {
    [self _beginTag:_tagName withAttrs:_attrs];
    if ([_content length] > 0) {
      if ((subItems = [self->subItemMapping objectForKey:_tagName])) {
        [self _addSubItems:subItems withData:_content];
      }
      else {
        [self->elementList addObject:
          [NSArray arrayWithObjects:@"DATA", _content, nil]];  
      }
    }
    [self _endTag:_tagName];
  }
}

- (void)_eventsForElements {
  NSEnumerator *enu;
  NSArray  *obj;
  NSString *type;
  NSString *name;
  unichar  *chardata;
  id<NSObject,SaxAttributes> attrs;
  
  enu = [elementList objectEnumerator];
  while ((obj = [enu nextObject])) {
    type = [obj objectAtIndex:0];
    name = [obj objectAtIndex:1];
    
    if ([obj count] > 2) 
      attrs = [obj objectAtIndex:2];
    else
      attrs = nil;

    if ([type isEqualToString:@"BEGIN"]) {
      [self->contentHandler startElement:name
                            namespace:self->prefixURI
                            rawName:name
                            attributes:attrs];
    } 
    else if ([type isEqualToString:@"END"]) {
      [self->contentHandler endElement:name
                            namespace:self->prefixURI
                            rawName:name];
    }
    else {
      unsigned len = [name length];
      chardata = malloc(len * sizeof(unichar));
      [name getCharacters:chardata range:NSMakeRange(0, len)];
      [self->contentHandler characters:chardata length:len];
      if (chardata)
        free(chardata);
    }
  }
  [elementList removeAllObjects];
}

- (void)_parseLine:(NSString *)_line {
  NSString       *tagName, *tagValue;
  NSMutableArray *tagAttributes;
  NSRange        r, todoRange;
  unsigned       length;

  length = [_line length];
  tagAttributes = [[NSMutableArray alloc] init];
  todoRange = NSMakeRange(0, length);
  r = [_line rangeOfCharacterFromSet:colonAndSemicolonCharSet
             options:0
             range:todoRange];
  tagName = [[_line substringToIndex:r.location] uppercaseString];
  if([_line characterAtIndex:r.location] != ':') {
    BOOL isAtEnd = NO, isInDquote = NO;
    unsigned start = NSMaxRange(r);

    todoRange = NSMakeRange(start, length - start);
    while(!isAtEnd) {
      BOOL skip = YES;

      /* scan for parameters */
      r = [_line rangeOfCharacterFromSet:colonSemicolonAndDquoteCharSet
                 options:0
                 range:todoRange];
      /* first check if delimiter candidate is escaped */
      if([_line characterAtIndex:(r.location - 1)] != '\\') {
        unichar delimiter;
        NSRange copyRange;

        delimiter = [_line characterAtIndex:r.location];
        if(delimiter == '\"') {
          /* not a real delimiter - toggle isInDquote for proper escaping */
          isInDquote = !isInDquote;
        }
        else {
          if(!isInDquote) {
            /* is a delimiter, which one? */
            skip = NO;
            if(delimiter == ':') {
              isAtEnd = YES;
            }
            copyRange = NSMakeRange(start, r.location - start);
            [tagAttributes addObject:[_line substringWithRange:copyRange]];
            if(!isAtEnd) {
              /* adjust start, todoRange */
              start     = NSMaxRange(r);
              todoRange = NSMakeRange(start, length - start);
            }
          }
        }
      }
      if(skip) {
        /* adjust todoRange */
        unsigned offset = NSMaxRange(r);
        todoRange = NSMakeRange(offset, length - offset);
      }
    }
  }
  tagValue = [_line substringFromIndex:NSMaxRange(r)];

  if ([tagName isEqualToString:@"BEGIN"]) {
    id tag;
    
    tag = [self _beginTag:[self _mapTagName:tagValue] 
                withAttrs:[[[SaxAttributes alloc] init] autorelease]];
    [self->cardStack addObject:tag];
  } 
  else if ([tagName isEqualToString:@"END"]) {
    [self _endTag:[self _mapTagName:tagValue]];
    [self->cardStack removeLastObject];
    if ([self->cardStack count] == 0)
      [self _eventsForElements];
  }
  else {
    [self _dataTag:[self _mapTagName:tagName]
         withAttrs:[self _mapAttrs:tagAttributes forTag:tagName] 
        andContent:tagValue];
  }
  [tagAttributes release];
}

- (void)_parseString:(NSString *)_rawString {
  unsigned pos, length;
  NSMutableString *line;
  NSRange r;

  [self->contentHandler startDocument];
  [self->contentHandler startPrefixMapping:@"" uri:self->prefixURI];
  
  length = [_rawString length];
  /* RFC2445:
     contentline        = name *(";" param ) ":" value CRLF
     ; When parsing a content line, folded lines MUST first
     ; be unfolded
  */
  r = NSMakeRange(0, 0);
  /* probably too optimistic */ 
  line = [[NSMutableString alloc] initWithCapacity:75 + 2];

  for(pos = 0; pos < length; pos++) {
    unichar c = [_rawString characterAtIndex:pos];
    
    if(c == '\r') {
      if(((length - 1) - pos) >= 1) {
        if([_rawString characterAtIndex:pos + 1] == '\n') {
          BOOL isAtEndOfLine = YES;
          /* test for folding first */
          if(((length - 1) - pos) >= 2) {
            unichar ws = [_rawString characterAtIndex:pos + 2];
            isAtEndOfLine = [whitespaceCharSet characterIsMember:ws] ? NO
                                                                     : YES;
            if(!isAtEndOfLine) {
              /* assemble part of line up to pos */
              if(r.length > 0) {
                [line appendString:[_rawString substringWithRange:r]];
              }
              /* unfold */
              pos += 2;
              r = NSMakeRange(pos + 1, 0); /* begin new range */
            }
          }
          if(isAtEndOfLine) {
            /* assemble part of line up to pos */
            if(r.length > 0) {
              [line appendString:[_rawString substringWithRange:r]];
            }
            [self _parseLine:line];
            /* reset line */
            [line deleteCharactersInRange:NSMakeRange(0, [line length])];
            pos += 1;
            r = NSMakeRange(pos + 1, 0); /* begin new range */
          }
        }
      }
      else {
        /* garbled last line! */
        if(debugOn) {
          NSLog(@"%s Last line is truncated, trying to parse anyways!",
                __PRETTY_FUNCTION__);
        }
      }
    }
    else if(c == '\n') { /* broken, non-standard */
      BOOL isAtEndOfLine = YES;
      /* test for folding first */
      if(((length - 1) - pos) >= 1) {
        unichar ws = [_rawString characterAtIndex:pos + 1];
        isAtEndOfLine = [whitespaceCharSet characterIsMember:ws] ? NO
                                                                 : YES;
        if(!isAtEndOfLine) {
          /* assemble part of line up to pos */
          if(r.length > 0) {
            [line appendString:[_rawString substringWithRange:r]];
          }
          /* unfold */
          pos += 1;
          r = NSMakeRange(pos + 1, 0); /* begin new range */
        }
      }
      if(isAtEndOfLine) {
        /* assemble part of line up to pos */
        if(r.length > 0) {
          [line appendString:[_rawString substringWithRange:r]];
        }
        [self _parseLine:line];
        /* reset line */
        [line deleteCharactersInRange:NSMakeRange(0, [line length])];
        r = NSMakeRange(pos + 1, 0); /* begin new range */
      }
    }
    else {
      r.length += 1;
    }
  }
  if(r.length > 0) {
    if(debugOn) {
      NSLog(@"%s Last line of iCal string is not properly terminated!",
            __PRETTY_FUNCTION__);
    }
    [line appendString:[_rawString substringWithRange:r]];
    [self _parseLine:line];
  }

  [line release];
  [self->contentHandler endPrefixMapping:@""];
  [self->contentHandler endDocument];
}

- (void)parseFromSource:(id)_source {
  if (debugOn)
    NSLog(@"%s: parse: %@", __PRETTY_FUNCTION__, _source);
  
  if ([_source isKindOfClass:[NSURL class]]) {
    if (debugOn) 
      NSLog(@"%s: trying to load URL...",__PRETTY_FUNCTION__);
    _source = [_source resourceDataUsingCache:NO];
  }
  
  if ([_source isKindOfClass:[NSData class]]) {
    // FIXME: Data is not always utf-8.....
    if (debugOn) 
      NSLog(@"%s: trying to decode data...",__PRETTY_FUNCTION__);
    _source = [[[NSString alloc]
		 initWithData:_source encoding:NSUTF8StringEncoding]
		 autorelease];
  }
  
  if ([_source isKindOfClass:[NSString class]]) {
    if (debugOn) 
      NSLog(@"%s: trying to parse string...",__PRETTY_FUNCTION__);
    [self _parseString:_source];
  } 
  else {
    if (debugOn) 
      NSLog(@"%s: unrecognizable source: %@", __PRETTY_FUNCTION__,_source);
    // FIXME: Return Error
  }
}

- (void)parseFromSource:(id)_source systemId:(NSString *)_sysId {
  [self parseFromSource:_source];
}

- (void)parseFromSystemId:(NSString *)_sysId {
  NSURL *url;
  
  if ((url = [NSURL URLWithString:_sysId]))
    [self parseFromSource:url systemId:_sysId];
}

/* debugging */

- (BOOL)isDebuggingEnabled {
  return debugOn;
}

@end /* VersitSaxDriver */
