/* This is xdvipsk, an eXtended version of dvips(k) by Tomas Rokicki.

	Copyright (C) 2016 by VTeX Ltd (www.vtex.lt),
	the xdvipsk project team - Sigitas Tolusis and Arunas Povilaitis.

    Program original code copyright by Floris van den Berg,
	Hervé Drolon and Karl-Heinz Bussian, the FreeImage 3 project team.

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program 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 General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
*/

// ==========================================================
// Bitmap conversion routines
//
// ==========================================================
#include <limits>

#include "bitmap.h"
#include "utilities.h"


// ==========================================================
// Utility functions declared in Utilities.h

BOOL SwapRedBlue32(FIBITMAP* dib) {
	if(Bitmap_GetImageType(dib) != FIT_BITMAP) {
		return FALSE;
	}
		
	const unsigned bytesperpixel = Bitmap_GetBPP(dib) / 8;
	if(bytesperpixel > 4 || bytesperpixel < 3) {
		return FALSE;
	}
		
	const unsigned height = Bitmap_GetHeight(dib);
	const unsigned pitch = Bitmap_GetPitch(dib);
	const unsigned lineSize = Bitmap_GetLine(dib);
	
	BYTE* line = Bitmap_GetBits(dib);
	for(unsigned y = 0; y < height; ++y, line += pitch) {
		for(BYTE* pixel = line; pixel < line + lineSize ; pixel += bytesperpixel) {
			BYTE tmp = pixel[0];
			pixel[0] = pixel[2];
			pixel[2] = tmp;
		}
	}
	
	return TRUE;
}

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

static inline void 
assignRGB(WORD r, WORD g, WORD b, WORD* out) {
	out[0] = r;
	out[1] = g;
	out[2] = b;
}

static inline void 
assignRGB(BYTE r, BYTE g, BYTE b, BYTE* out) {
	out[FI_RGBA_RED]	= r;
	out[FI_RGBA_GREEN]	= g;
	out[FI_RGBA_BLUE]	= b;
}

/// Clamp function
template <class T>
static inline T 
CLAMP(const T &value, const T &min_value, const T &max_value) {
	return ((value < min_value) ? min_value : (value > max_value) ? max_value : value);
}

/**
CMYK -> CMY -> RGB conversion from http://www.easyrgb.com/

CMYK to CMY [0-1]: C,M,Y * (1 - K) + K
CMY to RGB [0-1]: (1 - C,M,Y)

=> R,G,B = (1 - C,M,Y) * (1 - K)
mapped to [0-MAX_VAL]: 
(MAX_VAL - C,M,Y) * (MAX_VAL - K) / MAX_VAL
*/
template <class T>
static inline void 
CMYKToRGB(T C, T M, T Y, T K, T* out) {
	unsigned max_val = std::numeric_limits<T>::max();
	
	unsigned r = (max_val - C) * (max_val - K) / max_val;
	unsigned g = (max_val - M) * (max_val - K) / max_val;
	unsigned b = (max_val - Y) * (max_val - K) / max_val;

	// clamp values to [0..max_val]
	T red	= (T)CLAMP(r, (unsigned)0, max_val);
	T green	= (T)CLAMP(g, (unsigned)0, max_val);
	T blue	= (T)CLAMP(b, (unsigned)0, max_val);

	assignRGB(red, green, blue, out);
}

template <class T>
static void 
_convertCMYKtoRGBA(unsigned width, unsigned height, BYTE* line_start, unsigned pitch, unsigned samplesperpixel) {
	const BOOL hasBlack = (samplesperpixel > 3) ? TRUE : FALSE;
	const T MAX_VAL = std::numeric_limits<T>::max();
		
	T K = 0;
	for(unsigned y = 0; y < height; y++) {
		T *line = (T*)line_start;

		for(unsigned x = 0; x < width; x++) {
			if(hasBlack) {
				K = line[FI_RGBA_ALPHA];			
				line[FI_RGBA_ALPHA] = MAX_VAL; // TODO write the first extra channel as alpha!
			}			
			
			CMYKToRGB<T>(line[0], line[1], line[2], K, line);
			
			line += samplesperpixel;
		}
		line_start += pitch;
	}
}

BOOL 
ConvertCMYKtoRGBA(FIBITMAP* dib) {
	if (!Bitmap_HasPixels(dib)) {
		return FALSE;
	}
		
	const BITMAP_TYPE image_type = Bitmap_GetImageType(dib);
	const unsigned bytesperpixel = Bitmap_GetBPP(dib) / 8;
	
	unsigned channelSize = 1;
	if (!(image_type == FIT_BITMAP && (bytesperpixel > 2))) {
		return FALSE;
	}
				
	const unsigned width = Bitmap_GetWidth(dib);
	const unsigned height = Bitmap_GetHeight(dib);
	BYTE *line_start = Bitmap_GetScanLine(dib, 0);
	const unsigned pitch = Bitmap_GetPitch(dib);
	
	unsigned samplesperpixel = Bitmap_GetLine(dib) / width / channelSize;

	if(channelSize == sizeof(WORD)) {
		_convertCMYKtoRGBA<WORD>(width, height, line_start, pitch, samplesperpixel);
	} else {
		_convertCMYKtoRGBA<BYTE>(width, height, line_start, pitch, samplesperpixel);
	}

	return TRUE;	
}

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

/**
CIELab -> XYZ conversion from http://www.easyrgb.com/
*/
static void 
CIELabToXYZ(float L, float a, float b, float *X, float *Y, float *Z) {
	float pow_3;
	
	// CIELab -> XYZ conversion 
	// ------------------------
	float var_Y = (L + 16.F ) / 116.F;
	float var_X = a / 500.F + var_Y;
	float var_Z = var_Y - b / 200.F;

	pow_3 = powf(var_Y, 3);
	if(pow_3 > 0.008856F) {
		var_Y = pow_3;
	} else {
		var_Y = ( var_Y - 16.F / 116.F ) / 7.787F;
	}
	pow_3 = powf(var_X, 3);
	if(pow_3 > 0.008856F) {
		var_X = pow_3;
	} else {
		var_X = ( var_X - 16.F / 116.F ) / 7.787F;
	}
	pow_3 = powf(var_Z, 3);
	if(pow_3 > 0.008856F) {
		var_Z = pow_3;
	} else {
		var_Z = ( var_Z - 16.F / 116.F ) / 7.787F;
	}

	static const float ref_X =  95.047F;
	static const float ref_Y = 100.000F;
	static const float ref_Z = 108.883F;

	*X = ref_X * var_X;	// ref_X = 95.047 (Observer= 2°, Illuminant= D65)
	*Y = ref_Y * var_Y;	// ref_Y = 100.000
	*Z = ref_Z * var_Z;	// ref_Z = 108.883
}

/**
XYZ -> RGB conversion from http://www.easyrgb.com/
*/
static void 
XYZToRGB(float X, float Y, float Z, float *R, float *G, float *B) {
	float var_X = X / 100; // X from 0 to  95.047 (Observer = 2°, Illuminant = D65)
	float var_Y = Y / 100; // Y from 0 to 100.000
	float var_Z = Z / 100; // Z from 0 to 108.883

	float var_R = var_X *  3.2406F + var_Y * -1.5372F + var_Z * -0.4986F;
	float var_G = var_X * -0.9689F + var_Y *  1.8758F + var_Z *  0.0415F;
	float var_B = var_X *  0.0557F + var_Y * -0.2040F + var_Z *  1.0570F;

	float exponent = 1.F / 2.4F;

	if(var_R > 0.0031308F) {
		var_R = 1.055F * powf(var_R, exponent) - 0.055F;
	} else {
		var_R = 12.92F * var_R;
	}
	if(var_G > 0.0031308F) {
		var_G = 1.055F * powf(var_G, exponent) - 0.055F;
	} else {
		var_G = 12.92F * var_G;
	}
	if(var_B > 0.0031308F) {
		var_B = 1.055F * powf(var_B, exponent) - 0.055F;
	} else {
		var_B = 12.92F * var_B;
	}

	*R = var_R;
	*G = var_G;
	*B = var_B;
}

template<class T>
static void 
CIELabToRGB(float L, float a, float b, T *rgb) {
	float X, Y, Z;
	float R, G, B;
	const float max_val = std::numeric_limits<T>::max();

	CIELabToXYZ(L, a, b, &X, &Y, &Z);
	XYZToRGB(X, Y, Z, &R, &G, &B);
	
	// clamp values to [0..max_val]
	T red	= (T)CLAMP(R * max_val, 0.0F, max_val);
	T green	= (T)CLAMP(G * max_val, 0.0F, max_val);
	T blue	= (T)CLAMP(B * max_val, 0.0F, max_val);

	assignRGB(red, green, blue, rgb);
}

template<class T>
static void 
_convertLABtoRGB(unsigned width, unsigned height, BYTE* line_start, unsigned pitch, unsigned samplesperpixel) {
	const unsigned max_val = std::numeric_limits<T>::max();
	const float sL = 100.F / max_val;
	const float sa = 256.F / max_val;
	const float sb = 256.F / max_val;
	
	for(unsigned y = 0; y < height; y++) {
		T *line = (T*)line_start;

		for(unsigned x = 0; x < width; x++) {
			CIELabToRGB(line[0]* sL, line[1]* sa - 128.F, line[2]* sb - 128.F, line);
			
			line += samplesperpixel;
		}
		line_start += pitch;
	}
}

BOOL
ConvertLABtoRGB(FIBITMAP* dib) {
	if (!Bitmap_HasPixels(dib)) {
		return FALSE;
	}
		
	const BITMAP_TYPE image_type = Bitmap_GetImageType(dib);
	const unsigned bytesperpixel = Bitmap_GetBPP(dib) / 8;
	
	unsigned channelSize = 1;
	if (!(image_type == FIT_BITMAP && (bytesperpixel > 2))) {
		return FALSE;
	}
				
	const unsigned width = Bitmap_GetWidth(dib);
	const unsigned height = Bitmap_GetHeight(dib);
	BYTE *line_start = Bitmap_GetScanLine(dib, 0);
	const unsigned pitch = Bitmap_GetPitch(dib);
	
	unsigned samplesperpixel = Bitmap_GetLine(dib) / width / channelSize;
			
	if(channelSize == 1) {
		_convertLABtoRGB<BYTE>(width, height, line_start, pitch, samplesperpixel);
	}
	else {
		_convertLABtoRGB<WORD>(width, height, line_start, pitch, samplesperpixel);
	}

	return TRUE;	
}

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

FIBITMAP* 
RemoveAlphaChannel(FIBITMAP* src) { 

	if (!Bitmap_HasPixels(src)) {
		return NULL;
	}

	const BITMAP_TYPE image_type = Bitmap_GetImageType(src);
		
	switch(image_type) {
		case FIT_BITMAP:
			if (Bitmap_GetBPP(src) == 32) {
				// convert to 24-bit
				return Bitmap_ConvertTo24Bits(src);
			}
			break;
		default:
			// unsupported image type
			return NULL;
	}

	return NULL;
}
