/*
 * 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;

import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;

import de.tud.kom.p2psim.api.common.HostProperties;
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.TopologyComponent;
import de.tud.kom.p2psim.api.topology.movement.MovementModel;
import de.tud.kom.p2psim.api.topology.placement.PlacementModel;
import de.tud.kom.p2psim.api.topology.views.TopologyView;
import de.tud.kom.p2psim.impl.simengine.Simulator;
import de.tudarmstadt.maki.simonstrator.api.Event;
import de.tudarmstadt.maki.simonstrator.api.EventHandler;
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.Randoms;
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.common.graph.INodeID;
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.AttractionPoint;
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.sensor.location.LocationRequest;
import de.tudarmstadt.maki.simonstrator.api.component.sis.SiSComponent;
import de.tudarmstadt.maki.simonstrator.api.component.sis.SiSDataCallback;
import de.tudarmstadt.maki.simonstrator.api.component.sis.SiSInfoProperties;
import de.tudarmstadt.maki.simonstrator.api.component.sis.SiSInformationProvider.SiSProviderHandle;
import de.tudarmstadt.maki.simonstrator.api.component.sis.exception.InformationNotAvailableException;
import de.tudarmstadt.maki.simonstrator.api.component.sis.type.SiSTypes;
import de.tudarmstadt.maki.simonstrator.api.component.sis.util.SiSTopologyProvider;
import de.tudarmstadt.maki.simonstrator.api.component.topology.TopologyID;
import de.tudarmstadt.maki.simonstrator.api.component.transport.ConnectivityListener;

/**
 * Default implementation of a {@link TopologyComponent}.
 *
 * @author Bjoern Richerzhagen
 * @version 1.0, 29.02.2012
 */
public class DefaultTopologyComponent implements TopologyComponent {

	protected static Random rnd = Randoms.getRandom(AttractionPoint.class);

	private SimHost host;

	private final PositionVector position;

	private Topology topology;

	private double currentMovementSpeed = -1;

	private Map<LocationListener, LocationRequestImpl> openRequests = new LinkedHashMap<LocationListener, LocationRequestImpl>();

	private List<LocationListener> listeners = new LinkedList<>();

	private MovementModel movementModel;

	private PlacementModel placementModel;
	
	private final boolean registerAsInformationProviderInSiS;

	/**
	 * Create a TopologyComponent for the current host.
	 *
	 * @param host
	 * @param topology
	 * @param movementModel
	 */
	public DefaultTopologyComponent(SimHost host, Topology topology,
			MovementModel movementModel, PlacementModel placementModel, boolean registerAsInformationProviderInSiS) {
		this.topology = topology;
		this.host = host;
		this.position = new PositionVector(0, 0);

		this.movementModel = movementModel;
		if (this.movementModel != null) {
			this.movementModel.addComponent(this);
		}

		this.placementModel = placementModel;
		if (this.placementModel != null) {
			this.placementModel.addComponent(this);
		}
		this.registerAsInformationProviderInSiS = registerAsInformationProviderInSiS;
	}

	@Override
	public void initialize() {
		/*
		 * Set the component's initial position and notify listeners of the
		 * Topology that this component is initialized.
		 */
		topology.addComponent(this);
		movementModel.placeComponent(this);

		if (placementModel != null) {
			/*
			 * Legacy support for placement models.
			 */
			position.set(placementModel.place(this));
		}

		if (registerAsInformationProviderInSiS) {
			try {
				final SiSComponent sis = host.getComponent(SiSComponent.class);
				sis.provide().nodeState(SiSTypes.PHY_LOCATION,
						new SiSDataCallback<Location>() {

							Set<INodeID> localID = INodeID
									.getSingleIDSet(getHost().getId());

							@Override
							public Location getValue(INodeID nodeID,
									SiSProviderHandle providerHandle)
									throws InformationNotAvailableException {
								if (nodeID.equals(getHost().getId())) {
									return getLastLocation();
								} else {
									throw new InformationNotAvailableException();
								}
							}

							@Override
							public Set<INodeID> getObservedNodes() {
								return localID;
							}

							@Override
							public SiSInfoProperties getInfoProperties() {
								return new SiSInfoProperties();
							}
						});

				sis.provide().nodeState(SiSTypes.SPEED,
						new SiSDataCallback<Double>() {

							Set<INodeID> localID = INodeID
									.getSingleIDSet(getHost().getId());

							@Override
							public Double getValue(INodeID nodeID,
									SiSProviderHandle providerHandle)
									throws InformationNotAvailableException {
								if (nodeID.equals(getHost().getId())) {
									return getMovementSpeed();
								} else {
									throw new InformationNotAvailableException();
								}
							}

							@Override
							public Set<INodeID> getObservedNodes() {
								return localID;
							}

							@Override
							public SiSInfoProperties getInfoProperties() {
								return new SiSInfoProperties();
							}
						});

				// Provide Underlay topology
				Event.scheduleImmediately(new EventHandler() {

					@Override
					public void eventOccurred(Object content, int type) {
						if (getHost().getLinkLayer().hasPhy(PhyType.WIFI)) {
							new SiSTopologyProvider(sis,
									SiSTypes.NEIGHBORS_WIFI,
									DefaultTopologyComponent.this,
									getTopologyID(NetInterfaceName.WIFI, true),
									DefaultTopologyComponent.class);
						}
					}
				}, null, 0);

			} catch (ComponentNotAvailableException e) {
				// OK
			}
		}
	}

	@Override
	public void shutdown() {
		topology = null;
		host = null;
		movementModel = null;
	}

	@Override
	public SimHost getHost() {
		return host;
	}

	@Override
	public PositionVector getRealPosition() {
		return position;
	}

	@Override
	public Topology getTopology() {
		return topology;
	}

	/**
	 * Access to the movement model
	 * @return
	 */
	public MovementModel getMovementModel() {
		return movementModel;
	}

	@Override
	public double getMinMovementSpeed() {
		HostProperties properties = getHost().getProperties();

		return properties.getMinMovementSpeed();
	}

	@Override
	public double getMaxMovementSpeed() {
		HostProperties properties = getHost().getProperties();
		return properties.getMaxMovementSpeed();
	}

	private void calcRandomMovementSpeed() {
		double min_speed = getMinMovementSpeed();
		double max_speed = getMaxMovementSpeed();

		double value = rnd.nextDouble();
		this.currentMovementSpeed = (value * (max_speed - min_speed))
				+ min_speed;
	}

	@Override
	public double getMovementSpeed() {
		if (currentMovementSpeed == -1) {
			calcRandomMovementSpeed();
		}
		return this.currentMovementSpeed;
	}

	@Override
	public void setMovementSpeed(double speed) {
		this.currentMovementSpeed = speed;
	}

	@Override
	public Location getLastLocation() {
		/*
		 * As we want to mimic real world behavior, the current position
		 * snapshot is cloned to prevent information propagation due to Java.
		 */
		return position.clone();
	}

	@Override
	public void updateCurrentLocation(Location location) {
		position.set(location);
		/*
		 * FIXME utilization of the NodeDebugMonitor leads to huge performance drop.
		 */
//		NodeDebugMonitor.update(this.getClass(), getHost().getId(), "Current Location", location);
//		try {
//			NodeDebugMonitor.update(this.getClass(), getHost().getId(),
//					"Distance to target",
//					location.distanceTo(movementModel.getTargetLocation(this)));
//		} catch (UnsupportedOperationException e) {
//			// This is not supported by the movement model (which may happen see
//			// MovementModel.java - thus catch and ignore)
//		}
		// notify "non-request" listeners
		for (LocationListener locationListener : listeners) {
			locationListener.onLocationChanged(getHost(), getLastLocation());
		}
	}

	@Override
	public void setTargetAttractionPoint(AttractionPoint targetAttractionPoint)
			throws UnsupportedOperationException {
		movementModel.changeTargetLocation(this, targetAttractionPoint);
//		NodeDebugMonitor.update(this.getClass(), getHost().getId(), "Target Location", targetAttractionPoint);
	}

	@Override
	public AttractionPoint getCurrentTargetAttractionPoint() {
		return movementModel.getTargetLocation(this);
	}

	@Override
	public Set<AttractionPoint> getAllAttractionPoints() {
		return movementModel.getAllAttractionPoints();
	}

	@Override
	public void requestLocationUpdates(LocationRequest request,
			LocationListener listener) {
		if (openRequests.containsKey(listener)) {
			throw new AssertionError(
					"This LocationListener is already in use.");
		}
		if (request == null) {
			/*
			 * This listener wants to be triggered on EVERY position update, but
			 * it does not want to request position updates.
			 */
			if (!listeners.contains(listener)) {
				listeners.add(listener);
			}
		} else {
			/*
			 * Listener has its own request timing.
			 */
			LocationRequestImpl req = (LocationRequestImpl) request;
			openRequests.put(listener, req);
			req.immunizeAndStart(listener);
		}
	}

	@Override
	public void removeLocationUpdates(LocationListener listener) {
		listeners.remove(listener);
		LocationRequestImpl impl = openRequests.remove(listener);
		if (impl != null) {
			impl.cancel(listener);
		}
	}

	@Override
	public LocationRequest getLocationRequest() {
		return new LocationRequestImpl();
	}

	/**
	 * Update 15.03.16 added support for multiple listeners (however, frequency
	 * etc. is immune after the first request is registered.)
	 *
	 * @author Bjoern Richerzhagen
	 * @version 1.0, Mar 15, 2016
	 */
	private class LocationRequestImpl implements LocationRequest, EventHandler {

		private boolean immune = false;

		private long interval = 1 * Simulator.MINUTE_UNIT;

		private int priority = LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY;

		private Location lastLocation = null;

		private List<LocationListener> listeners = new LinkedList<LocationListener>();

		private int eventTypeSeq = 0;

		public LocationRequestImpl() {
			// nothing to do
		}

		protected void cancel(LocationListener listener) {
			boolean removed = listeners.remove(listener);
			if (listeners.isEmpty()) {
				// upcoming event is no longer valid!
				eventTypeSeq++;
			}
			assert removed;
		}

		protected void immunizeAndStart(LocationListener listener) {
			immune = true;
			assert interval > 0;
			if (listeners.isEmpty()) {
				// Only start once!
				lastLocation = null;
				Event.scheduleImmediately(this, null, eventTypeSeq);
			} else {
				// Fire each new listener at least once
				listener.onLocationChanged(getHost(), getLastLocation());
			}
			listeners.add(listener);
		}

		@Override
		public void setInterval(long interval) {
			if (!immune) {
				this.interval = interval;
			}
		}

		@Override
		public void setPriority(int priority) {
			if (!immune) {
				this.priority = priority;
			}
		}

		@Override
		public void eventOccurred(Object content, int type) {
			if (eventTypeSeq != type) {
				/*
				 * Discard invalid events caused when a client cancels updates
				 * but reactivates the request within the update frequency
				 * interval. In this case, the old events continue rescheduling
				 * themselves.
				 */
				return;
			}
			if (!listeners.isEmpty()) {
				// Only reschedule, if at least one listener is ... listening
				Location newLoc = getLastLocation();
				listeners.forEach((LocationListener listener) -> listener
						.onLocationChanged(getHost(), newLoc));
				lastLocation = newLoc;
				Event.scheduleWithDelay(interval, this, null, eventTypeSeq);
			}
		}

	}

	/*
	 * Methods for the Graph Interface
	 */

	/**
	 * Graph views: static, as we use global knowledge and maintain one shared
	 * graph (potentially with partitions!)
	 */
	private final static LinkedHashMap<TopologyID, LocalGraphView> graphViews = new LinkedHashMap<>();

	@Override
	public TopologyID getTopologyID(NetInterfaceName netName,
			boolean onlyOnline) {
		TopologyID id = TopologyID.getIdentifier(
				netName.toString() + (onlyOnline ? "-online" : "-all"),
				DefaultTopologyComponent.class);
		if (!this.graphViews.containsKey(id)) {
			this.graphViews.put(id, new LocalGraphView(netName, onlyOnline));
		}
		return id;
	}

	@Override
	public TopologyID getTopologyID(NetInterfaceName netName,
			boolean onlyOnline, double range) {
		TopologyID id = TopologyID.getIdentifier(
				netName.toString() + (onlyOnline ? "-online" : "-all")
				+ String.valueOf(range),
				DefaultTopologyComponent.class);
		if (!this.graphViews.containsKey(id)) {
			this.graphViews.put(id,
					new LocalGraphView(netName, onlyOnline, range));
		}
		return id;
	}

	@Override
	public INode getNode(TopologyID identifier) {
		assert graphViews.containsKey(identifier);
		return graphViews.get(identifier).getOwnNode(host);
	}

	@Override
	public Set<IEdge> getNeighbors(TopologyID topologyIdentifier) {
		assert graphViews.containsKey(topologyIdentifier);
		return graphViews.get(topologyIdentifier).getNeighbors(host);
	}

	@Override
	public Graph getLocalView(TopologyID topologyIdentifier) {
		assert graphViews.containsKey(topologyIdentifier);
		return graphViews.get(topologyIdentifier).getLocalView();
	}

	@Override
	public Iterable<TopologyID> getTopologyIdentifiers() {
		return graphViews.keySet();
	}

	/**
	 * 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
	 */
	private 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) {
			this(medium, onlyOnline, -1);
		}

		public LocalGraphView(NetInterfaceName medium, boolean onlyOnline,
				double distance) {
			this.medium = medium;
			PhyType localPhy = null;
			for (PhyType currPhy : PhyType.values()) {
				if (currPhy.getNetInterfaceName() == medium
						&& getTopology().getTopologyView(currPhy) != null) {
					localPhy = currPhy;
					break;
				}
			}
			phy = localPhy;
			assert localPhy != null;
			this.topoView = getTopology().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;
		}

	}

	@Override
	public String toString() {
		return "TopoComp: " + getHost().getId() + " at " + position.toString();
	}

}
