/*
 * otr4j, the open source java otr library.
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */

package net.java.otr4j.session;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.security.KeyPair;
import java.security.PublicKey;
import java.util.Arrays;
import java.util.List;
import java.util.Vector;
import java.util.logging.Logger;
import javax.crypto.interfaces.DHPublicKey;

import net.java.otr4j.OtrEngineHost;
import net.java.otr4j.OtrEngineListener;
import net.java.otr4j.OtrException;
import net.java.otr4j.OtrPolicy;
import net.java.otr4j.crypto.OtrCryptoEngine;
import net.java.otr4j.crypto.OtrCryptoEngineImpl;
import net.java.otr4j.io.OtrInputStream;
import net.java.otr4j.io.OtrOutputStream;
import net.java.otr4j.io.SerializationConstants;
import net.java.otr4j.io.SerializationUtils;
import net.java.otr4j.io.messages.DataMessage;
import net.java.otr4j.io.messages.AbstractEncodedMessage;
import net.java.otr4j.io.messages.ErrorMessage;
import net.java.otr4j.io.messages.AbstractMessage;
import net.java.otr4j.io.messages.MysteriousT;
import net.java.otr4j.io.messages.PlainTextMessage;
import net.java.otr4j.io.messages.QueryMessage;

/**
 * 
 * @author George Politis
 */
public class SessionImpl implements Session {

	class TLV {
		public TLV(int type, byte[] value) {
			this.setType(type);
			this.setValue(value);
		}

		public void setType(int type) {
			this.type = type;
		}

		public int getType() {
			return type;
		}

		public void setValue(byte[] value) {
			this.value = value;
		}

		public byte[] getValue() {
			return value;
		}

		private int type;
		private byte[] value;
	}

	private SessionID sessionID;
	private OtrEngineHost host;
	private SessionStatus sessionStatus;
	private AuthContext authContext;
	private SessionKeys[][] sessionKeys;
	private Vector<byte[]> oldMacKeys;
	private static Logger logger = Logger
			.getLogger(SessionImpl.class.getName());

	public SessionImpl(SessionID sessionID, OtrEngineHost listener) {

		this.setSessionID(sessionID);
		this.setHost(listener);

		// client application calls OtrEngine.getSessionStatus()
		// -> create new session if it does not exist, end up here
		// -> setSessionStatus() fires statusChangedEvent
		// -> client application calls OtrEngine.getSessionStatus()
		this.sessionStatus = SessionStatus.PLAINTEXT;
	}

	private SessionKeys getEncryptionSessionKeys() {
		logger.finest("Getting encryption keys");
		return getSessionKeysByIndex(SessionKeys.Previous, SessionKeys.Current);
	}

	private SessionKeys getMostRecentSessionKeys() {
		logger.finest("Getting most recent keys.");
		return getSessionKeysByIndex(SessionKeys.Current, SessionKeys.Current);
	}

	private SessionKeys getSessionKeysByID(int localKeyID, int remoteKeyID) {
		logger
				.finest("Searching for session keys with (localKeyID, remoteKeyID) = ("
						+ localKeyID + "," + remoteKeyID + ")");

		for (int i = 0; i < getSessionKeys().length; i++) {
			for (int j = 0; j < getSessionKeys()[i].length; j++) {
				SessionKeys current = getSessionKeysByIndex(i, j);
				if (current.getLocalKeyID() == localKeyID
						&& current.getRemoteKeyID() == remoteKeyID) {
					logger.finest("Matching keys found.");
					return current;
				}
			}
		}

		return null;
	}

	private SessionKeys getSessionKeysByIndex(int localKeyIndex,
			int remoteKeyIndex) {
		if (getSessionKeys()[localKeyIndex][remoteKeyIndex] == null)
			getSessionKeys()[localKeyIndex][remoteKeyIndex] = new SessionKeysImpl(
					localKeyIndex, remoteKeyIndex);

		return getSessionKeys()[localKeyIndex][remoteKeyIndex];
	}

	private void rotateRemoteSessionKeys(DHPublicKey pubKey)
			throws OtrException {

		logger.finest("Rotating remote keys.");
		SessionKeys sess1 = getSessionKeysByIndex(SessionKeys.Current,
				SessionKeys.Previous);
		if (sess1.getIsUsedReceivingMACKey()) {
			logger
					.finest("Detected used Receiving MAC key. Adding to old MAC keys to reveal it.");
			getOldMacKeys().add(sess1.getReceivingMACKey());
		}

		SessionKeys sess2 = getSessionKeysByIndex(SessionKeys.Previous,
				SessionKeys.Previous);
		if (sess2.getIsUsedReceivingMACKey()) {
			logger
					.finest("Detected used Receiving MAC key. Adding to old MAC keys to reveal it.");
			getOldMacKeys().add(sess2.getReceivingMACKey());
		}

		SessionKeys sess3 = getSessionKeysByIndex(SessionKeys.Current,
				SessionKeys.Current);
		sess1
				.setRemoteDHPublicKey(sess3.getRemoteKey(), sess3
						.getRemoteKeyID());

		SessionKeys sess4 = getSessionKeysByIndex(SessionKeys.Previous,
				SessionKeys.Current);
		sess2
				.setRemoteDHPublicKey(sess4.getRemoteKey(), sess4
						.getRemoteKeyID());

		sess3.setRemoteDHPublicKey(pubKey, sess3.getRemoteKeyID() + 1);
		sess4.setRemoteDHPublicKey(pubKey, sess4.getRemoteKeyID() + 1);
	}

	private void rotateLocalSessionKeys() throws OtrException {

		logger.finest("Rotating local keys.");
		SessionKeys sess1 = getSessionKeysByIndex(SessionKeys.Previous,
				SessionKeys.Current);
		if (sess1.getIsUsedReceivingMACKey()) {
			logger
					.finest("Detected used Receiving MAC key. Adding to old MAC keys to reveal it.");
			getOldMacKeys().add(sess1.getReceivingMACKey());
		}

		SessionKeys sess2 = getSessionKeysByIndex(SessionKeys.Previous,
				SessionKeys.Previous);
		if (sess2.getIsUsedReceivingMACKey()) {
			logger
					.finest("Detected used Receiving MAC key. Adding to old MAC keys to reveal it.");
			getOldMacKeys().add(sess2.getReceivingMACKey());
		}

		SessionKeys sess3 = getSessionKeysByIndex(SessionKeys.Current,
				SessionKeys.Current);
		sess1.setLocalPair(sess3.getLocalPair(), sess3.getLocalKeyID());
		SessionKeys sess4 = getSessionKeysByIndex(SessionKeys.Current,
				SessionKeys.Previous);
		sess2.setLocalPair(sess4.getLocalPair(), sess4.getLocalKeyID());

		KeyPair newPair = new OtrCryptoEngineImpl().generateDHKeyPair();
		sess3.setLocalPair(newPair, sess3.getLocalKeyID() + 1);
		sess4.setLocalPair(newPair, sess4.getLocalKeyID() + 1);
	}

	private byte[] collectOldMacKeys() {
		logger.finest("Collecting old MAC keys to be revealed.");
		int len = 0;
		for (int i = 0; i < getOldMacKeys().size(); i++)
			len += getOldMacKeys().get(i).length;

		ByteBuffer buff = ByteBuffer.allocate(len);
		for (int i = 0; i < getOldMacKeys().size(); i++)
			buff.put(getOldMacKeys().get(i));

		getOldMacKeys().clear();
		return buff.array();
	}

	private void setSessionStatus(SessionStatus sessionStatus)
			throws OtrException {

		if (sessionStatus == this.sessionStatus)
			return;

		switch (sessionStatus) {
		case ENCRYPTED:
			AuthContext auth = this.getAuthContext();
			logger.finest("Setting most recent session keys from auth.");
			for (int i = 0; i < this.getSessionKeys()[0].length; i++) {
				SessionKeys current = getSessionKeysByIndex(0, i);
				current.setLocalPair(auth.getLocalDHKeyPair(), 1);
				current.setRemoteDHPublicKey(auth.getRemoteDHPublicKey(), 1);
				current.setS(auth.getS());
			}

			KeyPair nextDH = new OtrCryptoEngineImpl().generateDHKeyPair();
			for (int i = 0; i < this.getSessionKeys()[1].length; i++) {
				SessionKeys current = getSessionKeysByIndex(1, i);
				current.setRemoteDHPublicKey(auth.getRemoteDHPublicKey(), 1);
				current.setLocalPair(nextDH, 2);
			}

			this.setRemotePublicKey(auth.getRemoteLongTermPublicKey());

			auth.reset();
			break;
		}

		this.sessionStatus = sessionStatus;

		for (OtrEngineListener l : this.listeners)
			l.sessionStatusChanged(getSessionID());
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.java.otr4j.session.ISession#getSessionStatus()
	 */

	public SessionStatus getSessionStatus() {
		return sessionStatus;
	}

	private void setSessionID(SessionID sessionID) {
		this.sessionID = sessionID;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.java.otr4j.session.ISession#getSessionID()
	 */
	public SessionID getSessionID() {
		return sessionID;
	}

	private void setHost(OtrEngineHost host) {
		this.host = host;
	}

	private OtrEngineHost getHost() {
		return host;
	}

	private SessionKeys[][] getSessionKeys() {
		if (sessionKeys == null)
			sessionKeys = new SessionKeys[2][2];
		return sessionKeys;
	}

	private AuthContext getAuthContext() {
		if (authContext == null)
			authContext = new AuthContextImpl(this);
		return authContext;
	}

	private Vector<byte[]> getOldMacKeys() {
		if (oldMacKeys == null)
			oldMacKeys = new Vector<byte[]>();
		return oldMacKeys;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * net.java.otr4j.session.ISession#handleReceivingMessage(java.lang.String)
	 */
	public String transformReceiving(String msgText) throws OtrException {
		OtrPolicy policy = getSessionPolicy();
		if (!policy.getAllowV1() && !policy.getAllowV2()) {
			logger
					.finest("Policy does not allow neither V1 not V2, ignoring message.");
			return msgText;
		}

		AbstractMessage m;
		try {
			m = SerializationUtils.toMessage(msgText);
		} catch (IOException e) {
			throw new OtrException(e);
		}
		
		if (m == null)
			return msgText; // Propably null or empty.

		switch (m.messageType) {
		case AbstractEncodedMessage.MESSAGE_DATA:
			return handleDataMessage((DataMessage) m);
		case AbstractMessage.MESSAGE_ERROR:
			handleErrorMessage((ErrorMessage) m);
			return null;
		case AbstractMessage.MESSAGE_PLAINTEXT:
			return handlePlainTextMessage((PlainTextMessage) m);
		case AbstractMessage.MESSAGE_QUERY:
			handleQueryMessage((QueryMessage) m);
			return null;
		case AbstractEncodedMessage.MESSAGE_DH_COMMIT:
		case AbstractEncodedMessage.MESSAGE_DHKEY:
		case AbstractEncodedMessage.MESSAGE_REVEALSIG:
		case AbstractEncodedMessage.MESSAGE_SIGNATURE:
			AuthContext auth = this.getAuthContext();
			auth.handleReceivingMessage(m);

			if (auth.getIsSecure()) {
				this.setSessionStatus(SessionStatus.ENCRYPTED);
				logger.finest("Gone Secure.");
			}
			return null;
		default:
			throw new UnsupportedOperationException(
					"Received an uknown message type.");
		}
	}

	private void handleQueryMessage(QueryMessage queryMessage)
			throws OtrException {
		logger.finest(getSessionID().getAccountID()
				+ " received a query message from "
				+ getSessionID().getUserID() + " throught "
				+ getSessionID().getProtocolName() + ".");

		setSessionStatus(SessionStatus.PLAINTEXT);

		OtrPolicy policy = getSessionPolicy();
		if (queryMessage.versions.contains(2) && policy.getAllowV2()) {
			logger.finest("Query message with V2 support found.");
			getAuthContext().respondV2Auth();
		} else if (queryMessage.versions.contains(1) && policy.getAllowV1()) {
			throw new UnsupportedOperationException();
		}
	}

	private void handleErrorMessage(ErrorMessage errorMessage)
			throws OtrException {
		logger.finest(getSessionID().getAccountID()
				+ " received an error message from "
				+ getSessionID().getUserID() + " throught "
				+ getSessionID().getUserID() + ".");

		getHost().showError(this.getSessionID(), errorMessage.error);

		OtrPolicy policy = getSessionPolicy();
		if (policy.getErrorStartAKE()) {
			logger.finest("Error message starts AKE.");
			Vector<Integer> versions = new Vector<Integer>();
			if (policy.getAllowV1())
				versions.add(1);

			if (policy.getAllowV2())
				versions.add(2);

			logger.finest("Sending Query");
			injectMessage(new QueryMessage(versions));
		}
	}

	private String handleDataMessage(DataMessage data) throws OtrException {
		logger.finest(getSessionID().getAccountID()
				+ " received a data message from " + getSessionID().getUserID()
				+ ".");

		switch (this.getSessionStatus()) {
		case ENCRYPTED:
			logger
					.finest("Message state is ENCRYPTED. Trying to decrypt message.");

			// Find matching session keys.
			int senderKeyID = data.senderKeyID;
			int receipientKeyID = data.recipientKeyID;
			SessionKeys matchingKeys = this.getSessionKeysByID(receipientKeyID,
					senderKeyID);

			if (matchingKeys == null) {
				logger.finest("No matching keys found.");
				return null;
			}

			// Verify received MAC with a locally calculated MAC.
			logger
					.finest("Transforming T to byte[] to calculate it's HmacSHA1.");

			byte[] serializedT;
			try {
				serializedT = SerializationUtils.toByteArray(data.getT());
			} catch (IOException e) {
				throw new OtrException(e);
			}

			OtrCryptoEngine otrCryptoEngine = new OtrCryptoEngineImpl();

			byte[] computedMAC = otrCryptoEngine.sha1Hmac(serializedT,
					matchingKeys.getReceivingMACKey(),
					SerializationConstants.TYPE_LEN_MAC);

			if (!Arrays.equals(computedMAC, data.mac)) {
				logger.finest("MAC verification failed, ignoring message");
				return null;
			}

			logger.finest("Computed HmacSHA1 value matches sent one.");

			// Mark this MAC key as old to be revealed.
			matchingKeys.setIsUsedReceivingMACKey(true);

			matchingKeys.setReceivingCtr(data.ctr);

			byte[] dmc = otrCryptoEngine.aesDecrypt(matchingKeys
					.getReceivingAESKey(), matchingKeys.getReceivingCtr(),
					data.encryptedMessage);
			String decryptedMsgContent;
			try {
				// Expect bytes to be text encoded in UTF-8.
				decryptedMsgContent = new String(dmc, "UTF-8");
			} catch (UnsupportedEncodingException e) {
				throw new OtrException(e);
			}

			logger.finest("Decrypted message: \"" + decryptedMsgContent + "\"");

			// Rotate keys if necessary.
			SessionKeys mostRecent = this.getMostRecentSessionKeys();
			if (mostRecent.getLocalKeyID() == receipientKeyID)
				this.rotateLocalSessionKeys();

			if (mostRecent.getRemoteKeyID() == senderKeyID)
				this.rotateRemoteSessionKeys(data.nextDH);

			// Handle TLVs
			List<TLV> tlvs = null;
			int tlvIndex = decryptedMsgContent.indexOf((char) 0x0);
			if (tlvIndex > -1) {
				decryptedMsgContent = decryptedMsgContent
						.substring(0, tlvIndex);
				tlvIndex++;
				byte[] tlvsb = new byte[dmc.length - tlvIndex];
				System.arraycopy(dmc, tlvIndex, tlvsb, 0, tlvsb.length);

				tlvs = new Vector<TLV>();
				ByteArrayInputStream tin = new ByteArrayInputStream(tlvsb);
				while (tin.available() > 0) {
					int type;
					byte[] tdata;
					OtrInputStream eois = new OtrInputStream(tin);
					try {
						type = eois.readShort();
						tdata = eois.readTlvData();
						eois.close();
					} catch (IOException e) {
						throw new OtrException(e);
					}

					tlvs.add(new TLV(type, tdata));
				}
			}
			if (tlvs != null && tlvs.size() > 0) {
				for (TLV tlv : tlvs) {
					switch (tlv.getType()) {
					case 1:
						this.setSessionStatus(SessionStatus.FINISHED);
						return null;
					default:
						return decryptedMsgContent;
					}
				}
			}

			return decryptedMsgContent;

		case FINISHED:
		case PLAINTEXT:
			getHost().showWarning(this.getSessionID(),
					"Unreadable encrypted message was received.");

			injectMessage(new ErrorMessage(AbstractMessage.MESSAGE_ERROR,
					"You sent me an unreadable encrypted message.."));
			break;
		}

		return null;
	}

	public void injectMessage(AbstractMessage m) throws OtrException {
		String msg;
		try {
			msg = SerializationUtils.toString(m);
		} catch (IOException e) {
			throw new OtrException(e);
		}
		getHost().injectMessage(getSessionID(), msg);
	}

	private String handlePlainTextMessage(PlainTextMessage plainTextMessage)
			throws OtrException {
		logger.finest(getSessionID().getAccountID()
				+ " received a plaintext message from "
				+ getSessionID().getUserID() + " throught "
				+ getSessionID().getProtocolName() + ".");

		OtrPolicy policy = getSessionPolicy();
		List<Integer> versions = plainTextMessage.versions;
		if (versions == null || versions.size() < 1) {
			logger
					.finest("Received plaintext message without the whitespace tag.");
			switch (this.getSessionStatus()) {
			case ENCRYPTED:
			case FINISHED:
				// Display the message to the user, but warn him that the
				// message was received unencrypted.
				getHost().showWarning(this.getSessionID(),
						"The message was received unencrypted.");
				return plainTextMessage.cleanText;
			case PLAINTEXT:
				// Simply display the message to the user. If
				// REQUIRE_ENCRYPTION
				// is set, warn him that the message was received
				// unencrypted.
				if (policy.getRequireEncryption()) {
					getHost().showWarning(this.getSessionID(),
							"The message was received unencrypted.");
				}
				return plainTextMessage.cleanText;
			}
		} else {
			logger
					.finest("Received plaintext message with the whitespace tag.");
			switch (this.getSessionStatus()) {
			case ENCRYPTED:
			case FINISHED:
				// Remove the whitespace tag and display the message to the
				// user, but warn him that the message was received
				// unencrypted.
				getHost().showWarning(this.getSessionID(),
						"The message was received unencrypted.");
			case PLAINTEXT:
				// Remove the whitespace tag and display the message to the
				// user. If REQUIRE_ENCRYPTION is set, warn him that the
				// message
				// was received unencrypted.
				if (policy.getRequireEncryption())
					getHost().showWarning(this.getSessionID(),
							"The message was received unencrypted.");
			}

			if (policy.getWhitespaceStartAKE()) {
				logger.finest("WHITESPACE_START_AKE is set");

				if (plainTextMessage.versions.contains(2)
						&& policy.getAllowV2()) {
					logger.finest("V2 tag found.");
					getAuthContext().respondV2Auth();
				} else if (plainTextMessage.versions.contains(1)
						&& policy.getAllowV1()) {
					throw new UnsupportedOperationException();
				}
			}
		}

		return plainTextMessage.cleanText;
	}

	// Retransmit last sent message. Spec document does not mention where or
	// when that should happen, must check libotr code.
	private String lastSentMessage;

	public String transformSending(String msgText, List<TLV> tlvs)
			throws OtrException {

		switch (this.getSessionStatus()) {
		case PLAINTEXT:
			if (getSessionPolicy().getRequireEncryption()) {
				this.lastSentMessage = msgText;
				this.startSession();
			} else
				// TODO this does not precisly behave according to
				// specification.
				return msgText;
		case ENCRYPTED:
			this.lastSentMessage = msgText;
			logger.finest(getSessionID().getAccountID()
					+ " sends an encrypted message to "
					+ getSessionID().getUserID() + " throught "
					+ getSessionID().getProtocolName() + ".");

			// Get encryption keys.
			SessionKeys encryptionKeys = this.getEncryptionSessionKeys();
			int senderKeyID = encryptionKeys.getLocalKeyID();
			int receipientKeyID = encryptionKeys.getRemoteKeyID();

			// Increment CTR.
			encryptionKeys.incrementSendingCtr();
			byte[] ctr = encryptionKeys.getSendingCtr();

			ByteArrayOutputStream out = new ByteArrayOutputStream();
			if (msgText != null && msgText.length() > 0)
				try {
					out.write(msgText.getBytes("UTF8"));
				} catch (IOException e) {
					throw new OtrException(e);
				}

			// Append tlvs
			if (tlvs != null && tlvs.size() > 0) {
				out.write((byte) 0x00);

				OtrOutputStream eoos = new OtrOutputStream(out);
				for (TLV tlv : tlvs) {
					try {
						eoos.writeShort(tlv.type);
						eoos.writeTlvData(tlv.value);
					} catch (IOException e) {
						throw new OtrException(e);
					}
				}
			}

			OtrCryptoEngine otrCryptoEngine = new OtrCryptoEngineImpl();

			byte[] data = out.toByteArray();
			// Encrypt message.
			logger
					.finest("Encrypting message with keyids (localKeyID, remoteKeyID) = ("
							+ senderKeyID + ", " + receipientKeyID + ")");
			byte[] encryptedMsg = otrCryptoEngine.aesEncrypt(encryptionKeys
					.getSendingAESKey(), ctr, data);

			// Get most recent keys to get the next D-H public key.
			SessionKeys mostRecentKeys = this.getMostRecentSessionKeys();
			DHPublicKey nextDH = (DHPublicKey) mostRecentKeys.getLocalPair()
					.getPublic();

			// Calculate T.
			MysteriousT t = new MysteriousT(2, 0, senderKeyID, receipientKeyID,
					nextDH, ctr, encryptedMsg);

			// Calculate T hash.
			byte[] sendingMACKey = encryptionKeys.getSendingMACKey();

			logger
					.finest("Transforming T to byte[] to calculate it's HmacSHA1.");
			byte[] serializedT;
			try {
				serializedT = SerializationUtils.toByteArray(t);
			} catch (IOException e) {
				throw new OtrException(e);
			}

			byte[] mac = otrCryptoEngine.sha1Hmac(serializedT, sendingMACKey,
					SerializationConstants.TYPE_LEN_MAC);

			// Get old MAC keys to be revealed.
			byte[] oldKeys = this.collectOldMacKeys();
			DataMessage m = new DataMessage(t, mac, oldKeys);

			try {
				return SerializationUtils.toString(m);
			} catch (IOException e) {
				throw new OtrException(e);
			}
		case FINISHED:
			this.lastSentMessage = msgText;
			getHost()
					.showError(
							sessionID,
							"Your message to "
									+ sessionID.getUserID()
									+ " was not sent.  Either end your private conversation, or restart it.");
			return null;
		default:
			logger.finest("Uknown message state, not processing.");
			return msgText;
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.java.otr4j.session.ISession#startSession()
	 */
	public void startSession() throws OtrException {
		if (this.getSessionStatus() == SessionStatus.ENCRYPTED)
			return;

		if (!getSessionPolicy().getAllowV2())
			throw new UnsupportedOperationException();

		this.getAuthContext().startV2Auth();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.java.otr4j.session.ISession#endSession()
	 */
	public void endSession() throws OtrException {
		SessionStatus status = this.getSessionStatus();
		switch (status) {
		case ENCRYPTED:
			Vector<TLV> tlvs = new Vector<TLV>();
			tlvs.add(new TLV(1, null));

			String msg = this.transformSending(null, tlvs);
			getHost().injectMessage(getSessionID(), msg);
			this.setSessionStatus(SessionStatus.PLAINTEXT);
			break;
		case FINISHED:
			this.setSessionStatus(SessionStatus.PLAINTEXT);
			break;
		case PLAINTEXT:
			return;
		}

	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.java.otr4j.session.ISession#refreshSession()
	 */
	public void refreshSession() throws OtrException {
		this.endSession();
		this.startSession();
	}

	private PublicKey remotePublicKey;

	private void setRemotePublicKey(PublicKey pubKey) {
		this.remotePublicKey = pubKey;
	}

	public PublicKey getRemotePublicKey() {
		return remotePublicKey;
	}

	private List<OtrEngineListener> listeners = new Vector<OtrEngineListener>();

	public void addOtrEngineListener(OtrEngineListener l) {
		synchronized (listeners) {
			if (!listeners.contains(l))
				listeners.add(l);
		}
	}

	public void removeOtrEngineListener(OtrEngineListener l) {
		synchronized (listeners) {
			listeners.remove(l);
		}
	}

	public OtrPolicy getSessionPolicy() {
		return getHost().getSessionPolicy(getSessionID());
	}

	public KeyPair getLocalKeyPair() {
		return getHost().getKeyPair(this.getSessionID());
	}
}
