/*
 * 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.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;

import com.fasterxml.jackson.databind.introspect.TypeResolutionContext.Basic;

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.scenario.DefaultConfigurator;
import de.tud.kom.p2psim.impl.topology.movement.modularosm.attraction.BasicAttractionPoint;
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.topology.views.VisualizationTopologyView.VisualizationInjector;
import de.tud.kom.p2psim.impl.util.Either;
import de.tudarmstadt.maki.simonstrator.api.Binder;
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.Monitor.Level;
import de.tudarmstadt.maki.simonstrator.api.component.sensor.location.IAttractionPoint;
import de.tudarmstadt.maki.simonstrator.api.component.sensor.location.Location;
import de.tud.kom.p2psim.impl.topology.movement.modularosm.attraction.*;

public class ProgrammableSmallScenarioMovement extends ModularMovementModel {

	private File actionsFile;
	private final double LEADER_GROUP_DISTANCE = 10;
	
	protected MovementGroupContainer groupContainer;
	
		
	private LinkedHashMap<String, SocialMovementGroup> groups = new LinkedHashMap<>();
	
	
	@Override
	public void initialize() {
		
		if (!initialized) {
			
			groupContainer = MovementGroupContainer.getInstance();
									
			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);

			
			parseActionsFile();
			
			// This adds the mobile hosts to the transition strategy
			for (SimLocationActuator ms : moveableHosts) {
				
				
				attractionAssigment.addComponent(ms);
			
				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() {	
		
		for (SocialMovementGroup group : groups.values()) {
			doLeaderMovement(group.getLeader(), currentTargets.get(group.getLeader()));
			// set location around leader
			for (SimLocationActuator host : group.getMembers()) {
				followLeader(group.getLeader(), host);
			}
		}
		
		super.move();
	}
			
	private void doLeaderMovement(SimLocationActuator ms, PositionVector destination) {
		Either<PositionVector, Boolean> either = localMovementStrategy.nextPosition(ms, destination);
		
		if (either.hasLeft()) {
			ms.updateCurrentLocation(either.getLeft());
			if(!checkBoundaries(ms.getRealPosition())) {
				System.err.println("Modular Movement Model: Host moved outside of simulated area!");
			}				
		} 
		else {
			PositionVector roam = this.addOffset(destination, 5);			
			ms.setTargetAttractionPoint(new BasicAttractionPoint("Roam", roam));
		}
	}
	
	@Override
	public void updatedAttractionAssignment(SimLocationActuator component,
			IAttractionPoint newAssignment) {
		
		if(moveableHosts.contains(component)) {
			super.updatedAttractionAssignment(component, newAssignment);
		}
		else {
			currentTargets.put(component, (PositionVector) newAssignment);
		}
	}

	/**
	 * 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 leader, SimLocationActuator host) {		
		
		// do nothing if leader is the same as host
		if(leader == host) 
			return;
		
		// 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 LocalMovementStrategy getMovementStrategy() {
		return localMovementStrategy;
	}
		

	public LinkedHashMap<SimLocationActuator, PositionVector> getCurrentTargets(){
		return currentTargets;
	}
	
	/* 
	 * =====================================================================================================
	 * === STUFF
	 * =====================================================================================================
	 */
	public void setActionFile(String filename) {
		actionsFile = new File(filename);		
	}
	
	private void parseActionsFile() {
		// read file with actions, parse them and store as list.
		try {
			BufferedReader in = new BufferedReader(new FileReader(actionsFile));
			String line;
			
			int lineCounter = 0;
			int groupNumber = -1;
			
			while ((line = in.readLine()) != null) {
				if (line.length() == 0 || line.startsWith("#"))
					continue;
				else {
					String[] tokens = line.split(" ");
										
					if(lineCounter == 0) {					
						groupNumber = Integer.parseInt(tokens[0]);
						System.out.println(groupNumber + " groups");
					}
					else if(lineCounter >= 1 && lineCounter <= groupNumber) {
						String name = tokens[0];
						SocialMovementGroup group = new SocialMovementGroup();
						
						
						int n = Integer.parseInt(tokens[1]);
						
						LinkedList<SimLocationActuator> hosts = new LinkedList<>(moveableHosts.stream().limit(n).toList());
						moveableHosts.removeAll(hosts);
						// set group members
						group.setMembers(new LinkedHashSet<>(hosts));
						
						// pick leader
						SimLocationActuator leader = group.getLeader();
						// set leader position
						int x = Integer.parseInt(tokens[2]);
						int y = Integer.parseInt(tokens[3]);
						leader.updateCurrentLocation(new PositionVector(x, y));
						// set location around leader
						for (SimLocationActuator host : group.getMembers()) {
							followLeader(leader, host);
						}
						
						leader.setTargetAttractionPoint(new BasicAttractionPoint("Group"+name+"Start", new PositionVector(x, y)));
						//attractionAssigment.updateTargetAttractionPoint(leader, new BasicAttractionPoint("Group"+name+"Start", new PositionVector(x, y)));
							
						
						System.out.println("Group " + name + " at " + x + " / " + y);
						
						groups.put(name, group);
					}
					else {
																							
						if(tokens[0].equals("split")) {
							long time = DefaultConfigurator.parseNumber(tokens[1], Long.class);
							String name = tokens[2];
							String name2 = tokens[3];
							double val = DefaultConfigurator.parseNumber(tokens[4], Double.class);
							
							Event.scheduleWithDelay(time, new EventHandler() {							
								@Override
								public void eventOccurred(Object content, int type) {
									SocialMovementGroup group = groups.get(name);									
									// split group									
									SocialMovementGroup group2 = new SocialMovementGroup();
									
									LinkedHashSet<SimLocationActuator> allHosts =  group.getMembers();
									int n = (int) (allHosts.size() * val);
									int target = allHosts.size() - n;
									
									System.out.println("splitting group " + name + "("+allHosts.size()+") into " + n + " and " + (allHosts.size()-n));
								
									LinkedList<SimLocationActuator> hosts2 = new LinkedList<>();
									
									while(hosts2.size() < target) {
										SimLocationActuator member = group.getRandomMember();
										if(group.getLeader() == member)
											continue;
										
										hosts2.add(member);
										group.removeHostFromGroup(member);
									}
									
									group2.setMembers(new LinkedHashSet<>(hosts2));
									// pick leader
									SimLocationActuator leader = group2.getLeader();									
									// set location around leader
									for (SimLocationActuator host : group2.getMembers()) {
										followLeader(leader, host);
									}
									
									Location lp = leader.getRealPosition();
									
									// new group stays where it is
									leader.setTargetAttractionPoint(new BasicAttractionPoint("Group"+name2+ "stay", new PositionVector(leader.getRealPosition())));
								
									groups.put(name2, group2);
								}
							}, name, 0);
							
						}
						else {
							String name = tokens[0];
							
							long time = DefaultConfigurator.parseNumber(tokens[1], Long.class);
							
							int x = Integer.parseInt(tokens[2]);
							int y = Integer.parseInt(tokens[3]);
							BasicAttractionPoint target = new BasicAttractionPoint("Group"+name+"-"+Time.getFormattedTime(time), new PositionVector(x, y));
													
							System.out.println("Group " + name + " to " + x + " / " + y + " at " + Time.getFormattedTime(time));
							
							Event.scheduleWithDelay(time, new EventHandler() {							
								@Override
								public void eventOccurred(Object content, int type) {
									groups.get(name).getLeader().setTargetAttractionPoint(target);		
									System.out.println(name + " moving towards " + target);
								}
							}, target, 0);
						}
						
					
					}
					
					
					lineCounter++;
				}
			}
			Monitor.log(ProgrammableSmallScenarioMovement.class, Level.INFO, "Created "
					+ groupNumber + " groups");
		} catch (IOException e) {
			throw new ConfigurationException("Failed to parse " + actionsFile
					+ " reason: ", e);
		}
	}
}
