package org.jivesoftware.spark.plugin.otr.impl;

import java.security.PublicKey;

import javax.swing.ImageIcon;
import javax.swing.JOptionPane;
import javax.swing.UIManager;

import net.java.otr4j.*;
import net.java.otr4j.io.SerializationConstants;
import net.java.otr4j.session.Session;
import net.java.otr4j.session.SessionImpl;

import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.MessageBuilder;
import org.jivesoftware.smackx.carbons.packet.CarbonExtension;
import org.jivesoftware.smackx.eme.element.ExplicitMessageEncryptionElement;
import org.jivesoftware.smackx.hints.element.NoCopyHint;
import org.jivesoftware.smackx.hints.element.NoPermanentStoreHint;
import org.jivesoftware.spark.ChatManager;
import org.jivesoftware.spark.plugin.otr.OTRManager;
import org.jivesoftware.spark.plugin.otr.ui.OTRConnectionPanel;
import org.jivesoftware.spark.plugin.otr.util.OTRProperties;
import org.jivesoftware.spark.plugin.otr.util.OTRResources;
import org.jivesoftware.spark.ui.ChatRoomButton;
import org.jivesoftware.spark.ui.MessageEventListener;
import org.jivesoftware.resource.Res;

import org.jivesoftware.spark.ui.rooms.ChatRoomImpl;
import org.jivesoftware.spark.util.UIComponentRegistry;

import net.java.otr4j.session.SessionID;
import net.java.otr4j.session.SessionStatus;
import org.jivesoftware.spark.util.log.Log;
import org.jxmpp.jid.impl.JidCreate;

import static javax.swing.JOptionPane.*;
import static org.jivesoftware.smackx.eme.element.ExplicitMessageEncryptionElement.ExplicitMessageEncryptionProtocol.otrV0;
import static org.jivesoftware.spark.plugin.otr.util.OTRResources.ICON_OTR_OFF;
import static org.jivesoftware.spark.plugin.otr.util.OTRResources.ICON_OTR_ON;

/**
 * OTRSession are unique for every conversation.
 * It handles the otrEngine for the chat and controls if the chat is encrypted or not.
 *
 * @author Bergunde Holger
 */
public class OTRSession {

    private static final OtrPolicyImpl OTR_POLICY = new OtrPolicyImpl(OtrPolicy.ALLOW_V2 | OtrPolicy.ALLOW_V3 | OtrPolicy.ERROR_START_AKE);
    private ChatRoomImpl _chatRoom;
    private final String _remoteJID;
    private final SessionID _mySessionID;
    private final Session _mySession;
    final ChatRoomButton _otrButton = UIComponentRegistry.getButtonFactory().createOtrButton();
    private OTRConnectionPanel _conPanel;
    private MessageEventListener _msgEvnt;
    private boolean _OtrEnabled = false;
    private OtrEngineListener _otrListener;

    /**
     * OTRSession Constructor
     *
     * @param chatroom  chat room related to this OTR session
     * @param myJID     my own JID
     * @param remoteJID the JID of the participant
     */
    public OTRSession(ChatRoomImpl chatroom, String myJID, String remoteJID) {
        _chatRoom = chatroom;
        _remoteJID = remoteJID;
        OtrEngineHost _otrEngineHost = new OTREngineHost(OTR_POLICY, _chatRoom);
        _mySessionID = new SessionID(myJID, _remoteJID, "prpl-jabber");

        _mySession = new SessionImpl(_mySessionID, _otrEngineHost);

        setUpMessageListener();
        createButton();
        // Only initialize the actionListener once
        _otrButton.addActionListener(e -> {
            if (_mySession.getSessionStatus() == SessionStatus.ENCRYPTED) {
                stopSession();
            } else {
                startSession();
            }
        });
        _otrButton.setToolTipText(OTRResources.getString("otr.chat.button.tooltip"));
        _OtrEnabled = OTRProperties.getInstance().getIsOTREnabled();
    }

    /**
     * Maybe you want to update the chat room because it was reopened but the OTR
     * session is still alive.
     *
     * @param chatroom the chat room related to this OTR session
     */
    public void updateChatRoom(ChatRoomImpl chatroom) {
        _OtrEnabled = OTRProperties.getInstance().getIsOTREnabled();
        _chatRoom = chatroom;
        setUpMessageListener();
        createButton();
    }

    private void setUpMessageListener() {
        _conPanel = new OTRConnectionPanel(_chatRoom);
        _chatRoom.removeMessageEventListener(_msgEvnt);
        _msgEvnt = new MessageEventListener() {

            @Override
            public void sendingMessage(MessageBuilder message) {
                String msgBody = message.getBody();
                if (msgBody == null) {
                    return;
                }
                if (_mySession.getSessionStatus() != SessionStatus.ENCRYPTED) {
                    return;
                }
                clearMsgBody(message);
                try {
                    String[] encodedMsg = _mySession.transformSending(msgBody);
                    String encodedMsgBody = encodedMsg.length == 1 ? encodedMsg[0] : String.join("", encodedMsg);
                    message.setBody(encodedMsgBody);
                    message.addExtension(new ExplicitMessageEncryptionElement(otrV0));
                    message.addExtension(NoCopyHint.INSTANCE);
                    message.addExtension(NoPermanentStoreHint.INSTANCE);
                    message.addExtension(CarbonExtension.Private.INSTANCE);
                } catch (OtrException e) {
                    _chatRoom.getTranscriptWindow().insertNotificationMessage(OTRResources.getString("otr.failed.to.decode"), ChatManager.ERROR_COLOR);
                    Log.error("An exception occurred while trying to send a message: " + msgBody, e);
                }
            }

            @Override
            public void receivingMessage(MessageBuilder message) {
                String msgBody = message.getBody();
                if (msgBody == null) {
                    return;
                }
                if (_OtrEnabled) {
                    try {
                        String decoded = _mySession.transformReceiving(msgBody);
                        // if the message wasn't encrypted, then the decoded will be the same string so compare by a reference
                        //noinspection StringEquality
                        if (decoded != msgBody) {
                            clearMsgBody(message); // clear the body of the original encrypted message
                            if (decoded != null) {
                                message.setBody(decoded);
                            }
                            // clear the EME extension, since we decoded the message
                            message.removeExtension(ExplicitMessageEncryptionElement.ELEMENT, ExplicitMessageEncryptionElement.NAMESPACE);
                        }
                    } catch (OtrException e) {
                        clearMsgBody(message);
                        _chatRoom.getTranscriptWindow().insertNotificationMessage(OTRResources.getString("otr.failed.to.decode") + " " + e.getMessage(), ChatManager.ERROR_COLOR);
                        Log.error("An exception occurred while receiving a message: " + msgBody, e);
                    }
                } else {
                    if (msgBody.startsWith(SerializationConstants.HEAD)) {
                        clearMsgBody(message);
                        _chatRoom.getTranscriptWindow().insertNotificationMessage(OTRResources.getString("otr.not.enabled"), ChatManager.NOTIFICATION_COLOR);
                    }
                }
            }
        };
        _chatRoom.addMessageEventListener(_msgEvnt);
    }

    private static void clearMsgBody(MessageBuilder message) {
        // Manually remove the existing body to add a new one.
        message.removeExtension(Message.Body.ELEMENT, Message.Body.NAMESPACE);
    }

    private void createButton() {
        if (!OTRProperties.getInstance().getIsOTREnabled()) {
            return;
        }
        ImageIcon otrIcon;
        if (_mySession.getSessionStatus() == SessionStatus.ENCRYPTED) {
            otrIcon = ICON_OTR_ON;
            _conPanel.successfullyCon();
        } else {
            otrIcon = ICON_OTR_OFF;
        }

        _otrButton.setIcon(otrIcon);

        _mySession.removeOtrEngineListener(_otrListener);
        _chatRoom.getToolBar().addChatRoomButton(_otrButton);
        _otrListener = new OtrEngineListener() {

            @Override
            public void sessionStatusChanged(SessionID sessionID) {
                if (_mySession.getSessionStatus() == SessionStatus.ENCRYPTED) {
                    _conPanel.successfullyCon();
                    OtrKeyManager keyManager = OTRManager.getInstance().getKeyManager();
                    String otrkey = keyManager.getRemoteFingerprint(_mySessionID);
                    if (otrkey == null) {
                        PublicKey pubkey = _mySession.getRemotePublicKey(_mySession.getReceiverInstanceTag());
                        keyManager.savePublicKey(_mySessionID, pubkey);
                        otrkey = keyManager.getRemoteFingerprint(_mySessionID);
                    }

                    if (!keyManager.isVerified(_mySessionID)) {
                        String dialogTitle = OTRResources.getString("otr.key.not.verified.title");
                        String dialogMessage = OTRResources.getString("otr.start.session.with", _remoteJID) + "\n" +
                            OTRResources.getString("otr.key.not.verified.text") + "\n" +
                            otrkey + "\n" +
                            OTRResources.getString("otr.question.verify");
                        if (JOptionPane.showConfirmDialog(_otrButton, dialogMessage, dialogTitle, YES_NO_OPTION, WARNING_MESSAGE) == YES_OPTION) {
                            if (JOptionPane.showConfirmDialog(_otrButton, OTRResources.getString("otr.question.authorize"), dialogTitle, YES_NO_OPTION, WARNING_MESSAGE) == YES_OPTION) {
                                OTRManager.getInstance().authenticateUser(JidCreate.entityBareFromOrNull(_mySessionID.getUserID()));
                            } else {
                                keyManager.verify(_mySessionID);
                            }
                        }
                    }
                    _otrButton.setIcon(ICON_OTR_ON);
                } else if (_mySession.getSessionStatus() == SessionStatus.FINISHED || _mySession.getSessionStatus() == SessionStatus.PLAINTEXT) {
                    stopSession();
                    _otrButton.setIcon(ICON_OTR_OFF);
                }
            }

            @Override
            public void multipleInstancesDetected(SessionID sessionID) {
                Log.warning("multipleInstancesDetected for OTR session " + sessionID);
            }

            @Override
            public void outgoingSessionChanged(SessionID sessionID) {
                Log.warning("outgoingSessionChanged for OTR session " + sessionID);
            }
        };
        _mySession.addOtrEngineListener(_otrListener);
    }

    /**
     * Start the OTR session manually from outside
     */
    public void startSession() {
        _conPanel.tryToStart();
        try {
            _mySession.startSession();
        } catch (OtrException e) {
            Log.error("An exception occurred while starting an OTR session.", e);
        }
    }

    /**
     * Stop the OTR session manually from outside
     */
    public void stopSession() {
        _conPanel.connectionClosed();
        if (_mySession.getSessionStatus() == SessionStatus.ENCRYPTED) {
            _otrButton.setIcon(ICON_OTR_OFF);
            try {
                _mySession.endSession();
            } catch (OtrException e) {
                Log.error("An exception occurred while stopping the OTR session.", e);
            }
        }
    }

    public Session getMySession() {
        return _mySession;
    }
}
