/*
 *	ionColour.cpp - Filter to create coloured batches of ions based upon value
 *	Copyright (C) 2018, D Haley 

 *	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 "ionColour.h"

#include "filterCommon.h"
#include "../state.h"
#include "common/stringFuncs.h"

#include "common/colourmap.h"

using std::vector;
using std::string;
using std::pair;
using std::make_pair;


const unsigned int MAX_NUM_COLOURS=256;
enum
{
	KEY_IONCOLOURFILTER_COLOURMAP,
	KEY_IONCOLOURFILTER_MAPSTART,
	KEY_IONCOLOURFILTER_MAPEND,
	KEY_IONCOLOURFILTER_NCOLOURS,
	KEY_IONCOLOURFILTER_REVERSE,
	KEY_IONCOLOURFILTER_SHOWBAR,
	KEY_IONCOLOURFILTER_ALPHA,
	KEY_IONCOLOURFILTER_AUTOEXTREMA,
};

enum
{
	IONCOLOUR_ABORT_ERR
};

IonColourFilter::IonColourFilter() : colourMap(COLOURMAP_VIRIDIS),reverseMap(false), 
		nColours(MAX_NUM_COLOURS),showColourBar(true), alpha(1.0f)
{
	mapBounds[0] = 0.0f;
	mapBounds[1] = 100.0f;
	autoExtrema=true;	

	cacheOK=false;
	cache=true; //By default, we should cache, but decision is made higher up

}

Filter *IonColourFilter::cloneUncached() const
{
	IonColourFilter *p=new IonColourFilter();
	p->colourMap = colourMap;
	p->mapBounds[0]=mapBounds[0];
	p->mapBounds[1]=mapBounds[1];
	p->autoExtrema=autoExtrema;	
	p->nColours =nColours;	
	p->alpha = alpha;
	p->showColourBar =showColourBar;	
	p->reverseMap=reverseMap;	
	
	//We are copying whether to cache or not,
	//not the cache itself
	p->cache=cache;
	p->cacheOK=false;
	p->userString=userString;
	return p;
}

size_t IonColourFilter::numBytesForCache(size_t nObjects) const
{
	return (size_t)((float)(nObjects*IONDATA_SIZE));
}



unsigned int IonColourFilter::refresh(const std::vector<const FilterStreamData *> &dataIn,
	std::vector<const FilterStreamData *> &getOut, ProgressData &progress)
{
	//use the cached copy if we have it.
	if(cacheOK)
	{
		ASSERT(filterOutputs.size());
		propagateStreams(dataIn,getOut,getRefreshBlockMask(),true);

		propagateCache(getOut);

		return 0;
	}

	size_t totalSize=numElements(dataIn,STREAM_TYPE_IONS);
	if(autoExtrema)
	{
		float tmpBounds[2];
		tmpBounds[0]= std::numeric_limits<float>::max();
		tmpBounds[1]=-std::numeric_limits<float>::max();
		

		for(unsigned int ui=0;ui<dataIn.size() ;ui++)
		{
			//Only process stream_type_ions. Do not propagate anything,
			//except for the spectrum
			if(dataIn[ui]->getStreamType() == STREAM_TYPE_IONS)
			{
				const IonStreamData *ions;
				ions = (IonStreamData *)dataIn[ui];
				for(unsigned int uj=0;uj<ions->data.size(); uj++)
				{
					float mToc;
					mToc =ions->data[uj].getMassToCharge();

					if(isinf(mToc))
						continue;
					tmpBounds[0] = std::min(tmpBounds[0],mToc);
					tmpBounds[1] = std::max(tmpBounds[1],mToc);

				}

			}
			
		}
	
		if(tmpBounds[0] != std::numeric_limits<float>::max() &&
			tmpBounds[1] != -std::numeric_limits<float>::max())
		{
			mapBounds[0] = tmpBounds[0];
			mapBounds[1] = tmpBounds[1];
		}
		else
		{
			//This will happen if, e.g. no data was given 
			appendConsoleMessage(TRANS("Unable to determine colour limits"));
		}
	}

	ASSERT(nColours >0 && nColours<=MAX_NUM_COLOURS);
	IonStreamData *d[nColours];
	unsigned char rgb[3]; //RGB array
	//Build the colourmap values, each as a unique filter output
	for(unsigned int ui=0;ui<nColours; ui++)
	{
		d[ui]=new IonStreamData;
		d[ui]->parent=this;
		float value;
		value = (float)ui*(mapBounds[1]-mapBounds[0])/(float)nColours + mapBounds[0];
		//Pick the desired colour map
		colourMapWrap(colourMap,rgb,value,mapBounds[0],mapBounds[1],reverseMap);
	
		d[ui]->r=rgb[0]/255.0f;
		d[ui]->g=rgb[1]/255.0f;
		d[ui]->b=rgb[2]/255.0f;
		d[ui]->a=1.0f;
	}



	//Try to maintain ion size if possible
	bool haveIonSize,sameSize; // have we set the ionSize?
	float ionSize;
	haveIonSize=false;
	sameSize=true;

	//Did we find any ions in this pass?
	bool foundIons=false;	
	unsigned int curProg=NUM_CALLBACK;
	size_t n=0;
	for(unsigned int ui=0;ui<dataIn.size() ;ui++)
	{
		switch(dataIn[ui]->getStreamType())
		{
			case STREAM_TYPE_IONS: 
			{
				foundIons=true;

				//Check for ion size consistency	
				if(haveIonSize)
				{
					sameSize &= (fabs(ionSize-((const IonStreamData *)dataIn[ui])->ionSize) 
									< std::numeric_limits<float>::epsilon());
				}
				else
				{
					ionSize=((const IonStreamData *)dataIn[ui])->ionSize;
					haveIonSize=true;
				}
				for(vector<IonHit>::const_iterator it=((const IonStreamData *)dataIn[ui])->data.begin();
					       it!=((const IonStreamData *)dataIn[ui])->data.end(); ++it)
				{
					//Work out the colour map assignment from the mass to charge.
					// linear assignment in range
					unsigned int colour;

					float tmp;	
					tmp= (it->getMassToCharge()-mapBounds[0])/(mapBounds[1]-mapBounds[0]);
					tmp = std::max(0.0f,tmp);
					tmp = std::min(tmp,1.0f);
					
					colour=(unsigned int)(tmp*(float)(nColours-1));	
					d[colour]->data.push_back(*it);
				
					//update progress every CALLBACK ions
					if(!curProg--)
					{
						n+=NUM_CALLBACK;
						progress.filterProgress= (unsigned int)((float)(n)/((float)totalSize)*100.0f);
						curProg=NUM_CALLBACK;
						if(*Filter::wantAbort)
						{
							for(unsigned int ui=0;ui<nColours;ui++)
								delete d[ui];
							return IONCOLOUR_ABORT_ERR;
						}
					}
				}

				
				break;
			}
			default:
				getOut.push_back(dataIn[ui]);

		}
	}
	progress.filterProgress=100;

	//create the colour bar as needed
	if(foundIons && showColourBar)
	{
		DrawStreamData *d = new DrawStreamData;
		d->drawables.emplace_back(makeColourBar(mapBounds[0],mapBounds[1],nColours,colourMap,reverseMap,alpha));
		d->parent=this;
		cacheAsNeeded(d);
		getOut.push_back(d);
	}


	//If all the ions are the same size, then propagate
	if(haveIonSize && sameSize)
	{
		for(unsigned int ui=0;ui<nColours;ui++)
			d[ui]->ionSize=ionSize;
	}
	//merge the results as needed
	if(cache)
	{
		for(unsigned int ui=0;ui<nColours;ui++)
		{
			if(d[ui]->data.size())
				d[ui]->cached=1;
			else
				d[ui]->cached=0;
			if(d[ui]->data.size())
				filterOutputs.push_back(d[ui]);
		}
		cacheOK=filterOutputs.size();
	}
	else
	{
		for(unsigned int ui=0;ui<nColours;ui++)
		{
			//NOTE: MUST set cached BEFORE push_back!
			d[ui]->cached=0;
		}
		cacheOK=false;
	}

	//push the colours onto the output. cached or not (their status is set above).
	for(unsigned int ui=0;ui<nColours;ui++)
	{
		if(d[ui]->data.size())
			getOut.push_back(d[ui]);
		else
			delete d[ui];
	}
	
	return 0;
}


void IonColourFilter::getProperties(FilterPropGroup &propertyList) const
{

	FilterProperty p;
	string tmpStr;
	vector<pair<unsigned int, string> > choices;

	size_t curGroup=0;

	for(unsigned int ui=0;ui<COLOURMAP_ENUM_END; ui++)
		choices.emplace_back(ui,getColourMapName(ui));
	FilterProperty pColourMap(KEY_IONCOLOURFILTER_COLOURMAP, TRANS("Colour Map"),choices,
			colourMap,TRANS("Colour scheme used to assign points colours by value"));
	propertyList.addProperty(pColourMap,curGroup);

	FilterProperty pReverse(KEY_IONCOLOURFILTER_REVERSE, TRANS("Reverse map"), 
			reverseMap,TRANS("Reverse the colour scale"));
	propertyList.addProperty(pReverse,curGroup);
	
	FilterProperty pShowBar(KEY_IONCOLOURFILTER_SHOWBAR, TRANS("Show Bar"), 
			showColourBar,TRANS("Display the colour legend in the 3D view"));
	propertyList.addProperty(pShowBar,curGroup);
	
	FilterProperty pOpacity(KEY_IONCOLOURFILTER_ALPHA, TRANS("Opacity"), 
			alpha,TRANS("How see-through to make the legend (0- transparent, 1- solid)"));
	propertyList.addProperty(pOpacity,curGroup);
	
	FilterProperty pNumColours(KEY_IONCOLOURFILTER_NCOLOURS, TRANS("Num Colours"), 
			nColours,TRANS("Number of unique colours to use in colour map")); 
	propertyList.addProperty(pNumColours,curGroup);

	FilterProperty pAutoBounds(KEY_IONCOLOURFILTER_AUTOEXTREMA, TRANS("Auto bounds"), 
			autoExtrema,TRANS("Automatically scale colour to min/max of data range"));
	propertyList.addProperty(pAutoBounds,curGroup);
	if(!autoExtrema)
	{

		FilterProperty pMapStart(KEY_IONCOLOURFILTER_MAPSTART, TRANS("Map start"), 
				mapBounds[0],TRANS("Assign points with this value to the first colour in map")) ;
		propertyList.addProperty(pMapStart,curGroup);

		FilterProperty pMapEnd(KEY_IONCOLOURFILTER_MAPEND, TRANS("Map End"), 
				mapBounds[1],TRANS("Assign points with this value to the last colour in map")); 
		propertyList.addProperty(pMapEnd,curGroup);
	}	
	propertyList.setGroupTitle(curGroup,TRANS("Data"));

}

bool IonColourFilter::setProperty(  unsigned int key,
					const std::string &value, bool &needUpdate)
{

	needUpdate=false;
	switch(key)
	{
		case KEY_IONCOLOURFILTER_COLOURMAP:
		{
			unsigned int tmpMap;
			tmpMap=(unsigned int)-1;
			for(unsigned int ui=0;ui<COLOURMAP_ENUM_END;ui++)
			{
				if(value== getColourMapName(ui))
				{
					tmpMap=ui;
					break;
				}
			}

			if(tmpMap >=COLOURMAP_ENUM_END || tmpMap ==colourMap)
				return false;

			clearCache();
			needUpdate=true;
			colourMap=tmpMap;
			break;
		}
		case KEY_IONCOLOURFILTER_REVERSE:
		{
			if(!applyPropertyNow(reverseMap,value,needUpdate))
				return false;
			break;
		}	
		case KEY_IONCOLOURFILTER_MAPSTART:
		{
			float tmpBound;
			if(stream_cast(tmpBound,value))
				return false;

			if(tmpBound >=mapBounds[1])
				return false;

			clearCache();
			needUpdate=true;
			mapBounds[0]=tmpBound;
			break;
		}
		case KEY_IONCOLOURFILTER_MAPEND:
		{
			float tmpBound;
			if(stream_cast(tmpBound,value))
				return false;

			if(tmpBound <=mapBounds[0])
				return false;

			clearCache();
			needUpdate=true;
			mapBounds[1]=tmpBound;
			break;
		}
		case KEY_IONCOLOURFILTER_NCOLOURS:
		{
			unsigned int numColours;
			if(stream_cast(numColours,value))
				return false;

			clearCache();
			needUpdate=true;
			//enforce 1->MAX_NUM_COLOURS range
			nColours=std::min(numColours,MAX_NUM_COLOURS);
			if(!nColours)
				nColours=1;
			break;
		}
		case KEY_IONCOLOURFILTER_SHOWBAR:
		{
			if(!applyPropertyNow(showColourBar,value,needUpdate))
				return false;
			break;
		}	
		case KEY_IONCOLOURFILTER_ALPHA:
		{
			if(!applyPropertyNow(alpha,value,needUpdate))
				return false;
			break;
		}
		case KEY_IONCOLOURFILTER_AUTOEXTREMA:
		{
			if(!applyPropertyNow(autoExtrema,value,needUpdate))
				return false;
			break;
		}
		default:
			ASSERT(false);
	}	
	return true;
}


std::string  IonColourFilter::getSpecificErrString(unsigned int code) const
{
	//Currently the only error is aborting
	return std::string(TRANS("Aborted"));
}

void IonColourFilter::setPropFromBinding(const SelectionBinding &b)
{
	ASSERT(false); 
}

bool IonColourFilter::writeState(std::ostream &f,unsigned int format, unsigned int depth) const
{
	using std::endl;
	switch(format)
	{
		case STATE_FORMAT_XML:
		{	
			f << tabs(depth) << "<" << trueName() << ">" << endl;
			f << tabs(depth+1) << "<userstring value=\""<< escapeXML(userString) << "\"/>"  << endl;

			f << tabs(depth+1) << "<colourmap value=\"" << colourMap << "\"/>" << endl;
			f << tabs(depth+1) << "<extrema auto=\""  << boolStrEnc(autoExtrema) << "\" min=\"" << mapBounds[0] << "\" max=\"" 
				<< mapBounds[1] << "\"/>" << endl;
			f << tabs(depth+1) << "<ncolours value=\"" << nColours << "\" opacity=\"" << alpha << "\"/>" << endl;

			f << tabs(depth+1) << "<showcolourbar value=\"" << boolStrEnc(showColourBar)<< "\"/>" << endl;
			f << tabs(depth+1) << "<reversemap value=\"" << boolStrEnc(reverseMap)<< "\"/>" << endl;
			
			f << tabs(depth) << "</" << trueName() << ">" << endl;
			break;
		}
		default:
			ASSERT(false);
			return false;
	}

	return true;
}

bool IonColourFilter::readState(xmlNodePtr &nodePtr, const std::string &stateFileDir)
{
	//Retrieve user string
	//===
	if(XMLHelpFwdToElem(nodePtr,"userstring"))
		return false;

	xmlChar *xmlString=xmlGetProp(nodePtr,(const xmlChar *)"value");
	if(!xmlString)
		return false;
	userString=(char *)xmlString;
	xmlFree(xmlString);
	//===

	std::string tmpStr;	
	//Retrieve colourmap
	//====
	if(XMLHelpFwdToElem(nodePtr,"colourmap"))
		return false;

	xmlString=xmlGetProp(nodePtr,(const xmlChar *)"value");
	if(!xmlString)
		return false;
	tmpStr=(char *)xmlString;

	//convert from string to digit
	if(stream_cast(colourMap,tmpStr))
		return false;

	//We defaulted to Jet prior to version 0.0.20, and did not store
	// the colour map data properly, so it loaded as jet 
	if( AnalysisState::getStateWriterVersion() < getVersionNumber(0,0,20)  && colourMap == 0)
		colourMap=COLOURMAP_JET;
		
			
	if(colourMap>= COLOURMAP_ENUM_END)
	       return false;	
	xmlFree(xmlString);

	if(XMLHelpGetProp(alpha, nodePtr,"opacity"))
	{
		alpha=1.0f;
	}
	else
	{
		//clamp alpha to [0,1]
		alpha = std::max(0.0f,std::min(alpha,1.0f));
	}
	//====
	
	//Retrieve Extrema 
	//===
	float tmpMin,tmpMax;
	if(XMLHelpFwdToElem(nodePtr,"extrema"))
		return false;

	if(XMLHelpGetProp(autoExtrema,nodePtr,"auto"))
	{
		if(AnalysisState::getStateWriterVersion() <=19)
		{
			//DEPRECATE ME
			//This property did not exist prior to 0.0.20
			autoExtrema=false;
		}
		else
			autoExtrema=true;
	}

	xmlString=xmlGetProp(nodePtr,(const xmlChar *)"min");
	if(!xmlString)
		return false;
	tmpStr=(char *)xmlString;

	//convert from string to digit
	if(stream_cast(tmpMin,tmpStr))
		return false;

	xmlString=xmlGetProp(nodePtr,(const xmlChar *)"max");
	if(!xmlString)
		return false;
	tmpStr=(char *)xmlString;

	//convert from string to digit
	if(stream_cast(tmpMax,tmpStr))
		return false;

	xmlFree(xmlString);

	if(tmpMin > tmpMax)
		return false;

	mapBounds[0]=tmpMin;
	mapBounds[1]=tmpMax;

	//===
	
	//Retrieve num colours 
	//====
	if(XMLHelpFwdToElem(nodePtr,"ncolours"))
		return false;

	xmlString=xmlGetProp(nodePtr,(const xmlChar *)"value");
	if(!xmlString)
		return false;
	tmpStr=(char *)xmlString;

	//convert from string to digit
	if(stream_cast(nColours,tmpStr))
		return false;

	xmlFree(xmlString);
	//====
	
	//Retrieve num colours 
	//====
	if(XMLHelpFwdToElem(nodePtr,"showcolourbar"))
		return false;

	xmlString=xmlGetProp(nodePtr,(const xmlChar *)"value");
	if(!xmlString)
		return false;
	tmpStr=(char *)xmlString;

	//convert from string to digit
	if(stream_cast(showColourBar,tmpStr))
		return false;

	xmlFree(xmlString);
	//====
	
	//Check for colour map reversal
	//=====
	if(XMLHelpFwdToElem(nodePtr,"reversemap"))
	{
		//Didn't exist prior to 0.0.15, assume off
		reverseMap=false;
	}
	else
	{
		xmlString=xmlGetProp(nodePtr,(const xmlChar *)"value");
		if(!xmlString)
			return false;
		tmpStr=(char *)xmlString;

		//convert from string to bool 
		if(!boolStrDec(tmpStr,reverseMap))
			return false;
	}

	xmlFree(xmlString);
	//====
	return true;
}

unsigned int IonColourFilter::getRefreshBlockMask() const
{
	//Anything but ions can go through this filter.
	return STREAM_TYPE_IONS;
}

unsigned int IonColourFilter::getRefreshEmitMask() const
{
	return  STREAM_TYPE_DRAW | STREAM_TYPE_IONS;
}

unsigned int IonColourFilter::getRefreshUseMask() const
{
	return  STREAM_TYPE_IONS;
}
#ifdef DEBUG

IonStreamData *sythIonCountData(unsigned int numPts, float mStart, float mEnd)
{
	IonStreamData *d = new IonStreamData;
	d->data.resize(numPts);
	for(unsigned int ui=0; ui<numPts;ui++)
	{
		IonHit h;

		h.setPos(Point3D(ui,ui,ui));
		h.setMassToCharge( (mEnd-mStart)*(float)ui/(float)numPts + mStart);
		d->data[ui] =h;
	}

	return d;
}


bool ionCountTest()
{
	const int NUM_PTS=1000;
	vector<const FilterStreamData*> streamIn,streamOut;
	IonStreamData *d=sythIonCountData(NUM_PTS,0,100);
	streamIn.push_back(d);


	IonColourFilter *f = new IonColourFilter;
	f->setCaching(false);

	bool needUpdate;
	TEST(f->setProperty(KEY_IONCOLOURFILTER_NCOLOURS,"100",needUpdate),"Set prop");
	TEST(f->setProperty(KEY_IONCOLOURFILTER_MAPSTART,"0",needUpdate),"Set prop");
	TEST(f->setProperty(KEY_IONCOLOURFILTER_MAPEND,"100",needUpdate),"Set prop");
	TEST(f->setProperty(KEY_IONCOLOURFILTER_SHOWBAR,"0",needUpdate),"Set prop");
	
	ProgressData p;
	TEST(!f->refresh(streamIn,streamOut,p),"refresh error code");
	delete f;
	delete d;
	
	TEST(streamOut.size() == 100,"stream count");

	for(unsigned int ui=0;ui<streamOut.size();ui++)
	{
		TEST(streamOut[ui]->getStreamType() == STREAM_TYPE_IONS,"stream type");
	}

	for(unsigned int ui=0;ui<streamOut.size();ui++)
		delete streamOut[ui];

	return true;
}


bool IonColourFilter::runUnitTests()
{
	if(!ionCountTest())
		return false;

	return true;
}


#endif

