/*
 *	voxelise.cpp - Compute 3D binning (voxelisation) of point clouds
 *	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 "voxelise.h"
#include "common/colourmap.h"
#include "filterCommon.h"

#include <map>

enum
{
	KEY_FIXEDWIDTH,
	KEY_NBINSX,
	KEY_NBINSY,
	KEY_NBINSZ,
	KEY_WIDTHBINSX,
	KEY_WIDTHBINSY,
	KEY_WIDTHBINSZ,
	
	KEY_COUNT_TYPE,
	KEY_NORMALISE_TYPE,
	

	KEY_THRESHOLD_MASK_ENABLE,
	KEY_THRESHOLD_VALUE,
	KEY_THRESHOLD_MASK_TOMAX,
	
	KEY_FILTER_MODE,
	KEY_FILTER_RATIO,
	KEY_FILTER_STDEV,
	KEY_ENABLE_NUMERATOR,
	KEY_ENABLE_DENOMINATOR,
	
	KEY_VOXEL_APPEARANCE_BASE,

};

//!Normalisation method
enum
{
	VOXELISE_NORMALISETYPE_NONE,// straight count
	VOXELISE_NORMALISETYPE_VOLUME,// density
	VOXELISE_NORMALISETYPE_ALLATOMSINVOXEL, // concentration
	VOXELISE_NORMALISETYPE_COUNT2INVOXEL,// ratio count1/count2
	VOXELISE_NORMALISETYPE_MAX // keep this at the end so it's a bookend for the last value
};

//!Filtering mode
enum
{
	VOXELISE_FILTERTYPE_NONE,
	VOXELISE_FILTERTYPE_GAUSS,
	VOXELISE_FILTERTYPE_LAPLACE,
	VOXELISE_FILTERTYPE_MAX // keep this at the end so it's a bookend for the last value
};


//Boundary behaviour for filtering 
enum
{
	VOXELISE_FILTERBOUNDMODE_ZERO,
	VOXELISE_FILTERBOUNDMODE_BOUNCE,
	VOXELISE_FILTERBOUNDMODE_MAX// keep this at the end so it's a bookend for the last value
};


//Error codes and corresponding strings
//--
enum
{
	VOXELISE_ABORT_ERR=1,
	VOXELISE_MEMORY_ERR,
	VOXELISE_CONVOLVE_ERR,
	VOXELISE_BOUNDS_INVALID_ERR,
	VOXELISE_ERR_ENUM_END
};
//--


const char *NORMALISE_TYPE_STRING[] = {
		NTRANS("None (Raw count)"),
		NTRANS("Volume (Density)"),
		NTRANS("All Ions (conc)"),
		NTRANS("Ratio (Num/Denom)"),
	};


const char *VOXELISE_FILTER_TYPE_STRING[]={
	NTRANS("None"),
	NTRANS("Gaussian (blur)"),
	NTRANS("Lapl. of Gauss. (edges)"),
	};


//This is not a member of voxels.h, as the voxels do not have any concept of the IonHit
int countPoints(Voxels<float> &v, const std::vector<IonHit> &points, 
				bool noWrap)
{

	size_t x,y,z;
	size_t binCount[3];
	v.getSize(binCount[0],binCount[1],binCount[2]);

	unsigned int downSample=MAX_CALLBACK;
	for (size_t ui=0; ui<points.size(); ui++)
	{
		if(!downSample--)
		{
			if(*Filter::wantAbort)
				return 1;
			downSample=MAX_CALLBACK;
		}
		v.getIndexWithUpper(x,y,z,points[ui].getPos());
		//Ensure it lies within the dataset
		if (x < binCount[0] && y < binCount[1] && z< binCount[2])
		{
			{
				float value;
				value=v.getData(x,y,z)+1.0f;

				ASSERT(value >= 0.0f);
				//Prevent wrap-around errors
				if (noWrap) {
					if (value > v.getData(x,y,z))
						v.setData(x,y,z,value);
				} else {
					v.setData(x,y,z,value);
				}
			}
		}
	}
	return 0;
}


// == Voxels filter ==
VoxeliseFilter::VoxeliseFilter() : fixedWidth(false), normaliseType(VOXELISE_NORMALISETYPE_NONE),voxAppearance(this)
{
	COMPILE_ASSERT(THREEDEP_ARRAYSIZE(NORMALISE_TYPE_STRING) ==  VOXELISE_NORMALISETYPE_MAX);
	COMPILE_ASSERT(THREEDEP_ARRAYSIZE(VOXELISE_FILTER_TYPE_STRING) == VOXELISE_FILTERTYPE_MAX );


	filterKernelSize=3.0;
	filterMode=VOXELISE_FILTERTYPE_NONE;
	gaussDev=0.5;	

	//Disable threshold masking by default
	thresholdMaskEnable=false;
	threshMaskValue=0;
	threshMaskToMax=false;

	//Fictitious bounds.
	bc.setBounds(Point3D(0,0,0),Point3D(1,1,1));

	for (unsigned int i = 0; i < INDEX_LENGTH; i++) 
		nBins[i] = 50;

	calculateWidthsFromNumBins(binWidth,nBins);

	numeratorAll = false;
	denominatorAll = true;


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


	rsdIncoming=0;
}


Filter *VoxeliseFilter::cloneUncached() const
{
	VoxeliseFilter *p=new VoxeliseFilter();

	p->filterMode=filterMode;
	p->filterKernelSize=filterKernelSize;
	p->gaussDev=gaussDev;
	
	p->voxAppearance=voxAppearance;

	p->normaliseType=normaliseType;
	p->numeratorAll=numeratorAll;
	p->denominatorAll=denominatorAll;

	p->bc=bc;

	p->fixedWidth=fixedWidth;
	for(size_t ui=0;ui<INDEX_LENGTH;ui++)
	{
		p->nBins[ui] = nBins[ui];
		p->binWidth[ui] = binWidth[ui];
	}

	p->enabledIons[0].resize(enabledIons[0].size());
	std::copy(enabledIons[0].begin(),enabledIons[0].end(),p->enabledIons[0].begin());
	
	p->enabledIons[1].resize(enabledIons[1].size());
	std::copy(enabledIons[1].begin(),enabledIons[1].end(),p->enabledIons[1].begin());

	if(rsdIncoming)
	{
		p->rsdIncoming=new RangeStreamData();
		*(p->rsdIncoming) = *rsdIncoming;
	}
	else
		p->rsdIncoming=0;

	p->thresholdMaskEnable=thresholdMaskEnable;
	p->threshMaskValue=threshMaskValue;
	p->threshMaskToMax=threshMaskToMax;

	p->cache=cache;
	p->cacheOK=false;
	p->userString=userString;
	return p;
}

void VoxeliseFilter::clearCache() 
{
	voxelCache.clear();
	Filter::clearCache();
}

size_t VoxeliseFilter::numBytesForCache(size_t nObjects) const
{
	//if we are using fixed width, we know the answer.
	//otherwise we dont until we are presented with the boundcube.
	//TODO: Modify the function description to pass in the boundcube
	if(!fixedWidth)
		return 	nBins[0]*nBins[1]*nBins[2]*sizeof(float);
	else
		return 0;
}

void VoxeliseFilter::initFilter(const std::vector<const FilterStreamData *> &dataIn,
						std::vector<const FilterStreamData *> &dataOut)
{
	const RangeStreamData *c=0;
	//Determine if we have an incoming range
	for (size_t i = 0; i < dataIn.size(); i++) 
	{
		if(dataIn[i]->getStreamType() == STREAM_TYPE_RANGE)
		{
			c=(const RangeStreamData *)dataIn[i];

			break;
		}
	}

	//we no longer (or never did) have any incoming ranges. Not much to do
	if(!c)
	{
		//delete the old incoming range pointer
		if(rsdIncoming)
			delete rsdIncoming;
		rsdIncoming=0;

		enabledIons[0].clear(); //clear numerator options
		enabledIons[1].clear(); //clear denominator options

		//Prevent normalisation type being set incorrectly
		// if we have no incoming range data
		if(normaliseType == VOXELISE_NORMALISETYPE_ALLATOMSINVOXEL || normaliseType == VOXELISE_NORMALISETYPE_COUNT2INVOXEL)
			normaliseType= VOXELISE_NORMALISETYPE_NONE;
	}
	else
	{


		//If we didn't have an incoming rsd, then make one up!
		if(!rsdIncoming)
		{
			rsdIncoming = new RangeStreamData;
			*rsdIncoming=*c;

			//set the numerator to all disabled
			enabledIons[0].resize(rsdIncoming->rangeFile->getNumIons(),0);
			//set the denominator to have all enabled
			enabledIons[1].resize(rsdIncoming->rangeFile->getNumIons(),1);
		}
		else
		{

			//OK, so we have a range incoming already (from last time)
			//-- the question is, is it the same
			//one we had before 
			//Do a pointer comparison (its a hack, yes, but it should work)
			if(rsdIncoming->rangeFile != c->rangeFile)
			{
				//hmm, it is different. well, trash the old incoming rng
				delete rsdIncoming;

				rsdIncoming = new RangeStreamData;
				*rsdIncoming=*c;

				//set the numerator to all disabled
				enabledIons[0].resize(rsdIncoming->rangeFile->getNumIons(),0);
				//set the denominator to have all enabled
				enabledIons[1].resize(rsdIncoming->rangeFile->getNumIons(),1);
			}
		}

	}
}

unsigned int VoxeliseFilter::refresh(const std::vector<const FilterStreamData *> &dataIn,
		  std::vector<const FilterStreamData *> &getOut, ProgressData &progress)
{
	//Disallow copying of anything in the blockmask. Copy everything else
	propagateStreams(dataIn,getOut,getRefreshBlockMask(),true);
	
	//use the cached copy if we have it.
	if(cacheOK)
	{
		propagateCache(getOut);
		progress.filterProgress=100;
		progress.step=progress.maxStep=1;
		return 0;
	}

	progress.step = 1;
	progress.maxStep=2;
	if(filterMode != VOXELISE_FILTERTYPE_NONE)
		progress.maxStep++;
	
	progress.stepName=TRANS("Counting");

	Voxels<float> voxelData;
	Voxels<bool> voxelMask;
	if(!voxelCache.size())
	{
		Point3D minP,maxP;

		bc.setInverseLimits();
		
		//FIXME: this is technically wrong in some edge cases. 
		// We disallow pair-wise points, even though if taken in conjunction
		// they might form a valid bounding box	with other points not in the same
		// data stream
		for (size_t i = 0; i < dataIn.size(); i++) 
		{
			//Check for ion stream types. Block others from propagation.
			if (dataIn[i]->getStreamType() != STREAM_TYPE_IONS) continue;

			const IonStreamData *is = (const IonStreamData *)dataIn[i];
			//Don't work on empty or single object streams (bounding box needs to be defined)
			if (is->getNumBasicObjects() < 2) continue;
		
			BoundCube bcTmp;
			IonHit::getBoundCube(is->data,bcTmp);

			//Bounds could be invalid if, for example, we had coplanar axis aligned points
			if (!bcTmp.isValid()) continue;

			bc.expand(bcTmp);
		}
		//No bounding box? Tough cookies
		if (!bc.isValid() || bc.isFlat()) return VOXELISE_BOUNDS_INVALID_ERR;

		bc.getBounds(minP,maxP);	
		//Disallow empty bounding boxes (ie, produce no output)
		if(minP == maxP)
			return 0;

		if (fixedWidth) 
			calculateNumBinsFromWidths(binWidth, nBins);
		else
			calculateWidthsFromNumBins(binWidth, nBins);
		
	
		//Rebuild the voxels from the point data
		Voxels<float> vsDenom;
		voxelData.init(nBins[0], nBins[1], nBins[2], bc);
		voxelData.fill(0);
		
		//If we are normalising using count data, then initialise the denominator with zeros	
		if (normaliseType == VOXELISE_NORMALISETYPE_COUNT2INVOXEL ||
			normaliseType == VOXELISE_NORMALISETYPE_ALLATOMSINVOXEL) {
			//Check we actually have incoming data
			ASSERT(rsdIncoming);
			vsDenom.init(nBins[0], nBins[1], nBins[2], bc);
			vsDenom.fill(0);
		}

		const IonStreamData *is;
		//Update totalsize of ion data
		size_t totalSize=getTotalSizeByType(dataIn,STREAM_TYPE_IONS);
		if(rsdIncoming)
		{


			size_t currentSize=0;
			for (size_t i = 0; i < dataIn.size(); i++) 
			{
				
				//Check for ion stream types. Don't use anything else in counting
				if (dataIn[i]->getStreamType() != STREAM_TYPE_IONS) continue;
				
				is= (const IonStreamData *)dataIn[i];

				
				//Count the numerator ions	
				if(is->data.size())
				{
					//Check what Ion type this stream belongs to. Assume all ions
					//in the stream belong to the same group
					unsigned int ionID;
					ionID = getIonstreamIonID(is,rsdIncoming->rangeFile);

					bool thisIonEnabled;
					if(ionID!=(unsigned int)-1)
						thisIonEnabled=enabledIons[0][ionID];
					else
						thisIonEnabled=false;

					if(thisIonEnabled)
						countPoints(voxelData,is->data,true);


				}

			
				//If the user requests normalisation, compute the denominator dataset
				if (normaliseType == VOXELISE_NORMALISETYPE_COUNT2INVOXEL) {
					if(is->data.size())
					{
						//Check what Ion type this stream belongs to. Assume all ions
						//in the stream belong to the same group
						unsigned int ionID;
						ionID = rsdIncoming->rangeFile->getIonID(is->data[0].getMassToCharge());

						bool thisIonEnabled;
						if(ionID!=(unsigned int)-1)
							thisIonEnabled=enabledIons[1][ionID];
						else
							thisIonEnabled=false;

						if(thisIonEnabled)
							countPoints(vsDenom,is->data,true);
					}
				} else if (normaliseType == VOXELISE_NORMALISETYPE_ALLATOMSINVOXEL)
				{
					countPoints(vsDenom,is->data,true);
				}

				if(*Filter::wantAbort)
					return VOXELISE_ABORT_ERR;

				//update progress
				currentSize+=is->data.size();
				progress.filterProgress = ((float)currentSize/(float)totalSize*100.0f);

			}
		

			//Perform post-calcs, if needed (e.g. normalisation)
			if (normaliseType == VOXELISE_NORMALISETYPE_VOLUME)
			{
				if(thresholdMaskEnable)
					voxelData.thresholdToBoolMask(threshMaskValue,true,voxelMask);
				voxelData.calculateDensity();
			}
			else if (normaliseType == VOXELISE_NORMALISETYPE_COUNT2INVOXEL ||
					 normaliseType == VOXELISE_NORMALISETYPE_ALLATOMSINVOXEL)
			{
				//Generate a mask if needed, using the denominator ions	
				if(thresholdMaskEnable)
					vsDenom.thresholdToBoolMask(threshMaskValue,true,voxelMask);

				voxelData /= vsDenom;
			}
			else
			{
				//Generate a mask if needed, using the denominator ions	
				if(thresholdMaskEnable)
					voxelData.thresholdToBoolMask(threshMaskValue,true,voxelMask);
			}
		}
		else
		{
			//No range data.  Just count
			size_t currentSize=0;
			for (size_t i = 0; i < dataIn.size(); i++) 
			{
				
				if(dataIn[i]->getStreamType() == STREAM_TYPE_IONS)
				{
					is= (const IonStreamData *)dataIn[i];

					countPoints(voxelData,is->data,true);
					
					if(*Filter::wantAbort)
						return VOXELISE_ABORT_ERR;

					currentSize+=is->data.size();
				}

				
				//update progress
				progress.filterProgress = ((float)currentSize/(float)totalSize*100.0f);
			}
			ASSERT(normaliseType != VOXELISE_NORMALISETYPE_COUNT2INVOXEL
					&& normaliseType!=VOXELISE_NORMALISETYPE_ALLATOMSINVOXEL);

			//Compute threshold mask if needed
			if(thresholdMaskEnable) 	
				voxelData.thresholdToBoolMask(threshMaskValue,true,voxelMask);

			//Compute normalisations if needed
			if (normaliseType == VOXELISE_NORMALISETYPE_VOLUME)
				voxelData.calculateDensity();
		}	
		progress.filterProgress=100;

		vsDenom.clear();
		
		//Perform threshold operations as needed
		if(thresholdMaskEnable)
		{
			//Apply the threshold, replacing with max or min value as appropriate
			float replaceValue;
			if(threshMaskToMax)
				replaceValue=voxelData.max();
			else
				replaceValue=voxelData.min();

			voxelData.applyMask(voxelMask,replaceValue);
		}

		//Perform voxel filtering
		switch(filterMode)
		{
			case VOXELISE_FILTERTYPE_NONE:
				break;
			case VOXELISE_FILTERTYPE_GAUSS:
			{	
				progress.stepName=TRANS("Gauss Smooth");
				voxelData.isotropicGaussianSmooth(gaussDev,filterKernelSize);
				break;
			}
			case VOXELISE_FILTERTYPE_LAPLACE:
			{
				progress.stepName=TRANS("Calc. Laplace");
				voxelData.laplaceOfGaussian(gaussDev,filterKernelSize);
				break;
			}
			default:
				ASSERT(false);
		}
	
		voxelCache=voxelData;
	}
	else
	{
		//Use the cached value
		voxelData=voxelCache;
		progress.filterProgress=100;
		if(filterMode!=VOXELISE_FILTERTYPE_NONE)
			progress.step++;	
	}
	
	float min,max;
	voxelData.minMax(min,max);


	string sMin,sMax;
	stream_cast(sMin,min);
	stream_cast(sMax,max);
	appendConsoleMessage(std::string(TRANS("Voxel Limits (min,max): (") + sMin + string(","))
		       	+  sMax + ")");


	//Update the bounding cube
	Point3D pb1,pb2;
	voxelData.getBounds(pb1,pb2);
	lastBounds.setBounds(pb1,pb2);

	progress.step++;
	progress.stepName=TRANS("Representation");

	//Obtain the visual representation from the voxel appearance code
	// NOTE: voxAppearance modifies this class's cache state!
	voxAppearance.refresh(voxelData,getOut, devices);
	
	//Copy the inputs into the outputs, provided they are not voxels
	return 0;
}

void VoxeliseFilter::setPropFromBinding(const SelectionBinding &b)
{
	//TODO: Refactor - This shares code with voxlLoadFilter
	switch(b.getID())
	{
		case BINDING_PLANE_ORIGIN:
		{
			switch(voxAppearance.representation )
			{
				case VOXEL_REPRESENT_AXIAL_SLICE:
				{
					ASSERT(lastBounds.isValid());
				
					//Convert the world coordinate value into a
					// fractional value of voxel bounds
					Point3D p;
					float f;
					b.getValue(p);
					f=p[voxAppearance.sliceAxis];

					float minB,maxB;
					minB = lastBounds.getBound(voxAppearance.sliceAxis,0);
					maxB = lastBounds.getBound(voxAppearance.sliceAxis,1);
					voxAppearance.sliceOffset= (f -minB)/(maxB-minB);
					
					voxAppearance.sliceOffset=std::min(
							voxAppearance.sliceOffset,1.0f);
					voxAppearance.sliceOffset=std::max(
							voxAppearance.sliceOffset,0.0f);
					ASSERT(voxAppearance.sliceOffset<=1 && voxAppearance.sliceOffset>=0);
					break;
				}
				case VOXEL_REPRESENT_SLICE:
				{
					Point3D p;
					b.getValue(p);
					Point3D pb1,pb2;
					lastBounds.getBounds(pb1,pb2);
					//The slice plane is defiend using fractional
					// coordinates
					voxAppearance.freeSlicePlane = (p- pb1)/(pb2-pb1);
					break;
				}
				default:

					ASSERT(false);
			}
			break;
		}
		case BINDING_PLANE_DIRECTION:
		{
			//Normal change should only be set for free-slice
			ASSERT(voxAppearance.representation == VOXEL_REPRESENT_SLICE);
			Point3D p;
			b.getValue(p);
			p.normalise();

			voxAppearance.freeSliceNormal =p;
			break;
		}
		default:
			ASSERT(false);
	}

}

std::string VoxeliseFilter::getNormaliseTypeString(int type){
	ASSERT(type < VOXELISE_NORMALISETYPE_MAX);
	return TRANS(NORMALISE_TYPE_STRING[type]);
}


std::string VoxeliseFilter::getFilterTypeString(int type)
{
	ASSERT(type < VOXELISE_FILTERTYPE_MAX);
	return std::string(TRANS(VOXELISE_FILTER_TYPE_STRING[type]));
}


void VoxeliseFilter::getProperties(FilterPropGroup &propertyList) const
{
	FilterProperty p;
	size_t curGroup=0;

	FilterProperty pFixedWidth(KEY_FIXEDWIDTH, TRANS("Fixed Width"),
		 fixedWidth,TRANS("If true, use fixed size voxels, otherwise use fixed count"));
	propertyList.addProperty(pFixedWidth,curGroup);

	if(fixedWidth)
	{
		FilterProperty pWidthX(KEY_WIDTHBINSX, TRANS("Bin Width, X"),
			 binWidth[0],TRANS("Voxel size in X direction"));
		propertyList.addProperty(pWidthX,curGroup);
		
		FilterProperty pWidthY(KEY_WIDTHBINSY, TRANS("Bin Width, Y"),
			 binWidth[1],TRANS("Voxel size in Y direction"));
		propertyList.addProperty(pWidthY,curGroup);

		FilterProperty pWidthZ(KEY_WIDTHBINSZ, TRANS("Bin Width, Z"),
			 binWidth[2],TRANS("Voxel size in Z direction"));
		propertyList.addProperty(pWidthZ,curGroup);
	}
	else
	{
		FilterProperty pNBinsX(KEY_NBINSX, TRANS("Num Bins , X"),
			 (unsigned int)nBins[0],TRANS("Number of bins in X direction"));
		propertyList.addProperty(pNBinsX,curGroup);
		
		FilterProperty pNBinsY(KEY_NBINSY, TRANS("Num Bins, Y"),
			 (unsigned int) nBins[1],TRANS("Number of bins in Y direction"));
		propertyList.addProperty(pNBinsY,curGroup);

		FilterProperty pNBinsZ(KEY_NBINSZ, TRANS("Num Bins, Z"),
			 (unsigned int)nBins[2],TRANS("Number of bins Z direction"));
		propertyList.addProperty(pNBinsZ,curGroup);
	}

	//Let the user know what the valid values for voxel value types are
	vector<pair<unsigned int,string> > choices;
	std::string tmpStr;
	unsigned int defaultChoice=normaliseType;
	tmpStr=getNormaliseTypeString(VOXELISE_NORMALISETYPE_NONE);
	choices.emplace_back((unsigned int)VOXELISE_NORMALISETYPE_NONE,tmpStr);
	tmpStr=getNormaliseTypeString(VOXELISE_NORMALISETYPE_VOLUME);
	choices.emplace_back((unsigned int)VOXELISE_NORMALISETYPE_VOLUME,tmpStr);
	if(rsdIncoming)
	{
		//Concentration mode
		tmpStr=getNormaliseTypeString(VOXELISE_NORMALISETYPE_ALLATOMSINVOXEL);
		choices.emplace_back((unsigned int)VOXELISE_NORMALISETYPE_ALLATOMSINVOXEL,tmpStr);
		//Ratio is only valid if we have a way of separation for the ions i.e. range
		tmpStr=getNormaliseTypeString(VOXELISE_NORMALISETYPE_COUNT2INVOXEL);
		choices.emplace_back((unsigned int)VOXELISE_NORMALISETYPE_COUNT2INVOXEL,tmpStr);
	}
	else
	{
		//prevent the case where we used to have an incoming range stream, but now we don't.
		// selected item within choice string must still be valid
		if(normaliseType > VOXELISE_NORMALISETYPE_VOLUME)
			defaultChoice= VOXELISE_NORMALISETYPE_NONE;
		
	}

	FilterProperty pNormaliseBy(KEY_NORMALISE_TYPE, TRANS("Normalise by"),
		 choices,defaultChoice,TRANS("Method to use to normalise scalar value in each voxel"));
	propertyList.addProperty(pNormaliseBy,curGroup);
	
	propertyList.setGroupTitle(curGroup,TRANS("Computation"));

	curGroup++;
	
	// numerator
	if (rsdIncoming) 
	{
		FilterProperty pEnableNumerator(KEY_ENABLE_NUMERATOR, TRANS("Numerator"),
			 numeratorAll,TRANS("Parameter \"a\" used in fraction (a/b) to get voxel value"));
		propertyList.addProperty(pEnableNumerator,curGroup);

		ASSERT(rsdIncoming->enabledIons.size()==enabledIons[0].size());	
		ASSERT(rsdIncoming->enabledIons.size()==enabledIons[1].size());	

		//Look at the numerator
		FilterProperty p;	
		for(unsigned  int ui=0; ui<rsdIncoming->enabledIons.size(); ui++)
		{
			string str;
			str=boolStrEnc(enabledIons[0][ui]);

			//Append the ion name with a checkbox
			p.name=rsdIncoming->rangeFile->getName(ui);
			p.data=str;
			p.type=PROPERTY_TYPE_BOOL;
			p.helpText=TRANS("Enable this ion for numerator");
			p.key=muxKey(KEY_ENABLE_NUMERATOR,ui);
			propertyList.addProperty(p,curGroup);
		}
	
		propertyList.setGroupTitle(curGroup,TRANS("Ranges"));
		curGroup++;
	}
	
	
	if (normaliseType == VOXELISE_NORMALISETYPE_COUNT2INVOXEL && rsdIncoming) 
	{

		FilterProperty pEnableDenominator(KEY_ENABLE_DENOMINATOR, TRANS("Denominator"),
			 denominatorAll,TRANS("Parameter \"b\" used in fraction (a/b) to get voxel value"));
		propertyList.addProperty(pEnableDenominator,curGroup);

		FilterProperty p;
		for(unsigned  int ui=0; ui<rsdIncoming->enabledIons.size(); ui++)
		{			
			string str;
			str=boolStrEnc(enabledIons[1][ui]);

			//Append the ion name with a checkbox
			p.key=muxKey(KEY_ENABLE_DENOMINATOR,ui);
			p.data=str;
			p.name=rsdIncoming->rangeFile->getName(ui);
			p.type=PROPERTY_TYPE_BOOL;
			p.helpText=TRANS("Enable this ion for denominator contribution");

			propertyList.addProperty(p,curGroup);
		}
		propertyList.setGroupTitle(curGroup,TRANS("Denominator"));
		curGroup++;
	}

	//Start a new set for filtering
	//----
	//TODO: Other filtering? threshold/median? laplacian? etc
	
	choices.clear();
	//Post-filtering method
	for(unsigned int ui=0;ui<VOXELISE_FILTERTYPE_MAX; ui++)
	{
		tmpStr=getFilterTypeString(ui);
		choices.emplace_back(ui,tmpStr);
	}

	FilterProperty pThresholding(KEY_THRESHOLD_MASK_ENABLE, TRANS("Threshold Mask"),
		thresholdMaskEnable,TRANS("Enable threshold based exclusion of values.")); 
	propertyList.addProperty(pThresholding,curGroup);

	if(thresholdMaskEnable)
	{
		FilterProperty pThreshValue(KEY_THRESHOLD_VALUE,TRANS("Count threshold"),
			threshMaskValue,TRANS("Threshold counts above which to keep data (if normalising, denominator)"));
		propertyList.addProperty(pThreshValue,curGroup);

		FilterProperty pThreshToMax(KEY_THRESHOLD_MASK_TOMAX,TRANS("Mask to Max"),
			threshMaskToMax,TRANS("If enabled, masked values are set to the voxel maximum. If disabled, minimum"));
		propertyList.addProperty(pThreshToMax,curGroup);
	}

	FilterProperty pFiltering(KEY_FILTER_MODE, TRANS("Filtering"),
		 choices,filterMode,TRANS("Smoothing method to use on voxels"));
	propertyList.addProperty(pFiltering,curGroup);
	choices.clear();

	propertyList.setGroupTitle(curGroup,TRANS("Processing"));
	if(filterMode != VOXELISE_FILTERTYPE_NONE)
	{

		FilterProperty pStdev(KEY_FILTER_STDEV, TRANS("Standard Dev"),
			 gaussDev,TRANS("Filtering Scale (standard deviation)"));
		propertyList.addProperty(pStdev,curGroup);

		FilterProperty pKernelSize(KEY_FILTER_RATIO, TRANS("Kernel Size"),
			 filterKernelSize,TRANS("Filter radius, in multiples of std. dev. Larger -> slower, more accurate"));
		propertyList.addProperty(pKernelSize,curGroup);

	}
	propertyList.setGroupTitle(curGroup,TRANS("Filtering"));
	curGroup++;
	//----

	//Obtain the appearance properties, specifying the key offset
	voxAppearance.getProperties(propertyList,curGroup,
			KEY_VOXEL_APPEARANCE_BASE);
	//----------------------------
}

bool VoxeliseFilter::setProperty(unsigned int key,
		  const std::string &value, bool &needUpdate)
{
	
	needUpdate=false;
	switch(key)
	{
		case KEY_FIXEDWIDTH: 
		{
			if(!applyPropertyNow(fixedWidth,value,needUpdate))
				return false;
			break;
		}	
		case KEY_NBINSX:
		case KEY_NBINSY:
		case KEY_NBINSZ:
		{
			//Cap the resolution to 20k
			const unsigned int MAX_NUMBINS=20000;
			size_t nB;
			stream_cast(nB,value);
			if(nB <1 || nB > MAX_NUMBINS)
				return false;

			if(!applyPropertyNow(nBins[key-KEY_NBINSX],value,needUpdate))
				return false;
			if(bc.isValid())
				calculateWidthsFromNumBins(binWidth, nBins);
			break;
		}
		case KEY_WIDTHBINSX:
		case KEY_WIDTHBINSY:
		case KEY_WIDTHBINSZ:
		{
			float dx;
			stream_cast(dx,value);
			if(dx <std::numeric_limits<float>::epsilon())
				return false;
			if(!applyPropertyNow(binWidth[key-KEY_WIDTHBINSX],value,needUpdate))
				return false;
			if(bc.isValid())
				calculateNumBinsFromWidths(binWidth, nBins);
			break;
		}
		case KEY_NORMALISE_TYPE:
		{
			unsigned int i;
			for(i = 0; i < VOXELISE_NORMALISETYPE_MAX; i++)
				if (value == getNormaliseTypeString(i)) break;
			if (i == VOXELISE_NORMALISETYPE_MAX)
				return false;
			if(normaliseType!=i)
			{
				needUpdate=true;
				clearCache();
				normaliseType=i;
			}
			break;
		}
		case KEY_ENABLE_NUMERATOR:
		{
			bool b;
			if(stream_cast(b,value))
				return false;
			//Set them all to enabled or disabled as a group	
			for (size_t i = 0; i < enabledIons[0].size(); i++) 
				enabledIons[0][i] = b;
			numeratorAll = b;
			needUpdate=true;
			clearCache();
			break;
		}
		case KEY_ENABLE_DENOMINATOR:
		{
			bool b;
			if(stream_cast(b,value))
				return false;
	
			//Set them all to enabled or disabled as a group	
			for (size_t i = 0; i < enabledIons[1].size(); i++) 
				enabledIons[1][i] = b;
			
			denominatorAll = b;
			needUpdate=true;			
			clearCache();
			break;
		}
		case KEY_FILTER_MODE:
		{
			//Locate the current string
			unsigned int i;
			for (i = 0; i < VOXELISE_FILTERTYPE_MAX; i++)
			{
				if (value == getFilterTypeString(i)) 
					break;
			}
			if (i == VOXELISE_FILTERTYPE_MAX)
				return false;
			if(i!=filterMode)
			{
				needUpdate=true;
				filterMode=i;
				clearCache();
			}
			break;
		}
		case KEY_FILTER_RATIO:
		{
			float i;
			if(stream_cast(i,value))
				return false;
			//forbid negative sizes
			if(i <= 0)
				return false;
			if(i != filterKernelSize)
			{
				needUpdate=true;
				filterKernelSize=i;
				clearCache();
			}
			break;
		}
		case KEY_FILTER_STDEV:
		{
			float i;
			if(stream_cast(i,value))
				return false;
			//forbid negative sizes
			if(i <= 0)
				return false;
			if(i != gaussDev)
			{
				needUpdate=true;
				gaussDev=i;
				clearCache();
			}
			break;
		}
		case KEY_THRESHOLD_MASK_ENABLE:
		{
			if(!applyPropertyNow(thresholdMaskEnable,value,needUpdate))
				return false;
			break;
		}
		case KEY_THRESHOLD_VALUE:
		{
			if(!applyPropertyNow(threshMaskValue,value,needUpdate))
				return false;
			break;
		}
		case KEY_THRESHOLD_MASK_TOMAX:
		{
			if(!applyPropertyNow(threshMaskToMax,value,needUpdate))
				return false;
			break;
		}
		default:
		{
			bool returnV,handled;
			returnV=voxAppearance.setProperty(KEY_VOXEL_APPEARANCE_BASE,key,handled,
					value,cacheOK,needUpdate,filterOutputs);
			if(handled)
				return returnV;

			unsigned int subKeyType,offset;
			demuxKey(key,subKeyType,offset);
			
			//Check for jump to denominator or numerator section
			// TODO: This is a bit of a hack.
			if (subKeyType==KEY_ENABLE_DENOMINATOR) {
				bool b;
				if(!boolStrDec(value,b))
					return false;

				enabledIons[1][offset]=b;
				if (!b) {
					denominatorAll = false;
				}
				needUpdate=true;			
				clearCache();
			} else if (subKeyType == KEY_ENABLE_NUMERATOR) {
				bool b;
				if(!boolStrDec(value,b))
					return false;
				
				enabledIons[0][offset]=b;
				if (!b) {
					numeratorAll = false;
				}
				needUpdate=true;			
					clearCache();
			}
			else
			{
				ASSERT(false);
			}
			break;
		}
	}
	return true;
}

std::string  VoxeliseFilter::getSpecificErrString(unsigned int code) const
{
	const char *errStrs[]={
	 	"",
		"Voxelisation aborted",
		"Out of memory",
		"Unable to perform filter convolution",
		"Voxelisation bounds are invalid",
	};
	COMPILE_ASSERT(THREEDEP_ARRAYSIZE(errStrs) == VOXELISE_ERR_ENUM_END);	
	
	ASSERT(code < VOXELISE_ERR_ENUM_END);
	return errStrs[code];
}

bool VoxeliseFilter::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) << "<fixedwidth value=\""<<fixedWidth << "\"/>"  << endl;
			f << tabs(depth+1) << "<nbins values=\""<<nBins[0] << ","<<nBins[1]<<","<<nBins[2] << "\"/>"  << endl;
			f << tabs(depth+1) << "<binwidth values=\""<<binWidth[0] << ","<<binWidth[1]<<","<<binWidth[2] << "\"/>"  << endl;
			f << tabs(depth+1) << "<normalisetype value=\""<< normaliseType << "\"/>" << endl;
			f << tabs(depth+1) << "<countmask enable=\"" << boolStrEnc(thresholdMaskEnable) << "\" value=\"" <<
					threshMaskValue << "\" tomax=\"" << boolStrEnc(threshMaskToMax) << "\"/>" << endl;
			f << tabs(depth+1) << "<filter mode=\""<<filterMode << 
				"\" kernelsize=\"" <<  filterKernelSize  << "\" stddev=\"" << gaussDev << "\"/>"  << endl;
			f << tabs(depth+1) << "<enabledions>" << endl;

			f << tabs(depth+2) << "<numerator>" << endl;
			for(unsigned int ui=0;ui<enabledIons[0].size(); ui++)
				f << tabs(depth+3) << "<enabled value=\"" << boolStrEnc(enabledIons[0][ui]) << "\"/>" << endl;
			f << tabs(depth+2) << "</numerator>" << endl;

			f << tabs(depth+2) << "<denominator>" << endl;
			for(unsigned int ui=0;ui<enabledIons[1].size(); ui++)
				f << tabs(depth+3) << "<enabled value=\"" << boolStrEnc(enabledIons[1][ui]) << "\"/>" << endl;
			f << tabs(depth+2) << "</denominator>" << endl;

			f << tabs(depth+1) << "</enabledions>" << endl;

			voxAppearance.writeState(f,format,depth);

			f << tabs(depth) << "</" << trueName() <<">" << endl;
			break;
		}
		default:
			ASSERT(false);
			return false;
	}
	
	return true;
}

bool VoxeliseFilter::readState(xmlNodePtr &nodePtr, const std::string &stateFileDir)
{
	using std::string;
	string tmpStr;
	xmlChar *xmlString;
	stack<xmlNodePtr> nodeStack;

	//Retrieve user string
	//===
	if(XMLHelpFwdToElem(nodePtr,"userstring"))
		return false;

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

	//Retrieve fixedWidth mode
	if(!XMLGetNextElemAttrib(nodePtr,tmpStr,"fixedwidth","value"))
		return false;
	if(!boolStrDec(tmpStr,fixedWidth))
		return false;
	
	//Retrieve nBins	
	if(XMLHelpFwdToElem(nodePtr,"nbins"))
		return false;
	xmlString=xmlGetProp(nodePtr,(const xmlChar *)"values");
	if(!xmlString)
		return false;
	std::vector<string> v1;
	splitStrsRef((char *)xmlString,',',v1);
	for (size_t i = 0; i < INDEX_LENGTH && i < v1.size(); i++)
	{
		if(stream_cast(nBins[i],v1[i]))
			return false;
		
		if(nBins[i] == 0)
			return false;
	}
	xmlFree(xmlString);
	
	//Retrieve bin width 
	if(XMLHelpFwdToElem(nodePtr,"binwidth"))
		return false;
	xmlString=xmlGetProp(nodePtr,(const xmlChar *)"values");
	if(!xmlString)
		return false;
	std::vector<string> v2;
	splitStrsRef((char *)xmlString,',',v2);
	for (size_t i = 0; i < INDEX_LENGTH && i < v2.size(); i++)
	{
		if(stream_cast(binWidth[i],v2[i]))
			return false;
		
		if(binWidth[i] <= 0)
			return false;
	}
	xmlFree(xmlString);

	//FIXME: COMPAT_BREAK : This was not recorded prior to 0.0.21 (old bug), so older
	// statefiles will not have this data. Do not abort if we can't find it.
	{
		xmlNodePtr tmpPtr;
		tmpPtr =nodePtr;

		if(!XMLHelpFwdToElem(tmpPtr, "filter"))
		{
			if(!XMLGetAttrib(tmpPtr,filterMode,"mode"))
				return false;
			if(!XMLGetAttrib(tmpPtr,filterKernelSize,"kernelsize"))
				return false;
			if(!XMLGetAttrib(tmpPtr,gaussDev,"stddev"))
				return false;
		}
	}


	//Retrieve normaliseType
	if(!XMLGetNextElemAttrib(nodePtr,normaliseType,"normalisetype","value"))
		return false;
	if(normaliseType >= VOXELISE_NORMALISETYPE_MAX)
		return false;

	//FIXME: COMPAT_BREAK : This feature did not exist < 0.0.22. Disabled by default
	{
		xmlNodePtr tmpPtr;
		tmpPtr=nodePtr;
		if(!XMLHelpFwdToElem(tmpPtr,"countmask"))
		{
			if(!XMLGetAttrib(tmpPtr,thresholdMaskEnable,"enable"))
				return false;
			if(!XMLGetAttrib(tmpPtr,threshMaskValue,"value"))
				return false;
			if(!XMLGetAttrib(tmpPtr,threshMaskToMax,"tomax"))
				return false;
		}
	}


	//Look for the enabled ions bit
	//-------	
	//
	
	if(!XMLHelpFwdToElem(nodePtr,"enabledions"))
	{

		nodeStack.push(nodePtr);
		if(!nodePtr->xmlChildrenNode)
			return false;
		nodePtr=nodePtr->xmlChildrenNode;
		
		//enabled ions for numerator
		if(XMLHelpFwdToElem(nodePtr,"numerator"))
			return false;

		nodeStack.push(nodePtr);

		if(!nodePtr->xmlChildrenNode)
			return false;

		nodePtr=nodePtr->xmlChildrenNode;

		while(nodePtr)
		{
			char c;
			//Retrieve enabled state 
			if(!XMLGetNextElemAttrib(nodePtr,c,"enabled","value"))
				break;

			if(c == '1')
				enabledIons[0].push_back(true);
			else
				enabledIons[0].push_back(false);


			nodePtr=nodePtr->next;
		}

		nodePtr=nodeStack.top();
		nodeStack.pop();

		//enabled ions for denominator
		if(XMLHelpFwdToElem(nodePtr,"denominator"))
			return false;


		if(!nodePtr->xmlChildrenNode)
			return false;

		nodeStack.push(nodePtr);
		nodePtr=nodePtr->xmlChildrenNode;

		while(nodePtr)
		{
			char c;
			//Retrieve enabled state (denom) 
			if(!XMLGetNextElemAttrib(nodePtr,c,"enabled","value"))
				break;

			if(c == '1')
				enabledIons[1].push_back(true);
			else
				enabledIons[1].push_back(false);
				

			nodePtr=nodePtr->next;
		}


		nodeStack.pop();
		nodePtr=nodeStack.top();
		nodeStack.pop();

		//Check that the enabled ions size makes at least some sense...
		if(enabledIons[0].size() != enabledIons[1].size())
			return false;

	}

	//-------
	if(!voxAppearance.readState(nodePtr))
		return false;

	return true;
	
}

unsigned int VoxeliseFilter::getRefreshBlockMask() const
{
	//Ions, plots and voxels cannot pass through this filter
	return STREAM_TYPE_IONS | STREAM_TYPE_PLOT | STREAM_TYPE_VOXEL;
}

unsigned int VoxeliseFilter::getRefreshEmitMask() const
{
	return STREAM_TYPE_VOXEL | STREAM_TYPE_DRAW;
}

unsigned int VoxeliseFilter::getRefreshUseMask() const
{
	return STREAM_TYPE_IONS | STREAM_TYPE_RANGE;
}


#ifdef ENABLE_LIBVD 
bool VoxeliseFilter::curConfigurationIsExperimental() const
{
	//Volume rendering requires libvd, which is not reliable from computer to computer.
	return voxAppearance.representation == VOXEL_REPRESENT_VOLUME_RENDER;
}
#endif
 

#ifdef DEBUG
bool voxelSingleCountTest()
{
	//Test counting a single vector
	
	vector<IonHit> ionVec;

	ionVec.resize(5);
	ionVec[0].setPos(Point3D(0.1,0.1,0.1));
	ionVec[1].setPos(Point3D(0.1,0.0,0.1));
	ionVec[2].setPos(Point3D(0.0,0.1,0.1));
	ionVec[3].setPos(Point3D(0.1,0.1,0.0));
	ionVec[4].setPos(Point3D(0.0,0.1,0.0));

	for(unsigned int ui=0;ui<ionVec.size();ui++)
		ionVec[ui].setMassToCharge(1);

	IonStreamData *ionData = new IonStreamData;
	std::swap(ionData->data,ionVec);
	
	size_t numIons=ionData->data.size();
	
	VoxeliseFilter *f = new VoxeliseFilter;
	f->setCaching(false);

	bool needUpdate;
	TEST(f->setProperty(KEY_NBINSX,"4",needUpdate),"num bins x");
	TEST(f->setProperty(KEY_NBINSY,"4",needUpdate),"num bins y");
	TEST(f->setProperty(KEY_NBINSZ,"4",needUpdate),"num bins z");


	vector<const FilterStreamData*> streamIn,streamOut;
	streamIn.push_back(ionData);

	ProgressData p;
	TEST(!f->refresh(streamIn,streamOut,p),"Refresh error code");
	delete f;

	TEST(streamOut.size() == 1,"stream count");
	TEST(streamOut[0]->getStreamType() == STREAM_TYPE_VOXEL,"Stream type");


	const VoxelStreamData *v= (const VoxelStreamData*)streamOut[0];

	TEST(v->data->max() <=numIons,
			"voxel max less than input stream")

	TEST(v->data->min() >= 0.0f,"voxel counting minimum sanity");

	
	float dataSum;
	sumVoxels(*(v->data),dataSum);
	TEST(fabs(dataSum - (float)numIons ) < 
		sqrtf(std::numeric_limits<float>::epsilon()),"voxel counting all input ions ");

	delete ionData;
	delete streamOut[0];

	return true;
}

bool voxelMultiCountTest()
{
	//Test counting multiple data streams containing ranged data 
	
	vector<const FilterStreamData*> streamIn,streamOut;
	vector<IonHit> ionVec;

	ionVec.resize(5);
	ionVec[0].setPos(Point3D(0.1,0.1,0.1));
	ionVec[1].setPos(Point3D(0.1,0.0,0.1));
	ionVec[2].setPos(Point3D(0.0,0.1,0.1));
	ionVec[3].setPos(Point3D(0.1,0.1,0.0));
	ionVec[4].setPos(Point3D(0.0,0.1,0.0));

	IonStreamData *ionData[2];
	RangeStreamData *rngStream;
	rngStream = new RangeStreamData;
	rngStream->rangeFile= new RangeFile;

	RGBf col; col.red=col.green=col.blue=1.0f;

	//create several input ion streams, each
	//containing the above data, but with differeing
	//mass to charge values.
	// - we range this data though!
	const unsigned int MAX_NUM_RANGES=2;
	for(unsigned int ui=0;ui<MAX_NUM_RANGES;ui++)
	{
		size_t ionNum;

		//Add a new ion "a1, a2... etc"
		string sTmp,sTmp2;
		sTmp="a";
		stream_cast(sTmp2,ui);
		sTmp+=sTmp2;
		ionNum=rngStream->rangeFile->addIon(sTmp,sTmp,col);
		rngStream->rangeFile->addRange((float)ui-0.5f,(float)ui+0.5f,ionNum);

		//Change m/c value for ion
		for(unsigned int uj=0;uj<ionVec.size();uj++)
			ionVec[uj].setMassToCharge(ui);
		
		ionData[ui]= new IonStreamData;
		ionData[ui]->data.resize(ionVec.size());
		std::copy(ionVec.begin(),ionVec.end(),ionData[ui]->data.begin());
		streamIn.push_back(ionData[ui]);
	}

	rngStream->enabledIons.resize(rngStream->rangeFile->getNumIons());
	rngStream->enabledRanges.resize(rngStream->rangeFile->getNumRanges());

	streamIn.push_back(rngStream);

	VoxeliseFilter *f = new VoxeliseFilter;

	//Initialise range data
	f->initFilter(streamIn,streamOut);


	f->setCaching(false);
	
	bool needUpdate;
	TEST(f->setProperty(KEY_NBINSX,"4",needUpdate),"num bins x");
	TEST(f->setProperty(KEY_NBINSY,"4",needUpdate),"num bins y");
	TEST(f->setProperty(KEY_NBINSZ,"4",needUpdate),"num bins z");


	TEST(f->setProperty(KEY_NORMALISE_TYPE,
		TRANS(NORMALISE_TYPE_STRING[VOXELISE_NORMALISETYPE_ALLATOMSINVOXEL]),needUpdate), 
				"Set normalise mode");

	ProgressData p;
	TEST(!f->refresh(streamIn,streamOut,p),"Refresh error code");
	delete f;
	for(unsigned int ui=0;ui<MAX_NUM_RANGES;ui++)
		delete streamIn[ui];
	TEST(streamOut.size() == 2,"stream count");
	TEST(streamOut[1]->getStreamType() == STREAM_TYPE_VOXEL,"Stream type");
	
	const VoxelStreamData *v= (const VoxelStreamData*)streamOut[1];

	TEST(v->data->max() <=1.0f,
			"voxel max less than input stream")
	TEST(v->data->min() >= 0.0f,"voxel counting minimum sanity");


	//all data should lie between 0 and 1
	for(unsigned int ui=0;ui<v->data->size();ui++)
	{
		float val;
		val=v->data->getData(ui);
		ASSERT(  val >= 0 && val <= 1.0f); 
	}

	delete v;

	delete rngStream->rangeFile;
	delete rngStream;

	return true;
}


bool VoxeliseFilter::runUnitTests()
{

	if(!voxelSingleCountTest())
		return false;

	if(!voxelMultiCountTest())
		return false;


	return true;
}

#endif
