/*******************************************************************************
 * Copyright (c) MOBAC 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 2 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/>.
 ******************************************************************************/

package mobac.gui.components;

import java.awt.Color;
import java.awt.Component;
import java.awt.Font;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.io.File;
import java.util.Enumeration;
import java.util.Vector;

import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JTree;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;

import org.apache.commons.lang3.StringEscapeUtils;

import mobac.mapsources.AbstractMultiLayerMapSource;
import mobac.program.interfaces.FileBasedMapSource;
import mobac.program.interfaces.HttpMapSource;
import mobac.program.interfaces.MapSource;
import mobac.program.model.MapSourceLoaderInfo;
import mobac.utilities.I18nUtils;
import mobac.utilities.Utilities;

//import org.apache.log4j.Logger;

/**
 * @author Maksym "elmuSSo" Kondej
 * 
 *         This class holds methods needed for managing a MapSources loaded into a JTree structure
 */
public class JMapSourceTree extends JTree {

	/**
	 * ComparableTreeNode is a DefaultMutableTreeNode with a mechanism of comparison with other nodes. This is used when
	 * putting a node into a tree in an alphabetic order and placing folder nodes above other types of nodes.
	 */
	class ComparableTreeNode extends DefaultMutableTreeNode implements Comparable<DefaultMutableTreeNode> {
		private static final long serialVersionUID = 1L;

		public ComparableTreeNode(MapSource mapSource) {
			super(mapSource);
		}

		public ComparableTreeNode(String string) {
			super(string);
		}

		@Override
		public int compareTo(DefaultMutableTreeNode treeNode) {
			Class<? extends Object> thisObjectClass = this.getUserObject().getClass();
			Class<? extends Object> comparedObjectClass = treeNode.getUserObject().getClass();

			// This rule will always put "folders" above MapSources.
			if (thisObjectClass.equals(folderClass) != comparedObjectClass.equals(folderClass)) {
				return thisObjectClass.equals(folderClass) ? -1 : 1;
			}

			return this.toString().compareToIgnoreCase(treeNode.toString());
		}

	}

	/**
	 * CustomIconRenderer was created to manage icons within a tree
	 */
	static class CustomIconRenderer extends DefaultTreeCellRenderer {
		private static final long serialVersionUID = 1L;

		private ImageIcon multiLayerIcon, fileBasedIcon, httpIcon, debugIcon, folderOpenedIcon, folderClosedIcon;

		public CustomIconRenderer() {
			multiLayerIcon = new ImageIcon(Utilities.getResourceImageUrl("icon_multilayer_ms.png"));
			fileBasedIcon = new ImageIcon(Utilities.getResourceImageUrl("icon_filebased_ms.png"));
			httpIcon = new ImageIcon(Utilities.getResourceImageUrl("icon_http_ms.png"));
			debugIcon = new ImageIcon(Utilities.getResourceImageUrl("icon_debug_ms.png"));
			folderOpenedIcon = new ImageIcon(Utilities.getResourceImageUrl("icon_folder_opened.png"));
			folderClosedIcon = new ImageIcon(Utilities.getResourceImageUrl("icon_folder_closed.png"));
		}

		public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded,
				boolean leaf, int row, boolean hasFocus) {
			super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);

			setTextSelectionColor(getTextNonSelectionColor());
			setBorderSelectionColor(null);

			if (selected) {
				// Special style for selected node
				this.setFont(getFont().deriveFont(Font.BOLD));
				setBackgroundSelectionColor(mapSourceInTreeHighlightColor);
			} else {
				this.setFont(getFont().deriveFont(Font.PLAIN));
				setBackgroundSelectionColor(null);
			}

			// Adding additional left margin of icon
			setBorder(BorderFactory.createEmptyBorder(0, 2, 0, 0));

			// Setting icons for opened/closed folder
			setOpenIcon(folderOpenedIcon);
			setClosedIcon(folderClosedIcon);

			Object treeNodeObject = ((DefaultMutableTreeNode) value).getUserObject();

			// Giving a appropriate icon to each category of MapSource (taken from node's userObject)
			if (treeNodeObject instanceof AbstractMultiLayerMapSource) {
				setIcon(multiLayerIcon);
			} else if (treeNodeObject instanceof FileBasedMapSource) {
				setIcon(fileBasedIcon);
			} else if (treeNodeObject instanceof HttpMapSource) {
				setIcon(httpIcon);
			} else if (treeNodeObject instanceof MapSource) {
				setIcon(debugIcon);
			}
			return this;
		}
	}

	// private final Logger log = Logger.getLogger(JMapSourceTree.class);
	private static final long serialVersionUID = 1L;

	// Specifying a class which will determine if a node is a folder
	static final Class<String> folderClass = String.class;

	private static Color mapSourceInTreeHighlightColor = new Color(230, 245, 255);

	private Vector<MapSource> mapSources;
	private MapSource selectedMapSource, previouslySelectedMapSource;

	private ComparableTreeNode rootNode = new ComparableTreeNode("Maps sources root");
	private DefaultTreeModel treeModel = new DefaultTreeModel(rootNode);

	public JMapSourceTree(Vector<MapSource> enabledOrderedMapSources) {
		super();
		// Setting a cell renderer which will provide proper icons behavior
		setCellRenderer(new CustomIconRenderer());
		initialize(enabledOrderedMapSources);
		setRootVisible(false);
		getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
		setExpandsSelectedPaths(true);
		setToggleClickCount(1);
		setToolTipText(I18nUtils.localizedStringForKey("lp_map_source_tree_tips"));
	}

	/**
	 * This method takes a list of all valid and enabled MapSources, saves them into internal list of MapSources, and
	 * then generates a tree model basing on them
	 * 
	 * @param enabledOrderedMapSources
	 *            - list of all MapSources that must be put into a tree.
	 */
	public void initialize(Vector<MapSource> enabledOrderedMapSources) {
		mapSources = enabledOrderedMapSources;
		resetTree();
		generateTreeModel();
		super.setModel(treeModel);
	}

	/**
	 * This method will take a MapSource and will analyzed it's folder path to check if there is no need to put it into
	 * any folders/subfolders structure.
	 * 
	 * @param mapSource
	 *            - this mapSource's path will be analyzed
	 */
	private void addChildBasedOnFolderPath(MapSource mapSource) {
		MapSourceLoaderInfo loaderInfo = mapSource.getLoaderInfo();
		String[] folderPath = null;
		if (loaderInfo != null) {
			folderPath = loaderInfo.getRelativePath();
		}
		ComparableTreeNode parent = rootNode;
		if (folderPath != null) {
			for (String folderPathElementName : folderPath) {

				ComparableTreeNode folderPathElement = null;
				ComparableTreeNode childFound = getChildByUserObject(parent, folderPathElementName);

				if (childFound != null) {
					folderPathElement = childFound;
				} else {
					folderPathElement = new ComparableTreeNode(folderPathElementName);
					insertInRightOrder(parent, folderPathElement);
				}
				parent = folderPathElement;
			}
		}
		ComparableTreeNode newLeaf = new ComparableTreeNode(mapSource);
		insertInRightOrder(parent, newLeaf);
	}

	/**
	 * This method clears a tree and its model to make sure it is ready for (re)initialization
	 */
	private void resetTree() {
		this.setModel(null);
		rootNode.removeAllChildren();
	}

	/**
	 * This method inserts a node into a tree in an order based on comparator from ComparableTreeNode
	 * 
	 * @param parentNode
	 *            - a parent node to which a new node will be attached
	 * @param insertedNode
	 *            - a new node
	 */
	private void insertInRightOrder(ComparableTreeNode parentNode, ComparableTreeNode insertedNode) {
		int parentChildCount = treeModel.getChildCount(parentNode);
		int insertIndex = 0;

		if (parentChildCount != 0) {
			insertIndex = parentChildCount;
			for (int i = parentChildCount - 1; i >= 0; i--) {
				ComparableTreeNode child = (ComparableTreeNode) treeModel.getChild(parentNode, i);

				if (insertedNode.compareTo(child) <= 0) {
					insertIndex = i;
				} else {
					break;
				}
			}
		}
		treeModel.insertNodeInto(insertedNode, parentNode, insertIndex);
	}

	/**
	 * This method searches for a parentNode's child relying on its name
	 * 
	 * @param parentNode
	 *            - its children will be iterated during search
	 * @param nodeName
	 *            - name of a sought node
	 * @return found child, or null if child was not found
	 */
	private static ComparableTreeNode getChildByUserObject(ComparableTreeNode parentNode, String nodeName) {
		Enumeration<TreeNode> childsOfParent = parentNode.breadthFirstEnumeration();
		while (childsOfParent.hasMoreElements()) {
			ComparableTreeNode child = (ComparableTreeNode) childsOfParent.nextElement();
			if (child.getUserObject().equals(nodeName)) {
				return child;
			}
		}
		return null;
	}

	/**
	 * Method is iterating over all MapSources and adding them to a tree's model in an appropriate place
	 */
	private void generateTreeModel() {
		for (MapSource mapSource : this.mapSources) {
			addChildBasedOnFolderPath(mapSource);
		}
	}

	/**
	 * This method searches for an index of a MapSource with a requested name
	 * 
	 * @param mapSourceName
	 *            - name of a sought MapSource
	 * @return index of found MapSource or -1 in case nothing was found
	 */
	private int getMapSourceIndexByName(String mapSourceName) {
		for (int i = 0; i < mapSources.size(); i++) {
			MapSource mapSource = mapSources.get(i);
			String mapSourceAsString = mapSource.toString();
			if (mapSourceAsString.equals(mapSourceName)) {
				return i;
			}
		}
		return -1;
	}

	/**
	 * This method searches for a MapSource object with a requested name
	 * 
	 * @param mapSourceName
	 *            - name of a sought MapSource
	 * @return found MapSource object or null otherwise
	 */
	private MapSource getMapSourceByName(String mapSourceName) {
		int mapSourceIndex = getMapSourceIndexByName(mapSourceName);

		if (mapSourceIndex == -1) {
			return null;
		}
		return this.mapSources.get(mapSourceIndex);
	}

	/**
	 * This method searches for a TreePath of a requested MapSouce
	 * 
	 * @param mapSource
	 *            - analyzed mapSource object
	 * @return TreePath of a specified MapSource or null otherwise
	 */
	private TreePath findTreePathOfMapSource(MapSource mapSource) {
		Enumeration<TreeNode> rootDescendants = rootNode.depthFirstEnumeration();
		while (rootDescendants.hasMoreElements()) {
			ComparableTreeNode descendantNode = (ComparableTreeNode) rootDescendants.nextElement();

			if (descendantNode.getUserObject().equals(mapSource)) {
				return new TreePath(descendantNode.getPath());
			}
		}
		return null;
	}

	/**
	 * This method is a main gateway for selecting a mapSource, it should be used by other functions. Firstly it
	 * searches for a requested MapSource and if it will find it, it will mark it internally as selected and then it
	 * will be passed to be selected in a graphical tree.
	 * 
	 * @param mapSourceToSelect
	 *            - MapSource that will be internally marked as selected
	 * @return a true if MapSource was found, false otherwise
	 */
	public boolean selectMapSource(MapSource mapSourceToSelect) {
		int foundMapSourceIndex = mapSources.indexOf(mapSourceToSelect);
		if (foundMapSourceIndex != -1) {
			previouslySelectedMapSource = selectedMapSource;
			// Marking internally as selected MapSource
			selectedMapSource = mapSourceToSelect;
			chooseAndShowTreeNodeInTree(mapSourceToSelect);
			return true;
		}
		return false;
	}

	/**
	 * This method is selecting a requested MapSource and focusing on it IN A TREE. MapSource is not being marked
	 * internally as selected in this method.
	 * 
	 * @param mapSourceToSelect
	 *            - MapSource that will be selected
	 */
	private void chooseAndShowTreeNodeInTree(MapSource mapSourceToSelect) {
		TreePath pathFound = findTreePathOfMapSource(mapSourceToSelect);
		if (pathFound != null) {
			// Expand all folders and subfolders to show a chosen node
			expandPath(pathFound.getParentPath());
			// Choose the node
			setSelectionPath(pathFound);
			// Scroll a JTree viewport to the chosen node
			scrollPathToVisibleVerticalOnly(pathFound);
			// Signaling to refresh a node, after its font/color was changed
			((DefaultTreeModel) this.getModel()).nodeChanged((TreeNode) pathFound.getLastPathComponent());
		}
	}

	/**
	 * This method will vertically scroll to the requested treePath, but without touching a horizontal scroll-bar
	 * 
	 * @param treePath
	 *            - TreePath to be vertically scrolled to
	 */
	private void scrollPathToVisibleVerticalOnly(TreePath treePath) {
		if (treePath != null) {
			makeVisible(treePath);

			Rectangle pathBounds = getPathBounds(treePath);
			if (pathBounds != null) {
				pathBounds.x = 0;
				scrollRectToVisible(pathBounds);
			}
		}
	}

	/**
	 * This method should be called after clicking on any node in a tree. It gets a clicked/chosen node from a tree and
	 * pass it to the method that will mark it as internally selected.
	 * 
	 * @return true if clicked MapSource was successfully selected internally, false otherwise
	 */
	public boolean selectClickedMapSource() {
		if (getSelectionPath() != null) {
			String selectedTreeElement = getSelectionPath().getLastPathComponent().toString();
			MapSource foundMapSource = getMapSourceByName(selectedTreeElement);
			if (foundMapSource == null) {
				// React if a non-MapSource node was clicked
				setSelectionPath(findTreePathOfMapSource(previouslySelectedMapSource));
			} else {
				// A mapSource type was clicked
				return selectMapSource(foundMapSource);
			}
		}
		return false;
	}

	/**
	 * @return internally selected MapSource
	 */
	public MapSource getSelectedMapSource() {
		return selectedMapSource;
	}

	/**
	 * Selects a next MapSource from internal list of MapSources
	 */
	public boolean selectNextMapSource() {
		if (mapSources.lastElement().equals(selectedMapSource)) {
			return false;
		} else {
			int indexOfSelectedMapSource = mapSources.indexOf(selectedMapSource);
			return selectMapSource(mapSources.get(indexOfSelectedMapSource + 1));
		}
	}

	/**
	 * Selects a previous MapSource from internal list of MapSources
	 */
	public boolean selectPreviousMapSource() {
		if (mapSources.firstElement().equals(selectedMapSource)) {
			return false;
		} else {
			int indexOfSelectedMapSource = mapSources.indexOf(selectedMapSource);
			return selectMapSource(mapSources.get(indexOfSelectedMapSource - 1));
		}
	}

	/**
	 * Selects a first MapSource from internal list of MapSources. Because internal list is not in the same order as
	 * nodes in tree, user gets a random (from his point of view) MapSource. Perhaps, this method should ask user what
	 * MapSource he want to pick, or maybe automatically select a closest sibling of the previously selected MapSource?
	 */
	public boolean selectFirstMapSource() {
		return selectMapSource(mapSources.get(0));
	}

	/**
	 * @return a size of internal list of MapSources
	 */
	public int getMapSourcesCount() {
		return mapSources.size();
	}

	/**
	 * Check if the mouse cursor was over a clickable node
	 * 
	 * @param eventPoint
	 *            - a mouse position coordinates
	 * @return true if a node was clickable, false otherwise
	 */
	public boolean isLocationClickable(Point eventPoint) {
		int x = (int) eventPoint.getX();
		int y = (int) eventPoint.getY();
		TreePath treePathForXY = getPathForLocation(x, y);

		// If a node is an ancestor of a currently selected node - it can't be closed, so it is unclickable.
		if (treePathForXY != null && treePathForXY.isDescendant(findTreePathOfMapSource(selectedMapSource))) {
			return false;
		}

		boolean isInside = false;
		if (treePathForXY != null) {
			Rectangle pathBounds = this.getPathBounds(treePathForXY);
			isInside = pathBounds.contains(eventPoint);
		}
		return isInside;
	}

	@Override
	public String getToolTipText(MouseEvent event) {
		if (getRowForLocation(event.getX(), event.getY()) == -1)
			return "";
		TreePath curPath = getPathForLocation(event.getX(), event.getY());
		Object lastPathComponent = curPath.getLastPathComponent();
		if (lastPathComponent == null)
			return null;

		Object userObject = ((ComparableTreeNode) lastPathComponent).getUserObject();
		if (userObject.getClass().equals(folderClass)) {
			return null;
		}
		return generateMapSourceTooltip((MapSource) userObject);
	}

	/**
	 * Static method used for dynamic generation of a tooltip with information about a mapSource
	 * 
	 * @param mapSource
	 *            - of which information will be put into a tooltip
	 * @return generated tooltip string
	 */
	public static String generateMapSourceTooltip(MapSource mapSource) {
		boolean multiLayer = (mapSource instanceof AbstractMultiLayerMapSource);
		boolean fileBased = (mapSource instanceof FileBasedMapSource);

		// Getting a localized string for an input to tooltip
		String locName = I18nUtils.localizedStringForKey("lp_map_source_tooltip_layer_name");
		String locInternalName = I18nUtils.localizedStringForKey("lp_map_source_tooltip_inernal_name");
		String locType = I18nUtils.localizedStringForKey("lp_map_source_tooltip_type");
		String locLoadedFrom = I18nUtils.localizedStringForKey("lp_map_source_tooltip_loaded_from");
		String locFileName = I18nUtils.localizedStringForKey("lp_map_source_tooltip_file_name");

		String locMultiLayer = I18nUtils.localizedStringForKey("lp_map_source_layer_multi");
		String locSingleLayer = I18nUtils.localizedStringForKey("lp_map_source_layer_single");
		String locFileBased = I18nUtils.localizedStringForKey("lp_map_source_layer_file_based");
		String locWebBased = I18nUtils.localizedStringForKey("lp_map_source_layer_web_based");

		// Getting a values for some attributes
		String name = StringEscapeUtils.escapeHtml4(mapSource.toString());
		String nameInternal = StringEscapeUtils.escapeHtml4(mapSource.getName());
		String type1 = multiLayer ? locMultiLayer : locSingleLayer;
		String type2 = fileBased ? locFileBased : locWebBased;

		String toolTipString = locName + ": <b>%s</b><br>" + locInternalName + ": %s<br>" + locType + ": %s (%s)";
		toolTipString = String.format(toolTipString, name, nameInternal, type1, type2);

		MapSourceLoaderInfo info = mapSource.getLoaderInfo();
		if (info != null) {
			toolTipString += "<br>" + locLoadedFrom + ": " + info.getLoaderType().displayName;

			File f = info.getSourceFile();
			if (f != null) {
				toolTipString += "<br>" + locFileName + ": <tt>" + StringEscapeUtils.escapeHtml4(f.getName()) + "</tt>";
			}
		}

		return "<html>" + toolTipString + "</html>";
	}
}