#include "mupdf/fitz.h"
#include "lcms2.h"
#include "lcms2_plugin.h"
#include "colorspace-imp.h"

#define LCMS_BYTES_MASK 0x7
/* #define DEBUG_LCMS_MEM(A) do { printf A; fflush(stdout); } while (0) */
#define DEBUG_LCMS_MEM(A) do { } while (0)

static void
fz_lcms_log_error(cmsContext id, cmsUInt32Number error_code, const char *error_text)
{
	fz_context *ctx = (fz_context *)cmsGetContextUserData(id);
	fz_warn(ctx, "lcms error: %s", error_text);
}

static void
*fz_lcms_malloc(cmsContext id, unsigned int size)
{
	void *result;
	fz_context *ctx = (fz_context *)cmsGetContextUserData(id);
	result = fz_malloc_no_throw(ctx, size);
	DEBUG_LCMS_MEM(("Allocation::  mupdf ctx = %p lcms ctx = %p allocation = %p \n", (void*) ctx, (void*) id, (void*) result));
	return result;
}

static void
fz_lcms_free(cmsContext id, void *ptr)
{
	fz_context *ctx = (fz_context *)cmsGetContextUserData(id);
	DEBUG_LCMS_MEM(("Free:: mupdf ctx = %p lcms ctx = %p allocation = %p \n", (void*) ctx, (void*) id, (void*) ptr));
	fz_free(ctx, ptr);
}

static void*
fz_lcms_realloc(cmsContext id, void *ptr, unsigned int size)
{
	fz_context *ctx = (fz_context *)cmsGetContextUserData(id);
	DEBUG_LCMS_MEM(("Realloc:: mupdf ctx = %p lcms ctx = %p allocation = %p \n", (void*) ctx, (void*) id, (void*) ptr));
	if (ptr == 0)
		return fz_lcms_malloc(id, size);
	if (size == 0)
	{
		fz_lcms_free(id, ptr);
		return NULL;
	}
	return fz_resize_array_no_throw(ctx, ptr, size, 1);
}

static cmsPluginMemHandler fz_lcms_memhandler =
{
	{
		cmsPluginMagicNumber,
		2000,
		cmsPluginMemHandlerSig,
		NULL
	},
	fz_lcms_malloc,
	fz_lcms_free,
	fz_lcms_realloc,
	NULL,
	NULL,
	NULL,
};

static int
fz_lcms_num_devcomps(cmsContext cmm_ctx, fz_iccprofile *profile)
{
	return cmsChannelsOf(cmm_ctx, cmsGetColorSpace(cmm_ctx, profile->cmm_handle));
}

static void
fz_lcms_premultiply_row(fz_context *ctx, int n, int w, unsigned char *s)
{
	unsigned char a;
	int k;
	int n1 = n-1;

	for (; w > 0; w--)
	{
		a = s[n1];
		for (k = 0; k < n1; k++)
			s[k] = fz_mul255(s[k], a);
		s += n;
	}
}

static void
fz_lcms_unmultiply_row(fz_context *ctx, int n, int w, unsigned char *s, const unsigned char *in)
{
	int a, inva;
	int k;
	int n1 = n-1;

	for (; w > 0; w--)
	{
		a = in[n1];
		inva = a ? 255 * 256 / a : 0;
		for (k = 0; k < n1; k++)
			s[k] = (in[k] * inva) >> 8;
		s += n;
		in += n;
	}
}

/* Transform pixmap */
void
fz_lcms_transform_pixmap(fz_cmm_instance *instance, fz_icclink *link, fz_pixmap *dst, fz_pixmap *src)
{
	cmsContext cmm_ctx = (cmsContext)instance;
	fz_context *ctx = (fz_context *)cmsGetContextUserData(cmm_ctx);
	cmsHTRANSFORM hTransform = (cmsHTRANSFORM)link->cmm_handle;
	int cmm_num_src, cmm_num_des;
	unsigned char *inputpos, *outputpos, *buffer;
	int ss = src->stride;
	int ds = dst->stride;
	int sw = src->w;
	int dw = dst->w;
	int sn = src->n;
	int dn = dst->n;
	int sa = src->alpha;
	int da = dst->alpha;
	int h = src->h;
	DEBUG_LCMS_MEM(("@@@@@@@ Transform Pixmap Start:: mupdf ctx = %p lcms ctx = %p link = %p \n", (void*)ctx, (void*)cmm_ctx, (void*)link->cmm_handle));

	/* check the channels. */
	cmm_num_src = T_CHANNELS(cmsGetTransformInputFormat(cmm_ctx, hTransform));
	cmm_num_des = T_CHANNELS(cmsGetTransformOutputFormat(cmm_ctx, hTransform));
	if (cmm_num_src != sn - sa || cmm_num_des != dn - da || sa != da)
		fz_throw(ctx, FZ_ERROR_GENERIC, "Mismatching color setup in cmm pixmap transformation: src: %d vs %d+%d, dst: %d vs %d+%d", cmm_num_src, sn-sa, sa, cmm_num_des, dn-da, da);

	/* Transform */
	inputpos = src->samples;
	outputpos = dst->samples;
	if (src->alpha)
	{
		/* Allow for premultiplied alpha */
		buffer = fz_malloc(ctx, ss);
		for (; h > 0; h--)
		{
			fz_lcms_unmultiply_row(ctx, sn, sw, buffer, inputpos);
			cmsDoTransform(cmm_ctx, hTransform, inputpos, outputpos, sw);
			fz_lcms_premultiply_row(ctx, dn, dw, outputpos);
			inputpos += ss;
			outputpos += ds;
		}
		fz_free(ctx, buffer);
	}
	else
	{
		for (; h > 0; h--)
		{
			cmsDoTransform(cmm_ctx, hTransform, inputpos, outputpos, sw);
			inputpos += ss;
			outputpos += ds;
		}
	}
	DEBUG_LCMS_MEM(("@@@@@@@ Transform Pixmap End:: mupdf ctx = %p lcms ctx = %p link = %p \n", (void*)ctx, (void*)cmm_ctx, (void*)link->cmm_handle));
}

/* Transform a single color. */
void
fz_lcms_transform_color(fz_cmm_instance *instance, fz_icclink *link, unsigned short *dst, const unsigned short *src)
{
	cmsContext cmm_ctx = (cmsContext)instance;
	cmsHTRANSFORM hTransform = (cmsHTRANSFORM) link->cmm_handle;

	cmsDoTransform(cmm_ctx, hTransform, src, dst, 1);
}

void
fz_lcms_init_link(fz_cmm_instance *instance, fz_icclink *link, const fz_color_params *rend, int cmm_flags, int num_bytes, int alpha, const fz_iccprofile *src, const fz_iccprofile *prf, const fz_iccprofile *dst)
{
	cmsContext cmm_ctx = (cmsContext)instance;
	fz_context *ctx = (fz_context *)cmsGetContextUserData(cmm_ctx);

	cmsUInt32Number src_data_type, des_data_type;
	cmsColorSpaceSignature src_cs, des_cs;
	int src_num_chan, des_num_chan;
	int lcms_src_cs, lcms_des_cs;
	unsigned int flag = cmsFLAGS_LOWRESPRECALC | cmm_flags;

	DEBUG_LCMS_MEM(("@@@@@@@ Create Link Start:: mupdf ctx = %p lcms ctx = %p src = %p des = %p \n", (void*)ctx, (void*)cmm_ctx, (void*)src->cmm_handle, (void*)dst->cmm_handle));

	/* src */
	src_cs = cmsGetColorSpace(cmm_ctx, src->cmm_handle);
	lcms_src_cs = _cmsLCMScolorSpace(cmm_ctx, src_cs);
	if (lcms_src_cs < 0)
		lcms_src_cs = 0;
	src_num_chan = cmsChannelsOf(cmm_ctx, src_cs);
	src_data_type = (COLORSPACE_SH(lcms_src_cs) | CHANNELS_SH(src_num_chan) | DOSWAP_SH(src->bgr) | BYTES_SH(num_bytes) | EXTRA_SH(alpha));

	/* dst */
	des_cs = cmsGetColorSpace(cmm_ctx, dst->cmm_handle);
	lcms_des_cs = _cmsLCMScolorSpace(cmm_ctx, des_cs);
	if (lcms_des_cs < 0)
		lcms_des_cs = 0;
	des_num_chan = cmsChannelsOf(cmm_ctx, des_cs);
	des_data_type = (COLORSPACE_SH(lcms_des_cs) | CHANNELS_SH(des_num_chan) | DOSWAP_SH(dst->bgr) | BYTES_SH(num_bytes) | EXTRA_SH(alpha));

	/* flags */
	if (rend->bp)
		flag |= cmsFLAGS_BLACKPOINTCOMPENSATION;

	if (alpha)
		flag |= cmsFLAGS_COPY_ALPHA;

	link->depth = num_bytes;
	link->alpha = alpha;

	if (prf == NULL)
	{
		link->cmm_handle = cmsCreateTransformTHR(cmm_ctx, src->cmm_handle, src_data_type, dst->cmm_handle, des_data_type, rend->ri, flag);
		if (!link->cmm_handle)
			fz_throw(ctx, FZ_ERROR_GENERIC, "cmsCreateTransform failed");
	}
	else
	{
		/* littleCMS proof creation links don't work properly with the Ghent
		 * test files. Handle this in a brutish manner.
		 */
		if (src == prf)
		{
			link->cmm_handle = cmsCreateTransformTHR(cmm_ctx, src->cmm_handle, src_data_type, dst->cmm_handle, des_data_type, INTENT_RELATIVE_COLORIMETRIC, flag);
			if (!link->cmm_handle)
				fz_throw(ctx, FZ_ERROR_GENERIC, "cmsCreateTransform failed");
		}
		else if (prf == dst)
		{
			link->cmm_handle = cmsCreateTransformTHR(cmm_ctx, src->cmm_handle, src_data_type, prf->cmm_handle, des_data_type, rend->ri, flag);
			if (!link->cmm_handle)
				fz_throw(ctx, FZ_ERROR_GENERIC, "cmsCreateTransform failed");
		}
		else
		{
			cmsHPROFILE src_to_prf_profile;
			cmsHTRANSFORM src_to_prf_link;
			cmsColorSpaceSignature prf_cs;
			int prf_num_chan;
			int lcms_prf_cs;
			cmsUInt32Number prf_data_type;
			cmsHPROFILE hProfiles[3];

			prf_cs = cmsGetColorSpace(cmm_ctx, prf->cmm_handle);
			lcms_prf_cs = _cmsLCMScolorSpace(cmm_ctx, prf_cs);
			if (lcms_prf_cs < 0)
				lcms_prf_cs = 0;
			prf_num_chan = cmsChannelsOf(cmm_ctx, prf_cs);
			prf_data_type = (COLORSPACE_SH(lcms_prf_cs) | CHANNELS_SH(prf_num_chan) | BYTES_SH(num_bytes));
			src_to_prf_link = cmsCreateTransformTHR(cmm_ctx, src->cmm_handle, src_data_type, prf->cmm_handle, prf_data_type, rend->ri, flag);
			if (!src_to_prf_link)
				fz_throw(ctx, FZ_ERROR_GENERIC, "cmsCreateTransform failed");
			src_to_prf_profile = cmsTransform2DeviceLink(cmm_ctx, src_to_prf_link, 3.4, flag);
			cmsDeleteTransform(cmm_ctx, src_to_prf_link);
			if (!src_to_prf_profile)
				fz_throw(ctx, FZ_ERROR_GENERIC, "cmsTransform2DeviceLink failed");

			hProfiles[0] = src_to_prf_profile;
			hProfiles[1] = prf->cmm_handle;
			hProfiles[2] = dst->cmm_handle;
			link->cmm_handle = cmsCreateMultiprofileTransformTHR(cmm_ctx, hProfiles, 3, src_data_type, des_data_type, INTENT_RELATIVE_COLORIMETRIC, flag);
			cmsCloseProfile(cmm_ctx, src_to_prf_profile);
			if (!link->cmm_handle)
				fz_throw(ctx, FZ_ERROR_GENERIC, "cmsCreateMultiprofileTransform failed");
		}
	}

	DEBUG_LCMS_MEM(("@@@@@@@ Create Link End:: mupdf ctx = %p lcms ctx = %p link = %p link_cmm = %p src = %p des = %p \n", (void*)ctx, (void*)cmm_ctx, (void*)link, (void*)link->cmm_handle, (void*)src->cmm_handle, (void*)dst->cmm_handle));
}

void
fz_lcms_fin_link(fz_cmm_instance *instance, fz_icclink *link)
{
	cmsContext cmm_ctx = (cmsContext)instance;
	DEBUG_LCMS_MEM(("Free Link:: link = %p \n", (void*)link->cmm_handle));
	if (link->cmm_handle != NULL)
		cmsDeleteTransform(cmm_ctx, link->cmm_handle);
	link->cmm_handle = NULL;
}

static fz_cmm_instance *
fz_lcms_new_instance(fz_context *ctx)
{
	cmsContext cmm_ctx;

	cmm_ctx = cmsCreateContext(&fz_lcms_memhandler, ctx);
	DEBUG_LCMS_MEM(("Context Creation:: mupdf ctx = %p lcms ctx = %p \n", (void*) ctx, (void*) cmm_ctx));
	if (cmm_ctx == NULL)
		fz_throw(ctx, FZ_ERROR_GENERIC, "cmsCreateContext failed");
	cmsSetLogErrorHandlerTHR(cmm_ctx, fz_lcms_log_error);
	return (fz_cmm_instance *)cmm_ctx;
}

static void
fz_lcms_drop_instance(fz_cmm_instance *instance)
{
	DEBUG_LCMS_MEM(("Context Destruction:: lcms ctx = %p \n", (void*)instance));
	if (instance == NULL)
		return;
	cmsDeleteContext((cmsContext)instance);
}

static void
fz_lcms_init_profile(fz_cmm_instance *instance, fz_iccprofile *profile)
{
	cmsContext cmm_ctx = (cmsContext)instance;
	fz_context *ctx = (fz_context *)cmsGetContextUserData(cmm_ctx);
	size_t size;
	unsigned char *data;

	DEBUG_LCMS_MEM(("@@@@@@@ Create Profile Start:: mupdf ctx = %p lcms ctx = %p \n", (void*)ctx, (void*)cmm_ctx));

	size = fz_buffer_storage(ctx, profile->buffer, &data);
	profile->cmm_handle = cmsOpenProfileFromMemTHR(cmm_ctx, data, size);
	if (profile->cmm_handle == NULL)
	{
		profile->num_devcomp = 0;
		fz_throw(ctx, FZ_ERROR_GENERIC, "cmsOpenProfileFromMem failed");
	}
	profile->num_devcomp = fz_lcms_num_devcomps(cmm_ctx, profile);

	DEBUG_LCMS_MEM(("@@@@@@@ Create Profile End:: mupdf ctx = %p lcms ctx = %p profile = %p profile_cmm = %p \n", (void*)ctx, (void*)cmm_ctx, (void*)profile, (void*)profile->cmm_handle));
}

static void
fz_lcms_fin_profile(fz_cmm_instance *instance, fz_iccprofile *profile)
{
	cmsContext cmm_ctx = (cmsContext)instance;
	DEBUG_LCMS_MEM(("Free Profile:: profile = %p \n", (void*) profile->cmm_handle));
	if (profile->cmm_handle != NULL)
		cmsCloseProfile(cmm_ctx, profile->cmm_handle);
	profile->cmm_handle = NULL;
}

fz_cmm_engine fz_cmm_engine_lcms = {
	fz_lcms_new_instance,
	fz_lcms_drop_instance,
	fz_lcms_transform_pixmap,
	fz_lcms_transform_color,
	fz_lcms_init_link,
	fz_lcms_fin_link,
	fz_lcms_init_profile,
	fz_lcms_fin_profile,
	cmsFLAGS_NOWHITEONWHITEFIXUP
};