/*
 * =====================================================
 * #%L
 * =====================================================
 *   _____                _     ____  _   _       _   _
 *  |_   _|_ __ _   _ ___| |_  / __ \| | | | ___ | | | |
 *    | | | '__| | | / __| __|/ / _` | |_| |/ __|| |_| |
 *    | | | |  | |_| \__ \ |_| | (_| |  _  |\__ \|  _  |
 *    |_| |_|   \__,_|___/\__|\ \__,_|_| |_||___/|_| |_|
 *                             \____/
 * 
 * =====================================================
 * 
 * Hochschule Hannover
 * (University of Applied Sciences and Arts, Hannover)
 * Faculty IV, Dept. of Computer Science
 * Ricklinger Stadtweg 118, 30459 Hannover, Germany
 * 
 * Email: trust@f4-i.fh-hannover.de
 * Website: http://trust.f4.hs-hannover.de/
 * 
 * This file is part of visitmeta-visualization, version 0.6.0,
 * implemented by the Trust@HsH research group at the Hochschule Hannover.
 * %%
 * Copyright (C) 2012 - 2016 Trust@HsH
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */
package de.hshannover.f4.trust.visitmeta.gui;

import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.List;
import java.util.Map.Entry;
import java.util.Observable;
import java.util.Observer;

import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;

import org.apache.commons.collections15.map.HashedMap;
import org.apache.log4j.Logger;

import de.hshannover.f4.trust.visitmeta.datawrapper.ExpandedLink;
import de.hshannover.f4.trust.visitmeta.datawrapper.GraphContainer;
import de.hshannover.f4.trust.visitmeta.datawrapper.NodeIdentifier;
import de.hshannover.f4.trust.visitmeta.datawrapper.NodeMetadata;
import de.hshannover.f4.trust.visitmeta.datawrapper.NodeType;
import de.hshannover.f4.trust.visitmeta.datawrapper.Position;
import de.hshannover.f4.trust.visitmeta.datawrapper.SettingManager;
import de.hshannover.f4.trust.visitmeta.datawrapper.TimeHolder;
import de.hshannover.f4.trust.visitmeta.datawrapper.TimeManagerCreation;
import de.hshannover.f4.trust.visitmeta.datawrapper.TimeManagerDeletion;
import de.hshannover.f4.trust.visitmeta.datawrapper.UpdateContainer;
import de.hshannover.f4.trust.visitmeta.graphCalculator.FacadeLogic;
import de.hshannover.f4.trust.visitmeta.graphCalculator.LayoutType;
import de.hshannover.f4.trust.visitmeta.graphDrawer.GraphPanel;
import de.hshannover.f4.trust.visitmeta.graphDrawer.GraphPanelFactory;
import de.hshannover.f4.trust.visitmeta.graphDrawer.graphicwrapper.GraphicWrapper;
import de.hshannover.f4.trust.visitmeta.gui.contextmenu.ContextMenuItem;
import de.hshannover.f4.trust.visitmeta.gui.contextmenu.ContextMenuItemFactory;
import de.hshannover.f4.trust.visitmeta.interfaces.Propable;

public class GraphConnection implements Observer {
	private static final Logger LOGGER = Logger.getLogger(GraphConnection.class);
	private GraphContainer mContainer = null;
	private FacadeLogic mFacadeLogic = null;
	private GraphPanel mGraphPanel = null;
	private SettingManager mSettingManager = null;
	private TimeHolder mTimeHolder = null;
	private TimeManagerCreation mTimerCreation = null;
	private TimeManagerDeletion mTimerDeletion = null;
	private boolean mAddHighlights = true;
	private ConnectionTab mParentTab = null;
	private HashedMap<Observable, Observable> mObservables = new HashedMap<>();
	private boolean mIsPropablePicked = false;
	private List<ContextMenuItem> mContextMenuItems;

	/**
	 * @param container
	 *            Contains information about the Connection.
	 */
	public GraphConnection(GraphContainer container) {
		mContainer = container;
		mFacadeLogic = mContainer.getFacadeLogic();
		mTimeHolder = mContainer.getTimeHolder();
		mSettingManager = mContainer.getSettingManager();
		mGraphPanel = GraphPanelFactory.getGraphPanel("Piccolo2D", this);

		mTimerCreation = mContainer.getTimeManagerCreation();
		mTimerDeletion = mContainer.getTimeManagerDeletion();
		mTimerCreation.setController(this);
		mTimerDeletion.setController(this);

		mSettingManager.addObserver(this);
		mFacadeLogic.addObserver(this);
		mTimeHolder.addObserver(this);

		mContextMenuItems = ContextMenuItemFactory.getAll();
	}

	public SettingManager getSettingManager() {
		return mSettingManager;
	}

	public FacadeLogic getLogic() {
		return mFacadeLogic;
	}

	public void setParentTab(ConnectionTab parentTab) {
		this.mParentTab = parentTab;
	}

	public ConnectionTab getParentTab() {
		return mParentTab;
	}

	public GraphPanel getGraphPanel() {
		return mGraphPanel;
	}

	/**
	 * Add the identifier in the list to the graph.
	 *
	 * @param pIdentifier
	 *            the list with identifier.
	 */
	private synchronized void addIdentifier(List<NodeIdentifier> pIdentifier) {
		LOGGER.trace("Method addIdentifier("
				+ pIdentifier + ") called.");
		for (NodeIdentifier vIdentifier : pIdentifier) {
			mGraphPanel.addIdentifier(vIdentifier);
			if (mAddHighlights) {
				mGraphPanel.markAsNew(vIdentifier);
				mTimerCreation.addNode(vIdentifier);
			}
			vIdentifier.addObserver(this);
			mObservables.put(vIdentifier, vIdentifier);
		}
	}

	/**
	 * Add the metadata of the list and create the edges to the identifier in the graph.
	 *
	 * @param pIdentifier
	 *            the identifier.
	 * @param pMetadata
	 *            the list with metadata.
	 */
	private synchronized void addMetadata(NodeIdentifier pIdentifier, List<NodeMetadata> pMetadata) {
		LOGGER.trace("Method addMetadata("
				+ pIdentifier + ", " + pMetadata + ") called.");
		for (NodeMetadata vMetadata : pMetadata) {
			mGraphPanel.addMetadata(pIdentifier, vMetadata);
			if (mAddHighlights) {
				mGraphPanel.markAsNew(vMetadata);
				mTimerCreation.addNode(vMetadata);
			}
			vMetadata.addObserver(this);
			mObservables.put(vMetadata, vMetadata);
		}
	}

	/**
	 * Add the metadata of the list and create the edges of the link in the graph.
	 *
	 * @param pLink
	 *            the link.
	 * @param pMetadata
	 *            the list with metadata.
	 */
	private synchronized void addMetadata(ExpandedLink pLink, List<NodeMetadata> pMetadata) {
		LOGGER.trace("Method addMetadata("
				+ pLink + ", " + pMetadata + ") called.");
		for (NodeMetadata vMetadata : pMetadata) {
			mGraphPanel.addMetadata(pLink, vMetadata);
			if (mAddHighlights) {
				mGraphPanel.markAsNew(vMetadata);
				mTimerCreation.addNode(vMetadata);
			}
			vMetadata.addObserver(this);
			mObservables.put(vMetadata, vMetadata);
		}
	}

	/**
	 * Remove the IdentifierNode.
	 *
	 * @param pIdentifier
	 *            the NodeIdentifier to identify the node in the graph.
	 */
	private synchronized void deleteIdentifier(List<NodeIdentifier> pIdentifier) {
		LOGGER.trace("Method deleteIdentifier("
				+ pIdentifier + ") called.");
		if (mAddHighlights) {
			for (NodeIdentifier vIdentifier : pIdentifier) {
				mGraphPanel.markAsDelete(vIdentifier);
				mTimerDeletion.addNode(vIdentifier);
			}
		}
	}

	/**
	 * Delete the MetadataNode and the edges to the node from the graph.
	 *
	 * @param pMetadata
	 *            the NodeMetadata to identify the node and edges in the graph.
	 */
	private synchronized void deleteMetadata(List<NodeMetadata> pMetadata) {
		LOGGER.trace("Method deleteMetadata("
				+ pMetadata + ") called.");
		if (mAddHighlights) {
			for (NodeMetadata vMetadata : pMetadata) {
				mGraphPanel.markAsDelete(vMetadata);
				mTimerDeletion.addNode(vMetadata);
			}
		}
	}

	/**
	 *
	 * @param pNode
	 */
	public synchronized void deleteNode(Position pNode) {
		LOGGER.trace("Method deleteNode("
				+ pNode + ") called.");
		mGraphPanel.deleteNode(pNode);
		pNode.deleteObserver(this);
		mObservables.remove(pNode);
	}

	/**
	 *
	 * @param pNode
	 */
	public void removeHighlight(Position pNode) {
		LOGGER.trace("Method removeHighlight("
				+ pNode + ") called.");
		mGraphPanel.clearHighlight(pNode);
	}

	/**
	 * Set the new position for a Position-Object.
	 *
	 * @param pNode
	 *            the Object.
	 * @param pNewX
	 *            the new x coordinate.
	 * @param pNewY
	 *            the new y coordinate.
	 * @param pNewZ
	 *            the new z coordinate.
	 * @param pinNode
	 *            TODO
	 */
	public void updateNode(Position pNode, double pNewX, double pNewY, double pNewZ, boolean pinNode) {
		LOGGER.trace("Method updateNode("
				+ pNode + ", " + pNewX + ", " + pNewY + ", " + pNewZ + ") called.");
		mFacadeLogic.updateNode(pNode, pNewX, pNewY, pNewZ, pinNode);
	}

	/**
	 * Get a list of all known publisher.
	 *
	 * @return stuff
	 */
	public List<String> getPublisher() {
		LOGGER.trace("Method getPublisher() called.");
		return mGraphPanel.getPublisher();
	}

	/**
	 * Repaint nodes of a specific type and publisher.
	 *
	 * @param pPublisher
	 *            the id of the current publisher, empty if the default color is changed.
	 * @param pType
	 *            the type of the node,
	 */
	public void repaintNodes(NodeType pType) {
		LOGGER.trace("Method repaintMetadata() called.");
		mGraphPanel.repaintNodes(pType);
	}

	/**
	 * Stop the calculation of node positions.
	 */
	public void stopGraphMotion() {
		LOGGER.trace("Method stopGraphMotion() called.");
		mFacadeLogic.stopCalculation();
	}

	/**
	 * Resume the calculation of node positions.
	 */
	public void startGraphMotion() {
		LOGGER.trace("Method startGraphMotion() called.");
		mFacadeLogic.startCalculation();
	}

	/**
	 * Checks whether the positions of nodes are currently calculated.
	 *
	 * @return False = Motion of graph is Off. True = Motion of graph is On.
	 */
	public boolean isGraphMotion() {
		LOGGER.trace("Method isGraphMotion() called.");
		return mFacadeLogic.isCalculationRunning();
	}

	/**
	 * Set layout type (e.g., force-directed)
	 *
	 * @param layoutType
	 */
	public void setLayoutType(LayoutType layoutType) {
		mFacadeLogic.setLayoutType(layoutType);
	}

	/**
	 * Reposition all nodes.
	 */
	public void redrawGraph() {
		LOGGER.trace("Method redrawGraph() called.");
		mFacadeLogic.recalculateGraph();
	}

	/**
	 * Remove all nodes and edges from the graph.
	 */
	public void clearGraph() {
		LOGGER.trace("Method clearGraph() called.");
		for (Observable vObserbale : mObservables.keySet()) {
			vObserbale.deleteObserver(this);
		}
		mObservables.clear();
		mGraphPanel.clearGraph();
	}

	/**
	 * @see FacadeLogic#loadGraphAtDeltaStart()
	 */
	private void loadGraphAtDeltaStart() {
		mTimerCreation.removeAll();
		mTimerDeletion.removeAll();
		clearGraph();
		mFacadeLogic.loadGraphAtDeltaStart();
	}

	/**
	 * @see FacadeLogic#loadCurrentGraph()
	 */
	private void loadCurrentGraph() {
		mTimerCreation.removeAll();
		mTimerDeletion.removeAll();
		clearGraph();
		mFacadeLogic.loadCurrentGraph();
	}

	/**
	 * @see FacadeLogic#loadDelta()
	 */
	private void loadDelta() {
		LOGGER.trace("Method loadDelta() called.");
		mFacadeLogic.loadDelta();
	}

	/**
	 * Loads the Delta.
	 *
	 * @see GraphConnection#loadGraphAtDeltaStart()
	 * @see GraphConnection#loadDelta()
	 */
	private void delta() {
		LOGGER.trace("Method delta() called.");
		mGraphPanel.setNodeTranslationDuration(0);
		mAddHighlights = false;
		loadGraphAtDeltaStart();
		mTimerCreation.removeAll();
		mTimerDeletion.removeAll();
		mAddHighlights = true;
		loadDelta();
		mGraphPanel.setNodeTranslationDuration(mSettingManager.getNodeTranslationDuration());
	}

	/**
	 * Fill the GraphPanel with the data.
	 *
	 * @param pData
	 *            the data.
	 */
	public void fillGraph(UpdateContainer pData) {
		/* Add nodes to graph. */
		addIdentifier(pData.getListAddIdentifier());
		for (NodeIdentifier vIdentifier : pData.getListAddIdentifier()) {
			addMetadata(vIdentifier, vIdentifier.getMetadata());
		}
		for (ExpandedLink vLink : pData.getListAddLinks()) {
			addMetadata(vLink, vLink.getMetadata());
		}
		for (Entry<NodeIdentifier, List<NodeMetadata>> vSet : pData.getListAddMetadataIdentifier().entrySet()) {
			addMetadata(vSet.getKey(), vSet.getValue());
		}
		for (Entry<ExpandedLink, List<NodeMetadata>> vSet : pData.getListAddMetadataLinks().entrySet()) {
			addMetadata(vSet.getKey(), vSet.getValue());
		}
		for (List<NodeMetadata> vMetadata : pData.getListDeleteMetadataLinks().values()) {
			deleteMetadata(vMetadata);
		}
		for (List<NodeMetadata> vMetadata : pData.getListDeleteMetadataIdentifier().values()) {
			deleteMetadata(vMetadata);
		}
		deleteIdentifier(pData.getListDeleteIdentifier());

		mGraphPanel.adjustPanelSize();
	}

	@Override
	public void update(Observable o, Object arg) {
		LOGGER.trace("Method update("
				+ o + ", " + arg + ") called.");
		if (o instanceof FacadeLogic) {
			fillGraph(mFacadeLogic.getUpdate());
			mGraphPanel.repaint();
		} else if (o instanceof NodeIdentifier) {
			mGraphPanel.updateIdentifier((NodeIdentifier) o);
			mGraphPanel.repaint();
		} else if (o instanceof NodeMetadata) {
			mGraphPanel.updateMetadata((NodeMetadata) o);
			mGraphPanel.repaint();
		} else if (o instanceof TimeHolder) {
			if (!mTimeHolder.isLiveView()) {
				delta();
				mFacadeLogic.recalculateGraph();
			} else if (mTimeHolder.isSetLive()) {
				loadCurrentGraph();
			}
			mGraphPanel.repaint();
		} else if (o instanceof SettingManager) {
			mGraphPanel.setNodeTranslationDuration(mSettingManager.getNodeTranslationDuration());
		}
	}

	/**
	 * Shows the properties of the given {@link Propable} object, sets is as
	 * selected in the {@link GraphPanel} instance and stores whether it was
	 * marked as picked or not.
	 *
	 * @param propable
	 *            the {@link Propable} object to show
	 */
	public void pickAndShowProperties(GraphicWrapper wrapper) {
		LOGGER.trace("Method pickAndShowProperties("
				+ wrapper + ") called.");
		mIsPropablePicked = true;
		mParentTab.showPropertiesOfNode(wrapper.getData());
		mParentTab.showExtendedNodeInformation(wrapper);
		mGraphPanel.selectNode(wrapper);
	}

	/**
	 * Just shows the properties of the given {@link Propable} object, without
	 * marking it as selected or storing whether it was picked.
	 *
	 * @param propable
	 *            the {@link Propable} object to show
	 */
	public void showProperty(Propable propable) {
		LOGGER.trace("Method showProperties("
				+ propable + ") called.");
		mParentTab.showPropertiesOfNode(propable);
	}

	/**
	 * Clears the shown properties, stores that nothing is picked at the moment
	 * and informs the {@link GraphPanel} that nothing is selected anymore.
	 */
	public void clearProperties() {
		LOGGER.trace("Method clearProperties() called.");
		mIsPropablePicked = false;
		mParentTab.showPropertiesOfNode(null);
		mParentTab.showExtendedNodeInformation(null);
		mGraphPanel.unselectNode();
	}

	/**
	 * Returns if the current shown {@link Propable} object was marked as picked or not.
	 *
	 * @return true if current shown {@link Propable} object was marked as picked, false if not
	 */
	public boolean isPropablePicked() {
		LOGGER.trace("Method isPropablePicked() called.");
		return mIsPropablePicked;
	}

	public void showContextMenu(final GraphicWrapper node, Point point) {
		JPopupMenu contextMenu = createContextMenu(node);
		contextMenu.show(mGraphPanel.getPanel(), point.x, point.y);
	}

	private JPopupMenu createContextMenu(final GraphicWrapper node) {
		JPopupMenu result = new JPopupMenu();

		for (final ContextMenuItem item : mContextMenuItems) {
			JMenuItem menuItem = new JMenuItem(item.getItemTitle());
			menuItem.addActionListener(new ActionListener() {
				@Override
				public void actionPerformed(ActionEvent e) {
					item.actionPerformed(node);
				}
			});

			if (!item.canHandle(node.getData())) {
				menuItem.setEnabled(false);
			}

			result.add(menuItem);
		}

		return result;
	}
	
}
