/**
 * Copyright (C) 2004-2011 Jive Software. All rights reserved.
 *
 * 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 org.jivesoftware.spark;

import org.jivesoftware.resource.Res;
import org.jivesoftware.smack.AbstractXMPPConnection;
import org.jivesoftware.smack.ConnectionListener;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.packet.StanzaBuilder;
import org.jivesoftware.smack.provider.ProviderManager;
import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
import org.jivesoftware.smackx.iqprivate.PrivateDataManager;
import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
import org.jivesoftware.smackx.disco.packet.DiscoverItems;
import org.jivesoftware.spark.ui.PresenceListener;
import org.jivesoftware.spark.util.log.Log;
import org.jivesoftware.sparkimpl.plugin.manager.Features;
import org.jxmpp.jid.DomainBareJid;
import org.jxmpp.jid.EntityBareJid;
import org.jxmpp.jid.EntityFullJid;
import org.jxmpp.jid.Jid;
import org.jxmpp.util.XmppStringUtils;

import javax.swing.SwingUtilities;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * This manager is responsible for the handling of the XMPPConnection used within Spark. This is used
 * for the changing of the user's presence, the handling of connection errors and the ability to add
 * presence listeners and retrieve the connection used in Spark.
 *
 * @author Derek DeMoro
 */
public final class SessionManager implements ConnectionListener {
    private AbstractXMPPConnection connection;
    private PrivateDataManager personalDataManager;

    private DomainBareJid serverAddress;
    private String username;
    private String password;

    private EntityFullJid JID;

    private final CopyOnWriteArrayList<PresenceListener> presenceListeners = new CopyOnWriteArrayList<>();

    private EntityBareJid userBareAddress;
    /** Key: service domain JID, value discovered items. Sorted as received from server */
    private final Map<Jid, DiscoverItems.Item> discoverItems = new LinkedHashMap<>();
    /** Key: service domain JID, value discovered infos. Sorted as received from server, same as discoverItems. */
    private final Map<Jid, DiscoverInfo> discoverInfos = new LinkedHashMap<>();

    // Stores our presence state at the time that the last connectionClosedOnError happened.
    private Presence preError;

    public SessionManager() {
    }

    /**
     * Initializes session.
     *
     * @param connection the XMPPConnection used in this session.
     * @param username   the agent's username.
     * @param password   the agent's password.
     */
    public void initializeSession( AbstractXMPPConnection connection, String username, String password) {
        ProviderManager.addExtensionProvider(Features.ELEMENT, Features.NAMESPACE, new Features.Provider());
        this.connection = connection;
        this.username = username;
        this.password = password;
        this.userBareAddress = connection.getUser().asEntityBareJid();

        // create a workgroup session
        personalDataManager = PrivateDataManager.getInstanceFor( connection );

        // Discover items
        discoverItems();

        connection.addConnectionListener(this);
    }

    /**
     * Does the initial service discovery.
     */
    private void discoverItems() {
        ServiceDiscoveryManager discoManager = SparkManager.getDiscoManager();
        DomainBareJid xmppServiceDomain = connection.getXMPPServiceDomain();
        // discover the domain info
        try {
            DiscoverInfo info = discoManager.discoverInfo(xmppServiceDomain);
            discoverInfos.put(xmppServiceDomain, info);
        } catch (Exception e) {
            Log.error("Unable to discover info for root", e);
        }
        // discover subdomain services
        try {
            DiscoverItems discoServiceItems = discoManager.discoverItems(xmppServiceDomain);
            for (DiscoverItems.Item item : discoServiceItems.getItems()) {
                Jid serviceJid = item.getEntityID();
                discoverItems.put(serviceJid, item);
            }
        } catch (XMPPException | SmackException | InterruptedException e) {
            Log.error(e);
            throw new RuntimeException("Unable to discover root services");
        }
        // discover subdomain services infos
        for (Map.Entry<Jid, DiscoverItems.Item> entry : discoverItems.entrySet()) {
            Jid serviceJid = entry.getKey();
            try {
                DiscoverInfo info = discoManager.discoverInfo(serviceJid);
                discoverInfos.put(serviceJid, info);
            } catch (Exception e) {
                Log.error("Unable to discover root services info " + serviceJid, e);
            }
        }
    }

    /**
     * Returns the XMPPConnection used for this session.
     */
    public AbstractXMPPConnection getConnection() {
        return connection;
    }


    /**
     * Returns the PrivateDataManager responsible for handling all private data for individual
     * agents.
     */
    public PrivateDataManager getPersonalDataManager() {
        return personalDataManager;
    }

    /**
     * Returns the host for this connection.
     */
    public DomainBareJid getServerAddress() {
        return serverAddress;
    }

    /**
     * Set the server address
     *
     * @param address the address of the server.
     */
    public void setServerAddress(DomainBareJid address) {
        this.serverAddress = address;
    }

    /**
     * Notify agent the connection was closed due to an exception.
     *
     * @param ex the Exception that took place.
     */
    @Override
	public void connectionClosedOnError(final Exception ex) {
        SwingUtilities.invokeLater( () -> {
            preError = Workspace.getInstance().getStatusBar().getPresence();
            final Presence presence = StanzaBuilder.buildPresence()
                .ofType(Presence.Type.unavailable)
                .setStatus(Res.getString("status.offline"))
                .build();
            changePresence(presence);

            Workspace.getInstance().getStatusBar().setStatusPanelEnabled(false);
            Log.debug("Connection closed on error.: " + ex.getMessage());
        } );
    }

    @Override
    public void authenticated( XMPPConnection xmppConnection, boolean b )
    {
        reconnectionSuccessful();
    }

    /**
     * Notify the agent that the connection has been closed.
     */
    @Override
	public void connectionClosed() {
    }

    /**
     * Return the username associated with this session.
     */
    public String getUsername() {
        return XmppStringUtils.unescapeLocalpart(username);
    }

    /**
     * Return the password associated with this session.
     */
    public String getPassword() {
        return password;
    }

    /**
     * Update the current availability of the user
     *
     * @param presence the current presence of the user.
     */
    public void changePresence(Presence presence) {
        // Fire Presence Listeners
        for (PresenceListener listener : this.presenceListeners) {
            listener.presenceChanged(presence);
        }

        // Do NOT send presence if disconnected.
        if (connection.isConnected()) {
            // Send Presence Packet
            try
            {
                connection.sendStanza(presence);
            }
            catch ( SmackException.NotConnectedException | InterruptedException e )
            {
                Log.error( "Unable to send presence to " + presence.getTo(), e );
            }
        }
    }

    /**
     * Returns the jid of the Spark user.
     *
     * @return the jid of the Spark user.
     */
    public EntityFullJid getJID() {
        return JID;
    }

    /**
     * Sets the jid of the current Spark user.
     *
     * @param jid the jid of the current Spark user.
     */
    public void setJID(EntityFullJid jid) {
        this.JID = jid;
    }

    /**
     * Adds a <code>PresenceListener</code> to Spark. PresenceListener's are used
     * to allow notification of when the Spark users changes their presence.
     *
     * @param listener the listener.
     */
    public void addPresenceListener(PresenceListener listener) {
        presenceListeners.addIfAbsent(listener);
    }

    /**
     * Remove a <code>PresenceListener</code> from Spark.
     *
     * @param listener the listener.
     */
    public void removePresenceListener(PresenceListener listener) {
        presenceListeners.remove(listener);
    }

    /**
     * Returns the users bare JID. A bare-JID is the JID without a resource (ex. derek@jivesoftware.com/spark would
     * be derek@jivesoftware.com)
     *
     * @return the users bare JID.
     */
    public EntityBareJid getUserBareAddress() {
        return userBareAddress;
    }

    /**
     * Returns the Discovered Items found on startup.
     */
    public Map<Jid, DiscoverItems.Item> getDiscoveredItems() {
        return discoverItems;
    }

    public Map<Jid, DiscoverInfo> getDiscoveredInfos() {
        return discoverInfos;
    }

    public void setConnection(AbstractXMPPConnection con) {
        this.connection = con;
    }

    private void reconnectionSuccessful()
    {
        // Restore the presence state that we were in just before the disconnection happened.
        if ( preError != null )
        {
            SwingUtilities.invokeLater( () ->
            {
                changePresence( preError );
                preError = null;

                Workspace.getInstance().getStatusBar().setStatusPanelEnabled(true);
            });
        }
    }

}
