/* -*- mode: java; c-basic-offset: 8; indent-tabs-mode: t; tab-width: 8 -*- */

/*-
 * #%L
 * Fiji distribution of ImageJ for the life sciences.
 * %%
 * Copyright (C) 2010 - 2020 Fiji developers.
 * %%
 * 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/gpl-3.0.html>.
 * #L%
 */

package tracing;

import java.awt.BorderLayout;
import java.awt.Button;
import java.awt.Checkbox;
import java.awt.CheckboxGroup;
import java.awt.Dialog;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Label;
import java.awt.Panel;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.TextField;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.TextEvent;
import java.awt.event.TextListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.awt.image.IndexColorModel;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;

import javax.swing.JButton;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;

import org.apache.batik.dom.GenericDOMImplementation;
import org.apache.batik.svggen.SVGGraphics2D;
import org.apache.commons.math3.stat.regression.SimpleRegression;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.LogAxis;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.StandardXYBarPainter;
import org.jfree.chart.renderer.xy.XYBarRenderer;
import org.jfree.chart.renderer.xy.XYItemRenderer;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.chart.title.TextTitle;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;

import ij.IJ;
import ij.ImagePlus;
import ij.ImageStack;
import ij.Prefs;
import ij.gui.GUI;
import ij.io.FileInfo;
import ij.io.SaveDialog;
import ij.measure.Calibration;
import ij.measure.ResultsTable;
import ij.plugin.filter.Analyzer;
import ij.process.ImageProcessor;
import ij.process.ShortProcessor;
import sholl.Sholl_Analysis;
import sholl.Sholl_Utils;
import util.FindConnectedRegions;

@SuppressWarnings("serial")
public class ShollAnalysisDialog extends Dialog implements WindowListener, ActionListener, TextListener, ItemListener {

	protected double x_start, y_start, z_start;

	protected CheckboxGroup pathsGroup = new CheckboxGroup();
	protected Checkbox useAllPathsCheckbox = new Checkbox("Use all paths in analysis?", pathsGroup, true);
	protected Checkbox useSelectedPathsCheckbox = new Checkbox("Use only selected paths in analysis?", pathsGroup,
			false);

	protected JButton swcTypesButton = new JButton("SWC Type Filtering...");
	protected JPopupMenu swcTypesMenu = new JPopupMenu();
	protected ArrayList<String> filteredTypes = Path.getSWCtypeNames();
	protected JLabel filteredTypesWarningLabel = new JLabel();

	protected Button makeShollImageButton = new Button("Sholl Image");
	protected Button exportProfileButton = new Button("Save Profile...");
	protected Button drawShollGraphButton = new Button("Preview Plot");
	protected Button analyzeButton = new Button("Analyze Profile (Sholl Analysis v" + Sholl_Utils.version() + ")...");

	protected int numberOfSelectedPaths;
	protected int numberOfAllPaths;

	protected CheckboxGroup axesGroup = new CheckboxGroup();
	protected Checkbox normalAxes = new Checkbox("Use standard axes", axesGroup, true);
	protected Checkbox semiLogAxes = new Checkbox("Use semi-log axes", axesGroup, false);
	protected Checkbox logLogAxes = new Checkbox("Use log-log axes", axesGroup, false);

	protected CheckboxGroup normalizationGroup = new CheckboxGroup();
	protected Checkbox noNormalization = new Checkbox("No normalization of intersections", normalizationGroup, true);
	protected Checkbox normalizationForSphereVolume = new Checkbox("[BUG: should be set in the constructor]",
			normalizationGroup, false);

	protected TextField sampleSeparation = new TextField(
			"" + Prefs.get("tracing.ShollAnalysisDialog.sampleSeparation", 0));

	protected ShollResults currentResults;
	protected ImagePlus originalImage;
	private String exportPath;

	GraphFrame graphFrame;

	@Override
	public void actionPerformed(final ActionEvent e) {
		final Object source = e.getSource();
		final ShollResults results;
		synchronized (this) {
			results = getCurrentResults();
		}
		if (results == null) {
			IJ.error("The sphere separation field must be a number, not '" + sampleSeparation.getText() + "'");
			return;
		}
		if (source == makeShollImageButton) {
			results.makeShollCrossingsImagePlus(originalImage);

		} else if (source == analyzeButton) {

			final Thread newThread = new Thread(new Runnable() {
				@Override
				public void run() {
					analyzeButton.setEnabled(false);
					analyzeButton.setLabel("Running Analysis. Please wait...");
					results.analyzeWithShollAnalysisPlugin(getExportPath(), shollpafm.getPathsStructured().length);
					analyzeButton.setLabel("Analyze Profile (Sholl Analysis v" + Sholl_Utils.version() + ")...");
					analyzeButton.setEnabled(true);
				}
			});
			newThread.start();
			return;

		} else if (source == exportProfileButton) {

			// We only only to save the detailed profile. Summary profile will
			// be handled by sholl.Sholl_Analysis

			final SaveDialog sd = new SaveDialog("Export data as...", getExportPath(),
					originalImage.getTitle() + "-sholl" + results.getSuggestedSuffix(), ".csv");

			if (sd.getFileName() == null) {
				return;
			}

			final File saveFile = new File(exportPath = sd.getDirectory(), sd.getFileName());
			if ((saveFile != null) && saveFile.exists()) {
				if (!IJ.showMessageWithCancel("Export data...",
						"The file " + saveFile.getAbsolutePath() + " already exists.\n" + "Do you want to replace it?"))
					return;
			}

			IJ.showStatus("Exporting CSV data to " + saveFile.getAbsolutePath());

			try {
				results.exportDetailToCSV(saveFile);
			} catch (final IOException ioe) {
				IJ.error("Saving to " + saveFile.getAbsolutePath() + " failed");
				return;
			}

		} else if (source == drawShollGraphButton) {
			graphFrame.setVisible(true);
		}
	}

	@Override
	public void textValueChanged(final TextEvent e) {
		final Object source = e.getSource();
		if (source == sampleSeparation) {
			final String sampleSeparationText = sampleSeparation.getText();
			float s;
			try {
				s = Float.parseFloat(sampleSeparationText);
			} catch (final NumberFormatException nfe) {
				return;
			}
			if (s >= 0)
				updateResults();
		}
	}

	protected synchronized void updateResults() {
		JFreeChart chart;

		if (numberOfAllPaths <= 0) {

			makePromptInteractive(false);
			if (graphFrame != null) {
				chart = graphFrame.chartPanel.getChart();
				if (chart != null) {
					chart.setNotify(false);
					final TextTitle currentitle = chart.getTitle();
					if (currentitle != null)
						currentitle.setText("");
					final XYPlot plot = chart.getXYPlot();
					if (plot != null)
						plot.setDataset(null);
					chart.setNotify(true);
				}
			}

		} else { // valid paths to be analyzed

			makePromptInteractive(true);
			final ShollResults results = getCurrentResults();
			resultsPanel.updateFromResults(results);
			chart = results.createGraph();
			if (chart == null)
				return;
			if (graphFrame == null)
				graphFrame = new GraphFrame(chart, results.getSuggestedSuffix());
			else
				graphFrame.updateWithNewChart(chart, results.getSuggestedSuffix());
		}

	}

	private void makePromptInteractive(final boolean interactive) {
		if (!interactive) {
			final String noData = " ";
			resultsPanel.criticalValuesLabel.setText(noData);
			resultsPanel.dendriteMaximumLabel.setText(noData);
			resultsPanel.shollsRegressionCoefficientLabel.setText(noData);
			resultsPanel.shollsRegressionInterceptLabel.setText(noData);
			resultsPanel.shollsRegressionRSquaredLabel.setText(noData);
			filteredTypesWarningLabel.setText("No paths matching current filter(s). Please revise choices...");
			filteredTypesWarningLabel.setForeground(java.awt.Color.RED);
		} else {
			filteredTypesWarningLabel.setText("" + filteredTypes.size() + " type(s) are currently selected");
			filteredTypesWarningLabel.setForeground(java.awt.Color.DARK_GRAY);
		}
		drawShollGraphButton.setEnabled(interactive);
		exportProfileButton.setEnabled(interactive);
		analyzeButton.setEnabled(interactive);
		makeShollImageButton.setEnabled(interactive);
	}

	@Override
	public void itemStateChanged(final ItemEvent e) {
		updateResults();
	}

	public ShollResults getCurrentResults() {
		List<ShollPoint> pointsToUse;
		String description = "Sholl analysis ";
		final String postDescription = " for " + originalImage.getTitle();
		final boolean useAllPaths = !useSelectedPathsCheckbox.getState();
		if (useAllPaths) {
			pointsToUse = shollPointsAllPaths;
			description += "of all paths" + postDescription;
		} else {
			pointsToUse = shollPointsSelectedPaths;
			description += "of selected paths " + postDescription;
		}

		int axes = 0;
		if (normalAxes.getState())
			axes = AXES_NORMAL;
		else if (semiLogAxes.getState())
			axes = AXES_SEMI_LOG;
		else if (logLogAxes.getState())
			axes = AXES_LOG_LOG;
		else
			throw new RuntimeException("BUG: somehow no axis checkbox was selected");

		int normalization = 0;
		if (noNormalization.getState())
			normalization = NOT_NORMALIZED;
		else if (normalizationForSphereVolume.getState())
			normalization = NORMALIZED_FOR_SPHERE_VOLUME;
		else
			throw new RuntimeException("BUG: somehow no normalization checkbox was selected");

		final String sphereSeparationString = sampleSeparation.getText();
		double sphereSeparation = Double.MIN_VALUE;

		try {
			sphereSeparation = Double.parseDouble(sphereSeparationString);
		} catch (final NumberFormatException nfe) {
			return null;
		}

		final ShollResults results = new ShollResults(pointsToUse, originalImage, useAllPaths,
				useAllPaths ? numberOfAllPaths : numberOfSelectedPaths, x_start, y_start, z_start, description, axes,
				normalization, sphereSeparation, twoDimensional);

		return results;
	}

	public static class GraphFrame extends JFrame implements ActionListener {
		JButton exportButton;
		JFreeChart chart = null;
		ChartPanel chartPanel = null;
		JPanel mainPanel;
		String suggestedSuffix;

		public void updateWithNewChart(final JFreeChart chart, final String suggestedSuffix) {
			updateWithNewChart(chart, suggestedSuffix, false);
		}

		synchronized public void updateWithNewChart(final JFreeChart chart, final String suggestedSuffix,
				final boolean setSize) {
			this.suggestedSuffix = suggestedSuffix;
			if (chartPanel != null)
				remove(chartPanel);
			chartPanel = null;
			this.chart = chart;
			chartPanel = new ChartPanel(chart);
			if (setSize)
				chartPanel.setPreferredSize(new java.awt.Dimension(800, 600));
			mainPanel.add(chartPanel, BorderLayout.CENTER);
			validate();
		}

		public GraphFrame(final JFreeChart chart, final String suggestedSuffix) {
			super();

			this.suggestedSuffix = suggestedSuffix;

			mainPanel = new JPanel();
			mainPanel.setLayout(new BorderLayout());

			updateWithNewChart(chart, suggestedSuffix, true);

			final JPanel buttonsPanel = new JPanel();
			exportButton = new JButton("Export graph as SVG");
			exportButton.addActionListener(this);
			buttonsPanel.add(exportButton);
			mainPanel.add(buttonsPanel, BorderLayout.SOUTH);

			setContentPane(mainPanel);
			validate();
			setSize(new java.awt.Dimension(500, 270));
			GUI.center(this);
		}

		@Override
		public void actionPerformed(final ActionEvent e) {
			final Object source = e.getSource();
			if (source == exportButton) {
				exportGraphAsSVG();
			}
		}

		public void exportGraphAsSVG() {

			final SaveDialog sd = new SaveDialog("Export graph as...", "sholl" + suggestedSuffix, ".svg");

			if (sd.getFileName() == null) {
				return;
			}

			final File saveFile = new File(sd.getDirectory(), sd.getFileName());
			if ((saveFile != null) && saveFile.exists()) {
				if (!IJ.showMessageWithCancel("Export graph...",
						"The file " + saveFile.getAbsolutePath() + " already exists.\n" + "Do you want to replace it?"))
					return;
			}

			IJ.showStatus("Exporting graph to " + saveFile.getAbsolutePath());

			try {
				exportChartAsSVG(chart, chartPanel.getBounds(), saveFile);
			} catch (final IOException ioe) {
				IJ.error("Saving to " + saveFile.getAbsolutePath() + " failed");
				return;
			}

		}

		/**
		 * Exports a JFreeChart to a SVG file.
		 *
		 * @param chart
		 *            JFreeChart to export
		 * @param bounds
		 *            the dimensions of the viewport
		 * @param svgFile
		 *            the output file.
		 * @throws IOException
		 *             if writing the svgFile fails.
		 *
		 *             This method is taken from:
		 *             http://dolf.trieschnigg.nl/jfreechart/
		 */
		void exportChartAsSVG(final JFreeChart chart, final Rectangle bounds, final File svgFile) throws IOException {

			// Get a DOMImplementation and create an XML document
			final DOMImplementation domImpl = GenericDOMImplementation.getDOMImplementation();
			final Document document = domImpl.createDocument(null, "svg", null);

			// Create an instance of the SVG Generator
			final SVGGraphics2D svgGenerator = new SVGGraphics2D(document);

			// draw the chart in the SVG generator
			chart.draw(svgGenerator, bounds);

			// Write svg file
			final OutputStream outputStream = new FileOutputStream(svgFile);
			final Writer out = new OutputStreamWriter(outputStream, "UTF-8");
			svgGenerator.stream(out, true /* use css */);
			outputStream.flush();
			outputStream.close();
		}
	}

	public static final int AXES_NORMAL = 1;
	public static final int AXES_SEMI_LOG = 2;
	public static final int AXES_LOG_LOG = 3;

	public static final String[] axesParameters = { null, "normal", "semi-log", "log-log" };

	public static final int NOT_NORMALIZED = 1;
	public static final int NORMALIZED_FOR_SPHERE_VOLUME = 2;

	public static final String[] normalizationParameters = { null, "not-normalized", "normalized" };

	public static class ShollResults {
		protected double[] squaredRangeStarts;
		protected int[] crossingsPastEach;
		protected int n;
		protected double x_start, y_start, z_start;
		/* maxCrossings is the same as the "Dendrite Maximum". */
		protected int maxCrossings = Integer.MIN_VALUE;
		protected double criticalValue = Double.MIN_VALUE;
		protected String description;
		protected int axes;
		protected int normalization;
		protected double sphereSeparation;
		protected double[] x_graph_points;
		protected double[] y_graph_points;
		protected double minY;
		protected double maxY;
		protected int graphPoints;
		protected String yAxisLabel;
		protected String xAxisLabel;
		protected double regressionGradient = Double.MIN_VALUE;
		protected double regressionIntercept = Double.MIN_VALUE;
		protected double regressionRSquare = Double.NaN;
		protected int n_samples;
		protected double[] sampled_distances;
		protected double[] sampled_counts;
		String parametersSuffix;

		public double[] getSampledDistances() {
			return sampled_distances;
		}

		public double[] getSampledCounts() {
			return sampled_counts;
		}

		public int getDendriteMaximum() {
			return maxCrossings;
		}

		public double getCriticalValue() {
			return criticalValue;
		}

		public double getRegressionGradient() {
			return regressionGradient;
		}

		public double getShollRegressionCoefficient() {
			return -regressionGradient;
		}

		public double getRegressionIntercept() {
			return regressionIntercept;
		}

		public double getRegressionRSquare() {
			return regressionRSquare;
		}

		public double getMaxDistanceSquared() {
			return squaredRangeStarts[n - 1];
		}

		public String getSuggestedSuffix() {
			return parametersSuffix;
		}

		/**
		 * Instructs the Sholl Analysis plugin to analyze the profile sampled by
		 * .
		 */
		public void analyzeWithShollAnalysisPlugin(final String exportDir, final double primaryBranches) {

			final Sholl_Analysis sa = new Sholl_Analysis();
			sa.setDescription("Tracings for " + originalImage.getTitle(), false);
			final Calibration cal = originalImage.getCalibration();
			if (cal != null) {
				final int pX = (int) cal.getRawX(x_start);
				final int pY = (int) cal.getRawY(y_start, originalImage.getHeight());
				final int pZ = (int) (z_start / cal.pixelDepth + cal.zOrigin);
				sa.setCenter(pX, pY, pZ);
				sa.setUnit(cal.getUnit());
			}
			sa.setStepRadius(sphereSeparation);
			sa.setPrimaryBranches(primaryBranches);
			sa.setExportPath(exportDir);
			sa.analyzeProfile(sampled_distances, sampled_counts, !twoDimensional);

		}

		public void addToResultsTable() {
			final ResultsTable rt = Analyzer.getResultsTable();
			if (!Analyzer.resetCounter())
				return;
			rt.incrementCounter();
			rt.addValue("Filename", getOriginalFilename());
			rt.addValue("All paths used", String.valueOf(useAllPaths));
			rt.addValue("Paths used", numberOfPathsUsed);
			rt.addValue("Sphere separation", sphereSeparation);
			rt.addValue("Normalization", normalization);
			rt.addValue("Axes", axes);
			rt.addValue("Max inters. radius", getCriticalValue());
			rt.addValue("Max inters.", getDendriteMaximum());
			rt.addValue("Regression coefficient", getShollRegressionCoefficient());
			rt.addValue("Regression gradient", getRegressionGradient());
			rt.addValue("Regression intercept", getRegressionIntercept());
			rt.show("Results");
		}

		boolean twoDimensional;
		ImagePlus originalImage;
		boolean useAllPaths;
		int numberOfPathsUsed;

		public ShollResults(final List<ShollPoint> shollPoints, final ImagePlus originalImage,
				final boolean useAllPaths, final int numberOfPathsUsed, final double x_start, final double y_start,
				final double z_start, final String description, final int axes, final int normalization,
				final double sphereSeparation, final boolean twoDimensional) {
			parametersSuffix = "_" + axesParameters[axes] + "_" + normalizationParameters[normalization] + "_"
					+ sphereSeparation;
			this.originalImage = originalImage;
			this.useAllPaths = useAllPaths;
			this.numberOfPathsUsed = numberOfPathsUsed;
			this.x_start = x_start;
			this.y_start = y_start;
			this.z_start = z_start;
			this.description = description;
			this.axes = axes;
			this.normalization = normalization;
			this.sphereSeparation = sphereSeparation;
			this.twoDimensional = twoDimensional;
			Collections.sort(shollPoints);
			n = shollPoints.size();
			squaredRangeStarts = new double[n];
			crossingsPastEach = new int[n];
			int currentCrossings = 0;
			for (int i = 0; i < n; ++i) {
				final ShollPoint p = shollPoints.get(i);
				if (p.nearer)
					++currentCrossings;
				else
					--currentCrossings;
				squaredRangeStarts[i] = p.distanceSquared;
				crossingsPastEach[i] = currentCrossings;
				if (currentCrossings > maxCrossings) {
					maxCrossings = currentCrossings;
					criticalValue = Math.sqrt(p.distanceSquared);
				}
				// System.out.println("Range starting at:
				// "+Math.sqrt(p.distanceSquared)+" has crossings:
				// "+currentCrossings);
			}

			// Retrieve the data points for the sampled profile
			if (sphereSeparation > 0) { // Discontinuous sampling

				n_samples = (int) Math.ceil(Math.sqrt(getMaxDistanceSquared()) / sphereSeparation);
				sampled_distances = new double[n_samples];
				sampled_counts = new double[n_samples];
				for (int i = 0; i < n_samples; ++i) {
					final double x = i * sphereSeparation;
					sampled_distances[i] = x;
					sampled_counts[i] = crossingsAtDistanceSquared(x * x);
				}

			} else { // Continuous sampling

				// We'll ensure we are not keeping duplicated data points so
				// we'll store unique distances in a temporary LinkedHashSet
				final LinkedHashSet<Double> uniqueDistancesSquared = new LinkedHashSet<>();
				for (int i = 0; i < n; ++i)
					uniqueDistancesSquared.add(squaredRangeStarts[i]);
				n_samples = uniqueDistancesSquared.size();
				sampled_distances = new double[n_samples];
				sampled_counts = new double[n_samples];
				final Iterator<Double> it = uniqueDistancesSquared.iterator();
				int idx = 0;
				while (it.hasNext()) {
					final double distanceSquared = it.next();
					sampled_distances[idx] = Math.sqrt(distanceSquared);
					sampled_counts[idx++] = crossingsAtDistanceSquared(distanceSquared);
				}
			}

			// At this point what has been sampled is what is set to be plotted
			// in a non-normalized linear plot
			graphPoints = n_samples;
			x_graph_points = Arrays.copyOf(sampled_distances, n_samples);
			y_graph_points = Arrays.copyOf(sampled_counts, n_samples);

			xAxisLabel = "Distance from (" + IJ.d2s(x_start, 3) + ", " + IJ.d2s(y_start, 3) + ", " + IJ.d2s(z_start, 3)
					+ ")";
			yAxisLabel = "N. of Intersections";

			if (normalization == NORMALIZED_FOR_SPHERE_VOLUME) {
				for (int i = 0; i < graphPoints; ++i) {
					final double x = x_graph_points[i];
					final double distanceSquared = x * x;
					if (twoDimensional)
						y_graph_points[i] /= (Math.PI * distanceSquared);
					else
						y_graph_points[i] /= ((4.0 * Math.PI * x * distanceSquared) / 3.0);
				}
				if (twoDimensional)
					yAxisLabel = "Inters./Area";
				else
					yAxisLabel = "Inters./Volume";
			}

			final SimpleRegression regression = new SimpleRegression();

			maxY = Double.MIN_VALUE;
			minY = Double.MAX_VALUE;
			for (int i = 0; i < graphPoints; ++i) {
				final double x = x_graph_points[i];
				final double y = y_graph_points[i];
				double x_for_regression = x;
				double y_for_regression = y;
				if (!(Double.isInfinite(y) || Double.isNaN(y))) {
					if (y > maxY)
						maxY = y;
					if (y < minY)
						minY = y;
					if (axes == AXES_SEMI_LOG) {
						if (y <= 0)
							continue;
						y_for_regression = Math.log(y);
					} else if (axes == AXES_LOG_LOG) {
						if (x <= 0 || y <= 0)
							continue;
						x_for_regression = Math.log(x);
						y_for_regression = Math.log(y);
					}
					regression.addData(x_for_regression, y_for_regression);
				}
			}
			regressionGradient = regression.getSlope();
			regressionIntercept = regression.getIntercept();
			// Retrieve r-squared, i.e., the square of the Pearson regression
			// coefficient
			regressionRSquare = regression.getRSquare();

			if (maxY == Double.MIN_VALUE)
				throw new RuntimeException("[BUG] Somehow there were no valid points found");
		}

		public JFreeChart createGraph() {

			XYSeriesCollection data = null;

			double minX = Double.MAX_VALUE;
			double maxX = Double.MIN_VALUE;

			final XYSeries series = new XYSeries("Intersections");
			for (int i = 0; i < graphPoints; ++i) {
				final double x = x_graph_points[i];
				final double y = y_graph_points[i];
				if (Double.isInfinite(y) || Double.isNaN(y))
					continue;
				if (axes == AXES_SEMI_LOG || axes == AXES_LOG_LOG) {
					if (y <= 0)
						continue;
				}
				if (axes == AXES_LOG_LOG) {
					if (x <= 0)
						continue;
				}
				if (x < minX)
					minX = x;
				if (x > maxX)
					maxX = x;
				series.add(x, y);
			}
			data = new XYSeriesCollection(series);

			ValueAxis xAxis = null;
			ValueAxis yAxis = null;
			if (axes == AXES_NORMAL) {
				xAxis = new NumberAxis(xAxisLabel);
				yAxis = new NumberAxis(yAxisLabel);
			} else if (axes == AXES_SEMI_LOG) {
				xAxis = new NumberAxis(xAxisLabel);
				yAxis = new LogAxis(yAxisLabel);
			} else if (axes == AXES_LOG_LOG) {
				xAxis = new LogAxis(xAxisLabel);
				yAxis = new LogAxis(yAxisLabel);
			}

			try {
				xAxis.setRange(minX, maxX);
				if (axes == AXES_NORMAL)
					yAxis.setRange(0, maxY);
				else
					yAxis.setRange(minY, maxY);
			} catch (final IllegalArgumentException iae) {
				yAxis.setAutoRange(true);
			}

			XYItemRenderer renderer = null;
			if (sphereSeparation > 0) {
				renderer = new XYLineAndShapeRenderer();
			} else {
				final XYBarRenderer barRenderer = new XYBarRenderer();
				barRenderer.setShadowVisible(false);
				//barRenderer.setGradientPaintTransformer(null);
				barRenderer.setDrawBarOutline(false);
				barRenderer.setBarPainter(new StandardXYBarPainter());
				renderer = barRenderer;
			}
			renderer.setSeriesVisibleInLegend(0, false);
			final XYPlot plot = new XYPlot(data, xAxis, yAxis, renderer);

			return new JFreeChart(description, plot);
		}

		public int crossingsAtDistanceSquared(final double distanceSquared) {

			int minIndex = 0;
			int maxIndex = n - 1;

			if (distanceSquared < squaredRangeStarts[minIndex])
				return 1;
			else if (distanceSquared > squaredRangeStarts[maxIndex])
				return 0;

			while (maxIndex - minIndex > 1) {

				final int midPoint = (maxIndex + minIndex) / 2;

				if (distanceSquared < squaredRangeStarts[midPoint])
					maxIndex = midPoint;
				else
					minIndex = midPoint;
			}
			return crossingsPastEach[minIndex];
		}

		public ImagePlus makeShollCrossingsImagePlus(final ImagePlus original) {
			final int width = original.getWidth();
			final int height = original.getHeight();
			final int depth = original.getStackSize();
			final Calibration c = original.getCalibration();
			double x_spacing = 1;
			double y_spacing = 1;
			double z_spacing = 1;
			if (c != null) {
				x_spacing = c.pixelWidth;
				y_spacing = c.pixelHeight;
				z_spacing = c.pixelDepth;
			}
			final ImageStack stack = new ImageStack(width, height);
			for (int z = 0; z < depth; ++z) {
				final short[] pixels = new short[width * height];
				for (int y = 0; y < height; ++y) {
					for (int x = 0; x < width; ++x) {
						final double xdiff = x_spacing * x - x_start;
						final double ydiff = y_spacing * y - y_start;
						final double zdiff = z_spacing * z - z_start;
						final double distanceSquared = xdiff * xdiff + ydiff * ydiff + zdiff * zdiff;
						pixels[y * width + x] = (short) crossingsAtDistanceSquared(distanceSquared);
					}
				}
				final ShortProcessor sp = new ShortProcessor(width, height);
				sp.setPixels(pixels);
				stack.addSlice("", sp);
			}
			final ImagePlus result = new ImagePlus(description, stack);
			result.show();
			final IndexColorModel icm = FindConnectedRegions.backgroundAndSpectrum(255);
			stack.setColorModel(icm);
			final ImageProcessor ip = result.getProcessor();
			if (ip != null) {
				ip.setColorModel(icm);
				ip.setMinAndMax(0, maxCrossings);
			}
			result.updateAndDraw();

			if (c != null)
				result.setCalibration(c);
			return result;
		}

		public static void csvQuoteAndPrint(final PrintWriter pw, final Object o) {
			pw.print(PathAndFillManager.stringForCSV("" + o));
		}

		public String getOriginalFilename() {
			final FileInfo originalFileInfo = originalImage.getOriginalFileInfo();
			if (originalFileInfo.directory == null)
				return "[unknown]";
			else
				return new File(originalFileInfo.directory, originalFileInfo.fileName).getAbsolutePath();

		}

		public void exportSummaryToCSV(final File outputFile) throws IOException {
			final String[] headers = new String[] { "Filename", "All paths used", "Paths used", "Sphere separation",
					"Normalization", "Axes", "Max inters. radius", "Max inters.", "Regression coefficient",
					"Regression gradient", "Regression intercept" };

			final PrintWriter pw = new PrintWriter(
					new OutputStreamWriter(new FileOutputStream(outputFile.getAbsolutePath()), "UTF-8"));
			final int columns = headers.length;
			for (int c = 0; c < columns; ++c) {
				csvQuoteAndPrint(pw, headers[c]);
				if (c < (columns - 1))
					pw.print(",");
			}
			pw.print("\r\n");
			csvQuoteAndPrint(pw, getOriginalFilename());
			pw.print(",");
			csvQuoteAndPrint(pw, useAllPaths);
			pw.print(",");
			csvQuoteAndPrint(pw, numberOfPathsUsed);
			pw.print(",");
			csvQuoteAndPrint(pw, sphereSeparation);
			pw.print(",");
			csvQuoteAndPrint(pw, normalizationParameters[normalization]);
			pw.print(",");
			csvQuoteAndPrint(pw, axesParameters[axes]);
			pw.print(",");
			csvQuoteAndPrint(pw, getCriticalValue());
			pw.print(",");
			csvQuoteAndPrint(pw, getDendriteMaximum());
			pw.print(",");
			csvQuoteAndPrint(pw, getShollRegressionCoefficient());
			pw.print(",");
			csvQuoteAndPrint(pw, getRegressionGradient());
			pw.print(",");
			csvQuoteAndPrint(pw, getRegressionIntercept());
			pw.print("\r\n");

			pw.close();
		}

		public void exportDetailToCSV(final File outputFile) throws IOException {
			String[] headers;
			headers = new String[] { "Radius", "Inters.", (twoDimensional) ? "Inters./Area" : "Inters./Volume" };

			final PrintWriter pw = new PrintWriter(
					new OutputStreamWriter(new FileOutputStream(outputFile.getAbsolutePath()), "UTF-8"));
			final int columns = headers.length;
			for (int c = 0; c < columns; ++c) {
				csvQuoteAndPrint(pw, headers[c]);
				if (c < (columns - 1))
					pw.print(",");
			}
			pw.print("\r\n");

			for (int i = 0; i < n_samples; ++i) {
				double normalizedCrossings = -Double.MIN_VALUE;
				final double x = sampled_distances[i];
				final double y = sampled_counts[i];
				final double distanceSquared = x * x;
				if (twoDimensional)
					normalizedCrossings = y / (Math.PI * distanceSquared);
				else
					normalizedCrossings = y / ((4.0 * Math.PI * x * distanceSquared) / 3.0);
				csvQuoteAndPrint(pw, x);
				pw.print(",");
				csvQuoteAndPrint(pw, y);
				pw.print(",");
				csvQuoteAndPrint(pw, normalizedCrossings);
				pw.print("\r\n");
			}
			pw.close();
		}

	}

	public static class ShollPoint implements Comparable<ShollPoint> {
		protected boolean nearer;
		protected double distanceSquared;

		@Override
		public int compareTo(final ShollPoint other) {
			return Double.compare(this.distanceSquared, other.distanceSquared);
		}

		ShollPoint(final double distanceSquared, final boolean nearer) {
			this.distanceSquared = distanceSquared;
			this.nearer = nearer;
		}
	}

	public static void addPathPointsToShollList(final Path p, final double x_start, final double y_start,
			final double z_start, final List<ShollPoint> shollPointsList) {

		for (int i = 0; i < p.points - 1; ++i) {
			final double xdiff_first = p.precise_x_positions[i] - x_start;
			final double ydiff_first = p.precise_y_positions[i] - y_start;
			final double zdiff_first = p.precise_z_positions[i] - z_start;
			final double xdiff_second = p.precise_x_positions[i + 1] - x_start;
			final double ydiff_second = p.precise_y_positions[i + 1] - y_start;
			final double zdiff_second = p.precise_z_positions[i + 1] - z_start;
			final double distanceSquaredFirst = xdiff_first * xdiff_first + ydiff_first * ydiff_first
					+ zdiff_first * zdiff_first;
			final double distanceSquaredSecond = xdiff_second * xdiff_second + ydiff_second * ydiff_second
					+ zdiff_second * zdiff_second;
			shollPointsList.add(new ShollPoint(distanceSquaredFirst, distanceSquaredFirst < distanceSquaredSecond));
			shollPointsList.add(new ShollPoint(distanceSquaredSecond, distanceSquaredFirst >= distanceSquaredSecond));
		}

	}

	ArrayList<ShollPoint> shollPointsAllPaths;
	ArrayList<ShollPoint> shollPointsSelectedPaths;
	PathAndFillManager shollpafm;
	ResultsPanel resultsPanel = new ResultsPanel();

	protected boolean twoDimensional;

	public ShollAnalysisDialog(final String title, final double x_start, final double y_start, final double z_start,
			final PathAndFillManager pafm, final ImagePlus originalImage) {

		super(IJ.getInstance(), title, false);

		this.x_start = x_start;
		this.y_start = y_start;
		this.z_start = z_start;

		this.originalImage = originalImage;
		twoDimensional = (originalImage.getStackSize() == 1);

		shollPointsAllPaths = new ArrayList<>();
		shollPointsSelectedPaths = new ArrayList<>();
		shollpafm = pafm;
		reloadPaths();

		useAllPathsCheckbox.setLabel("Use all " + numberOfAllPaths + " paths in analysis?");
		useSelectedPathsCheckbox.setLabel("Use only the " + numberOfSelectedPaths + " selected paths in analysis?");

		addWindowListener(this);

		setLayout(new GridBagLayout());

		final GridBagConstraints c = new GridBagConstraints();

		c.gridx = 0;
		c.gridy = 0;
		c.anchor = GridBagConstraints.LINE_START;
		final int margin = 10;
		c.insets = new Insets(margin, margin, 0, margin);
		useAllPathsCheckbox.addItemListener(this);
		add(useAllPathsCheckbox, c);

		++c.gridy;
		c.insets = new Insets(0, margin, margin, margin);
		add(useSelectedPathsCheckbox, c);
		useSelectedPathsCheckbox.addItemListener(this);

		++c.gridy;
		c.insets = new Insets(0, margin, 0, margin);
		buildTypeFilteringMenu();
		add(swcTypesButton, c);
		++c.gridy;
		c.insets = new Insets(0, margin, margin, margin);
		add(filteredTypesWarningLabel, c);
		makePromptInteractive(true);

		++c.gridy;
		c.insets = new Insets(margin, margin, 0, margin);
		add(normalAxes, c);
		normalAxes.addItemListener(this);
		++c.gridy;
		c.insets = new Insets(0, margin, 0, margin);
		add(semiLogAxes, c);
		semiLogAxes.addItemListener(this);
		++c.gridy;
		c.insets = new Insets(0, margin, margin, margin);
		add(logLogAxes, c);
		logLogAxes.addItemListener(this);

		++c.gridy;
		c.insets = new Insets(margin, margin, 0, margin);
		add(noNormalization, c);
		noNormalization.addItemListener(this);
		++c.gridy;
		c.insets = new Insets(0, margin, margin, margin);
		if (twoDimensional)
			normalizationForSphereVolume.setLabel("Normalize for area enclosed by circle");
		else
			normalizationForSphereVolume.setLabel("Normalize for volume enclosed by circle");
		add(normalizationForSphereVolume, c);
		normalizationForSphereVolume.addItemListener(this);

		++c.gridy;
		c.gridx = 0;
		final Panel separationPanel = new Panel();
		separationPanel.add(new Label("Radius step size (0 for continuous sampling)"));
		sampleSeparation.addTextListener(this);
		separationPanel.add(sampleSeparation);
		final String unit = shollpafm.spacing_units;
		if (unit != null && !unit.equals("unknown") && !unit.equals("pixel"))
			separationPanel.add(new Label(unit));
		add(separationPanel, c);

		c.gridx = 0;
		++c.gridy;
		c.insets = new Insets(margin, margin, margin, margin);
		add(resultsPanel, c);

		++c.gridy;
		final Panel buttonsPanel = new Panel();
		buttonsPanel.setLayout(new BorderLayout());
		final Panel topRow = new Panel();
		final Panel middleRow = new Panel();
		final Panel bottomRow = new Panel();

		topRow.add(makeShollImageButton);
		makeShollImageButton.addActionListener(this);

		topRow.add(drawShollGraphButton);
		drawShollGraphButton.addActionListener(this);

		topRow.add(exportProfileButton);
		exportProfileButton.addActionListener(this);

		middleRow.add(analyzeButton);
		analyzeButton.addActionListener(this);

		buttonsPanel.add(topRow, BorderLayout.NORTH);
		buttonsPanel.add(middleRow, BorderLayout.CENTER);
		buttonsPanel.add(bottomRow, BorderLayout.SOUTH);

		add(buttonsPanel, c);

		pack();

		updateResults();

		GUI.center(this);
		setVisible(true);
		toFront();

	}

	private void reloadPaths() {

		// Reset analysis
		numberOfAllPaths = 0;
		numberOfSelectedPaths = 0;
		shollPointsAllPaths.clear();
		shollPointsSelectedPaths.clear();

		// load paths considering only those whose type has been chosen by user
		for (Path p : shollpafm.allPaths) {
			final boolean selected = p.getSelected();
			if (p.getUseFitted()) {
				p = p.fitted;
			} else if (p.fittedVersionOf != null)
				continue;

			if (filteredTypes.contains(Path.getSWCtypeName(p.getSWCType()))) {
				addPathPointsToShollList(p, x_start, y_start, z_start, shollPointsAllPaths);
				++numberOfAllPaths;
				if (selected) {
					addPathPointsToShollList(p, x_start, y_start, z_start, shollPointsSelectedPaths);
					++numberOfSelectedPaths;
				}
			}
		}

	}

	private void buildTypeFilteringMenu() {
		swcTypesButton.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(final ActionEvent e) {
				if (!swcTypesMenu.isVisible()) {
					final Point p = swcTypesButton.getLocationOnScreen();
					swcTypesMenu.setInvoker(swcTypesButton);
					swcTypesMenu.setLocation((int) p.getX(), (int) p.getY() + swcTypesButton.getHeight());
					swcTypesMenu.setVisible(true);
				} else {
					swcTypesMenu.setVisible(false);
				}
			}
		});
		for (final String swcType : Path.getSWCtypeNames()) {
			final JMenuItem mi = new JCheckBoxMenuItem(swcType, true);
			mi.addActionListener(new ActionListener() {
				@Override
				public void actionPerformed(final ActionEvent e) {
					swcTypesMenu.show(swcTypesButton, 0, swcTypesButton.getHeight());
				}
			});
			mi.addItemListener(new ItemListener() {
				@Override
				public void itemStateChanged(final ItemEvent e) {
					if (filteredTypes.contains(mi.getText()) && !mi.isSelected()) {
						filteredTypes.remove(mi.getText());
					} else if (!filteredTypes.contains(mi.getText()) && mi.isSelected()) {
						filteredTypes.add(mi.getText());
					}
					reloadPaths();
					updateResults();
				}
			});
			swcTypesMenu.add(mi);
		}

	}

	public class ResultsPanel extends Panel {
		Label headingLabel = new Label("Results:");
		String defaultText = "[Not calculated yet]";
		Label criticalValuesLabel = new Label(defaultText, Label.RIGHT);
		Label dendriteMaximumLabel = new Label(defaultText, Label.RIGHT);
		// Label schoenenRamificationIndexLabel = new Label(defaultText);
		Label shollsRegressionCoefficientLabel = new Label(defaultText, Label.RIGHT);
		Label shollsRegressionInterceptLabel = new Label(defaultText, Label.RIGHT);
		Label shollsRegressionRSquaredLabel = new Label(defaultText, Label.RIGHT);

		public ResultsPanel() {
			super();
			setLayout(new GridBagLayout());
			final GridBagConstraints c = new GridBagConstraints();
			c.anchor = GridBagConstraints.LINE_START;
			c.gridx = 0;
			c.gridy = 0;
			c.gridwidth = 2;
			add(headingLabel, c);
			c.anchor = GridBagConstraints.LINE_END;
			c.gridx = 0;
			++c.gridy;
			c.gridwidth = 1;
			add(new Label("Max inters. radius: "), c);
			c.gridx = 1;
			add(criticalValuesLabel, c);
			c.gridx = 0;
			++c.gridy;
			add(new Label("Max inters.: "), c);
			c.gridx = 1;
			add(dendriteMaximumLabel, c);
			// c.gridx = 0;
			// ++ c.gridy;
			// add(new Label("Schoenen Ramification Index:"),c);
			// c.gridx = 1;
			// add(schoenenRamificationIndexLabel,c);
			c.gridx = 0;
			++c.gridy;
			add(new Label("Regression coeff.: "), c);
			c.gridx = 1;
			add(shollsRegressionCoefficientLabel, c);
			c.gridx = 0;
			++c.gridy;
			add(new Label("Regression intercept: "), c);
			c.gridx = 1;
			add(shollsRegressionInterceptLabel, c);
			c.gridx = 0;
			++c.gridy;
			add(new Label("Regression R2: "), c);
			c.gridx = 1;
			add(shollsRegressionRSquaredLabel, c);
		}

		public void updateFromResults(final ShollResults results) {
			dendriteMaximumLabel.setText("" + results.getDendriteMaximum());
			criticalValuesLabel.setText(IJ.d2s(results.getCriticalValue(), 3));
			shollsRegressionCoefficientLabel.setText(IJ.d2s(results.getShollRegressionCoefficient(), -3));
			shollsRegressionInterceptLabel.setText(IJ.d2s(results.getRegressionIntercept(), 3));
			shollsRegressionRSquaredLabel.setText(IJ.d2s(results.getRegressionRSquare(), 3));
		}
	}

	@Override
	public void windowClosing(final WindowEvent e) {
		dispose();
	}

	@Override
	public void windowActivated(final WindowEvent e) {
	}

	@Override
	public void windowDeactivated(final WindowEvent e) {
	}

	@Override
	public void windowClosed(final WindowEvent e) {
	}

	@Override
	public void windowOpened(final WindowEvent e) {
	}

	@Override
	public void windowIconified(final WindowEvent e) {
	}

	@Override
	public void windowDeiconified(final WindowEvent e) {
	}

	/**
	 * @return the path to save current profile
	 */
	private String getExportPath() {
		if (this.exportPath == null && originalImage != null) {
			final FileInfo fi = originalImage.getOriginalFileInfo();
			if (fi != null && fi.directory != null)
				this.exportPath = fi.directory;
		}
		return this.exportPath;
	}

}