package de.tudarmstadt.informatik.hostage;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Iterator;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;

import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.preference.PreferenceManager;
import android.util.Log;

import de.tudarmstadt.informatik.hostage.commons.HelperUtils;
import de.tudarmstadt.informatik.hostage.location.MyLocationManager;
import de.tudarmstadt.informatik.hostage.logging.AttackRecord;
import de.tudarmstadt.informatik.hostage.logging.Logger;
import de.tudarmstadt.informatik.hostage.logging.NetworkRecord;
import de.tudarmstadt.informatik.hostage.net.MyServerSocketFactory;
import de.tudarmstadt.informatik.hostage.protocol.Protocol;
import de.tudarmstadt.informatik.hostage.protocol.SMB;
import de.tudarmstadt.informatik.hostage.protocol.SSLProtocol;

/**
 * Protocol listener class:<br>
 * Creates a Socket on the port of a given protocol and listens for incoming
 * connections.<br>
 * For each connection creates a Socket and instantiate an {@link Handler}.
 * 
 * @author Mihai Plasoianu
 * @author Wulf Pfeiffer
 */
public class Listener implements Runnable {

	private ArrayList<Handler> handlers = new ArrayList<Handler>();

	private Protocol protocol;

	private ServerSocket server;
	private Thread thread;
	private int port;
	private Hostage service;

	private ConnectionRegister conReg;
	private boolean running = false;

	/**
	 * Constructor for the class. Instantiate class variables.
	 * 
	 * @param service
	 *            The Background service that started the listener.
	 * @param protocol
	 *            The Protocol on which the listener is running.
	 */
	public Listener(Hostage service, Protocol protocol) {
		this.service = service;
		this.protocol = protocol;
		port = protocol.getPort();
		conReg = new ConnectionRegister(service);
	}

	public Listener(Hostage service, Protocol protocol, int port) {
		this.service = service;
		this.protocol = protocol;
		this.port = port;
		conReg = new ConnectionRegister(service);
	}

	/**
	 * Determines the amount of active handlers.
	 * 
	 * @return The number of active handlers.
	 */
	public int getHandlerCount() {
		return handlers.size();
	}

	/**
	 * Return the port number on which the listener listening.
	 * 
	 * @return Used port number.
	 */
	public int getPort() {
		return port;
	}

	/**
	 * Determine the name of the protocol the listener is running on.
	 * 
	 * @return Name of the protocol
	 */
	public String getProtocolName() {
		return protocol.toString();
	}

	/**
	 * Determines if the service is running.
	 * 
	 * @return True if the service is running, else false.
	 */
	public boolean isRunning() {
		return running;
	}

	/**
	 * Remove all terminated handlers from its internal ArrayList.
	 */
	public void refreshHandlers() {
		for (Iterator<Handler> iterator = handlers.iterator(); iterator.hasNext();) {
			Handler handler = iterator.next();
			if (handler.isTerminated()) {
				conReg.closeConnection();
				iterator.remove();
			}
		}
	}

	@Override
	public void run() {
		while (!thread.isInterrupted()) {
			addHandler();
		}
		for (Handler handler : handlers) {
			//TODO kann ConcurrentModificationException auslösen, da über collection iteriert wird während elemente entfernt werden
			handler.kill();
		}
	}

	/**
	 * Starts the listener. Creates a server socket runs itself in a new Thread
	 * and notifies the background service.
	 */
	public boolean start() {
		if (protocol.toString().equals("SMB")) {
			return false; // disable smb for the moment to prevent crashes
		}
		try {
			server = new MyServerSocketFactory().createServerSocket(port);
			if (server == null)
				return false;
			if (protocol.toString().equals("SMB")) {
				((SMB) protocol).setIP(HelperUtils.inetAddressToString(Hostage.getContext()
						.getSharedPreferences(
								Hostage.getContext().getString(R.string.connection_info),
								Hostage.MODE_PRIVATE)
						.getInt(Hostage.getContext()
								.getString(R.string.connection_info_internal_ip), 0)));
			}
			(this.thread = new Thread(this)).start();
			running = true;
			service.notifyUI(this.getClass().getName(),
					new String[] { service.getString(R.string.broadcast_started), protocol.toString(), Integer.toString(port) });
			return true;
		} catch (IOException e) {
			return false;
		}
	}

	/**
	 * Stops the listener. Closes the server socket, interrupts the Thread its
	 * running in and notifies the background service.
	 */
	public void stop() {
		try {
			server.close();
			thread.interrupt();
			running = false;
			service.notifyUI(this.getClass().getName(),
					new String[] { service.getString(R.string.broadcast_stopped), protocol.toString(), Integer.toString(port) });
		} catch (IOException e) {
		}
	}

	/**
	 * Waits for an incoming connection, accepts it and starts a {@link Handler}
	 */
	private void addHandler() {
		if (conReg.isConnectionFree()) {
			try {
				final Socket client = server.accept();
				if (ConnectionGuard.portscanInProgress()) {
					// stop logging attacks
					return;
				}
				new Thread( new Runnable() {
				    @Override
				    public void run() {
				    	try {
				    		String ip = client.getInetAddress().getHostAddress();
				    		if (ConnectionGuard.registerConnection(port, ip)){ // returns true when a port scan is detected
								logPortscan(client, System.currentTimeMillis());
				    			return;
				    		}
							if (protocol.isSecure()) {
								startSecureHandler(client);
							} else {
								startHandler(client);
							}
							conReg.newOpenConnection();
				    	} catch (Exception e) {
				    		e.printStackTrace();
				    	}
				    }
				}).start();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}

	/**
	 * Creates a new instance of an {@link Handler}.
	 * 
	 * @param service
	 *            The background service
	 * @param listener
	 *            The listener that created the handler
	 * @param protocol
	 *            The Protocol the handler will run on
	 * @param client
	 *            The Socket the handler uses
	 * @return A Instance of a {@link Handler} with the specified parameter.
	 */
	private Handler newInstance(Hostage service, Listener listener, Protocol protocol, Socket client) {
		return new Handler(service, listener, protocol, client);
	}

	/**
	 * Starts a {@link Handler} with the given socket.
	 * 
	 * @param client
	 *            The socket with the accepted connection.
	 * @throws Exception
	 */
	private void startHandler(Socket client) throws Exception {
		handlers.add(newInstance(service, this, protocol.getClass().newInstance(), client));
	}

	/**
	 * Creates a SSLSocket out of the given socket and starts a {@link Handler}.
	 * 
	 * @param client
	 *            The socket with the accepted connection.
	 * @throws Exception
	 */
	private void startSecureHandler(Socket client) throws Exception {
		SSLContext sslContext = ((SSLProtocol) protocol).getSSLContext();
		SSLSocketFactory factory = sslContext.getSocketFactory();
		SSLSocket sslClient = (SSLSocket) factory.createSocket(client, null, client.getPort(), false);
		sslClient.setUseClientMode(false);
		handlers.add(newInstance(service, this, protocol.getClass().newInstance(), sslClient));
	}	
	
	/**
	 * Logs a port scan attack
	 * @param client The socket on which a port scan has been detected.
	 * @param timestamp Timestamp when the portscan has been detected.
	 */
	private void logPortscan(Socket client, long timestamp){
		SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(service);
		SharedPreferences connInfo = service.getSharedPreferences(service.getString(R.string.connection_info), Context.MODE_PRIVATE);
		
		AttackRecord attackRecord = new AttackRecord(true);

		attackRecord.setProtocol("PORTSCAN");
		attackRecord.setExternalIP(connInfo.getString(service.getString(R.string.connection_info_external_ip), null));
		attackRecord.setLocalIP(client.getLocalAddress().getHostAddress());
		attackRecord.setLocalPort(0);
		attackRecord.setRemoteIP(client.getInetAddress().getHostAddress());
		attackRecord.setRemotePort(client.getPort());
		attackRecord.setBssid(connInfo.getString(service.getString(R.string.connection_info_bssid), null));
		
		NetworkRecord networkRecord = new NetworkRecord();
		networkRecord.setBssid(connInfo.getString(service.getString(R.string.connection_info_bssid), null));		
		networkRecord.setSsid(connInfo.getString(service.getString(R.string.connection_info_ssid), null));		
		if (MyLocationManager.getNewestLocation() != null) {
			networkRecord.setLatitude(MyLocationManager.getNewestLocation().getLatitude());
			networkRecord.setLongitude(MyLocationManager.getNewestLocation().getLongitude());
			networkRecord.setAccuracy(MyLocationManager.getNewestLocation().getAccuracy());
			networkRecord.setTimestampLocation(MyLocationManager.getNewestLocation().getTime());
		} else {
			networkRecord.setLatitude(0.0);
			networkRecord.setLongitude(0.0);
			networkRecord.setAccuracy(Float.MAX_VALUE);
			networkRecord.setTimestampLocation(0);
		}
		Logger.logPortscan(Hostage.getContext(), attackRecord, networkRecord, timestamp);
	}
}
