//
//  FontLabelStringDrawing.m
//  FontLabel
//
//  Created by Kevin Ballard on 5/5/09.
//  Copyright © 2009 Zynga Game Networks
//
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

#import "FontLabelStringDrawing.h"
#import "ZFont.h"
#import "ZAttributedStringPrivate.h"

@interface ZFont (ZFontPrivate)
@property (nonatomic, readonly) CGFloat ratio;
@end

#define kUnicodeHighSurrogateStart 0xD800
#define kUnicodeHighSurrogateEnd 0xDBFF
#define kUnicodeHighSurrogateMask kUnicodeHighSurrogateStart
#define kUnicodeLowSurrogateStart 0xDC00
#define kUnicodeLowSurrogateEnd 0xDFFF
#define kUnicodeLowSurrogateMask kUnicodeLowSurrogateStart
#define kUnicodeSurrogateTypeMask 0xFC00
#define UnicharIsHighSurrogate(c) ((c & kUnicodeSurrogateTypeMask) == kUnicodeHighSurrogateMask)
#define UnicharIsLowSurrogate(c) ((c & kUnicodeSurrogateTypeMask) == kUnicodeLowSurrogateMask)
#define ConvertSurrogatePairToUTF32(high, low) ((UInt32)((high - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000))

typedef enum {
	kFontTableFormat4 = 4,
	kFontTableFormat12 = 12,
} FontTableFormat;

typedef struct fontTable {
	NSUInteger retainCount;
	CFDataRef cmapTable;
	FontTableFormat format;
	union {
		struct {
			UInt16 segCountX2;
			UInt16 *endCodes;
			UInt16 *startCodes;
			UInt16 *idDeltas;
			UInt16 *idRangeOffsets;
		} format4;
		struct {
			UInt32 nGroups;
			struct {
				UInt32 startCharCode;
				UInt32 endCharCode;
				UInt32 startGlyphCode;
			} *groups;
		} format12;
	} cmap;
} fontTable;

static FontTableFormat supportedFormats[] = { kFontTableFormat4, kFontTableFormat12 };
static size_t supportedFormatsCount = sizeof(supportedFormats) / sizeof(FontTableFormat);

static fontTable *newFontTable(CFDataRef cmapTable, FontTableFormat format) {
	fontTable *table = (struct fontTable *)malloc(sizeof(struct fontTable));
	table->retainCount = 1;
	table->cmapTable = CFRetain(cmapTable);
	table->format = format;
	return table;
}

static fontTable *retainFontTable(fontTable *table) {
	if (table != NULL) {
		table->retainCount++;
	}
	return table;
}

static void releaseFontTable(fontTable *table) {
	if (table != NULL) {
		if (table->retainCount <= 1) {
			CFRelease(table->cmapTable);
			free(table);
		} else {
			table->retainCount--;
		}
	}
}

static const void *fontTableRetainCallback(CFAllocatorRef allocator, const void *value) {
	return retainFontTable((fontTable *)value);
}

static void fontTableReleaseCallback(CFAllocatorRef allocator, const void *value) {
	releaseFontTable((fontTable *)value);
}

static const CFDictionaryValueCallBacks kFontTableDictionaryValueCallBacks = {
	.version = 0,
	.retain = &fontTableRetainCallback,
	.release = &fontTableReleaseCallback,
	.copyDescription = NULL,
	.equal = NULL
};

// read the cmap table from the font
// we only know how to understand some of the table formats at the moment
static fontTable *readFontTableFromCGFont(CGFontRef font) {
	CFDataRef cmapTable = CGFontCopyTableForTag(font, 'cmap');
	NSCAssert1(cmapTable != NULL, @"CGFontCopyTableForTag returned NULL for 'cmap' tag in font %@",
			   (font ? [(id)CFCopyDescription(font) autorelease] : @"(null)"));
	const UInt8 * const bytes = CFDataGetBytePtr(cmapTable);
	NSCAssert1(OSReadBigInt16(bytes, 0) == 0, @"cmap table for font %@ has bad version number",
			   (font ? [(id)CFCopyDescription(font) autorelease] : @"(null)"));
	UInt16 numberOfSubtables = OSReadBigInt16(bytes, 2);
	const UInt8 *unicodeSubtable = NULL;
	//UInt16 unicodeSubtablePlatformID;
	UInt16 unicodeSubtablePlatformSpecificID;
	FontTableFormat unicodeSubtableFormat;
	const UInt8 * const encodingSubtables = &bytes[4];
	for (UInt16 i = 0; i < numberOfSubtables; i++) {
		const UInt8 * const encodingSubtable = &encodingSubtables[8 * i];
		UInt16 platformID = OSReadBigInt16(encodingSubtable, 0);
		UInt16 platformSpecificID = OSReadBigInt16(encodingSubtable, 2);
		// find the best subtable
		// best is defined by a combination of encoding and format
		// At the moment we only support format 4, so ignore all other format tables
		// We prefer platformID == 0, but we will also accept Microsoft's unicode format
		if (platformID == 0 || (platformID == 3 && platformSpecificID == 1)) {
			BOOL preferred = NO;
			if (unicodeSubtable == NULL) {
				preferred = YES;
			} else if (platformID == 0 && platformSpecificID > unicodeSubtablePlatformSpecificID) {
				preferred = YES;
			}
			if (preferred) {
				UInt32 offset = OSReadBigInt32(encodingSubtable, 4);
				const UInt8 *subtable = &bytes[offset];
				UInt16 format = OSReadBigInt16(subtable, 0);
				for (size_t i = 0; i < supportedFormatsCount; i++) {
					if (format == supportedFormats[i]) {
						if (format >= 8) {
							// the version is a fixed-point
							UInt16 formatFrac = OSReadBigInt16(subtable, 2);
							if (formatFrac != 0) {
								// all the current formats with a Fixed version are always *.0
								continue;
							}
						}
						unicodeSubtable = subtable;
						//unicodeSubtablePlatformID = platformID;
						unicodeSubtablePlatformSpecificID = platformSpecificID;
						unicodeSubtableFormat = format;
						break;
					}
				}
			}
		}
	}
	fontTable *table = NULL;
	if (unicodeSubtable != NULL) {
		table = newFontTable(cmapTable, unicodeSubtableFormat);
		switch (unicodeSubtableFormat) {
			case kFontTableFormat4:
				// subtable format 4
				//UInt16 length = OSReadBigInt16(unicodeSubtable, 2);
				//UInt16 language = OSReadBigInt16(unicodeSubtable, 4);
				table->cmap.format4.segCountX2 = OSReadBigInt16(unicodeSubtable, 6);
				//UInt16 searchRange = OSReadBigInt16(unicodeSubtable, 8);
				//UInt16 entrySelector = OSReadBigInt16(unicodeSubtable, 10);
				//UInt16 rangeShift = OSReadBigInt16(unicodeSubtable, 12);
				table->cmap.format4.endCodes = (UInt16*)&unicodeSubtable[14];
				table->cmap.format4.startCodes = (UInt16*)&((UInt8*)table->cmap.format4.endCodes)[table->cmap.format4.segCountX2+2];
				table->cmap.format4.idDeltas = (UInt16*)&((UInt8*)table->cmap.format4.startCodes)[table->cmap.format4.segCountX2];
				table->cmap.format4.idRangeOffsets = (UInt16*)&((UInt8*)table->cmap.format4.idDeltas)[table->cmap.format4.segCountX2];
				//UInt16 *glyphIndexArray = &idRangeOffsets[segCountX2];
				break;
			case kFontTableFormat12:
				table->cmap.format12.nGroups = OSReadBigInt32(unicodeSubtable, 12);
				table->cmap.format12.groups = (void *)&unicodeSubtable[16];
				break;
			default:
				releaseFontTable(table);
				table = NULL;
		}
	}
	CFRelease(cmapTable);
	return table;
}

// outGlyphs must be at least size n
static void mapCharactersToGlyphsInFont(const fontTable *table, unichar characters[], size_t charLen, CGGlyph outGlyphs[], size_t *outGlyphLen) {
	if (table != NULL) {
		NSUInteger j = 0;
		switch (table->format) {
			case kFontTableFormat4: {
				for (NSUInteger i = 0; i < charLen; i++, j++) {
					unichar c = characters[i];
					UInt16 segOffset;
					BOOL foundSegment = NO;
					for (segOffset = 0; segOffset < table->cmap.format4.segCountX2; segOffset += 2) {
						UInt16 endCode = OSReadBigInt16(table->cmap.format4.endCodes, segOffset);
						if (endCode >= c) {
							foundSegment = YES;
							break;
						}
					}
					if (!foundSegment) {
						// no segment
						// this is an invalid font
						outGlyphs[j] = 0;
					} else {
						UInt16 startCode = OSReadBigInt16(table->cmap.format4.startCodes, segOffset);
						if (!(startCode <= c)) {
							// the code falls in a hole between segments
							outGlyphs[j] = 0;
						} else {
							UInt16 idRangeOffset = OSReadBigInt16(table->cmap.format4.idRangeOffsets, segOffset);
							if (idRangeOffset == 0) {
								UInt16 idDelta = OSReadBigInt16(table->cmap.format4.idDeltas, segOffset);
								outGlyphs[j] = (c + idDelta) % 65536;
							} else {
								// use the glyphIndexArray
								UInt16 glyphOffset = idRangeOffset + 2 * (c - startCode);
								outGlyphs[j] = OSReadBigInt16(&((UInt8*)table->cmap.format4.idRangeOffsets)[segOffset], glyphOffset);
							}
						}
					}
				}
				break;
			}
			case kFontTableFormat12: {
				UInt32 lastSegment = UINT32_MAX;
				for (NSUInteger i = 0; i < charLen; i++, j++) {
					unichar c = characters[i];
					UInt32 c32 = c;
					if (UnicharIsHighSurrogate(c)) {
						if (i+1 < charLen) { // do we have another character after this one?
							unichar cc = characters[i+1];
							if (UnicharIsLowSurrogate(cc)) {
								c32 = ConvertSurrogatePairToUTF32(c, cc);
								i++;
							}
						}
					}
					// Start the heuristic search
					// If this is an ASCII char, just do a linear search
					// Otherwise do a hinted, modified binary search
					// Start the first pivot at the last range found
					// And when moving the pivot, limit the movement by increasing
					// powers of two. This should help with locality
					__typeof__(table->cmap.format12.groups[0]) *foundGroup = NULL;
					if (c32 <= 0x7F) {
						// ASCII
						for (UInt32 idx = 0; idx < table->cmap.format12.nGroups; idx++) {
							__typeof__(table->cmap.format12.groups[idx]) *group = &table->cmap.format12.groups[idx];
							if (c32 < OSSwapBigToHostInt32(group->startCharCode)) {
								// we've fallen into a hole
								break;
							} else if (c32 <= OSSwapBigToHostInt32(group->endCharCode)) {
								// this is the range
								foundGroup = group;
								break;
							}
						}
					} else {
						// heuristic search
						UInt32 maxJump = (lastSegment == UINT32_MAX ? UINT32_MAX / 2 : 8);
						UInt32 lowIdx = 0, highIdx = table->cmap.format12.nGroups; // highIdx is the first invalid idx
						UInt32 pivot = (lastSegment == UINT32_MAX ? lowIdx + (highIdx - lowIdx) / 2 : lastSegment);
						while (highIdx > lowIdx) {
							__typeof__(table->cmap.format12.groups[pivot]) *group = &table->cmap.format12.groups[pivot];
							if (c32 < OSSwapBigToHostInt32(group->startCharCode)) {
								highIdx = pivot;
							} else if (c32 > OSSwapBigToHostInt32(group->endCharCode)) {
								lowIdx = pivot + 1;
							} else {
								// we've hit the range
								foundGroup = group;
								break;
							}
							if (highIdx - lowIdx > maxJump * 2) {
								if (highIdx == pivot) {
									pivot -= maxJump;
								} else {
									pivot += maxJump;
								}
								maxJump *= 2;
							} else {
								pivot = lowIdx + (highIdx - lowIdx) / 2;
							}
						}
						if (foundGroup != NULL) lastSegment = pivot;
					}
					if (foundGroup == NULL) {
						outGlyphs[j] = 0;
					} else {
						outGlyphs[j] = (CGGlyph)(OSSwapBigToHostInt32(foundGroup->startGlyphCode) +
												 (c32 - OSSwapBigToHostInt32(foundGroup->startCharCode)));
					}
				}
				break;
			}
		}
		if (outGlyphLen != NULL) *outGlyphLen = j;
	} else {
		// we have no table, so just null out the glyphs
		bzero(outGlyphs, charLen*sizeof(CGGlyph));
		if (outGlyphLen != NULL) *outGlyphLen = 0;
	}
}

static BOOL mapGlyphsToAdvancesInFont(ZFont *font, size_t n, CGGlyph glyphs[], CGFloat outAdvances[]) {
	int advances[n];
	if (CGFontGetGlyphAdvances(font.cgFont, glyphs, n, advances)) {
		CGFloat ratio = font.ratio;
		
		for (size_t i = 0; i < n; i++) {
			outAdvances[i] = advances[i]*ratio;
		}
		return YES;
	} else {
		bzero(outAdvances, n*sizeof(CGFloat));
	}
	return NO;
}

static id getValueOrDefaultForRun(ZAttributeRun *run, NSString *key) {
	id value = [run.attributes objectForKey:key];
	if (value == nil) {
		static NSDictionary *defaultValues = nil;
		if (defaultValues == nil) {
			defaultValues = [[NSDictionary alloc] initWithObjectsAndKeys:
							 [ZFont fontWithUIFont:[UIFont systemFontOfSize:12]], ZFontAttributeName,
							 [UIColor blackColor], ZForegroundColorAttributeName,
							 [UIColor clearColor], ZBackgroundColorAttributeName,
							 [NSNumber numberWithInt:ZUnderlineStyleNone], ZUnderlineStyleAttributeName,
							 nil];
		}
		value = [defaultValues objectForKey:key];
	}
	return value;
}

static void readRunInformation(NSArray *attributes, NSUInteger len, CFMutableDictionaryRef fontTableMap,
							   NSUInteger index, ZAttributeRun **currentRun, NSUInteger *nextRunStart,
							   ZFont **currentFont, fontTable **currentTable) {
	*currentRun = [attributes objectAtIndex:index];
	*nextRunStart = ([attributes count] > index+1 ? [[attributes objectAtIndex:index+1] index] : len);
	*currentFont = getValueOrDefaultForRun(*currentRun, ZFontAttributeName);
	if (!CFDictionaryGetValueIfPresent(fontTableMap, (*currentFont).cgFont, (const void **)currentTable)) {
		*currentTable = readFontTableFromCGFont((*currentFont).cgFont);
		CFDictionarySetValue(fontTableMap, (*currentFont).cgFont, *currentTable);
		releaseFontTable(*currentTable);
	}
}

static CGSize drawOrSizeTextConstrainedToSize(BOOL performDraw, NSString *string, NSArray *attributes, CGSize constrainedSize, NSUInteger maxLines,
											  UILineBreakMode lineBreakMode, UITextAlignment alignment, BOOL ignoreColor) {
	NSUInteger len = [string length];
	NSUInteger idx = 0;
	CGPoint drawPoint = CGPointZero;
	CGSize retValue = CGSizeZero;
	CGContextRef ctx = (performDraw ? UIGraphicsGetCurrentContext() : NULL);
	
	BOOL convertNewlines = (maxLines == 1);
	
	// Extract the characters from the string
	// Convert newlines to spaces if necessary
	unichar *characters = (unichar *)malloc(sizeof(unichar) * len);
	if (convertNewlines) {
		NSCharacterSet *charset = [NSCharacterSet newlineCharacterSet];
		NSRange range = NSMakeRange(0, len);
		size_t cIdx = 0;
		while (range.length > 0) {
			NSRange newlineRange = [string rangeOfCharacterFromSet:charset options:0 range:range];
			if (newlineRange.location == NSNotFound) {
				[string getCharacters:&characters[cIdx] range:range];
				cIdx += range.length;
				break;
			} else {
				NSUInteger delta = newlineRange.location - range.location;
				if (newlineRange.location > range.location) {
					[string getCharacters:&characters[cIdx] range:NSMakeRange(range.location, delta)];
				}
				cIdx += delta;
				characters[cIdx] = (unichar)' ';
				cIdx++;
				delta += newlineRange.length;
				range.location += delta, range.length -= delta;
				if (newlineRange.length == 1 && range.length >= 1 &&
					[string characterAtIndex:newlineRange.location] == (unichar)'\r' &&
					[string characterAtIndex:range.location] == (unichar)'\n') {
					// CRLF sequence, skip the LF
					range.location += 1, range.length -= 1;
				}
			}
		}
		len = cIdx;
	} else {
		[string getCharacters:characters range:NSMakeRange(0, len)];
	}
	
	// Create storage for glyphs and advances
	CGGlyph *glyphs;
	CGFloat *advances;
	{
		NSUInteger maxRunLength = 0;
		ZAttributeRun *a = [attributes objectAtIndex:0];
		for (NSUInteger i = 1; i < [attributes count]; i++) {
			ZAttributeRun *b = [attributes objectAtIndex:i];
			maxRunLength = MAX(maxRunLength, b.index - a.index);
			a = b;
		}
		maxRunLength = MAX(maxRunLength, len - a.index);
		maxRunLength++; // for a potential ellipsis
		glyphs = (CGGlyph *)malloc(sizeof(CGGlyph) * maxRunLength);
		advances = (CGFloat *)malloc(sizeof(CGFloat) * maxRunLength);
	}
	
	// Use this table to cache all fontTable objects
	CFMutableDictionaryRef fontTableMap = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks,
																	 &kFontTableDictionaryValueCallBacks);
	
	// Fetch initial style values
	NSUInteger currentRunIdx = 0;
	ZAttributeRun *currentRun;
	NSUInteger nextRunStart;
	ZFont *currentFont;
	fontTable *currentTable;
	
#define READ_RUN() readRunInformation(attributes, len, fontTableMap, \
									  currentRunIdx, &currentRun, &nextRunStart, \
									  &currentFont, &currentTable)
	
	READ_RUN();
	
	// fetch the glyphs for the first run
	size_t glyphCount;
	NSUInteger glyphIdx;
	
#define READ_GLYPHS() do { \
		mapCharactersToGlyphsInFont(currentTable, &characters[currentRun.index], (nextRunStart - currentRun.index), glyphs, &glyphCount); \
		mapGlyphsToAdvancesInFont(currentFont, (nextRunStart - currentRun.index), glyphs, advances); \
		glyphIdx = 0; \
	} while (0)
	
	READ_GLYPHS();
	
	NSMutableCharacterSet *alphaCharset = [NSMutableCharacterSet alphanumericCharacterSet];
	[alphaCharset addCharactersInString:@"([{'\"\u2019\u02BC"];
	
	// scan left-to-right looking for newlines or until we hit the width constraint
	// When we hit a wrapping point, calculate truncation as follows:
	// If we have room to draw at least one more character on the next line, no truncation
	// Otherwise apply the truncation algorithm to the current line.
	// After calculating any truncation, draw.
	// Each time we hit the end of an attribute run, calculate the new font and make sure
	// it fits (vertically) within the size constraint. If not, truncate this line.
	// When we draw, iterate over the attribute runs for this line and draw each run separately
	BOOL lastLine = NO; // used to indicate truncation and to stop the iterating
	NSUInteger lineCount = 1;
	while (idx < len && !lastLine) {
		if (maxLines > 0 && lineCount == maxLines) {
			lastLine = YES;
		}
		// scan left-to-right
		struct {
			NSUInteger index;
			NSUInteger glyphIndex;
			NSUInteger currentRunIdx;
		} indexCache = { idx, glyphIdx, currentRunIdx };
		CGSize lineSize = CGSizeMake(0, currentFont.leading);
		CGFloat lineAscender = currentFont.ascender;
		struct {
			NSUInteger index;
			NSUInteger glyphIndex;
			NSUInteger currentRunIdx;
			CGSize lineSize;
		} lastWrapCache = {0, 0, 0, CGSizeZero};
		BOOL inAlpha = NO; // used for calculating wrap points
		
		BOOL finishLine = NO;
		for (;idx <= len && !finishLine;) {
			NSUInteger skipCount = 0;
			if (idx == len) {
				finishLine = YES;
				lastLine = YES;
			} else {
				if (idx >= nextRunStart) {
					// cycle the font and table and grab the next set of glyphs
					do {
						currentRunIdx++;
						READ_RUN();
					} while (idx >= nextRunStart);
					READ_GLYPHS();
					// re-scan the characters to synchronize the glyph index
					for (NSUInteger j = currentRun.index; j < idx; j++) {
						if (UnicharIsHighSurrogate(characters[j]) && j+1<len && UnicharIsLowSurrogate(characters[j+1])) {
							j++;
						}
						glyphIdx++;
					}
					if (currentFont.leading > lineSize.height) {
						lineSize.height = currentFont.leading;
						if (retValue.height + currentFont.ascender > constrainedSize.height) {
							lastLine = YES;
							finishLine = YES;
						}
					}
					lineAscender = MAX(lineAscender, currentFont.ascender);
				}
				unichar c = characters[idx];
				// Mark a wrap point before spaces and after any stretch of non-alpha characters
				BOOL markWrap = NO;
				if (c == (unichar)' ') {
					markWrap = YES;
				} else if ([alphaCharset characterIsMember:c]) {
					if (!inAlpha) {
						markWrap = YES;
						inAlpha = YES;
					}
				} else {
					inAlpha = NO;
				}
				if (markWrap) {
					lastWrapCache = (__typeof__(lastWrapCache)){
						.index = idx,
						.glyphIndex = glyphIdx,
						.currentRunIdx = currentRunIdx,
						.lineSize = lineSize
					};
				}
				// process the line
				if (c == (unichar)'\n' || c == 0x0085) { // U+0085 is the NEXT_LINE unicode character
					finishLine = YES;
					skipCount = 1;
				} else if (c == (unichar)'\r') {
					finishLine = YES;
					// check for CRLF
					if (idx+1 < len && characters[idx+1] == (unichar)'\n') {
						skipCount = 2;
					} else {
						skipCount = 1;
					}
				} else if (lineSize.width + advances[glyphIdx] > constrainedSize.width) {
					finishLine = YES;
					if (retValue.height + lineSize.height + currentFont.ascender > constrainedSize.height) {
						lastLine = YES;
					}
					// walk backwards if wrapping is necessary
					if (lastWrapCache.index > indexCache.index && lineBreakMode != UILineBreakModeCharacterWrap &&
						(!lastLine || lineBreakMode != UILineBreakModeClip)) {
						// we're doing some sort of word wrapping
						idx = lastWrapCache.index;
						lineSize = lastWrapCache.lineSize;
						if (!lastLine) {
							// re-check if this is the last line
							if (lastWrapCache.currentRunIdx != currentRunIdx) {
								currentRunIdx = lastWrapCache.currentRunIdx;
								READ_RUN();
								READ_GLYPHS();
							}
							if (retValue.height + lineSize.height + currentFont.ascender > constrainedSize.height) {
								lastLine = YES;
							}
						}
						glyphIdx = lastWrapCache.glyphIndex;
						// skip any spaces
						for (NSUInteger j = idx; j < len && characters[j] == (unichar)' '; j++) {
							skipCount++;
						}
					}
				}
			}
			if (finishLine) {
				// TODO: support head/middle truncation
				if (lastLine && idx < len && lineBreakMode == UILineBreakModeTailTruncation) {
					// truncate
					unichar ellipsis = 0x2026; // ellipsis (…)
					CGGlyph ellipsisGlyph;
					mapCharactersToGlyphsInFont(currentTable, &ellipsis, 1, &ellipsisGlyph, NULL);
					CGFloat ellipsisWidth;
					mapGlyphsToAdvancesInFont(currentFont, 1, &ellipsisGlyph, &ellipsisWidth);
					while ((idx - indexCache.index) > 1 && lineSize.width + ellipsisWidth > constrainedSize.width) {
						// we have more than 1 character and we're too wide, so back up
						idx--;
						if (UnicharIsHighSurrogate(characters[idx]) && UnicharIsLowSurrogate(characters[idx+1])) {
							idx--;
						}
						if (idx < currentRun.index) {
							ZFont *oldFont = currentFont;
							do {
								currentRunIdx--;
								READ_RUN();
							} while (idx < currentRun.index);
							READ_GLYPHS();
							glyphIdx = glyphCount-1;
							if (oldFont != currentFont) {
								mapCharactersToGlyphsInFont(currentTable, &ellipsis, 1, &ellipsisGlyph, NULL);
								mapGlyphsToAdvancesInFont(currentFont, 1, &ellipsisGlyph, &ellipsisWidth);
							}
						} else {
							glyphIdx--;
						}
						lineSize.width -= advances[glyphIdx];
					}
					// skip any spaces before truncating
					while ((idx - indexCache.index) > 1 && characters[idx-1] == (unichar)' ') {
						idx--;
						if (idx < currentRun.index) {
							currentRunIdx--;
							READ_RUN();
							READ_GLYPHS();
							glyphIdx = glyphCount-1;
						} else {
							glyphIdx--;
						}
						lineSize.width -= advances[glyphIdx];
					}
					lineSize.width += ellipsisWidth;
					glyphs[glyphIdx] = ellipsisGlyph;
					idx++;
					glyphIdx++;
				}
				retValue.width = MAX(retValue.width, lineSize.width);
				retValue.height += lineSize.height;
				
				// draw
				if (performDraw) {
					switch (alignment) {
						case UITextAlignmentLeft:
							drawPoint.x = 0;
							break;
						case UITextAlignmentCenter:
							drawPoint.x = (constrainedSize.width - lineSize.width) / 2.0f;
							break;
						case UITextAlignmentRight:
							drawPoint.x = constrainedSize.width - lineSize.width;
							break;
					}
					NSUInteger stopGlyphIdx = glyphIdx;
					NSUInteger lastRunIdx = currentRunIdx;
					NSUInteger stopCharIdx = idx;
					idx = indexCache.index;
					if (currentRunIdx != indexCache.currentRunIdx) {
						currentRunIdx = indexCache.currentRunIdx;
						READ_RUN();
						READ_GLYPHS();
					}
					glyphIdx = indexCache.glyphIndex;
					for (NSUInteger drawIdx = currentRunIdx; drawIdx <= lastRunIdx; drawIdx++) {
						if (drawIdx != currentRunIdx) {
							currentRunIdx = drawIdx;
							READ_RUN();
							READ_GLYPHS();
						}
						NSUInteger numGlyphs;
						if (drawIdx == lastRunIdx) {
							numGlyphs = stopGlyphIdx - glyphIdx;
							idx = stopCharIdx;
						} else {
							numGlyphs = glyphCount - glyphIdx;
							idx = nextRunStart;
						}
						CGContextSetFont(ctx, currentFont.cgFont);
						CGContextSetFontSize(ctx, currentFont.pointSize);
						// calculate the fragment size
						CGFloat fragmentWidth = 0;
						for (NSUInteger g = 0; g < numGlyphs; g++) {
							fragmentWidth += advances[glyphIdx + g];
						}
						
						if (!ignoreColor) {
							UIColor *foregroundColor = getValueOrDefaultForRun(currentRun, ZForegroundColorAttributeName);
							UIColor *backgroundColor = getValueOrDefaultForRun(currentRun, ZBackgroundColorAttributeName);
							if (backgroundColor != nil && ![backgroundColor isEqual:[UIColor clearColor]]) {
								[backgroundColor setFill];
								UIRectFillUsingBlendMode((CGRect){ drawPoint, { fragmentWidth, lineSize.height } }, kCGBlendModeNormal);
							}
							[foregroundColor setFill];
						}
						
						CGContextShowGlyphsAtPoint(ctx, drawPoint.x, drawPoint.y + lineAscender, &glyphs[glyphIdx], numGlyphs);
						NSNumber *underlineStyle = getValueOrDefaultForRun(currentRun, ZUnderlineStyleAttributeName);
						if ([underlineStyle	integerValue] & ZUnderlineStyleMask) {
							// we only support single for the time being
							UIRectFill(CGRectMake(drawPoint.x, drawPoint.y + lineAscender, fragmentWidth, 1));
						}
						drawPoint.x += fragmentWidth;
						glyphIdx += numGlyphs;
					}
					drawPoint.y += lineSize.height;
				}
				idx += skipCount;
				glyphIdx += skipCount;
				lineCount++;
			} else {
				lineSize.width += advances[glyphIdx];
				glyphIdx++;
				idx++;
				if (idx < len && UnicharIsHighSurrogate(characters[idx-1]) && UnicharIsLowSurrogate(characters[idx])) {
					// skip the second half of the surrogate pair
					idx++;
				}
			}
		}
	}
	CFRelease(fontTableMap);
	free(glyphs);
	free(advances);
	free(characters);
	
#undef READ_GLYPHS
#undef READ_RUN
	
	return retValue;
}

static NSArray *attributeRunForFont(ZFont *font) {
	return [NSArray arrayWithObject:[ZAttributeRun attributeRunWithIndex:0
															  attributes:[NSDictionary dictionaryWithObject:font
																									 forKey:ZFontAttributeName]]];
}

static CGSize drawTextInRect(CGRect rect, NSString *text, NSArray *attributes, UILineBreakMode lineBreakMode,
							 UITextAlignment alignment, NSUInteger numberOfLines, BOOL ignoreColor) {
	CGContextRef ctx = UIGraphicsGetCurrentContext();
	
	CGContextSaveGState(ctx);
	
	// flip it upside-down because our 0,0 is upper-left, whereas ttfs are for screens where 0,0 is lower-left
	CGAffineTransform textTransform = CGAffineTransformMake(1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f);
	CGContextSetTextMatrix(ctx, textTransform);
	
	CGContextTranslateCTM(ctx, rect.origin.x, rect.origin.y);
	
	CGContextSetTextDrawingMode(ctx, kCGTextFill);
	CGSize size = drawOrSizeTextConstrainedToSize(YES, text, attributes, rect.size, numberOfLines, lineBreakMode, alignment, ignoreColor);
	
	CGContextRestoreGState(ctx);
	
	return size;
}

@implementation NSString (FontLabelStringDrawing)
// CGFontRef-based methods
- (CGSize)sizeWithCGFont:(CGFontRef)font pointSize:(CGFloat)pointSize {
	return [self sizeWithZFont:[ZFont fontWithCGFont:font size:pointSize]];
}

- (CGSize)sizeWithCGFont:(CGFontRef)font pointSize:(CGFloat)pointSize constrainedToSize:(CGSize)size {
	return [self sizeWithZFont:[ZFont fontWithCGFont:font size:pointSize] constrainedToSize:size];
}

- (CGSize)sizeWithCGFont:(CGFontRef)font pointSize:(CGFloat)pointSize constrainedToSize:(CGSize)size
		   lineBreakMode:(UILineBreakMode)lineBreakMode {
	return [self sizeWithZFont:[ZFont fontWithCGFont:font size:pointSize] constrainedToSize:size lineBreakMode:lineBreakMode];
}

- (CGSize)drawAtPoint:(CGPoint)point withCGFont:(CGFontRef)font pointSize:(CGFloat)pointSize {
	return [self drawAtPoint:point withZFont:[ZFont fontWithCGFont:font size:pointSize]];
}

- (CGSize)drawInRect:(CGRect)rect withCGFont:(CGFontRef)font pointSize:(CGFloat)pointSize {
	return [self drawInRect:rect withZFont:[ZFont fontWithCGFont:font size:pointSize]];
}

- (CGSize)drawInRect:(CGRect)rect withCGFont:(CGFontRef)font pointSize:(CGFloat)pointSize lineBreakMode:(UILineBreakMode)lineBreakMode {
	return [self drawInRect:rect withZFont:[ZFont fontWithCGFont:font size:pointSize] lineBreakMode:lineBreakMode];
}

- (CGSize)drawInRect:(CGRect)rect withCGFont:(CGFontRef)font pointSize:(CGFloat)pointSize
	   lineBreakMode:(UILineBreakMode)lineBreakMode alignment:(UITextAlignment)alignment {
	return [self drawInRect:rect withZFont:[ZFont fontWithCGFont:font size:pointSize] lineBreakMode:lineBreakMode alignment:alignment];
}

// ZFont-based methods
- (CGSize)sizeWithZFont:(ZFont *)font {
	CGSize size = drawOrSizeTextConstrainedToSize(NO, self, attributeRunForFont(font), CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX), 1,
												  UILineBreakModeClip, UITextAlignmentLeft, YES);
	return CGSizeMake(ceilf(size.width), ceilf(size.height));
}

- (CGSize)sizeWithZFont:(ZFont *)font constrainedToSize:(CGSize)size {
	return [self sizeWithZFont:font constrainedToSize:size lineBreakMode:UILineBreakModeWordWrap];
}

/*
 According to experimentation with UIStringDrawing, this can actually return a CGSize whose height is greater
 than the one passed in. The two cases are as follows:
 1. If the given size parameter's height is smaller than a single line, the returned value will
 be the height of one line.
 2. If the given size parameter's height falls between multiples of a line height, and the wrapped string
 actually extends past the size.height, and the difference between size.height and the previous multiple
 of a line height is >= the font's ascender, then the returned size's height is extended to the next line.
 To put it simply, if the baseline point of a given line falls in the given size, the entire line will
 be present in the output size.
 */
- (CGSize)sizeWithZFont:(ZFont *)font constrainedToSize:(CGSize)size lineBreakMode:(UILineBreakMode)lineBreakMode {
	size = drawOrSizeTextConstrainedToSize(NO, self, attributeRunForFont(font), size, 0, lineBreakMode, UITextAlignmentLeft, YES);
	return CGSizeMake(ceilf(size.width), ceilf(size.height));
}

- (CGSize)sizeWithZFont:(ZFont *)font constrainedToSize:(CGSize)size lineBreakMode:(UILineBreakMode)lineBreakMode
		  numberOfLines:(NSUInteger)numberOfLines {
	size = drawOrSizeTextConstrainedToSize(NO, self, attributeRunForFont(font), size, numberOfLines, lineBreakMode, UITextAlignmentLeft, YES);
	return CGSizeMake(ceilf(size.width), ceilf(size.height));
}

- (CGSize)drawAtPoint:(CGPoint)point withZFont:(ZFont *)font {
	return [self drawAtPoint:point forWidth:CGFLOAT_MAX withZFont:font lineBreakMode:UILineBreakModeClip];
}

- (CGSize)drawAtPoint:(CGPoint)point forWidth:(CGFloat)width withZFont:(ZFont *)font lineBreakMode:(UILineBreakMode)lineBreakMode {
	return drawTextInRect((CGRect){ point, { width, CGFLOAT_MAX } }, self, attributeRunForFont(font), lineBreakMode, UITextAlignmentLeft, 1, YES);
}

- (CGSize)drawInRect:(CGRect)rect withZFont:(ZFont *)font {
	return [self drawInRect:rect withZFont:font lineBreakMode:UILineBreakModeWordWrap];
}

- (CGSize)drawInRect:(CGRect)rect withZFont:(ZFont *)font lineBreakMode:(UILineBreakMode)lineBreakMode {
	return [self drawInRect:rect withZFont:font lineBreakMode:lineBreakMode alignment:UITextAlignmentLeft];
}

- (CGSize)drawInRect:(CGRect)rect withZFont:(ZFont *)font lineBreakMode:(UILineBreakMode)lineBreakMode
		   alignment:(UITextAlignment)alignment {
	return drawTextInRect(rect, self, attributeRunForFont(font), lineBreakMode, alignment, 0, YES);
}

- (CGSize)drawInRect:(CGRect)rect withZFont:(ZFont *)font lineBreakMode:(UILineBreakMode)lineBreakMode
		   alignment:(UITextAlignment)alignment numberOfLines:(NSUInteger)numberOfLines {
	return drawTextInRect(rect, self, attributeRunForFont(font), lineBreakMode, alignment, numberOfLines, YES);
}
@end

@implementation ZAttributedString (ZAttributedStringDrawing)
- (CGSize)size {
	CGSize size = drawOrSizeTextConstrainedToSize(NO, self.string, self.attributes, CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX), 1,
												  UILineBreakModeClip, UITextAlignmentLeft, NO);
	return CGSizeMake(ceilf(size.width), ceilf(size.height));
}

- (CGSize)sizeConstrainedToSize:(CGSize)size {
	return [self sizeConstrainedToSize:size lineBreakMode:UILineBreakModeWordWrap];
}

- (CGSize)sizeConstrainedToSize:(CGSize)size lineBreakMode:(UILineBreakMode)lineBreakMode {
	size = drawOrSizeTextConstrainedToSize(NO, self.string, self.attributes, size, 0, lineBreakMode, UITextAlignmentLeft, NO);
	return CGSizeMake(ceilf(size.width), ceilf(size.height));
}

- (CGSize)sizeConstrainedToSize:(CGSize)size lineBreakMode:(UILineBreakMode)lineBreakMode
				  numberOfLines:(NSUInteger)numberOfLines {
	size = drawOrSizeTextConstrainedToSize(NO, self.string, self.attributes, size, numberOfLines, lineBreakMode, UITextAlignmentLeft, NO);
	return CGSizeMake(ceilf(size.width), ceilf(size.height));
}

- (CGSize)drawAtPoint:(CGPoint)point {
	return [self drawAtPoint:point forWidth:CGFLOAT_MAX lineBreakMode:UILineBreakModeClip];
}

- (CGSize)drawAtPoint:(CGPoint)point forWidth:(CGFloat)width lineBreakMode:(UILineBreakMode)lineBreakMode {
	return drawTextInRect((CGRect){ point, { width, CGFLOAT_MAX } }, self.string, self.attributes, lineBreakMode, UITextAlignmentLeft, 1, NO);
}

- (CGSize)drawInRect:(CGRect)rect {
	return [self drawInRect:rect withLineBreakMode:UILineBreakModeWordWrap];
}

- (CGSize)drawInRect:(CGRect)rect withLineBreakMode:(UILineBreakMode)lineBreakMode {
	return [self drawInRect:rect withLineBreakMode:lineBreakMode alignment:UITextAlignmentLeft];
}

- (CGSize)drawInRect:(CGRect)rect withLineBreakMode:(UILineBreakMode)lineBreakMode alignment:(UITextAlignment)alignment {
	return drawTextInRect(rect, self.string, self.attributes, lineBreakMode, alignment, 0, NO);
}

- (CGSize)drawInRect:(CGRect)rect withLineBreakMode:(UILineBreakMode)lineBreakMode alignment:(UITextAlignment)alignment
	   numberOfLines:(NSUInteger)numberOfLines {
	return drawTextInRect(rect, self.string, self.attributes, lineBreakMode, alignment, numberOfLines, NO);
}
@end
