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

import java.util.LinkedHashMap;
import java.util.LinkedHashSet;

import de.tud.kom.p2psim.api.scenario.ConfigurationException;
import de.tud.kom.p2psim.api.topology.Topology;
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.groups.MovementGroupContainer;
import de.tud.kom.p2psim.impl.topology.movement.modularosm.groups.SocialMovementGroup;
import de.tud.kom.p2psim.impl.topology.movement.modularosm.groups.groupencounter.IGroupEncounterBehavior;
import de.tud.kom.p2psim.impl.topology.movement.modularosm.groups.groupforming.IGroupFormingBehavior;
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.topology.views.VisualizationTopologyView.VisualizationInjector;
import de.tudarmstadt.maki.simonstrator.api.Binder;
import de.tudarmstadt.maki.simonstrator.api.Event;
import de.tudarmstadt.maki.simonstrator.api.Monitor;
import de.tudarmstadt.maki.simonstrator.api.Time;
import de.tudarmstadt.maki.simonstrator.api.component.sensor.location.IAttractionPoint;

/**
 * 
 * @author Julian Zobel
 * @version 1.0, 28.01.2020
 */
public class SocialGroupMovementModel extends ModularMovementModel {

	private final double MEETING_POINT_DISTANCE = 5;
	private final double LEADER_GROUP_DISTANCE = 10;
	
	protected MovementGroupContainer groupContainer;
	protected IGroupFormingBehavior groupFormingBehavior;
	protected IGroupEncounterBehavior groupEncounterBehavior;
		
	private LinkedHashSet<SimLocationActuator> singleHosts = new LinkedHashSet<SimLocationActuator>();
	private int numberOfSingleHosts;
		
	
	
	@Override
	public void initialize() {
		
		if (!initialized) {
			
			groupContainer = MovementGroupContainer.getInstance();
			groupFormingBehavior.initialize(this);
			groupEncounterBehavior.initialize(this);
			
			// Choose single hosts
			if(numberOfSingleHosts > 0) {
				do {
					singleHosts.add(getRandomActuator());
				} while(singleHosts.size() < numberOfSingleHosts);
			}
			
			if (modelVisualisation == null) {
				modelVisualisation = new ModularMovementModelViz(this);
			}
			VisualizationInjector.injectComponent(modelVisualisation);
			if (mapVisualization != null) {
				VisualizationInjector.injectComponent(mapVisualization);
			}
			if (attractionPointViz != null) {
				System.out.println("insert AP viz");
				VisualizationInjector.injectComponent(attractionPointViz);
			}

			checkConfiguration();

			// setWayPointModel
			localMovementStrategy.setObstacleModel(Binder
					.getComponentOrNull(Topology.class).getObstacleModel());
			localMovementStrategy.setWaypointModel(Binder
					.getComponentOrNull(Topology.class).getWaypointModel());

			/*
			 * Scale depending on calculation interval, if interval != 1 Second.
			 */
			localMovementStrategy
					.setScaleFactor(timeBetweenMoveOperation / (double) Time.SECOND);
			
			attractionAssigment.addAttractionAssignmentListener(this);
			attractionAssigment.setAttractionProvider(attractionProvider);

			// This adds the mobile hosts (smartphones/users) to the transition
			// strategy
			for (SimLocationActuator ms : moveableHosts) {
				attractionAssigment.addComponent(ms);
				if(placeNodesAtAP) {
					IAttractionPoint assignment = attractionAssigment.getAssignment(ms);
					ms.updateCurrentLocation(this.addOffset(new PositionVector(assignment),
							(assignment.hasRadius() ? Math.max(assignment.getRadius(), 25.0) : 25.0)));	
					attractionAssigment.updateTargetAttractionPoint(ms, assignment);
				}								
			}
						
			setTimeBetweenMoveOperations(timeBetweenMoveOperation);

			// Inform analyzer of resolved movement
			if(Monitor.hasAnalyzer(IAttractionBasedMovementAnalyzer.class)) {			
				Monitor.getOrNull(IAttractionBasedMovementAnalyzer.class).onAllNodeInitializationCompleted(moveableHosts);
			}
			
			// initial move
			move();

			initialized = true;
		}
	}
		
	
	/**
	 * Returns a random SimLocationActuator component from the set of moveable hosts.
	 * 
	 * @return SimLocationActuator
	 */
	private SimLocationActuator getRandomActuator() {
		
		int index = rand.nextInt(moveableHosts.size());
		int i = 0;
		for(SimLocationActuator actuator : moveableHosts) {
			if(i == index) {
				return actuator;
			}
			i++;
		}

		return null;
	}
	
	/**
	 * The movement model. 
	 * 1) Updates the number of hosts within each attraction point.
	 * 2) Creates and deletes groups, if necessary.
	 * 3) Applies encounter action to meeting groups.
	 * 4) Moves the hosts using doGroupMovement, movement style depends on role of the host (Leader, Participant, Single)
	 */
	@Override
	protected void move() {	
				
		// Check POIs if groups can be created. Delete groups, if destination reached.
		groupFormingBehavior.manageGroups();
		
		// Checks if groups encounter each other and applies a specified action to these groups.		
		groupEncounterBehavior.handleEncounters();
	
		/*	
		 * Moves all nodes according to definition in group movement method
		 */
		for (SimLocationActuator host : moveableHosts) {
			assert currentTargets.containsKey(host);
						
			// Single Host Movement
			if(singleHosts.contains(host) || !groupContainer.isGroupMember(host)) {			
				doLocalMovement(host, currentTargets.get(host));
			}
			else if(groupContainer.isGroupMember(host)){
				doGroupMovement(host, currentTargets.get(host));
			}
			else {
				throw new UnsupportedOperationException("SimLocationActuator " + host + " is neither in a group nor a single node");
			}
	
			// TODO maybe remodel the group movement to do a whole group?
			
		}
		
	
		// TODO Move each group
		
		
		Event.scheduleWithDelay(timeBetweenMoveOperation, this, null, EVENT_MOVE);
	}
		
	/**
	 * Applies movement of the host according to different circumstances:
	 * 
	 * Towards Destination: Host is not a group member || Group has met at meeting point successfully.
	 * Towards MeetingPoint: Host if group member and has not reached meeting point yet.
	 * No movement: Host reached meeting point, but still waits for group members to arrive.
	 * 
	 * @param SimLocationActuator The host to move.
	 * @param PositionVector Destination of the host.
	 */
	protected void doGroupMovement(SimLocationActuator host, PositionVector destination) {
		// This host is a member of a group, thus...
		SocialMovementGroup group = groupContainer.getGroupOfHost(host);	
		
		// if the group is currently gathering at a meeting point...
		if(movesToGroupMeetingPoint(host)) {
			// if this host has already arrived at the group meeting point..
			if(hostIsAtMeetingPoint(host)) {
				// ... and if the whole group is also at the meeting point...					
				if(groupGatheredAtMeetingPoint(group)) {				
					// remove the meeting point and thus, start moving towards the actual destination								
					group.setMeetingPoint(null);
				}
				// ... but the group is not gathered at the meeting point, do nothing and wait for the rest.
				else {								
					// inform analyzer of waiting group members
					if(Monitor.hasAnalyzer(ISocialGroupMovementAnalyzer.class)) {						
						Monitor.getOrNull(ISocialGroupMovementAnalyzer.class).onMemberWaitAtMeetingPoint(group, host);											
					}					
				}				
			}
			// if the host has not reached the group meeting point, move towards it.
			else {				
				PositionVector mp = groupContainer.getGroupOfHost(host).getMeetingPoint();
				doLocalMovement(host, mp);
				
				// inform analyzer group member moves to meeting point
				if(Monitor.hasAnalyzer(ISocialGroupMovementAnalyzer.class)) {						
					Monitor.getOrNull(ISocialGroupMovementAnalyzer.class).onMoveToMeetingPoint(group, host);								
				}
			}
		}
		// ... but if the group is currently on their way to the destination
		else {
			// the leader does a local movement
			if(groupContainer.isLeader(host)) {
				if(!groupContainer.isWaiting(groupContainer.getGroupOfHost(host))) {		
					// inform analyzer of movement to attraction point
					if(Monitor.hasAnalyzer(ISocialGroupMovementAnalyzer.class)) {						
						Monitor.getOrNull(ISocialGroupMovementAnalyzer.class).onGroupMovesToAttractionPoint(group);			
					}	
					
					doLocalMovement(host, destination);		
					
					PositionVector leaderPos = host.getRealPosition();					
					LinkedHashSet<SimLocationActuator> groupMembers = groupContainer.getGroupMembers(host);
					groupMembers.remove(host); // remove leader
									
					for (SimLocationActuator groupMember : groupMembers) {
						// Assign small offset to the host depending on the leaders position.						
						PositionVector offset = new PositionVector(rand.nextDouble() * LEADER_GROUP_DISTANCE, rand.nextDouble() * LEADER_GROUP_DISTANCE);
						PositionVector newPos = leaderPos.plus(offset);
					
						// Update location of host, which will be around the leaders location.
						groupMember.updateCurrentLocation(newPos);
					}
				}
				else {
					// inform analyzer of waiting group
					if(Monitor.hasAnalyzer(ISocialGroupMovementAnalyzer.class)) {						
						Monitor.getOrNull(ISocialGroupMovementAnalyzer.class).onGroupWait(group);
					}		
				}
			}
		}
		
	}
	
	/* 
	 * =====================================================================================================
	 * === MEETING POINT FUNCTIONS
	 * =====================================================================================================
	 */
	/**
	 * Checks if a host is currently on its way to a meeting point or to its destination.
	 * Returns true if moving towards meeting point, false else.
	 * 
	 * @param SimLocationActuator The host to be checked.
	 * @return Boolean
	 */
	private boolean movesToGroupMeetingPoint(SimLocationActuator host) {
		PositionVector meetingpoint = groupContainer.getGroupOfHost(host).getMeetingPoint();
		
		if(meetingpoint == null) 
			return false;		
		else if(meetingpoint != groupContainer.getGroupOfHost(host).getDestination())
			return true;
		else		
			return false;
	}

	/**
	 * Returns true, if all group members of a host including the host itself reached the meeting point, false else.
	 * 
	 * @param SimLocationActuator The host to be checked.
	 * @return Boolean
	 */
	private boolean groupGatheredAtMeetingPoint(SocialMovementGroup group) {
	
		for(SimLocationActuator participant : group.getMembers()) {
			if(!hostIsAtMeetingPoint(participant)) {
				return false;
			}
		}
		
		return true;
	}

	/**
	 * Returns true if the host reached its current meeting point, false else.
	 * 
	 * @param SimLocationActuator The host to be checked.
	 * @return Boolean
	 * 
	 */
	private boolean hostIsAtMeetingPoint(SimLocationActuator host) {
		PositionVector meetingpoint = groupContainer.getGroupOfHost(host).getMeetingPoint();
	
		if(host.getLastLocation().distanceTo(meetingpoint) <= MEETING_POINT_DISTANCE) 			
			return true;
		else
			return false;
	}

	/* 
	 * =====================================================================================================
	 * === MOVEMENT FUNCTIONS
	 * =====================================================================================================
	 */
	

	/**
	 * Host position adapts to leaders position by taking the leader position and adding a small offset
	 * 
	 * @param SimLocationActuator The host to be moved.
	 */
	private void followLeader(SimLocationActuator host) {
		SimLocationActuator leader = groupContainer.getGroupOfHost(host).getLeader();
						
		// Assign small offset to the host depending on the leaders position.
		PositionVector leaderPos = leader.getRealPosition();
		PositionVector offset = new PositionVector(rand.nextDouble() * LEADER_GROUP_DISTANCE, rand.nextDouble() * LEADER_GROUP_DISTANCE);
		PositionVector newPos = leaderPos.plus(offset);

		// Update location of host, which will be around the leaders location.
		host.updateCurrentLocation(newPos);
	}


	/* 
	 * =====================================================================================================
	 * === GETTER AND SETTER FUNCTIONS
	 * =====================================================================================================
	 */
	
	public void setGroupFormingBehavior(IGroupFormingBehavior defaultGroupForming) {
		if (defaultGroupForming == null) {
			throw new ConfigurationException(
					"GroupFormingStrategy is missing in ModularMovementModel!");
		}
		this.groupFormingBehavior = defaultGroupForming;
	}

	public void setGroupEncounterBehavior(IGroupEncounterBehavior defaultGroupEncounterBehavior) {
		if (defaultGroupEncounterBehavior == null) {
			throw new ConfigurationException(
					"GroupEncounterStrategy is missing in ModularMovementModel!");
		}
		this.groupEncounterBehavior = defaultGroupEncounterBehavior;
	}

	public IAttractionAssigmentStrategy getAttractionAssignmentStrategy() {
		return attractionAssigment;
	}
	
	public IGroupFormingBehavior getGroupFormingBehavior() {
		return groupFormingBehavior;
	}

	public LocalMovementStrategy getMovementStrategy() {
		return localMovementStrategy;
	}
	
	public void setNumberOfSingleHosts(int numberOfSingleHosts) {
		this.numberOfSingleHosts = numberOfSingleHosts;
	}
	
	public LinkedHashSet<SimLocationActuator> getSingleHosts(){
		return singleHosts;
	}	

	public LinkedHashMap<SimLocationActuator, PositionVector> getCurrentTargets(){
		return currentTargets;
	}
}
