/**
* Name: Balls, Groups and Clouds Multilevel Architecture
* Author: 
* Description: This model shows how to use multi-level architecture to group agents, and regroup groups. The operators capture 
* 	is used to capture an agent by a group and change its species as a species contained by the group and defined in the group species section. 
* 	The operator release is used to release contained agents and change them into an other species. The experiment shows ball moving 
* 	randomly, and following other balls. When they are close to each other, they generate a group of balls with its own behavior. A group of group 
* 	agents generate a cloud in the same way. When the number of balls contained inside the group is too high, the group disappears and releases 
* 	all its balls repulsively. 
* Tags: multi_level, agent_movement
*/
model balls_groups_clouds

global {
// Parameters
	bool create_group <- true;
	bool create_cloud <- true;

	// Environment
	point environment_bounds <- {500, 500};
	geometry shape <- rectangle(environment_bounds);

	//Define a inner environment smaller inside the environment
	int inner_bounds_x <- (int((environment_bounds.x) / 20));
	int inner_bounds_y <- (int((environment_bounds.y) / 20));
	int xmin <- inner_bounds_x;
	int ymin <- inner_bounds_y;
	int xmax <- int((environment_bounds.x) - inner_bounds_x);
	int ymax <- int((environment_bounds.y) - inner_bounds_y);
	float MAX_DISTANCE <- environment_bounds.x + environment_bounds.y;

	//Global variables for ball agents
	rgb ball_color <- #green;
	rgb chaos_ball_color <- #red;
	float ball_size <- float(3);
	float ball_speed <- float(1);
	float chaos_ball_speed <- 8 * ball_speed;
	int ball_number <- 400 min: 2 max: 1000;
	geometry ball_shape <- circle(ball_size);
	float ball_separation <- 6 * ball_size;

	//Global variables for group agents
	int group_creation_distance <- int(ball_separation + 1);
	int min_group_member <- 3;
	int group_base_speed <- (int(ball_speed * 1.5));
	int base_perception_range <- int(environment_bounds.x / 100) min: 1;
	int creation_frequency <- 3;
	int update_frequency <- 3;
	int merge_frequency <- 3;
	float merge_possibility <- 0.3;

	//Global variables for Clouds Agents
	int cloud_creation_distance <- 30 const: true;
	int min_cloud_member <- 3 const: true;
	int cloud_speed <- 3 const: true;
	int cloud_perception_range <- base_perception_range const: true;

	init {
		create ball number: ball_number;
		create group_agents_viewer;
	}

	//The simulation will try to create group at each frequence cycle
	reflex create_groups when: (create_group and ((cycle mod creation_frequency) = 0)) {
	//create a list from all balls following the nearest ball
		list free_balls <- ball where ((each.state) = 'follow_nearest_ball');
		if (length(free_balls) > 1) {
		//Clustering of the balls according to their distance with at least a minimal number of balls in a group
			list> satisfying_ball_groups <- (free_balls simple_clustering_by_distance group_creation_distance) where ((length(each)) > min_group_member);
			loop one_group over: satisfying_ball_groups {
				create group {
					capture one_group as: ball_in_group;
				}
				//Capture by the new groups created of the different balls present in the list one_group
			}

		}

	}

	//The simulation will try to create clouds at each frequence cycle
	reflex create_clouds when: (create_cloud and ((cycle mod creation_frequency) = 0)) {
	//A cloud can be created only using group with a number of balls inside greater than 5% of the total ball number
		list candidate_groups <- group where (length(each.members) > (0.05 * ball_number));

		//A cloud can be created also only using group which aren't too far away 
		list> satisfying_groups <- (candidate_groups simple_clustering_by_distance cloud_creation_distance) where (length(each) >= min_cloud_member);

		//Creation of the different clouds using the groups satisfying both conditions
		loop one_group over: satisfying_groups {
			create cloud  {
				capture one_group as: group_delegation {
					migrate ball_in_group target: ball_in_cloud;
				}
				color <- one_of(group_delegation).color.darker;
			}
		}
	}
}

//Species with a specified type of control architecture, here the final state machine FSM
species ball control: fsm {
	float speed <- ball_speed;
	rgb color <- ball_color;
	int beginning_chaos_time;
	int time_in_chaos_state;

	//create the ball in a certain way to not make balls intersect each other
	init {
		bool continue_loop <- true;
		loop while: continue_loop {
			point tmp_location <- {(rnd(xmax - xmin)) + xmin, (rnd(ymax - ymin)) + ymin};
			geometry potential_geom <- ball_shape at_location tmp_location;
			if (empty(ball where (each intersects potential_geom))) {
				location <- tmp_location;
				continue_loop <- false;
			}

		}

	}

	//Action used to separate the balls and make them repulsive for the other balls of the group
	action separation (list nearby_balls) {
		float repulsive_dx <- 0.0;
		float repulsive_dy <- 0.0;
		loop nb over: nearby_balls {
			float repulsive_distance <- ball_separation - (location distance_to nb.location);
			float repulsive_direction <- (nb.location) towards (location);
			repulsive_dx <- repulsive_dx + (repulsive_distance * (cos(repulsive_direction)));
			repulsive_dy <- repulsive_dy + (repulsive_distance * (sin(repulsive_direction)));
		}

		location <- location + {repulsive_dx, repulsive_dy};
	}

	bool in_bounds (point a_point) {
		return (!(a_point.x < xmin) and !(a_point.x > xmax) and !(a_point.y < ymin) and !(a_point.y > ymax));
	}

	//State that will make the agent follows the closest ball if it is not in the chaos state anymore
	state follow_nearest_ball initial: true {
		enter {
			color <- ball_color;
			speed <- ball_speed;
		}

		ball nearest_free_ball <- (ball where ((each.state) = 'follow_nearest_ball')) closest_to self;
		if nearest_free_ball != self {
			float heading <- self towards (nearest_free_ball);
			float step_distance <- speed * step;
			float step_x <- step_distance * (cos(heading));
			float step_y <- step_distance * (sin(heading));
			point tmp_location <- location + {step_x, step_y};
			if (self.in_bounds(tmp_location)) {
				location <- tmp_location;
				do separation(((ball overlapping (shape + ball_separation)) - self));
			}

		}

	}

	//Make the ball move randomly during a certain time
	state chaos {
		enter {
			beginning_chaos_time <- int(time);
			time_in_chaos_state <- 10 + (rnd(10));
			color <- chaos_ball_color;
			speed <- chaos_ball_speed;
			float heading <- rnd(360.0);
		}

		float step_distance <- speed * step;
		float step_x <- step_distance * (cos(heading));
		float step_y <- step_distance * (sin(heading));
		point tmp_location <- location + {step_x, step_y};
		if (self.in_bounds(tmp_location)) {
			location <- tmp_location;
			do separation(nearby_balls: (ball overlapping (shape + ball_separation)) - self);
		}

		transition to: follow_nearest_ball when: time > (beginning_chaos_time + time_in_chaos_state);
	}

	aspect default {
		draw ball_shape color: color size: ball_size at: self.location;
	} }

	//Species representing the group of balls
species group {
	rgb color <- rgb([rnd(255), rnd(255), rnd(255)]);
	geometry shape <- any_point_in(host) update: convex_hull(polygon(ball_in_group collect each.location));
	float speed update: float(group_base_speed);

	//Parameter to capture the balls contains in the perception range
	float perception_range update: float(base_perception_range + (rnd(5)));
	agent target update: get_nearer_target();

	//Function to return the closest ball or small group of balls that the agent could capture
	agent get_nearer_target {
		int size <- length(members);
		ball nearest_free_ball <- (ball where ((each.state = 'follow_nearest_ball'))) closest_to self;
		group nearest_smaller_group <- ((group - self) where ((length(each.members)) < size)) closest_to self;
		if (nearest_free_ball = nil) and (nearest_smaller_group = nil) {
			return nil;
		}

		float distance_to_ball <- (nearest_free_ball != nil) ? (self distance_to nearest_free_ball) : MAX_DISTANCE;
		float distance_to_group <- (nearest_smaller_group != nil) ? (self distance_to nearest_smaller_group) : MAX_DISTANCE;
		return (distance_to_ball < distance_to_group) ?  nearest_free_ball :  nearest_smaller_group;
	}

	//Action to use when the group of balls explode
	action separate_components {
		loop com over: (list(ball_in_group)) {
			list nearby_balls <- ((ball_in_group overlapping (com.shape + ball_separation)) - com) where (each in members);
			float repulsive_dx <- 0.0;
			float repulsive_dy <- 0.0;
			loop nb over: nearby_balls {
				float repulsive_distance <- ball_separation - ((ball_in_group(com)).location distance_to nb.location);
				float repulsive_direction <- (nb.location) direction_to ((ball_in_group(com)).location);
				repulsive_dx <- repulsive_dx + (repulsive_distance * (cos(repulsive_direction)));
				repulsive_dy <- repulsive_dy + (repulsive_distance * (sin(repulsive_direction)));
			}

			(ball_in_group(com)).location <- (ball_in_group(com)).location + {repulsive_dx, repulsive_dy};
		}

	}

	//Species that will represent the balls captured by the group agent
	species ball_in_group parent: ball topology: topology((world).shape) {
		float my_age <- 1.0 update: my_age + 0.01;
		state follow_nearest_ball initial: true {
		}

		state chaos {
		}

		aspect default {
			draw circle(my_age) color: ((host as group).color).darker;
		}

	}

	//Reflex to capture all the balls close to the group agent
	reflex capture_nearby_free_balls when: (cycle mod update_frequency) = 0 {
		list nearby_free_balls <- (ball overlapping (shape + perception_range)) where (each.state = 'follow_nearest_ball');
		if !(empty(nearby_free_balls)) {
			capture nearby_free_balls as: ball_in_group;
		}

	}

	//Action to do when the group is disaggregated
	action disaggregate {
		release list(members) as: ball in: world {
			state <- 'chaos';
		}

		do die;
	}

	//Reflex to merge the group close to the agent when the cycle is in the frequency of merging
	reflex merge_nearby_groups when: (cycle mod merge_frequency) = 0 and (target != nil) and ((species_of(target)) = group) {
		list nearby_groups <- (group overlapping (shape + perception_range)) - self;
		if target in nearby_groups {
			if (rnd(10)) < (merge_possibility * 10) {
				list released_balls <- [];
				ask target {
					release list(ball_in_group) as: ball in: world {
						released_balls << self;
					}
					do die;
				}
				capture released_balls as: ball_in_group;
			} else {
				ask target as group {
					do disaggregate;
				}

			}

		}

	}

	//Reflex to chase a target agent 
	reflex chase_target when: (target != nil) {
		float direction_to_nearest_ball <- (self towards (target));
		float step_distance <- speed * step;
		float dx <- step_distance * (cos(direction_to_nearest_ball));
		float dy <- step_distance * (sin(direction_to_nearest_ball));
		geometry envelope <- shape.envelope;
		point topleft_point <- (envelope.points) at 0;
		point bottomright_point <- (envelope.points) at 0;
		loop p over: envelope.points {
			if ((p.x <= topleft_point.x) and (p.y <= topleft_point.y)) {
				topleft_point <- p;
			}

			if ((p.x >= bottomright_point.x) and (p.y >= bottomright_point.y)) {
				bottomright_point <- p;
			}

		}

		if ((dx + topleft_point.x) < 0) {
			float tmp_dx <- dx + topleft_point.x;
			dx <- dx - tmp_dx;
		} else {
			if (dx + bottomright_point.x) > (environment_bounds.x) {
				float tmp_dx <- (dx + bottomright_point.x) - environment_bounds.x;
				dx <- dx - tmp_dx;
			}

		}

		if (dy + topleft_point.y) < 0 {
			float tmp_dy <- dy + topleft_point.y;
			dy <- dy - tmp_dy;
		} else {
			if (dy + topleft_point.y) > (environment_bounds.y) {
				float tmp_dy <- (dy + bottomright_point.y) - (environment_bounds.y);
				dy <- dy - tmp_dy;
			}

		}

		loop com over: ball_in_group {
			com.location <- com.location + {dx, dy};
		}

		//shape <- convex_hull((polygon(ball_in_group collect each.location)) + 2.0);
	}
	//Reflex to disaggregate the group if it is too important ie the number of balls is greater than 80% of the total ball number
	reflex self_disaggregate {
		if ((length(members)) > (0.8 * (ball_number))) {
			do disaggregate;
		}

	}

	aspect default {
		draw shape color: color;
	}

}

//Species cloud that will be created by an agglomeration of groups.
species cloud {
	geometry shape <- any_point_in(host) update: convex_hull(polygon(group_delegation collect ((each.shape).location)));
	rgb color;

	//Species contained in the cloud to represent the groups captured by the cloud agent
	species group_delegation parent: group topology: (topology(world.shape)) {
		geometry shape update: convex_hull((polygon((ball_in_cloud) collect (each.location)))) buffer 10;

		init {
			shape <- convex_hull((polygon((ball_in_cloud) collect (each.location)))) buffer 10;
		}

		reflex capture_nearby_free_balls when: false {
		}

		reflex merge_nearby_groups when: false {
		}

		reflex chase_target when: false {
		}

		reflex self_disaggregate {
		}

		action move2 (float with_heading, float with_speed) {
			ask ball_in_cloud {
				do move2(with_heading, with_speed);
			}

		}

		species ball_in_cloud parent: ball_in_group topology: (world.shape) as topology control: fsm {

			action move2 (float with_heading, float with_speed) {
				float dx <- cos(with_heading) * with_speed;
				float dy <- sin(with_heading) * with_speed;
				location <- {((location.x) + dx), ((location.y) + dy)};
			}

			aspect default {
			}

		}

	}

	group target_group;

	//The cloud try to look for small groups to capture them
	reflex chase_group {
		if ((target_group = nil) or (dead(target_group))) {
			target_group <- one_of(group);
		}

		if (target_group != nil) {
			float direction_target <- self towards (target_group);
			ask group_delegation {
				do move2 with: [with_heading::float(direction_target), with_speed::float(cloud_speed)];
			}

		}

	}

	//Operator to know if a cloud can capture a group overlapping the cloud agent. 
	bool can_capture (group a_group) {
		if (a_group = nil or dead(a_group)) {
			return false;
		}

		if (shape overlaps a_group.shape) {
			return true;
		}

		loop gd over: group_delegation {
			if ((a_group.shape) overlaps gd.shape) {
				return true;
			}

		}

		return false;
	}

	//Reflex to capture group
	reflex capture_group {
		if (can_capture(target_group)) {
			capture target_group as: group_delegation {
				migrate ball_in_group target: ball_in_cloud;
			}

		}

	}

	//Reflex to disaggregate the clouds when they are no more group to capture
	reflex disaggregate when: (empty(list(group))) {
		ask group_delegation {
			migrate ball_in_cloud target: ball_in_group;
		}

		release list(group_delegation) as: group in: world {
			do disaggregate;
		}

		do die;
	}

	aspect default {
		draw shape color: color wireframe: true;
		draw (name + ' with ' + (string(length(members))) + ' groups.') size: 15 color: color at: {location.x - 65, location.y};
	}

}

species group_agents_viewer {

	aspect default {
		draw ('Number of groups: ' + (string(length(group)))) at: {(environment_bounds.x) / 2 - 210, (environment_bounds.y) / 2} color: #blue size: 40;
	}

}


experiment group_experiment type: gui {
	parameter 'Create groups?' var: create_group <- true;
	parameter 'Create clouds?' var: create_cloud <- false;
	output {
		layout horizontal([vertical([1::5000, 0::5000])::5000, 2::5000]) tabs: true editors: false;
		display 'Standard display' {
			species ball aspect: default transparency: 0.5;
			species group aspect: default transparency: 0.5 {
				species ball_in_group;
			}

		}

		display 'Ball display' {
			species ball;
		}

		display 'Group display' {
			species group;
			species group_agents_viewer;
		}

	}

}

experiment cloud_experiment type: gui {
	parameter 'Create groups?' var: create_group <- true;
	parameter 'Create clouds?' var: create_cloud <- true;
	output {
		layout vertical([horizontal([0::5000, 1::5000])::5000, horizontal([2::5000, 3::5000])::5000]) tabs: true toolbars: true editors: false;
		display 'Standard display'  background: #black{
			species ball aspect: default transparency: 0.5;
			species group aspect: default transparency: 0.5 {
				species ball_in_group;
			}

			species cloud aspect: default {
				species group_delegation transparency: 0.9 {
					species ball_in_cloud;
					species ball_in_group;
				}

			}

		}

		display 'Ball display'  {
			species ball;
		}

		display 'Group display'  {
			species group;
			species group_agents_viewer;
		}

		display 'Cloud display'  {
			species cloud;
		}

	}

}