/*
 * 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.LinkedHashMap;
import java.util.Random;

import de.tud.kom.p2psim.api.topology.Topology;
import de.tud.kom.p2psim.api.topology.movement.SimLocationActuator;
import de.tud.kom.p2psim.impl.topology.movement.distributions.ISpeedDistributionProvider;
import de.tud.kom.p2psim.impl.topology.movement.modularosm.ISocialGroupMovementAnalyzer;
import de.tud.kom.p2psim.impl.topology.movement.modularosm.SocialGroupMovementModel;
import de.tud.kom.p2psim.impl.topology.movement.modularosm.attraction.AttractionPoint;
import de.tud.kom.p2psim.impl.topology.movement.modularosm.attraction.BasicAttractionPoint;
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.Monitor;
import de.tudarmstadt.maki.simonstrator.api.Randoms;
import de.tudarmstadt.maki.simonstrator.api.component.sensor.location.IAttractionPoint;
import de.tudarmstadt.maki.simonstrator.api.util.XMLConfigurableConstructor;

/**
 * With this transition strategy, nodes are roaming around {@link IAttractionPoint}s without a set transition time. 
 * A specific call from outside is required to trigger a transition to another {@link IAttractionPoint}, such as 
 * used in the {@link SocialGroupMovementModel}. Nodes may also go to a random location instead of a pre-defined
 * {@link IAttractionPoint} with a certain probability.
 * 
 * @author Julian Zobel
 * @version 10.02.2020
 */
public class AttractionPointRoamingStrategy extends AbstractAttractionBasedAssignmentStrategy implements EventHandler {

	private Random rand = Randoms.getRandom(AttractionPointRoamingStrategy.class);		
	
	public static enum roamingTransitionState {
		PAUSE,
		ROAMING
	}	
	
	protected ISpeedDistributionProvider roamSpeedProvider;
	
	protected LinkedHashMap<SimLocationActuator, roamingTransitionState> roamingStates = new LinkedHashMap<>();
	
	protected long roamingPauseTime;
	
	protected double randomTargetProbability = 0.2;
	
	@XMLConfigurableConstructor({ "defaultPauseTimeMin", "defaultPauseTimeMax", "roamingPauseTime" })
	public AttractionPointRoamingStrategy(long defaultPauseTimeMin, long defaultPauseTimeMax, long roamingPauseTime) {
		super(defaultPauseTimeMin, defaultPauseTimeMax);
		this.roamingPauseTime = roamingPauseTime;
	}
	
	@Override
	public void addComponent(SimLocationActuator comp) {
		this.roamingStates.put(comp, null);	
		
		// trigger recalculation of speed
		comp.setMovementSpeed(-1);		
			
		IAttractionPoint nextAP = getNewAttractionPointAssignment(comp);			
		updateTargetAttractionPoint(comp, nextAP);
	}
	
	@Override
	public void newAssignment(SimLocationActuator comp) {
		if(rand.nextDouble() > randomTargetProbability) {
			IAttractionPoint nextAP = getNewAttractionPointAssignment(comp);			
			updateTargetAttractionPoint(comp, nextAP);
		}
		else {
			IAttractionPoint nextAP = createRandomAttraction(comp);
			updateTargetAttractionPoint(comp, nextAP);
		}
	}
	
	@Override
	public void updateTargetAttractionPoint(SimLocationActuator comp,
			IAttractionPoint attractionPoint) {		
		this.roamingStates.put(comp, null);	
		super.updateTargetAttractionPoint(comp, attractionPoint);
	}

	@Override
	public void reachedAttractionPoint(SimLocationActuator comp, IAttractionPoint attractionPoint) {
			
		if(roamingStates.get(comp) == roamingTransitionState.PAUSE) {
			return;
		}
				
		if(Monitor.hasAnalyzer(ISocialGroupMovementAnalyzer.class)) {			
			Monitor.getOrNull(ISocialGroupMovementAnalyzer.class).onNodeReachedAttractionPoint(comp, attractionPoint);
		}
		
		this.roamingStates.put(comp, roamingTransitionState.PAUSE);
		
		// schedule roaming
		long pause =  (long) Math.max(0, rnd.nextGaussian() * (roamingPauseTime / 3)) + roamingPauseTime;		
		Event.scheduleWithDelay(pause, this, comp, 0);	
	}	
	
	private void roamAroundAttractionPoint(SimLocationActuator comp) {
		if(roamingStates.get(comp) == roamingTransitionState.PAUSE) {
			IAttractionPoint currentAttractionPoint = this.assignments.get(comp);			
			this.roamingStates.put(comp, roamingTransitionState.ROAMING);			
			
			comp.setMovementSpeed(roamSpeedProvider.calculateSpeed());
			
			notifyListenersOfAssignmentUpdate(comp, currentAttractionPoint);			
		}		
	}
	
	@Override
	public void eventOccurred(Object content, int type) {
	
		SimLocationActuator comp = (SimLocationActuator) content;			
		IAttractionPoint currentAttractionPoint = this.assignments.get(comp);
			
		// if the attraction point was removed in the meantime, go directly to transit state
		if(currentAttractionPoint == null || !attractionProvider.getAttractionPoints().contains(currentAttractionPoint)) {				
			this.newAssignment(comp);
		}
		else {			
			this.roamAroundAttractionPoint(comp);
		}		
	}	
	
	public void setRoamingSpeedDistribution(ISpeedDistributionProvider dist) {
		this.roamSpeedProvider = dist;
	}
	
	///////////////////
	private PositionVector createPosVec() {
		double x = rand.nextDouble() * Binder.getComponentOrNull(Topology.class).getWorldDimensions().getX();
		double y = rand.nextDouble() * Binder.getComponentOrNull(Topology.class).getWorldDimensions().getY();
		return new PositionVector(x, y);
	}
	
	private BasicAttractionPoint createRandomAttraction(SimLocationActuator comp) {
		
		
		// make a break counter to prevent more than 10 iterations and an infinity loop in general.
		int tries = 0;
		
		PositionVector r = null;  
		
		while(tries < 100) {
			r = createPosVec();
			
			IAttractionPoint currentAttractionPoint = this.assignments.get(comp);
			
			if(currentAttractionPoint == null) {
				break;
			}
			else if (currentAttractionPoint instanceof AttractionPoint) {
				AttractionPoint ap = (AttractionPoint) currentAttractionPoint;
				
				// check that random location is outside of current attraction point
				if(ap.distanceTo(r) > ap.getRadius()) {
					break;
				}
			}
			else if (currentAttractionPoint instanceof BasicAttractionPoint) {
				break;
			}
			
			tries++;
			continue;
		}
		
		if(tries >= 100) throw new AssertionError("Unable to find a valid target destination within <100 tries.");
		
		return new BasicAttractionPoint("RNDPOS", r);		
	}
}