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

import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Random;
import java.util.Set;

import de.tud.kom.p2psim.api.scenario.ConfigurationException;
import de.tud.kom.p2psim.api.topology.movement.SimLocationActuator;
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.BasicAttractionPoint;
import de.tud.kom.p2psim.impl.topology.movement.modularosm.attraction.IAttractionProvider;
import de.tud.kom.p2psim.impl.topology.movement.modularosm.attraction.hostcount.HostAtAttractionPointCounter;
import de.tud.kom.p2psim.impl.topology.movement.modularosm.groups.SocialMovementGroup;
import de.tud.kom.p2psim.impl.topology.movement.modularosm.groups.MovementGroupContainer;
import de.tud.kom.p2psim.impl.topology.util.PositionVector;
import de.tud.kom.p2psim.impl.util.Tuple;
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.Time;
import de.tudarmstadt.maki.simonstrator.api.common.graph.INodeID;
import de.tudarmstadt.maki.simonstrator.api.component.sensor.location.IAttractionPoint;
import de.tudarmstadt.maki.simonstrator.api.util.XMLConfigurableConstructor;

/**
 * Abstract class implementation for implementing GroupFormingStrategies.
 * 
 * @author Marcel Verst, Julian Zobel
 * @version 1.1, 30.01.2020
 */
public abstract class AbstractGroupForming implements IGroupFormingBehavior {
	protected Random rand = Randoms.getRandom(AbstractGroupForming.class);	
	
	protected SocialGroupMovementModel movementModel;	
	protected MovementGroupContainer groupCon;	
	
	// Holds time information of nodes for the groups (stay duration, last update)
	protected Map<INodeID, Tuple<Long,Long>> stayDuration;
	
	// GROUP VARIABLES FROM CONFIGS
	protected boolean enableGroups;
	protected int minGroupSize;
	protected int maxGroupSize;
	protected double probabilityToJoin;
	protected int maxNumberOfGroups;	
	
	protected long groupRejoinWaitTime;
	protected long groupFormationSetupDelay;
	private long groupFormationDelay;
	
	public static AbstractGroupForming instance;

	@XMLConfigurableConstructor({"enableGroups", "maxNumberOfGroups", "minGroupSize", "maxGroupSize", "probabilityToJoin", "groupFormationSetupDelay", "groupFormationDelay","groupRejoinWaitTime"})
	public AbstractGroupForming(boolean enableGroups, int maxNumberOfGroups, int minGroupSize, int maxGroupSize, double probabilityToJoin, long groupFormationSetupDelay, long groupFormationDelay, long groupRejoinWaitTime) {
		
		instance = this;
		
		this.enableGroups = enableGroups;
		
		if(enableGroups) {
			if (probabilityToJoin < 0 || probabilityToJoin > 1.0) {
				throw new ConfigurationException(
						"GroupForming: probabilityToJoin must be between 0.0 and 1.0!");
			}
			
			if (maxNumberOfGroups <= 0) {
				throw new ConfigurationException(
						"GroupForming: maxNumberOfGroups must be >= 1!");
			}
			
			if (minGroupSize < 0) {
				throw new ConfigurationException(
						"GroupForming: minGroupSize must be >= 1!");
			}
			
			if (maxGroupSize < minGroupSize) {
				throw new ConfigurationException(
						"GroupForming: maxGroupSize cannot be bigger than minGroupSize!");
			}
			
			if (groupFormationSetupDelay <= 0) {
				throw new ConfigurationException(
						"GroupForming: groupFormationSetupDelay must be > 0!");
			}
			
			if (groupFormationDelay < -1) {
				throw new ConfigurationException(
						"GroupForming: groupFormationDelay must be >= -1!");
			}
			
			if (groupRejoinWaitTime < 0) {
				throw new ConfigurationException(
						"GroupForming: groupRejoinWaitTime must be >= 1!");
			}
		}
		
		this.maxNumberOfGroups = maxNumberOfGroups;
		this.minGroupSize = minGroupSize;
		this.maxGroupSize = maxGroupSize;
		this.probabilityToJoin = probabilityToJoin;		
		this.groupFormationSetupDelay = groupFormationSetupDelay;
		this.groupFormationDelay = groupFormationDelay;
		this.groupRejoinWaitTime = groupRejoinWaitTime;
	}
	
	@Override
	public void initialize(SocialGroupMovementModel movementModel) {
		this.movementModel = movementModel;
		
		groupCon = MovementGroupContainer.getInstance();		
		
		stayDuration = new LinkedHashMap<INodeID, Tuple<Long,Long>>();

		for(SimLocationActuator host : movementModel.getAllLocationActuators()) {
			stayDuration.put(host.getHost().getId(), new Tuple<Long, Long>(0L, Time.getCurrentTime()));			
		}
				
		
		if(!enableGroups) {
			return;
		}
		
		for(int g = 0; g < maxNumberOfGroups; g++) {			
			long delay = Math.max(Time.MINUTE, (long) ((rand.nextDouble() * (Time.MINUTE * 45) + Time.MINUTE)));										
			//System.out.println("("+g+") Init Group Formation Time " + Time.getFormattedTime(delay) );
						
			Event.scheduleWithDelay(delay, new EventHandler() {			
				@Override
				public void eventOccurred(Object content, int type) {
					assembleGroup();
				}
			}, null, 0);			
		}		
	}

	
	/**
	 * Returns a time delay between removal and creation of a group. A random part of up to half of the 
	 * stated formation delay is added to/substracted from the formation delay.
	 * 
	 * @return
	 */
	protected long rndGroupFormationDelay() {		
		return (long) ((rand.nextDouble() + 0.5) * groupFormationDelay);		
	}
	
	/**
	 * Returns a random integer value between the minimum and maximumn group size boundaries 
	 * with an additional upper boundary. 
	 *  
	 * @return
	 */
	protected int rndGroupSize(int max) {			
		return Math.min(rndGroupSize(), max);
	}
	
	/**
	 * Returns a random integer value between the minimum and maximumn group size boundaries.
	 *  
	 * @return
	 */
	protected int rndGroupSize() {
		if(maxGroupSize == minGroupSize) 
			return minGroupSize;
		else {
			// int groupsize = (int) Math.max(1, rand.nextGaussian() * 0.93 + 2.76);
			int groupsize = rand.nextInt(maxGroupSize - minGroupSize + 1) + minGroupSize;
			//System.out.println("[AbstractGroupForming] Group Size: " + groupsize + " ("+minGroupSize+"/"+maxGroupSize+")");
			return groupsize;
		}
		//return rand.nextInt(maxGroupSize - minGroupSize) + minGroupSize;
	}
	
	/**
	 * Creates a group with the given set of members.
	 * 
	 * @param Set<SimLocationActuator> The members which want to form a group.
	 */
	protected void createGroup(LinkedHashSet<SimLocationActuator> members) {		
		SocialMovementGroup group = new SocialMovementGroup(members);
	
		// force a new attraction point assignment on the leader
		IAttractionPoint currentDestination = movementModel.getAttractionAssignmentStrategy().getAssignment(group.getLeader());
		movementModel.getAttractionAssignmentStrategy().addComponent(group.getLeader());		
		IAttractionPoint targetDestination =  movementModel.getAttractionAssignmentStrategy().getAssignment(group.getLeader());
				
		// Add Offset to group destination
		PositionVector destination = movementModel.addOffset(new PositionVector(targetDestination),
				targetDestination.getRadius() / 3);
				
		group.setDestination(destination);
		setGroupMeetingPoint(group);

		for(SimLocationActuator member : group.getMembers()) {
			// only set the attraction point for non-leaders, as the leader was already assigned an attraction point above
			if(group.getLeader() != member) {
				member.setTargetAttractionPoint(targetDestination);
			}
			// remove any left-over entries
			if(groupCon.hostWasGroupMember(member)) {
				groupCon.removeLeftGroupAtTimeEntry(member);
			}
		}
		
		groupCon.addGroup(group);
		
		// Inform analyzer of new group
		if(Monitor.hasAnalyzer(ISocialGroupMovementAnalyzer.class)) {			
			Monitor.getOrNull(ISocialGroupMovementAnalyzer.class).onGroupForming(group, currentDestination, targetDestination);
		}
	}

	/**
	 * Removes a group.
	 * 
	 * @param SocialMovementGroup The group to be removed.
	 * 
	 */
	@Override
	public void removeGroup(SocialMovementGroup group) {
			
		// Inform analyzer of group removal
		if(Monitor.hasAnalyzer(ISocialGroupMovementAnalyzer.class)) {			
			Monitor.getOrNull(ISocialGroupMovementAnalyzer.class).onGroupDisbanding(group);
		}
				
		// If this group is removed, reassign the group target attraction point as new target attraction point 
		// for each former group member (prevents errors when routing was not reset correctly)
		if(group.getGroupSize() > 0 || group.getLeader() != null) {
			IAttractionPoint currentAP = group.getLeader().getCurrentTargetAttractionPoint();
			
			for (SimLocationActuator host : group.getMembers()) {		
				host.setTargetAttractionPoint(currentAP);
			}			
		}			
	
		groupCon.removeGroup(group);
		
		long delay = 0;		
			
		if(groupFormationDelay == -1) {
			if(group.getLeader() == null) {
				delay = movementModel.getAttractionAssignmentStrategy().getPauseTime(null);
			}	
			else {
				delay = movementModel.getAttractionAssignmentStrategy().getPauseTime(group.getLeader().getCurrentTargetAttractionPoint());
			}
		}
		else {
			delay = rndGroupFormationDelay();
		}
	
		Event.scheduleWithDelay(delay, new EventHandler() {			
			@Override
			public void eventOccurred(Object content, int type) {
				assembleGroup();		
			}
		}, null, 0);
	}
	
	public static void nodeready() {
		instance.assembleGroup();
	}

	protected abstract void assembleGroup();
	
	
	/**
	 * Returns the first AttractionPoint which contains the host with the greatest stayDuration of all hosts located in AttractionPoints.
	 * 
	 * @return AttractionPoint
	 * 
	 */
	protected IAttractionPoint getAttractionPointWithOldestHost() {
		IAttractionPoint result = null;
		long maxDuration = 0;
		
		for(IAttractionPoint ap : movementModel.getAttractionPoints()) {
			for(SimLocationActuator host :  HostAtAttractionPointCounter.getHostsOfAttractionPoint(ap, movementModel.getAllLocationActuators())) {
				INodeID id = host.getHost().getId();
				long duration = stayDuration.get(id).getA();
				
				if(duration > maxDuration) {					
					result = ap;
					maxDuration = duration;
				}
			}
		}
		
		return result;
	}
		
	protected IAttractionPoint getAttractionPointWithMostHosts() {
		IAttractionPoint apCandidate = null;
		int size = 0;
		for(IAttractionPoint ap : movementModel.getAttractionPoints()) {
			int numberOfHostsInAP = HostAtAttractionPointCounter.getHostCountOfAttractionPoint(ap, movementModel.getAllLocationActuators());
			if(numberOfHostsInAP > size) {
				apCandidate = ap;
				size = numberOfHostsInAP;
			}
		}
		return apCandidate;
	}
	
	/**
	 * Checks if a host wants to join a group or not based on a given probability.
	 * Returns true if willing to join, false else.
	 * 
	 * @return boolean
	 */
	protected boolean wantsToJoin() {
		return (rand.nextDouble() <= probabilityToJoin);
	}

	/**
	 * Checks if a host has waited long enough before he is allowed to join groups again.
	 * Returns true, if the host has waited long enough, false otherwise.
	 * 
	 * @param SimLocationActuator The host to be checked.
	 * @return boolean
	 */
	protected boolean groupRejoinTimerExpired(SimLocationActuator host) {		
		return groupCon.getTimeSinceHostLeftLastGroup(host) >= groupRejoinWaitTime;		
	}

	/**
	 * Returns true, if a host has been in a group before at least once. False else.
	 * 
	 * @param SimLocacationActuator The host to be checked.
	 * @return Boolean
	 */
	protected boolean beenInGroup(SimLocationActuator host) {
		return groupCon.hostWasGroupMember(host);
	}

	/* 
	 * =====================================================================================================
	 * === MEETING POINT FUNCTIONS
	 * =====================================================================================================
	 */
	/**
	 * Sets the meeting point of a group after the group has been created. Before walking towards the destination,
	 * the group members first have to meet at the meeting point.
	 * 
	 * @param SocialMovementGroup The group to set the meeting point.
	 */
	protected void setGroupMeetingPoint(SocialMovementGroup group) {
		PositionVector leaderPos = group.getLeader().getRealPosition();
		
		BasicAttractionPoint meetingPoint = 
				new BasicAttractionPoint("Group Movement Meeting Point of Leader #"+group.getLeader().getHost().getId(), leaderPos);
	
		group.setMeetingPoint(meetingPoint);
	}	
	
	@Override
	public int getMinGroupSize() {
		return minGroupSize;
	}

	@Override
	public int getMaxGroupSize() {
		return maxGroupSize;
	}

}
