/*
 * 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.movement.modularosm.transition;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.Vector;
import de.tud.kom.p2psim.api.common.SimHost;
import de.tud.kom.p2psim.api.common.SimHostComponent;
import de.tud.kom.p2psim.api.scenario.ConfigurationException;
import de.tud.kom.p2psim.api.topology.Topology;
import de.tud.kom.p2psim.api.topology.movement.SimLocationActuator;
import de.tud.kom.p2psim.api.topology.social.SocialView;
import de.tud.kom.p2psim.impl.topology.movement.modularosm.attraction.IAttractionProvider;
import de.tud.kom.p2psim.impl.topology.util.PositionVector;
import de.tudarmstadt.maki.simonstrator.api.Binder;
import de.tudarmstadt.maki.simonstrator.api.Event;
import de.tudarmstadt.maki.simonstrator.api.EventHandler;
import de.tudarmstadt.maki.simonstrator.api.component.sensor.location.IAttractionPoint;
import de.tudarmstadt.maki.simonstrator.api.util.XMLConfigurableConstructor;

/**
 * This is a {@link TransitionStrategy} for the Social Case. It will be try to
 * build groups based on the {@link SocialView} information. For this, it tries
 * to assignment the given objects to the given {@link IAttractionPoint}s. For
 * the {@link SocialView}, it is required a {@link #socialId}, to find the right
 * {@link SocialView}.
 * 
 * <br>
 * 
 * The Strategy has the parameter of {@link #socialFactor},
 * {@link #minPauseTime} and {@link #maxPauseTime}. The socialFactor should be a
 * value between 0 and 1. It gives the probability for a social based transition
 * or the transition to a random {@link IAttractionPoint}. If the social based
 * transition is selected, then will be used a scoring to find the right
 * {@link IAttractionPoint}. For that, it will be used only the AttractionPoints,
 * of the hosts, which are in the same SocialCluster or are SocialNeighbors. For
 * this AttractionPoints it will be find the highest scoring, which is to found
 * in score. .
 * 
 * <br>
 * 
 * After the finding of the next {@link IAttractionPoint}, it will be scheduled
 * an Event, with a delay between min- and maxPauseTime. After this delay, it
 * will be tried to assign a new {@link IAttractionPoint} like the described
 * above.
 *  
 * @author Christoph Muenker
 * @version 1.0, 02.07.2013
 * 
 * <hr>
 * 
 * 
 * 
 */
public class SocialTransitionStrategy extends AbstractAttractionBasedAssignmentStrategy implements EventHandler {

	private String socialId = null;

	private SocialView socialView;

	private List<SimLocationActuator> comps = new Vector<SimLocationActuator>();	

	private Set<SimLocationActuator> arrivedAtAttractionPoint = new LinkedHashSet<>();

	private Map<SimLocationActuator, Set<IAttractionPoint>> favoritePlaces = new HashMap<SimLocationActuator, Set<IAttractionPoint>>();

	private Map<SimLocationActuator, SimHost> mapMsHost = new HashMap<SimLocationActuator, SimHost>();

	private Map<SimHost, SimLocationActuator> mapHostMs = new HashMap<SimHost, SimLocationActuator>();
	
	private double socialFactor = 0.8;

	private long numberOfFavoritePlaces = 4;

	private PositionVector worldDimension;

	private boolean init = false;

	@XMLConfigurableConstructor({ "defaultPauseTimeMin", "defaultPauseTimeMax" })
	public SocialTransitionStrategy(long defaultPauseTimeMin, long defaultPauseTimeMax) {
		super(defaultPauseTimeMin, defaultPauseTimeMax);
	}

	
	private void init() {
		if (!init) {
			if (socialId == null) {
				throw new ConfigurationException(
						"SocialId is not set, to find the needed SocialView!");
			}

			socialView = Binder.getComponentOrNull(Topology.class)
					.getSocialView(socialId);

			if (socialView == null) {
				throw new ConfigurationException(
						"Cannot find the right socialView. Is the socialId correct?");
			}

			if (defaultPauseTimeMin > defaultPauseTimeMax) {
				throw new ConfigurationException(
						"MinPauseTime should be smaller then maxPauseTime.");
			}

			worldDimension = Binder.getComponentOrNull(Topology.class)
					.getWorldDimensions();
			init = true;
		}
	}

	public void setSocialId(String socialId) {
		this.socialId = socialId;
	}
	
	@Override
	public void addComponent(SimLocationActuator ms) {
		comps.add(ms);
		mappingHost(ms);
		// assign the ms to an attractionPoint, which is near to the ms
		// position.
		// TODO: needed? We do Transition as next, and this will delete the
		// assignment..
		IAttractionPoint nearest = attractionProvider.getAttractionPoints().iterator().next();
		for (IAttractionPoint aPoint : attractionProvider.getAttractionPoints()) {
			if (nearest.distanceTo(ms.getRealPosition()) > aPoint
					.distanceTo(ms.getRealPosition())) {
				nearest = aPoint;
			}
		}
		assignments.put(ms, nearest);

		assignFavoritePlaces(ms);

		doTransition(ms);
	}

	@Override
	public void updateTargetAttractionPoint(SimLocationActuator comp,
			IAttractionPoint attractionPoint) {
		arrivedAtAttractionPoint.remove(comp);
		assignments.put(comp, attractionPoint);
		notifyListenersOfAssignmentUpdate(comp, attractionPoint);
	}

	@Override
	public void reachedAttractionPoint(SimLocationActuator ms, IAttractionPoint attractionPoint) {
		if (!arrivedAtAttractionPoint.contains(ms)) {
			Event.scheduleWithDelay(getPauseTime(attractionPoint), this, ms, 0);
			arrivedAtAttractionPoint.add(ms);
		}
	}

	public void doTransition(SimLocationActuator ms) {
		List<IAttractionPoint> apFavorites = getFavoritePlaces(ms);
		List<IAttractionPoint> apFriends = getFriendsPlaces(ms);
		List<IAttractionPoint> apClusters = getClusterPlaces(ms);
		List<IAttractionPoint> apRandom = getRandomPlaces(ms,
				(int) Math.max(attractionProvider.getAttractionPoints().size() * 0.2, 5));

		IAttractionPoint ap = null;
		if (rnd.nextDouble() < socialFactor) {
			ap = findHighestScore(ms, apFavorites, apFriends, apClusters,
					apRandom);
		} else {
			List<IAttractionPoint> aps = new ArrayList<IAttractionPoint>();
			aps.addAll(apRandom);
			aps.addAll(apFavorites);
			ap = aps.get(rnd.nextInt(apRandom.size()));
		}
		assignments.put(ms, ap);
		arrivedAtAttractionPoint.remove(ms);
		notifyListenersOfAssignmentUpdate(ms, ap);
	}

	private IAttractionPoint findHighestScore(SimLocationActuator ms,
			List<IAttractionPoint> apFavorites, List<IAttractionPoint> apFriends,
			List<IAttractionPoint> apClusters, List<IAttractionPoint> apRandom) {
		Set<IAttractionPoint> aps = new LinkedHashSet<IAttractionPoint>();
		aps.addAll(apFavorites);
		aps.addAll(apFriends);
		aps.addAll(apClusters);
		aps.addAll(apRandom);

		double maxScore = 0;
		IAttractionPoint maxAp = null;
		for (IAttractionPoint ap : aps) {
			double score = score(ms, ap, apFavorites, apFriends, apClusters,
					apRandom);
			// System.out.println(score);
			if (score > maxScore) {
				maxScore = score;
				maxAp = ap;
			}
		}
		return maxAp;
	}

	/**
	 * Score the given AttractionPoint for the given SimLocationActuator. <br>
	 * (clusterScore/#NodesInAp + friendsScore + 1/#NodesInAp) * socialFactor +
	 * (distanceScore + penalty) + (1-socialFactor) <br>
	 * 
	 * clusterScore = 1 if one is in the same cluster in this AP<br>
	 * friendsScore = 1 if one friend is in the same AP<br>
	 * penalty = -1 if AP the actually AP is <br>
	 * distance = 1 - (distance / maxDistance)
	 * 
	 * @param ms
	 * @param ap
	 * @param apFavorites
	 * @param apFriends
	 * @param apClusters
	 * @param apRandom
	 * @return
	 */
	private double score(SimLocationActuator ms, IAttractionPoint ap,
			List<IAttractionPoint> apFavorites, List<IAttractionPoint> apFriends,
			List<IAttractionPoint> apClusters, List<IAttractionPoint> apRandom) {
		double distance = ms.getRealPosition().distanceTo(ap);
		double distanceScore = 1 - (distance / worldDimension.getLength());

		double clusterScore = 0;
		double friendsScore = 0;
		if (apClusters.contains(ap)) {
			if (occurence(ap, apClusters) > 3) {
				// occurence give the number of other peers in this AP
				clusterScore = 1.0 / (occurence(ap, apClusters) - 1);
			} else {
				clusterScore = 1.0;
			}
		}
		if (apFriends.contains(ap)) {
			if (occurence(ap, apFriends) > 3) {
				// occurence give the number of other peers in this AP
				friendsScore = 1.0 / (occurence(ap, apFriends) - 1);
			} else {
				friendsScore = 1.0;
			}
		}
		// penalty for distance
		double penalty = 0;
		if (ap.equals(assignments.get(ms))) {
			penalty = -1;
		}

		return (clusterScore / assignedToAp(ap) + friendsScore
				+ 1.0 / assignedToAp(ap)) * socialFactor

				+ (distanceScore + penalty) * (1 - socialFactor);
	}

	// counts the number of the AttractionPoint in the list
	private int occurence(IAttractionPoint ap, List<IAttractionPoint> aps) {
		int i = 0;
		for (IAttractionPoint a : aps) {
			if (a.equals(ap)) {
				i++;
			}
		}
		return i;
	}

	private int assignedToAp(IAttractionPoint ap) {
		int i = 1;
		for (Entry<SimLocationActuator, IAttractionPoint> entry : assignments
				.entrySet()) {
			if (entry.getValue().equals(ap)) {
				i++;
			}
		}
		return i;
	}

	private List<IAttractionPoint> getRandomPlaces(SimLocationActuator ms,
			int number) {
		List<IAttractionPoint> result = new Vector<IAttractionPoint>(attractionProvider.getAttractionPoints());
		Collections.shuffle(result, rnd);
		return result.subList(0, Math.min(result.size(), number));
	}

	private List<IAttractionPoint> getClusterPlaces(SimLocationActuator ms) {
		List<IAttractionPoint> result = new Vector<IAttractionPoint>();
		SimHost msHost = mapMsHost.get(ms);

		for (SimHost host : socialView.getCluster(msHost)) {
			SimLocationActuator temp = mapHostMs.get(host);
			if (assignments.get(temp) != null) {
				result.add(assignments.get(temp));
			}
		}

		return result;
	}

	private List<IAttractionPoint> getFriendsPlaces(SimLocationActuator ms) {
		List<IAttractionPoint> result = new Vector<IAttractionPoint>();
		SimHost msHost = mapMsHost.get(ms);

		for (SimHost host : socialView.getNeighbors(msHost)) {
			SimLocationActuator temp = mapHostMs.get(host);
			if (assignments.get(temp) != null) {
				result.add(assignments.get(temp));
			}
		}

		return result;
	}

	private List<IAttractionPoint> getFavoritePlaces(SimLocationActuator ms) {
		return new Vector<IAttractionPoint>(favoritePlaces.get(ms));
	}

	private void assignFavoritePlaces(SimLocationActuator ms) {
		Set<IAttractionPoint> msFavoritePlaces = new LinkedHashSet<IAttractionPoint>();
		LinkedList<IAttractionPoint> temp = new LinkedList<IAttractionPoint>(
				attractionProvider.getAttractionPoints());
		Collections.shuffle(temp, rnd);
		for (int i = 0; i < numberOfFavoritePlaces; i++) {
			if (!temp.isEmpty()) {
				msFavoritePlaces.add(temp.removeFirst());
			}
		}
		favoritePlaces.put(ms, msFavoritePlaces);
	}

	private void mappingHost(SimLocationActuator ms) {
		SimHostComponent comp = (SimHostComponent) ms;
		SimHost host = comp.getHost();

		assert host != null;

		mapHostMs.put(host, ms);
		mapMsHost.put(ms, host);
	}
	
	@Override
	public void eventOccurred(Object se, int type) {
		if (arrivedAtAttractionPoint.contains(se)) {
			doTransition((SimLocationActuator) se);
		}
	}

	public void setSocialFactor(double socialFactor) {
		if (socialFactor < 0 || socialFactor > 1) {
			throw new ConfigurationException(
					"socialFactor should be between 0 and 1!");
		}
		this.socialFactor = socialFactor;
	}

}
