/*
 * 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.util.account;

import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.service.protocol.event.*;
import net.java.sip.communicator.service.protocol.globalstatus.*;
import net.java.sip.communicator.util.*;

import org.osgi.framework.*;

/**
 * The <tt>LoginManager</tt> manages the login operation. Here we obtain the
 * <tt>ProtocolProviderFactory</tt>, we make the account installation and we
 * handle all events related to the registration state.
 * <p>
 * The <tt>LoginManager</tt> is the one that opens one or more
 * <tt>LoginWindow</tt>s for each <tt>ProtocolProviderFactory</tt>. The
 * <tt>LoginWindow</tt> is where user could enter an identifier and password.
 * <p>
 * Note that the behavior of this class will be changed when the Configuration
 * Service is ready.
 *
 * @author Yana Stamcheva
 */
public class LoginManager
    implements  ServiceListener,
                RegistrationStateChangeListener,
                AccountManagerListener
{
    private static final Logger logger = Logger.getLogger(LoginManager.class);

    private boolean manuallyDisconnected = false;

    private final LoginRenderer loginRenderer;

    /**
     * Creates an instance of the <tt>LoginManager</tt>, by specifying the main
     * application window.
     *
     * @param loginRenderer the main application window
     */
    public LoginManager(LoginRenderer loginRenderer)
    {
        this.loginRenderer = loginRenderer;

        UtilActivator.bundleContext.addServiceListener(this);
    }

    /**
     * Registers the given protocol provider.
     *
     * @param protocolProvider the ProtocolProviderService to register.
     */
    public void login(ProtocolProviderService protocolProvider)
    {
        loginRenderer.startConnectingUI(protocolProvider);

        new RegisterProvider(protocolProvider,
            loginRenderer.getSecurityAuthorityImpl(protocolProvider)).start();
    }

    /**
     * Unregisters the given protocol provider.
     *
     * @param protocolProvider the ProtocolProviderService to unregister
     */
    public static void logoff(ProtocolProviderService protocolProvider)
    {
        new UnregisterProvider(protocolProvider).start();
    }

    /**
     * Shows login window for each registered account.
     */
    public void runLogin()
    {
        // if someone is late registering catch it
        UtilActivator.getAccountManager().addListener(this);

        for (ProtocolProviderFactory providerFactory : UtilActivator
            .getProtocolProviderFactories().values())
        {
            addAccountsForProtocolProviderFactory(providerFactory);
        }
    }

    /**
     * Notifies that the loading of the stored accounts of a
     * specific <code>ProtocolProviderFactory</code> has finished.
     *
     * @param event the <code>AccountManagerEvent</code> describing the
     *            <code>AccountManager</code> firing the notification and the
     *            other details of the specific notification.
     */
    public void handleAccountManagerEvent(AccountManagerEvent event)
    {
        if(event.getType()
            == AccountManagerEvent.STORED_ACCOUNTS_LOADED)
        {
            addAccountsForProtocolProviderFactory(event.getFactory());
        }
    }

    /**
     * Handles stored accounts for a protocol provider factory and add them
     * to the UI and register them if needed.
     * @param providerFactory the factory to handle.
     */
    private void addAccountsForProtocolProviderFactory(
        ProtocolProviderFactory providerFactory)
    {
        for (AccountID accountID : providerFactory.getRegisteredAccounts())
        {
            ServiceReference<ProtocolProviderService> serRef
                = providerFactory.getProviderForAccount(accountID);
            ProtocolProviderService protocolProvider
                = UtilActivator.bundleContext.getService(serRef);

            handleProviderAdded(protocolProvider);
        }
    }

    /**
     * The method is called by a ProtocolProvider implementation whenever a
     * change in the registration state of the corresponding provider had
     * occurred.
     *
     * @param evt ProviderStatusChangeEvent the event describing the status
     *            change.
     */
    public void registrationStateChanged(RegistrationStateChangeEvent evt)
    {
        RegistrationState newState = evt.getNewState();
        ProtocolProviderService protocolProvider = evt.getProvider();
        AccountID accountID = protocolProvider.getAccountID();

        if (logger.isTraceEnabled())
            logger.trace("Protocol provider: " + protocolProvider
            + " changed its state to: " + evt.getNewState().getStateName());

        if (newState.equals(RegistrationState.REGISTERED)
            || newState.equals(RegistrationState.UNREGISTERED)
            || newState.equals(RegistrationState.EXPIRED)
            || newState.equals(RegistrationState.AUTHENTICATION_FAILED)
            || newState.equals(RegistrationState.CONNECTION_FAILED)
            || newState.equals(RegistrationState.CHALLENGED_FOR_AUTHENTICATION)
            || newState.equals(RegistrationState.REGISTERED))
        {
            loginRenderer.stopConnectingUI(protocolProvider);
        }

        if (newState.equals(RegistrationState.REGISTERED))
        {
            loginRenderer.protocolProviderConnected(protocolProvider,
                                                    System.currentTimeMillis());
        }
        else
        {
            String msgText;
            if (newState.equals(RegistrationState.AUTHENTICATION_FAILED))
            {
                switch (evt.getReasonCode())
                {
                case RegistrationStateChangeEvent
                    .REASON_RECONNECTION_RATE_LIMIT_EXCEEDED:

                    msgText = UtilActivator.getResources().getI18NString(
                        "service.gui.RECONNECTION_LIMIT_EXCEEDED", new String[]
                        { accountID.getUserID(), accountID.getService() });

                    UtilActivator.getAlertUIService().showAlertDialog(
                        UtilActivator.getResources()
                            .getI18NString("service.gui.ERROR"),
                        msgText);
                    break;

                case RegistrationStateChangeEvent.REASON_NON_EXISTING_USER_ID:
                    msgText = UtilActivator.getResources().getI18NString(
                        "service.gui.NON_EXISTING_USER_ID",
                        new String[]
                        { protocolProvider.getProtocolDisplayName() });

                    UtilActivator.getAlertUIService().showAlertDialog(
                        UtilActivator.getResources()
                            .getI18NString("service.gui.ERROR"),
                        msgText);
                    break;
                case RegistrationStateChangeEvent.REASON_TLS_REQUIRED:
                    msgText = UtilActivator.getResources().getI18NString(
                        "service.gui.NON_SECURE_CONNECTION",
                        new String[]
                        { accountID.getAccountAddress() });

                    UtilActivator.getAlertUIService().showAlertDialog(
                        UtilActivator.getResources()
                            .getI18NString("service.gui.ERROR"),
                        msgText);
                    break;
                default:
                    break;
                }

                if (logger.isTraceEnabled())
                    logger.trace(evt.getReason());
            }
//            CONNECTION_FAILED events are now dispatched in reconnect plugin
//            else if (newState.equals(RegistrationState.CONNECTION_FAILED))
//            {
//                loginRenderer.protocolProviderConnectionFailed(
//                    protocolProvider,
//                    this);
//
//                logger.trace(evt.getReason());
//            }
            else if (newState.equals(RegistrationState.EXPIRED))
            {
                msgText = UtilActivator.getResources().getI18NString(
                    "service.gui.CONNECTION_EXPIRED_MSG",
                    new String[]
                    { protocolProvider.getProtocolDisplayName() });

                UtilActivator.getAlertUIService().showAlertDialog(
                    UtilActivator.getResources()
                        .getI18NString("service.gui.ERROR"),
                    msgText);

                logger.error(evt.getReason());
            }
            else if (newState.equals(RegistrationState.UNREGISTERED))
            {
                if (!manuallyDisconnected)
                {
                    if (evt.getReasonCode() == RegistrationStateChangeEvent
                                                .REASON_MULTIPLE_LOGINS)
                    {
                        msgText = UtilActivator.getResources().getI18NString(
                            "service.gui.MULTIPLE_LOGINS",
                            new String[]
                            { accountID.getUserID(), accountID.getService() });

                        UtilActivator.getAlertUIService().showAlertDialog(
                            UtilActivator.getResources()
                                .getI18NString("service.gui.ERROR"),
                            msgText);
                    }
                    else if (evt.getReasonCode() == RegistrationStateChangeEvent
                                                .REASON_CLIENT_LIMIT_REACHED_FOR_IP)
                    {
                        msgText = UtilActivator.getResources().getI18NString(
                            "service.gui.LIMIT_REACHED_FOR_IP", new String[]
                            { protocolProvider.getProtocolDisplayName() });

                        UtilActivator.getAlertUIService().showAlertDialog(
                            UtilActivator.getResources()
                                .getI18NString("service.gui.ERROR"),
                            msgText);
                    }
                    else if (evt.getReasonCode() == RegistrationStateChangeEvent
                                                .REASON_USER_REQUEST)
                    {
                        // do nothing
                    }
                    else
                    {
                        msgText = UtilActivator.getResources().getI18NString(
                            "service.gui.UNREGISTERED_MESSAGE", new String[]
                            { accountID.getUserID(), accountID.getService() });

                        UtilActivator.getAlertUIService().showAlertDialog(
                            UtilActivator.getResources()
                                .getI18NString("service.gui.ERROR"),
                            msgText);
                    }
                    if (logger.isTraceEnabled())
                        logger.trace(evt.getReason());
                }
            }
        }
    }

    /**
     * Implements the <tt>ServiceListener</tt> method. Verifies whether the
     * passed event concerns a <tt>ProtocolProviderService</tt> and adds the
     * corresponding UI controls.
     *
     * @param event The <tt>ServiceEvent</tt> object.
     */
    public void serviceChanged(ServiceEvent event)
    {
        ServiceReference<?> serviceRef = event.getServiceReference();

        // if the event is caused by a bundle being stopped, we don't want to
        // know
        if (serviceRef.getBundle().getState() == Bundle.STOPPING)
            return;

        Object service = UtilActivator.bundleContext.getService(serviceRef);

        // we don't care if the source service is not a protocol provider
        if (!(service instanceof ProtocolProviderService))
            return;

        switch (event.getType())
        {
        case ServiceEvent.REGISTERED:
            handleProviderAdded((ProtocolProviderService) service);
            break;
        case ServiceEvent.UNREGISTERING:
            handleProviderRemoved((ProtocolProviderService) service);
            break;
        }
    }

    /**
     * Adds all UI components (status selector box, etc) related to the given
     * protocol provider.
     *
     * @param protocolProvider the <tt>ProtocolProviderService</tt>
     */
    private void handleProviderAdded(ProtocolProviderService protocolProvider)
    {
        if (logger.isTraceEnabled())
            logger.trace("The following protocol provider was just added: "
            + protocolProvider.getAccountID().getAccountAddress());

        synchronized(loginRenderer)
        {
            if(!loginRenderer.containsProtocolProviderUI(protocolProvider))
            {
                protocolProvider.addRegistrationStateChangeListener(this);
                loginRenderer.addProtocolProviderUI(protocolProvider);
            }
            // we have already added this provider and scheduled
            // a login if needed
            // we've done our work, if it fails or something else
            // reconnect or other plugins will take care
            else
                return;
        }

        Object status = AccountStatusUtils
            .getProtocolProviderLastStatus(protocolProvider);

        if (status == null
            || status.equals(GlobalStatusEnum.ONLINE_STATUS)
            || ((status instanceof PresenceStatus) && (((PresenceStatus) status)
                .getStatus() >= PresenceStatus.ONLINE_THRESHOLD)))
        {
            login(protocolProvider);
        }
    }

    /**
     * Removes all UI components related to the given protocol provider.
     *
     * @param protocolProvider the <tt>ProtocolProviderService</tt>
     */
    private void handleProviderRemoved(ProtocolProviderService protocolProvider)
    {
        loginRenderer.removeProtocolProviderUI(protocolProvider);
    }

    /**
     * Returns <tt>true</tt> to indicate the jitsi has been manually
     * disconnected, <tt>false</tt> - otherwise.
     *
     * @return <tt>true</tt> to indicate the jitsi has been manually
     * disconnected, <tt>false</tt> - otherwise
     */
    public boolean isManuallyDisconnected()
    {
        return manuallyDisconnected;
    }

    /**
     * Sets the manually disconnected property.
     *
     * @param manuallyDisconnected <tt>true</tt> to indicate the jitsi has been
     * manually disconnected, <tt>false</tt> - otherwise
     */
    public void setManuallyDisconnected(boolean manuallyDisconnected)
    {
        this.manuallyDisconnected = manuallyDisconnected;
    }

    /**
     * Registers a protocol provider in a separate thread.
     */
    private class RegisterProvider
        extends Thread
    {
        private final ProtocolProviderService protocolProvider;

        private final SecurityAuthority secAuth;

        RegisterProvider(   ProtocolProviderService protocolProvider,
                            SecurityAuthority secAuth)
        {
            this.protocolProvider = protocolProvider;
            this.secAuth = secAuth;

            if(logger.isTraceEnabled())
                logger.trace("Registering provider: "
                    + protocolProvider.getAccountID().getAccountAddress(),
                    new Exception(
                        "Just tracing, provider registering, not an error!"));
        }

        /**
         * Registers the contained protocol provider and process all possible
         * errors that may occur during the registration process.
         */
        @Override
        public void run()
        {
            try
            {
                protocolProvider.register(secAuth);
            }
            catch (OperationFailedException ex)
            {
                handleOperationFailedException(ex);
            }
            catch (Throwable ex)
            {
                logger.error("Failed to register protocol provider. ", ex);

                AccountID accountID = protocolProvider.getAccountID();
                UtilActivator.getAlertUIService().showAlertDialog(
                    UtilActivator.getResources()
                        .getI18NString("service.gui.ERROR"),
                    UtilActivator.getResources()
                        .getI18NString("service.gui.LOGIN_GENERAL_ERROR",
                    new String[]
                    { accountID.getUserID(),
                      accountID.getProtocolName(),
                      accountID.getService() }));
            }
        }

        private void handleOperationFailedException(OperationFailedException ex)
        {
            String errorMessage = "";

            switch (ex.getErrorCode())
            {
            case OperationFailedException.GENERAL_ERROR:
            {
                logger.error("Provider could not be registered"
                    + " due to the following general error: ", ex);

                AccountID accountID = protocolProvider.getAccountID();
                errorMessage =
                    UtilActivator.getResources().getI18NString(
                        "service.gui.LOGIN_GENERAL_ERROR",
                        new String[]
                           { accountID.getUserID(),
                             accountID.getProtocolName(),
                             accountID.getService() });

                UtilActivator.getAlertUIService().showAlertDialog(
                    UtilActivator.getResources()
                        .getI18NString("service.gui.ERROR"), errorMessage, ex);
            }
                break;
            case OperationFailedException.INTERNAL_ERROR:
            {
                logger.error("Provider could not be registered"
                    + " due to the following internal error: ", ex);

                AccountID accountID = protocolProvider.getAccountID();
                errorMessage =
                    UtilActivator.getResources().getI18NString(
                        "service.gui.LOGIN_INTERNAL_ERROR",
                        new String[]
                           { accountID.getUserID(), accountID.getService() });

                UtilActivator.getAlertUIService().showAlertDialog(
                    UtilActivator.getResources().getI18NString(
                        "service.gui.ERROR"), errorMessage, ex);
            }
                break;
            case OperationFailedException.NETWORK_FAILURE:
            {
                if (logger.isInfoEnabled())
                {
                    logger.info("Provider could not be registered"
                            + " due to a network failure: " + ex);
                }

                loginRenderer.protocolProviderConnectionFailed(
                    protocolProvider,
                    LoginManager.this);
            }
                break;
            case OperationFailedException.INVALID_ACCOUNT_PROPERTIES:
            {
                logger.error("Provider could not be registered"
                    + " due to an invalid account property: ", ex);

                AccountID accountID = protocolProvider.getAccountID();
                errorMessage =
                    UtilActivator.getResources().getI18NString(
                        "service.gui.LOGIN_INVALID_PROPERTIES_ERROR",
                        new String[]
                        { accountID.getUserID(), accountID.getService() });

                UtilActivator.getAlertUIService().showAlertDialog(
                    UtilActivator.getResources()
                        .getI18NString("service.gui.ERROR"),
                    errorMessage, ex);
            }
                break;
            default:
                logger.error("Provider could not be registered.", ex);
            }
        }
    }

    /**
     * Unregisters a protocol provider in a separate thread.
     */
    private static class UnregisterProvider
        extends Thread
    {
        ProtocolProviderService protocolProvider;

        UnregisterProvider(ProtocolProviderService protocolProvider)
        {
            this.protocolProvider = protocolProvider;
        }

        /**
         * Unregisters the contained protocol provider and process all possible
         * errors that may occur during the un-registration process.
         */
        @Override
        public void run()
        {
            try
            {
                protocolProvider.unregister(true);
            }
            catch (OperationFailedException ex)
            {
                int errorCode = ex.getErrorCode();

                if (errorCode == OperationFailedException.GENERAL_ERROR)
                {
                    logger.error("Provider could not be unregistered"
                        + " due to the following general error: " + ex);
                }
                else if (errorCode == OperationFailedException.INTERNAL_ERROR)
                {
                    logger.error("Provider could not be unregistered"
                        + " due to the following internal error: " + ex);
                }
                else if (errorCode == OperationFailedException.NETWORK_FAILURE)
                {
                    logger.error("Provider could not be unregistered"
                        + " due to a network failure: " + ex);
                }

                UtilActivator.getAlertUIService().showAlertDialog(
                    UtilActivator.getResources()
                        .getI18NString("service.gui.ERROR"),
                    UtilActivator.getResources()
                        .getI18NString("service.gui.LOGOFF_NOT_SUCCEEDED",
                        new String[]
                        { protocolProvider.getAccountID().getUserID(),
                            protocolProvider.getAccountID().getService() }));
            }
        }
    }
}
