/*
 * Created on 7-mag-2004
 */
package jaCop.protocol;

import jaCop.traffic.*;
import jaCop.nodeStatus.*;
import jaCop.domain.*;
import jaCop.exceptions.*;
import jaCop.event.*;

import convFramework.domain.*;
import convFramework.traffic.event.*;

import java.util.*;

/**
 * @author Lompa
 */
public class ProtocolTrigger implements INewConversationListener, IMissingAckListener{
	
	/* flag di istanziazione */
	//private static boolean instantiated = false;
	
	/* campi */
	private COPPacketManager packetMan = null;
	private COPEventListenerManager listenerMan = null;
	private ControlSystem controlSystem = null;
	private StatusManager statusMan = null;
	private TaskConversersList taskConversersList = null;
	
	private ConversationGraveyard convGraveyard = null;
	
	private long conversationTimeout = 20000;
	private boolean optimizedReJoin = false;
	
	/**
	 * @param packetMan
	 * @param statusStore
	 * @param connectionRank
	 * @param monitor
	 */
	public ProtocolTrigger(ConfigurationSet conf, COPPacketManager packetMan, StatusManager statusMan,
			COPEventListenerManager listenerMan) throws InstantiationRuleBrokenException {
		/* controllo */
		//if (instantiated) throw new InstantiationRuleBrokenException("cannot create more than one ProtocolTrigger");
		//else instantiated = true;
		/* assegnazioni */
		/* strumenti */
		this.packetMan = packetMan;
		this.statusMan = statusMan;
		this.listenerMan = listenerMan;
		this.controlSystem = new ControlSystem(this);
		/* opzioni */
		this.conversationTimeout = conf.getConversationTimeout();
		this.optimizedReJoin = conf.isOptimizedReJoin();
		/* istanziazione nuovi oggetti */
		this.convGraveyard = new FakeConversationGraveyard(200); //TODO attenzione: ho usato un fake conversatin graveyard
		this.taskConversersList = new TaskConversersList();
	}
	
	/* metodi per l'inizio di conversazioni ***********************************************************/
	
	public synchronized void joinCommunity(String community, Node server, ILoadFactorCalculator calc, int maxDelegations){
		/* controlli */
		/* se la comunità esiste già non faccio nulla */
		if (statusMan.existsCommunity(community)) return;
		
		/* conversione del servant in un COPNode */
		COPNode juncture = null;
		try { juncture = new COPNode(server.getAddress(), server.getPort(), 0, false); }
		catch(convFramework.exceptions.BadInvocationArgumentException e){ throw new JaCOPError(e.getMessage()); }
		
		/* creazione di una nuova conversazione */
		IConversation conv = packetMan.newConversation();
		
		/* creazione set di configurazione */
		JoinRequestClient.ConfigurationSet conf = new JoinRequestClient.ConfigurationSet(community, conv.getConversationID());
		conf.setReceiver(juncture);
		conf.setWaitingTimeout(this.conversationTimeout);
		conf.setMaxDelegationsNumber(maxDelegations);
		conf.setLoadFactorCalculator(calc);
		/* creazione di un client per richieste di giunzione */
		JoinRequestClient client = new JoinRequestClient(conf, packetMan, statusMan, listenerMan, convGraveyard, controlSystem, taskConversersList, true, false);
		
		/* esecuzione del client */
		client.start();
	}
	
	public synchronized void leaveCommunity(String community){
		
		/* notifica della perdita dei vicini */
		COPNode[] neighbors = statusMan.getNeighbors(community);
		for(int i = 0; i < neighbors.length; i++){
			this.notifyNeighborLost(community, neighbors[i]);
		}
		
		/* uscita dalla comunità */
		statusMan.removeCommunity(community);
		/* notifica */
		this.notifyCommunityLeft(community);
	}
	
	public synchronized void openCommunity(String community, ILoadFactorCalculator calc, int connectionRank){
		if (!statusMan.existsCommunity(community)){
			statusMan.addCommunity(community, calc, connectionRank);
		}
		notifyCommunityJoined(community);
	}
	
	/* metodi per la gestione di eventi **************************************************************/
	
	public synchronized void newConversationStarted(NewConversationEvent event){
		/* recupero dati */
		//System.out.println("NUOVA CONVERSAZIONE");
		COPPacket packet = null;
		try { packet = packetMan.translate((ContentPacket) event.getArrivedPacket()); }
		catch(TranslationException e){ return; /* non faccio nulla */ }
		IConversationID convID = packet.getConvID();
		String community = packet.getCommunity();
		
		//System.out.println("packet type: "+packet.getType());
		/* i pacchetti relativi a comunità non esistenti vengno scartati */
		if(!statusMan.existsCommunity(community) && !packet.getType().equals(JoinRequest.getTypeDescription()))
			return;
		
		/* calcolo del flag di priorità */
		boolean priority = false;
		if (statusMan.isNeighbor(community, packet.getSender()))
			priority = statusMan.isStructure(community, packet.getSender());
		
		/* l'azione dipende dal tipo del pacchetto */
		if (convGraveyard.isDead(convID)){
			packetMan.removeConversation(convID);
		}
		/* richiesta di join */
		else if(packet.getType().equals(JoinRequest.getTypeDescription())){
			//System.out.println("NUOVA CONVERSAZIONE: JOIN REQUEST");
			/* casting */
			JoinRequest joinRequest = (JoinRequest) packet;
			/* recupero dati */
			/* creazione di un servant per richiesta di join */
			JoinRequestServer.ConfigurationSet conf = new JoinRequestServer.ConfigurationSet(community, convID);
			conf.setWaitingTimeout(this.conversationTimeout);
			JoinRequestServer server = new JoinRequestServer(conf, packetMan, statusMan, listenerMan, convGraveyard, controlSystem, taskConversersList, false, priority);
			/* esecuzione */
			server.start();
		}
		/* notifica nuovi vicini */
		else if(packet.getType().equals(NewNeighbor.getTypeDescription())) {
			//System.out.println("NUOVA CONVERSAZIONE: NEW NEIGHBORS");
			/* casting */
			NewNeighbor newNeighbors = (NewNeighbor) packet;
			/* recupero dati */
			/* creazione di un servant per notfica nuovi vicini */
			NewNeighborsServer.ConfigurationSet conf = new NewNeighborsServer.ConfigurationSet(community, convID);
			conf.setWaitingTimeout(this.conversationTimeout);
			NewNeighborsServer server = new NewNeighborsServer(conf, packetMan, statusMan, listenerMan, convGraveyard, controlSystem, taskConversersList, false, priority);
			/* esecuzione */
			server.start();
		}
		/* notifica di caduta di un nodo */
		else if(packet.getType().equals(FaultNotification.getTypeDescription())){
			//System.out.println("NUOVA CONVERSAZIONE: FAULT NOTIFICATION");
			/* casting */
			FaultNotification faultNotification = (FaultNotification) packet;
			/* se a cadere non è il punto di giunzione attivo una notifica di caduta */
			/* costruzione del servant per una notifica di caduta */
			FaultNotificationServer.ConfigurationSet conf = new FaultNotificationServer.ConfigurationSet(community, convID);
			conf.setWaitingTimeout(this.conversationTimeout);
			FaultNotificationServer server = new FaultNotificationServer(conf, packetMan, statusMan, listenerMan, convGraveyard, controlSystem, taskConversersList, false, priority);
			/* esecuzione */
			server.start();
		}
		else {
		}
		
	}
	
	public synchronized void missingAck(MissingAckEvent event){
		/* recupero dati */
		COPPacket packet = null;
		try { packet = packetMan.translate((ContentPacket) event.getTransmittedPacket()); }
		catch(TranslationException e){ return; /* non faccio nulla */ }
		
		/* se la comunità non esiste non faccio nulla */
		if (!statusMan.existsCommunity(packet.getCommunity())) return;
		
		/* se il vicino è già stato rimosso non faccio nulla innesco ugualmente un gestore */
		//if (!statusStore.isNeighbor(packet.getCommunity(), fallenNode)) return;
		
		/* l'azione dipende da quale è il nodo caduto */
		String community = packet.getCommunity();
		COPNode fallenNode = packet.getReceiver();
		
		/* rimozione del nodo */
		statusMan.removeNeighbor(community, fallenNode);
	}
	
	public synchronized void noMoreClients(String community){
		/* se la struttura non è integra simulo la caduta di un nodo per innecara un nuovo client */
		if (statusMan.existsCommunity(community))
			if (!statusMan.hasCompleteStructure(community))
				neighborLostTrigger(community, null);
	}
	
	public synchronized void neighborLostTrigger(String community, COPNode fallenNode){
		
		//System.out.println("neighbor lost trigger!");
		if (fallenNode != null) {
			//System.out.println("il nodo perduto " + (fallenNode.isStructure() ? "" : " NON") + " era di struttura");
			/* notifica di caduta */
			notifyNeighborLost(community, fallenNode);
			/* interruzione di tutti i client che hanno il nodo caduto come servant */
			int ic = taskConversersList.interruptClientFor(community, fallenNode);
			//jaCop.Debug.print("***** CLIENTS INTERROTTI: "+ic+" *****");
		}
		COPNode juncturePoint = statusMan.getJuncturePoint(community);
		/* se a cadere è il punto di giunzione effettuo una nuova richiesta di giunzione */
		if(juncturePoint == null && statusMan.getStructureNeighbors(community).length > 0){
			/* aggiungo una frattura di struttura */
			statusMan.breakStructure(community);
			/* costruzine di un client per un nuova giunzione */
			NewJoinClient.ConfigurationSet conf = new NewJoinClient.ConfigurationSet(community, packetMan.newConversation().getConversationID());
			conf.setWaitingTimeout(this.conversationTimeout);
			conf.setFallenNode(fallenNode);
			conf.setOptimizedReJoin(false);
			NewJoinClient client = new NewJoinClient(conf, packetMan, statusMan, listenerMan, convGraveyard, controlSystem, taskConversersList, true, false);
			/* esecuzione */
			client.start();
		}
		else if (juncturePoint == null  && statusMan.getStructureNeighbors(community).length == 0){
			/* se il nodo faceva parte del core significa che è diventato la nuova testa */
			if (statusMan.isCoreNode(community)){
				/* la struttura viene completamente riparata */
				statusMan.repairStructure(community, statusMan.getStructureBreaksNumber(community));
				controlSystem.signalStructureComplete();
			}
		}
		else if (juncturePoint == null && statusMan.getNeighbors(community).length == 0){
			/* NOTIFICA DI USCITA DALLA COMUNITA' */
			this.notifyCommunityLeft(community);
			//System.out.println("comunità abbandonata: " + community);
			return;
		}
		else if (juncturePoint != null && (fallenNode == null || fallenNode.isStructure())){
			/* aggiungo una frattura di struttura */
			statusMan.breakStructure(community);
			/* altrimenti istanzio un cliente per la notifica di caduta */
			ReJoinClient.ConfigurationSet conf = new ReJoinClient.ConfigurationSet(community, packetMan.newConversation().getConversationID());
			conf.setWaitingTimeout(this.conversationTimeout);
			conf.setFallenNode(fallenNode);
			conf.setOptimizedReJoin(false);
			ReJoinClient client = new ReJoinClient(conf, packetMan, statusMan, listenerMan, convGraveyard, controlSystem, taskConversersList, true, false);
			/* ...e lo eseguo */
			client.start();
		}
	}
	
	/* metodi privati di utilità **************************************************************************/
	
	private void notifyCommunityLeft(String community){
		CommunityLeftEvent event = new CommunityLeftEvent(this, community);
		ICommunityLeftListener[] listeners = this.listenerMan.getCommunityLeftListeners();
		for(int i = 0; i< listeners.length; i++)
			listeners[i].communityLeft(event);
	}
	
	private void notifyCommunityJoined(String community){
		CommunityJoinedEvent event = new CommunityJoinedEvent(this, community);
		ICommunityJoinedListener[] listeners = this.listenerMan.getCommunityJoinedListeners();
		for(int i = 0; i< listeners.length; i++)
			listeners[i].communityJoined(event);
	}
	
	private void notifyNeighborLost(String community, COPNode neighbor){
		NeighborLostEvent event = new NeighborLostEvent(this, community, neighbor);
		INeighborLostListener[] listeners = this.listenerMan.getNeighborLostListeners();
		for(int i = 0; i< listeners.length; i++)
			listeners[i].neighborLost(event);
	}
	
	public static class ConfigurationSet {
		
		private long conversationTimeout = 20000;
		private boolean optimizedReJoin = false;		
		
		/**
		 * @return Returns the conversationTimeout.
		 */
		public long getConversationTimeout() {
			return conversationTimeout;
		}
		/**
		 * @param conversationTimeout The conversationTimeout to set.
		 */
		public void setConversationTimeout(long conversationTimeout) {
			this.conversationTimeout = conversationTimeout;
		}
		/**
		 * @return Returns the optimizedReJoin.
		 */
		public boolean isOptimizedReJoin() {
			return optimizedReJoin;
		}
		/**
		 * @param optimizedReJoin The optimizedReJoin to set.
		 */
		public void setOptimizedReJoin(boolean optimizedReJoin) {
			this.optimizedReJoin = optimizedReJoin;
		}
	}

}
