package de.tudarmstadt.informatik.hostage;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.List;

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.logging.Logger;
import de.tudarmstadt.informatik.hostage.logging.MyLocationManager;
import de.tudarmstadt.informatik.hostage.logging.Record;
import de.tudarmstadt.informatik.hostage.logging.Record.TYPE;
import de.tudarmstadt.informatik.hostage.nio.Reader;
import de.tudarmstadt.informatik.hostage.nio.Writer;
import de.tudarmstadt.informatik.hostage.protocol.GHOST;
import de.tudarmstadt.informatik.hostage.protocol.Protocol;
import de.tudarmstadt.informatik.hostage.protocol.Protocol.TALK_FIRST;
import de.tudarmstadt.informatik.hostage.wrapper.Packet;

/**
 * Abstract class for a connection handler using a given protocol.
 * 
 * @author Mihai Plasoianu
 * @author Wulf Pfeiffer
 */
public class HoneyHandler implements Runnable {

	/** Time until the socket throws a time out. The time is in milliseconds. */
	private int TIMEOUT;

	private HoneyService service;
	protected Protocol protocol;
	private Socket client;
	protected Thread thread;

	private int attack_id;
	private int message_id = 0;
	private String externalIP;
	private String BSSID;
	private String SSID;

	private HoneyListener listener;

	/**
	 * Constructor of the class. Initializes class variables for communication
	 * and logging. Then starts itself in a new Thread.
	 * 
	 * @param service
	 *            The background service.
	 * @param listener
	 *            The Listener that called the service.
	 * @param protocol
	 *            The protocol on which the handler is running.
	 * @param client
	 *            A Socket for the communication with a remote client.
	 */
	public HoneyHandler(HoneyService service, HoneyListener listener,
			Protocol protocol, Socket client) {
		this.service = service;
		this.listener = listener;
		this.protocol = protocol;
		if (protocol.toString().equals("GHOST")) {
			((GHOST) protocol).setAttackerIP(client.getInetAddress());
			((GHOST) protocol).setCurrentPort(listener.getPort());
		}
		this.client = client;
		this.thread = new Thread(this);
		SharedPreferences pref = PreferenceManager
				.getDefaultSharedPreferences(service);
		TIMEOUT = pref.getInt("timeout", 30) * 1000;
		// TODO ThreadSicher?
		getAndIncrementAttackID(pref);
		SharedPreferences connInfo = service.getSharedPreferences(
				service.getString(R.string.connection_info),
				Context.MODE_PRIVATE);
		BSSID = connInfo.getString(
				service.getString(R.string.connection_info_bssid), null);
		SSID = connInfo.getString(
				service.getString(R.string.connection_info_ssid), null);
		externalIP = connInfo.getString(
				service.getString(R.string.connection_info_external_ip), null);
		setSoTimeout(client);
		thread.start();
	}

	/**
	 * Determines if the interrupt flag of the thread is set.
	 * 
	 * @return True when the flag is set, else false.
	 */
	public boolean isTerminated() {
		return thread.isInterrupted();
	}

	/**
	 * Sets the interrupt flag of the thread and tries to close the socket.
	 */
	public void kill() {
		service.notifyUI(
				this.getClass().getName(),
				new String[] { service.getString(R.string.broadcast_started),
						protocol.toString(),
						Integer.toString(listener.getPort()) });
		thread.interrupt();
		try {
			client.close();
			Log.i("HoneyHandler", "Socket closed: " + client.isClosed());
		} catch (Exception e) {
			e.printStackTrace();
		} finally {

		}
		listener.refreshHandlers();
	}

	/**
	 * Creates InputStream and OutputStream for the socket. Starts communication
	 * with client. When the client closes the connection or a time out occurs
	 * the handler is finished.
	 */
	@Override
	public void run() {
		service.notifyUI(
				this.getClass().getName(),
				new String[] { service.getString(R.string.broadcast_started),
						protocol.toString(),
						Integer.toString(listener.getPort()) });
		InputStream in;
		OutputStream out;
		try {
			in = client.getInputStream();
			out = client.getOutputStream();
			talkToClient(in, out);
		} catch (Exception e) {
			e.printStackTrace();
		}
		kill();
	}

	/**
	 * Gets attack ID for the attack. Also increases the attack ID counter by
	 * one. Method is synchronized for thread safety.
	 * 
	 * @param pref
	 *            The default SharedPreference of the application
	 * @return Unique integer attack ID
	 */
	private synchronized void getAndIncrementAttackID(SharedPreferences pref) {
		Editor editor = pref.edit();
		attack_id = pref.getInt("ATTACK_ID_COUNTER", 0);
		editor.putInt("ATTACK_ID_COUNTER", attack_id + 1);
		editor.commit();
	}

	/**
	 * Set the timeout of the socket to the hard coded time out variable.
	 * 
	 * @param client
	 *            The socket
	 * @see #TIMEOUT
	 */
	private void setSoTimeout(Socket client) {
		try {
			client.setSoTimeout(TIMEOUT);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	/**
	 * Creates a Record for a message exchanged with a client.
	 * 
	 * @param type
	 *            The type of the message.
	 * @param packet
	 *            The content of the message.
	 * @return The Record representing the communication message.
	 */
	protected Record createRecord(TYPE type, String packet) {
		Record record = new Record();
		record.setId(message_id++);
		record.setAttack_id(attack_id);
		record.setProtocol(protocol.toString());
		record.setType(type);
		record.setTimestamp(System.currentTimeMillis());
		record.setExternalIP(externalIP);
		record.setLocalIP(client.getLocalAddress().getHostAddress());
		record.setLocalHost(client.getLocalAddress().getHostAddress());
		record.setLocalPort(protocol.getPort());
		record.setRemoteIP(client.getInetAddress().getHostAddress());
		record.setRemoteHost(client.getInetAddress().getHostAddress());
		record.setRemotePort(client.getPort());
		record.setBssid(BSSID);
		record.setSsid(SSID);
		record.setPacket(packet);
		if (MyLocationManager.getNewestLocation() != null) {
			record.setLatitude(MyLocationManager.getNewestLocation()
					.getLatitude());
			record.setLongitude(MyLocationManager.getNewestLocation()
					.getLongitude());
			record.setAccuracy(MyLocationManager.getNewestLocation()
					.getAccuracy());
			record.setTimestampLocation(MyLocationManager.getNewestLocation()
					.getTime());
		} else {
			record.setLatitude(0.0);
			record.setLongitude(0.0);
			record.setAccuracy(Float.MAX_VALUE);
			record.setTimestampLocation(0);
		}
		return record;
	}

	/**
	 * Communicates with a client using the corresponding protocol
	 * implementation.
	 * 
	 * @param in
	 *            InputStream of the socket.
	 * @param out
	 *            OutputStream of the socket.
	 * @throws IOException
	 */
	protected void talkToClient(InputStream in, OutputStream out)
			throws IOException {
		Reader reader = new Reader(in);
		Writer writer = new Writer(out);
		Packet inputLine;
		List<Packet> outputLine;
		if (protocol.whoTalksFirst() == TALK_FIRST.SERVER) {
			outputLine = protocol.processMessage(null);
			writer.write(outputLine);
			for (Packet o : outputLine) {
				Logger.log(HoneyService.getContext(),
						createRecord(TYPE.SEND, o.toString()));
			}
		}
		while (!thread.isInterrupted() && (inputLine = reader.read()) != null) {
			outputLine = protocol.processMessage(inputLine);
			Logger.log(HoneyService.getContext(),
					createRecord(TYPE.RECEIVE, inputLine.toString()));
			if (outputLine != null) {
				writer.write(outputLine);
				for (Packet o : outputLine) {
					Logger.log(HoneyService.getContext(),
							createRecord(TYPE.SEND, o.toString()));
				}
			}
			if (protocol.isClosed()) {
				break;
			}
		}
	}
}
