/*
 *	colourmap.cpp  - contiuum colourmap header
 *	Copyright (C) 2010, ViewerGTKQt project
 *	Modifed by D Haley 2013

 *	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 3 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, see <http://www.gnu.org/licenses/>.
*/


#include <math.h>
#include <stdlib.h>
#include "colourmap.h"
#include <limits>

#include "common/translation.h"
#include "common/assertion.h"

//4th-order polynomial fitted colour map. Coeffs are in decreasing power (p^n... p^0), and per channel
// so you need [5 + 5 + 5] sized array (R + G + B) 
void fittedColorMap(unsigned char *rgb, float value, float min, float max, const float *coeffs);

void jetColorMap(unsigned char *rgb,float value,float min,float max)
{
	float max4=(max-min)/4;
	value-=min;
	if (value==HUGE_VAL)
	{
		rgb[0]=rgb[1]=rgb[2]=255;
	}
	else if (value<0)
	{
		rgb[0]=rgb[1]=rgb[2]=0;
	}
	else if (value<max4)
	{
		unsigned char c1=144;
		rgb[0]=0;
		rgb[1]=0;
		rgb[2]=c1+(unsigned char)((255-c1)*value/max4);
	}
	else if (value<2*max4)
	{
		rgb[0]=0;
		rgb[1]=(unsigned char)(255*(value-max4)/max4);
		rgb[2]=255;
	}
	else if (value<3*max4)
	{
		rgb[0]=(unsigned char)(255*(value-2*max4)/max4);
		rgb[1]=255;
		rgb[2]=255-rgb[0];
	}
	else if (value<max)
	{
		rgb[0]=255;
		rgb[1]=(unsigned char)(255-255*(value-3*max4)/max4);
		rgb[2]=0;
	}
	else {
		rgb[0]=255;
		rgb[1]=rgb[2]=0;
	}
}

void hotColorMap(unsigned char *rgb,float value,float min,float max)
{
  float max3=(max-min)/3;
  value-=min;
  if(value==HUGE_VAL)
    {rgb[0]=rgb[1]=rgb[2]=255;}
  else if(value<0)
    {rgb[0]=rgb[1]=rgb[2]=0;}
  else if(value<max3)
    {rgb[0]=(unsigned char)(255*value/max3);rgb[1]=0;rgb[2]=0;}
  else if(value<2*max3)
    {rgb[0]=255;rgb[1]=(unsigned char)(255*(value-max3)/max3);rgb[2]=0;}
  else if(value<max)
    {rgb[0]=255;rgb[1]=255;rgb[2]=(unsigned char)(255*(value-2*max3)/max3);}
  else {rgb[0]=rgb[1]=rgb[2]=255;}
}

void coldColorMap(unsigned char *rgb,float value,float min,float max)
{
  float max3=(max-min)/3;
  value-=min;
  if(value==HUGE_VAL)
    {rgb[0]=rgb[1]=rgb[2]=255;}
  else if(value<0)
    {rgb[0]=rgb[1]=rgb[2]=0;}
  else if(value<max3)
    {rgb[0]=0;rgb[1]=0;rgb[2]=(unsigned char)(255*value/max3);}
  else if(value<2*max3)
    {rgb[0]=0;rgb[1]=(unsigned char)(255*(value-max3)/max3);rgb[2]=255;}
  else if(value<max)
    {rgb[0]=(unsigned char)(255*(value-2*max3)/max3);rgb[1]=255;rgb[2]=255;}
  else {rgb[0]=rgb[1]=rgb[2]=255;}
}

void blueColorMap(unsigned char *rgb,float value,float min,float max)
{
  value-=min;
  if(value==HUGE_VAL)
    {rgb[0]=rgb[1]=rgb[2]=255;}
  else if(value<0)
    {rgb[0]=rgb[1]=rgb[2]=0;}
  else if(value<max)
    {rgb[0]=0;rgb[1]=0;rgb[2]=(unsigned char)(255*value/max);}
  else {rgb[0]=rgb[1]=0;rgb[2]=255;}
}

void positiveColorMap(unsigned char *rgb,float value,float min,float max)
{
  value-=min;
  max-=min;
  value/=max;

  if(value<0){
  rgb[0]=rgb[1]=rgb[2]=0;
    return;
  }
  if(value>1){
  rgb[0]=rgb[1]=rgb[2]=255;
  return;
  }

  rgb[0]=192;rgb[1]=0;rgb[2]=0;
  rgb[0]+=(unsigned char)(63*value);
  rgb[1]+=(unsigned char)(255*value);
  if(value>0.5)
  rgb[2]+=(unsigned char)(255*2*(value-0.5));
}

void negativeColorMap(unsigned char *rgb,float value,float min,float max)
{
  value-=min;
  max-=min;
  rgb[0]=0;rgb[1]=0;rgb[2]=0;
  
  if(max>std::numeric_limits<float>::epsilon())
	  value/=max;
  if(value<0) return;
  if(value>1){
  rgb[1]=rgb[2]=255;
  return;
  }

  rgb[1]+=(unsigned char)(255*value);
  if(value>0.5)
  rgb[2]+=(unsigned char)(255*2*(value-0.5));

}

void colorMap(unsigned char *rgb,float value,float min,float max)
{
  if(value>0) 
    positiveColorMap(rgb,value,0,max);
  else 
    negativeColorMap(rgb,value,min,0);
}

void cyclicColorMap(unsigned char *rgb,float value,float min,float max)
{
  float max3=(max-min)/3;
  value-=(max-min)*(float)floor((value-min)/(max-min));
  if(value<max3)
    {rgb[0]=(unsigned char)(255-255*value/max3);rgb[1]=0;rgb[2]=255-rgb[0];}
  else if(value<2*max3)
    {rgb[0]=0;rgb[1]=(unsigned char)(255*(value-max3)/max3);rgb[2]=255-rgb[1];}
  else if(value<max)
    {rgb[0]=(unsigned char)(255*(value-2*max3)/max3);rgb[1]=255-rgb[0];rgb[2]=0;}

}
void randColorMap(unsigned char *rgb,float value,float min,float max)
{
  srand((int)(65000*(value-min)/(max-min)));
  rgb[0]=(unsigned char)(255*rand());
  rgb[1]=(unsigned char)(255*rand());
  rgb[2]=(unsigned char)(255*rand());
}

void grayColorMap(unsigned char *rgb,float value,float min,float max)
{
  max-=min;
  value-=min;
  rgb[0]=rgb[1]=rgb[2]=(unsigned char)(255*value/max);
}


void infernoColorMap(unsigned char *rgb, float value, float min, float max)
{
	//I have performed a simple parameterised fit
	// to the colourmap provided at
	// BIDS (github).  There is some quality loss in the fit
	// https://bids.github.io/colormap/
	//4th-order fit
	const float INFERNO_COEFFS[3*5]= {  3.9160e-10, -3.0773e-7,  5.6460e-5, 2.8489e-3, -1.3064e-2 , //Fitted R
					   -4.8786e-10,  2.6912e-7, -2.9237e-5,  2.0396e-3, -3.5979e-3, // Fitted G
					   1.6772e-09,  -4.0943e-07 , -3.4314e-05 ,  1.0220e-02,-7.5587e-3 }; // Fitted B
	fittedColorMap(rgb,value,min,max,INFERNO_COEFFS);
}


void viridisColorMap(unsigned char *rgb, float value, float min, float max)
{
	//4th order polynomial fit to viridis. Each row is a different channel (RGB) 
	const float VIRIDIS_COEFFS[5*3]={	-1.0011e-09,6.8506e-07,-1.1678e-04,4.9495e-03,2.3277e-01,
						-2.9027e-10,1.3784e-07,-2.6865e-05,6.2223e-03,1.3762e-03,
						2.2975e-10,-1.0751e-07,-6.6945e-06,3.6694e-03,3.6022e-01};

	fittedColorMap(rgb,value,min,max,VIRIDIS_COEFFS);
}

void fittedColorMap(unsigned char *rgb, float value, float min, float max, const float *coeffs)
{

	//scale value to the 0-255 range

	value= (value - min)/(max-min)*255.0f;
	value = std::min(value,255.0f);
	value = std::max(value,0.0f);

	const unsigned int FIT_ORDER=5;
		
	//Compute the fitted RGB 
	for(unsigned int ui=0;ui<3;ui++)
	{
		float f;
		unsigned int offset = ui*FIT_ORDER;
		f= value*value*value*value*coeffs[offset+0] + 
				value*value*value*coeffs[offset+1] +
				value*value*coeffs[offset+2] +
				value*coeffs[offset+3] +
				coeffs[offset+4];
		f = std::max(f,0.0f);
		f = std::min(f,255.0f);
		rgb[ui] = f*255.0f;
	}

}


void colourMapWrap(unsigned int mapID,unsigned char *rgb, float v, 
				float min, float max, bool reverse)

{
	//Colour functions assume  positive value, so remap
	v= v-min;
	max-=min;
	min=0;

	if(reverse)
		v= max-v;

	//Select the desired colour map
	switch(mapID)
	{
		case  COLOURMAP_JET:
			jetColorMap(rgb, v, min, max);
			break;
		case  COLOURMAP_HOT:
			hotColorMap(rgb, v, min, max);
			break;
		case  COLOURMAP_COLD:
			coldColorMap(rgb, v, min, max);
			break;
		case  COLOURMAP_GRAY:
			 grayColorMap(rgb, v, min, max);
			break;
		case  COLOURMAP_CYCLIC:
			cyclicColorMap(rgb, v, min, max);
			break;
		case  COLOURMAP_GENERAL:
			colorMap(rgb, v, min, max);
			break;
		case  COLOURMAP_BLUE:
			blueColorMap(rgb, v, min, max);
			break;
		case  COLOURMAP_RAND:
			 randColorMap(rgb, v, min, max);
			break;
		case  COLOURMAP_INFERNO:
			infernoColorMap(rgb, v, min, max);
			break;
		case  COLOURMAP_VIRIDIS:
			viridisColorMap(rgb, v, min, max);
			break;
	}



}

std::string getColourMapName(unsigned int mapID)
{
	ASSERT(mapID < COLOURMAP_ENUM_END);
	const char *mapNames[] = { NTRANS("Jet"),
				NTRANS("Hot"),
				NTRANS("Cold"),
				NTRANS("Grey"),
				NTRANS("Cyclic"),
				NTRANS("General"),
				NTRANS("Blue"),
				NTRANS("Pseudo-Random"),
				NTRANS("Inferno"),
				NTRANS("Viridis")};

	return TRANS(mapNames[mapID]);
}


void applyCustomColourMap(const std::vector<std::pair<float,ColourRGBAf> > &cMap, unsigned char *rgb, 
							unsigned char &alphaThis, float value, float minV, float maxV)
{
	if(!cMap.size())
	{
		WARN(false,"No colour map data specified, but map requested")
		//Return grey if no map
		for(unsigned int ui=0; ui<3;ui++)
			rgb[ui]=128;
		alphaThis=255;
		return;
	}
	
	//Clamp lower value to first colour
	if(value <= minV)
	{
		cMap[0].second.copyToRGBArray(rgb);
		alphaThis = cMap[0].second.a()*255;
		return;
	}

	//Clamp upper value to last colour
	if(value >=maxV)
	{
		cMap[cMap.size()-1].second.copyToRGBArray(rgb);
		alphaThis = cMap[cMap.size()-1].second.a()*255;
		return;

	}


	//Find the interval in which the value is contained, and then interpolate
	for(unsigned int ui=0;ui<cMap.size()-1;ui++)
	{
		float av,bv;
		av= cMap[ui].first ;
		bv= cMap[ui+1].first ;

		//Check for equality case
		if(bv == av && bv == value)
		{
			ColourRGBAf c;
			c = cMap[ui].second.interpolate(0.5,cMap[ui+1].second);
			c.copyToRGBArray(rgb);
			alphaThis = c.a()*255;
		}
		else if( value >=av && value < bv) // Check if contained on inverval
		{
			//Linearly interpolate
			float delta;
			delta = (value-av)/(bv-av);
			ColourRGBAf c;
			c = cMap[ui].second.interpolate(delta,cMap[ui+1].second);
			
			c.copyToRGBArray(rgb);
			alphaThis = c.a()*255;
			return;
		}
	}

	//Failed. Return last colour
	cMap[cMap.size()-1].second.copyToRGBArray(rgb);
	alphaThis = cMap[cMap.size()-1].second.a()*255;
}


#ifdef DEBUG

bool testColourMap()
{
	//FIXME: More tests

	float v,minV,maxV;
	v = 50;
	minV=0;
	maxV=100;

	unsigned char rgb[3];

	infernoColorMap(rgb,v,minV,maxV);

	TEST(EQ_TOLV( (float) rgb[0],0.7364*255.0f,2.0),"inferno Red") ;
	TEST(EQ_TOLV((float)rgb[1],0.2118*255.0f,2.0f),"inferno Green") ;
	TEST(EQ_TOLV((float)rgb[2],0.33004*255.0f,2.0f),"inferno Blue") ;

	return true;
}

#endif

