/**
 * This file is part of fr.inria.plasmalab :: Plugin for discussing with SoS-ADL simulator.
 *
 * fr.inria.plasmalab :: Plugin for discussing with SoS-ADL simulator is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * fr.inria.plasmalab :: Plugin for discussing with SoS-ADL simulator 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with fr.inria.plasmalab :: Plugin for discussing with SoS-ADL simulator.  If not, see <http://www.gnu.org/licenses/>.
 */
package fr.inria.plasmalab.sosadl.simulator;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.jdom2.Element;

import events.Done;
import events.Event;
import events.NewLink;
import events.NewNode;
import events.RendezVous;
import fr.inria.plasmalab.sosadl.checker.ast.value.IntValue;
import fr.inria.plasmalab.workflow.concrete.Variable;
import fr.inria.plasmalab.workflow.concrete.VariableConstraint;
import fr.inria.plasmalab.workflow.data.AbstractFunction;
import fr.inria.plasmalab.workflow.data.AbstractModel;
import fr.inria.plasmalab.workflow.data.simulation.InterfaceIdentifier;
import fr.inria.plasmalab.workflow.data.simulation.InterfaceState;
import fr.inria.plasmalab.workflow.exceptions.PlasmaDeadlockException;
import fr.inria.plasmalab.workflow.exceptions.PlasmaException;
import fr.inria.plasmalab.workflow.exceptions.PlasmaRunException;
import fr.inria.plasmalab.workflow.exceptions.PlasmaSimulationException;
import fr.inria.plasmalab.workflow.shared.ResultIdentifier;
import requests.Request; // see lib folder of this project
import responses.Events;
import responses.Init;
import responses.Response;
import responses.TraceStart;
import values.IntegerValue;

/** Simulator for dynamic architectures in go languages.
 *  The simulator receive simulation updates from the external go simulator and maintain
 *  the states of the architecture with graphs.
 *  
 * @author ltraonou
 *
 */
public class SosADLSimulator extends AbstractModel {
	static final String nameAtt = "name", contc="content";

	/** The id of the factory that built this model */
	private String id;
	/** -1 if no deadlock.
	 *  Size of the trace otherwise.
	 */
	private int deadlockPos;
	
	/** the socket used for connecting to the simulator */
	private Socket connection;
	
	/** Stream for sending objects (i.e. requests) to the simulator */
	private ObjectOutputStream toSimulator;
	/** Stream for receiving objects (i.e. responses) from the simulator */
	private ObjectInputStream fromSimulator;
	
	/** List of events to process received from the simulator */
	private List<Event> eventsToProcess;
    
	private ArchState initialState, currentState;
	private List<InterfaceState> trace;

	private List<PlasmaException> errors;
	private InterfaceIdentifier[] identifiers;

	public SosADLSimulator(String name, String content, String id) {
		this.name = name;
		this.id = id;
		this.content = content;
		this.origin = null;

		this.errors = new ArrayList<PlasmaException>();
	}
	
	/** Reads the hostname and port from a file */
	public SosADLSimulator(String name, File file, String id) {
		this.name = name;
		this.id = id;
		this.content = "";
		this.origin = file;

		try {
			FileReader fr = new FileReader(file);
			BufferedReader br = new BufferedReader(fr);
			while(br.ready())
				content = content+br.readLine()+"\n";
			br.close();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		this.errors = new ArrayList<PlasmaException>();
	}
		
	@Override
	public boolean checkForErrors() {
		String formatError = "Expected format: <hostname>:<port>:";
		errors.clear();
		content = content.trim();
		if(!content.matches("[^:]*:[^:]*:")){
			errors.add(new PlasmaException(formatError));
			return true;
		}
		// we separate elements and should get exactly 2 of them
		String []coords = content.split(":");
		InetAddress a;
		Integer port;
		try {
			a = InetAddress.getByName(coords[0]);
			port = Integer.decode((coords[1]));
			if(connection != null){
				if( a.equals(connection.getInetAddress()) && port.equals(connection.getPort())){
					// already connected, no error
					return false;
				}
			}
			connection = new Socket(a,port);
			toSimulator = new ObjectOutputStream(connection.getOutputStream());
            fromSimulator = new ObjectInputStream(connection.getInputStream());
            toSimulator.writeObject(Request.INIT);
            Response r = (Response) fromSimulator.readObject();
            if(! (r instanceof Init)){
            	errors.add(new PlasmaException("Unexpected simulator state."));
            }
		} catch (UnknownHostException e1) {
			errors.add(new PlasmaException("The host name/address " + coords[0] + " is invalid."));
		} catch (NumberFormatException e2){
			errors.add(new PlasmaException("The port number " + coords[1] + " should be an integer."));
		} catch (IOException e) {
			errors.add(new PlasmaException("Unable to connect to the simulator."));
		} catch (ClassNotFoundException e) {
			errors.add(new PlasmaException("Unexpected message from the simulator."));
		}
		return !errors.isEmpty();
	}

	@Override
	public InterfaceState newPath() {
		trace = new ArrayList<InterfaceState>();
		initialState = new ArchState(); // empty graph
		currentState = initialState;
		trace.add(initialState);
		deadlockPos = -1; // no deadlock
		eventsToProcess = new ArrayList<Event>();
		Response r = requestSimulator(Request.NEW_TRACE);
		if(!(r instanceof TraceStart)){
			throw new PlasmaRunException("Error when communicating with the simulator.");
		}
		return initialState;
	}

	@Override
	public InterfaceState newPath(List<InterfaceState> initTrace) {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public InterfaceState newPath(long seed) {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public void setValueOf(Map<InterfaceIdentifier, Double> update) {
		// TODO Auto-generated method stub
	}

	@Override
	public InterfaceState simulate() throws PlasmaDeadlockException {
		if(deadlockPos > 0){
			// if we have reached a deadlock already, simulate is not possible
			throw new PlasmaDeadlockException(currentState,deadlockPos);
		}
		if(eventsToProcess.isEmpty()){
			Events evs = (Events) requestSimulator(Request.NEW_STATE);
			eventsToProcess = evs.getEvents();		
		}
		// initialize the new state as a copy of the previous one
		currentState = new ArchState(currentState);
		// update the new current state by applying the event from the simulator
		update_currentstate(eventsToProcess.get(0));
		eventsToProcess.remove(0);
		trace.add(currentState);
		return currentState;
	}


	@Override
	public void backtrack() {
		throw new PlasmaSimulationException(
				"Backtrack not implemented in the SoS-ADL Simulator");
	}

	@Override
	public void cut(int stateNb) throws PlasmaRunException {
		// In cut we forget the state before stateNb
		for(int i=0; i<stateNb; i++)
			trace.remove(0);
	}

	@Override
	public InterfaceState getCurrentState() {
		return currentState;
	}

	//@Override
	public InterfaceState getInitialState() {
		return initialState;
	}

	@Override
	public InterfaceState getStateAtPos(int pos) {
		return trace.get(pos);
	}

	@Override
	public List<InterfaceState> getTrace() {
		return trace;
	}

	@Override
	public int getTraceLength() {
		return trace.size();
	}

	@Override
	public int getDeadlockPos() {
		return deadlockPos;
	}

	//@Override
	public List<InterfaceIdentifier> getPathProperty() {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public List<Variable> getOptimizationVariables() {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public List<VariableConstraint> getOptimizationConstraints() {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public Map<String, InterfaceIdentifier> getIdentifiers() {
		// These are:
		// - the identifiers used by property plugins. Not needed for the GoSImu
		// - the project viewer. DO we want ot show something ?
		// - some SMC Result to get optim variables. Not needed until we unable optimization.
		return new HashMap<String, InterfaceIdentifier>(0);
	}

	@Override
	public InterfaceIdentifier getTimeId() {
		if(identifiers == null){
			initHeaders();
		};
		return identifiers[0];
	}

	@Override
	public boolean hasTime() {
		return true;
	}

	@Override
	public InterfaceIdentifier[] getHeaders() {
		if(identifiers == null){
			initHeaders();
		};
		return identifiers;
	}
	
	private void initHeaders() {
		identifiers = new InterfaceIdentifier[3];
		identifiers[0] = new ResultIdentifier("#Time", false);
		identifiers[1] = new ResultIdentifier("#Nodes", false);
		identifiers[2] = new ResultIdentifier("#Edges", false);
	}

	@Override
	public String getId() {
		return id;
	}


	@Override
	public List<PlasmaException> getErrors() {
		return errors;
	}

	//@Override
	public void toJDOM(Element dataElt) {
		dataElt.setAttribute(nameAtt, name);
		Element cont = new Element(contc);
		cont.setText(content);
		dataElt.addContent(cont);
	}

	@Override
	public List<AbstractFunction> getFunctions() {
		return Collections.emptyList();
	}
	
	/**
	 * Do a request / response exchange with the simulator.
	 * @param req The request to be sent.
	 * @return The response from the simulator.
	 */
	private Response requestSimulator(Request req){
		Response rep;
		if(connection == null){
			throw new PlasmaRunException("Simulator disconnected.");
		}
		try {
			toSimulator.writeObject(req);
			rep = (Response) fromSimulator.readObject();
		} catch (IOException e) {
			closeConnection();
			throw new PlasmaRunException("Error when communicating with the simulator.");
		} catch (ClassNotFoundException e) {
			throw new PlasmaRunException("Error when communicating with the simulator.");
		}
		return rep;
	}
	
	private void closeConnection(){
		try {
			connection.close();
		} catch (IOException e) {
			// we ignore any error during the closing of the socket
		}
		connection = null;
	}
	

	
	private void update_currentstate(Event event) throws  PlasmaDeadlockException {
		if(event instanceof NewNode){
			NewNode nn = (NewNode) event;
			currentState.CreateNode(nn.name, nn.type, nn.connections);
		}
		if(event instanceof NewLink){
			NewLink nl = (NewLink) event;
			currentState.CreateEdge(nl.src.nodeName, nl.src.connName, nl.dest.nodeName, nl.dest.connName);
		}
		if(event instanceof RendezVous){
			RendezVous rv = (RendezVous) event;
			//TODO interface with values !!!!
			currentState.SetEdgeValue(rv.src.nodeName, rv.src.connName, rv.dest.nodeName, rv.dest.connName, new IntValue(((IntegerValue)rv.val).value.intValue()));
		}
		if(event instanceof Done){
			Done d = (Done) event;
			currentState.MarkInactive(d.nodeName);	
		}
	}
	
	

	@Override
	public List<InterfaceIdentifier> getStateProperties() {
		// TODO Auto-generated method stub
		return null;
	}

}
