/**
 * Copyright (c) 2005 Tony Binci
 */
package pacemaker;

import peersim.config.Configuration;
import peersim.config.FastConfig;
import peersim.core.CommonState;
import peersim.core.Linkable;
import peersim.core.Node;
import peersim.edsim.EDProtocol;
import peersim.edsim.EDSimulator;
import peersim.transport.Transport;

/**
 * The implementation of a protocol inspired by Fireflies.
 *  
 * @author Tony Binci
 *
 */
public class FireflyProtocol extends Firefly implements EDProtocol {

	/**
	 * String name of the parameter used to configure the 
	 * starting time of the firefly protocol, e.g. when that firefly
	 * do its first flash (cycle)!<p>
	 * 
	 * Suggestion: <b>First flash must not be <i>all-together</i></b>: what
	 * you want to find if you already start in sync?.<p>
	 *  
	 * Some ways to start:<p><ol>
	 * <li>Sync emersion/Sync speed: 
	 * start somewhere during the first
	 * period because fireflies still do 1 flash each period.
	 * <li>Sunset happening: fireflies usually start flashing when 
	 * Light/Dark Ratio is favorable (after sunset), 
	 * you can use some random larger-then-period start time to simulate this.
	 * <li>Mass flashing: Will all fireflies start flashing and will they
	 * end up being in sync if just 
	 * 1, 2, 3, few, some, or many-but-not-all of them will voluntary emit their first flash?
	 * </ol><p>
	 * 
	 * Defaults to: random start time during first period.
	 */	
	public static final String PAR_START = "start";
	public int start;
	
	public static final String PAR_FANOUT = "fanout";
	private int fanout;
	
	public static final String PAR_HEAR = "hear";
	private int hear;
	
	
	//---------------------------------------
	// Tipical Peersim fields for protocols
	//---------------------------------------
	
	/** Identifier of the linkable ID */
	public static int lid;
	
	/** Identifier of the transport ID */
	public static int tid;
	
	/** Identifier of the protocol ID */
	public static int pid;

	//-------------------------------------
	// Time management variables
	//-------------------------------------
	
	/** Count the number of cycles done. */
	public int cycle = 0;
	
	/** Useful for implementing a technique of modifiable timer
	 *  for peersim EDSimulator.
	 *  @see peersim.edsim.EDSimulator 
	 */
	public int timestamp = 0;
	
	/** the time (simulator's timestamp) we start the last cycle.<p>
	 *  Note that a cycle do not imply flashing.
	 *  Ex: {@link Firefly#model DELAY model}.*/
	public long last_cycle_time = 0;
	
	/** the time (simulator's timestamp) we scheduled our next cycle.<p>
 	 *  Note that a cycle do not imply flashing. 
 	 *  Ex: {@link Firefly#model DELAY model}.*/
	public long next_cycle_time = 0;
	
	/** The LAST TIME (simulator's timestamp) we DO FLASH. */
	public long last_flash_time = 0;
	
	/** the period we choiced to use, for models that adjust the
	 * frequency of flashing, eg {@link Firefly#model ADAPTIVE model}
	 * NOW it's a double: store also micro-adjustment.*/
	public double adjustable_period = 0;
	
	/* Remember the natural period (or: base period) (or: simply the 
	 * first period we used) of the Firefly.<p>
	 * It's a useful backup for models that adjust the frequency of
	 * flashing, e.g: {@link Firefly#model ADAPTIVE model}.<p>
	 * This field is set once at startup by copying the value of 
	 * {@link Firefly#period}. 
	 */
	//public static int natural_period = 0;
	
	/** the time (simulator's timestamp) we received the last
	 * stimulus (other's pulse) from the environment. */
	public long last_stimulus_time = 0;
	
	/**
	 * the time when I first come to life, in Simulator time.
	 */
	public long birth = 0;
	
	/**
	 * How many msg I received since birth
	 */
	public int n_received = 0;

	/** Buffer for storing index of newscast nodes */
	private static final int[] buffer = new int[200];
	
	
	
	/**
	 * @return the adjustable_period
	 */
	public double getAdjustable_period()
	{
		return adjustable_period;
	}

	public FireflyProtocol(String prefix) {
		super(prefix);
		pid = CommonState.getPid();
		lid = FastConfig.getLinkable(pid);
		tid = FastConfig.getTransport(pid); //Configuration.getPid(prefix+"."+PAR_TRANSPORT);
		start = Configuration.getInt(prefix+"."+PAR_START, -1); //-1 => Defaults to: random start time during first period.
		fanout = Configuration.getInt(prefix+"."+PAR_FANOUT, -1); //-1 => Defaults to: all nodes
		hear = Configuration.getInt(prefix+"."+PAR_HEAR, -1); //-1 => Defaults to: no hear period
		adjustable_period = period;
	}
	
	/** 
	 * @see pacemaker.Firefly#initSchedule()
	 * @inheritDoc
	 */
	public void initSchedule() {
		Node me = CommonState.getNode();
		EDSimulator.add(0, new Timer(0), me, CommonState.getPid());
	}
	
	/**
	 * Setter for use with initializers.<p>
	 */
	public void setStart(int value) { start = value; }
	
	/**
	 * Setter for use with initializers.<p>
	 * XXX note the conversion from int (randomized by setter) to double
	 */
	public void setPeriod(int value) { adjustable_period = value; }
	

	/** @inheritDoc */
	public void doFlash() {
		long now = CommonState.getTime();
		if (now < (birth + start)) return; // because this node has not reached start time

		Node me = CommonState.getNode();
		Linkable link = (Linkable) me.getProtocol(lid);
		int degree = link.degree();
		if (degree <= 0) { 
			System.out.print("degree "+degree);
			return;
		}
		Transport transport = (Transport) me.getProtocol(tid);
		//---
		
		if (cycle <= hear && birth > 0)
			return;
		//System.out.println(birth + " " + cycle + " " + hear);
		EmissionObserver.addFlash(now, now-last_flash_time);
		//System.out.println("FLASH: time "+now+" node "+me.getID()+" last_flash "+last_flash_time + " -- " + (now-last_flash_time));
		last_flash_time = now;
		if (fanout < 0) {
			// Flooding
			for (int i=0; i<link.degree(); i++) {
				Node remote = link.getNeighbor(i); //CommonState.r.nextInt(link.degree()));
				transport.send(me, remote, new Pulse(me, this), CommonState.getPid());
			}
		} else {
			// Broadcast to "fanout" random neighbors
			for (int i=0; i<link.degree(); i++) {
				buffer[i] = i;
			}
			int size = degree;
			fanout = Math.min(size, fanout);
			for (int i=0; i < fanout; i++) {
				int r = CommonState.r.nextInt(size);
				Node remote = link.getNeighbor(buffer[r]); 
				transport.send(me, remote, new Pulse(me, this), CommonState.getPid());
				buffer[r] = buffer[--size];
			}
		}
	}

	
	/**
	 * Process 2 types of events: Timer (when clock ticks) and Pulse (from other fireflies).<p>
	 * 
	 * The periodic timed flash of a firefly will be implemented as a special 
	 * event delivered each period to the node itself.<p>
	 */
	public void processEvent(Node node, int p_id, Object event) {
		
		if (event instanceof Timer) { // reached the end of the period: do a light pulse
			int t = ((Timer) event).timestamp;
			if (t == 0) { // 1st event, cycle=0, time=0: simulation just started after initializers
				birth = CommonState.getTime();
				//System.out.println(CommonState.getNode().getID()+" birth at "+CommonState.getTime());
				// parameter "start" in handled here because we must wait PeerSim initialiazers execution 
				if (start == -1) start = CommonState.r.nextInt(period);
				//System.out.println("START time "+start+" node "+node.getID());				
				//System.out.println("PERIOD val "+Math.round(adjustable_period)+" node "+node.getID());				
				//last_cycle_time = start;
				next_cycle_time = start+period; //at start, no need to use adjustable_period
				timestamp = 1;
				EDSimulator.add(start, new Timer(1), node, p_id);
				return;
			}
			if (t == timestamp) { // verify that delivered event is the last Timer set
				doFlash();
				cycle++;
				timestamp++;
				last_cycle_time = CommonState.getTime();
				next_cycle_time = last_cycle_time + Math.round(adjustable_period);
				EDSimulator.add(Math.round(adjustable_period), new Timer(timestamp), node, p_id);
			} else  // else that Timer is a old timer, ignore the event! 
				;//System.out.println("OLD_TIMER time "+CommonState.getTime()+" t "+t+" expected "+timestamp+" node "+node.getID());
			return;
		}
		
		if (event instanceof Pulse) // received a pulse from a neighbor: do a reaction (model-based)
			receive(node, pid, event);
	}
	

	/** 
	 * Called by {@link #processEvent(Node, int, Object)} when the event is a
	 * light pulse from a neighbor firefly.<p>
	 * The models (ADVANCE,DELAY,MAS,ADAPTIVE...) are applied here: 
	 * choice how to react to the event.
	 */
	public void receive(Node node, int pid, Object event) {
		long now = CommonState.getTime();
		n_received++;
		if (now < (start)) return; // because this node has not reached start time
		
		if (model.equals("ADVANCE")) {
			if ( now <= (last_cycle_time+inert) // we are in: inert period
			  || now >= (next_cycle_time-prep) // we are in: prep period
				) 
				; // ignore that flash 
			else { // flash immediately and reset period.
				doFlash();
				cycle++;
				timestamp++;
				last_cycle_time = now;
				next_cycle_time = now+period;
				EDSimulator.add(period, new Timer(timestamp), node, pid);
			}
		}
	
		if (model.equals("DELAY")) {
			
			if ( now <= (last_cycle_time+inert) // we are in: inert period 
			  || now >= (next_cycle_time-prep) // we are in: prep period
				) 
				; // ignore that flash 
			else { // don't flash, reset period.
				//System.out.println("SILENT time "+now+" node "+node.getID());
				cycle++;
				timestamp++;
				last_cycle_time = now;
				next_cycle_time = now+period;
				EDSimulator.add(period, new Timer(timestamp), node, pid);
			}
		}
		
		if (model.equals("MAS")) {
			double phase = ((double)(now - last_cycle_time)) / period;
			double new_state = MaSModel.eval_PRC(phase, b) + epsilon;
			//if (node.getID() == 0) 
			//System.out.println("MAS phase "+phase+" PRC "+PRC.eval_PRC(phase, b)+" epsilon "+epsilon+" newstate "+new_state+" last "+last_cycle_time+" now "+now+" node "+node.getID());
			if (new_state < 1) { // jump insufficient to flash, but new state has to be memorized 
				double new_phase = MaSModel.eval_inverse_PRC(new_state, b);
				// The mening of "new_phase" it's like you have done a "jump in the future" 
				// so that it seems more time has passed from the start of the cycle.
				long virtual_elapsed_time = (long)(new_phase*period); 
				long remaining_time = period - virtual_elapsed_time; // remaining time before next cycle, in simulator units
				timestamp++;
				last_cycle_time = now - virtual_elapsed_time; //XXX abused! here the intent is to record the "time jump". I set the last_cycle_time back in the past...
				next_cycle_time = now + remaining_time;
				//if (node.getID() == 0) 
				//System.out.println("MAS newphase "+new_phase+" remain "+remaining_time+" virtua "+virtual_elapsed_time+" node "+node.getID());				
				EDSimulator.add(remaining_time, new Timer(timestamp), node, pid); // adjust remaining period before flash
			} else { // state >=1: reached the end point (1,1): do flash
				doFlash();
				cycle++;
				timestamp++;
				last_cycle_time = now;
				next_cycle_time = now+period;
				EDSimulator.add(period, new Timer(timestamp), node, pid);					 
			}
		}
		
		if (model.equals("ADAPTIVE")) {
			//if (node.getID()==1001) System.out.println(node.getID()+" adjusting at"+now);
			//long forcing_period = now - last_stimulus_time; // it's the period between the last and the actual stimulus.
			
			double phase = ((now - last_cycle_time)) / adjustable_period;
			
			// transform all periods because the Ermentrout's equation works on frequencies
			double freq = 1.0d/adjustable_period; // Wn (intrinsic frequency)
			double max_freq = 1.0d/min; //Wmax
			double min_freq = 1.0d/max; //Wmin
			double nat_freq = 1.0d/period; // Wnat (natural frequency)
			
			// the big equation:
			double next_freq = freq + relaxation*(nat_freq - freq) +
				(AdaptiveModel.gplus (phase + (adjustable_period*freq))*(min_freq - freq)) +
				(AdaptiveModel.gminus(phase + (adjustable_period*freq))*(max_freq - freq));
			/*System.out.println("my_period*freq="+adjustable_period*freq+
					" g+="+AdaptiveModel.gplus (phase + (adjustable_period*freq))+
					" g-="+AdaptiveModel.gminus(phase + (adjustable_period*freq))+
					" diff+="+(AdaptiveModel.gplus (phase + (adjustable_period*freq))*(min_freq - freq)) +
					" diff-="+(AdaptiveModel.gminus(phase + (adjustable_period*freq))*(max_freq - freq)) +
					" freq="+freq+" next="+next_freq
					);*/
			// this is the next adjustable period
			double next_period = 1.0d/next_freq;  
				
			// ensure that the bounds are respected:
			//if (next_period < min) next_period = min;
			//if (next_period > max) next_period = max;
			double adjustment = next_period - adjustable_period; //if positive: lengthen the period; if negative: shorten the period
			long remaining_time = Math.round(adjustable_period) - (now - last_cycle_time); //remaining time before last timer set, and it's relative to the last period we used.
				
			//System.out.println("ADAPTIVE now="+now+" node="+node.getID()+" phase="+phase+" last="+adjustable_period+" next(.)="+Math.round(next_period)+"("+next_period+") adjustment(.)="+Math.round(adjustment)+"("+adjustment+") new_phase="+new_phase+" eff="+(new_phase-phase)*next_period);
			
			// last op is the update. 
			adjustable_period = next_period;
			last_stimulus_time = now;
			//System.out.println("ADJUSTABLE time "+now+" val "+adjustable_period+" node "+node.getID());

			// update the timer mechanism
			if (Math.round(adjustment) != 0) {
				if (remaining_time + Math.round(adjustment) <= 0) {// if < 0, due to adjustment you reach the cycle-end: YOU MUST FLASH, NOW!
					//EDSimulator.add(0, new Timer(timestamp), node, pid); // force an immediate timer tick
					//System.out.println("PATH time"+now);
					doFlash();
					cycle++;
					timestamp++;
					last_cycle_time = now;
					next_cycle_time = now+Math.round(adjustable_period);
					EDSimulator.add(Math.round(adjustable_period), new Timer(timestamp), node, pid);
				} else {
					timestamp++;
					EDSimulator.add(remaining_time + Math.round(adjustment), new Timer(timestamp), node, pid);
				}
			}
			
		}
		
	}

}
