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

import de.tud.kom.p2psim.api.topology.movement.SimLocationActuator;
import de.tud.kom.p2psim.api.topology.movement.local.LocalMovementStrategy;
import de.tud.kom.p2psim.impl.topology.movement.modularosm.ISocialGroupMovementAnalyzer;
import de.tud.kom.p2psim.impl.topology.movement.modularosm.attraction.IAttractionGenerator;
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.transition.IAttractionAssigmentStrategy;
import de.tud.kom.p2psim.impl.topology.util.PositionVector;
import de.tud.kom.p2psim.impl.util.Either;
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.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;

/**
 * This class is responsible for creating or deleting groups. Therefore the amount of hosts in each POI
 * is checked and from this set of hosts candidates are chosen which then form a group.
 * Deletion of groups happens only in the case when they reached their group leaders destination.
 * 
 * @author Marcel Verst
 * @version 1.0, 09.11.2018
 * 
 * 
 * Updated the group forming to use the simulation time units instead of integer +1 counters.
 * Simplified code and bug fixes.
 * 
 * @author Julian Zobel
 * @version 1.1, 29.01.2020
 */
public class DefaultGroupForming extends AbstractGroupForming {
		
	@XMLConfigurableConstructor({"enableGroups", "maxNumberOfGroups", "minGroupSize", "maxGroupSize", "probabilityToJoin", "groupFormationSetupDelay", "groupFormationDelay", "groupRejoinWaitTime"})
	public DefaultGroupForming(boolean enableGroups, int maxNumberOfGroups,	int minGroupSize, int maxGroupSize, double probabilityToJoin, 
			long groupFormationSetupDelay, long groupFormationDelay, long groupRejoinWaitTime) {
		
		super(enableGroups, maxNumberOfGroups, minGroupSize, maxGroupSize,
				probabilityToJoin, groupFormationSetupDelay, groupFormationDelay,
				groupRejoinWaitTime);		
	}

	public void manageGroups() {			
		
		// group forming may be deactivated
		if(!enableGroups || probabilityToJoin == 0) {
			return;
		}
		
		// setup delay before forming the first group...
		if(Time.getCurrentTime() <= groupFormationSetupDelay) {
			return;
		}		
			
		runGroupDeletionProcess();	
	}

	/* 
	 * =====================================================================================================
	 * === GROUP BUILDING PROCESS
	 * =====================================================================================================
	 */	
	
	@Override
	protected void assembleGroup() {		
		IAttractionPoint apCandidate = IAttractionGenerator.attractionPoints.get(rand.nextInt(IAttractionGenerator.attractionPoints.size()));
		if(apCandidate == null) {
			return;
		}

		Set<SimLocationActuator> hostsAtAttractionPoint = HostAtAttractionPointCounter.getHostsOfAttractionPoint(apCandidate, movementModel.getAllLocationActuators());
		
		int numberOfHostsInAttractionPoint = hostsAtAttractionPoint.size();
		int groupSize = this.rndGroupSize(numberOfHostsInAttractionPoint);
					
		Set<SimLocationActuator> groupCandidates = new LinkedHashSet<>();
		
		// shuffle the hosts at the attraction point (more randomness ftw!) 
		LinkedList<SimLocationActuator> randomShuffledHostsAtAttractionPoint = new LinkedList<>(hostsAtAttractionPoint);
		Collections.shuffle(randomShuffledHostsAtAttractionPoint, rand);					
					
		for (SimLocationActuator m : randomShuffledHostsAtAttractionPoint) {
			
			// do not continue if enough group members are gathered
			if(groupCandidates.size() == groupSize) {
				break;
			}
			
			// skip already added hosts (should never happen)
			if(groupCandidates.contains(m)) {
				continue;
			}
			
			// skip single hosts
			if(movementModel.getSingleHosts().contains(m)) {
				continue;
			}
			
			// skip hosts that are already part of a group
			if(groupCon.isGroupMember(m)) {
				continue;
			}
			
			// test if the host wants and also is allowed to join this group 
			if(wantsToJoin()) {					
				if(!beenInGroup(m) || groupRejoinTimerExpired(m)) {
					groupCandidates.add(m);
				}					
			}			
		}		
		
		// if there are no candidates, wait a minute until asking again
		if(groupCandidates.isEmpty() || groupCandidates == null || groupCandidates.size() < minGroupSize) {			
			Event.scheduleWithDelay(Time.MINUTE, new EventHandler() {			
				@Override
				public void eventOccurred(Object content, int type) {
					assembleGroup();			
				}
			}, null, 0);
			
			return;
		}
		
		for(SimLocationActuator candidate : groupCandidates) {
			INodeID id = candidate.getHost().getId();
			stayDuration.put(id, new Tuple<Long, Long>(0L, Time.getCurrentTime())); // FIXME why?
		}			
		
		createGroup(groupCandidates);
	}
	
	/* 
	 * =====================================================================================================
	 * === GROUP DELETION PROCESS
	 * =====================================================================================================
	 */
	
	public void runGroupDeletionProcess() {
		
		LocalMovementStrategy movementStrategy = movementModel.getMovementStrategy();
		IAttractionAssigmentStrategy transition = movementModel.getAttractionAssignmentStrategy();

		Set<SocialMovementGroup> groupsToRemove = new LinkedHashSet<>();

		Set<SocialMovementGroup> groups = groupCon.getGroups();
		
		for(SocialMovementGroup group : groups) {
			SimLocationActuator leader = group.getLeader();
			PositionVector target = movementModel.getCurrentTargets().get(leader);
			
			Either<PositionVector, Boolean> either = movementStrategy.nextPosition(leader, target);
			if (either.hasLeft()) {
				// Do nothing
			} else {
				transition.reachedAttractionPoint(leader, leader.getCurrentTargetAttractionPoint());
				
				// Inform analyzer of group removal
				if(Monitor.hasAnalyzer(ISocialGroupMovementAnalyzer.class)) {			
					Monitor.getOrNull(ISocialGroupMovementAnalyzer.class).onGroupArriveAtAttractionPoint(group);
				}				
				
				groupsToRemove.add(group);
			}
		}

		for(SocialMovementGroup group : groupsToRemove) {
			removeGroup(group);
		}
	}

	/* 
	 * =====================================================================================================
	 * === HELPER FUNCTIONS
	 * =====================================================================================================
	 */
	/**
	 * Updates the stayDuration of each Host
	 */
	public void updateStayDuration() {
		for(SimLocationActuator component : movementModel.getAllLocationActuators()) {
			INodeID id = component.getHost().getId();

			// In AP, increase stayDuration
			if(movementModel.getAttractionAssignmentStrategy().hostInAttractionPointArea(component)) {
				
				Tuple<Long, Long> timeInfo = stayDuration.get(id);					
				long delta = Time.getCurrentTime() - timeInfo.getB();
				
				// update information (increase overall stay duration and set current time as update time) 
				stayDuration.put(id, new Tuple<Long, Long>(timeInfo.getA() + delta, Time.getCurrentTime()));
			}
			// Not in AP, reset stayDuration
			else {
				stayDuration.put(id, new Tuple<Long, Long>(0L, Time.getCurrentTime()));
			}
		}
	}
	
}
