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


import java.util.Random;
import de.tud.kom.p2psim.api.scenario.ConfigurationException;
import de.tud.kom.p2psim.impl.topology.util.PositionVector;
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.Monitor.Level;
import de.tudarmstadt.maki.simonstrator.api.Randoms;
import de.tudarmstadt.maki.simonstrator.api.util.XMLConfigurableConstructor;

/**
 * Implementation of the interface {@link AttractionGenerator}.
 * 
 * Generates a random number (out of a given interval) of attraction points. The radius is chosen randomly from a given interval.
 * Generated attraction points will most likely not overlay and be completely within the world dimensions.
 * With a certain lifetime chosen from a given lifetime interval, attraction points are removed after time and the number of 
 * attraction points that will be in the area are recalculated. If necessary, attraction points are either removed
 * or added.  
 * 
 * @author Julian Zobel
 * @version 1.0, March 2019
 */
public class RandomDynamicAttractionGenerator extends AbstractAttractionProvider {

	private Random rand;

	private int minNumberOfAttractionPoints;
	private int maxNumberOfAttractionPoints;
	
	private long minDynamicIntervall;
	private long maxDynamicIntervall;
	
	private double minimumDistance = 100;
	
	private double maximumRadius = 100;	
	private double minimumRadius = 10;
	
	@XMLConfigurableConstructor({"minNumberOfAttractionPoints", "maxNumberOfAttractionPoints", "minimumRadius", 
		"maximumRadius", "minDynamicIntervall", "maxDynamicIntervall", "minimumDistance" })
	public RandomDynamicAttractionGenerator(int minNumberOfAttractionPoints, int maxNumberOfAttractionPoints, 
			double minimumRadius, double maximumRadius, long minDynamicIntervall, long maxDynamicIntervall,
			double minimumDistance) {
		
		this.rand = Randoms.getRandom(RandomDynamicAttractionGenerator.class);
				
		if (minNumberOfAttractionPoints <= 0) {
			throw new ConfigurationException(
					"NumberOfAttractionPoints should be at least 1!");
		}
		if(minNumberOfAttractionPoints > maxNumberOfAttractionPoints) {
			throw new ConfigurationException(
					"Minimum and maximum numbers of attraction points not correctly set!");
		}
		
		this.minNumberOfAttractionPoints = minNumberOfAttractionPoints;
		this.maxNumberOfAttractionPoints = maxNumberOfAttractionPoints;
		
		this.minimumRadius = minimumRadius;
		this.maximumRadius = maximumRadius;
		this.minimumDistance = minimumDistance;
		
		this.minDynamicIntervall = minDynamicIntervall;
		this.maxDynamicIntervall = maxDynamicIntervall;
				
		updateAttractionPoints();		
	}
	
	private int randomNumberOfAttractionPoints() {
		return minNumberOfAttractionPoints + rand.nextInt((maxNumberOfAttractionPoints - minNumberOfAttractionPoints) + 1);
	}
		
	private void scheduleDynamicEvent(AttractionPoint attractionPoint) {
		
		long r =  (long) (rand.nextDouble() * (maxDynamicIntervall - minDynamicIntervall)) + minDynamicIntervall;
		
		Event.scheduleWithDelay(r, new EventHandler() {
			@Override
			public void eventOccurred(Object content, int type) {
				// maybe was already removed...
				if(hasAttractionPoint(attractionPoint)) {
					removeAttractionPoint(attractionPoint);
					updateAttractionPoints();
				}
			}
		}, null, 0);
	}
			
	protected void updateAttractionPoints() {			
		int numberOfAttractionPoints = randomNumberOfAttractionPoints();
		int currentSize = getAttractionPoints().size();
		
		// do nothing if this is the required amount of attraction points
		if(numberOfAttractionPoints == currentSize) {
			return;
		}
		
		// remove until this number fits
		else if(numberOfAttractionPoints < currentSize) {			
			int deltaAP = currentSize - numberOfAttractionPoints;			
			
			for(int i = 0; i < deltaAP; i++) {
				int random = rand.nextInt(currentSize);
				removeAttractionPoint(getAttractionPoints().get(random));
			}			
			return;
		}
		
		// add more attraction points until it fits
		else {			
			int deltaAP = numberOfAttractionPoints - currentSize;			
			
			for(int i = 0; i < deltaAP; i++) {
				AttractionPoint newAP = createAttractionPoint();
				scheduleDynamicEvent(newAP);
				addAttractionPoint(newAP);
			}
		}		
	}
	
	/**
	 * Create an attraction point that is conform to all other currently saved attraction points.
	 * 
	 * @return
	 */
	private AttractionPoint createAttractionPoint() {
		// make a break counter to prevent more than 100 iterations and an infinity loop in general.
		int c = 100;
		
		create: for(int i = 0; i < c; i++) {
			PositionVector posVec = createPosVec();
			
			// set the radius of this attraction point
			// minimum radius is 10 meters
			double radius = Math.max(minimumRadius, rand.nextDouble() * maximumRadius);
					
			if(i < c) {				
				// check if the attraction points would be completely within world dimensions (including radius!)
				if((posVec.getX() + radius) >= worldDimension.getX() || (posVec.getY() + radius) >= worldDimension.getY()
							|| (posVec.getX() - radius) <= 0 || (posVec.getY() - radius) <= 0) {
					continue create;
				}
				
				// if within world dimensions, continue checking against other attraction points							
				for (AttractionPoint ap : getAttractionPoints()) {					
					// if this point is closer than the given minimum distance to another point, or the radii of the points would overlap, 
					// or if the radius would exceed the simulation area
					// then discard this attraction point and create a new one
					if(posVec.distanceTo(ap) < minimumDistance || (posVec.distanceTo(ap) - radius - ap.getRadius()) < 0) {							
						continue create;
					}
				}			
				
			}
			else {	
				radius = 0;
			}			
					
			AttractionPoint aPoint = new AttractionPoint("AP-" + rand.nextInt(), posVec);
			aPoint.setRadius(radius);			
			return aPoint;
		}
		
		AttractionPoint ap = new AttractionPoint("AP-ERROR", new PositionVector(worldDimension.getX() / 2, worldDimension.getY() / 2));
		ap.setRadius(0);
		ap.setWeight(0);
		
		Monitor.log(RandomDynamicAttractionGenerator.class, Level.WARN, "RandomDynamicAttractionGenerator could not find a suitable location for a new attraction point within 100 iterations.");
		
		return ap;
	}
		
	private PositionVector createPosVec() {
		double x = rand.nextDouble() * worldDimension.getX();
		double y = rand.nextDouble() * worldDimension.getY();
		return new PositionVector(x, y);
	}	
}
