package ij.plugin;
import ij.*;
import ij.gui.*;
import ij.process.*;
import ij.plugin.PlugIn;
import ij.measure.*;
import ij.util.Tools;
import java.util.*;
import java.awt.*;
import java.awt.event.*;
import java.util.Vector;

/**
This plugin implements the Analyze/Distribution command.
It reads the data from the ResultsTable and plots a frequency histogram.
@author G. Landini at bham. ac. uk
*/
public class Distribution implements PlugIn, TextListener {
	static String parameter = "Area";
	static boolean autoBinning = true;
	static int nBins = 10;
	static String range = "0-0";
	Checkbox checkbox;
	TextField nBinsField, rangeField;
	String defaultNBins, defaultRange;

	public void run(String arg) {
		ResultsTable rt=ResultsTable.getResultsTable();
		if (rt.size()==0) {
			IJ.error("Distribution", "The \"Results\" table is empty");
			return;
		}
		run(rt);
	}
	
	public void run(ResultsTable rt) {
		if (rt==null)
			return;
		int count = rt.size();
		String head = rt.getColumnHeadings();
		//IJ.log(head);

		StringTokenizer t = new StringTokenizer(head, "\t");
		int tokens = t.countTokens()-1;
		String[] strings = new String[tokens];
		strings[0] = t.nextToken(); // first token is empty?
	   	for(int i=0; i<tokens; i++)
			strings[i] = t.nextToken();

		defaultNBins = ""+nBins;
		defaultRange = range;
		GenericDialog gd = new GenericDialog("Distribution");
		gd.addChoice("Parameter: ", strings, strings[getIndex(strings)]);
		gd.setInsets(0, 40, 0);
		gd.addMessage(count+" data points", null, Color.darkGray);
		gd.addCheckbox("Automatic binning", autoBinning);
		gd.addNumericField ("or specify bins:", nBins, 0);
		gd.addStringField ("and range:", range);

		Vector v = gd.getNumericFields();
		if (v!=null) {
			nBinsField = (TextField)v.elementAt(0);
			nBinsField.addTextListener(this);
		}
		v = gd.getStringFields();
		if (v!=null) {
			rangeField = (TextField)v.elementAt(0);
			rangeField.addTextListener(this);
		}
		checkbox = (Checkbox)(gd.getCheckboxes().elementAt(0));
		gd.showDialog();
		if (gd.wasCanceled())
			return;

		parameter = gd.getNextChoice ();
		autoBinning = gd.getNextBoolean();
		double nMin=0.0, nMax=0.0;
		if (!autoBinning) {
			nBins = (int)gd.getNextNumber();
			range = gd.getNextString();
			// find hyphen and replace with comma
			String[] minAndMax = range.replaceAll("([0-9.])[\t ]*-", "$1,").split(",");
			nMin = Tools.parseDouble(minAndMax[0]);
			nMax = minAndMax.length==2?Tools.parseDouble(minAndMax[1]):Double.NaN;
			if (Double.isNaN(nMin) || Double.isNaN(nMax))
				{nMin=0.0; nMax=0.0; range="0-0";}
		}

		float[] data = null;
		int index = rt.getColumnIndex(parameter);
		if (index>=0)
			data = rt.getColumn(index);
		if (data==null) {
			IJ.error("Distribution", "No available results: \""+parameter+"\"");
			return;
		}

		float [] pars = new float [11];
		stats(count, data, pars);
		if (autoBinning) {
			//sd = 7, min = 3, max = 4
			// use Scott's method (1979 Biometrika, 66:605-610) for optimal binning: 3.49*sd*N^-1/3
			float binWidth = (float)(3.49 * pars[7]*(float)Math.pow((float)count, -1.0/3.0));
			nBins= (int)Math.floor(((pars[4]-pars[3])/binWidth)+.5);
			if (nBins<2) nBins = 2;
		}

		ImageProcessor ip = new FloatProcessor(count, 1, data, null);
		ImagePlus imp = new ImagePlus("", ip);
		ImageStatistics stats = new StackStatistics(imp, nBins, nMin, nMax);
		int maxCount = 0;
		for (int i=0; i<stats.histogram.length; i++) {
			if (stats.histogram[i]>maxCount)
				maxCount = stats.histogram[i];
		}
		stats.histYMax = maxCount;
		HistogramWindow hw = new HistogramWindow(parameter+" Distribution", imp, stats);
		try {
			ResultsTable rt2 = hw.getResultsTable();
			int col = rt2.getColumnIndex("bin start");
			float[] xvalues = rt2.getColumn(col);
			col = rt2.getColumnIndex("count");
			float[] yvalues = rt2.getColumn(col);
			ImagePlus img = hw.getImagePlus();
			img.setProperty("XValues", xvalues); // Allows values to be retrieved by
			img.setProperty("YValues", yvalues); // by Plot.getValues() macro function
		} catch (Exception e) {}
	}
	
	int getIndex(String[] strings) {
		for (int i=0; i<strings.length; i++) {
			if (strings[i].equals(parameter))
				return i;
		}
		return 0;
	}

	public void textValueChanged(TextEvent e) {
		if (!defaultNBins.equals(nBinsField.getText()))
			checkbox.setState(false);
		if (!defaultRange.equals(rangeField.getText()))
			checkbox.setState(false);
	}

	void stats(int nc, float[] data, float[] pars){
 // ("\tPoints\tEdges_n\tGraph_Length\tMin\tMax\tMean\tAvDev\tSDev\tVar\tSkew\tKurt");
		int i;
		float s = 0, min = Float.MAX_VALUE, max = -Float.MAX_VALUE, totl=0, ave=0, adev=0, sdev=0, var=0, skew=0, kurt=0, p;

		for(i=0;i<nc;i++){
			totl+= data[i];
			//tot& = tot& + 1
				if(data[i]<min) min = data[i];
			if(data[i]>max) max = data[i];
		}

		ave = totl/nc;

		for(i=0;i<nc;i++){
			s = data[i] - ave;
			adev+=Math.abs(s);
			p = s * s;
			var+= p;
			p*=s;
			skew+= p;
			p*= s;
			kurt+= p;
		}

		adev/= nc;
		var/=nc-1;
		sdev = (float) Math.sqrt(var);

		if(var> 0){
			skew = (float)skew / (nc * (float) Math.pow(sdev,3));
			kurt = (float)kurt / (nc * (float) Math.pow(var, 2)) - 3;
		}
		pars[1]=(float) nc;
		pars[2]=totl;
		pars[3]=min;
		pars[4]=max;
		pars[5]=ave;
		pars[6]=adev;
		pars[7]=sdev;
		pars[8]=var;
		pars[9]=skew;
		pars[10]=kurt;

	}

}
