/*
 * Copyright (c) 2008-2024 Jonathan Schleifer <js@nil.im>
 *
 * All rights reserved.
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License version 3.0 only,
 * as published by the Free Software Foundation.
 *
 * 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 Lesser General Public License
 * version 3.0 for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * version 3.0 along with this program. If not, see
 * <https://www.gnu.org/licenses/>.
 */

#include "config.h"

#define OF_XML_ELEMENT_M

#include <stdlib.h>
#include <string.h>

#import "OFXMLElement.h"
#import "OFArray.h"
#import "OFData.h"
#import "OFDictionary.h"
#import "OFStream.h"
#import "OFString.h"
#import "OFXMLAttribute.h"
#import "OFXMLCDATA.h"
#import "OFXMLCharacters.h"
#import "OFXMLElementBuilder.h"
#import "OFXMLNode+Private.h"
#import "OFXMLParser.h"

#import "OFInvalidArgumentException.h"
#import "OFInvalidFormatException.h"
#import "OFMalformedXMLException.h"
#import "OFUnboundNamespaceException.h"

@interface OFXMLElementElementBuilderDelegate: OFObject
    <OFXMLElementBuilderDelegate>
{
@public
	OFXMLElement *_element;
}
@end

@implementation OFXMLElementElementBuilderDelegate
- (void)elementBuilder: (OFXMLElementBuilder *)builder
       didBuildElement: (OFXMLElement *)element
{
	if (_element == nil)
		_element = [element retain];
}

- (void)dealloc
{
	[_element release];

	[super dealloc];
}
@end

@implementation OFXMLElement
@synthesize name = _name, namespace = _namespace;

+ (instancetype)elementWithName: (OFString *)name
{
	return [[[self alloc] initWithName: name] autorelease];
}

+ (instancetype)elementWithName: (OFString *)name
		    stringValue: (OFString *)stringValue
{
	return [[[self alloc] initWithName: name
			       stringValue: stringValue] autorelease];
}

+ (instancetype)elementWithName: (OFString *)name
		      namespace: (OFString *)namespace
{
	return [[[self alloc] initWithName: name
				 namespace: namespace] autorelease];
}

+ (instancetype)elementWithName: (OFString *)name
		      namespace: (OFString *)namespace
		    stringValue: (OFString *)stringValue
{
	return [[[self alloc] initWithName: name
				 namespace: namespace
			       stringValue: stringValue] autorelease];
}

+ (instancetype)elementWithXMLString: (OFString *)string
{
	return [[[self alloc] initWithXMLString: string] autorelease];
}

+ (instancetype)elementWithStream: (OFStream *)stream
{
	return [[[self alloc] initWithStream: stream] autorelease];
}

- (instancetype)init
{
	OF_INVALID_INIT_METHOD
}

- (instancetype)initWithName: (OFString *)name
{
	return [self initWithName: name namespace: nil stringValue: nil];
}

- (instancetype)initWithName: (OFString *)name
		 stringValue: (OFString *)stringValue
{
	return [self initWithName: name
			namespace: nil
		      stringValue: stringValue];
}

- (instancetype)initWithName: (OFString *)name
		   namespace: (OFString *)namespace
{
	self = [super of_init];

	@try {
		if (name == nil)
			@throw [OFInvalidArgumentException exception];

		_name = [name copy];
		_namespace = [namespace copy];

		_namespaces = [[OFMutableDictionary alloc]
		    initWithKeysAndObjects:
		    @"http://www.w3.org/XML/1998/namespace", @"xml",
		    @"http://www.w3.org/2000/xmlns/", @"xmlns", nil];
	} @catch (id e) {
		[self release];
		@throw e;
	}

	return self;
}

- (instancetype)initWithName: (OFString *)name
		   namespace: (OFString *)namespace
		 stringValue: (OFString *)stringValue
{
	self = [self initWithName: name namespace: namespace];

	@try {
		if (stringValue != nil)
			self.stringValue = stringValue;
	} @catch (id e) {
		[self release];
		@throw e;
	}

	return self;
}

- (instancetype)initWithXMLString: (OFString *)string
{
	void *pool;
	OFXMLElement *element;

	@try {
		OFXMLParser *parser;
		OFXMLElementBuilder *builder;
		OFXMLElementElementBuilderDelegate *delegate;

		if (string == nil)
			@throw [OFInvalidArgumentException exception];

		pool = objc_autoreleasePoolPush();

		parser = [OFXMLParser parser];
		builder = [OFXMLElementBuilder builder];
		delegate = [[[OFXMLElementElementBuilderDelegate alloc] init]
		    autorelease];

		parser.delegate = builder;
		builder.delegate = delegate;

		[parser parseString: string];

		if (!parser.hasFinishedParsing)
			@throw [OFMalformedXMLException
			    exceptionWithParser: parser];

		element = delegate->_element;
	} @catch (id e) {
		[self release];
		@throw e;
	}

	self = [self initWithName: element->_name
			namespace: element->_namespace];

	@try {
		[_attributes release];
		_attributes = [element->_attributes retain];
		[_namespaces release];
		_namespaces = [element->_namespaces retain];
		[_children release];
		_children = [element->_children retain];

		objc_autoreleasePoolPop(pool);
	} @catch (id e) {
		[self release];
		@throw e;
	}

	return self;
}

- (instancetype)initWithStream: (OFStream *)stream
{
	void *pool;
	OFXMLElement *element;

	@try {
		OFXMLParser *parser;
		OFXMLElementBuilder *builder;
		OFXMLElementElementBuilderDelegate *delegate;

		pool = objc_autoreleasePoolPush();

		parser = [OFXMLParser parser];
		builder = [OFXMLElementBuilder builder];
		delegate = [[[OFXMLElementElementBuilderDelegate alloc] init]
		    autorelease];

		parser.delegate = builder;
		builder.delegate = delegate;

		[parser parseStream: stream];

		if (!parser.hasFinishedParsing)
			@throw [OFMalformedXMLException
			    exceptionWithParser: parser];

		element = delegate->_element;
	} @catch (id e) {
		[self release];
		@throw e;
	}

	self = [self initWithName: element->_name
			namespace: element->_namespace];

	@try {
		[_attributes release];
		_attributes = [element->_attributes retain];
		[_namespaces release];
		_namespaces = [element->_namespaces retain];
		[_children release];
		_children = [element->_children retain];

		objc_autoreleasePoolPop(pool);
	} @catch (id e) {
		[self release];
		@throw e;
	}

	return self;
}

- (void)dealloc
{
	[_name release];
	[_namespace release];
	[_attributes release];
	[_namespaces release];
	[_children release];

	[super dealloc];
}

- (OFArray *)attributes
{
	return [[_attributes copy] autorelease];
}

- (void)setChildren: (OFArray *)children
{
	OFArray *old = _children;
	_children = [children mutableCopy];
	[old release];
}

- (OFArray *)children
{
	return [[_children copy] autorelease];
}

- (void)setStringValue: (OFString *)stringValue
{
	void *pool = objc_autoreleasePoolPush();

	self.children = [OFArray arrayWithObject:
	    [OFXMLCharacters charactersWithString: stringValue]];

	objc_autoreleasePoolPop(pool);
}

- (OFString *)stringValue
{
	OFMutableString *ret;

	if (_children.count == 0)
		return @"";

	ret = [OFMutableString string];

	for (OFXMLNode *child in _children) {
		void *pool = objc_autoreleasePoolPush();

		[ret appendString: child.stringValue];

		objc_autoreleasePoolPop(pool);
	}

	[ret makeImmutable];

	return ret;
}

- (OFString *)of_XMLStringWithDefaultNS: (OFString *)defaultNS
			     namespaces: (OFDictionary *)allNS
			    indentation: (unsigned int)indentation
				  level: (unsigned int)level OF_DIRECT
{
	void *pool;
	char *cString;
	size_t length, i;
	OFString *prefix, *ret;

	pool = objc_autoreleasePoolPush();

	/* Add the namespaces of the current element */
	if (allNS != nil) {
		OFEnumerator *keyEnumerator = [_namespaces keyEnumerator];
		OFEnumerator *objectEnumerator = [_namespaces objectEnumerator];
		OFMutableDictionary *tmp;
		OFString *key, *object;

		tmp = [[allNS mutableCopy] autorelease];

		while ((key = [keyEnumerator nextObject]) != nil &&
		    (object = [objectEnumerator nextObject]) != nil)
			[tmp setObject: object forKey: key];

		allNS = tmp;
	} else
		allNS = _namespaces;

	prefix = [allNS objectForKey:
	    (_namespace != nil ? _namespace : (OFString *)@"")];

	i = 0;
	length = _name.UTF8StringLength + 3 + (level * indentation);
	cString = OFAllocMemory(length, 1);

	@try {
		memset(cString + i, ' ', level * indentation);
		i += level * indentation;

		/* Start of tag */
		cString[i++] = '<';

		if (prefix.length > 0) {
			length += prefix.UTF8StringLength + 1;
			cString = OFResizeMemory(cString, length, 1);

			memcpy(cString + i, prefix.UTF8String,
			    prefix.UTF8StringLength);
			i += prefix.UTF8StringLength;
			cString[i++] = ':';
		}

		memcpy(cString + i, _name.UTF8String, _name.UTF8StringLength);
		i += _name.UTF8StringLength;

		/* xmlns if necessary */
		if (prefix.length == 0 && defaultNS != _namespace &&
		    ![defaultNS isEqual: _namespace]) {
			length += _namespace.UTF8StringLength + 9;
			cString = OFResizeMemory(cString, length, 1);

			memcpy(cString + i, " xmlns='", 8);
			i += 8;
			memcpy(cString + i, _namespace.UTF8String,
			    _namespace.UTF8StringLength);
			i += _namespace.UTF8StringLength;
			cString[i++] = '\'';

			defaultNS = _namespace;
		}

		/* Attributes */
		for (OFXMLAttribute *attribute in _attributes) {
			void *pool2 = objc_autoreleasePoolPush();
			const char *attributeNameCString =
			    attribute->_name.UTF8String;
			size_t attributeNameLength =
			    attribute->_name.UTF8StringLength;
			OFString *attributePrefix = nil;
			OFString *tmp =
			    attribute.stringValue.stringByXMLEscaping;
			char delimiter = (attribute->_useDoubleQuotes
			    ? '"' : '\'');

			if (attribute->_namespace != nil &&
			    [(attributePrefix = [allNS objectForKey:
			    attribute->_namespace]) length] == 0)
				@throw [OFUnboundNamespaceException
				    exceptionWithNamespace: attribute.namespace
						   element: self];

			length += attributeNameLength + (attributePrefix != nil
			    ? attributePrefix.UTF8StringLength + 1 : 0) +
			    tmp.UTF8StringLength + 4;
			cString = OFResizeMemory(cString, length, 1);

			cString[i++] = ' ';
			if (attributePrefix != nil) {
				memcpy(cString + i, attributePrefix.UTF8String,
				    attributePrefix.UTF8StringLength);
				i += attributePrefix.UTF8StringLength;
				cString[i++] = ':';
			}
			memcpy(cString + i, attributeNameCString,
			    attributeNameLength);
			i += attributeNameLength;
			cString[i++] = '=';
			cString[i++] = delimiter;
			memcpy(cString + i, tmp.UTF8String,
			    tmp.UTF8StringLength);
			i += tmp.UTF8StringLength;
			cString[i++] = delimiter;

			objc_autoreleasePoolPop(pool2);
		}

		/* Children */
		if (_children != nil) {
			OFMutableData *tmp = [OFMutableData data];
			bool indent;

			if (indentation > 0) {
				indent = true;

				for (OFXMLNode *child in _children) {
					if ([child isKindOfClass:
					    [OFXMLCharacters class]] ||
					    [child isKindOfClass:
					    [OFXMLCDATA class]]) {
						indent = false;
						break;
					}
				}
			} else
				indent = false;

			for (OFXMLNode *child in _children) {
				OFString *childString;
				unsigned int ind = (indent ? indentation : 0);

				if (ind)
					[tmp addItem: "\n"];

				if ([child isKindOfClass: [OFXMLElement class]])
					childString = [(OFXMLElement *)child
					    of_XMLStringWithDefaultNS: defaultNS
							   namespaces: allNS
							  indentation: ind
								level: level +
									   1];
				else {
					childString = child.XMLString;
					for (unsigned int j = 0;
					    j < ind * (level + 1); j++)
						[tmp addItem: " "];
				}

				[tmp addItems: childString.UTF8String
					count: childString.UTF8StringLength];
			}

			if (indent)
				[tmp addItem: "\n"];

			length += tmp.count + _name.UTF8StringLength + 2 +
			    (indent ? level * indentation : 0);
			cString = OFResizeMemory(cString, length, 1);

			cString[i++] = '>';

			memcpy(cString + i, tmp.items, tmp.count);
			i += tmp.count;

			if (indent) {
				memset(cString + i, ' ', level * indentation);
				i += level * indentation;
			}

			cString[i++] = '<';
			cString[i++] = '/';
			if (prefix.length > 0) {
				length += prefix.UTF8StringLength + 1;
				cString = OFResizeMemory(cString, length, 1);

				memcpy(cString + i, prefix.UTF8String,
				    prefix.UTF8StringLength);
				i += prefix.UTF8StringLength;
				cString[i++] = ':';
			}
			memcpy(cString + i, _name.UTF8String,
			    _name.UTF8StringLength);
			i += _name.UTF8StringLength;
		} else
			cString[i++] = '/';

		cString[i++] = '>';
		OFAssert(i == length);

		objc_autoreleasePoolPop(pool);

		ret = [OFString stringWithUTF8String: cString
					      length: length];
	} @finally {
		OFFreeMemory(cString);
	}
	return ret;
}

- (OFString *)XMLString
{
	return [self of_XMLStringWithDefaultNS: nil
				    namespaces: nil
				   indentation: 0
					 level: 0];
}

- (OFString *)XMLStringWithIndentation: (unsigned int)indentation
{
	return [self of_XMLStringWithDefaultNS: nil
				    namespaces: nil
				   indentation: indentation
					 level: 0];
}

- (OFString *)XMLStringWithDefaultNamespace: (OFString *)defaultNS
				indentation: (unsigned int)indentation
{
	return [self of_XMLStringWithDefaultNS: defaultNS
				    namespaces: nil
				   indentation: indentation
					 level: 0];
}

- (void)addAttribute: (OFXMLAttribute *)attribute
{
	if (![attribute isKindOfClass: [OFXMLAttribute class]])
		@throw [OFInvalidArgumentException exception];

	if (_attributes == nil)
		_attributes = [[OFMutableArray alloc] init];

	if ([self attributeForName: attribute->_name
			 namespace: attribute->_namespace] == nil)
		[_attributes addObject: attribute];
}

- (void)addAttributeWithName: (OFString *)name
		 stringValue: (OFString *)stringValue
{
	[self addAttributeWithName: name
			 namespace: nil
		       stringValue: stringValue];
}

- (void)addAttributeWithName: (OFString *)name
		   namespace: (OFString *)namespace
		 stringValue: (OFString *)stringValue
{
	void *pool = objc_autoreleasePoolPush();

	[self addAttribute: [OFXMLAttribute attributeWithName: name
						    namespace: namespace
						  stringValue: stringValue]];

	objc_autoreleasePoolPop(pool);
}

- (OFXMLAttribute *)attributeForName: (OFString *)attributeName
{
	for (OFXMLAttribute *attribute in _attributes)
		if (attribute->_namespace == nil &&
		    [attribute->_name isEqual: attributeName])
			return attribute;

	return nil;
}

- (OFXMLAttribute *)attributeForName: (OFString *)attributeName
			   namespace: (OFString *)attributeNS
{
	if (attributeNS == nil)
		return [self attributeForName: attributeName];

	for (OFXMLAttribute *attribute in _attributes)
		if ([attribute->_namespace isEqual: attributeNS] &&
		    [attribute->_name isEqual: attributeName])
			return attribute;

	return nil;
}

- (void)removeAttributeForName: (OFString *)attributeName
{
	OFXMLAttribute *const *objects = _attributes.objects;
	size_t count = _attributes.count;

	for (size_t i = 0; i < count; i++) {
		if (objects[i]->_namespace == nil &&
		    [objects[i]->_name isEqual: attributeName]) {
			[_attributes removeObjectAtIndex: i];

			return;
		}
	}
}

- (void)removeAttributeForName: (OFString *)attributeName
		     namespace: (OFString *)attributeNS
{
	OFXMLAttribute *const *objects;
	size_t count;

	if (attributeNS == nil) {
		[self removeAttributeForName: attributeName];
		return;
	}

	objects = _attributes.objects;
	count = _attributes.count;

	for (size_t i = 0; i < count; i++) {
		if ([objects[i]->_namespace isEqual: attributeNS] &&
		    [objects[i]->_name isEqual: attributeName]) {
			[_attributes removeObjectAtIndex: i];
				return;
		}
	}
}

- (void)setPrefix: (OFString *)prefix forNamespace: (OFString *)namespace
{
	if (prefix.length == 0)
		@throw [OFInvalidArgumentException exception];

	[_namespaces setObject: prefix forKey: namespace];
}

- (void)bindPrefix: (OFString *)prefix forNamespace: (OFString *)namespace
{
	[self setPrefix: prefix forNamespace: namespace];
	[self addAttributeWithName: prefix
			 namespace: @"http://www.w3.org/2000/xmlns/"
		       stringValue: namespace];
}

- (void)addChild: (OFXMLNode *)child
{
	if ([child isKindOfClass: [OFXMLAttribute class]])
		@throw [OFInvalidArgumentException exception];

	if (_children == nil)
		_children = [[OFMutableArray alloc] init];

	[_children addObject: child];
}

- (void)insertChild: (OFXMLNode *)child atIndex: (size_t)idx
{
	if ([child isKindOfClass: [OFXMLAttribute class]])
		@throw [OFInvalidArgumentException exception];

	if (_children == nil)
		_children = [[OFMutableArray alloc] init];

	[_children insertObject: child atIndex: idx];
}

- (void)insertChildren: (OFArray *)children atIndex: (size_t)idx
{
	for (OFXMLNode *node in children)
		if ([node isKindOfClass: [OFXMLAttribute class]])
			@throw [OFInvalidArgumentException exception];

	[_children insertObjectsFromArray: children atIndex: idx];
}

- (void)removeChild: (OFXMLNode *)child
{
	if ([child isKindOfClass: [OFXMLAttribute class]])
		@throw [OFInvalidArgumentException exception];

	[_children removeObject: child];
}

- (void)removeChildAtIndex: (size_t)idx
{
	[_children removeObjectAtIndex: idx];
}

- (void)replaceChild: (OFXMLNode *)child withNode: (OFXMLNode *)node
{
	if ([node isKindOfClass: [OFXMLAttribute class]] ||
	    [child isKindOfClass: [OFXMLAttribute class]])
		@throw [OFInvalidArgumentException exception];

	[_children replaceObject: child withObject: node];
}

- (void)replaceChildAtIndex: (size_t)idx withNode: (OFXMLNode *)node
{
	if ([node isKindOfClass: [OFXMLAttribute class]])
		@throw [OFInvalidArgumentException exception];

	[_children replaceObjectAtIndex: idx withObject: node];
}

- (OFXMLElement *)elementForName: (OFString *)elementName
{
	return [self elementsForName: elementName].firstObject;
}

- (OFXMLElement *)elementForName: (OFString *)elementName
		       namespace: (OFString *)elementNS
{
	return [self elementsForName: elementName
			   namespace: elementNS].firstObject;
}

- (OFArray *)elements
{
	OFMutableArray OF_GENERIC(OFXMLElement *) *ret = [OFMutableArray array];

	for (OFXMLNode *child in _children)
		if ([child isKindOfClass: [OFXMLElement class]])
			[ret addObject: (OFXMLElement *)child];

	[ret makeImmutable];

	return ret;
}

- (OFArray *)elementsForName: (OFString *)elementName
{
	OFMutableArray OF_GENERIC(OFXMLElement *) *ret = [OFMutableArray array];

	for (OFXMLNode *child in _children) {
		if ([child isKindOfClass: [OFXMLElement class]]) {
			OFXMLElement *element = (OFXMLElement *)child;

			if (element->_namespace == nil &&
			    [element->_name isEqual: elementName])
				[ret addObject: element];
		}
	}

	[ret makeImmutable];

	return ret;
}

- (OFArray *)elementsForNamespace: (OFString *)elementNS
{
	OFMutableArray OF_GENERIC(OFXMLElement *) *ret = [OFMutableArray array];

	for (OFXMLNode *child in _children) {
		if ([child isKindOfClass: [OFXMLElement class]]) {
			OFXMLElement *element = (OFXMLElement *)child;

			if (element->_name != nil &&
			    [element->_namespace isEqual: elementNS])
				[ret addObject: element];
		}
	}

	[ret makeImmutable];

	return ret;
}

- (OFArray *)elementsForName: (OFString *)elementName
		   namespace: (OFString *)elementNS
{
	OFMutableArray OF_GENERIC(OFXMLElement *) *ret;

	if (elementNS == nil)
		return [self elementsForName: elementName];

	ret = [OFMutableArray array];

	for (OFXMLNode *child in _children) {
		if ([child isKindOfClass: [OFXMLElement class]]) {
			OFXMLElement *element = (OFXMLElement *)child;

			if ([element->_namespace isEqual: elementNS] &&
			    [element->_name isEqual: elementName])
				[ret addObject: element];
		}
	}

	[ret makeImmutable];

	return ret;
}

- (bool)isEqual: (id)object
{
	OFXMLElement *element;

	if (object == self)
		return true;

	if (![object isKindOfClass: [OFXMLElement class]])
		return false;

	element = object;

	if (element->_name != _name && ![element->_name isEqual: _name])
		return false;
	if (element->_namespace != _namespace &&
	    ![element->_namespace isEqual: _namespace])
		return false;
	if (element->_attributes != _attributes &&
	    ![element->_attributes isEqual: _attributes])
		return false;
	if (element->_namespaces != _namespaces &&
	    ![element->_namespaces isEqual: _namespaces])
		return false;
	if (element->_children != _children &&
	    ![element->_children isEqual: _children])
		return false;

	return true;
}

- (unsigned long)hash
{
	unsigned long hash;

	OFHashInit(&hash);

	OFHashAddHash(&hash, _name.hash);
	OFHashAddHash(&hash, _namespace.hash);
	OFHashAddHash(&hash, _attributes.hash);
	OFHashAddHash(&hash, _namespaces.hash);
	OFHashAddHash(&hash, _children.hash);

	OFHashFinalize(&hash);

	return hash;
}

- (id)copy
{
	OFXMLElement *copy = [[OFXMLElement alloc] of_init];
	@try {
		copy->_name = [_name copy];
		copy->_namespace = [_namespace copy];
		copy->_attributes = [_attributes mutableCopy];
		copy->_namespaces = [_namespaces mutableCopy];
		copy->_children = [_children mutableCopy];
	} @catch (id e) {
		[copy release];
		@throw e;
	}

	return copy;
}
@end
