package ini.trakem2.imaging.filters;

import ij.IJ;
import ij.gui.GenericDialog;
import ij.gui.YesNoCancelDialog;
import ini.trakem2.display.Display;
import ini.trakem2.display.Displayable;
import ini.trakem2.display.Layer;
import ini.trakem2.display.LayerSet;
import ini.trakem2.display.Patch;
import ini.trakem2.utils.Bureaucrat;
import ini.trakem2.utils.Utils;
import ini.trakem2.utils.Worker;

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.Future;
import java.util.regex.Pattern;

import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.AbstractTableModel;

public class FilterEditor {

	// They are all IFilter, and all have protected fields.
	@SuppressWarnings("rawtypes")
	static public final Class[] available =
		new Class[]{CLAHE.class, NormalizeLocalContrast.class, EqualizeHistogram.class,
					EnhanceContrast.class, ResetMinAndMax.class, DefaultMinAndMax.class,
					GaussianBlur.class, Invert.class, Normalize.class,
		            RankFilter.class, RobustNormalizeLocalContrast.class,
		            ValueToNoise.class, SubtractBackground.class,
		            CorrectBackground.class,
		            LUTRed.class, LUTGreen.class, LUTBlue.class,
		            LUTMagenta.class, LUTCyan.class, LUTYellow.class,
		            LUTOrange.class, LUTCustom.class};

	static private class TableAvailableFilters extends JTable {
		public TableAvailableFilters(final TableChosenFilters tcf) {
			setModel(new AbstractTableModel() {
				@Override
				public Object getValueAt(final int rowIndex, final int columnIndex) {
					return available[rowIndex].getSimpleName();
				}
				@Override
				public int getRowCount() {
					return available.length;
				}
				@Override
				public int getColumnCount() {
					return 1;
				}
				@Override
				public String getColumnName(final int col) {
					return "Available Filters";
				}
			});
			addMouseListener(new MouseAdapter() {
				@Override
				public void mousePressed(final MouseEvent me) {
					if (2 == me.getClickCount()) {
						tcf.add(available[getSelectedRow()]);
					}
				}
			});
		}
	}

	static private class FilterWrapper {
		IFilter filter;
		final Field[] fields;
		FilterWrapper(final Class<?> filterClass) {
			this.fields = filterClass.getDeclaredFields();
			try {
				this.filter = (IFilter) filterClass.getConstructor().newInstance();
			} catch (final Exception e) {
				e.printStackTrace();
			}
		}
		/** Makes a copy of {@code filter}. */
		FilterWrapper(final IFilter filter) {
			this.fields = filter.getClass().getDeclaredFields();
			try {
				// Create a copy and set its parameters to the same values
				this.filter = (IFilter) filter.getClass().getConstructor().newInstance();
				for (final Field f : fields) {
					f.set(this.filter, f.get(filter));
				}
			} catch (final Exception e) {
				e.printStackTrace();
			}
		}
		FilterWrapper() {
			this.fields = new Field[0]; // empty
		}
		String name(final int i) {
			return fields[i].getName();
		}
		String value(final int i) {
			try {
				return "" + fields[i].get(filter);
			} catch (final Exception e) {
				e.printStackTrace();
			}
			return null;
		}
		void set(final int i, Object v) {
			Utils.log2("v class: " + v.getClass());
			final Field f = fields[i];
			f.setAccessible(true);
			final Class<?> c = f.getType();
			try {
				final String sv = v.toString().trim();
				Utils.log2("sv is: " + sv + ", and f.getDeclaringClass() == " + c);
				if (Double.TYPE == c) v = Double.parseDouble(sv);
				else if (Float.TYPE == c) v = Float.parseFloat(sv);
				else if (Integer.TYPE == c) v = Integer.parseInt(sv);
				else if (Boolean.TYPE == c) v = Boolean.parseBoolean(sv);
				else if (Long.TYPE == c) v = Long.parseLong(sv);
				else if (Short.TYPE == c) v = Short.parseShort(sv);
				else if (Byte.TYPE == c) v = Byte.parseByte(sv);
				else if (String.class == c) v = sv;
				//
				f.set(filter, v);
			} catch (final Exception e) {
				Utils.logAll("New value '" + v + "' is invalid; keeping the last value.");
				e.printStackTrace();
			}
		}
		public boolean sameParameterValues(final FilterWrapper w) {
			if (this.filter == w.filter) return true;
			if (filter.getClass() != w.filter.getClass()) return false;
			for (int i=0; i<fields.length; ++i) {
				if (!value(i).equals(w.value(i))) {
					return false;
				}
			}
			return true;
		}
	}

	static private class TableChosenFilters extends JTable {
		private final ArrayList<FilterWrapper> filters;
		public TableChosenFilters(final ArrayList<FilterWrapper> filters) {
			this.filters = filters;
			setModel(new AbstractTableModel() {
				@Override
				public Object getValueAt(final int rowIndex, final int columnIndex) {
					switch (columnIndex) {
					case 0: return rowIndex + 1;
					case 1: return filters.get(rowIndex).filter.getClass().getSimpleName();
					}
					return null;
				}
				@Override
				public int getRowCount() {
					return filters.size();
				}
				@Override
				public int getColumnCount() {
					return 2;
				}
				@Override
				public String getColumnName(final int col) {
					switch (col) {
						case 0: return "";
						case 1: return "Chosen Filters";
					}
					return null;
				}
			});
			addMouseListener(new MouseAdapter() {
				@Override
				public void mousePressed(final MouseEvent me) {
					if (me.getClickCount() == 2) {
						int row = getSelectedRow();
						filters.remove(row);
						((AbstractTableModel)getModel()).fireTableStructureChanged();
						if (filters.size() > 0) {
							if (row > 0) --row;
							getSelectionModel().setSelectionInterval(row, row);
						}
						getColumnModel().getColumn(0).setMaxWidth(10);
					}
				}
			});
			addKeyListener(new KeyAdapter() {
				@Override
				public void keyPressed(final KeyEvent ke) {
					// Check preconditions
					final int row = getSelectedRow();
					if (-1 == row) return;
					int selRow = -1;
					//
					switch (ke.getKeyCode()) {
					case KeyEvent.VK_PAGE_UP:
						if (filters.size() > 1 && row > 0) {
							filters.add(row -1, filters.remove(row));
							selRow = row -1;
							ke.consume();
						}
						break;
					case KeyEvent.VK_PAGE_DOWN:
						if (filters.size() > 1 && row < filters.size() -1) {
							filters.add(row + 1, filters.remove(row));
							selRow = row + 1;
							ke.consume();
						}
						break;
					case KeyEvent.VK_DELETE:
						if (filters.size() > 1) {
							if (0 == row) selRow = 0;
							else if (filters.size() -1 == row) selRow = filters.size() -2;
							else selRow = row -1;
						}
						filters.remove(row);
						ke.consume();
						break;
					case KeyEvent.VK_UP:
						selRow = row > 0 ? row -1 : row;
						ke.consume();
						break;
					case KeyEvent.VK_DOWN:
						selRow = row < filters.size() -1 ? row + 1 : row;
						ke.consume();
						break;
					}
					((AbstractTableModel)getModel()).fireTableStructureChanged();
					getColumnModel().getColumn(0).setMaxWidth(10);
					if (-1 != selRow) {
						getSelectionModel().setSelectionInterval(selRow, selRow);
					}
				}
			});
			getColumnModel().getColumn(0).setMaxWidth(10);
		}
		final FilterWrapper selected() {
			final int row = getSelectedRow();
			if (-1 == row) return new FilterWrapper(); // empty
			return filters.get(row);
		}
		final void add(final Class<?> filterClass) {
			filters.add(new FilterWrapper(filterClass));
			((AbstractTableModel)getModel()).fireTableStructureChanged();
			this.getSelectionModel().setSelectionInterval(filters.size()-1, filters.size()-1);
		}
		final void setupListener(final TableParameters tp) {
			getSelectionModel().addListSelectionListener(new ListSelectionListener() {
				@Override
				public void valueChanged(final ListSelectionEvent e) {
					tp.triggerUpdate();
				}
			});
		}
	}

	static private class TableParameters extends JTable {
		TableParameters(final TableChosenFilters tcf) {
			setModel(new AbstractTableModel() {
				@Override
				public Object getValueAt(final int rowIndex, final int columnIndex) {
					final FilterWrapper w = tcf.selected();
					switch (columnIndex) {
					case 0:
						return w.name(rowIndex);
					case 1:
						return w.value(rowIndex);
					}
					return null;
				}
				@Override
				public int getRowCount() {
					return tcf.selected().fields.length;
				}

				@Override
				public int getColumnCount() {
					return 2;
				}

				@Override
				public String getColumnName(final int col) {
					switch (col) {
					case 0: return "Parameter";
					case 1: return "Value";
					default:
						return null;
					}
				}

				@Override
				public void setValueAt(final Object v, final int rowIndex, final int columnIndex) {
					tcf.selected().set(rowIndex, v);
			    }

				@Override
				public boolean isCellEditable(final int rowIndex, final int columnIndex) {
					return 1 == columnIndex;
				}
			});
		}
		void triggerUpdate() {
			((AbstractTableModel)getModel()).fireTableStructureChanged();
		}
	}

	static public final void GUI(final Collection<Patch> patches, final Patch reference) {
		final ArrayList<FilterWrapper> filters = new ArrayList<FilterWrapper>();

		// Find out if all images have the exact same filters
		final StringBuilder sb = new StringBuilder();
		final Patch ref = (null == reference? patches.iterator().next() : reference);
		final IFilter[] refFilters = ref.getFilters();
		if (null != refFilters) {
			for (final IFilter f : refFilters) filters.add(new FilterWrapper(f)); // makes a copy of the IFilter
		}
		//
		for (final Patch p : patches) {
			if (ref == p) continue;
			final IFilter[] fs = p.getFilters();
			if (null == fs && null == refFilters) continue; // ok
			if ((null != refFilters && null == fs)
			 || (null == refFilters && null != fs)
			 || (null != refFilters && null != fs && fs.length != refFilters.length)) {
				sb.append("WARNING: patch #" + p.getId() + " has a different number of filters than reference patch #" + ref.getId());
				continue;
			}
			// Compare each
			for (int i=0; i<refFilters.length; ++i) {
				if (fs[i].getClass() != refFilters[i].getClass()) {
					sb.append("WARNING: patch #" + p.getId() + " has a different filters than reference patch #" + ref.getId());
					break;
				}
				// Does the filter have the same parameters?
				if (!filters.get(i).sameParameterValues(new FilterWrapper(fs[i]))) {
					sb.append("WARNING: patch #" + p.getId() + " has filter '" + fs[i].getClass().getSimpleName() + "' with different parameters than the reference patch #" + ref.getId());
				}
			}
		}
		if (sb.length() > 0) {
			final GenericDialog gd = new GenericDialog("WARNING", null == Display.getFront() ? IJ.getInstance() : Display.getFront().getFrame());
			gd.addMessage("Filters are not all the same for all images:");
			gd.addTextAreas(sb.toString(), null, 20, 30);
			final String[] s = new String[]{"Use the filters of the reference image", "Start from an empty list of filters"};
			gd.addChoice("Do:", s, s[0]);
			gd.showDialog();
			if (gd.wasCanceled()) return;
			if (1 == gd.getNextChoiceIndex()) filters.clear();
		}

		final TableChosenFilters tcf = new TableChosenFilters(filters);
		final TableParameters tp = new TableParameters(tcf);
		tcf.setupListener(tp);
		final TableAvailableFilters taf = new TableAvailableFilters(tcf);

		if (filters.size() > 0) {
			tcf.getSelectionModel().setSelectionInterval(0, 0);
		}

		final JFrame frame = new JFrame("Image filters");
		final JButton set = new JButton("Set");
		final JComboBox pulldown = new JComboBox(new String[]{"Selected images (" + patches.size() + ")", "All images in layer " + (ref.getLayer().getParent().indexOf(ref.getLayer()) + 1), "All images in layer range..."});

		final Component[] cs = new Component[]{set, pulldown, tcf, tp, taf};

		set.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(final ActionEvent e) {
				if (check(frame, filters)) {
					final ArrayList<Patch> ps = new ArrayList<Patch>();
					switch (pulldown.getSelectedIndex()) {
					case 0:
						ps.addAll(patches);
						break;
					case 1:
						for (final Displayable d : ref.getLayer().getDisplayables(Patch.class)) {
							ps.add((Patch)d);
						}
						break;
					case 2:
						final GenericDialog gd = new GenericDialog("Apply filters");
						Utils.addLayerRangeChoices(ref.getLayer(), gd);
						gd.addStringField("Image title matches:", "", 30);
						gd.addCheckbox("Visible images only", true);
						gd.showDialog();
						if (gd.wasCanceled()) return;
						final String regex = gd.getNextString();
						final boolean visible_only = gd.getNextBoolean();
						Pattern pattern = null;
						if (0 != regex.length()) {
							pattern = Pattern.compile(regex);
						}
						for (final Layer l : ref.getLayer().getParent().getLayers(gd.getNextChoiceIndex(), gd.getNextChoiceIndex())) {
							for (final Displayable d : l.getDisplayables(Patch.class, visible_only)) {
								if (null != pattern && !pattern.matcher(d.getTitle()).matches()) {
									continue;
								}
								ps.add((Patch)d);
							}
						}
					}
					apply(ps, filters, cs, false);
				}
			}
		});
		final JPanel buttons = new JPanel();
		final JLabel label = new JLabel("Push F1 for help");
		final GridBagConstraints c2 = new GridBagConstraints();
		final GridBagLayout gb2 = new GridBagLayout();
		buttons.setLayout(gb2);

		c2.anchor = GridBagConstraints.WEST;
		c2.gridx = 0;
		gb2.setConstraints(label, c2);
		buttons.add(label);

		c2.gridx = 1;
		c2.weightx = 1;
		final JPanel empty = new JPanel();
		gb2.setConstraints(empty, c2);
		buttons.add(empty);

		c2.gridx = 2;
		c2.weightx = 0;
		c2.anchor = GridBagConstraints.EAST;
		final JLabel a = new JLabel("Apply to:");
		gb2.setConstraints(a, c2);
		buttons.add(a);

		c2.gridx = 3;
		c2.insets = new Insets(4, 10, 4, 0);
		gb2.setConstraints(pulldown, c2);
		buttons.add(pulldown);

		c2.gridx = 4;
		gb2.setConstraints(set, c2);
		buttons.add(set);

		//
		taf.setPreferredSize(new Dimension(350, 500));
		tcf.setPreferredSize(new Dimension(350, 250));
		tp.setPreferredSize(new Dimension(350, 250));
		//
		final GridBagLayout gb = new GridBagLayout();
		final GridBagConstraints c = new GridBagConstraints();

		final JPanel all = new JPanel();
		all.setBackground(Color.white);
		all.setPreferredSize(new Dimension(700, 500));
		all.setLayout(gb);

		c.gridx = 0;
		c.gridy = 0;
		c.anchor = GridBagConstraints.NORTHWEST;
		c.fill = GridBagConstraints.BOTH;
		c.gridheight = 2;
		c.weightx = 0.5;
		final JScrollPane p1 = new JScrollPane(taf);
		p1.setPreferredSize(taf.getPreferredSize());
		gb.setConstraints(p1, c);
		all.add(p1);

		c.gridx = 1;
		c.gridy = 0;
		c.gridheight = 1;
		c.weighty = 0.7;
		final JScrollPane p2 = new JScrollPane(tcf);
		p2.setPreferredSize(tcf.getPreferredSize());
		gb.setConstraints(p2, c);
		all.add(p2);

		c.gridx = 1;
		c.gridy = 1;
		c.weighty = 0.3;
		final JScrollPane p3 = new JScrollPane(tp);
		p3.setPreferredSize(tp.getPreferredSize());
		gb.setConstraints(p3, c);
		all.add(p3);

		c.gridx = 0;
		c.gridy = 2;
		c.gridwidth = 2;
		c.weightx = 1;
		c.weighty = 0;
		gb.setConstraints(buttons, c);
		all.add(buttons);

		final KeyAdapter help = new KeyAdapter() {
			@Override
			public void keyPressed(final KeyEvent ke) {
				if (ke.getKeyCode() == KeyEvent.VK_F1) {
					final GenericDialog gd = new GenericDialog("Help :: image filters");
					gd.addMessage(
						  "In the table 'Available Filters':\n"
						+ " - double-click a filter to add it to the table of 'Chosen Filters'\n \n"
						+ "In the table 'Chosen Filters':\n"
						+ " - double-click a filter to remove it.\n"
						+ " - PAGE UP/DOWN keys move the filter up/down in the list.\n"
						+ " - 'Delete' key removes the selected filter.\n \n"
						+ "In the table of parameter names and values:\n"
						+ " - double-click a value to edit it. Then push enter to set the new value.\n \n"
						+ "What you need to know:\n"
						+ " - filters are set and applied in order, so order matters.\n"
						+ " - filters with parameters for which you entered a value of zero\nwill result in a warning message.\n"
						+ " - when applying the filters, if you choose 'Selected images', these are the images\nthat were selected when the filter editor was opened.\n"
						+ " - when applying the filters, if you want to filter for a regular expression pattern\nin the image name, use the 'All images in layer range...' option,\nwhere a range of one single layer is also possible."
					);
					gd.hideCancelButton();
					gd.setModal(false);
					gd.showDialog();
				}
			}
		};
		taf.addKeyListener(help);
		tcf.addKeyListener(help);
		tp.addKeyListener(help);
		all.addKeyListener(help);
		buttons.addKeyListener(help);
		empty.addKeyListener(help);
		a.addKeyListener(help);

		frame.getContentPane().add(all);
		frame.pack();
		frame.setVisible(true);
	}

	private static boolean check(final JFrame frame, final List<FilterWrapper> wrappers) {
		if (!wrappers.isEmpty()) {
			final String s = sanityCheck(wrappers);
			return 0 == s.length()
					|| new YesNoCancelDialog(frame, "WARNING", s + "\nContinue?").yesPressed();
		}
		return true;
	}

	private static String sanityCheck(final List<FilterWrapper> wrappers) {
		// Check all variables, find any numeric ones whose value is zero
		// Check for duplicated filters
		final HashSet<Class<?>> unique = new HashSet<Class<?>>();
		for (final FilterWrapper w : wrappers) {
			unique.add(w.filter.getClass());
		}
		final StringBuilder sb = new StringBuilder();
		if (wrappers.size() != unique.size()) {
			sb.append("WARNING: there are repeated filters!\n");
		}
		for (final FilterWrapper w : wrappers) {
			for (final Field f : w.fields) {
				try {
					final String zero = f.get(w.filter).toString();
					if ("0".equals(zero) || "0.0".equals(zero)) {
						sb.append("WARNING: parameter '" + f.getName() + "' of filter '" + w.filter.getClass().getSimpleName() + "' is zero!\n");
					}
				} catch (final Exception e) {
					e.printStackTrace();
				}
			}
		}
		return sb.toString();
	}

	private static IFilter[] asFilters(final List<FilterWrapper> wrappers) {
		if (wrappers.isEmpty()) return null;
		final IFilter[] fs = new IFilter[wrappers.size()];
		int next = 0;
		for (final FilterWrapper fw : wrappers) {
			fs[next++] = new FilterWrapper(fw.filter).filter; // a copy
		}
		return fs;
	}

	private static void apply(final Collection<Patch> patches, final List<FilterWrapper> wrappers, final Component[] cs, final boolean append) {
		Bureaucrat.createAndStart(new Worker.Task("Set filters") {
			@Override
			public void exec() {
				try {
					for (final Component c : cs) c.setEnabled(false);
					// Undo step
					final LayerSet ls = patches.iterator().next().getLayerSet();
					ls.addDataEditStep(new HashSet<Displayable>(patches));
					//
					final ArrayList<Future<?>> fus = new ArrayList<Future<?>>();
					for (final Patch p : patches) {
						final IFilter[] fs = asFilters(wrappers); // each Patch gets a copy
						if (append) p.appendFilters(fs);
						else p.setFilters(fs);
						p.getProject().getLoader().decacheImagePlus(p.getId());
						fus.add(p.updateMipMaps());
					}
					Utils.wait(fus);
					// Current state
					ls.addDataEditStep(new HashSet<Displayable>(patches));
				} finally {
					for (final Component c : cs) c.setEnabled(true);
				}
			}
		}, patches.iterator().next().getProject());
	}

	static public final IFilter[] duplicate(final IFilter[] fs) {
		if (null == fs) return fs;
		final IFilter[] copy = new IFilter[fs.length];
		for (int i=0; i<fs.length; ++i) copy[i] = new FilterWrapper(fs[i]).filter;
		return copy;
	}
}