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

import de.tud.kom.p2psim.api.topology.movement.SimLocationActuator;
import de.tud.kom.p2psim.impl.topology.movement.modularosm.IAttractionBasedMovementAnalyzer;
import de.tud.kom.p2psim.impl.topology.movement.modularosm.attraction.IAttractionProvider;
import de.tudarmstadt.maki.simonstrator.api.Monitor;
import de.tudarmstadt.maki.simonstrator.api.Randoms;
import de.tudarmstadt.maki.simonstrator.api.component.sensor.location.IAttractionPoint;

/**
 * Abstract base class for Transition Strategies ({@link IAttractionAssigmentStrategy}s) using {@link IAttractionPoint}s.
 * 
 * @author Julian Zobel
 * @version 1.0, 23.01.2019
 */
public abstract class AbstractAttractionBasedAssignmentStrategy implements IAttractionAssigmentStrategy {

	protected Random rnd = Randoms.getRandom(AbstractAttractionBasedAssignmentStrategy.class);
	
	protected IAttractionProvider attractionProvider;
		
	protected LinkedHashMap<SimLocationActuator, IAttractionPoint> assignments = new LinkedHashMap<>();	
	protected LinkedHashMap<SimLocationActuator, IAttractionPoint> lastAssignments = new LinkedHashMap<>();
	
	private LinkedList<AttractionAssignmentListener> listeners = new LinkedList<>();
	
	protected long defaultPauseTimeMin = 0;	
	protected long defaultPauseTimeMax = 0;
	
	protected final static int EVENT_PAUSE_ENDED = 1;
	
	public AbstractAttractionBasedAssignmentStrategy(long defaultPauseTimeMin, long defaultPauseTimeMax) {
		assert defaultPauseTimeMax >= 0 && defaultPauseTimeMin >= 0;
		assert defaultPauseTimeMax >= defaultPauseTimeMin;
		
		this.defaultPauseTimeMax = defaultPauseTimeMax;
		this.defaultPauseTimeMin = defaultPauseTimeMin;
	}
	
	public void setAttractionProvider(IAttractionProvider attractionProvider) {
		this.attractionProvider = attractionProvider;
	}
	
	@Override
	public IAttractionPoint getAssignment(SimLocationActuator host) {
		return assignments.get(host);
	}
	
	@Override
	public IAttractionPoint getLastAssignment(SimLocationActuator host) {
		return lastAssignments.get(host);
	}
	
	@Override
	public void addAttractionAssignmentListener(
			AttractionAssignmentListener listener) {
		listeners.add(listener);
	}
	
	@Override
	public void removeAttractionAssignmentListener(
			AttractionAssignmentListener listener) {
		listeners.remove(listener);
	}

	/**
	 * Notify all listeners of an updated attraction point assignment for the given component
	 * 
	 * @param comp
	 * @param attractionPoint
	 */
	protected void notifyListenersOfAssignmentUpdate(SimLocationActuator comp,
			IAttractionPoint attractionPoint) {
		listeners.forEach(listener -> listener.updatedAttractionAssignment(comp, attractionPoint));
		
		// also iInform analyzer of assigment update
		if(Monitor.hasAnalyzer(IAttractionBasedMovementAnalyzer.class)) {			
			Monitor.getOrNull(IAttractionBasedMovementAnalyzer.class).onUpdateAttractionAssignment(comp, attractionPoint);
		}
	}
	
	@Override
	public void updateTargetAttractionPoint(SimLocationActuator comp,
			IAttractionPoint attractionPoint) {
		if(assignments.containsKey(comp)) {
			this.lastAssignments.put(comp, this.assignments.remove(comp));
		}
		this.assignments.put(comp, attractionPoint);
		notifyListenersOfAssignmentUpdate(comp, attractionPoint);		
	}
	
	private long getRandomUniformDistributionPauseTime(IAttractionPoint attractionPoint) {
		
		// if attraction point has own pause time use it, otherwise use default values
		if(attractionPoint != null && attractionPoint.hasPauseTime()) {
			return (long) (rnd.nextDouble() * (attractionPoint.getPauseTimeMax() - attractionPoint.getPauseTimeMin())) + attractionPoint.getPauseTimeMin();
		}
		else {
			return (long) (rnd.nextDouble() * (defaultPauseTimeMax - defaultPauseTimeMin)) + defaultPauseTimeMin;
		}		
	}
	
	public long getPauseTime(IAttractionPoint attractionPoint) {
		long pause = getRandomUniformDistributionPauseTime(attractionPoint);
		//System.out.println(Time.getFormattedTime(pause));
		return pause;
	}	
		
	/**
	 * Get a new assignment for this component, that is not the last assignment
	 * 
	 * @param host
	 * @return
	 */
	protected IAttractionPoint getNewAttractionPointAssignment(SimLocationActuator host) {		
			
		double score = rnd.nextDouble();		
		LinkedList<IAttractionPoint> candidates = new LinkedList<>();
		for (IAttractionPoint ap : attractionProvider.getAttractionPoints()) {
			// skip null values (should never occur!)
			if(ap == null) {
				continue;
			}
			
			// fast skip forward if the attraction point would not be considered due to its score
			if (score >= ap.getWeight()) {
				continue;
			}
			
			// skip attraction points that are currently assigned to the host
			if(assignments.containsKey(host) && ap.equals(assignments.get(host))) {
				continue;
			}
			
			// add the attraction point to the list of candidates, if no attraction point was 
			// previously visited, or if currently no attraction point is assigned and the last
			// assignment was not this attraction point (this prevents that, if the current attraction 
			// point was already set to the last assignment, the last visited attraction point is also considered)
			if(lastAssignments.get(host) == null || 
					(!assignments.containsKey(host) && !ap.equals(lastAssignments.get(host)))) {
				candidates.add(ap);
			}				
			
		}
		
		// fallback: if no attraction point is considered, randomly select one, except the current assignment
		if (candidates.isEmpty()) {
			candidates.addAll(attractionProvider.getAttractionPoints());
			
			if(assignments.containsKey(host) && candidates.size() > 1) {
				candidates.remove(assignments.get(host));
			}
		}
		
		IAttractionPoint assignment = candidates.get(rnd.nextInt(candidates.size()));		
		return assignment;		
	}  
	

	@Override
	public boolean hostInAttractionPointArea(SimLocationActuator host) {		
		for(IAttractionPoint ap : attractionProvider.getAttractionPoints()) {			
			if(ap.distanceTo(host.getRealPosition()) <= ap.getRadius()) {
				return true;
			}
		}
		
		return false;
	}
}
