package ini.trakem2.display;

import ini.trakem2.utils.Bureaucrat;
import ini.trakem2.utils.IJError;
import ini.trakem2.utils.Utils;
import ini.trakem2.utils.Worker;

import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.swing.JFrame;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.table.AbstractTableModel;

/** List all connectors whose origins intersect with the given tree. */
public class TreeConnectorsView {

	static private Map<Tree<?>,TreeConnectorsView> open = Collections.synchronizedMap(new HashMap<Tree<?>,TreeConnectorsView>());
	
	private JFrame frame;
	private TargetsTableModel outgoing_model = new TargetsTableModel(),
				  incoming_model = new TargetsTableModel();
	private Tree<?> tree;

	public TreeConnectorsView(final Tree<?> tree) {
		this.tree = tree;
		update();
		createGUI();
		open.put(tree,this);
	}

	static public Bureaucrat create(final Tree<?> tree) {
		return Bureaucrat.createAndStart(new Worker.Task("Opening connectors table") {
			public void exec() {
				TreeConnectorsView tcv = open.get(tree);
				if (null != tcv) {
					tcv.update();
					tcv.frame.setVisible(true);
					tcv.frame.toFront();
				} else {
					// Create and store in the Map of 'open'
					new TreeConnectorsView(tree);
				}
			}
		}, tree.getProject());
	}

	static public void dispose(final Tree<?> tree) {
		TreeConnectorsView tcv = open.remove(tree);
		if (null == tcv) return;
		tcv.frame.dispose();
	}

	static private final Comparator<Displayable> IDSorter = new Comparator<Displayable>() {
		@Override
		public int compare(Displayable o1, Displayable o2) {
			if (o1.getId() < o1.getId()) return -1;
			return 1;
		}
	};
	
	private class Row {
		final Connector connector;
		final int i;
		final ArrayList<Displayable> origins, targets;
		String originids, targetids;
		Row(final Connector c, final int i, final ArrayList<Displayable> origins, final ArrayList<Displayable> targets) {
			this.connector = c;
			this.i = i;
			this.origins = origins;
			this.targets = targets;
			for (final Iterator<Displayable> it = this.targets.iterator(); it.hasNext(); ) {
				if (it.next().getClass() == Connector.class) {
					it.remove();
				}
			}
		}
		final Coordinate<Node<Float>> getCoordinate(int col) {
			switch (col) {
				case 0:
				case 1:
					return connector.getCoordinateAtOrigin();
				case 2:
					return connector.getCoordinate(i);
				default:
					Utils.log2("Can't deal with column " + col);
					return null;
			}
		}
		private final long getFirstId(final ArrayList<Displayable> c) {
			if (c.isEmpty())
					return 0;
			else
					return c.get(0).getId();
		}
		final long getColumn(final int col) {
			switch (col) {
				case 0:
					return connector.getId();
				case 1:
					return getFirstId(origins);
				case 2:
					return getFirstId(targets);
				default:
					Utils.log2("Don't know how to deal with column " + col);
					return 0;
			}
		}
		private final String getIds(String ids, final ArrayList<Displayable> ds) {
			if (null == ids) {
				switch (ds.size()) {
					case 0:
						ids = "";
						break;
					case 1:
						ids = ds.get(0).toString();
						break;
					default:
						final StringBuilder sb = new StringBuilder();
						for (final Displayable d : ds) {
							sb.append(d).append(',').append(' ');
						}
						sb.setLength(sb.length() -2);
						ids = sb.toString();
						break;
				}
			}
			return ids;
		}
		final String getTargetIds() {
			targetids = getIds(targetids, targets);
			return targetids;
		}
		final String getOriginIds() {
			originids = getIds(originids, origins);
			return originids;
		}
	}

	public void update() {
		// Find all Connector instances intersecting with the nodes of Tree
		try {
			final Collection<Connector>[] connectors = this.tree.findConnectors();
			outgoing_model.setData(connectors[0]);
			incoming_model.setData(connectors[1]);
		} catch (Exception e) {
			IJError.print(e);
		}
	}

	private void addTab(JTabbedPane tabs, String title, TargetsTableModel model) {
		JTable table = new Table();
		table.setModel(model);
		JScrollPane jsp = new JScrollPane(table);
		jsp.setPreferredSize(new Dimension(500, 500));
		tabs.addTab(title, jsp);
	}

	private void createGUI() {
		this.frame = new JFrame("Connectors for Tree #" + this.tree.getId());
		frame.addWindowListener(new WindowAdapter() {
			public void windowClosing(WindowEvent we) {
				open.remove(tree);
			}
		});
		JTabbedPane tabs = new JTabbedPane();
		addTab(tabs, "Outgoing", outgoing_model);
		addTab(tabs, "Incoming", incoming_model);
		frame.getContentPane().add(tabs);
		frame.pack();
		frame.setVisible(true);
	}

	private class Table extends JTable {
		private static final long serialVersionUID = 1L;
		Table() {
			super();
			getTableHeader().addMouseListener(new MouseAdapter() {
				public void mouseClicked(MouseEvent me) {
					if (2 != me.getClickCount()) return;
					int viewColumn = getColumnModel().getColumnIndexAtX(me.getX());
					int column = convertColumnIndexToModel(viewColumn);
					if (-1 == column) return;
					((TargetsTableModel)getModel()).sortByColumn(column, me.isShiftDown());
				}
			});
			addMouseListener(new MouseAdapter() {
				public void mousePressed(MouseEvent me) {
					final int row = Table.this.rowAtPoint(me.getPoint());
					final int col = Table.this.columnAtPoint(me.getPoint());
					if (2 == me.getClickCount()) {
						go(col, row);
					} else if (Utils.isPopupTrigger(me)) {
						JPopupMenu popup = new JPopupMenu();
						final JMenuItem go = new JMenuItem("Go"); popup.add(go);
						final JMenuItem goandsel = new JMenuItem("Go and select"); popup.add(go);
						final JMenuItem update = new JMenuItem("Update"); popup.add(update);
						ActionListener listener = new ActionListener() {
							public void actionPerformed(ActionEvent ae) {
								final Object src = ae.getSource();
								if (src == go) go(col, row);
								else if (src == goandsel) {
									go(col, row);
									if (0 != (ae.getModifiers() ^ ActionEvent.SHIFT_MASK)) Display.getFront().getSelection().clear();
									TargetsTableModel ttm = (TargetsTableModel)getModel();
									Display.getFront().getSelection().add(ttm.rows.get(row).connector);
								} else if (src == update) {
									Bureaucrat.createAndStart(new Worker.Task("Updating...") {
										public void exec() {
											TreeConnectorsView.this.update();
										}
									}, TreeConnectorsView.this.tree.getProject());
								}
							}
						};
						go.addActionListener(listener);
						goandsel.addActionListener(listener);
						update.addActionListener(listener);
						popup.show(Table.this, me.getX(), me.getY());
					}
				}
			});
		}
		void go(int col, int row) {
			TargetsTableModel ttm = (TargetsTableModel)getModel();
			Display.centerAt(ttm.rows.get(row).getCoordinate(col));
		}
	}

	private class TargetsTableModel extends AbstractTableModel {
		private static final long serialVersionUID = 1L;
		List<Row> rows = null;

		synchronized public void setData(final Collection<Connector> connectors) {
			this.rows = new ArrayList<Row>(connectors.size());
			for (final Connector c : connectors) {
				int i = 0;
				final ArrayList<Displayable> origins = new ArrayList<Displayable>(c.getOrigins(VectorData.class, true));
				Collections.sort(origins, IDSorter);
				for (final Set<Displayable> targets : c.getTargets(VectorData.class, true)) {
					final ArrayList<Displayable> ts = new ArrayList<Displayable>(targets);
					Collections.sort(ts, IDSorter);
					this.rows.add(new Row(c, i++, origins, ts));
				}
			}
			SwingUtilities.invokeLater(new Runnable() {public void run() {
				fireTableDataChanged();
				fireTableStructureChanged();
			}});
		}

		public int getColumnCount() { return 3; }
		public String getColumnName(final int col) {
			switch (col) {
				case 0: return "Connector id";
				case 1: return "Origin id";
				case 2: return "Target id";
				default: return null;
			}
		}
		public int getRowCount() { return rows.size(); }
		public Object getValueAt(final int row, final int col) {
			switch (col) {
				case 0: return rows.get(row).connector.getId();
				case 1: return rows.get(row).getOriginIds();
				case 2: return rows.get(row).getTargetIds();
				default: return null;
			}
		}
		public boolean isCellEditable(int row, int col) { return false; }
		public void setValueAt(Object value, int row, int col) {}
		final void sortByColumn(final int col, final boolean descending) {
			final ArrayList<Row> rows = new ArrayList<Row>(this.rows);
			Collections.sort(rows, new Comparator<Row>() {
				public int compare(final Row r1, final Row r2) {
					final long op = r1.getColumn(col) - r2.getColumn(col);
					if (descending) {
						if (op > 0) return -1;
						if (op < 0) return 1;
						return 0;
					}
					if (op < 0) return -1;
					if (op > 0) return 1;
					return 0;
				}
			});
			this.rows = rows; // swap
			fireTableDataChanged();
			fireTableStructureChanged();
		}
	}

}