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

import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import de.tud.kom.p2psim.api.common.SimHost;
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.impl.topology.util.LocalGraphView;
import de.tud.kom.p2psim.impl.topology.util.LocationRequestImpl;
import de.tud.kom.p2psim.impl.topology.util.PositionVector;
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.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.exception.InformationNotAvailableException;
import de.tudarmstadt.maki.simonstrator.api.component.sis.type.SiSTypes;
import de.tudarmstadt.maki.simonstrator.api.component.sis.SiSInformationProvider.SiSProviderHandle;
import de.tudarmstadt.maki.simonstrator.api.component.sis.SiSInfoProperties;
import de.tudarmstadt.maki.simonstrator.api.Event;
import de.tudarmstadt.maki.simonstrator.api.EventHandler;
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.network.NetworkComponent.NetInterfaceName;

/**
 * Abstract implementation of the topology component interface, to support basic 
 * functionalities to all topology components.
 * 
 * @author Julian Zobel
 * @version 1.0, 6 Sep 2018
 */
public abstract class AbstractTopologyComponent implements TopologyComponent {
	
	protected SimHost host;	
	protected final PositionVector position;
	protected Topology topology;
	
	protected MovementModel movementModel;
	protected PlacementModel placementModel;
	
	private final boolean registerAsInformationProviderInSiS;
	
	private Map<LocationListener, LocationRequestImpl> openRequests = new LinkedHashMap<LocationListener, LocationRequestImpl>();
	private List<LocationListener> listeners = new LinkedList<>();
	
	//
	public AbstractTopologyComponent(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()  {		
				
		topology.addComponent(this);
		movementModel.placeComponent(this);

		if (placementModel != null) {			
			// retrieve the initial position of this topology component 
			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,
									AbstractTopologyComponent.this,
									getTopologyID(NetInterfaceName.WIFI, true),
									AbstractTopologyComponent.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 Topology getTopology() {
		return topology;
	}
	
	/**
	 * Access to the movement model
	 * @return
	 */
	public MovementModel getMovementModel() {
		return movementModel;
	}
	
	@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 PositionVector getRealPosition() {
		return position; // no copy! See SimLocationActuator Interface Description!
	}	
	
	@Override
	public void updateCurrentLocation(Location location) {		
		position.set(location);
		
		for (LocationListener locationListener : listeners) {
			locationListener.onLocationChanged(getHost(), getLastLocation());
		}
	}
	
	@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(getHost(), this);
	}
	
	/*
	 * 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, topology));
		}
		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, topology));
		}
		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();
	}


	@Override
	public String toString() {
		return "Topology Component #" + getHost().getId() + " at pos " + position.toString();
	}	
}
