// Copyright 2000 Omni Development, Inc.  All rights reserved.
//
// This software may only be used and reproduced according to the
// terms in the file OmniSourceLicense.html, which should be
// distributed with this project and can also be found at
// http://www.omnigroup.com/DeveloperResources/OmniSourceLicense.html.

#import "NSString-OEXPExtensions.h"

#import <Foundation/Foundation.h>
#import <OmniBase/OmniBase.h>
#import <OmniFoundation/OmniFoundation.h>

#import "xmlparse.h"

RCS_ID("$Header: /Network/Source/CVS/OmniGroup/Frameworks/OmniExpat/FoundationExtensions.subproj/NSString-OEXPExtensions.m,v 1.3 2000/10/17 00:51:25 kc Exp $")

static unsigned int debugLevel = 0;
static NSString *OmniExpatElementStackKey = @"_omniExpat_elementStack";
static NSString *OmniExpatAttributeNameKey = @"_omniExpat_attributeName";
static NSString *OmniExpatAttributeValueKey = @"_omniExpat_attributeValue";

// Expat handlers for converting XML to a property list
void omniExpat_xml2plistStartElement(void *userData, const char *name, const char **attributes);
void omniExpat_xml2plistEndElement(void *userData, const char *name);
void omniExpat_xml2plistDataHandler(void *userData, const char *data, int len);

// Expat handlers for identing XML
void omniExpat_indentStartElement(void *userData, const char *name, const char **attributes);
void omniExpat_indentEndElement(void *userData, const char *name);
void omniExpat_indentDataHandler(void *userData, const char *data, int len);

@interface NSString (PrivateOEXPExtensions)

- (void)parseWithStartHandler:(XML_StartElementHandler)startHandler dataHandler:(XML_CharacterDataHandler)dataHandler endHandler:(XML_EndElementHandler)endHandler userData:(void *)userData;

@end

@implementation NSString (OEXPExtensions)

+ (void)didLoad;
{
    NSUserDefaults *userDefaults;

    userDefaults = [NSUserDefaults standardUserDefaults];
    debugLevel = [userDefaults integerForKey:@"OmniExpatDebugLevel"];
}

- (NSDictionary *)propertyListFromXMLFormat;
{
    NSMutableDictionary *parsedResult;
    NSMutableArray *elementStack;

    // Create the parser result dictionary which we'll load using the expat parser callbacks.  Also, create a mutable array to represent the elementStack and store it under a hidden key in the parser result.
    parsedResult = [NSMutableDictionary dictionary];
    elementStack = [NSMutableArray array];
    [parsedResult setObject:elementStack forKey:OmniExpatElementStackKey];

    // Engage parser
    [self parseWithStartHandler:omniExpat_xml2plistStartElement dataHandler:omniExpat_xml2plistDataHandler endHandler:omniExpat_xml2plistEndElement userData:parsedResult];
    
    [parsedResult removeObjectForKey:OmniExpatElementStackKey];

    return parsedResult;
}

- (NSString *)indentedStringFromXMLFormat;
{
    NSMutableDictionary *userInfo;
    NSMutableArray *elementStack;
    NSMutableArray *indentedLines;
    NSNumber *depth;

    userInfo = [NSMutableDictionary dictionary];
    elementStack = [NSMutableArray array];
    indentedLines = [NSMutableArray array];
    depth = [NSNumber numberWithInt:0];

    [userInfo setObject:elementStack forKey:@"elementStack"];
    [userInfo setObject:depth forKey:@"depth"];
    [userInfo setObject:indentedLines forKey:@"indentedLines"];

    // Engage parser
    [self parseWithStartHandler:omniExpat_indentStartElement dataHandler:omniExpat_indentDataHandler endHandler:omniExpat_indentEndElement userData:userInfo];

    return [[userInfo objectForKey:@"indentedLines"] componentsJoinedByString:@"\n"];
}

@end

// Expat handlers for converting XML to a property list

void omniExpat_xml2plistStartElement(void *userData, const char *name, const char **attributes)
{
    NSString *elementName;
    NSString *existingAttributeName;
    NSMutableDictionary *parsedResult;
    NSMutableDictionary *newElement;
    NSMutableDictionary *parentElement;
    NSMutableArray *elementStack;
    id previousElement;

    elementName = [NSString stringWithUTF8String:name];
    parsedResult = (NSMutableDictionary *)userData;
    elementStack = [parsedResult objectForKey:OmniExpatElementStackKey];

    if (debugLevel) {
        NSLog(@"Start: %@", elementName);

        if (debugLevel > 1)
            NSLog(@"Parsed Result: %@", parsedResult);
    }

    [parsedResult removeObjectForKey:OmniExpatAttributeValueKey];

    if (!(parentElement = [elementStack lastObject]))
        parentElement = parsedResult;

    newElement = [NSMutableDictionary dictionary];

    // Element population
    if (*attributes) {
        char **attributeNameIndex;

        if (debugLevel)
            NSLog(@"Setting attributes for new element");

        for (attributeNameIndex = attributes; *attributeNameIndex != NULL; attributeNameIndex += 2 /* Step to next key/value pair */) {
            NSString *attributeName;
            NSString *attributeValue;

            attributeName = [NSString stringWithCString:*attributeNameIndex];
            if ((attributeValue = [NSString stringWithCString:*(attributeNameIndex + 1)]))
                [newElement setObject:attributeValue forKey:attributeName];
        }
    }

    // Promotion
    if ((existingAttributeName = [parsedResult objectForKey:OmniExpatAttributeNameKey])) {
        NSMutableDictionary *promotedElement;

        if (debugLevel)
            NSLog(@"Promoting the element which we previously thought was an attribute to a full element and pushing it on the element stack.");

        promotedElement = [NSMutableDictionary dictionary];
        [parentElement setObject:promotedElement forKey:existingAttributeName];
        [elementStack addObject:promotedElement];
        [parsedResult removeObjectForKey:OmniExpatAttributeNameKey];

        parentElement = promotedElement;
    }

    // Element determination
    if ((previousElement = [parentElement objectForKey:elementName])) {
        NSMutableArray *elementGroup;

        // Since the parent has an element by the same name as the current element, add the current element to the parent, coercing the previous value into an array if necessary.
        if (debugLevel)
            NSLog(@"Detected previous element of same name stored in parent.  Adding current element to parent instead of assigning element value to parent");
        
        if ([previousElement isKindOfClass:[NSMutableArray class]] == NO) {
            if (debugLevel)
                NSLog(@"Coercing previous element into an array containing itself");

            elementGroup = [NSMutableArray array];
            [elementGroup addObject:previousElement];
            [parentElement setObject:elementGroup forKey:elementName];
        } else
            elementGroup = previousElement;

        if (debugLevel)
            NSLog(@"Adding element to the element stack");
        
        [elementGroup addObject:newElement];
        [elementStack addObject:newElement];

        [parsedResult removeObjectForKey:OmniExpatAttributeNameKey];
    } else {
        if (*attributes == NULL) {
            // Might be an element

            if (debugLevel)
                NSLog(@"This element looks like an attribute key.  Assigning it as such.");

            [parsedResult setObject:elementName forKey:OmniExpatAttributeNameKey];
        } else {
            // Definitely an element
            
            if (debugLevel)
                NSLog(@"Pushing new element onto the element stack.");

            [parentElement setObject:newElement forKey:elementName];
            [elementStack addObject:newElement];
        }
    }
}

void omniExpat_xml2plistEndElement(void *userData, const char *name)
{
    NSString *elementName;
    NSString *attributeName;
    NSMutableDictionary *parsedResult;
    NSMutableArray *elementStack;

    elementName = [NSString stringWithUTF8String:name];
    parsedResult = (NSMutableDictionary *)userData;
    elementStack = [parsedResult objectForKey:OmniExpatElementStackKey];

    if (debugLevel) {
        NSLog(@"End: %@", elementName);

        if (debugLevel > 1)
            NSLog(@"Parsed Result: %@", parsedResult);
    }

    if ((attributeName = [parsedResult objectForKey:OmniExpatAttributeNameKey]) != nil) {
        NSMutableDictionary *element;
        NSString *attributeValue;
        
        if (debugLevel)
            NSLog(@"This element was representing the value for the %@ attribute.  Applying that value to the last element on the element stack.", attributeName);

        element = [elementStack lastObject];
        if ((attributeValue = [parsedResult objectForKey:OmniExpatAttributeValueKey]))
            [element setObject:[attributeValue stringByRemovingSurroundingWhitespace] forKey:attributeName];

        [parsedResult removeObjectForKey:OmniExpatAttributeNameKey];
        [parsedResult removeObjectForKey:OmniExpatAttributeValueKey];
    } else {
        if (debugLevel)
            NSLog(@"Removing the %@ element from the element stack.", elementName);

        [elementStack removeLastObject];
    }
}

void omniExpat_xml2plistDataHandler(void *userData, const char *data, int len)
{
    NSString *dataString;
    NSString *attributeName;
    NSData *utf8Data;
    NSMutableDictionary *parsedResult;
    NSMutableArray *elementStack;

    parsedResult = (NSMutableDictionary *)userData;
    elementStack = [parsedResult objectForKey:OmniExpatElementStackKey];

    utf8Data = [NSData dataWithBytes:data length:len];
    dataString = [[[NSString alloc] initWithData:utf8Data encoding:NSUTF8StringEncoding] autorelease];

    if (debugLevel) {
        NSLog(@"Data: '%@'", dataString);

        if (debugLevel > 1)
            NSLog(@"Parsed Result: %@", parsedResult);
    }

    // The only CDATA we care about is that which represents the value to an associated attribute.
    if ((attributeName = [parsedResult objectForKey:OmniExpatAttributeNameKey]) != nil) {
        NSString *existingAttributeValue;

        if (debugLevel)
            NSLog(@"This cdata represents the value to the %@ attribute.  Storing attribute value in the parsed result for later application during element end processing.", attributeName);
        
        if ((existingAttributeValue = [parsedResult objectForKey:OmniExpatAttributeValueKey]) != nil)
            [parsedResult setObject:[existingAttributeValue stringByAppendingString:dataString] forKey:OmniExpatAttributeValueKey];
        else
            [parsedResult setObject:dataString forKey:OmniExpatAttributeValueKey];
    } else {
        if (debugLevel)
            NSLog(@"Ignoring cdata since it probably represents whitespace between elements.");
    }
}

// Expat handlers for identing XML

void omniExpat_indentStartElement(void *userData, const char *name, const char **attributes)
{
    NSString *elementName;
    NSMutableDictionary *userInfo;
    NSMutableArray *elementStack;
    NSMutableArray *indentedLines;
    NSMutableString *element;
    unsigned int originalDepth, depth;

    elementName = [NSString stringWithUTF8String:name];
    element = [NSMutableString string];
    
    userInfo = (NSMutableDictionary *)userData;
    elementStack = [userInfo objectForKey:@"elementStack"];
    indentedLines = [userInfo objectForKey:@"indentedLines"];
    originalDepth = [[userInfo objectForKey:@"depth"] intValue];
    depth = originalDepth;

    // Indent by the depth
    while (depth--)
        [element appendString:@"    "];

    if (*attributes == NULL)
        [element appendFormat:@"<%@>", elementName];
    else {
        char **attributeNameIndex;

        [element appendFormat:@"<%@", elementName];

        for (attributeNameIndex = attributes; *attributeNameIndex != NULL; attributeNameIndex += 2 /* Step to next key/value pair */) {
            NSString *attributeName;
            NSString *attributeValue;

            attributeName = [NSString stringWithCString:*attributeNameIndex];
            if ((attributeValue = [NSString stringWithCString:*(attributeNameIndex + 1)]))
                [element appendFormat:@" %@=\"%@\"", attributeName, attributeValue];
        }
        [element appendString:@">"];
    }

    [elementStack addObject:elementName];
    [indentedLines addObject:element];
    [userInfo setObject:[NSNumber numberWithInt:originalDepth + 1] forKey:@"depth"];
}

void omniExpat_indentEndElement(void *userData, const char *name)
{
    NSString *elementName;
    NSMutableDictionary *userInfo;
    NSMutableArray *elementStack;
    NSMutableArray *indentedLines;
    NSMutableString *element;
    unsigned int originalDepth, depth;

    elementName = [NSString stringWithUTF8String:name];
    element = [NSMutableString string];
    
    userInfo = (NSMutableDictionary *)userData;
    elementStack = [userInfo objectForKey:@"elementStack"];
    indentedLines = [userInfo objectForKey:@"indentedLines"];
    originalDepth = [[userInfo objectForKey:@"depth"] intValue];
    depth = originalDepth;

    depth--;

    // Indent by the depth
    while (depth--)
        [element appendString:@"    "];

    [element appendFormat:@"</%@>", [elementStack lastObject]];

    [elementStack removeLastObject];
    [indentedLines addObject:element];
    [userInfo setObject:[NSNumber numberWithInt:originalDepth - 1] forKey:@"depth"];
}

void omniExpat_indentDataHandler(void *userData, const char *data, int len)
{
    NSString *dataString;
    NSData *utf8Data;
    NSMutableDictionary *userInfo;
    NSMutableArray *indentedLines;
    unsigned int depth;

    userInfo = (NSMutableDictionary *)userData;
    indentedLines = [userInfo objectForKey:@"indentedLines"];
    depth = [[userInfo objectForKey:@"depth"] intValue];
    
    utf8Data = [NSData dataWithBytes:data length:len];
    dataString = [[[NSString alloc] initWithData:utf8Data encoding:NSUTF8StringEncoding] autorelease];
    dataString = [dataString stringByRemovingSurroundingWhitespace];

    if ([dataString length]) {
        NSMutableString *indentionString;

        indentionString = [NSMutableString string];
        while (depth--)
            [indentionString appendString:@"    "];

        [indentedLines addObject:[indentionString stringByAppendingString:dataString]];
    }

}

// Private

@implementation NSString (PrivateOEXPExtensions)

- (void)parseWithStartHandler:(XML_StartElementHandler)startHandler dataHandler:(XML_CharacterDataHandler)dataHandler endHandler:(XML_EndElementHandler)endHandler userData:(void *)userData;
{
    XML_Parser parser;
    const char *stringChars;

    parser = XML_ParserCreate("UTF-8");
    stringChars = [self UTF8String];

    XML_SetUserData(parser, userData);
    XML_SetElementHandler(parser, startHandler, endHandler);
    XML_SetCharacterDataHandler(parser, dataHandler);

    // Pass the entire UTF8 representation of the string to the parser
    if (XML_Parse(parser, stringChars, strlen(stringChars), YES /* isFinal */) == 0) {
        int errorCode;
        const char *errorDescription;
        int lineNumber;

        errorCode = XML_GetErrorCode(parser);
        errorDescription = XML_ErrorString(errorCode);
        lineNumber = XML_GetCurrentLineNumber(parser);

        if (debugLevel)
            NSLog(@"Failure parsing string, error code: %d, line number: %d, error description: %s", errorCode, lineNumber, errorDescription);

        XML_ParserFree(parser);

        [NSException raise:NSInvalidArgumentException format:@"Unable to parse string as XML content, expat error code: %d, line number: %d, error description: %s", errorCode, lineNumber, errorDescription];
    }

    // Free parser and remove element stack from parsed result
    XML_ParserFree(parser);
}

@end

