/*
 * Jitsi, the OpenSource Java VoIP and Instant Messaging client.
 *
 * Copyright @ 2015 Atlassian Pty Ltd
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.java.sip.communicator.plugin.otr;

import java.net.*;
import java.security.*;
import java.util.*;
import java.util.concurrent.*;

import net.java.otr4j.*;
import net.java.otr4j.crypto.*;
import net.java.otr4j.session.*;
import net.java.sip.communicator.plugin.otr.OtrContactManager.OtrContact;
import net.java.sip.communicator.plugin.otr.authdialog.*;
import net.java.sip.communicator.service.browserlauncher.*;
import net.java.sip.communicator.service.contactlist.*;
import net.java.sip.communicator.service.gui.*;
import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.util.*;

import org.osgi.framework.*;

/**
 *
 * @author George Politis
 * @author Lyubomir Marinov
 * @author Pawel Domas
 * @author Marin Dzhigarov
 * @author Danny van Heumen
 */
public class ScOtrEngineImpl
    implements ScOtrEngine,
               ChatLinkClickedListener,
               ServiceListener
{
    private class ScOtrEngineHost
        implements OtrEngineHost
    {
        @Override
        public KeyPair getLocalKeyPair(SessionID sessionID)
        {
            AccountID accountID =
                OtrActivator.getAccountIDByUID(sessionID.getAccountID());
            KeyPair keyPair =
                OtrActivator.scOtrKeyManager.loadKeyPair(accountID);
            if (keyPair == null)
                OtrActivator.scOtrKeyManager.generateKeyPair(accountID);

            return OtrActivator.scOtrKeyManager.loadKeyPair(accountID);
        }

        @Override
        public OtrPolicy getSessionPolicy(SessionID sessionID)
        {
            return getContactPolicy(getOtrContact(sessionID).contact);
        }

        @Override
        public void injectMessage(SessionID sessionID, String messageText)
            throws OtrException
        {
            OtrContact otrContact = getOtrContact(sessionID);
            Contact contact = otrContact.contact;
            ContactResource resource = null;

            if (contact.supportResources())
            {
                Collection<ContactResource> resources = contact.getResources();
                if (resources != null)
                {
                    for (ContactResource r : resources)
                    {
                        if (r.equals(otrContact.resource))
                        {
                            resource = r;
                            break;
                        }
                    }
                }
            }

            OperationSetBasicInstantMessaging imOpSet
                = contact
                    .getProtocolProvider()
                        .getOperationSet(
                                OperationSetBasicInstantMessaging.class);

            // This is a dirty way of detecting whether the injected message
            // contains HTML markup. If this is the case then we should create
            // the message with the appropriate content type so that the remote
            // party can properly display the HTML.
            // When otr4j injects QueryMessages it calls
            // OtrEngineHost.getFallbackMessage() which is currently the only
            // host method that uses HTML so we can simply check if the injected
            // message contains the string that getFallbackMessage() returns.
            String otrHtmlFallbackMessage
                = "<a href=\"http://en.wikipedia.org/wiki/Off-the-Record_Messaging\">";
            String contentType
                = messageText.contains(otrHtmlFallbackMessage)
                    ? OperationSetBasicInstantMessaging.HTML_MIME_TYPE
                    : OperationSetBasicInstantMessaging.DEFAULT_MIME_TYPE;
            Message message
                = imOpSet.createMessage(
                        messageText,
                        contentType,
                        OperationSetBasicInstantMessaging.DEFAULT_MIME_ENCODING,
                        null);

            injectedMessageUIDs.add(message.getMessageUID());
            try
            {
                imOpSet.sendInstantMessage(contact, resource, message);
            }
            catch (OperationFailedException e)
            {
                throw new OtrException(e);
            }
        }

        @Override
        public void showError(SessionID sessionID, String err)
        {
            ScOtrEngineImpl.this.showError(sessionID, err);
        }

        public void showWarning(SessionID sessionID, String warn)
        {
            OtrContact otrContact = getOtrContact(sessionID);
            if (otrContact == null)
                return;

            Contact contact = otrContact.contact;
            OtrActivator.uiService.getChat(contact).addMessage(
                contact.getDisplayName(), new Date(),
                Chat.SYSTEM_MESSAGE, warn,
                OperationSetBasicInstantMessaging.DEFAULT_MIME_TYPE);
        }

        @Override
        public void unreadableMessageReceived(SessionID sessionID)
            throws OtrException
        {
            OtrContact otrContact = getOtrContact(sessionID);
            String resourceName = otrContact.resource != null ?
                "/" + otrContact.resource.getResourceName() : "";

            Contact contact = otrContact.contact;
            String error =
                OtrActivator.resourceService.getI18NString(
                    "plugin.otr.activator.unreadablemsgreceived",
                    new String[]
                        {contact.getDisplayName() + resourceName});
            OtrActivator.uiService.getChat(contact).addMessage(
                contact.getDisplayName(), new Date(),
                Chat.ERROR_MESSAGE, error,
                OperationSetBasicInstantMessaging.DEFAULT_MIME_TYPE);
        }

        @Override
        public void unencryptedMessageReceived(SessionID sessionID, String msg)
            throws OtrException
        {
            OtrContact otrContact = getOtrContact(sessionID);
            if (otrContact == null)
                return;

            Contact contact = otrContact.contact;
            String warn =
                OtrActivator.resourceService.getI18NString(
                    "plugin.otr.activator.unencryptedmsgreceived");
            OtrActivator.uiService.getChat(contact).addMessage(
                contact.getDisplayName(), new Date(),
                Chat.SYSTEM_MESSAGE, warn,
                OperationSetBasicInstantMessaging.DEFAULT_MIME_TYPE);
        }

        @Override
        public void smpError(SessionID sessionID, int tlvType, boolean cheated)
            throws OtrException
        {
            OtrContact otrContact = getOtrContact(sessionID);
            if (otrContact == null)
                return;

            Contact contact = otrContact.contact;
            logger.debug("SMP error occurred"
                        + ". Contact: " + contact.getDisplayName()
                        + ". TLV type: " + tlvType
                        + ". Cheated: " + cheated);

            String error =
                OtrActivator.resourceService.getI18NString(
                    "plugin.otr.activator.smperror");
            OtrActivator.uiService.getChat(contact).addMessage(
                contact.getDisplayName(), new Date(),
                Chat.ERROR_MESSAGE, error,
                OperationSetBasicInstantMessaging.DEFAULT_MIME_TYPE);

            SmpProgressDialog progressDialog = progressDialogMap.get(otrContact);
            if (progressDialog == null)
            {
                progressDialog = new SmpProgressDialog(contact);
                progressDialogMap.put(otrContact, progressDialog);
            }

            progressDialog.setProgressFail();
            progressDialog.setVisible(true);
        }

        @Override
        public void smpAborted(SessionID sessionID) throws OtrException
        {
            OtrContact otrContact = getOtrContact(sessionID);
            if (otrContact == null)
                return;

            Contact contact = otrContact.contact;
            Session session = otrEngine.getSession(sessionID);
            if (session.isSmpInProgress())
            {
                String warn =
                    OtrActivator.resourceService.getI18NString(
                        "plugin.otr.activator.smpaborted",
                        new String[] {contact.getDisplayName()});
                OtrActivator.uiService.getChat(contact).addMessage(
                    contact.getDisplayName(), new Date(),
                    Chat.SYSTEM_MESSAGE, warn,
                    OperationSetBasicInstantMessaging.DEFAULT_MIME_TYPE);

                SmpProgressDialog progressDialog =
                    progressDialogMap.get(otrContact);
                if (progressDialog == null)
                {
                    progressDialog = new SmpProgressDialog(contact);
                    progressDialogMap.put(otrContact, progressDialog);
                }

                progressDialog.setProgressFail();
                progressDialog.setVisible(true);
            }
        }

        @Override
        public void finishedSessionMessage(SessionID sessionID, String msgText)
            throws OtrException
        {
            OtrContact otrContact = getOtrContact(sessionID);
            if (otrContact == null)
                return;

            String resourceName = otrContact.resource != null ?
                "/" + otrContact.resource.getResourceName() : "";
            Contact contact = otrContact.contact;
            String error =
                OtrActivator.resourceService.getI18NString(
                    "plugin.otr.activator.sessionfinishederror",
                    new String[]
                        {msgText, contact.getDisplayName() + resourceName});
            OtrActivator.uiService.getChat(contact).addMessage(
                contact.getDisplayName(), new Date(),
                Chat.ERROR_MESSAGE, error,
                OperationSetBasicInstantMessaging.DEFAULT_MIME_TYPE);
        }

        @Override
        public void requireEncryptedMessage(SessionID sessionID, String msgText)
            throws OtrException
        {
            OtrContact otrContact = getOtrContact(sessionID);
            if (otrContact == null)
                return;

                Contact contact = otrContact.contact;
            String error =
                OtrActivator.resourceService.getI18NString(
                    "plugin.otr.activator.requireencryption",
                    new String[]
                        {msgText});
            OtrActivator.uiService.getChat(contact).addMessage(
                contact.getDisplayName(), new Date(),
                Chat.ERROR_MESSAGE, error,
                OperationSetBasicInstantMessaging.DEFAULT_MIME_TYPE);
        }

        @Override
        public byte[] getLocalFingerprintRaw(SessionID sessionID)
        {
            AccountID accountID =
                OtrActivator.getAccountIDByUID(sessionID.getAccountID());
            return
                OtrActivator.scOtrKeyManager.getLocalFingerprintRaw(accountID);
        }

        @Override
        public void askForSecret(
            SessionID sessionID, InstanceTag receiverTag, String question)
        {
            OtrContact otrContact = getOtrContact(sessionID);
            if (otrContact == null)
                return;

            Contact contact = otrContact.contact;
            SmpAuthenticateBuddyDialog dialog =
                new SmpAuthenticateBuddyDialog(
                    otrContact, receiverTag, question);
            dialog.setVisible(true);

            SmpProgressDialog progressDialog = progressDialogMap.get(otrContact);
            if (progressDialog == null)
            {
                progressDialog = new SmpProgressDialog(contact);
                progressDialogMap.put(otrContact, progressDialog);
            }

            progressDialog.init();
            progressDialog.setVisible(true);
        }

        @Override
        public void verify(
            SessionID sessionID, String fingerprint, boolean approved)
        {
            OtrContact otrContact = getOtrContact(sessionID);
            if (otrContact == null)
                return;

            Contact contact = otrContact.contact;
            OtrActivator.scOtrKeyManager.verify(otrContact, fingerprint);

            SmpProgressDialog progressDialog = progressDialogMap.get(otrContact);
            if (progressDialog == null)
            {
                progressDialog = new SmpProgressDialog(contact);
                progressDialogMap.put(otrContact, progressDialog);
            }

            progressDialog.setProgressSuccess();
            progressDialog.setVisible(true);
        }

        @Override
        public void unverify(SessionID sessionID, String fingerprint)
        {
            OtrContact otrContact = getOtrContact(sessionID);
            if (otrContact == null)
                return;

            Contact contact = otrContact.contact;
            OtrActivator.scOtrKeyManager.unverify(otrContact, fingerprint);

            SmpProgressDialog progressDialog = progressDialogMap.get(otrContact);
            if (progressDialog == null)
            {
                progressDialog = new SmpProgressDialog(contact);
                progressDialogMap.put(otrContact, progressDialog);
            }

            progressDialog.setProgressFail();
            progressDialog.setVisible(true);
        }

        @Override
        public String getReplyForUnreadableMessage(SessionID sessionID)
        {
            AccountID accountID =
                OtrActivator.getAccountIDByUID(sessionID.getAccountID());

            return OtrActivator.resourceService.getI18NString(
                "plugin.otr.activator.unreadablemsgreply",
                new String[] {accountID.getDisplayName(),
                              accountID.getDisplayName()});
        }

        @Override
        public String getFallbackMessage(SessionID sessionID)
        {
            AccountID accountID =
                OtrActivator.getAccountIDByUID(sessionID.getAccountID());

            return OtrActivator.resourceService.getI18NString(
                "plugin.otr.activator.fallbackmessage",
                new String[] {accountID.getDisplayName()});
        }

        @Override
        public void multipleInstancesDetected(SessionID sessionID)
        {
            OtrContact otrContact = getOtrContact(sessionID);
            if (otrContact == null)
                return;

            String resourceName = otrContact.resource != null ?
                "/" + otrContact.resource.getResourceName() : "";
            Contact contact = otrContact.contact;
            String message =
                OtrActivator.resourceService.getI18NString(
                    "plugin.otr.activator.multipleinstancesdetected",
                    new String[]
                        {contact.getDisplayName() + resourceName});
            OtrActivator.uiService.getChat(contact).addMessage(
                contact.getDisplayName(),
                new Date(), Chat.SYSTEM_MESSAGE,
                message,
                OperationSetBasicInstantMessaging.HTML_MIME_TYPE);
        }

        @Override
        public void messageFromAnotherInstanceReceived(SessionID sessionID)
        {
            OtrContact otrContact = getOtrContact(sessionID);
            if (otrContact == null)
                return;

            String resourceName = otrContact.resource != null ?
                "/" + otrContact.resource.getResourceName() : "";
            Contact contact = otrContact.contact;
            String message =
                OtrActivator.resourceService.getI18NString(
                    "plugin.otr.activator.msgfromanotherinstance",
                    new String[]
                        {contact.getDisplayName() + resourceName});
            OtrActivator.uiService.getChat(contact).addMessage(
                contact.getDisplayName(),
                new Date(), Chat.SYSTEM_MESSAGE,
                message,
                OperationSetBasicInstantMessaging.HTML_MIME_TYPE);
        }

        /**
         * Provide fragmenter instructions according to the Instant Messaging
         * transport channel of the contact's protocol.
         */
        @Override
        public FragmenterInstructions getFragmenterInstructions(
            final SessionID sessionID)
        {
            final OtrContact otrContact = getOtrContact(sessionID);
            final OperationSetBasicInstantMessagingTransport transport =
                otrContact.contact.getProtocolProvider().getOperationSet(
                    OperationSetBasicInstantMessagingTransport.class);
            if (transport == null)
            {
                // There is no operation set for querying transport parameters.
                // Assuming transport capabilities are unlimited.
                if (logger.isDebugEnabled())
                {
                    logger.debug("No implementation of "
                        + "BasicInstantMessagingTransport available. Assuming "
                        + "OTR defaults for OTR fragmentation instructions.");
                }
                return null;
            }
            int messageSize = transport.getMaxMessageSize(otrContact.contact);
            if (messageSize
                == OperationSetBasicInstantMessagingTransport.UNLIMITED)
            {
                messageSize = FragmenterInstructions.UNLIMITED;
            }
            int numberOfMessages =
                transport.getMaxNumberOfMessages(otrContact.contact);
            if (numberOfMessages
                == OperationSetBasicInstantMessagingTransport.UNLIMITED)
            {
                numberOfMessages = FragmenterInstructions.UNLIMITED;
            }
            if (logger.isDebugEnabled())
            {
                logger.debug("OTR fragmentation instructions for sending a "
                    + "message to " + otrContact.contact.getDisplayName()
                    + " (" + otrContact.contact.getAddress()
                    + "). Maximum number of " + "messages: " + numberOfMessages
                    + ", maximum message size: " + messageSize);
            }
            return new FragmenterInstructions(numberOfMessages, messageSize);
        }
    }

    /**
     * The max timeout period elapsed prior to establishing a TIMED_OUT session.
     */
    private static final int SESSION_TIMEOUT =
        OtrActivator.configService.getInt(
            "net.java.sip.communicator.plugin.otr.SESSION_STATUS_TIMEOUT",
            30000);

    /**
     * Manages the scheduling of TimerTasks that are used to set Contact's
     * ScSessionStatus (to TIMED_OUT) after a period of time.
     */
    private ScSessionStatusScheduler scheduler = new ScSessionStatusScheduler();

    /**
     * This mapping is used for taking care of keeping SessionStatus and
     * ScSessionStatus in sync for every Session object.
     */
    private Map<SessionID, ScSessionStatus> scSessionStatusMap =
        new ConcurrentHashMap<SessionID, ScSessionStatus>();

    private static final Map<ScSessionID, OtrContact> contactsMap =
        new Hashtable<ScSessionID, OtrContact>();

    private static final Map<OtrContact, SmpProgressDialog> progressDialogMap =
        new ConcurrentHashMap<OtrContact, SmpProgressDialog>();

    public static OtrContact getOtrContact(SessionID sessionID)
    {
        return contactsMap.get(new ScSessionID(sessionID));
    }

    /**
     * Returns the <tt>ScSessionID</tt> for given <tt>UUID</tt>.
     * @param guid the <tt>UUID</tt> identifying <tt>ScSessionID</tt>.
     * @return the <tt>ScSessionID</tt> for given <tt>UUID</tt> or <tt>null</tt>
     *         if no matching session found.
     */
    public static ScSessionID getScSessionForGuid(UUID guid)
    {
        for(ScSessionID scSessionID : contactsMap.keySet())
        {
            if(scSessionID.getGUID().equals(guid))
            {
                return scSessionID;
            }
        }
        return null;
    }

    public static SessionID getSessionID(OtrContact otrContact)
    {
        ProtocolProviderService pps = otrContact.contact.getProtocolProvider();
        String resourceName = otrContact.resource != null ?
            "/" + otrContact.resource.getResourceName() : "";
        SessionID sessionID
            = new SessionID(
                    pps.getAccountID().getAccountUniqueID(),
                    otrContact.contact.getAddress() + resourceName,
                    pps.getProtocolName());

        synchronized (contactsMap)
        {
            if(contactsMap.containsKey(new ScSessionID(sessionID)))
                return sessionID;

            ScSessionID scSessionID = new ScSessionID(sessionID);

            contactsMap.put(scSessionID, otrContact);
        }

        return sessionID;
    }

    private final OtrConfigurator configurator = new OtrConfigurator();

    private final List<String> injectedMessageUIDs = new Vector<String>();

    private final List<ScOtrEngineListener> listeners =
        new Vector<ScOtrEngineListener>();

    /**
     * The logger
     */
    private final Logger logger = Logger.getLogger(ScOtrEngineImpl.class);

    private final OtrEngineHost otrEngineHost = new ScOtrEngineHost();

    private final OtrSessionManager otrEngine;

    public ScOtrEngineImpl()
    {
        otrEngine = new OtrSessionManagerImpl(otrEngineHost);

        // Clears the map after previous instance
        // This is required because of OSGi restarts in the same VM on Android
        contactsMap.clear();
        scSessionStatusMap.clear();

        this.otrEngine.addOtrEngineListener(new OtrEngineListener()
        {
            @Override
            public void sessionStatusChanged(SessionID sessionID)
            {
                OtrContact otrContact = getOtrContact(sessionID);
                if (otrContact == null)
                    return;

                String resourceName = otrContact.resource != null ?
                    "/" + otrContact.resource.getResourceName() : "";
                Contact contact = otrContact.contact;
                // Cancels any scheduled tasks that will change the
                // ScSessionStatus for this Contact
                scheduler.cancel(otrContact);

                ScSessionStatus scSessionStatus = getSessionStatus(otrContact);
                String message = "";
                final Session session = otrEngine.getSession(sessionID);
                switch (session.getSessionStatus())
                {
                case ENCRYPTED:
                    scSessionStatus = ScSessionStatus.ENCRYPTED;
                    scSessionStatusMap.put(sessionID, scSessionStatus);
                    PublicKey remotePubKey = session.getRemotePublicKey();

                    String remoteFingerprint = null;
                    try
                    {
                        remoteFingerprint =
                            new OtrCryptoEngineImpl().
                                getFingerprint(remotePubKey);
                    }
                    catch (OtrCryptoException e)
                    {
                        logger.debug(
                            "Could not get the fingerprint from the "
                            + "public key of contact: " + contact);
                    }

                    List<String> allFingerprintsOfContact =
                        OtrActivator.scOtrKeyManager.
                            getAllRemoteFingerprints(contact);
                    if (allFingerprintsOfContact != null)
                    {
                        if (!allFingerprintsOfContact.contains(
                                remoteFingerprint))
                        {
                            OtrActivator.scOtrKeyManager.saveFingerprint(
                                contact, remoteFingerprint);
                        }
                    }

                    if (!OtrActivator.scOtrKeyManager.isVerified(
                            contact, remoteFingerprint))
                    {
                        OtrActivator.scOtrKeyManager.unverify(
                            otrContact, remoteFingerprint);
                        UUID sessionGuid = null;
                        for(ScSessionID scSessionID : contactsMap.keySet())
                        {
                            if(scSessionID.getSessionID().equals(sessionID))
                            {
                                sessionGuid = scSessionID.getGUID();
                                break;
                            }
                        }

                        OtrActivator.uiService.getChat(contact)
                            .addChatLinkClickedListener(ScOtrEngineImpl.this);

                        String unverifiedSessionWarning
                            = OtrActivator.resourceService.getI18NString(
                                "plugin.otr.activator.unverifiedsessionwarning",
                                new String[]
                                {
                                    contact.getDisplayName() + resourceName,
                                    this.getClass().getName(),
                                    "AUTHENTIFICATION",
                                    sessionGuid.toString()
                                });
                        OtrActivator.uiService.getChat(contact).addMessage(
                            contact.getDisplayName(),
                            new Date(), Chat.SYSTEM_MESSAGE,
                            unverifiedSessionWarning,
                            OperationSetBasicInstantMessaging.HTML_MIME_TYPE);

                    }

                    // show info whether history is on or off
                    String otrAndHistoryMessage;
                    if(!OtrActivator.getMessageHistoryService()
                        .isHistoryLoggingEnabled() || 
                        !isHistoryLoggingEnabled(contact))
                    {
                        otrAndHistoryMessage =
                            OtrActivator.resourceService.getI18NString(
                                "plugin.otr.activator.historyoff",
                                new String[]{
                                    OtrActivator.resourceService
                                        .getSettingsString(
                                            "service.gui.APPLICATION_NAME"),
                                    this.getClass().getName(),
                                    "showHistoryPopupMenu"
                                });
                    }
                    else
                    {
                        otrAndHistoryMessage =
                            OtrActivator.resourceService.getI18NString(
                                "plugin.otr.activator.historyon",
                                new String[]{
                                    OtrActivator.resourceService
                                        .getSettingsString(
                                            "service.gui.APPLICATION_NAME"),
                                    this.getClass().getName(),
                                    "showHistoryPopupMenu"
                                });
                    }
                    OtrActivator.uiService.getChat(contact).addMessage(
                        contact.getDisplayName(),
                        new Date(), Chat.SYSTEM_MESSAGE,
                        otrAndHistoryMessage,
                        OperationSetBasicInstantMessaging.HTML_MIME_TYPE);

                    message =
                        OtrActivator.resourceService.getI18NString(
                            "plugin.otr.activator.multipleinstancesdetected",
                            new String[]
                                {contact.getDisplayName()});

                    if (contact.supportResources()
                        && contact.getResources() != null
                        && contact.getResources().size() > 1)
                        OtrActivator.uiService.getChat(contact).addMessage(
                            contact.getDisplayName(),
                            new Date(), Chat.SYSTEM_MESSAGE,
                            message,
                            OperationSetBasicInstantMessaging.DEFAULT_MIME_TYPE);

                    message
                        = OtrActivator.resourceService.getI18NString(
                                OtrActivator.scOtrKeyManager.isVerified(
                                    contact, remoteFingerprint)
                                    ? "plugin.otr.activator.sessionstared"
                                    : "plugin.otr.activator.unverifiedsessionstared",
                                new String[]
                                    {contact.getDisplayName() + resourceName});

                    break;
                case FINISHED:
                    scSessionStatus = ScSessionStatus.FINISHED;
                    scSessionStatusMap.put(sessionID, scSessionStatus);
                    message =
                        OtrActivator.resourceService.getI18NString(
                            "plugin.otr.activator.sessionfinished",
                            new String[]
                                {contact.getDisplayName() + resourceName});
                    break;
                case PLAINTEXT:
                    scSessionStatus = ScSessionStatus.PLAINTEXT;
                    scSessionStatusMap.put(sessionID, scSessionStatus);
                    message =
                        OtrActivator.resourceService.getI18NString(
                            "plugin.otr.activator.sessionlost", new String[]
                                {contact.getDisplayName() + resourceName});
                    break;
                }

                OtrActivator.uiService.getChat(contact).addMessage(
                    contact.getDisplayName(), new Date(),
                    Chat.SYSTEM_MESSAGE, message,
                    OperationSetBasicInstantMessaging.HTML_MIME_TYPE);

                for (ScOtrEngineListener l : getListeners())
                    l.sessionStatusChanged(otrContact);
            }

            @Override
            public void multipleInstancesDetected(SessionID sessionID)
            {
                OtrContact otrContact = getOtrContact(sessionID);
                if (otrContact == null)
                    return;

                for (ScOtrEngineListener l : getListeners())
                    l.multipleInstancesDetected(otrContact);
            }

            @Override
            public void outgoingSessionChanged(SessionID sessionID)
            {
                OtrContact otrContact = getOtrContact(sessionID);
                if (otrContact == null)
                    return;

                for (ScOtrEngineListener l : getListeners())
                    l.outgoingSessionChanged(otrContact);
            }
        });
    }

    /**
     * Checks whether history is enabled for the metacontact containing
     * the <tt>contact</tt>.
     * @param contact the contact to check.
     * @return whether chat logging is enabled while chatting
     * with <tt>contact</tt>.
     */
    private boolean isHistoryLoggingEnabled(Contact contact)
    {
        MetaContact metaContact = OtrActivator
            .getContactListService().findMetaContactByContact(contact);
        if(metaContact != null)
            return OtrActivator.getMessageHistoryService()
                .isHistoryLoggingEnabled(metaContact.getMetaUID());
        else
            return true;
    }

    @Override
    public void addListener(ScOtrEngineListener l)
    {
        synchronized (listeners)
        {
            if (!listeners.contains(l))
                listeners.add(l);
        }
    }

    @Override
    public void chatLinkClicked(URI url)
    {
        String action = url.getPath();
        if(action.equals("/AUTHENTIFICATION"))
        {
            UUID guid = UUID.fromString(url.getQuery());

            if(guid == null)
                throw new RuntimeException(
                        "No UUID found in OTR authenticate URL");

            // Looks for registered action handler
            OtrActionHandler actionHandler
                    = ServiceUtils.getService(
                            OtrActivator.bundleContext,
                            OtrActionHandler.class);

            if(actionHandler != null)
            {
                actionHandler.onAuthenticateLinkClicked(guid);
            }
            else
            {
                logger.error("No OtrActionHandler registered");
            }
        }
    }

    @Override
    public void endSession(OtrContact otrContact)
    {
        SessionID sessionID = getSessionID(otrContact);
        try
        {
            setSessionStatus(otrContact, ScSessionStatus.PLAINTEXT);

            otrEngine.getSession(sessionID).endSession();
        }
        catch (OtrException e)
        {
            showError(sessionID, e.getMessage());
        }
    }

    @Override
    public OtrPolicy getContactPolicy(Contact contact)
    {
        ProtocolProviderService pps = contact.getProtocolProvider();
        SessionID sessionID
            = new SessionID(
                pps.getAccountID().getAccountUniqueID(),
                contact.getAddress(),
                pps.getProtocolName());
        int policy =
            this.configurator.getPropertyInt(sessionID + "contact_policy",
                -1);
        if (policy < 0)
            return getGlobalPolicy();
        else
            return new OtrPolicyImpl(policy);
    }

    @Override
    public OtrPolicy getGlobalPolicy()
    {
        /*
         * SEND_WHITESPACE_TAG bit will be lowered until we stabilize the OTR.
         */
        int defaultScOtrPolicy =
            OtrPolicy.OTRL_POLICY_DEFAULT & ~OtrPolicy.SEND_WHITESPACE_TAG;
        return new OtrPolicyImpl(this.configurator.getPropertyInt(
            "GLOBAL_POLICY", defaultScOtrPolicy));
    }

    /**
     * Gets a copy of the list of <tt>ScOtrEngineListener</tt>s registered with
     * this instance which may safely be iterated without the risk of a
     * <tt>ConcurrentModificationException</tt>.
     *
     * @return a copy of the list of <tt>ScOtrEngineListener<tt>s registered
     * with this instance which may safely be iterated without the risk of a
     * <tt>ConcurrentModificationException</tt>
     */
    private ScOtrEngineListener[] getListeners()
    {
        synchronized (listeners)
        {
            return listeners.toArray(new ScOtrEngineListener[listeners.size()]);
        }
    }

    /**
     * Manages the scheduling of TimerTasks that are used to set Contact's
     * ScSessionStatus after a period of time.
     * 
     * @author Marin Dzhigarov
     */
    private class ScSessionStatusScheduler
    {
        private final Timer timer = new Timer();

        private final Map<OtrContact, TimerTask> tasks =
            new ConcurrentHashMap<OtrContact, TimerTask>();

        public void scheduleScSessionStatusChange(
            final OtrContact otrContact, final ScSessionStatus status)
        {
            cancel(otrContact);

            TimerTask task
                = new TimerTask()
                {
                    @Override
                    public void run()
                    {
                        setSessionStatus(otrContact, status);
                    }
                };
            timer.schedule(task, SESSION_TIMEOUT);
            tasks.put(otrContact, task);
        }

        public void cancel(final OtrContact otrContact)
        {
            TimerTask task = tasks.get(otrContact);
            if (task != null)
                task.cancel();
            tasks.remove(otrContact);
        }

        public void serviceChanged(ServiceEvent ev)
        {
            Object service
                = OtrActivator.bundleContext.getService(
                    ev.getServiceReference());

            if (!(service instanceof ProtocolProviderService))
                return;
    
            if (ev.getType() == ServiceEvent.UNREGISTERING)
            {
                ProtocolProviderService provider
                    = (ProtocolProviderService) service;
    
                Iterator<OtrContact> i = tasks.keySet().iterator();
    
                while (i.hasNext())
                {
                    OtrContact otrContact = i.next();
                    if (provider.equals(
                        otrContact.contact.getProtocolProvider()))
                    {
                        cancel(otrContact);
                        i.remove();
                    }
                }
            }
        }
    }

    private void setSessionStatus(OtrContact contact, ScSessionStatus status)
    {
        scSessionStatusMap.put(getSessionID(contact), status);
        scheduler.cancel(contact);
        for (ScOtrEngineListener l : getListeners())
            l.sessionStatusChanged(contact);
    }

    @Override
    public ScSessionStatus getSessionStatus(OtrContact contact)
    {
        SessionID sessionID = getSessionID(contact);
        SessionStatus sessionStatus = otrEngine.getSession(sessionID).getSessionStatus();
        ScSessionStatus scSessionStatus = null;
        if (!scSessionStatusMap.containsKey(sessionID))
        {
            switch (sessionStatus)
            {
            case PLAINTEXT:
                scSessionStatus = ScSessionStatus.PLAINTEXT;
                break;
            case ENCRYPTED:
                scSessionStatus = ScSessionStatus.ENCRYPTED;
                break;
            case FINISHED:
                scSessionStatus = ScSessionStatus.FINISHED;
                break;
            }
            scSessionStatusMap.put(sessionID, scSessionStatus);
        }
        return scSessionStatusMap.get(sessionID);
    }

    @Override
    public boolean isMessageUIDInjected(String mUID)
    {
        return injectedMessageUIDs.contains(mUID);
    }

    @Override
    public void launchHelp()
    {
        ServiceReference ref =
            OtrActivator.bundleContext
                .getServiceReference(BrowserLauncherService.class.getName());

        if (ref == null)
            return;

        BrowserLauncherService service =
            (BrowserLauncherService) OtrActivator.bundleContext.getService(ref);

        service.openURL(OtrActivator.resourceService
            .getI18NString("plugin.otr.authbuddydialog.HELP_URI"));
    }

    @Override
    public void refreshSession(OtrContact otrContact)
    {
        SessionID sessionID = getSessionID(otrContact);
        try
        {
            otrEngine.getSession(sessionID).refreshSession();
        }
        catch (OtrException e)
        {
            logger.error("Error refreshing session", e);
            showError(sessionID, e.getMessage());
        }
    }

    @Override
    public void removeListener(ScOtrEngineListener l)
    {
        synchronized (listeners)
        {
            listeners.remove(l);
        }
    }

    /**
     * Cleans the contactsMap when <tt>ProtocolProviderService</tt>
     * gets unregistered.
     */
    @Override
    public void serviceChanged(ServiceEvent ev)
    {
        Object service
            = OtrActivator.bundleContext.getService(ev.getServiceReference());

        if (!(service instanceof ProtocolProviderService))
            return;

        if (ev.getType() == ServiceEvent.UNREGISTERING)
        {
            if (logger.isDebugEnabled())
            {
                logger.debug(
                        "Unregistering a ProtocolProviderService, cleaning"
                            + " OTR's ScSessionID to Contact map.");
                logger.debug(
                        "Unregistering a ProtocolProviderService, cleaning"
                            + " OTR's Contact to SpmProgressDialog map.");
            }

            ProtocolProviderService provider
                = (ProtocolProviderService) service;

            synchronized(contactsMap)
            {
                Iterator<OtrContact> i = contactsMap.values().iterator();

                while (i.hasNext())
                {
                    OtrContact otrContact = i.next();
                    if (provider.equals(
                        otrContact.contact.getProtocolProvider()))
                    {
                        scSessionStatusMap.remove(getSessionID(otrContact));
                        i.remove();
                    }
                }
            }

            Iterator<OtrContact> i = progressDialogMap.keySet().iterator();

            while (i.hasNext())
            {
                if (provider.equals(i.next().contact.getProtocolProvider()))
                    i.remove();
            }
            scheduler.serviceChanged(ev);
        }
    }

    @Override
    public void setContactPolicy(Contact contact, OtrPolicy policy)
    {
        ProtocolProviderService pps = contact.getProtocolProvider();
        SessionID sessionID
            = new SessionID(
                pps.getAccountID().getAccountUniqueID(),
                contact.getAddress(),
                pps.getProtocolName());

        String propertyID = sessionID + "contact_policy";
        if (policy == null)
            this.configurator.removeProperty(propertyID);
        else
            this.configurator.setProperty(propertyID, policy.getPolicy());

        for (ScOtrEngineListener l : getListeners())
            l.contactPolicyChanged(contact);
    }

    @Override
    public void setGlobalPolicy(OtrPolicy policy)
    {
        if (policy == null)
            this.configurator.removeProperty("GLOBAL_POLICY");
        else
            this.configurator.setProperty("GLOBAL_POLICY", policy.getPolicy());

        for (ScOtrEngineListener l : getListeners())
            l.globalPolicyChanged();
    }

    public void showError(SessionID sessionID, String err)
    {
        OtrContact otrContact = getOtrContact(sessionID);
        if (otrContact == null)
            return;

        Contact contact = otrContact.contact;
        OtrActivator.uiService.getChat(contact).addMessage(
            contact.getDisplayName(), new Date(),
            Chat.ERROR_MESSAGE, err,
            OperationSetBasicInstantMessaging.DEFAULT_MIME_TYPE);
    }

    @Override
    public void startSession(OtrContact otrContact)
    {
        SessionID sessionID = getSessionID(otrContact);

        ScSessionStatus scSessionStatus = getSessionStatus(otrContact);
        scSessionStatus = ScSessionStatus.LOADING;
        scSessionStatusMap.put(sessionID, scSessionStatus);
        for (ScOtrEngineListener l : getListeners())
        {
            l.sessionStatusChanged(otrContact);
        }

        scheduler.scheduleScSessionStatusChange(
            otrContact, ScSessionStatus.TIMED_OUT);

        try
        {
            otrEngine.getSession(sessionID).startSession();
        }
        catch (OtrException e)
        {
            logger.error("Error starting session", e);
            showError(sessionID, e.getMessage());
        }
    }

    @Override
    public String transformReceiving(OtrContact otrContact, String msgText)
    {
        SessionID sessionID = getSessionID(otrContact);
        try
        {
            return otrEngine.getSession(sessionID).transformReceiving(msgText);
        }
        catch (OtrException e)
        {
            logger.error("Error receiving the message", e);
            showError(sessionID, e.getMessage());
            return null;
        }
    }

    @Override
    public String[] transformSending(OtrContact otrContact, String msgText)
    {
        SessionID sessionID = getSessionID(otrContact);
        try
        {
            return otrEngine.getSession(sessionID).transformSending(msgText);
        }
        catch (OtrException e)
        {
            logger.error("Error transforming the message", e);
            showError(sessionID, e.getMessage());
            return null;
        }
    }

    private Session getSession(OtrContact contact)
    {
        SessionID sessionID = getSessionID(contact);
        return otrEngine.getSession(sessionID);
    }

    @Override
    public void initSmp(OtrContact otrContact, String question, String secret)
    {
        Session session = getSession(otrContact);
        try
        {
            session.initSmp(question, secret);

            SmpProgressDialog progressDialog = progressDialogMap.get(otrContact);
            if (progressDialog == null)
            {
                progressDialog = new SmpProgressDialog(otrContact.contact);
                progressDialogMap.put(otrContact, progressDialog);
            }

            progressDialog.init();
            progressDialog.setVisible(true);
        }
        catch (OtrException e)
        {
            logger.error("Error initializing SMP session with contact "
                         + otrContact.contact.getDisplayName(), e);
            showError(session.getSessionID(), e.getMessage());
        }
    }

    @Override
    public void respondSmp( OtrContact otrContact,
                            InstanceTag receiverTag,
                            String question,
                            String secret)
    {
        Session session = getSession(otrContact);
        try
        {
            session.respondSmp(receiverTag, question, secret);

            SmpProgressDialog progressDialog = progressDialogMap.get(otrContact);
            if (progressDialog == null)
            {
                progressDialog = new SmpProgressDialog(otrContact.contact);
                progressDialogMap.put(otrContact, progressDialog);
            }

            progressDialog.incrementProgress();
            progressDialog.setVisible(true);
        }
        catch (OtrException e)
        {
            logger.error(
                "Error occured when sending SMP response to contact "
                + otrContact.contact.getDisplayName(), e);
            showError(session.getSessionID(), e.getMessage());
        }
    }

    @Override
    public void abortSmp(OtrContact otrContact)
    {
        Session session = getSession(otrContact);
        try
        {
            session.abortSmp();

            SmpProgressDialog progressDialog = progressDialogMap.get(otrContact);
            if (progressDialog == null)
            {
                progressDialog = new SmpProgressDialog(otrContact.contact);
                progressDialogMap.put(otrContact, progressDialog);
            }

            progressDialog.dispose();
        }
        catch (OtrException e)
        {
            logger.error("Error aborting SMP session with contact "
                         + otrContact.contact.getDisplayName(), e);
            showError(session.getSessionID(), e.getMessage());
        }
    }

    @Override
    public PublicKey getRemotePublicKey(OtrContact otrContact)
    {
        if (otrContact == null)
            return null;

        Session session = getSession(otrContact);

        return session.getRemotePublicKey();
    }

    @Override
    public List<Session> getSessionInstances(OtrContact otrContact)
    {
        if (otrContact == null)
            return Collections.emptyList();
        return getSession(otrContact).getInstances();
    }

    @Override
    public boolean setOutgoingSession(OtrContact contact, InstanceTag tag)
    {
        if (contact == null)
            return false;

        Session session = getSession(contact);

        scSessionStatusMap.remove(session.getSessionID());
        return session.setOutgoingInstance(tag);
    }

    @Override
    public Session getOutgoingSession(OtrContact contact)
    {
        if (contact == null)
            return null;

        SessionID sessionID = getSessionID(contact);

        return otrEngine.getSession(sessionID).getOutgoingInstance();
    }
}
