// 1 november 2017
#import "uipriv_darwin.h"

static BOOL fontRegistered(CTFontDescriptorRef desc)
{
	CFNumberRef scope;
	CTFontManagerScope val;

	scope = (CFNumberRef) CTFontDescriptorCopyAttribute(desc, kCTFontRegistrationScopeAttribute);
	if (scope == NULL)
		// header says this should be treated as the same as not registered
		return NO;
	if (CFNumberGetValue(scope, kCFNumberSInt32Type, &val) == false) {
		// TODO
		CFRelease(scope);
		return NO;
	}
	CFRelease(scope);
	// examination of Core Text shows this is accurate
	return val != kCTFontManagerScopeNone;
}

static BOOL getTraits(CTFontDescriptorRef desc, CTFontSymbolicTraits *symbolic, double *weight, double *width)
{
	CFDictionaryRef traits = NULL;
	CFNumberRef num = NULL;

	traits = (CFDictionaryRef) CTFontDescriptorCopyAttribute(desc, kCTFontTraitsAttribute);
	if (traits == NULL)
		return NO;

	num = (CFNumberRef) CFDictionaryGetValue(traits, kCTFontSymbolicTrait);
	if (num == NULL)
		goto fail;
	if (CFNumberGetValue(num, kCFNumberSInt32Type, symbolic) == false)
		goto fail;

	num = (CFNumberRef) CFDictionaryGetValue(traits, kCTFontWeightTrait);
	if (num == NULL)
		goto fail;
	if (CFNumberGetValue(num, kCFNumberDoubleType, weight) == false)
		goto fail;

	num = (CFNumberRef) CFDictionaryGetValue(traits, kCTFontWidthTrait);
	if (num == NULL)
		goto fail;
	if (CFNumberGetValue(num, kCFNumberDoubleType, width) == false)
		goto fail;

	CFRelease(traits);
	return YES;

fail:
	CFRelease(traits);
	return NO;
}

// Core Text doesn't seem to differentiate between Italic and Oblique.
// Pango's Core Text code just does a g_strrstr() (backwards case-sensitive search) for "Oblique" in the font's style name (see https://git.gnome.org/browse/pango/tree/pango/pangocoretext-fontmap.c); let's do that too I guess
static uiDrawTextFontItalic guessItalicOblique(CTFontDescriptorRef desc)
{
	CFStringRef styleName;
	BOOL isOblique;

	isOblique = NO;		// default value
	styleName = (CFStringRef) CTFontDescriptorCopyAttribute(desc, kCTFontStyleNameAttribute);
	if (styleName != NULL) {
		CFRange range;

		range = CFStringFind(styleName, CFSTR("Oblique"), kCFCompareBackwards);
		if (range.location != kCFNotFound)
			isOblique = YES;
		CFRelease(styleName);
	}
	if (isOblique)
		return uiDrawFontItalicOblique;
	return uiDrawFontItalicItalic;
}

// Core Text does (usWidthClass / 10) - 0.5 here.
// This roughly maps to our values with increments of 0.1, except for the fact 0 and 10 are allowed by Core Text, despite being banned by TrueType and OpenType themselves.
// We'll just treat them as identical to 1 and 9, respectively.
static const uiDrawFontStretch os2WidthsToStretches[] = {
	uiDrawTextStretchUltraCondensed,
	uiDrawTextStretchUltraCondensed,
	uiDrawTextStretchExtraCondensed,
	uiDrawTextStretchCondensed,
	uiDrawTextStretchSemiCondensed,
	uiDrawTextStretchNormal,
	uiDrawTextStretchSemiExpanded,
	uiDrawTextStretchExpanded,
	uiDrawTextStretchExtraExpanded,
	uiDrawTextStretchUltraExpanded,
	uiDrawTextStretchUltraExpanded,
};

static const CFStringRef exceptions[] = {
	CFSTR("LucidaGrande"),
	CFSTR(".LucidaGrandeUI"),
	CFSTR("STHeiti"),
	CFSTR("STXihei"),
	CFSTR("TimesNewRomanPSMT"),
	NULL,
};

static void trySecondaryOS2Values(CTFontDescriptorRef desc, uiDrawFontDescriptor *out, BOOL *hasWeight, BOOL *hasWidth)
{
	CTFontRef font;
	CFDataRef os2;
	uint16_t usWeightClass, usWidthClass;
	CFStringRef psname;
	CFStringRef *ex;

	*hasWeight = NO;
	*hasWidth = NO;

	// only applies to unregistered fonts
	if (fontRegistered(desc))
		return;

	font = CTFontCreateWithFontDescriptor(desc, 0.0, NULL);

	os2 = CTFontCopyTable(font, kCTFontTableOS2, kCTFontTableOptionNoOptions);
	if (os2 == NULL) {
		// no OS2 table, so no secondary values
		CFRelease(font);
		return;
	}

	if (CFDataGetLength(os2) > 77) {
		const UInt8 *b;

		b = CFDataGetBytePtr(os2);

		usWeightClass = ((uint16_t) (b[4])) << 8;
		usWeightClass |= (uint16_t) (b[5]);
		if (usWeightClass <= 1000) {
			if (usWeightClass < 11)
				usWeigthClass *= 100;
			*hasWeight = YES;
		}

		usWidthClass = ((uint16_t) (b[6])) << 8;
		usWidthClass |= (uint16_t) (b[7]);
		if (usWidthClass <= 10)
			*hasWidth = YES;
	} else {
		usWeightClass = 0;
		*hasWeight = YES;

		usWidthClass = 0;
		*hasWidth = YES;
	}
	if (*hasWeight)
		// we can just use this directly
		out->Weight = usWeightClass;
	if (*hasWidth)
		out->Stretch = os2WidthsToStretches[usWidthClass];
	CFRelease(os2);

	// don't use secondary weights in the event of special predefined names
	psname = CTFontCopyPostScriptName(font);
	for (ex = exceptions; *ex != NULL; ex++)
		if (CFEqual(psname, *ex)) {
			*hasWeight = NO;
			break;
		}
	CFRelease(psname);

	CFRelease(font);
}

// TODO explicitly mark these as undocumented
extern const CFStringRef kCTFontPreferredSubFamilyNameKey;
extern const CFStringRef kCTFontPreferredFamilyNameKey;

static const CFStringRef subfamilyKeys[] = {
	kCTFontSubFamilyNameKey,
	kCTFontPreferredSubFamilyNameKey,
	kCTFontFullNameKey,
	kCTFontPreferredFamilyNameKey,
	kCTFontFamilyNameKey,
	NULL,
};

static BOOL testTTFOTFSubfamilyNames(CTFontDescriptorRef desc, CFStringRef want)
{
	CFNumberRef num;
	CTFontFormat type;
	CGFontRef font;
	CFString *key;

	num = (CFNumberRef) CTFontDescriptorCopyAttribute(desc, kCTFontFormatAttribute);
	if (num == NULL) {
		// TODO
		return NO;
	}
	if (CFNumberGetValue(num, kCFNumberSInt32Type, &type) == false) {
		// TODO
		CFRelease(num);
		return NO;
	}
	CFRelease(num);
	switch (type) {
	case kCTFontFormatOpenTypePostScript:
	case kCTFontFormatOpenTypeTrueType:
	case kCTFontFormatTrueType:
		break;
	default:
		return NO;
	}

	font = CTFontCreateWithFontDescriptor(desc, 0.0, NULL);
	for (key = subfamilyKeys; *key != NULL; key++) {
		CFStringRef val;
		CFRange range;

		val = CTFontCopyName(font, *key);
		if (val == NULL)
			continue;
		range.location = 0;
		range.length = CFStringGetLength(val);
		if (CFStringFindWithOptions(val, want, range,
			(kCFCompareCaseInsensitive | kCFCompareBackwards | kCFCompareNonliteral), NULL) != false) {
			CFRelease(val);
			CFRelease(font);
			return YES;
		}
		CFRelease(val);
	}
	CFRelease(font);
	return NO;
}

// work around a bug in libFontRegistry.dylib
static BOOL shouldReallyBeThin(CTFontDescriptorRef desc)
{
	return testTTFOTFSubfamilyNames(desc, CFSTR("W1"));
}

// work around a bug in libFontRegistry.dylib
static BOOL shouldReallyBeSemiCondensed(CTFontDescriptorRef desc)
{
	return testTTFOTFSubfamilyNames(desc, CFSTR("Semi Condensed"));
}

void processFontTraits(CTFontDescriptorRef desc, uiDrawFontDescriptor *out)
{
	CTFontSymbolicTraits symbolic;
	double weight;
	double width;
	BOOL hasWeight, hasWidth;
	uint16_t usWeightClasss, usWidthClass;
	CTFontRef font;

	if (!getTraits(desc, &symbolic, &weight, &width)) {
		// TODO
		goto fail;
	}

	out->Italic = uiDrawTextItalicNormal;
	if ((symbolic & kCTFontItalicTrait) != 0)
		out->Italic = guessItalicOblique(desc);

	hasWeight = NO;
	hasWidth = NO;
	trySecondaryOS2Values(desc, out, &hasWeight, &hasWidth);

	if (!hasWeight)
		// TODO this scale is a bit lopsided
		if (weight <= -0.7)
			out->Weight = uiDrawTextWeightThin;
		else if (weight <= -0.5)
			out->Weight = uiDrawTextWeightUltraLight;
		else if (weight <= -0.3)
			out->Weight = uiDrawTextWeightLight;
		else if (weight <= -0.23) {
			out->Weight = uiDrawTextWeightBook;
			if (shouldReallyBeThin(desc))
				out->Weight = uiDrawTextWeightThin;
		} else if (weight <= 0.0)
			out->Weight = uiDrawTextWeightNormal;
		else if (weight <= 0.23)
			out->Weight = uiDrawTextWeightMedium;
		else if (weight <= 0.3)
			out->Weight = uiDrawTextWeightSemiBold;
		else if (weight <= 0.4)
			out->Weight = uiDrawTextWeightBold;
		else if (weight <= 0.5)
			out->Weight = uiDrawTextWeightUltraBold;
		else if (weight <= 0.7)
			out->Weight = uiDrawTextWeightHeavy;
		else
			out->Weight = uiDrawTextWeightUltraHeavy;

	if (!hasWidth)
		// TODO this scale is a bit lopsided
		if (width <= -0.7) {
			out->Stretch = uiDrawTextStretchUltraCondensed;
			if (shouldReallyBeSemiCondensed(desc))
				out->Stretch = uiDrawTextStretchSemiCondensed;
		} else if (width <= -0.5)
			out->Stretch = uiDrawTextStretchExtraCondensed;
		else if (width <= -0.2)
			out->Stretch = uiDrawTextStretchCondensed;
		else if (width <= -0.1)
			out->Stretch = uiDrawTextStretchSemiCondensed;
		else if (width <= 0.0)
			out->Stretch = uiDrawTextStretchNormal;
		else if (width <= 0.1)
			out->Stretch = uiDrawTextStretchSemiExpanded;
		else if (width <= 0.2)
			out->Stretch = uiDrawTextStretchExpanded;
		else if (width <= 0.6)
			out->Stretch = uiDrawTextStretchExtraExpanded;
		else
			out->Stretch = uiDrawTextStretchUltraExpanded;
}