/**

TrakEM2 plugin for ImageJ(C).
Copyright (C) 2005-2009 Albert Cardona and Rodney Douglas.

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 (http://www.gnu.org/licenses/gpl.txt )

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, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA. 

You may contact Albert Cardona at acardona at ini.phys.ethz.ch
Institute of Neuroinformatics, University of Zurich / ETH, Switzerland.
**/

package ini.trakem2;

import ij.IJ;
import ij.ImageJ;
import ij.gui.GenericDialog;
import ij.gui.YesNoCancelDialog;
import ini.trakem2.display.Display3D;
import ini.trakem2.display.ImageJCommandListener;
import ini.trakem2.display.YesNoDialog;
import ini.trakem2.persistence.Loader;
import ini.trakem2.tree.LayerTree;
import ini.trakem2.tree.ProjectTree;
import ini.trakem2.tree.TemplateTree;
import ini.trakem2.utils.IJError;
import ini.trakem2.utils.ProjectToolbar;
import ini.trakem2.utils.RedPhone;
import ini.trakem2.utils.StdOutWindow;
import ini.trakem2.utils.Utils;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Transparency;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import java.awt.image.ImageProducer;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Map;
import java.util.Set;

import javax.swing.BorderFactory;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTabbedPane;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;


/** Static class that shows one project per tab in a JFrame.
 *  Creates itself when a project requests to be have its trees displayed.
 *  Destroys itself when there are no more projects to show.
 * 
 * */
public class ControlWindow {

	static private JFrame frame = null;
	static private JTabbedPane tabs = null;
	/** Project instances are keys, JSplitPane are the objects. */
	static private Hashtable<Project,JSplitPane> ht_projects = null;
	/** While the instance is not null, the other fields (frame, tabs, ht_projects) are not null either. */
	static private ControlWindow instance = null;
	/** Control changes to the instance. */
	static private final Object LOCK = new Object();
	
	private final RedPhone red_phone = new RedPhone();

	static private boolean gui_enabled = true;

	/** Intercept ImageJ menu commands if the front image is a FakeImagePlus. */
	private ImageJCommandListener command_listener;

	private ControlWindow() {
		if (null != ij.gui.Toolbar.getInstance()) {
			ij.gui.Toolbar.getInstance().addMouseListener(tool_listener);
		}
		Utils.setup(this);
		Loader.setupPreloader(this);
		if (IJ.isWindows() && isGUIEnabled()) StdOutWindow.start();
		Display3D.init();
		setLookAndFeel();
		this.command_listener = new ImageJCommandListener();
		this.red_phone.start();
	}
	
	// private to the package
	static final ControlWindow getInstance() {
		synchronized (LOCK) {
			if (null == instance) instance = new ControlWindow();
			return instance;
		}
	}

	static public void setLookAndFeel() {
		try {
			if (ij.IJ.isLinux()) {
				// Nimbus looks great but it's unstable: after a while, swing components stop repainting, throwing all sort of exceptions.
				//UIManager.setLookAndFeel("com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel");
				UIManager.setLookAndFeel("javax.swing.plaf.metal.MetalLookAndFeel");
				for (final Frame frame : Frame.getFrames()) {
					if (frame.isEnabled()) SwingUtilities.updateComponentTreeUI(frame);
				}
				// all done above
				//if (null != frame) SwingUtilities.updateComponentTreeUI(frame);
				//if (null != IJ.getInstance()) javax.swing.SwingUtilities.updateComponentTreeUI(IJ.getInstance());
				//Display.updateComponentTreeUI();
			}
		} catch (ClassNotFoundException cnfe) {
			Utils.log2("Could not find Nimbus L&F");
		} catch (Exception e) {
			IJError.print(e);
		}
	}

	/** Prevents ControlWindow from displaying projects.*/
	static public void setGUIEnabled(boolean b) {
		gui_enabled = b;
		if (gui_enabled && null != frame) frame.setVisible(true);
	}

	static public final boolean isGUIEnabled() {
		return gui_enabled;
	}

	/** Returns null if there are no projects */
	synchronized static public Set<Project> getProjects() {
		synchronized (LOCK) {
			if (null == ht_projects) return null;
			return ht_projects.keySet();
		}
	}

	static private MouseListener tool_listener = new MouseAdapter() {
		private int last_tool = ij.gui.Toolbar.RECTANGLE;
		public void mousePressed(MouseEvent me) {
			int tool = ini.trakem2.utils.ProjectToolbar.getToolId();
			if (tool != last_tool) {
				last_tool = tool;
				ini.trakem2.display.Display.toolChanged(tool);
			}
		}
	};

	static private void destroy() {
		synchronized(LOCK) {
			if (null == instance) return;
			if (IJ.isWindows()) StdOutWindow.quit();
			Display3D.destroy();
			if (null != ht_projects) {
				// destroy open projects, release memory
				Enumeration<Project> e = ht_projects.keys();
				Project[] project = new Project[ht_projects.size()]; //concurrent modifications ..
				int next = 0;
				while (e.hasMoreElements()) {
					project[next++] = e.nextElement();
				}
				for (int i=0; i<next; i++) {
					ht_projects.remove(project[i]);
					if (!project[i].destroy()) {
						return;
					}
				}
				ht_projects = null;
			}
			if (null != tabs) {
				tabs.removeMouseListener((tabs.getMouseListeners())[0]);
				tabs = null;
			}
			if (null != frame) {
				final JFrame fr = frame;
				SwingUtilities.invokeLater(new Runnable() { public void run() {
					fr.setVisible(false);
					fr.dispose();
					if (null != ij.gui.Toolbar.getInstance()) ij.gui.Toolbar.getInstance().repaint();
				}});
				frame = null;
				ProjectToolbar.destroy();
			}
			if (null != tool_listener && null != ij.gui.Toolbar.getInstance()) {
				ij.gui.Toolbar.getInstance().removeMouseListener(tool_listener);
			}
			Utils.destroy(instance);
			Loader.destroyPreloader(instance);
			instance.command_listener.destroy();
			instance.command_listener = null;
			if (null != instance.red_phone) instance.red_phone.quit();
			instance = null;
		}
	}

	static private boolean hooked = false;

	/** Beware that this method is asynchronous, as it delegates the launching to the SwingUtilities.invokeLater method to avoid havoc with Swing components. */
	static public void add(final Project project, final TemplateTree template_tree, final ProjectTree thing_tree, final LayerTree layer_tree) {

		final Runnable[] other = new Runnable[2];

        if (!gui_enabled)
        {
            return;
        }

		final Runnable gui_thread = new Runnable() {
			public void run() {

		synchronized (LOCK) {

			getInstance(); // init
			if (null == frame) {
				if (!hooked) {
					Runtime.getRuntime().addShutdownHook(new Thread() { // necessary to disconnect properly from the database instead of with an EOF, and also to ask to save changes for FSLoader projects.
						public void run() {
							// threaded quit???// if (null != IJ.getInstance() && !IJ.getInstance().quitting()) IJ.getInstance().quit(); // to ensure the Project offers a YesNoDialog, not a YesNoCancelDialog
							ControlWindow.destroy();
						}
					});
					hooked = true;
				}
				frame = createJFrame("TrakEM2");
				frame.setBackground(Color.white);
				frame.getContentPane().setBackground(Color.white);
				frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
				frame.addWindowListener(new WindowAdapter() {
					public void windowClosing(WindowEvent we) {
						synchronized (LOCK) {
							if (!Utils.check("Close " + (1 == ht_projects.size() ? "the project?" : "all projects?"))) {
								return;
							}
							destroy();
						}
					}
					public void windowClosed(WindowEvent we) {
						// ImageJ is quitting (never detected, so I added the dispose extension above)
						destroy();
					}
				});
				tabs = new JTabbedPane(JTabbedPane.TOP);
				tabs.setBackground(Color.white);
				tabs.setMinimumSize(new Dimension(500, 400));
				tabs.addMouseListener(new TabListener());
				frame.getContentPane().add(tabs);
				// register with ij.WindowManager so that when ImageJ quits it can be detected
				// ADDS annoying dialog "Are you sure you want to close ImageJ?"//ij.WindowManager.addWindow(frame);
				// Make the JPopupMenu instances be heavy weight components by default in Windows and elsewhere, not macosx.
				if (!ij.IJ.isMacOSX()) JPopupMenu.setDefaultLightWeightPopupEnabled(false);
				// make the tool tip text for JLabel be heavy weight so they don't hide under the AWT DisplayCanvas
				javax.swing.ToolTipManager.sharedInstance().setLightWeightPopupEnabled(false);
			}

			// create the tab
			final JSplitPane tab = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
			tab.setBackground(Color.white);
			// store the tab linked to the project (before setting the trees, so that they won't get repainted and get in trouble not being able to get a project title if the project has no name)
			if (null == ht_projects) ht_projects = new Hashtable<Project,JSplitPane>();
			ht_projects.put(project, tab);

			// create a scrolling pane for the template_tree
			final JScrollPane scroll_template = new JScrollPane(template_tree);
			scroll_template.setBackground(Color.white);
			scroll_template.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEmptyBorder(0,5,0,5), "Template"));
			scroll_template.setMinimumSize(new Dimension(0, 100));
			scroll_template.setPreferredSize(new Dimension(300, 400));

			// create a scrolling pane for the thing_tree
			final JScrollPane scroll_things   = new JScrollPane(thing_tree);
			scroll_things.setBackground(Color.white);
			scroll_things.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEmptyBorder(0,5,0,5), "Project Objects"));
			scroll_things.setMinimumSize(new Dimension(0, 100));
			scroll_things.setPreferredSize(new Dimension(300, 400));

			// create a scrolling pane for the layer_tree
			final JScrollPane scroll_layers = new JScrollPane(layer_tree);
			scroll_layers.setBackground(Color.white);
			scroll_layers.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEmptyBorder(0,5,0,5), "Layers"));
			scroll_layers.setMinimumSize(new Dimension(0, 100));
			scroll_layers.setPreferredSize(new Dimension(300, 400));

			// make a new tab for the project
			final JSplitPane left = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, scroll_template, scroll_things);
			left.setBackground(Color.white);
			left.setPreferredSize(new Dimension(600, 400));

			// setup the tab
			tab.setBackground(Color.white);
			tab.setLeftComponent(left);
			tab.setRightComponent(scroll_layers);
			tab.setPreferredSize(new Dimension(900, 400));

			// add the tab, titled with the project title
			tabs.addTab(project.toString(), new CloseIcon(), tab);
			tabs.setSelectedIndex(tabs.getTabCount() -1);

			// the frame is created ANYWAY, it is just not made visible if !gui_enabled
			if (!frame.isVisible() && gui_enabled) {
				frame.pack();
				frame.setVisible(true);
				frame.toFront();
			}
			Rectangle bounds = frame.getBounds();
			if (bounds.width < 200) {
				frame.setSize(new Dimension(200, bounds.height > 100 ? bounds.height : 100));
				frame.pack();
			}
			// now set minimum size again, after showing it (stupid Java), so they are shown correctly (opened) but can be completely collapsed to the sides.
			try { Thread.sleep(100); } catch (Exception e) {}
			//scroll_template.setMinimumSize(new Dimension(0, 100));
			//scroll_things.setMinimumSize(new Dimension(0, 100));
			//scroll_layers.setMinimumSize(new Dimension(0, 100));
			tab.setDividerLocation(0.66D); // first, so that left is visible! setDividerLocation depends on the dimensions as they are when painted on the screen
			left.setDividerLocation(0.5D);

			// select the SELECT tool if it's the first open project
			if (1 == ht_projects.size() && gui_enabled) {
				ProjectToolbar.setTool(ProjectToolbar.SELECT);
			}

			// so wait until the setDividerLocation of the 'tab' has finished, then do the left one
			other[0] = new Runnable() {
				public void run() {
					tab.setDividerLocation(0.66D);
				}
			};
			other[1] = new Runnable() {
				public void run() {
					left.setDividerLocation(0.5D);
				}
			};
			// FINALLY! WHAT DEGREE OF IDIOCY POSSESSED SWING DEVELOPERS?

		}

		}};

		new Thread() {
			{ setPriority(Thread.NORM_PRIORITY); }
			public void run() {
				try {
					SwingUtilities.invokeAndWait(gui_thread);
					for (int i=0; i<other.length; i++) {
						SwingUtilities.invokeAndWait(other[i]);
					}
					//Utils.log2("done");
				} catch (Exception e) { IJError.print(e); }
			}
		}.start();
	}

	synchronized static public Project getActive() {
		synchronized (LOCK) {
			if (null == tabs || 0 == ht_projects.size()) return null;
			if (1 == ht_projects.size()) return (Project)ht_projects.keySet().iterator().next();
			else {
				Component c = tabs.getSelectedComponent();
				for (final Map.Entry<Project,JSplitPane> e : ht_projects.entrySet()) {
					if (e.getValue().equals(c)) return e.getKey();
				}
			}
			return null;
		}
	}

	static public void remove(final Project project) {
		synchronized (LOCK) {
			if (null == tabs || null == ht_projects) return;
			if (null == instance) return;
			if (ht_projects.containsKey(project)) {
				int n_tabs = 0;
				JSplitPane tab = (JSplitPane)ht_projects.get(project);
				tabs.remove(tab);
				ht_projects.remove(project);
				n_tabs = tabs.getTabCount();
				// close the ControlWindow if no projects remain open.
				if (0 == n_tabs) {
					destroy();
				}
			}
		}
	}

	static public void updateTitle(final Project project) {
		SwingUtilities.invokeLater(new Runnable() {
			public void run() {
				synchronized (LOCK) {
					if (null == tabs) return;
					if (ht_projects.containsKey(project)) {
						if (null == instance) return;
						JSplitPane tab = (JSplitPane)ht_projects.get(project);
						int index = tabs.indexOfComponent(tab);
						if (-1 != index) {
							tabs.setTitleAt(index, project.toString());
						}
					}
				}
			}
		});
	}

	private static class TabListener extends MouseAdapter {
		public void mouseReleased(MouseEvent me) {
			if (me.isConsumed()) return;
			synchronized (LOCK) {
				if (null == tabs) return;
				int i_tab = tabs.getSelectedIndex();
				Component comp = tabs.getComponentAt(i_tab);
				Icon icon = tabs.getIconAt(i_tab);
				if (icon instanceof CloseIcon) {
					CloseIcon ci = (CloseIcon)icon;
					// find the project
					Project project = null;
					for (final Map.Entry<Project,JSplitPane> e: ht_projects.entrySet()) {
						project = e.getKey();
						if (e.getValue().equals(comp)) break;
					}
					if (ci.contains(me.getX(), me.getY())) {
						if (null == project) return;
						// ask for confirmation before closing
						if (!Utils.check("Close the project " + project.toString() + " ?")) {
							return;
						}
						// proceed to close:
						if (project.destroy()) { // will call ControlWindow.remove(project)
							ci.flush();
						}
					} else if (2 == me.getClickCount()) {
						// pop dialog to rename the project
						if (null == project) return;
						project.getProjectTree().rename(project.getRootProjectThing());
					}
				}
			}
		}
	}

	static private class CloseIcon implements Icon {

		private Icon icon;
		private BufferedImage img;
		private int x = 0;
		private int y = 0;

		CloseIcon() {
			img = frame.getGraphicsConfiguration().createCompatibleImage(20, 16, Transparency.TRANSLUCENT);
			Graphics2D g = img.createGraphics();
			g.setColor(Color.black);
			g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
			g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,  RenderingHints.VALUE_ANTIALIAS_ON);
			g.drawOval(4 + 2, 2, 12, 12);
			g.setStroke(new BasicStroke(1.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
			g.drawLine(4 + 4, 4, 4 + 11, 12);
			g.drawLine(4 + 4, 12, 4 + 11, 4);
			icon = new ImageIcon(img);
		}

		public void paintIcon(Component c, Graphics g, int x, int y) {
			// store coordinates of the last painting event
			this.x = x;
			this.y = y;
			icon.paintIcon(c, g, x, y );
		}

		public boolean contains(int x, int y) {
			return new Rectangle(this.x, this.y, icon.getIconWidth(), icon.getIconHeight()).contains(x, y);
		}

		public int getIconWidth() { return icon.getIconWidth(); }
		public int getIconHeight() { return icon.getIconHeight(); }

		public void flush() {
			if (null != img) {
				img.flush();
				img = null;
			}
		}
	}

	/** For the generic dialogs to be parented properly. */
	static public GenericDialog makeGenericDialog(String title) {
		Frame f = (null == frame ? IJ.getInstance() : (java.awt.Frame)frame);
		return new GenericDialog(title, f);
	}

	/** For the YesNoCancelDialog dialogs to be parented properly. */
	static public YesNoCancelDialog makeYesNoCancelDialog(String title, String msg) {
		Frame f = (null == frame ? IJ.getInstance() : (java.awt.Frame)frame);
		return new YesNoCancelDialog(f, title, msg);
	}
	/** For the YesNoDialog dialogs to be parented properly. */
	static public YesNoDialog makeYesNoDialog(String title, String msg) {
		Frame f = (null == frame ? IJ.getInstance() : (java.awt.Frame)frame);
		return new YesNoDialog(f, title, msg);
	}

	static public void toFront() {
		synchronized (instance) {
			if (null != frame) frame.toFront();
		}
	}

	/** Appends to the buffer data relative to the viewport of the given tree. */
	/*
	static public void exportTreesXML(final Project project, final StringBuffer sb_data, final String indent, final JTree tree) {
		// find the JSplitPane of the given tree
		JScrollPane[] jsp = new JScrollPane[1];
		jsp[0] = null;
		findJSP((Container)ht_projects.get(project), tree, jsp);
		if (null == jsp[0]) {
			Utils.log2("Cound not find a JScrollPane for the tree.");
			return;
		}
		// else, we have it
		tree.exportXML(sb_data, indent, jsp[0]);
	}
	*/

	// /** Recursive. */
	/*
	static private void findJSP(final Container parent, final JTree  tree, final JScrollPane[] jsp) {
		if (null != jsp[0]) return;
		Component[] comps = parent.getComponents();
		for (int i=0; i<comps.length; i++) {
			if (comps[i] instanceof Container) {
				findJSP(comps[i], tree, jsp);
			} else if (comps[i].equals(tree)) {
				jsp[0] = (JScrollPane)parent; // MUST be
				break;
			}
		}
	}
	*/

	static public void startWaitingCursor() { setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); }

	static public void endWaitingCursor() { setCursor(Cursor.getDefaultCursor()); }

	static private void setCursor(final Cursor c) {
		Utils.invokeLater(new Runnable() { public void run() {
			if (null != IJ.getInstance()) IJ.getInstance().setCursor(c);
			ini.trakem2.display.Display.setCursorToAll(c);
			if (null != frame && frame.isVisible()) frame.setCursor(c); // the ControlWindow frame
		}});
	}

	/** Returns -1 if not found. */
	synchronized static public int getTabIndex(final Project project) {
		if (null == project || null == ht_projects) return -1;
		Component tab = (Component)ht_projects.get(project);
		if (null == tab) return -1;
		return tabs.indexOfComponent(tab);
	}

	static private Image icon = null;

	/** Returns a new JFrame with the proper icon from ImageJ.iconPath set, if any. */
	static public JFrame createJFrame(final String title) {
		if (null == instance) return new JFrame(title);
		return instance.newJFrame(title);
	}
	synchronized private JFrame newJFrame(final String title) {
		final JFrame frame = new JFrame(title);

		if (null == icon) {
			try {
				Field mic = ImageJ.class.getDeclaredField("iconPath");
				mic.setAccessible(true);
				String path = (String) mic.get(IJ.getInstance());
				icon = IJ.getInstance().createImage((ImageProducer) new URL("file:" + path).getContent());
			} catch (Exception e) {}
		}

		if (null != icon) frame.setIconImage(icon);
		return frame;
	}
}