//
//  DTHTMLAttributedStringBuilder.m
//  DTCoreText
//
//  Created by Oliver Drobnik on 21.01.12.
//  Copyright (c) 2012 Drobnik.com. All rights reserved.
//

#import "DTCoreText.h"
#import "DTHTMLAttributedStringBuilder.h"

#import "DTTextHTMLElement.h"
#import "DTBreakHTMLElement.h"
#import "DTStylesheetHTMLElement.h"
#import "DTTextAttachmentHTMLElement.h"
#import "DTLog.h"

#if DEBUG_LOG_METRICS
#import "NSString+DTFormatNumbers.h"
#endif

@interface DTHTMLAttributedStringBuilder ()

- (void)_registerTagStartHandlers;
- (void)_registerTagEndHandlers;

@end


@implementation DTHTMLAttributedStringBuilder
{
	NSData *_data;
	NSDictionary *_options;
	BOOL _shouldKeepDocumentNodeTree;
	
	// settings for parsing
	CGFloat _textScale;
	DTColor *_defaultLinkColor;
	DTCSSStylesheet *_globalStyleSheet;
	NSURL *_baseURL;
	DTCoreTextFontDescriptor *_defaultFontDescriptor;
	DTCoreTextParagraphStyle *_defaultParagraphStyle;
	
	// root node inherits these defaults
	DTHTMLElement *_defaultTag;
	
	// parsing state, accessed from inside blocks
	NSMutableAttributedString *_tmpString;
	
	// GCD
	dispatch_queue_t _stringAssemblyQueue;
	dispatch_group_t _stringAssemblyGroup;
	dispatch_queue_t _dataParsingQueue;
	dispatch_group_t _dataParsingGroup;
	dispatch_queue_t _treeBuildingQueue;;
	dispatch_group_t _treeBuildingGroup;
	
	// lookup table for blocks that deal with begin and end tags
	NSMutableDictionary *_tagStartHandlers;
	NSMutableDictionary *_tagEndHandlers;
	
	DTHTMLAttributedStringBuilderWillFlushCallback _willFlushCallback;
	BOOL _shouldProcessCustomHTMLAttributes;
	
	// new parsing
	DTHTMLElement *_rootNode;
	DTHTMLElement *_bodyElement;
	DTHTMLElement *_currentTag;
	BOOL _ignoreParseEvents; // ignores events from parser after first HTML tag was finished
	BOOL _ignoreInlineStyles; // ignores style blocks attached on elements
}

- (id)initWithHTML:(NSData *)data options:(NSDictionary *)options documentAttributes:(NSDictionary * __autoreleasing*)docAttributes
{
	self = [super init];
	if (self)
	{
		_data = data;
		_options = options;
		
		// documentAttributes ignored for now
		
		// GCD setup
		_stringAssemblyQueue = dispatch_queue_create("DTHTMLAttributedStringBuilder", 0);
		_stringAssemblyGroup = dispatch_group_create();
		_dataParsingQueue = dispatch_queue_create("DTHTMLAttributedStringBuilderParser", 0);
		_dataParsingGroup = dispatch_group_create();
		_treeBuildingQueue = dispatch_queue_create("DTHTMLAttributedStringBuilderParser Tree Queue", 0);
		_treeBuildingGroup = dispatch_group_create();
	}
	
	return self;
}

- (void)dealloc
{
#if !OS_OBJECT_USE_OBJC
	dispatch_release(_stringAssemblyQueue);
	dispatch_release(_stringAssemblyGroup);
	dispatch_release(_dataParsingQueue);
	dispatch_release(_dataParsingGroup);
	dispatch_release(_treeBuildingQueue);
	dispatch_release(_treeBuildingGroup);
#endif
}

- (BOOL)_buildString
{
#if DEBUG_LOG_METRICS
	// metrics: get start time
	CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
#endif
	
	// only with valid data
	if (![_data length])
	{
		return NO;
	}
	
	// register default handlers
	[self _registerTagStartHandlers];
	[self _registerTagEndHandlers];
	
 	// Specify the appropriate text encoding for the passed data, default is UTF8
	NSString *textEncodingName = [_options objectForKey:NSTextEncodingNameDocumentOption];
	NSStringEncoding encoding = NSUTF8StringEncoding; // default
	
	if (textEncodingName)
	{
		CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((__bridge CFStringRef)textEncodingName);
		encoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding);
	}
	
#if DTCORETEXT_SUPPORT_NS_ATTRIBUTES
	
	// custom option to use iOS 6 attributes if running on iOS 6
	if ([[_options objectForKey:DTUseiOS6Attributes] boolValue])
	{
#if TARGET_OS_IPHONE
		// NS-attributes only supported running on iOS 6.0 or greater
		if (floor(NSFoundationVersionNumber) >= DTNSFoundationVersionNumber_iOS_6_0)
		{
			___useiOS6Attributes = YES;
		}
		else
		{
			___useiOS6Attributes = NO;
		}
#else
		// Mac generally supports it
		___useiOS6Attributes = YES;
#endif
	}
	
#endif

	
	// custom option to scale text
	_textScale = [[_options objectForKey:NSTextSizeMultiplierDocumentOption] floatValue];
	if (!_textScale)
	{
		_textScale = 1.0f;
	}
	
	// use baseURL from options if present
	_baseURL = [_options objectForKey:NSBaseURLDocumentOption];
	
	// the combined style sheet for entire document
	_globalStyleSheet = [[DTCSSStylesheet defaultStyleSheet] copy];
	
	// do we have a default style sheet passed as option?
	DTCSSStylesheet *defaultStylesheet = [_options objectForKey:DTDefaultStyleSheet];
	if (defaultStylesheet)
	{
		// merge the default styles to the combined style sheet
		[_globalStyleSheet mergeStylesheet:defaultStylesheet];
	}
	
	// for performance reasons we will return this mutable string
	_tmpString = [[NSMutableAttributedString alloc] init];
	
	// base tag with font defaults
	_defaultFontDescriptor = [[DTCoreTextFontDescriptor alloc] initWithFontAttributes:nil];
	
	
	// set the default font size
	CGFloat defaultFontSize = 12.0f;
	
	NSNumber *defaultFontSizeNumber = [_options objectForKey:DTDefaultFontSize];
	
	if (defaultFontSizeNumber)
	{
		defaultFontSize = [defaultFontSizeNumber floatValue];
	}
	
	_defaultFontDescriptor.pointSize = defaultFontSize * _textScale;
	
	NSString *defaultFontFamily = [_options objectForKey:DTDefaultFontFamily];
	
	if (defaultFontFamily)
	{
		_defaultFontDescriptor.fontFamily = defaultFontFamily;
	}
	else
	{
		_defaultFontDescriptor.fontFamily = @"Times New Roman";
	}

	NSString *defaultFontName = [_options objectForKey:DTDefaultFontName];

	if (defaultFontName) {
		_defaultFontDescriptor.fontName = defaultFontName;
	}

	
	_defaultLinkColor = [_options objectForKey:DTDefaultLinkColor];
	
	if (_defaultLinkColor)
	{
		if ([_defaultLinkColor isKindOfClass:[NSString class]])
		{
			// convert from string to color
			_defaultLinkColor = DTColorCreateWithHTMLName((NSString *)_defaultLinkColor);
		}
		
		// get hex code for the passed color
		NSString *colorHex = DTHexStringFromDTColor(_defaultLinkColor);
		
		// overwrite the style
		NSString *styleBlock = [NSString stringWithFormat:@"a {color:#%@;}", colorHex];
		[_globalStyleSheet parseStyleBlock:styleBlock];
	}
	
	// default is to have A underlined
	NSNumber *linkDecorationDefault = [_options objectForKey:DTDefaultLinkDecoration];
	
	if (linkDecorationDefault)
	{
		if (![linkDecorationDefault boolValue])
		{
			// remove default decoration
			[_globalStyleSheet parseStyleBlock:@"a {text-decoration:none;}"];
		}
	}
	
	DTColor *defaultLinkHighlightColor = [_options objectForKey:DTDefaultLinkHighlightColor];
	
	if (defaultLinkHighlightColor)
	{
		if ([defaultLinkHighlightColor isKindOfClass:[NSString class]])
		{
			// convert from string to color
			defaultLinkHighlightColor = DTColorCreateWithHTMLName((NSString *)defaultLinkHighlightColor);
		}
		
		// get hex code for the passed color
		NSString *colorHex = DTHexStringFromDTColor(defaultLinkHighlightColor);
		
		// overwrite the style
		NSString *styleBlock = [NSString stringWithFormat:@"a:active {color:#%@;}", colorHex];
		[_globalStyleSheet parseStyleBlock:styleBlock];
	}
	
	// default paragraph style
	_defaultParagraphStyle = [DTCoreTextParagraphStyle defaultParagraphStyle];
	
	NSNumber *defaultLineHeightMultiplierNum = [_options objectForKey:DTDefaultLineHeightMultiplier];
	
	if (defaultLineHeightMultiplierNum)
	{
		CGFloat defaultLineHeightMultiplier = [defaultLineHeightMultiplierNum floatValue];
		_defaultParagraphStyle.lineHeightMultiple = defaultLineHeightMultiplier;
	}
	
	NSNumber *defaultTextAlignmentNum = [_options objectForKey:DTDefaultTextAlignment];
	
	if (defaultTextAlignmentNum)
	{
		_defaultParagraphStyle.alignment = (CTTextAlignment)[defaultTextAlignmentNum integerValue];
	}
	
	NSNumber *defaultFirstLineHeadIndent = [_options objectForKey:DTDefaultFirstLineHeadIndent];
	if (defaultFirstLineHeadIndent)
	{
		_defaultParagraphStyle.firstLineHeadIndent = [defaultFirstLineHeadIndent integerValue];
	}
	
	NSNumber *defaultHeadIndent = [_options objectForKey:DTDefaultHeadIndent];
	if (defaultHeadIndent)
	{
		_defaultParagraphStyle.headIndent = [defaultHeadIndent integerValue];
	}
	
	_defaultTag = [[DTHTMLElement alloc] init];
	_defaultTag.fontDescriptor = _defaultFontDescriptor;
	_defaultTag.paragraphStyle = _defaultParagraphStyle;
	_defaultTag.textScale = _textScale;
	_defaultTag.currentTextSize = _defaultFontDescriptor.pointSize;
	
#if DTCORETEXT_FIX_14684188
	// workaround, only necessary while rdar://14684188 is not fixed
	_defaultTag.textColor = [UIColor blackColor];
#endif
	
	id defaultColor = [_options objectForKey:DTDefaultTextColor];
	if (defaultColor)
	{
		if ([defaultColor isKindOfClass:[DTColor class]])
		{
			// already a DTColor
			_defaultTag.textColor = defaultColor;
		}
		else
		{
			// need to convert first
			_defaultTag.textColor = DTColorCreateWithHTMLName(defaultColor);
		}
	}
	
	_shouldProcessCustomHTMLAttributes = [[_options objectForKey:DTProcessCustomHTMLAttributes] boolValue];
	
	// ignore inline styles if option is passed
	_ignoreInlineStyles = [[_options objectForKey:DTIgnoreInlineStylesOption] boolValue];
	
	// create a parser
	DTHTMLParser *parser = [[DTHTMLParser alloc] initWithData:_data encoding:encoding];
	parser.delegate = (id)self;
	
	__block BOOL result;
	dispatch_group_async(_dataParsingGroup, _dataParsingQueue, ^{ result = [parser parse]; });
	
	// wait until all string assembly is complete
	dispatch_group_wait(_dataParsingGroup, DISPATCH_TIME_FOREVER);
	dispatch_group_wait(_treeBuildingGroup, DISPATCH_TIME_FOREVER);
	dispatch_group_wait(_stringAssemblyGroup, DISPATCH_TIME_FOREVER);
	
	// clean up handlers because they retained self
	_tagStartHandlers = nil;
	_tagEndHandlers = nil;

#if DEBUG_LOG_METRICS
	// metrics: get end time
	CFAbsoluteTime endTime = CFAbsoluteTimeGetCurrent();
	
	// output metrics
	DTLogInfo((@"DTCoreText created string from %@ HTML in %.2f sec", [NSString stringByFormattingBytes:[_data length]], endTime-startTime);
#endif
	
	return result;
}

- (NSAttributedString *)generatedAttributedString
{
	if (!_tmpString)
	{
		[self _buildString];
	}
	
	return _tmpString;
}

#pragma mark GCD

- (void)_registerTagStartHandlers
{
	if (_tagStartHandlers)
	{
		return;
	}
	
	_tagStartHandlers = [[NSMutableDictionary alloc] init];
	
	
	void (^blockquoteBlock)(void) = ^
	{
		_currentTag.paragraphStyle.headIndent += (CGFloat)25.0 * _textScale;
		_currentTag.paragraphStyle.firstLineHeadIndent = _currentTag.paragraphStyle.headIndent;
		_currentTag.paragraphStyle.paragraphSpacing = _defaultFontDescriptor.pointSize;
	};
	
	[_tagStartHandlers setObject:[blockquoteBlock copy] forKey:@"blockquote"];
	
	
	void (^aBlock)(void) = ^
	{
		if (_currentTag.isColorInherited || !_currentTag.textColor)
		{
			_currentTag.textColor = _defaultLinkColor;
			_currentTag.isColorInherited = NO;
		}
		
		// the name attribute of A becomes an anchor
		_currentTag.anchorName = [_currentTag attributeForKey:@"name"];

		// remove line breaks and whitespace in links
		NSString *cleanString = [[_currentTag attributeForKey:@"href"] stringByReplacingOccurrencesOfString:@"\n" withString:@""];
		cleanString = [cleanString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
		
		if (![cleanString length])
		{
			// no valid href
			return;
		}
		
		NSURL *link = [NSURL URLWithString:cleanString];
		
		// deal with relative URL
		if (![link scheme])
		{
			if ([cleanString length])
			{
				link = [NSURL URLWithString:cleanString relativeToURL:_baseURL];
				
				if (!link)
				{
					// NSURL did not like the link, so let's encode it
					cleanString = [cleanString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
					
					link = [NSURL URLWithString:cleanString relativeToURL:_baseURL];
				}
			}
			else
			{
				link = _baseURL;
			}
		}
		
		_currentTag.link = link;
	};
	
	[_tagStartHandlers setObject:[aBlock copy] forKey:@"a"];
	
	void (^listBlock)(void) = ^
	{
		_currentTag.paragraphStyle.firstLineHeadIndent = _currentTag.paragraphStyle.headIndent;
		
		// create the appropriate list style from CSS
		DTCSSListStyle *newListStyle = [_currentTag listStyle];
		
		// append this list style to the current paragraph style text lists
		NSMutableArray *textLists = [_currentTag.paragraphStyle.textLists mutableCopy];
		
		if (!textLists)
		{
			textLists = [NSMutableArray array];
		}
		
		[textLists addObject:newListStyle];
		
		_currentTag.paragraphStyle.textLists = textLists;
	};
	
	[_tagStartHandlers setObject:[listBlock copy] forKey:@"ul"];
	[_tagStartHandlers setObject:[listBlock copy] forKey:@"ol"];
	
	void (^h1Block)(void) = ^
	{
		_currentTag.headerLevel = 1;
	};
	[_tagStartHandlers setObject:[h1Block copy] forKey:@"h1"];
	
	void (^h2Block)(void) = ^
	{
		_currentTag.headerLevel = 2;
	};
	[_tagStartHandlers setObject:[h2Block copy] forKey:@"h2"];
	
	
	void (^h3Block)(void) = ^
	{
		_currentTag.headerLevel = 3;
	};
	[_tagStartHandlers setObject:[h3Block copy] forKey:@"h3"];
	
	
	void (^h4Block)(void) = ^
	{
		_currentTag.headerLevel = 4;
	};
	[_tagStartHandlers setObject:[h4Block copy] forKey:@"h4"];
	
	
	void (^h5Block)(void) = ^
	{
		_currentTag.headerLevel = 5;
	};
	[_tagStartHandlers setObject:[h5Block copy] forKey:@"h5"];
	
	
	void (^h6Block)(void) = ^
	{
		_currentTag.headerLevel = 6;
	};
	[_tagStartHandlers setObject:[h6Block copy] forKey:@"h6"];
	
	
	void (^fontBlock)(void) = ^
	{
		CGFloat pointSize;
		
		NSString *sizeAttribute = [_currentTag attributeForKey:@"size"];
		
		if (sizeAttribute)
		{
			NSInteger sizeValue = [sizeAttribute intValue];
			
			switch (sizeValue)
			{
				case 1:
					pointSize = _textScale * 10.0f;
					break;
				case 2:
					pointSize = _textScale * 13.0f;
					break;
				case 3:
					pointSize = _textScale * 16.0f;
					break;
				case 4:
					pointSize = _textScale * 18.0f;
					break;
				case 5:
					pointSize = _textScale * 24.0f;
					break;
				case 6:
					pointSize = _textScale * 32.0f;
					break;
				case 7:
					pointSize = _textScale * 48.0f;
					break;
				default:
					pointSize = _defaultFontDescriptor.pointSize;
					break;
			}
		}
		else
		{
			// size is inherited
			pointSize = _currentTag.fontDescriptor.pointSize; 
		}
		
		NSString *face = [_currentTag attributeForKey:@"face"];
		
		if (face)
		{
			// create a temp font with this face
			CTFontRef font = CTFontCreateWithName((__bridge CFStringRef)face, pointSize, NULL);
			
			_currentTag.fontDescriptor = [DTCoreTextFontDescriptor fontDescriptorForCTFont:font];
			
			CFRelease(font);
		}
		else
		{
			// modify inherited descriptor
			_currentTag.fontDescriptor.pointSize = pointSize;
		}
		
		NSString *color = [_currentTag attributeForKey:@"color"];
		
		if (color)
		{
			_currentTag.textColor = DTColorCreateWithHTMLName(color);
		}
	};
	
	[_tagStartHandlers setObject:[fontBlock copy] forKey:@"font"];
	
	
	void (^pBlock)(void) = ^
	{
		_currentTag.paragraphStyle.firstLineHeadIndent = _currentTag.paragraphStyle.headIndent + _defaultParagraphStyle.firstLineHeadIndent;
	};
	
	[_tagStartHandlers setObject:[pBlock copy] forKey:@"p"];
}

- (void)_registerTagEndHandlers
{
	if (_tagEndHandlers)
	{
		return;
	}
	
	_tagEndHandlers = [[NSMutableDictionary alloc] init];
	
	void (^objectBlock)(void) = ^
	{
		if ([_currentTag isKindOfClass:[DTTextAttachmentHTMLElement class]])
		{
			if ([_currentTag.textAttachment isKindOfClass:[DTObjectTextAttachment class]])
			{
				DTObjectTextAttachment *objectAttachment = (DTObjectTextAttachment *)_currentTag.textAttachment;
				
				// transfer the child nodes to the attachment
				objectAttachment.childNodes = [_currentTag.childNodes copy];
			}
		}
	};
	
	[_tagEndHandlers setObject:[objectBlock copy] forKey:@"object"];

	void (^videoBlock)(void) = ^
	{
		if ([_currentTag isKindOfClass:[DTTextAttachmentHTMLElement class]])
		{
			DTTextAttachmentHTMLElement *attachmentElement = (DTTextAttachmentHTMLElement *)_currentTag;
			
			if ([attachmentElement.textAttachment isKindOfClass:[DTVideoTextAttachment class]])
			{
				DTVideoTextAttachment *videoAttachment = (DTVideoTextAttachment *)attachmentElement.textAttachment;
				
				// find first child that has a source
				if (!videoAttachment.contentURL)
				{
					for (DTHTMLElement *child in attachmentElement.childNodes)
					{
						if ([child.name isEqualToString:@"source"])
						{
							NSString *src = [child attributeForKey:@"src"];
							
							// content URL
							videoAttachment.contentURL = [NSURL URLWithString:src relativeToURL:_baseURL];
							
							break;
						}
					}
				}
			}
		}
	};
	
	[_tagEndHandlers setObject:[videoBlock copy] forKey:@"video"];
	
	void (^styleBlock)(void) = ^
	{
		DTCSSStylesheet *localSheet = [(DTStylesheetHTMLElement *)_currentTag stylesheet];
		[_globalStyleSheet mergeStylesheet:localSheet];
	};
	
	[_tagEndHandlers setObject:[styleBlock copy] forKey:@"style"];
	
	
	void (^linkBlock)(void) = ^
	{
		NSString *href = [_currentTag attributeForKey:@"href"];
		NSString *type = [[_currentTag attributeForKey:@"type"] lowercaseString];
		
		if ([type isEqualToString:@"text/css"])
		{
			NSURL *stylesheetURL = [NSURL URLWithString:href relativeToURL:_baseURL];
			if ([stylesheetURL isFileURL])
			{
				NSString *stylesheetContent = [NSString stringWithContentsOfURL:stylesheetURL encoding:NSUTF8StringEncoding error:nil];
				if (stylesheetContent)
				{
					DTCSSStylesheet *localSheet = [[DTCSSStylesheet alloc] initWithStyleBlock:stylesheetContent];
					[_globalStyleSheet mergeStylesheet:localSheet];
				}
			}
			else
			{
				DTLogWarning(@"CSS link referencing a non-local target, ignored");
			}
		}
	};
	
	[ _tagEndHandlers setObject:[linkBlock copy] forKey:@"link"];
}

#pragma mark DTHTMLParser Delegate

- (void)parser:(DTHTMLParser *)parser didStartElement:(NSString *)elementName attributes:(NSDictionary *)attributeDict
{
	
	dispatch_group_async(_treeBuildingGroup, _treeBuildingQueue, ^{
		
		if (_ignoreParseEvents)
		{
			return;
		}

		DTHTMLElement *newNode = [DTHTMLElement elementWithName:elementName attributes:attributeDict options:_options];
		DTHTMLElement *previousLastChild = nil;
		
		if (_currentTag)
		{
			// inherit stuff
			[newNode inheritAttributesFromElement:_currentTag];
			[newNode interpretAttributes];
			
			previousLastChild = [_currentTag.childNodes lastObject];
			
			// add as new child of current node
			[_currentTag addChildNode:newNode];
			
			// remember body node
			if (!_bodyElement && [newNode.name isEqualToString:@"body"])
			{
				_bodyElement = newNode;
			}
			
			if (_shouldProcessCustomHTMLAttributes)
			{
				newNode.shouldProcessCustomHTMLAttributes = _shouldProcessCustomHTMLAttributes;
			}
		}
		else
		{
			NSAssert(!_rootNode, @"Something went wrong, second root node found in document and not ignored.");
			
			// might be first node ever
			if (!_rootNode)
			{
				_rootNode = newNode;
				
				[_rootNode inheritAttributesFromElement:_defaultTag];
				[_rootNode interpretAttributes];
			}
		}
		
		// apply style from merged style sheet
		NSSet *matchedSelectors;
		NSDictionary *mergedStyles = [_globalStyleSheet mergedStyleDictionaryForElement:newNode matchedSelectors:&matchedSelectors ignoreInlineStyle:_ignoreInlineStyles];
		
		if (mergedStyles)
		{
			[newNode applyStyleDictionary:mergedStyles];
			
			// do not add the matched class names to 'class' custom attribute 
			if (matchedSelectors)
			{
				NSMutableSet *classNamesToIgnoreForCustomAttributes = [NSMutableSet set];
				
				for (NSString *oneSelector in matchedSelectors)
				{
					// class selectors have a period
					NSRange periodRange = [oneSelector rangeOfString:@"."];
					
					if (periodRange.location != NSNotFound)
					{
						NSString *className = [oneSelector substringFromIndex:periodRange.location+1];
						
						// add this to ignored classes
						[classNamesToIgnoreForCustomAttributes addObject:className];
					}
				}
				
				if ([classNamesToIgnoreForCustomAttributes count])
				{
					newNode.CSSClassNamesToIgnoreForCustomAttributes = classNamesToIgnoreForCustomAttributes;
				}
			}
		}
		
		// adding a block element eliminates previous trailing white space text node
		// because a new block starts on a new line
		if (previousLastChild && newNode.displayStyle != DTHTMLElementDisplayStyleInline)
		{
			if ([previousLastChild isKindOfClass:[DTTextHTMLElement class]])
			{
				DTTextHTMLElement *textElement = (DTTextHTMLElement *)previousLastChild;
				
				if ([[textElement text] isIgnorableWhitespace])
				{
					[_currentTag removeChildNode:textElement];
				}
			}
		}
		
		_currentTag = newNode;
		
		// find block to execute for this tag if any
		void (^tagBlock)(void) = [_tagStartHandlers objectForKey:elementName];
		
		if (tagBlock)
		{
			tagBlock();
		}
	});
}

- (void)parser:(DTHTMLParser *)parser didEndElement:(NSString *)elementName
{

	dispatch_group_async(_treeBuildingGroup, _treeBuildingQueue, ^{
		@autoreleasepool {
			if (_ignoreParseEvents)
			{
				return;
			}
			
			// output the element if it is direct descendant of body tag, or close of body in case there are direct text nodes
			
			// find block to execute for this tag if any
			void (^tagBlock)(void) = [_tagEndHandlers objectForKey:elementName];
			
			if (tagBlock)
			{
				tagBlock();
			}
			
			if (_currentTag.displayStyle != DTHTMLElementDisplayStyleNone)
			{
				if (_currentTag == _bodyElement || _currentTag.parentElement == _bodyElement)
				{
					DTHTMLElement *theTag = _currentTag;
					
					dispatch_group_async(_stringAssemblyGroup, _stringAssemblyQueue, ^{
						// has children that have not been output yet
						if ([theTag needsOutput])
						{
							// caller gets opportunity to modify tag before it is written
							if (_willFlushCallback)
							{
								_willFlushCallback(theTag);
							}
							
							NSAttributedString *nodeString = [theTag attributedString];
							
							if (nodeString)
							{
								// if this is a block element then we need a paragraph break before it
								if (theTag.displayStyle != DTHTMLElementDisplayStyleInline)
								{
									if ([_tmpString length] && ![[_tmpString string] hasSuffix:@"\n"])
									{
										// trim off whitespace
										while ([[_tmpString string] hasSuffixCharacterFromSet:[NSCharacterSet ignorableWhitespaceCharacterSet]])
										{
											[_tmpString deleteCharactersInRange:NSMakeRange([_tmpString length]-1, 1)];
										}
										
										[_tmpString appendString:@"\n"];
									}
								}
								
								[_tmpString appendAttributedString:nodeString];
								theTag.didOutput = YES;
								
								if (!_shouldKeepDocumentNodeTree)
								{
									// we don't need the children any more
									[theTag removeAllChildNodes];
								}
							}
							
						}
					});
				}
				
			}
			
			while (![_currentTag.name isEqualToString:elementName])
			{
				// missing end of element, attempt to recover
				_currentTag = [_currentTag parentElement];
			}
			
			// closing the root node, ignore everything afterwards
			if (_currentTag == _rootNode)
			{
				_ignoreParseEvents = YES;
			}
			
			// go back up a level
			_currentTag = [_currentTag parentElement];
		}
	});
}

- (void)parser:(DTHTMLParser *)parser foundCharacters:(NSString *)string
{
	dispatch_group_async(_treeBuildingGroup, _treeBuildingQueue, ^{
		
		if (_ignoreParseEvents)
		{
			return;
		}
		
		NSAssert(_currentTag, @"Cannot add text node without a current node");
		
		if (!_currentTag.preserveNewlines && [string isIgnorableWhitespace])
		{
			// ignore whitespace as first element of block element
			if (_currentTag.displayStyle!=DTHTMLElementDisplayStyleInline && ![_currentTag.childNodes count])
			{
				return;
			}
			
			// ignore whitespace following a block element
			DTHTMLElement *previousTag = [_currentTag.childNodes lastObject];
			
			if (previousTag.displayStyle != DTHTMLElementDisplayStyleInline)
			{
				return;
			}
			
			// ignore whitespace following a BR
			if ([previousTag isKindOfClass:[DTBreakHTMLElement class]])
			{
				return;
			}
		}
		
		// adds a text node to the current node
		DTTextHTMLElement *textNode = [[DTTextHTMLElement alloc] init];
		textNode.text = string;
		
		[textNode inheritAttributesFromElement:_currentTag];
		[textNode interpretAttributes];
		
		// save it for later output
		[_currentTag addChildNode:textNode];
		
		DTHTMLElement *theTag = _currentTag;
		
		// text directly contained in body needs to be output right away
		if (theTag == _bodyElement)
		{
			dispatch_group_async(_stringAssemblyGroup, _stringAssemblyQueue, ^{
				[_tmpString appendAttributedString:[textNode attributedString]];
				theTag.didOutput = YES;
			});
			
			// only add it to current tag if we need it
			if (_shouldKeepDocumentNodeTree)
			{
				[theTag addChildNode:textNode];
			}
			
			return;
		}
		
	});
}

- (void)parser:(DTHTMLParser *)parser foundCDATA:(NSData *)CDATABlock
{
	dispatch_group_async(_treeBuildingGroup, _treeBuildingQueue, ^{
		
		if (_ignoreParseEvents)
		{
			return;
		}
		
		NSAssert(_currentTag, @"Cannot add text node without a current node");
		
		NSString *styleBlock = [[NSString alloc] initWithData:CDATABlock encoding:NSUTF8StringEncoding];
		
		// adds a text node to the current node
		DTHTMLParserTextNode *textNode = [[DTHTMLParserTextNode alloc] initWithCharacters:styleBlock];
		
		[_currentTag addChildNode:textNode];
	});
}

- (void)parserDidEndDocument:(DTHTMLParser *)parser
{

	dispatch_group_async(_treeBuildingGroup, _treeBuildingQueue, ^{
		NSAssert(!_currentTag, @"Something went wrong, at end of document there is still an open node");

		dispatch_group_async(_stringAssemblyGroup, _stringAssemblyQueue, ^{
			// trim off white space at end
			while ([[_tmpString string] hasSuffixCharacterFromSet:[NSCharacterSet whitespaceCharacterSet]])
			{
				[_tmpString deleteCharactersInRange:NSMakeRange([_tmpString length]-1, 1)];
			}
		});
	});
}

#pragma mark Properties

@synthesize willFlushCallback = _willFlushCallback;
@synthesize shouldKeepDocumentNodeTree = _shouldKeepDocumentNodeTree;

@end
