// Copyright (C) 2004-2021 Artifex Software, Inc.
//
// This file is part of MuPDF.
//
// MuPDF is free software: you can redistribute it and/or modify it under the
// terms of the GNU Affero General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// MuPDF 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 Affero General Public License for more
// details.
//
// You should have received a copy of the GNU Affero General Public License
// along with MuPDF. If not, see <https://www.gnu.org/licenses/agpl-3.0.en.html>
//
// Alternative licensing terms are available from the licensor.
// For commercial licensing, see <https://www.artifex.com/> or contact
// Artifex Software, Inc., 39 Mesa Street, Suite 108A, San Francisco,
// CA 94129, USA, for further information.

#include "mupdf/fitz.h"
#include "xps-imp.h"

#include <string.h>
#include <stdio.h> /* for sscanf */
#include <math.h> /* for pow */

static inline int unhex(int a)
{
	if (a >= 'A' && a <= 'F') return a - 'A' + 0xA;
	if (a >= 'a' && a <= 'f') return a - 'a' + 0xA;
	if (a >= '0' && a <= '9') return a - '0';
	return 0;
}

fz_xml *
xps_lookup_alternate_content(fz_context *ctx, xps_document *doc, fz_xml *node)
{
	for (node = fz_xml_down(node); node; node = fz_xml_next(node))
	{
		if (fz_xml_is_tag(node, "Choice") && fz_xml_att(node, "Requires"))
		{
			char list[64];
			char *next = list, *item;
			fz_strlcpy(list, fz_xml_att(node, "Requires"), sizeof(list));
			while ((item = fz_strsep(&next, " \t\r\n")) != NULL && (!*item || !strcmp(item, "xps")));
			if (!item)
				return fz_xml_down(node);
		}
		else if (fz_xml_is_tag(node, "Fallback"))
			return fz_xml_down(node);
	}
	return NULL;
}

void
xps_parse_brush(fz_context *ctx, xps_document *doc, fz_matrix ctm, fz_rect area, char *base_uri, xps_resource *dict, fz_xml *node)
{
	if (doc->cookie && doc->cookie->abort)
		return;
	/* SolidColorBrushes are handled in a special case and will never show up here */
	if (fz_xml_is_tag(node, "ImageBrush"))
		xps_parse_image_brush(ctx, doc, ctm, area, base_uri, dict, node);
	else if (fz_xml_is_tag(node, "VisualBrush"))
		xps_parse_visual_brush(ctx, doc, ctm, area, base_uri, dict, node);
	else if (fz_xml_is_tag(node, "LinearGradientBrush"))
		xps_parse_linear_gradient_brush(ctx, doc, ctm, area, base_uri, dict, node);
	else if (fz_xml_is_tag(node, "RadialGradientBrush"))
		xps_parse_radial_gradient_brush(ctx, doc, ctm, area, base_uri, dict, node);
	else
		fz_warn(ctx, "unknown brush tag");
}

void
xps_parse_element(fz_context *ctx, xps_document *doc, fz_matrix ctm, fz_rect area, char *base_uri, xps_resource *dict, fz_xml *node)
{
	if (doc->cookie && doc->cookie->abort)
		return;
	if (fz_xml_is_tag(node, "Path"))
		xps_parse_path(ctx, doc, ctm, base_uri, dict, node);
	if (fz_xml_is_tag(node, "Glyphs"))
		xps_parse_glyphs(ctx, doc, ctm, base_uri, dict, node);
	if (fz_xml_is_tag(node, "Canvas"))
		xps_parse_canvas(ctx, doc, ctm, area, base_uri, dict, node);
	if (fz_xml_is_tag(node, "AlternateContent"))
	{
		node = xps_lookup_alternate_content(ctx, doc, node);
		if (node)
			xps_parse_element(ctx, doc, ctm, area, base_uri, dict, node);
	}
	/* skip unknown tags (like Foo.Resources and similar) */
}

void
xps_begin_opacity(fz_context *ctx, xps_document *doc, fz_matrix ctm, fz_rect area,
	char *base_uri, xps_resource *dict,
	char *opacity_att, fz_xml *opacity_mask_tag)
{
	fz_device *dev = doc->dev;
	float opacity;

	if (!opacity_att && !opacity_mask_tag)
		return;

	opacity = 1;
	if (opacity_att)
		opacity = fz_atof(opacity_att);

	if (fz_xml_is_tag(opacity_mask_tag, "SolidColorBrush"))
	{
		char *scb_opacity_att = fz_xml_att(opacity_mask_tag, "Opacity");
		char *scb_color_att = fz_xml_att(opacity_mask_tag, "Color");
		if (scb_opacity_att)
			opacity = opacity * fz_atof(scb_opacity_att);
		if (scb_color_att)
		{
			fz_colorspace *colorspace;
			float samples[FZ_MAX_COLORS];
			xps_parse_color(ctx, doc, base_uri, scb_color_att, &colorspace, samples);
			opacity = opacity * samples[0];
		}
		opacity_mask_tag = NULL;
	}

	if (doc->opacity_top + 1 < (int)nelem(doc->opacity))
	{
		doc->opacity[doc->opacity_top + 1] = doc->opacity[doc->opacity_top] * opacity;
		doc->opacity_top++;
	}

	if (opacity_mask_tag)
	{
		fz_begin_mask(ctx, dev, area, 0, NULL, NULL, fz_default_color_params);
		xps_parse_brush(ctx, doc, ctm, area, base_uri, dict, opacity_mask_tag);
		fz_end_mask(ctx, dev);
	}
}

void
xps_end_opacity(fz_context *ctx, xps_document *doc, char *base_uri, xps_resource *dict,
	char *opacity_att, fz_xml *opacity_mask_tag)
{
	fz_device *dev = doc->dev;

	if (!opacity_att && !opacity_mask_tag)
		return;

	if (doc->opacity_top > 0)
		doc->opacity_top--;

	if (opacity_mask_tag)
	{
		if (!fz_xml_is_tag(opacity_mask_tag, "SolidColorBrush"))
			fz_pop_clip(ctx, dev);
	}
}

static fz_matrix
xps_parse_render_transform(fz_context *ctx, xps_document *doc, char *transform)
{
	fz_matrix matrix;
	float args[6];
	char *s = transform;
	int i;

	args[0] = 1; args[1] = 0;
	args[2] = 0; args[3] = 1;
	args[4] = 0; args[5] = 0;

	for (i = 0; i < 6 && *s; i++)
	{
		args[i] = fz_atof(s);
		while (*s && *s != ',')
			s++;
		if (*s == ',')
			s++;
	}

	matrix.a = args[0]; matrix.b = args[1];
	matrix.c = args[2]; matrix.d = args[3];
	matrix.e = args[4]; matrix.f = args[5];
	return matrix;
}

static fz_matrix
xps_parse_matrix_transform(fz_context *ctx, xps_document *doc, fz_xml *root)
{
	if (fz_xml_is_tag(root, "MatrixTransform"))
	{
		char *transform = fz_xml_att(root, "Matrix");
		if (transform)
			return xps_parse_render_transform(ctx, doc, transform);
	}
	return fz_identity;
}

fz_matrix
xps_parse_transform(fz_context *ctx, xps_document *doc, char *att, fz_xml *tag, fz_matrix ctm)
{
	if (att)
		return fz_concat(xps_parse_render_transform(ctx, doc, att), ctm);
	if (tag)
		return fz_concat(xps_parse_matrix_transform(ctx, doc, tag), ctm);
	return ctm;
}

fz_rect
xps_parse_rectangle(fz_context *ctx, xps_document *doc, char *text)
{
	fz_rect rect;
	float args[4];
	char *s = text;
	int i;

	args[0] = 0; args[1] = 0;
	args[2] = 1; args[3] = 1;

	for (i = 0; i < 4 && *s; i++)
	{
		args[i] = fz_atof(s);
		while (*s && *s != ',')
			s++;
		if (*s == ',')
			s++;
	}

	rect.x0 = args[0];
	rect.y0 = args[1];
	rect.x1 = args[0] + args[2];
	rect.y1 = args[1] + args[3];
	return rect;
}

static int count_commas(char *s)
{
	int n = 0;
	while (*s)
	{
		if (*s == ',')
			n ++;
		s ++;
	}
	return n;
}

static float sRGB_from_scRGB(float x)
{
	if (x < 0.0031308f)
		return 12.92f * x;
	return 1.055f * powf(x, 1/2.4f) - 0.055f;
}

void
xps_parse_color(fz_context *ctx, xps_document *doc, char *base_uri, char *string,
		fz_colorspace **csp, float *samples)
{
	char *p;
	int i, n;
	char buf[1024];
	char *profile;

	*csp = fz_device_rgb(ctx);

	samples[0] = 1;
	samples[1] = 0;
	samples[2] = 0;
	samples[3] = 0;

	if (string[0] == '#')
	{
		size_t z = strlen(string);
		if (z == 9)
		{
			samples[0] = unhex(string[1]) * 16 + unhex(string[2]);
			samples[1] = unhex(string[3]) * 16 + unhex(string[4]);
			samples[2] = unhex(string[5]) * 16 + unhex(string[6]);
			samples[3] = unhex(string[7]) * 16 + unhex(string[8]);
		}
		else
		{
			samples[0] = 255;
/* Use a macro to protect against overrunning the string. */
#define UNHEX(idx) (idx < z ? unhex(string[idx]) : 0)
			samples[1] = UNHEX(1) * 16 + UNHEX(2);
			samples[2] = UNHEX(3) * 16 + UNHEX(4);
			samples[3] = UNHEX(5) * 16 + UNHEX(6);
#undef UNHEX
		}

		samples[0] /= 255;
		samples[1] /= 255;
		samples[2] /= 255;
		samples[3] /= 255;
	}

	else if (string[0] == 's' && string[1] == 'c' && string[2] == '#')
	{
		if (count_commas(string) == 2)
			sscanf(string, "sc#%g,%g,%g", samples + 1, samples + 2, samples + 3);
		if (count_commas(string) == 3)
			sscanf(string, "sc#%g,%g,%g,%g", samples, samples + 1, samples + 2, samples + 3);

		/* Convert from scRGB gamma 1.0 to sRGB gamma */
		samples[1] = sRGB_from_scRGB(samples[1]);
		samples[2] = sRGB_from_scRGB(samples[2]);
		samples[3] = sRGB_from_scRGB(samples[3]);
	}

	else if (strstr(string, "ContextColor ") == string)
	{
		/* Crack the string for profile name and sample values */
		fz_strlcpy(buf, string, sizeof buf);

		profile = strchr(buf, ' ');
		if (!profile)
		{
			fz_warn(ctx, "cannot find icc profile uri in '%s'", string);
			return;
		}

		*profile++ = 0;
		p = strchr(profile, ' ');
		if (!p)
		{
			fz_warn(ctx, "cannot find component values in '%s'", profile);
			return;
		}

		*p++ = 0;
		n = count_commas(p) + 1;
		if (n > FZ_MAX_COLORS)
		{
			fz_warn(ctx, "ignoring %d color components (max %d allowed)", n - FZ_MAX_COLORS, FZ_MAX_COLORS);
			n = FZ_MAX_COLORS;
		}
		i = 0;
		while (i < n)
		{
			samples[i++] = fz_atof(p);
			p = strchr(p, ',');
			if (!p)
				break;
			p ++;
			if (*p == ' ')
				p ++;
		}
		while (i < n)
		{
			samples[i++] = 0;
		}

		/* TODO: load ICC profile */
		switch (n)
		{
		case 2: *csp = fz_device_gray(ctx); break;
		case 4: *csp = fz_device_rgb(ctx); break;
		case 5: *csp = fz_device_cmyk(ctx); break;
		default: *csp = fz_device_gray(ctx); break;
		}
	}
}

void
xps_set_color(fz_context *ctx, xps_document *doc, fz_colorspace *colorspace, float *samples)
{
	int i;
	int n = fz_colorspace_n(ctx, colorspace);
	doc->colorspace = colorspace;
	for (i = 0; i < n; i++)
		doc->color[i] = samples[i + 1];
	doc->alpha = samples[0] * doc->opacity[doc->opacity_top];
}
