/****************************************************************************\
 Part of the XeTeX typesetting system
 copyright (c) 1994-2005 by SIL International
 written by Jonathan Kew

 This software is distributed under the terms of the Common Public License,
 version 1.0.
 For details, see <http://www.opensource.org/licenses/cpl1.0.php> or the file
 cpl1.0.txt included with the software.
\****************************************************************************/

// \special{} support for xdv2pdf

#define MAC_OS_X_VERSION_MIN_REQUIRED	1020

#include <ApplicationServices/ApplicationServices.h>

#include "xdv2pdf_globals.h"


struct dimenRec {
	const char*	name;
	double_t	factor;	// multiplier to convert to BP
};

static dimenRec sDimenRatios[] =
{
	{ "in", 72.0 },
	{ "mm", 72.0 / 25.4 },
	{ "cm", 72.0 / 2.54 },
	{ "bp", 1.0 },
	{ "pt", 72.0 / 72.27 },
	{ "pc", 12.0 * 72.0 / 72.27 },
	{ "dd", 1238.0 * 72.0 / 72.27 / 1157.0 },
	{ "cc", 14856.0 * 72.0 / 72.27 / 1157.0 },
	{ 0, 0 }
};

static int
readHexDigit(const char*& cp)
{
    if (*cp >= '0' && *cp <= '9')
        return *cp++ - '0';
    if (*cp >= 'A' && *cp <= 'F')
        return *cp++ - 'A' + 10;
    if (*cp >= 'a' && *cp <= 'f')
        return *cp++ - 'a' + 10;
    return 0;
}

static CGColorRef
readColorValue(const char* s)
{
	float components[4];
    int	x;
    x = readHexDigit(s);
    x <<= 4;
    x += readHexDigit(s);
    components[0] = x / 255.0;
    x = readHexDigit(s);
    x <<= 4;
    x += readHexDigit(s);
	components[1] = x / 255.0;
    x = readHexDigit(s);
    x <<= 4;
    x += readHexDigit(s);
	components[2] = x / 255.0;
	components[3] = 1.0;
	return CGColorCreate(gRGBColorSpace, components);
}

static CGColorRef
readColorValueWithAlpha(const char* s)
{
	float components[4];
    int	x;
    x = readHexDigit(s);
    x <<= 4;
    x += readHexDigit(s);
    components[0] = x / 255.0;
    x = readHexDigit(s);
    x <<= 4;
    x += readHexDigit(s);
	components[1] = x / 255.0;
    x = readHexDigit(s);
    x <<= 4;
    x += readHexDigit(s);
	components[2] = x / 255.0;
    x = readHexDigit(s);
    x <<= 4;
    x += readHexDigit(s);
	components[3] = x / 255.0;
	return CGColorCreate(gRGBColorSpace, components);
}

static float
readFloat(const char*& cp)
{
	float	result = 0.0;
	bool	negate = false;
	if (*cp == '-') {
		negate = true;
		++cp;
	}
	while (*cp >= '0' && *cp <= '9')
		result = result * 10 + *cp++ - '0';
	if (*cp == '.') {
		++cp;
		float	dec = 1.0;
		while (*cp >= '0' && *cp <= '9')
			result = result + (dec /= 10.0) * (*cp++ - '0');
	}
	if (negate)
		result = -result;
	return result;
}

void
doPDFspecial(const char* special)
{
	ensurePageStarted();

	if (gPdfMarkFile == NULL) {
		static bool firstTime = true;
		if (firstTime) {
			firstTime = false;
			CFURLRef	markURL = CFURLCreateCopyAppendingPathExtension(kCFAllocatorDefault, gSaveURL, CFSTR("marks"));
			Boolean	gotPath = CFURLGetFileSystemRepresentation(markURL, true, (UInt8*)gPdfMarkPath, _POSIX_PATH_MAX);
			if (gotPath) {
				gPdfMarkFile = fopen(gPdfMarkPath, "w");
				if (gPdfMarkFile == NULL)
					fprintf(stderr, "*** unable to write to PDFmark file \"%s\"\n", gPdfMarkPath);
			}
			else
				fprintf(stderr, "*** unable to get pathname for PDFmark file\n");
		}
	}
	if (gPdfMarkFile != NULL) {
		if (strncmp(special, "bann", 4) == 0) {
			gTagLevel = gDviLevel;
			initAnnotBox();
			gTrackingBoxes = true;
		}
		else if (strncmp(special, "eann", 4) == 0) {
			flushAnnotBox();
			gTrackingBoxes = false;
			gTagLevel = -1;
		}
		fprintf(gPdfMarkFile, "%d\t%f\t%f\t%s\n",
			gPageIndex, kDvi2Scr * dvi.h + 72.0, kDvi2Scr * (gPageHt - dvi.v) - 72.0, special);
	}
}

static void
pushTextColor()
{
	gTextColorStack.push_back(gTextColor);
}

static void
popTextColor()
{
	if (gTextColorStack.size() == 0) {
		fprintf(stderr, "\n*** text color stack underflow\n");
	}
	else {
		gTextColor = gTextColorStack.back();
		gTextColorStack.pop_back();
	}
}

static void
pushRuleColor()
{
	gRuleColorStack.push_back(gRuleColor);
}

static void
popRuleColor()
{
	if (gRuleColorStack.size() == 0) {
		fprintf(stderr, "\n*** rule color stack underflow\n");
	}
	else {
		gRuleColor = gRuleColorStack.back();
		gRuleColorStack.pop_back();
	}
}

static void
setRuleColor(CGColorRef color)
{
	gRuleColor = color;
}

static void
resetRuleColor()
{
	gRuleColor = kBlackColor;
}

static void
setTextColor(CGColorRef color)
{
	gTextColor = color;
}

static void
resetTextColor()
{
	gTextColor = kBlackColor;
}

static void
setBackground(CGColorRef bg)
{
	gBackground = bg;
	paintBackground();
}

static bool
readColorValues(const char*& s, int n, float* v)
{
	while (n-- > 0) {
		while (*s == ' ')
			++s;
		if ( !( ( (*s >= '0') && (*s <= '9') )		// unless we have a digit
				|| (*s == '.') || (*s == ',') ) )	// or decimal sign, break
			return false;
		float val = 0.0;
		while ((*s >= '0') && (*s <= '9')) {
			val = val * 10.0 + *s - '0';
			++s;
		}
		if ((*s == '.') || (*s == ',')) {
			++s;
			float	dec = 10.0;
			while ((*s >= '0') && (*s <= '9')) {
				val += (*s - '0') / dec;
				dec *= 10;
				++s;
			}
		}
		*v++ = val;
	}
	return true;
}

struct namedColor {
  char *name;
  float cmyk[4];
} colorsByName[] = {
  {"GreenYellow", {0.15, 0, 0.69, 0}},
  {"Yellow", {0, 0, 1, 0}},
  {"Goldenrod", {0, 0.10, 0.84, 0}},
  {"Dandelion", {0, 0.29, 0.84, 0}},
  {"Apricot", {0, 0.32, 0.52, 0}},
  {"Peach", {0, 0.50, 0.70, 0}},
  {"Melon", {0, 0.46, 0.50, 0}},
  {"YellowOrange", {0, 0.42, 1, 0}},
  {"Orange", {0, 0.61, 0.87, 0}},
  {"BurntOrange", {0, 0.51, 1, 0}},
  {"Bittersweet", {0, 0.75, 1, 0.24}},
  {"RedOrange", {0, 0.77, 0.87, 0}},
  {"Mahogany", {0, 0.85, 0.87, 0.35}},
  {"Maroon", {0, 0.87, 0.68, 0.32}},
  {"BrickRed", {0, 0.89, 0.94, 0.28}},
  {"Red", {0, 1, 1, 0}},
  {"OrangeRed", {0, 1, 0.50, 0}},
  {"RubineRed", {0, 1, 0.13, 0}},
  {"WildStrawberry", {0, 0.96, 0.39, 0}},
  {"Salmon", {0, 0.53, 0.38, 0}},
  {"CarnationPink", {0, 0.63, 0, 0}},
  {"Magenta", {0, 1, 0, 0}},
  {"VioletRed", {0, 0.81, 0, 0}},
  {"Rhodamine", {0, 0.82, 0, 0}},
  {"Mulberry", {0.34, 0.90, 0, 0.02}},
  {"RedViolet", {0.07, 0.90, 0, 0.34}},
  {"Fuchsia", {0.47, 0.91, 0, 0.08}},
  {"Lavender", {0, 0.48, 0, 0}},
  {"Thistle", {0.12, 0.59, 0, 0}},
  {"Orchid", {0.32, 0.64, 0, 0}},
  {"DarkOrchid", {0.40, 0.80, 0.20, 0}},
  {"Purple", {0.45, 0.86, 0, 0}},
  {"Plum", {0.50, 1, 0, 0}},
  {"Violet", {0.79, 0.88, 0, 0}},
  {"RoyalPurple", {0.75, 0.90, 0, 0}},
  {"BlueViolet", {0.86, 0.91, 0, 0.04}},
  {"Periwinkle", {0.57, 0.55, 0, 0}},
  {"CadetBlue", {0.62, 0.57, 0.23, 0}},
  {"CornflowerBlue", {0.65, 0.13, 0, 0}},
  {"MidnightBlue", {0.98, 0.13, 0, 0.43}},
  {"NavyBlue", {0.94, 0.54, 0, 0}},
  {"RoyalBlue", {1, 0.50, 0, 0}},
  {"Blue", {1, 1, 0, 0}},
  {"Cerulean", {0.94, 0.11, 0, 0}},
  {"Cyan", {1, 0, 0, 0}},
  {"ProcessBlue", {0.96, 0, 0, 0}},
  {"SkyBlue", {0.62, 0, 0.12, 0}},
  {"Turquoise", {0.85, 0, 0.20, 0}},
  {"TealBlue", {0.86, 0, 0.34, 0.02}},
  {"Aquamarine", {0.82, 0, 0.30, 0}},
  {"BlueGreen", {0.85, 0, 0.33, 0}},
  {"Emerald", {1, 0, 0.50, 0}},
  {"JungleGreen", {0.99, 0, 0.52, 0}},
  {"SeaGreen", {0.69, 0, 0.50, 0}},
  {"Green", {1, 0, 1, 0}},
  {"ForestGreen", {0.91, 0, 0.88, 0.12}},
  {"PineGreen", {0.92, 0, 0.59, 0.25}},
  {"LimeGreen", {0.50, 0, 1, 0}},
  {"YellowGreen", {0.44, 0, 0.74, 0}},
  {"SpringGreen", {0.26, 0, 0.76, 0}},
  {"OliveGreen", {0.64, 0, 0.95, 0.40}},
  {"RawSienna", {0, 0.72, 1, 0.45}},
  {"Sepia", {0, 0.83, 1, 0.70}},
  {"Brown", {0, 0.81, 1, 0.60}},
  {"Tan", {0.14, 0.42, 0.56, 0}},
  {"Gray", {0, 0, 0, 0.50}},
  {"Black", {0, 0, 0, 1}},
  {"White", {0, 0, 0, 0}}
};

inline bool
kwmatch(const char*& s, const char* k)
{
	while (*s == ' ')
		++s;
	int kwlen = strlen(k);
	if ( (s[kwlen] == 0) || (s[kwlen] == ' ') ) {
		if (strncmp(s, k, kwlen) == 0) {
			s += kwlen;
			return true;
		}
	}
	return false;
}

static float
readAlphaIfPresent(const char*& s)
{
	if (kwmatch(s, "alpha")) {
		float	alpha;
		if (readColorValues(s, 1, &alpha))
			return alpha;
	}
	return 1.0;
}

static void
doColorSpecial(const char* s, bool background)
{
	while (*s != 0) {
		if (!background) {
			if (kwmatch(s, "push")) {
				pushRuleColor();
				pushTextColor();
				continue;
			}
			
			if (kwmatch(s, "pop")) {
				popRuleColor();
				popTextColor();
				continue;
			}
		}
		
		if (kwmatch(s, "rgb")) {
			float	rgba[4];
			if (readColorValues(s, 3, rgba)) {
				rgba[3] = readAlphaIfPresent(s);
				CGColorRef	c = CGColorCreate(gRGBColorSpace, rgba);
				if (background)
					setBackground(c);
				else {
					setRuleColor(c);
					setTextColor(c);
				}
			}
			break;
		}
		
		if (kwmatch(s, "cmyk")) {
			float	cmyka[5];
			if (readColorValues(s, 4, cmyka)) {
				cmyka[4] = readAlphaIfPresent(s);
				CGColorRef	c = CGColorCreate(gCMYKColorSpace, cmyka);
				if (background)
					setBackground(c);
				else {
					setRuleColor(c);
					setTextColor(c);
				}
			}
			break;
		}
		
		if (kwmatch(s, "hsb") || kwmatch(s, "hsv")) {
			float	hsb[3];
			if (readColorValues(s, 3, hsb)) {
				CMColor	cm;
				cm.hsv.hue = (UInt16)(hsb[0] * 65535);
				cm.hsv.saturation = (UInt16)(hsb[1] * 65535);
				cm.hsv.value = (UInt16)(hsb[2] * 65535);
				CMConvertHSVToRGB(&cm, &cm, 1);
				float	rgba[4];
				rgba[0] = cm.rgb.red / 65535.0;
				rgba[1] = cm.rgb.green / 65535.0;
				rgba[2] = cm.rgb.blue / 65535.0;
				rgba[3] = readAlphaIfPresent(s);
				CGColorRef	c = CGColorCreate(gRGBColorSpace, rgba);
				if (background)
					setBackground(c);
				else {
					setRuleColor(c);
					setTextColor(c);
				}
			}
			break;
		}
		
		if (kwmatch(s, "hls")) {
			float	hls[3];
			if (readColorValues(s, 3, hls)) {
				CMColor	cm;
				cm.hls.hue = (UInt16)(hls[0] * 65535);
				cm.hls.lightness = (UInt16)(hls[1] * 65535);
				cm.hls.saturation = (UInt16)(hls[2] * 65535);
				CMConvertHLSToRGB(&cm, &cm, 1);
				float	rgba[4];
				rgba[0] = cm.rgb.red / 65535.0;
				rgba[1] = cm.rgb.green / 65535.0;
				rgba[2] = cm.rgb.blue / 65535.0;
				rgba[3] = readAlphaIfPresent(s);
				CGColorRef	c = CGColorCreate(gRGBColorSpace, rgba);
				if (background)
					setBackground(c);
				else {
					setRuleColor(c);
					setTextColor(c);
				}
			}
			break;
		}
		
		if (kwmatch(s, "gray")) {
			float	gray[2];
			if (readColorValues(s, 1, gray)) {
				gray[1] = readAlphaIfPresent(s);
				CGColorRef	c = CGColorCreate(gGrayColorSpace, gray);
				if (background)
					setBackground(c);
				else {
					setRuleColor(c);
					setTextColor(c);
				}
			}
			break;
		}
		
		const namedColor*	nc = colorsByName + sizeof(colorsByName) / sizeof(namedColor);
		while (nc-- > colorsByName) {
			if (kwmatch(s, nc->name)) {
				float	cmyka[5];
				cmyka[0] = nc->cmyk[0];
				cmyka[1] = nc->cmyk[1];
				cmyka[2] = nc->cmyk[2];
				cmyka[3] = nc->cmyk[3];
				cmyka[4] = readAlphaIfPresent(s);
				CGColorRef	c = CGColorCreate(gCMYKColorSpace, cmyka);
				if (background)
					setBackground(c);
				else {
					setRuleColor(c);
					setTextColor(c);
				}
				break;
			}
		}
		break;		
	}
}

static double_t
readDimen(const char* arg)	// currently reads unsigned dimens only; no negative paper sizes!
{
	double_t	rval = 0.0;
	while ((*arg >= '0') && (*arg <= '9')) {
		rval *= 10.0;
		rval += *arg - '0';
		++arg;
	}
	if (*arg == '.') {
		++arg;
		double_t	frac = 10.0;
		while ((*arg >= '0') && (*arg <= '9')) {
			rval += (*arg - '0') / frac;
			frac *= 10.0;
			++arg;
		}
	}
	const dimenRec*	dim = &sDimenRatios[0];
	while (dim->name != 0) {
		if (strcasecmp(arg, dim->name) == 0) {
			rval *= dim->factor;
			break;
		}
		++dim;
	}
	return rval;
}

bool
getPaperSize(const char* arg, double_t& paperWd, double_t& paperHt)
{
	paperHt = paperWd = 0.0;

	char*   s1 = strdup(arg);
	char*   s2 = strchr(s1, ':');
	if (s2 != NULL)
		*s2++ = 0;

	const paperSizeRec*	ps = &gPaperSizes[0];
	while (ps->name != 0) {
		if (strcasecmp(s1, ps->name) == 0) {
			paperWd = ps->width;
			paperHt = ps->height;
			break;
		}
		++ps;
	}
	if (ps->name == 0) {
		char*   s3 = strchr(s1, ',');
		if (s3 != NULL) {
			*s3++ = 0;
			paperWd = readDimen(s1);
			paperHt = readDimen(s3);
		}
	}
	if (s2 != NULL && strcasecmp(s2, "landscape") == 0) {
		double_t  t = paperHt;
		paperHt = paperWd;
		paperWd = t;
	}
	
	free(s1);
	
	return (paperHt > 0.0) && (paperWd > 0.0);
}

static bool
prefixMatch(const char* str, const char* prefix, const char*& remainder)
{
	int	len = strlen(prefix);
	if (strncmp(str, prefix, len) == 0) {
		remainder = str + len;
		return true;
	}
	return false;
}

inline bool
strMatch(const char* s, const char* t)
{
	return (strcmp(s, t) == 0);
}

static void
shadowWarning()
{
	static bool	alreadyWarned = false;
	if (alreadyWarned)
		return;
	fprintf(stderr, "\n"
					"## xdv2pdf: CoreGraphics shadow effects not supported on this system\n"
					"## (at least Mac OS X 10.3 is required)\n");
	alreadyWarned = true;
}

void
doSpecial(const char* special)
{
	int	u = strlen(special);
	const char* specialArg;
	
	if (prefixMatch(special, "pdf:", specialArg))
		doPDFspecial(specialArg);
	
	else if (prefixMatch(special, "color ", specialArg))
		doColorSpecial(specialArg, false);
	
	else if (prefixMatch(special, "background ", specialArg))
		doColorSpecial(specialArg, true);
	
	else if (prefixMatch(special, "x:fontmapfile", specialArg))
		doPdfMapFile(specialArg);
	else if (prefixMatch(special, "x:fontmapline", specialArg))
		doPdfMapLine(specialArg, 0);
	
	else if (strMatch(special, "x:textcolorpush"))
		pushTextColor();
	else if (strMatch(special, "x:textcolorpop"))
		popTextColor();
	else if (strMatch(special, "x:rulecolorpush"))
		pushRuleColor();
	else if (strMatch(special, "x:rulecolorpop"))
		popRuleColor();

	else if (prefixMatch(special, "x:rulecolor", specialArg)) {
		// set rule color
		if (*specialArg != '=' || u < 18)
			// not long enough for a proper color spec, so set it to black
			resetRuleColor();
		else {
			++specialArg;
			CGColorRef	color;
			if (u < 20)
				color = readColorValue(specialArg);
			else
				color = readColorValueWithAlpha(specialArg);
			setRuleColor(color);
		}
	}

	else if (prefixMatch(special, "x:textcolor", specialArg)) {
		// set text color (overriding color of the font style)
		if (*specialArg != '=' || u < 18)
			resetTextColor();
		else {
			++specialArg;
			CGColorRef	color;
			if (u < 20)
				color = readColorValue(specialArg);
			else
				color = readColorValueWithAlpha(specialArg);
			setTextColor(color);
		}
	}

	else if (strMatch(special, "x:gsave")) {
		ensurePageStarted();
		CGContextSaveGState(gCtx);
	}
	else if (strMatch(special, "x:grestore")) {
		ensurePageStarted();
		CGContextRestoreGState(gCtx);
		setColor(gCurrentColor, true);
		cur_cgFont = kUndefinedFont;
	}
	else if (prefixMatch(special, "x:scale", specialArg)) {
		ensurePageStarted();
		while (*specialArg && (*specialArg <= ' '))
			++specialArg;
		float	xscale = atof(specialArg);
		while (*specialArg && (((*specialArg >= '0') && (*specialArg <= '9')) || *specialArg == '.' || *specialArg == '-'))
			++specialArg;
		while (*specialArg && (*specialArg <= ' '))
			++specialArg;
		float	yscale = atof(specialArg);
		if ((xscale != 0.0) && (yscale != 0.0)) {
			CGContextTranslateCTM(gCtx, kDvi2Scr * dvi.h, kDvi2Scr * (gPageHt - dvi.v));
			CGContextScaleCTM(gCtx, xscale, yscale);
			CGContextTranslateCTM(gCtx, -kDvi2Scr * dvi.h, -kDvi2Scr * (gPageHt - dvi.v));
		}
	}
	else if (prefixMatch(special, "x:rotate", specialArg)) {
		ensurePageStarted();
		while (*specialArg && (*specialArg <= ' '))
			++specialArg;
		float	rotation = atof(specialArg);
		CGContextTranslateCTM(gCtx, kDvi2Scr * dvi.h, kDvi2Scr * (gPageHt - dvi.v));
		CGContextRotateCTM(gCtx, rotation * M_PI / 180.0);
		CGContextTranslateCTM(gCtx, -kDvi2Scr * dvi.h, -kDvi2Scr * (gPageHt - dvi.v));
	}
	else if (prefixMatch(special, "x:backgroundcolor", specialArg)) {
		ensurePageStarted();
		if ((*specialArg != '=') || (u < 24)) {
			// ignore incorrect special
		}
		else {
			++specialArg;
			CGColorRef	bg;
			if (u < 26)
				bg = readColorValue(specialArg);
			else
				bg = readColorValueWithAlpha(specialArg);
			setBackground(bg);
	   }
	}
	else if (prefixMatch(special, "x:shadow", specialArg)) {
		// syntax: \special{x:shadow(x,y),b}
		do {
			CGSize	offset;
			float	blur;
			
			if (*specialArg++ != '(')	break;
			offset.width = readFloat(specialArg);
			
			if (*specialArg++ != ',')	break;
			offset.height = readFloat(specialArg);
			
			if (*specialArg++ != ')')	break;
			if (*specialArg++ != ',')	break;
			blur = readFloat(specialArg);
			
			// NB: API only available on 10.3 and later
			if (&CGContextSetShadow != NULL)
				CGContextSetShadow(gCtx, offset, blur);
			else
				shadowWarning();
		} while (false);
	}
	else if (prefixMatch(special, "x:colorshadow", specialArg)) {
		// syntax: \special{x:colorshadow(x,y),b,c}
		do {
			CGSize		offset;
			float		blur;
			CGColorRef	color;
			
			if (*specialArg++ != '(')	break;
			offset.width = readFloat(specialArg);
			
			if (*specialArg++ != ',')	break;
			offset.height = readFloat(specialArg);
			
			if (*specialArg++ != ')')	break;
			if (*specialArg++ != ',')	break;
			blur = readFloat(specialArg);
			
			if (*specialArg++ != ',')	break;
			if (special + u < specialArg + 6)	break;
			if (special + u < specialArg + 8)
				color = readColorValue(specialArg);
			else
				color = readColorValueWithAlpha(specialArg);
			
			// NB: APIs only available on 10.3 and later
			if (&CGContextSetShadowWithColor != NULL) {
				CGContextSetShadowWithColor(gCtx, offset, blur, color);
			}
			else
				shadowWarning();
			CGColorRelease(color);
		} while (false);
	}
	else if (prefixMatch(special, "x:papersize", specialArg) || prefixMatch(special, "papersize", specialArg)) {
		// syntax: \special{x:papersize=a4:landscape} etc
		while (*specialArg && (*specialArg <= ' '))
			++specialArg;
		if (*specialArg == '=') {
			++specialArg;
			while (*specialArg && (*specialArg <= ' '))
				++specialArg;
		}
		double_t	paperWd, paperHt;
		if (getPaperSize(specialArg, paperWd, paperHt)) {
			gMediaBox = CGRectMake(0, 0, paperWd, paperHt);
			if (gPageStarted) {
				fprintf(stderr, "\n### warning on page [");
				int i, j;
				for (j = 9; j > 1; --j)
					if (gCounts[j] != 0)
						break;
				for (i = 0; i < j; ++i)
					fprintf(stderr, "%d.", gCounts[i]);
				fprintf(stderr, "%d", gCounts[j]);
				fprintf(stderr, "]: paper size \"%s\" will take effect from NEXT page ", specialArg);
			}
		}
	}
}