//
//  Little cms
//  Copyright (C) 1998-2010 Marti Maria, Ignacio Ruiz de Conejo
//
// Permission is hereby granted, free of charge, to any person obtaining 
// a copy of this software and associated documentation files (the "Software"), 
// to deal in the Software without restriction, including without limitation 
// the rights to use, copy, modify, merge, publish, distribute, sublicense, 
// and/or sell copies of the Software, and to permit persons to whom the Software 
// is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in 
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 
// THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


#include "mex.h"

#include "lcms2.h"
#include "string.h"
#include "stdarg.h"

// xgetopt() interface -----------------------------------------------------

static int   xoptind;    
static char *xoptarg; 
static int   xopterr;  
static char  *letP;
static char   SW = '-';

// ------------------------------------------------------------------------


static int  Verbose ;			// Print some statistics
static char *cInProf;			// Input profile
static char *cOutProf;			// Output profile
static char *cProofing;			// Softproofing profile


static int  Intent;				// Rendering Intent
static int  ProofingIntent;		// RI for proof

static int  PrecalcMode;		// 0 = Not, 1=Normal, 2=Accurate, 3=Fast

static cmsBool BlackPointCompensation;
static cmsBool lIsDeviceLink;
static cmsBool lMultiProfileChain;		// Multiple profile chain

static cmsHPROFILE hInput, hOutput, hProof;
static cmsHTRANSFORM hColorTransform;
static cmsHPROFILE hProfiles[255];
static int nProfiles;

static cmsColorSpaceSignature InputColorSpace, OutputColorSpace;
static int OutputChannels, InputChannels, nBytesDepth;


// Error. Print error message and abort

static
cmsBool FatalError(const char *frm, ...)
{
	va_list args;
	char Buffer[1024];

	va_start(args, frm);
	vsprintf(Buffer, frm, args);
	mexErrMsgTxt(Buffer);   
	va_end(args);

	return FALSE;               
}

// This is the handler passed to lcms

static
void MatLabErrorHandler(cmsContext ContextID, cmsUInt32Number ErrorCode, 
						const char *Text)
{      
	mexErrMsgTxt(Text);    
}
//
//  Parse the command line options, System V style.
//

static
void xoptinit()
{   
	xoptind = 1;
	xopterr = 0;
	letP = NULL;
}


static
int xgetopt(int argc, char *argv[], char *optionS)
{
	unsigned char ch;
	char *optP;

	if (SW == 0) {
		SW = '/';
	}

	if (argc > xoptind) {
		if (letP == NULL) {
			if ((letP = argv[xoptind]) == NULL ||
				*(letP++) != SW)  goto gopEOF;
			if (*letP == SW) {
				xoptind++;  goto gopEOF;
			}
		}
		if (0 == (ch = *(letP++))) {
			xoptind++;  goto gopEOF;
		}
		if (':' == ch  ||  (optP = strchr(optionS, ch)) == NULL)
			goto gopError;
		if (':' == *(++optP)) {
			xoptind++;
			if (0 == *letP) {
				if (argc <= xoptind)  goto  gopError;
				letP = argv[xoptind++];
			}
			xoptarg = letP;
			letP = NULL;
		} else {
			if (0 == *letP) {
				xoptind++;
				letP = NULL;
			}
			xoptarg = NULL;
		}
		return ch;
	}
gopEOF:
	xoptarg = letP = NULL;
	return EOF;

gopError:
	xoptarg = NULL;    
	if (xopterr)
		FatalError ("get command line option");
	return ('?');
}


// Return Mathlab type by depth

static
size_t SizeOfArrayType(const mxArray *Array)
{

	switch (mxGetClassID(Array))  {

	 case mxINT8_CLASS:   return 1;
	 case mxUINT8_CLASS:  return 1;
	 case mxINT16_CLASS:  return 2;
	 case mxUINT16_CLASS: return 2;  
	 case mxSINGLE_CLASS: return 4;
	 case mxDOUBLE_CLASS: return 0; // Special case -- lcms handles double as size=0


	 default:
		 FatalError("Unsupported data type");
		 return 0;
	}
}


// Get number of pixels of input array. Supported arrays are 
// organized as NxMxD, being N and M the size of image and D the
// number of components.

static
size_t GetNumberOfPixels(const mxArray* In)
{
	int nDimensions  = mxGetNumberOfDimensions(In); 
	const int  *Dimensions   = mxGetDimensions(In);

	switch (nDimensions) {

		case 1: return 1;                            // It is just a spot color
		case 2: return Dimensions[0];                // A scanline
		case 3: return Dimensions[0]*Dimensions[1];  // A image

		default:
			FatalError("Unsupported array of %d dimensions", nDimensions);
			return 0;
	}
}   


// Allocates the output array. Copies the input array modifying the pixel
// definition to match "OutputChannels".

static
mxArray* AllocateOutputArray(const mxArray* In, int OutputChannels)
{       

	mxArray*	Out			  = mxDuplicateArray(In);   // Make a "deep copy" of Input array 
	int         nDimensions   = mxGetNumberOfDimensions(In);    
	const int*	Dimensions    = mxGetDimensions(In);
	int         InputChannels = Dimensions[nDimensions-1];


	// Modify pixel size only if needed

	if (InputChannels != OutputChannels) {


		int i, NewSize;
		int *ModifiedDimensions = (int*) mxMalloc(nDimensions * sizeof(int));


		memmove(ModifiedDimensions, Dimensions, nDimensions * sizeof(int));
		ModifiedDimensions[nDimensions - 1] = OutputChannels;

		switch (mxGetClassID(In))  {

		case mxINT8_CLASS:   NewSize = sizeof(char); break;
		case mxUINT8_CLASS:  NewSize = sizeof(unsigned char); break;
		case mxINT16_CLASS:  NewSize = sizeof(short); break;
		case mxUINT16_CLASS: NewSize = sizeof(unsigned short); break;

		default:
		case mxDOUBLE_CLASS: NewSize = sizeof(double); break;
		}


		// NewSize = 1;
		for (i=0; i < nDimensions; i++)
			NewSize *= ModifiedDimensions[i];


		mxSetDimensions(Out, ModifiedDimensions, nDimensions);
		mxFree(ModifiedDimensions);

		mxSetPr(Out, mxRealloc(mxGetPr(Out), NewSize));             

	}


	return Out;
}



// Does create a format descriptor. "Bytes" is the sizeof type in bytes
//  
//  Bytes  Meaning
//  ------ --------
//   0      Floating point (double)
//   1      8-bit samples
//   2      16-bit samples   

static
cmsUInt32Number MakeFormatDescriptor(cmsColorSpaceSignature ColorSpace, int Bytes)
{
	int IsFloat = (Bytes == 0 || Bytes == 4) ? 1 : 0;
	int Channels = cmsChannelsOf(ColorSpace);
	return FLOAT_SH(IsFloat)|COLORSPACE_SH(_cmsLCMScolorSpace(ColorSpace))|BYTES_SH(Bytes)|CHANNELS_SH(Channels)|PLANAR_SH(1);
}


// Opens a profile or proper built-in

static
cmsHPROFILE OpenProfile(const char* File)
{   

	cmsContext ContextID = 0;

	   if (!File) 
            return cmsCreate_sRGBProfileTHR(ContextID);    

       if (cmsstrcasecmp(File, "*Lab2") == 0)
                return cmsCreateLab2ProfileTHR(ContextID, NULL);

       if (cmsstrcasecmp(File, "*Lab4") == 0)
                return cmsCreateLab4ProfileTHR(ContextID, NULL);

       if (cmsstrcasecmp(File, "*Lab") == 0)
                return cmsCreateLab4ProfileTHR(ContextID, NULL);
       
       if (cmsstrcasecmp(File, "*LabD65") == 0) {

           cmsCIExyY D65xyY;
           
           cmsWhitePointFromTemp( &D65xyY, 6504);           
           return cmsCreateLab4ProfileTHR(ContextID, &D65xyY);
       }

       if (cmsstrcasecmp(File, "*XYZ") == 0)
                return cmsCreateXYZProfileTHR(ContextID);

       if (cmsstrcasecmp(File, "*Gray22") == 0) {

           cmsToneCurve* Curve = cmsBuildGamma(ContextID, 2.2);
           cmsHPROFILE hProfile = cmsCreateGrayProfileTHR(ContextID, cmsD50_xyY(), Curve);
           cmsFreeToneCurve(Curve);
           return hProfile;
       }

        if (cmsstrcasecmp(File, "*Gray30") == 0) {

           cmsToneCurve* Curve = cmsBuildGamma(ContextID, 3.0);
           cmsHPROFILE hProfile = cmsCreateGrayProfileTHR(ContextID, cmsD50_xyY(), Curve);
           cmsFreeToneCurve(Curve);
           return hProfile;
       }

       if (cmsstrcasecmp(File, "*srgb") == 0)
                return cmsCreate_sRGBProfileTHR(ContextID);

       if (cmsstrcasecmp(File, "*null") == 0)
                return cmsCreateNULLProfileTHR(ContextID);

       
       if (cmsstrcasecmp(File, "*Lin2222") == 0) {

            cmsToneCurve*  Gamma = cmsBuildGamma(0, 2.2);
            cmsToneCurve*  Gamma4[4];
            cmsHPROFILE hProfile; 

            Gamma4[0] = Gamma4[1] = Gamma4[2] = Gamma4[3] = Gamma;
            hProfile = cmsCreateLinearizationDeviceLink(cmsSigCmykData, Gamma4);
            cmsFreeToneCurve(Gamma);
            return hProfile;
       }

           
        return cmsOpenProfileFromFileTHR(ContextID, File, "r");
}


static
cmsUInt32Number GetFlags()
{
	cmsUInt32Number dwFlags = 0; 

	switch (PrecalcMode) {

	case 0: dwFlags = cmsFLAGS_NOOPTIMIZE; break;
	case 2: dwFlags = cmsFLAGS_HIGHRESPRECALC; break;
	case 3: dwFlags = cmsFLAGS_LOWRESPRECALC; break;
	case 1: break;

	default: FatalError("Unknown precalculation mode '%d'", PrecalcMode);
	}

	if (BlackPointCompensation) 
		dwFlags |= cmsFLAGS_BLACKPOINTCOMPENSATION;

	return dwFlags;
}

// Create transforms

static
void OpenTransforms(int argc, char *argv[])
{

	cmsUInt32Number dwIn, dwOut, dwFlags;


	if (lMultiProfileChain) {

		int i;
		cmsHTRANSFORM hTmp;


		nProfiles = argc - xoptind;
		for (i=0; i < nProfiles; i++) {

			hProfiles[i] = OpenProfile(argv[i+xoptind]);
		}


		// Create a temporary devicelink 

		hTmp = cmsCreateMultiprofileTransform(hProfiles, nProfiles, 
			0, 0, Intent, GetFlags());

		hInput = cmsTransform2DeviceLink(hTmp, 4.2, 0);
		hOutput = NULL;
		cmsDeleteTransform(hTmp);

		InputColorSpace  = cmsGetColorSpace(hInput);
		OutputColorSpace = cmsGetPCS(hInput);        
		lIsDeviceLink = TRUE;

	}
	else
		if (lIsDeviceLink) {

			hInput  = cmsOpenProfileFromFile(cInProf, "r");
			hOutput = NULL;
			InputColorSpace  = cmsGetColorSpace(hInput);
			OutputColorSpace = cmsGetPCS(hInput);


		}
		else {

			hInput  = OpenProfile(cInProf);
			hOutput = OpenProfile(cOutProf);    

			InputColorSpace   = cmsGetColorSpace(hInput);
			OutputColorSpace  = cmsGetColorSpace(hOutput);

			if (cmsGetDeviceClass(hInput) == cmsSigLinkClass ||
				cmsGetDeviceClass(hOutput) == cmsSigLinkClass)   
				FatalError("Use %cl flag for devicelink profiles!\n", SW);

		}


		/*

		if (Verbose) {

		mexPrintf("From: %s\n", cmsTakeProductName(hInput));
		if (hOutput) mexPrintf("To  : %s\n\n", cmsTakeProductName(hOutput));

		}
		*/


		OutputChannels = cmsChannelsOf(OutputColorSpace);
		InputChannels  = cmsChannelsOf(InputColorSpace);


		dwIn  = MakeFormatDescriptor(InputColorSpace, nBytesDepth);
		dwOut = MakeFormatDescriptor(OutputColorSpace, nBytesDepth);


		dwFlags = GetFlags();

		if (cProofing != NULL) {

			hProof = OpenProfile(cProofing);
			dwFlags |= cmsFLAGS_SOFTPROOFING;
		}




		hColorTransform = cmsCreateProofingTransform(hInput, dwIn, 
			hOutput, dwOut, 
			hProof, Intent, 
			ProofingIntent, 
			dwFlags);

}



static
void ApplyTransforms(const mxArray *In, mxArray *Out)
{   
	double *Input  = mxGetPr(In); 
	double *Output = mxGetPr(Out);    
	size_t nPixels = GetNumberOfPixels(In);;

	cmsDoTransform(hColorTransform, Input, Output, nPixels );

}


static
void CloseTransforms(void)
{
	int i;

	if (hColorTransform) cmsDeleteTransform(hColorTransform);
	if (hInput) cmsCloseProfile(hInput);
	if (hOutput) cmsCloseProfile(hOutput);             
	if (hProof) cmsCloseProfile(hProof);

	for (i=0; i < nProfiles; i++)
		cmsCloseProfile(hProfiles[i]);

	hColorTransform = NULL; hInput = NULL; hOutput = NULL; hProof = NULL;
}


static
void HandleSwitches(int argc, char *argv[])
{
	int  s;

	xoptinit();

	while ((s = xgetopt(argc, argv,"C:c:VvbBI:i:O:o:T:t:L:l:r:r:P:p:Mm")) != EOF) {


		switch (s){

		case 'b':
		case 'B': 
			BlackPointCompensation = TRUE;
			break;

		case 'c':
		case 'C':
			PrecalcMode = atoi(xoptarg);
			if (PrecalcMode < 0 || PrecalcMode > 3)
				FatalError("Unknown precalc mode '%d'", PrecalcMode);
			break;

		case 'v':
		case 'V':
			Verbose = TRUE;
			break;

		case 'i':
		case 'I':
			if (lIsDeviceLink)
				FatalError("Device-link already specified");
			cInProf = xoptarg;
			break;

		case 'o':
		case 'O':
			if (lIsDeviceLink)
				FatalError("Device-link already specified"); 
			cOutProf = xoptarg;
			break;

		case 't':
		case 'T':
			Intent = atoi(xoptarg);
			// if (Intent > 3) Intent = 3;
			if (Intent < 0) Intent = 0;
			break;


		case 'l':
		case 'L': 
			cInProf = xoptarg;
			lIsDeviceLink = TRUE;
			break;

		case 'p':
		case 'P':
			cProofing = xoptarg;
			break;



		case 'r':
		case 'R':
			ProofingIntent = atoi(xoptarg);
			// if (ProofingIntent > 3) ProofingIntent = 3;
			if (ProofingIntent < 0) ProofingIntent = 0;
			break;


		case 'm':
		case 'M':
			lMultiProfileChain = TRUE;
			break;

		default:
			FatalError("Unknown option.");
		}
	}

	// For multiprofile, need to specify -m

	if (xoptind < argc) {

		if (!lMultiProfileChain)
			FatalError("Use %cm for multiprofile transforms", SW);
	}

}



// -------------------------------------------------- Print some fancy help
static
void PrintHelp(void)
{
	mexPrintf("(MX) little cms ColorSpace conversion tool - v2.0\n\n");

	mexPrintf("usage: icctrans (mVar, flags)\n\n");

	mexPrintf("mVar : Matlab array.\n");
	mexPrintf("flags: a string containing one or more of following options.\n\n");
	mexPrintf("\t%cv - Verbose\n", SW);
	mexPrintf("\t%ci<profile> - Input profile (defaults to sRGB)\n", SW);
	mexPrintf("\t%co<profile> - Output profile (defaults to sRGB)\n", SW);   
	mexPrintf("\t%cl<profile> - Transform by device-link profile\n", SW);      
	mexPrintf("\t%cm<profiles> - Apply multiprofile chain\n", SW);      

	mexPrintf("\t%ct<n> - Rendering intent\n", SW);    

	mexPrintf("\t%cb - Black point compensation\n", SW);
	mexPrintf("\t%cc<0,1,2,3> - Optimize transform (0=Off, 1=Normal, 2=Hi-res, 3=Lo-Res) [defaults to 1]\n", SW);     

	mexPrintf("\t%cp<profile> - Soft proof profile\n", SW);
	mexPrintf("\t%cr<0,1,2,3> - Soft proof intent\n", SW);

	mexPrintf("\nYou can use following built-ins as profiles:\n\n");

	mexPrintf("\t*Lab2  -- D50-based v2 CIEL*a*b\n"
	"\t*Lab4  -- D50-based v4 CIEL*a*b\n"
	"\t*Lab   -- D50-based v4 CIEL*a*b\n"
	"\t*XYZ   -- CIE XYZ (PCS)\n"
	"\t*sRGB  -- IEC6 1996-2.1 sRGB color space\n" 
	"\t*Gray22 - Monochrome of Gamma 2.2\n"
	"\t*Gray30 - Monochrome of Gamma 3.0\n"
	"\t*null   - Monochrome black for all input\n"
	"\t*Lin2222- CMYK linearization of gamma 2.2 on each channel\n\n");

	mexPrintf("For suggestions, comments, bug reports etc. send mail to info@littlecms.com\n\n");

}



// Main entry point

void mexFunction(
				 int nlhs,              // Number of left hand side (output) arguments
				 mxArray *plhs[],       // Array of left hand side arguments
				 int nrhs,              // Number of right hand side (input) arguments
				 const mxArray *prhs[]  // Array of right hand side arguments
)
{

	char CommandLine[4096+1];
	char *pt, *argv[128];
	int argc = 1;


	if (nrhs != 2) {    

		PrintHelp();              
		return;
	}


	if(nlhs > 1) {        
		FatalError("Too many output arguments.");
	}


	// Setup error handler

	cmsSetLogErrorHandler(MatLabErrorHandler);

	// Defaults

	Verbose     = 0;
	cInProf     = NULL;
	cOutProf    = NULL;
	cProofing   = NULL;

	lMultiProfileChain = FALSE;
	nProfiles   = 0;

	Intent                  = INTENT_PERCEPTUAL;
	ProofingIntent          = INTENT_ABSOLUTE_COLORIMETRIC;
	PrecalcMode = 1;
	BlackPointCompensation  = FALSE;
	lIsDeviceLink           = FALSE;

	// Check types. Fist parameter is array of values, second parameter is command line

	if (!mxIsNumeric(prhs[0]))
		FatalError("Type mismatch on argument 1 -- Must be numeric");

	if (!mxIsChar(prhs[1]))
		FatalError("Type mismatch on argument 2 -- Must be string");




	// Unpack string to command line buffer

	if (mxGetString(prhs[1], CommandLine, 4096))
		FatalError("Cannot unpack command string");

	// Separate to argv[] convention

	argv[0] = NULL;
	for (pt = strtok(CommandLine, " ");
		pt;
		pt = strtok(NULL, " ")) {

			argv[argc++] = pt;
	}



	// Parse arguments
	HandleSwitches(argc, argv);


	nBytesDepth = SizeOfArrayType(prhs[0]);

	OpenTransforms(argc, argv);


	plhs[0] = AllocateOutputArray(prhs[0], OutputChannels);


	ApplyTransforms(prhs[0], plhs[0]);

	CloseTransforms();

	// Done!
}


