/****************************************************************************/
// Eclipse SUMO, Simulation of Urban MObility; see https://eclipse.org/sumo
// Copyright (C) 2017-2018 German Aerospace Center (DLR) and others.
// TraCI4J module
// Copyright (C) 2011 ApPeAL Group, Politecnico di Torino
// This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v2.0
// which accompanies this distribution, and is available at
// http://www.eclipse.org/legal/epl-v20.html
// SPDX-License-Identifier: EPL-2.0
/****************************************************************************/
/// @file    SumoTraciConnection.java
/// @author  Enrico Gueli
/// @author  Mario Krumnow
/// @date    2011
/// @version $Id$
///
//
/****************************************************************************/
package it.polito.appeal.traci;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.Observer;

import de.tudresden.sumo.cmd.Vehicle;
import de.tudresden.sumo.subscription.Subscription;
import de.tudresden.sumo.util.CommandProcessor;
import de.tudresden.sumo.util.SumoCommand;

/**
 * Models a TCP/IP connection to a local or remote SUMO server via the TraCI
 * protocol.
 * 
 * @author Enrico Gueli &lt;enrico.gueli@gmail.com&gt;
 * @author Mario Krumnow
 * 
 */
public class SumoTraciConnection {

	/**
	 * Reads an InputStream object and logs each row in the containing class's
	 * logger.
	 * 
	 * @author Enrico
	 * 
	 */
	
	static boolean stdout = false;
	static boolean stderr = true;
	
	private static class StreamLogger implements Runnable {
		final InputStream stream;
		@SuppressWarnings("unused")
		final String prefix;

		public StreamLogger(InputStream stream, String prefix) {
			this.stream = stream;
			this.prefix = prefix;
		}

		public void run() {
		
			BufferedReader br = new BufferedReader(new InputStreamReader(stream));
			try {
				
				String strLine;
				while ((strLine = br.readLine()) != null)   {
					
					if(strLine.contains("Error:") && !strLine.contains("peer shutdown")){
						if(stderr) System.err.println (strLine);
					}else{
						if(stdout) System.out.println (strLine);
					}
					
				  }	
				
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

	private String configFile;
	private int randomSeed;
	private int remotePort;
	private Socket socket;
	
	//new
	private String net_file;
	private String route_file;
	private String additional_file;
	private String gui_settings;
	
	String sumoEXE = "/opt/sumo/sumo-1.1.0/bin/sumo";
	private CommandProcessor cp;
	
	private Process sumoProcess;
	private static final int CONNECT_RETRIES = 3;
	@SuppressWarnings("unused")
	private CloseQuery closeQuery;
	private List<String> args = new ArrayList<String>();
	
	private boolean remote = false;
	
	public SumoTraciConnection(String sumo_bin) {
		this.sumoEXE=sumo_bin;
	}
	
	public SumoTraciConnection(String sumo_bin, String net_file, String route_file) {
		this.sumoEXE=sumo_bin;
		this.net_file=net_file;
		this.route_file=route_file;
	}
	
	public SumoTraciConnection(String sumo_bin, String net_file, String route_file, String additional_file) {
		this.sumoEXE=sumo_bin;
		this.net_file=net_file;
		this.route_file=route_file;
		this.additional_file=additional_file;
	}
	
	public SumoTraciConnection(String sumo_bin, String net_file, String route_file, String additional_file, String gui_settings) {
		this.sumoEXE=sumo_bin;
		this.net_file=net_file;
		this.route_file=route_file;
		this.additional_file=additional_file;
		this.gui_settings = gui_settings;
	}
	
	public SumoTraciConnection(String sumo_bin, String configFile) {
		this.sumoEXE=sumo_bin;
		this.configFile=configFile;
	}
	
	public SumoTraciConnection(String configFile, int randomSeed, boolean useGeoOffset) {
		this.randomSeed = randomSeed;
		this.configFile = configFile;
	}

	public SumoTraciConnection(int remotePort) throws IOException, InterruptedException {
        this(new InetSocketAddress("127.0.0.1", remotePort));
    }

	public SumoTraciConnection(SocketAddress sockAddr) throws IOException,
			InterruptedException {
		
		this.remote=true;
		socket = new Socket();
		socket.setTcpNoDelay(true);
		
		int waitTime = 500; // milliseconds
		for (int i = 0; i < CONNECT_RETRIES; i++) {

			try {
				socket.connect(sockAddr);
				break;
			} catch (ConnectException ce) {
				Thread.sleep(waitTime);
				waitTime *= 2;
			}
		}

		if (!socket.isConnected()) {
			throw new IOException("can't connect to SUMO server");
		}else{
			this.cp = new CommandProcessor(socket);
		}
		
	}

	/**
	 * Adds a custom option to the SUMO command line before executing it.
	 * 
	 * @param option
	 *            the option name, in long form (e.g. &quot;no-warnings&quot;
	 *            instead of &quot;W&quot;) and without initial dashes
	 * @param value
	 *            the option value, or <code>null</code> if the option has no
	 *            value
	 */
	public void addOption(String option, String value) {
		args.add("--" + option);
		if (value != null)
			args.add(value);
	}

	/**
	 * Runs a SUMO instance and tries to connect at it.
	 * 
	 * @throws IOException
	 *             if something wrong occurs while starting SUMO or connecting
	 *             at it.
	 */
	public void runServer() throws IOException {
        runServer(0);
    }
	
	/**
	 * Runs a SUMO instance and tries to connect at it.
	 * 
	 * @throws IOException
	 *             if something wrong occurs while starting SUMO or connecting
	 *             at it.
	 */
	public void runServer(int _remotePort) throws IOException {
		
		
		
		if(!this.remote){
		
        remotePort = _remotePort;
        if (remotePort == 0) {
            findAvailablePort();
        }

		runSUMO();

		int waitTime = 500; // milliseconds
		try {
			for (int i = 0; i < CONNECT_RETRIES; i++) {
			

				socket = new Socket();
				socket.setTcpNoDelay(true);
				
				try {
					socket.connect(new InetSocketAddress("127.0.0.1", remotePort));
					break;
				} catch (ConnectException ce) {
					Thread.sleep(waitTime);
					waitTime *= 2;
				}
				
				
				
				
			}

			if (!socket.isConnected()) {
				throw new IOException("can't connect to SUMO server at " + remotePort);
			}else{
				this.cp = new CommandProcessor(socket);
			}

		} catch (InterruptedException e) {
			e.printStackTrace();
		}

		closeQuery = new CloseQuery(socket);
		
		}
		
	}

	private void runSUMO() throws IOException {
		
		args.add(0, sumoEXE);
		
		if(this.configFile != null){
		args.add("-c");
		args.add(configFile);
		}else if(this.net_file != null && this.route_file != null){
			
			args.add("--net-file");
			args.add(this.net_file);
			args.add("--route-files");
			args.add(this.route_file);
		
			if(this.additional_file != null){
				args.add("--additional-files");
				args.add(this.additional_file);
			}
			
			if(this.gui_settings != null){
				args.add("--gui-settings-file");
				args.add(this.gui_settings);
			}
			
		}else if(this.net_file != null){
			args.add("--net-file");
			args.add(this.net_file);
		}
		
		args.add("--remote-port");
		args.add(Integer.toString(remotePort));
		
		if (randomSeed != -1) {
			args.add("--seed");
			args.add(Integer.toString(randomSeed));
		}

		String[] argsArray = new String[args.size()];
		args.toArray(argsArray);
		
		
		sumoProcess = Runtime.getRuntime().exec(argsArray);

		

		StreamLogger errStreamLogger = new StreamLogger(sumoProcess.getErrorStream(), "SUMO-err:");
		StreamLogger outStreamLogger = new StreamLogger(sumoProcess.getInputStream(), "SUMO-out:");
		new Thread(errStreamLogger, "StreamLogger-SUMO-err").start();
		new Thread(outStreamLogger, "StreamLogger-SUMO-out").start();
	}

	private void findAvailablePort() throws IOException {
		ServerSocket testSock = new ServerSocket(0);
		remotePort = testSock.getLocalPort();
		testSock.close();
		testSock = null;
	}

	/**
	 * Closes the connection, quits the simulator, frees any stale
	 * resource and makes all {@link Vehicle} instances inactive.
	 * 
	 */

	public void close(){
		try {
			socket.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Closes the connection, eating the {@link InterruptedException} it may
	 * throw, hoping that Murphy's Law doesn't notice all this ugly thing.
	 */
	private void closeAndDontCareAboutInterruptedException() {
		close();
	}
	
	/**
	 * Returns <code>true</code> if the connection was closed by the user, or if
	 * an {@link IOException} was thrown after the connection was made.
	 * @see #close()
	 * @return boolean
	 */
	public boolean isClosed() {
		return socket == null || socket.isClosed();
	}

	
	public synchronized void do_job_set(SumoCommand cmd) throws Exception{
		
		if (isClosed())
			throw new IllegalStateException("connection is closed");
		
		try {this.cp.do_job_set(cmd);}
		catch (Exception e) {
			closeAndDontCareAboutInterruptedException();
			throw e;
		}
		
	}
	
	public synchronized Object do_job_get(SumoCommand cmd) throws Exception{
		
		Object output = null;
		if (isClosed())
			throw new IllegalStateException("connection is closed");
		
		try {
			output = this.cp.do_job_get(cmd);
		}
		catch (Exception e) {
			closeAndDontCareAboutInterruptedException();
			throw e;
		}
		
		return output;
	}


	public synchronized void setOrder(int index) throws Exception{

		if (isClosed())
			throw new IllegalStateException("connection is closed");

		try {this.cp.do_setOrder(index);}
		catch (Exception e) {
			closeAndDontCareAboutInterruptedException();
			throw e;
		}

	}

	public synchronized void do_timestep() throws Exception{this.do_timestep(0);}
	
	public synchronized void do_timestep(double targetTime) throws Exception{
		
		if (isClosed())
			throw new IllegalStateException("connection is closed");
		
		try {this.cp.do_SimulationStep(targetTime);}
		catch (Exception e) {
			closeAndDontCareAboutInterruptedException();
			throw e;
		}
		
	}
	
	public synchronized void addObserver(Observer o){this.cp.addObserver(o);}
	
	public synchronized void do_subscription(Subscription cs) throws Exception{
		
		if (isClosed())
			throw new IllegalStateException("connection is closed");
		
		try {
			this.cp.do_subscription(cs);
		}catch (Exception e) {
			closeAndDontCareAboutInterruptedException();
			throw e;
		}
		
	}
	
	public void printSumoOutput(boolean b){stdout = b;}
	
	public void printSumoError(boolean b){stderr = b;}
	
}

