package code;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.FileReader;

import java.security.KeyPair;
import java.security.KeyStore;
import java.security.Security;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMDecryptorProvider;
import org.bouncycastle.openssl.PEMEncryptedKeyPair;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;

import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.eclipse.paho.client.mqttv3.MqttTopic;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

public class Antifurto {

	private static String brokerUrl;
	private String interruttore;
	private String interruttoreOutputSuono;
	private String nomeOutputAntifurto;
	private static JSONArray sensori;  // sensori di movimento
	private static ArrayList<String> topicsSub;
	private Date date = new Date();
	private String clientId = Long.toString(date.getTime()) + "-sub-pub"; // unique client id
	private Automa automa;
	
	private MqttClient mqttClient;
	private static String mqttDomain;
	private static String mqttSubdomain;
	
	private final String CONF_FILE = "../res/CONF/conf.json";
	public static final String CONF_ZONA = "../res/CONF/zona.json";
	
	public Antifurto(Automa automa) throws JSONException, IOException, MqttException {
		this.automa = automa;
		
		JSONObject jsonObject = new JSONObject(Helper.leggiFile(CONF_FILE));
		brokerUrl = jsonObject.getString("protocol") + "://" + jsonObject.getString("broker") + ":" + jsonObject.getInt("port");
		mqttDomain = jsonObject.getString("mqttDomain");
		mqttSubdomain = jsonObject.getString("mqttSubdomain");
		topicsSub = new ArrayList<String>();
		
		// Su questo topic ricevero' un messaggio del tipo {request:description} 
		// Dovro' quindi mandare la mia descrizione json
		topicsSub.add("to/all"); 
		
		// Su questo topic ricevero' le richieste di inviare il mio stato attuale
		topicsSub.add("rpc/"+getMqttTree()+"/antifurto");
		
		
		jsonObject = new JSONObject(Helper.leggiFile(CONF_ZONA));
		interruttore = jsonObject.getString("interruttore"); 
		
		topicsSub.add("from/"+getMqttTree()+"/gpio/" + interruttore); // Sottoscrivo i messaggi che notificano il cambiamento di stato dell'interruttore
		
		// Per ogni sensore di movimento, sottoscrivo i messaggi che notificano il loro cambiamento di stato 
		sensori = jsonObject.getJSONArray("sensoriMovimento");
		for(int i=0; i<sensori.length(); i++) {
			topicsSub.add("from/"+getMqttTree()+"/gpio/" + sensori.get(i)); 
		}
		
		nomeOutputAntifurto = jsonObject.getString("nomeOutputAntifurto");
		topicsSub.add("from/"+getMqttTree()+"/gpio/" + nomeOutputAntifurto);
		
		interruttoreOutputSuono = jsonObject.getString("outputSuono");
		topicsSub.add("from/"+getMqttTree()+"/gpio/" + interruttoreOutputSuono); // Sottoscrivo i messaggi che notificano il cambiamento di stato dell'interruttore
		
		
		topicsSub.add("to/"+getMqttTree()+"/antifurto/soglia"); // Su questo topic mi arrivera' un messaggio {"soglia": 30} e dovrò impostare la soglia di conseguenza
		
		topicsSub.add("conf/"+getMqttTree()+"/antifurto/sensore"); // Su questo topic mi arrivera' un messaggio per l'aggiunta di un sensore di movimento. 
																   // Ad esempio se mi arriva il messaggio {"in": "IN3", "delta":33 } devo aggiungere il sensore di movimento che si 
																   // chiama IN3, il cui valore di delta e' 33 (devo quindi aggiornare il file deltaSensoriMovimento.json
		
		topicsSub.add("to/"+getMqttTree()+"/antifurto/luceAntifurto");
		this.mqttClient = new MqttClient(brokerUrl, clientId, new MemoryPersistence());
		
	}
	
	public void startClient(Esecutore esec, Publisher publisher) throws MqttException {
		String caFilePath = "";
		String clientCrtFilePath = "";
		String clientKeyFilePath = "";
		
		MqttConnectOptions options = new MqttConnectOptions();  
		options.setCleanSession(false);
		if(brokerUrl.contains("luci.local")) {  // devo connettermi al mosquitto della beaglebone
			caFilePath = "../certificates/beaglebone/caGruppo2.crt";
			clientCrtFilePath = "../certificates/beaglebone/clientGruppo2.crt";
			clientKeyFilePath = "../certificates/beaglebone/clientGruppo2.key";
			
			options.setUserName("gruppo2");
			options.setPassword("funziona".toCharArray());
		}
		else if(brokerUrl.contains("localhost")) { // DA FARE: il microservizio girerà sulla beaglebone. Quindi qui voglio l'url del broker connesso in bridge con il mosquitto della beaglebone. Non potrà essere localhost!
			caFilePath = "../certificates/localhost/ca.crt";
			clientCrtFilePath = "../certificates/localhost/client.crt";
			clientKeyFilePath = "../certificates/localhost/client.key";
		} else {
			System.out.println("Unknown broken url " + brokerUrl);
			System.exit(1);
		}

		options.setMqttVersion(MqttConnectOptions.MQTT_VERSION_3_1);
		
		SSLSocketFactory socketFactory;
		try {
			socketFactory = getSocketFactory(caFilePath, clientCrtFilePath, clientKeyFilePath, "");
			options.setSocketFactory(socketFactory);
		} catch (Exception e) {
			e.printStackTrace();
		}
		
		mqttClient.setCallback(new SubscribeCallback(this, publisher, interruttore, interruttoreOutputSuono, esec, automa, nomeOutputAntifurto));
		
		mqttClient.connect(options);
		
		for(String t: topicsSub) 
			addTopicToSubscribe(t);
	}
	
	// sottoscrive il topic passato come parametro
	public void addTopicToSubscribe(String topic) throws MqttException {
		mqttClient.subscribe(topic);
	}
	
	public void publishMethod(String topic, String msg) throws MqttException {
		final MqttTopic msgTopic = mqttClient.getTopic(topic);
		msgTopic.publish(new MqttMessage(msg.getBytes()));
	}
	
	
	
	
	
	/*
	 * Abbiamo 3 tipi di eventi:
	 * 		to 		Command Events - mi viene richiesto di eseguire un comando --> sottoscrivo 
	 * 		from 	Status Events - io annuncio un cambiamento di stato  	
	 * 								(inviato a tutti i client che hanno sottoscritto il topic su cui faccio la publish)
	 * 		rpc 	Query Events - richiesta di inviare uno stato a un client (o microservizio). --> sottoscrivo 
	 * 			  				   Query implementati come coppia di eventi: <query event, status event>
	 * RICORDIAMO COME ABBIAMO IMPLEMENTATO LA RPC SU MQTT
	 * 
	 * 
	 * 
	 * Il cuore della nostra business logic è un metodo in cui mi metto in attesa su mqtt e: 
	 * 		se arriva un comando, chiamo il metodo responsabile per eseguire quel comando;
	 * 		se arriva un evento di stato, chiamo il metodo responsabile per gestire quell'evento di stato;
	 * 		se arriva una remote procedure call, attivo la risposta sulla remote procedure call 
	 * 
	 * 
	 * 
	 * Capire come gestire sistema a regole (vedi slide "Esempio" in "Applicazioni IoT "Cloud Based"")
	 * 
	 * 
	 * 
	 * Sicurezza gestita con TLS
	 * 
	 * 
	 * 
	 * FILE DA FARE per un servizio: (?)
	 * 		file che contiene le informazioni per connettersi al mosquitto locale 
	 * 		file che dice qual'è la porta che lui deve esporre
	 * 		file che mi dice tutte le cose che lo configurano, ad esempio quanti device deve guardare
	 * 		...
	 * 
	 * 	I file di configurazione sono in JSON o in XML
	 * I dati persistiti si troveranno nella sottodirectory (chiamata con il nome del servizio) di Home
	 * 
	 * 
	 * Nella fase di configurazione potremo ancora modificare i file di configurazione (ad esempio aggiungendo device, 
	 * regole). Dopodiché quando si fa partire il servizio si inizierà a usare l'altra parte di file system (ovvero il Local 
	 * Dynamic File System) dove si andranno a salvare gli stati per renderli persistenti.
	 * 
	 * 
	 * 
	 * Ho un processo principale che come unico compito ha quello di fare una fork() e creare un processo figlio. 
	 * Sarà questo processo figlio a eseguire effettivamente il programma. Se il processo figlio termina (PER UN QUALCHE 
	 * PROBLEMA), allora il processo padre se ne accorge e fa ripartire un nuovo processo figlio.
	 * 
	 *  
	 * Devo avere una thread di configurazione che obbedisce ai miei comandi (dati sulla HAT interface andando su 
	 * 	192.168.0.101:9001/configure.html (dove 192.168.0.101 è l'indirizzo della beaglebone)
	 * 
	 * 
	 * Ho una classe che implementa l'automa a stati finiti
	 * 
	 * 
	 * 
	 * 
	 * NOTA:
	 * All'avvio, leggo il file stato e mi imposto di conseguenza. Poi interrogo la beaglebone per sapere SOLO lo stato 
	 * dell'antifurto (luce rossa), ovvero se l'antifurto e' on o off. Lo stato del sensore di suono, non mi interessa: poiche' 
	 * sono solo io a impostarlo (la web app si comporta in conseguenza dei miei messaggi).
	 * Mi serve interrogare la beaglebone per lo stato dell'antifurto perche' puo' succedere che l'antifurto abbia avuto 
	 * dei problemi e quindi non abbia funzionato per un po' e nel mentre l'utente ha attivato l'antifurto dagli scenari. 
	 * Quindi proprio perche' sia il mio antifurto sia gli scenari comandano lo stesso antifurto, devo fare questa richiesta
	 * alla beaglebone.
	 * Dopo aver chiesto lo stato dell'antifurto alla beaglebone, so esattamente lo stato in cui devo essere --> mi imposto 
	 * di conseguenza!
	*/
	
	public static void main(String args[]) throws JSONException, IOException, MqttException {
		
//		while(true) {
			try {
				startSystem();
			}
			catch(Exception e) {
				System.out.println("Error: "+ e.getMessage() + "\nRestarting system...");
				e.printStackTrace();
			}
//		}
	}
	
	
	private static void startSystem() throws JSONException, IOException, MqttException {
		
		MyQueue<Integer> codaVal = new MyQueue<Integer>();
		MyQueue<Pair> codaMsg = new MyQueue<Pair>();
		Automa automa = new Automa();
		
		Antifurto antifurto = new Antifurto(automa);
		Publisher publisher = new Publisher(codaMsg, antifurto);
		Esecutore esec = new Esecutore(publisher, codaVal, automa, antifurto.interruttoreOutputSuono);
		Timer timer = new Timer(6000,-1,esec,automa);
		
		antifurto.startClient(esec, publisher);
		publisher.start();
		esec.start();
		timer.start(); 
	}
	

	public static void stopSystem() {
		// DA FARE
//		codaVal.removeAll();
//		codaMsg.removeAll();
//		
//		publisher.interrupt();
//		esec.interrupt();
//		timer.interrupt();
//		mqttClient.disconnect();
//		
//		startSystem();
		
	}
	
	public static String getMqttTree() {
		return mqttDomain+"/"+mqttSubdomain;
	}

	public static JSONArray getSensori() {
		return sensori;
	}
	
	public static void addSensore(String newSensore) { // aggiunge un sensore di movimento
		sensori.put(newSensore);
	}
	
	
	private static SSLSocketFactory getSocketFactory(final String caCrtFile,
			final String crtFile, final String keyFile, final String password)
			throws Exception {
		Security.addProvider(new BouncyCastleProvider());

		// load CA certificate
		X509Certificate caCert = null;

		FileInputStream fis = new FileInputStream(caCrtFile);
		BufferedInputStream bis = new BufferedInputStream(fis);
		CertificateFactory cf = CertificateFactory.getInstance("X.509");

		while (bis.available() > 0) {
			caCert = (X509Certificate) cf.generateCertificate(bis);
		}

		// load client certificate
		bis = new BufferedInputStream(new FileInputStream(crtFile));
		X509Certificate cert = null;
		while (bis.available() > 0) {
			cert = (X509Certificate) cf.generateCertificate(bis);
		}

		// load client private key
		PEMParser pemParser = new PEMParser(new FileReader(keyFile));
		Object object = pemParser.readObject();
		PEMDecryptorProvider decProv = new JcePEMDecryptorProviderBuilder()
				.build(password.toCharArray());
		JcaPEMKeyConverter converter = new JcaPEMKeyConverter()
				.setProvider("BC");
		KeyPair key;
		if (object instanceof PEMEncryptedKeyPair) {
			//System.out.println("Encrypted key - we will use provided password");
			key = converter.getKeyPair(((PEMEncryptedKeyPair) object)
					.decryptKeyPair(decProv));
		} else {
			//System.out.println("Unencrypted key - no password needed");
			key = converter.getKeyPair((PEMKeyPair) object);
		}
		pemParser.close();

		// CA certificate is used to authenticate server
		KeyStore caKs = KeyStore.getInstance(KeyStore.getDefaultType());
		caKs.load(null, null);
		caKs.setCertificateEntry("ca-certificate", caCert);
		TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
		tmf.init(caKs);

		// client key and certificates are sent to server so it can authenticate
		// us
		KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
		ks.load(null, null);
		ks.setCertificateEntry("certificate", cert);
		ks.setKeyEntry("private-key", key.getPrivate(), password.toCharArray(),
				new java.security.cert.Certificate[] { cert });
		KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory
				.getDefaultAlgorithm());
		kmf.init(ks, password.toCharArray());

		// finally, create SSL socket factory
		SSLContext context = SSLContext.getInstance("TLSv1.2");
		context.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);

		return context.getSocketFactory();
	}
	

}