// Copyright (C) 2004-2024 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 "color-imp.h"

typedef struct
{
	fz_device super;
	int *is_color;
	float threshold;
	int options;
	int resolved;
} fz_test_device;

static int
is_rgb_color(float threshold, float r, float g, float b)
{
	float rg_diff = fz_abs(r - g);
	float rb_diff = fz_abs(r - b);
	float gb_diff = fz_abs(g - b);
	return rg_diff > threshold || rb_diff > threshold || gb_diff > threshold;
}

static int
is_rgb_color_u8(int threshold_u8, int r, int g, int b)
{
	int rg_diff = fz_absi(r - g);
	int rb_diff = fz_absi(r - b);
	int gb_diff = fz_absi(g - b);
	return rg_diff > threshold_u8 || rb_diff > threshold_u8 || gb_diff > threshold_u8;
}

static void
fz_test_color(fz_context *ctx, fz_test_device *t, fz_colorspace *colorspace, const float *color, fz_color_params color_params)
{
	if (!*t->is_color && colorspace && fz_colorspace_type(ctx, colorspace) != FZ_COLORSPACE_GRAY)
	{
		if (colorspace == fz_device_rgb(ctx))
		{
			if (is_rgb_color(t->threshold, color[0], color[1], color[2]))
			{
				*t->is_color = 2;
				t->resolved = 1;
				if (t->super.passthrough == NULL)
					fz_throw(ctx, FZ_ERROR_ABORT, "Page found as color; stopping interpretation");
			}
		}
		else
		{
			float rgb[3];
			fz_convert_color(ctx, colorspace, color, fz_device_rgb(ctx), rgb, NULL, color_params);
			if (is_rgb_color(t->threshold, rgb[0], rgb[1], rgb[2]))
			{
				*t->is_color = 2;
				t->resolved = 1;
				if (t->super.passthrough == NULL)
					fz_throw(ctx, FZ_ERROR_ABORT, "Page found as color; stopping interpretation");
			}
		}
	}
}

static void
fz_test_fill_path(fz_context *ctx, fz_device *dev_, const fz_path *path, int even_odd, fz_matrix ctm,
	fz_colorspace *colorspace, const float *color, float alpha, fz_color_params color_params)
{
	fz_test_device *dev = (fz_test_device*)dev_;

	if (dev->resolved == 0 && alpha != 0.0f)
		fz_test_color(ctx, dev, colorspace, color, color_params);
	if (dev->super.passthrough)
		fz_fill_path(ctx, dev->super.passthrough, path, even_odd, ctm, colorspace, color, alpha, color_params);
}

static void
fz_test_stroke_path(fz_context *ctx, fz_device *dev_, const fz_path *path, const fz_stroke_state *stroke,
	fz_matrix ctm, fz_colorspace *colorspace, const float *color, float alpha, fz_color_params color_params)
{
	fz_test_device *dev = (fz_test_device*)dev_;

	if (dev->resolved == 0 && alpha != 0.0f)
		fz_test_color(ctx, dev, colorspace, color, color_params);
	if (dev->super.passthrough)
		fz_stroke_path(ctx, dev->super.passthrough, path, stroke, ctm, colorspace, color, alpha, color_params);
}

static void
fz_test_fill_text(fz_context *ctx, fz_device *dev_, const fz_text *text, fz_matrix ctm,
	fz_colorspace *colorspace, const float *color, float alpha, fz_color_params color_params)
{
	fz_test_device *dev = (fz_test_device*)dev_;

	if (dev->resolved == 0 && alpha != 0.0f)
		fz_test_color(ctx, dev, colorspace, color, color_params);
	if (dev->super.passthrough)
		fz_fill_text(ctx, dev->super.passthrough, text, ctm, colorspace, color, alpha, color_params);
}

static void
fz_test_stroke_text(fz_context *ctx, fz_device *dev_, const fz_text *text, const fz_stroke_state *stroke,
	fz_matrix ctm, fz_colorspace *colorspace, const float *color, float alpha, fz_color_params color_params)
{
	fz_test_device *dev = (fz_test_device*)dev_;

	if (dev->resolved == 0 && alpha != 0.0f)
		fz_test_color(ctx, dev, colorspace, color, color_params);
	if (dev->super.passthrough)
		fz_stroke_text(ctx, dev->super.passthrough, text, stroke, ctm, colorspace, color, alpha, color_params);
}

struct shadearg
{
	fz_test_device *dev;
	fz_shade *shade;
	fz_color_params color_params;
};

static void
prepare_vertex(fz_context *ctx, void *arg_, fz_vertex *v, const float *color)
{
	struct shadearg *arg = arg_;
	fz_test_device *dev = arg->dev;
	fz_shade *shade = arg->shade;
	if (shade->function_stride == 0)
		fz_test_color(ctx, dev, shade->colorspace, color, arg->color_params);
}

static void
fz_test_fill_shade(fz_context *ctx, fz_device *dev_, fz_shade *shade, fz_matrix ctm, float alpha, fz_color_params color_params)
{
	fz_test_device *dev = (fz_test_device*)dev_;

	if (dev->resolved == 0)
	{
		if ((dev->options & FZ_TEST_OPT_SHADINGS) == 0)
		{
			if (fz_colorspace_type(ctx, shade->colorspace) != FZ_COLORSPACE_GRAY)
			{
				/* Don't test every pixel. Upgrade us from "black and white" to "probably color" */
				if (*dev->is_color == 0)
					*dev->is_color = 1;
				dev->resolved = 1;
				if (dev->super.passthrough == NULL)
					fz_throw(ctx, FZ_ERROR_ABORT, "Page found as color; stopping interpretation");
			}
		}
		else
		{
			int stride = shade->function_stride;
			if (stride)
			{
				int i;
				for (i = 0; i < 256; i++)
					fz_test_color(ctx, dev, shade->colorspace, &shade->function[i*stride], color_params);
			}
			else
			{
				struct shadearg arg;
				arg.dev = dev;
				arg.shade = shade;
				arg.color_params = color_params;
				fz_process_shade(ctx, shade, ctm, fz_device_current_scissor(ctx, dev_), prepare_vertex, NULL, &arg);
			}
		}
	}
	if (dev->super.passthrough)
		fz_fill_shade(ctx, dev->super.passthrough, shade, ctm, alpha, color_params);
}

static void fz_test_fill_compressed_8bpc_image(fz_context *ctx, fz_test_device *dev, fz_image *image, fz_stream *stream, fz_color_params color_params)
{
	unsigned int count = (unsigned int)image->w * (unsigned int)image->h;
	unsigned int i;

	if (image->colorspace == fz_device_rgb(ctx))
	{
		int threshold_u8 = dev->threshold * 255;
		for (i = 0; i < count; i++)
		{
			int r = fz_read_byte(ctx, stream);
			int g = fz_read_byte(ctx, stream);
			int b = fz_read_byte(ctx, stream);
			if (is_rgb_color_u8(threshold_u8, r, g, b))
			{
				*dev->is_color = 1;
				dev->resolved = 1;
				if (dev->super.passthrough == NULL)
					fz_throw(ctx, FZ_ERROR_ABORT, "Page found as color; stopping interpretation");
				break;
			}
		}
	}
	else
	{
		fz_color_converter cc;
		unsigned int n = (unsigned int)image->n;

		fz_init_cached_color_converter(ctx, &cc, image->colorspace, fz_device_rgb(ctx), NULL, NULL, color_params);

		fz_try(ctx)
		{
			for (i = 0; i < count; i++)
			{
				float cs[FZ_MAX_COLORS];
				float ds[FZ_MAX_COLORS];
				unsigned int k;

				for (k = 0; k < n; k++)
					cs[k] = fz_read_byte(ctx, stream) / 255.0f;

				cc.convert(ctx, &cc, ds, cs);

				if (is_rgb_color(dev->threshold, ds[0], ds[1], ds[2]))
				{
					*dev->is_color = 1;
					dev->resolved = 1;
					if (dev->super.passthrough == NULL)
						fz_throw(ctx, FZ_ERROR_ABORT, "Page found as color; stopping interpretation");
					break;
				}
			}
		}
		fz_always(ctx)
			fz_fin_cached_color_converter(ctx, &cc);
		fz_catch(ctx)
			fz_rethrow(ctx);
	}
}

static void
fz_test_fill_other_image(fz_context *ctx, fz_test_device *dev, fz_pixmap *pix, fz_color_params color_params)
{
	unsigned int count, i, k, h, sa;
	size_t ss;
	unsigned char *s;

	count = pix->w;
	h = pix->h;
	s = pix->samples;
	sa = pix->alpha;
	ss = pix->stride - pix->w * (size_t)pix->n;

	if (pix->colorspace == fz_device_rgb(ctx))
	{
		int threshold_u8 = dev->threshold * 255;
		while (h--)
		{
			for (i = 0; i < count; i++)
			{
				if ((!sa || s[3] != 0) && is_rgb_color_u8(threshold_u8, s[0], s[1], s[2]))
				{
					*dev->is_color = 1;
					dev->resolved = 1;
					if (dev->super.passthrough == NULL)
						fz_throw(ctx, FZ_ERROR_ABORT, "Page found as color; stopping interpretation");
					break;
				}
				s += 3 + sa;
			}
			s += ss;
		}
	}
	else
	{
		fz_color_converter cc;
		unsigned int n = (unsigned int)pix->n-1;

		fz_init_cached_color_converter(ctx, &cc, pix->colorspace, fz_device_rgb(ctx), NULL, NULL, color_params);

		fz_try(ctx)
		{
			while (h--)
			{
				for (i = 0; i < count; i++)
				{
					float cs[FZ_MAX_COLORS];
					float ds[FZ_MAX_COLORS];

					for (k = 0; k < n; k++)
						cs[k] = (*s++) / 255.0f;
					if (sa && *s++ == 0)
						continue;

					cc.convert(ctx, &cc, ds, cs);

					if (is_rgb_color(dev->threshold, ds[0], ds[1], ds[2]))
					{
						*dev->is_color = 1;
						dev->resolved = 1;
						if (dev->super.passthrough == NULL)
							fz_throw(ctx, FZ_ERROR_ABORT, "Page found as color; stopping interpretation");
						break;
					}
				}
				s += ss;
			}
		}
		fz_always(ctx)
			fz_fin_cached_color_converter(ctx, &cc);
		fz_catch(ctx)
			fz_rethrow(ctx);
	}
}


static void
fz_test_fill_image(fz_context *ctx, fz_device *dev_, fz_image *image, fz_matrix ctm, float alpha, fz_color_params color_params)
{
	fz_test_device *dev = (fz_test_device*)dev_;

	while (dev->resolved == 0) /* So we can break out */
	{
		fz_compressed_buffer *buffer;

		if (*dev->is_color || !image->colorspace || fz_colorspace_is_gray(ctx, image->colorspace))
			break;

		if ((dev->options & FZ_TEST_OPT_IMAGES) == 0)
		{
			/* Don't test every pixel. Upgrade us from "black and white" to "probably color" */
			if (*dev->is_color == 0)
				*dev->is_color = 1;
			dev->resolved = 1;
			if (dev->super.passthrough == NULL)
				fz_throw(ctx, FZ_ERROR_ABORT, "Page found as color; stopping interpretation");
			break;
		}

		buffer = fz_compressed_image_buffer(ctx, image);
		if (buffer && image->bpc == 8)
		{
			fz_stream *stream = fz_open_compressed_buffer(ctx, buffer);
			fz_try(ctx)
				fz_test_fill_compressed_8bpc_image(ctx, dev, image, stream, color_params);
			fz_always(ctx)
				fz_drop_stream(ctx, stream);
			fz_catch(ctx)
				fz_rethrow(ctx);
		}
		else
		{
			fz_pixmap *pix = fz_get_pixmap_from_image(ctx, image, NULL, NULL, 0, 0);
			if (pix == NULL) /* Should never happen really, but... */
				break;

			fz_try(ctx)
				fz_test_fill_other_image(ctx, dev, pix, color_params);
			fz_always(ctx)
				fz_drop_pixmap(ctx, pix);
			fz_catch(ctx)
				fz_rethrow(ctx);
		}
		break;
	}
	if (dev->super.passthrough)
		fz_fill_image(ctx, dev->super.passthrough, image, ctm, alpha, color_params);
}

static void
fz_test_fill_image_mask(fz_context *ctx, fz_device *dev_, fz_image *image, fz_matrix ctm,
	fz_colorspace *colorspace, const float *color, float alpha, fz_color_params color_params)
{
	fz_test_device *dev = (fz_test_device*)dev_;

	if (dev->resolved == 0)
	{
		/* We assume that at least some of the image pixels are non-zero */
		fz_test_color(ctx, dev, colorspace, color, color_params);
	}
	if (dev->super.passthrough)
		fz_fill_image_mask(ctx, dev->super.passthrough, image, ctm, colorspace, color, alpha, color_params);
}

fz_device *
fz_new_test_device(fz_context *ctx, int *is_color, float threshold, int options, fz_device *passthrough)
{
	fz_test_device *dev = fz_new_derived_passthrough_device(ctx, passthrough, fz_test_device);

	dev->super.fill_path = fz_test_fill_path;
	dev->super.stroke_path = fz_test_stroke_path;
	dev->super.fill_text = fz_test_fill_text;
	dev->super.stroke_text = fz_test_stroke_text;
	dev->super.fill_shade = fz_test_fill_shade;
	dev->super.fill_image = fz_test_fill_image;
	dev->super.fill_image_mask = fz_test_fill_image_mask;

	dev->is_color = is_color;
	dev->options = options;
	dev->threshold = threshold;
	dev->resolved = 0;

	*dev->is_color = 0;

	return (fz_device*)dev;
}
