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

import java.util.LinkedHashSet;
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.groups.MovementGroupContainer;
import de.tud.kom.p2psim.impl.topology.movement.modularosm.groups.SocialMovementGroup;
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.datastructures.Pair;
import de.tudarmstadt.maki.simonstrator.api.util.XMLConfigurableConstructor;

/**
 * This class contains methods used by all encounter strategies.
 * 
 * @author Marcel Verst, Julian Zobel
 * @version 1.1, 30.01.2020
 */
public abstract class AbstractGroupEncounter implements IGroupEncounterBehavior {
	
	//--- COMMON VARIABLES TO BE DECLARED IN CONFIG ---
	protected boolean enableGroupEncounters;
	protected double groupEncounterMeetingDistance;
	protected long groupReencounterWaitTime;
	//-------------------------------------------------
	
	protected final int DECISION_NONE = 0;
	protected final int DECISION_MERGE_FULL = 1;
	protected final int DECISION_MERGE_PARTIAL = 2; 
	protected final int DECISION_DISSOLVE = 3;
	protected final int DECISION_WAIT = 3;
	
	protected SocialGroupMovementModel movementModel;
	protected MovementGroupContainer groupCon;
	protected Random rand = Randoms.getRandom(AbstractGroupEncounter.class);
	
	@XMLConfigurableConstructor({"enableGroupEncounters","groupEncounterMeetingDistance","groupReencounterWaitTime"})
	public AbstractGroupEncounter(boolean enableGroupEncounters, double groupEncounterMeetingDistance, long groupReencounterWaitTime) {
		
		if(enableGroupEncounters) {
			if (groupEncounterMeetingDistance < 0) {
				throw new ConfigurationException(
						"AbstractGroupEncounter: Variable groupEncounterMeetingDistance must be >= 0!");
			}
			
			if(groupReencounterWaitTime < 0) {
				throw new ConfigurationException(
						"AbstractGroupEncounter: Variable groupReencounterWaitTime must be >= 0!");
			}
		}
		
		this.enableGroupEncounters = enableGroupEncounters;
		this.groupEncounterMeetingDistance = groupEncounterMeetingDistance;
		this.groupReencounterWaitTime = groupReencounterWaitTime;
	}
	
	@Override
	public void initialize(SocialGroupMovementModel movementModel) {
		this.movementModel = movementModel;
		groupCon = MovementGroupContainer.getInstance();
		
		for(SimLocationActuator host : movementModel.getAllLocationActuators()) {
			groupCon.addHasMergedEntry(host, false);
		}
	}
	
	/**
	 * Checks if any of the current groups come in a close range which is equivalent to a group meeting. 
	 * Calls the handleGroupEncounters(Group, Group) method to define the group behavior.
	 *
	 * @return 
	 */
	private Set<Pair<SocialMovementGroup>> getEncounteringGroups() {
		if(!enableGroupEncounters)
			return null;
		
		Set<Pair<SocialMovementGroup>> encounteringGroups = new LinkedHashSet<>();
		Set<SocialMovementGroup> alreadyProcessed = new LinkedHashSet<>();
				
		Set<SocialMovementGroup> allGroups = groupCon.getGroups();
		
		for(SocialMovementGroup group1 : allGroups) {
			for(SocialMovementGroup group2 : allGroups) {
				if(group1 == group2) {
					continue;
				}
				
				// skip if at least one of the groups is in the area of an attraction point
				if(movementModel.getAttractionAssignmentStrategy().hostInAttractionPointArea(group1.getLeader())
						|| movementModel.getAttractionAssignmentStrategy().hostInAttractionPointArea(group2.getLeader())) {				
					continue;
				}
				
				if(!(alreadyProcessed.contains(group1) && alreadyProcessed.contains(group2))) {
					
					if(getDistanceBetweenGroups(group1, group2) <= groupEncounterMeetingDistance) {
						SimLocationActuator g1Leader = group1.getLeader();
						
						if(!groupCon.getHasMerged().get(g1Leader) || waitedLongEnoughAfterMerging(g1Leader) ) {
							
							Pair<SocialMovementGroup> encounter = new Pair<SocialMovementGroup>(group1, group2);						
							encounteringGroups.add(encounter);
							
							alreadyProcessed.add(group1);
							alreadyProcessed.add(group2);
						}							
					}
				}			
			}
		}
		
		return encounteringGroups;
	}
	

	public void handleEncounters() {
		
		if(!enableGroupEncounters)
			return;			
		
		Set<Pair<SocialMovementGroup>> encounteringGroups = getEncounteringGroups();
		
		if(encounteringGroups == null || encounteringGroups.isEmpty()) {
			return;
		}
		
		for(Pair<SocialMovementGroup> encounter : encounteringGroups) {
			if(groupCon.hasGroup(encounter.getFirst()) && groupCon.hasGroup(encounter.getSecond())) {	
				
				// Inform analyzer of resolved movement
				if(Monitor.hasAnalyzer(ISocialGroupMovementAnalyzer.class)) {			
					Monitor.getOrNull(ISocialGroupMovementAnalyzer.class).onGroupEncounter(encounter.getFirst(), encounter.getSecond());
				}
				
				handleGroupEncounter(encounter.getFirst(), encounter.getSecond());
			}			
		}
	}

	/**
	 * Defines what to do with both groups in case they encountered.
	 * 
	 * @param SocialMovementGroup The first group.
	 * @param SocialMovementGroup The second group.
	 * 
	 */
	protected abstract void handleGroupEncounter(SocialMovementGroup group1, SocialMovementGroup group2);
		
	/**
	 * Returns the distance between two groups based on their leaders position.
	 * 
	 * @param SocialMovementGroup The first group.
	 * @param SocialMovementGroup The second group.
	 * 
	 * @return Double Distance between group leaders of both groups.
	 * 
	 */
	public double getDistanceBetweenGroups(SocialMovementGroup group1, SocialMovementGroup group2) {
		PositionVector l1Pos = group1.getLeaderPosition();
		PositionVector l2Pos = group2.getLeaderPosition();
		return l1Pos.distanceTo(l2Pos);
	}

	/**
	 * Compares the size of two groups and returns the larger group.
	 * 
	 * @param SocialMovementGroup The first group.
	 * @param SocialMovementGroup The second group.
	 * @return GroupInstance The larger group, or the first group if both are the same size.
	 * 
	 */
	protected SocialMovementGroup getLargerGroup(SocialMovementGroup group1, SocialMovementGroup group2) {
		
		if(group1.getGroupSize() >= group2.getGroupSize()) {
			return group1;
		}
		else {
			return group2;
		}		
	}
	
	/**
	 * Compares the size of two groups and returns the smaller group.
	 * 
	 * @param SocialMovementGroup The first group.
	 * @param SocialMovementGroup The second group.
	 * @return GroupInstance The smaller group, or the second group if both are the same size.
	 * 
	 */
	protected SocialMovementGroup getSmallerGroup(SocialMovementGroup group1, SocialMovementGroup group2) {
		
		if(group1.getGroupSize() < group2.getGroupSize()) {
			return group1;
		}
		else {
			return group2;
		}
	}
	
	/**
	 * Returns true, if a host has waited long enough after its last merging process.
	 * 
	 * @param SimLocationActuator The host to be checked.
	 * @param int The time a host has to wait until it can merge again.
	 * @return boolean
	 * 
	 */
	protected boolean waitedLongEnoughAfterMerging(SimLocationActuator host) {
		return ((Time.getCurrentTime() - groupCon.getMergedAtTime().get(host)) >= groupReencounterWaitTime);
	}	
	
	/**
	 * Removes both groups on encounter.
	 * 
	 */
	protected void dissolveGroups(SocialMovementGroup group1, SocialMovementGroup group2) {
		groupCon.updateMergeVars(group1, group2);		
		
		if(groupCon.getGroups().contains(group1)) {			
			movementModel.getGroupFormingBehavior().removeGroup(group1);
		}
			
		if(groupCon.getGroups().contains(group2)) {					
			movementModel.getGroupFormingBehavior().removeGroup(group2);			
		}
	}
	
	/**
	 * Merges two groups together into one single group. The smaller group is merged into the larger group.
	 * 
	 * @param SocialMovementGroup The first group.
	 * @param SocialMovementGroup The second group.
	 * 
	 * @return Returns the merged group
	 */
	protected SocialMovementGroup mergeGroupsFully(SocialMovementGroup group1, SocialMovementGroup group2) {
		groupCon.updateMergeVars(group1, group2);
				
		SocialMovementGroup large = getLargerGroup(group1, group2);
		SocialMovementGroup small = getSmallerGroup(group1, group2);

		Set<SimLocationActuator> toRemove = new LinkedHashSet<>(small.getMembers());
		
		for(SimLocationActuator participant : toRemove) {
			small.removeHostFromGroup(participant);
			groupCon.addLeftGroupAtTimeEntry(participant, Time.getCurrentTime());
			large.addHostToGroup(participant);
		}

		// Inform analyzer of resolved movement
		if(Monitor.hasAnalyzer(ISocialGroupMovementAnalyzer.class)) {			
			Monitor.getOrNull(ISocialGroupMovementAnalyzer.class).onGroupMerge(group1, group2);
		}
		
		movementModel.getGroupFormingBehavior().removeGroup(small);
				
		return large;
	}	
	
	/**
	 * Merges two groups partially. A specified number of hosts leaves the larger group and joins the smaller group.
	 * 
	 * @param SocialMovementGroup The first group.
	 * @param SocialMovementGroup The second group.
	 * 
	 */
	protected void mergeGroupsPartially(SocialMovementGroup group1, SocialMovementGroup group2, int numberOfHostsToLeave) {
		
		if(numberOfHostsToLeave <= 0) 
			return;			
			
		SocialMovementGroup large = getLargerGroup(group1, group2);
		SocialMovementGroup small = getSmallerGroup(group1, group2);

		if((large.getGroupSize() - numberOfHostsToLeave) >= movementModel.getGroupFormingBehavior().getMinGroupSize()) {
			groupCon.updateMergeVars(group1, group2);

			for(int i = 0; i < numberOfHostsToLeave; i++) {
				SimLocationActuator randomParticipant = large.getRandomMember();
				groupCon.addLeftGroupAtTimeEntry(randomParticipant, Time.getCurrentTime());
				large.removeHostFromGroup(randomParticipant);
				small.addHostToGroup(randomParticipant);
				groupCon.updateGroupEntry(small);
				groupCon.updateGroupEntry(large);
			}
			
			// Inform analyzer of resolved movement
			if(Monitor.hasAnalyzer(ISocialGroupMovementAnalyzer.class)) {			
				Monitor.getOrNull(ISocialGroupMovementAnalyzer.class).onGroupMerge(group1, group2);
			}
		}
		
	}
		
	protected void wait(SocialMovementGroup group, long timeToWait) {
		// only start the waiting process if not already waiting (duh!)
		if(!groupCon.waitingGroups.containsKey(group)) {		
			
			// Inform analyzer of resolved movement
			if(Monitor.hasAnalyzer(ISocialGroupMovementAnalyzer.class)) {			
				Monitor.getOrNull(ISocialGroupMovementAnalyzer.class).onGroupWait(group);
			}
			
			groupCon.waitingGroups.put(group, new Tuple<Long, Long>(timeToWait, Time.getCurrentTime()));			
			Event.scheduleWithDelay(timeToWait, new EventHandler() {				
				@Override
				public void eventOccurred(Object content, int type) {					
					SocialMovementGroup g = (SocialMovementGroup) content;					
					// if group was deleted in the meantime (e.g., by dissolving/merging), just do nothing
					if(!groupCon.hasGroup(g)) {					
						return;
					}
					groupCon.updateMergeVars(g);
					groupCon.waitingGroups.remove(g);
				}
			}, group, 0);			
		}
	}
	
	protected abstract int decide();

}
