/*
 * Copyright (c) 2005-2010 KOM – Multimedia Communications Lab
 *
 * This file is part of PeerfactSim.KOM.
 * 
 * PeerfactSim.KOM 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 3 of the License, or
 * any later version.
 * 
 * PeerfactSim.KOM 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 PeerfactSim.KOM.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

package de.tud.kom.p2psim.impl.topology.util;

import java.util.List;
import java.util.Set;

import de.tud.kom.p2psim.api.common.SimHost;
import de.tud.kom.p2psim.api.linklayer.mac.MacAddress;
import de.tud.kom.p2psim.api.linklayer.mac.MacLayer;
import de.tud.kom.p2psim.api.linklayer.mac.PhyType;
import de.tud.kom.p2psim.api.topology.Topology;
import de.tud.kom.p2psim.api.topology.views.TopologyView;
import de.tud.kom.p2psim.impl.topology.component.DefaultTopologyComponent;
import de.tudarmstadt.maki.simonstrator.api.Graphs;
import de.tudarmstadt.maki.simonstrator.api.Host;
import de.tudarmstadt.maki.simonstrator.api.Oracle;
import de.tudarmstadt.maki.simonstrator.api.common.graph.Graph;
import de.tudarmstadt.maki.simonstrator.api.common.graph.IEdge;
import de.tudarmstadt.maki.simonstrator.api.common.graph.INode;
import de.tudarmstadt.maki.simonstrator.api.component.ComponentNotAvailableException;
import de.tudarmstadt.maki.simonstrator.api.component.network.NetInterface;
import de.tudarmstadt.maki.simonstrator.api.component.network.NetworkComponent.NetInterfaceName;
import de.tudarmstadt.maki.simonstrator.api.component.sensor.location.Location;
import de.tudarmstadt.maki.simonstrator.api.component.sensor.location.LocationListener;
import de.tudarmstadt.maki.simonstrator.api.component.sis.type.SiSTypes;
import de.tudarmstadt.maki.simonstrator.api.component.transport.ConnectivityListener;

/**
 * This is calculated based on global knowledge. It only registers as
 * {@link LocationListener}, if a range is specified by the Provider.
 *
 * @author Bjoern Richerzhagen
 * @version 1.0, May 13, 2015
 */
public class LocalGraphView implements LocationListener, ConnectivityListener {

	/**
	 * Marker: has there been any movement since the graph view was last
	 * requested? If so: recalculate! Otherwise, we ignore this object to
	 * not perform calculations if no one is interested...
	 */
	private final double distance;

	private final boolean isDistanceBased;

	private final NetInterfaceName medium;

	private final TopologyView topoView;

	private final boolean onlyOnline;

	private Graph currentView;

	private boolean isInvalid = true;

	private final PhyType phy;

	public LocalGraphView(NetInterfaceName medium, boolean onlyOnline, Topology topology) {
		this(medium, onlyOnline, -1, topology);
	}

	public LocalGraphView(NetInterfaceName medium, boolean onlyOnline,
			double distance, Topology topology) {
		this.medium = medium;
		PhyType localPhy = null;
		for (PhyType currPhy : PhyType.values()) {
			if (currPhy.getNetInterfaceName() == medium
					&& topology.getTopologyView(currPhy) != null) {
				localPhy = currPhy;
				break;
			}
		}
		phy = localPhy;
		assert localPhy != null;
		this.topoView = topology.getTopologyView(localPhy);
		this.distance = distance;
		this.onlyOnline = onlyOnline;
		this.isDistanceBased = (distance > 0);
		assert !isDistanceBased || phy.isBroadcastMedium();

		if (phy.isBroadcastMedium() || onlyOnline) {
			for (Host host : Oracle.getAllHosts()) {
				if (phy.isBroadcastMedium()) {
					try {
						DefaultTopologyComponent dcomp = host.getComponent(
								DefaultTopologyComponent.class);
						dcomp.requestLocationUpdates(null,
								LocalGraphView.this);
					} catch (ComponentNotAvailableException e) {
						continue;
					}
				}
				if (onlyOnline) {
					if (host.getNetworkComponent()
							.getByName(medium) != null) {
						host.getNetworkComponent().getByName(medium)
						.addConnectivityListener(
								LocalGraphView.this);
					}
				}
			}
		}
	}

	private void recalculateLocalView() {
		if (!isInvalid) {
			/*
			 * Graphs are invalidated (i) based on movement, IFF a range was
			 * specified, (ii) based on online/offline events, IFF only
			 * online hosts are to be considered.
			 */
			return;
		}
		/*
		 * Calculate a complete global connectivity graph
		 */

		// Create new, empty graph
		currentView = Graphs.createGraph();

		// Add all (online?) nodes
		for (MacLayer mac : topoView.getAllMacs()) {
			if (!onlyOnline || mac.isOnline()) {
				INode node = currentView.createNode(mac.getHost().getId());
				node.setProperty(SiSTypes.PHY_LOCATION,
						topoView.getPosition(mac.getMacAddress()).clone());
				currentView.addElement(node);
			}
		}

		if (isDistanceBased) {
			// Build neighbors solely based on an assumed range
			for (MacLayer mac : topoView.getAllMacs()) {
				// Fix Christoph Storm:
				// Do not take offline nodes into account, unless told to do
				// so...
				if (onlyOnline && !currentView
						.containsNode(mac.getHost().getId())) {
					continue;
				}
				// Consider all nodes as potential neighbors
				for (MacLayer neighborMac : topoView.getAllMacs()) {
					// create, but do NOT add the node object
					INode neighbor = currentView
							.createNode(neighborMac.getHost().getId());
					// only online nodes (already in graph)
					if (!onlyOnline
							|| currentView.containsNode(neighbor.getId())) {
						// Distance?
						if (topoView.getDistance(mac.getMacAddress(),
								neighborMac.getMacAddress()) <= distance) {
							IEdge edge = currentView.createEdge(
									mac.getHost().getId(),
									neighborMac.getHost().getId());
							currentView.addElement(edge);
						}
					}
				}
			}
		} else {
			// Build neighborhoods based on underlay neighbors (1-hop)
			for (MacLayer mac : topoView.getAllMacs()) {
				// Fix Christoph Storm:
				// Do not take offline nodes into account, unless told to do
				// so...
				if (onlyOnline && !currentView
						.containsNode(mac.getHost().getId())) {
					continue;
				}
				// Rely on underlay for neighbors
				List<MacAddress> neighbors = topoView
						.getNeighbors(mac.getMacAddress());
				for (MacAddress neighborMac : neighbors) {
					// create, but do NOT add the node object
					INode neighbor = currentView.createNode(
							topoView.getMac(neighborMac).getHost().getId());
					// only online nodes (already in graph)
					if (!onlyOnline
							|| currentView.containsNode(neighbor.getId())) {
						IEdge edge = currentView.createEdge(
								mac.getHost().getId(),
								topoView.getMac(neighborMac).getHost()
								.getId());
						currentView.addElement(edge);
						edge.setProperty(SiSTypes.PHY_DISTANCE,
								topoView.getDistance(mac.getMacAddress(),
										neighborMac));
					}
				}
			}
		}

		isInvalid = false;
	}

	public INode getOwnNode(SimHost ownHost) {
		MacLayer mac = ownHost.getLinkLayer().getMac(phy);
		if (!onlyOnline || mac.isOnline()) {
			return currentView.createNode(ownHost.getId());
		}
		return null;
	}

	public Set<IEdge> getNeighbors(SimHost ownHost) {
		recalculateLocalView();
		INode ownNode = getOwnNode(ownHost);
		return currentView.getOutgoingEdges(ownNode.getId());
	}

	/**
	 * This is the global view, therefore we do not distinguish between
	 * hosts.
	 *
	 * @return
	 */
	public Graph getLocalView() {
		recalculateLocalView();
		return currentView.clone();
	}

	@Override
	public void onLocationChanged(Host host, Location location) {
		this.isInvalid = true;
	}

	@Override
	public void wentOnline(Host host, NetInterface netInterface) {
		assert netInterface.getName() == medium;
		this.isInvalid = true;
	}

	@Override
	public void wentOffline(Host host, NetInterface netInterface) {
		assert netInterface.getName() == medium;
		this.isInvalid = true;
	}

}
