/**
 *
 * Copyright 2003-2006 Jive Software.
 *
 * 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.smackx.jingleold.nat;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.IQ;

import org.jivesoftware.smackx.jingleold.ContentNegotiator;
import org.jivesoftware.smackx.jingleold.JingleActionEnum;
import org.jivesoftware.smackx.jingleold.JingleException;
import org.jivesoftware.smackx.jingleold.JingleNegotiator;
import org.jivesoftware.smackx.jingleold.JingleNegotiatorState;
import org.jivesoftware.smackx.jingleold.JingleSession;
import org.jivesoftware.smackx.jingleold.listeners.JingleListener;
import org.jivesoftware.smackx.jingleold.listeners.JingleTransportListener;
import org.jivesoftware.smackx.jingleold.nat.ICECandidate.Type;
import org.jivesoftware.smackx.jingleold.packet.Jingle;
import org.jivesoftware.smackx.jingleold.packet.JingleContent;
import org.jivesoftware.smackx.jingleold.packet.JingleTransport;
import org.jivesoftware.smackx.jingleold.packet.JingleTransport.JingleTransportCandidate;

/**
 * Transport negotiator.
 * <p/>
 * <p/>
 * This class is responsible for managing the transport negotiation process,
 * handling all the stanza(/packet) interchange and the stage control.
 *
 * @author Alvaro Saurin <alvaro.saurin@gmail.com>
 */
public abstract class TransportNegotiator extends JingleNegotiator {

    private static final Logger LOGGER = Logger.getLogger(TransportNegotiator.class.getName());

    // The time we give to the candidates check before we accept or decline the
    // transport (in milliseconds)
    public final static int CANDIDATES_ACCEPT_PERIOD = 4000;

    // The session this nenotiator belongs to
    //private final JingleSession session;

    // The transport manager
    private final TransportResolver resolver;

    // Transport candidates we have offered
    private final List<TransportCandidate> offeredCandidates = new ArrayList<TransportCandidate>();

    // List of remote transport candidates
    private final List<TransportCandidate> remoteCandidates = new ArrayList<TransportCandidate>();

    // Valid remote candidates
    private final List<TransportCandidate> validRemoteCandidates = new ArrayList<TransportCandidate>();

    // Accepted Remote Candidates
    private final List<TransportCandidate> acceptedRemoteCandidates = new ArrayList<TransportCandidate>();

    // The best local candidate we have offered (and accepted by the other part)
    private TransportCandidate acceptedLocalCandidate;

    // The thread that will report the result to the other end
    private Thread resultThread;

    // Listener for the resolver
    private TransportResolverListener.Resolver resolverListener;

    private ContentNegotiator parentNegotiator;

    /**
    * Default constructor.
    *
    * @param session            The Jingle session
    * @param transResolver The JingleTransportManager to use
    */
    public TransportNegotiator(JingleSession session, TransportResolver transResolver, ContentNegotiator parentNegotiator) {
        super(session);

        resolver = transResolver;
        this.parentNegotiator = parentNegotiator;

        resultThread = null;
    }

    /**
     * Get a new instance of the right TransportNegotiator class with this
     * candidate.
     *
     * @return A TransportNegotiator instance
     */
    public abstract JingleTransport getJingleTransport(TransportCandidate cand);

    /**
     * Return true if the transport candidate is acceptable for the current
     * negotiator.
     *
     * @return true if the transport candidate is acceptable
     */
    public abstract boolean acceptableTransportCandidate(TransportCandidate tc, List<TransportCandidate> localCandidates);

    /**
     * Obtain the best local candidate we want to offer.
     *
     * @return the best local candidate
     */
    public final TransportCandidate getBestLocalCandidate() {
        return resolver.getPreferredCandidate();
    }

    /**
     * Set the best local transport candidate we have offered and accepted by
     * the other endpoint.
     *
     * @param bestLocalCandidate the acceptedLocalCandidate to set
     */
    private void setAcceptedLocalCandidate(TransportCandidate bestLocalCandidate) {
        for (int i = 0; i < resolver.getCandidateCount(); i++) {
            //TODO FIX The EQUAL Sentence
            if (resolver.getCandidate(i).getIp().equals(bestLocalCandidate.getIp())
                    && resolver.getCandidate(i).getPort() == bestLocalCandidate.getPort()) {
                acceptedLocalCandidate = resolver.getCandidate(i);
                return;
            }
        }
        LOGGER.fine("BEST: ip=" + bestLocalCandidate.getIp() + " port=" + bestLocalCandidate.getPort() + " has not been offered.");
        //throw new XMPPException("Local transport candidate has not be offered.");
    }

    /**
     * Get the best accepted local candidate we have offered.
     *
     * @return a transport candidate we have offered.
     */
    public TransportCandidate getAcceptedLocalCandidate() {
        return acceptedLocalCandidate;
    }

    /**
     *  Called from above to start the negotiator during a session-initiate.
     */
    @Override
    protected void doStart() {

        try {
            sendTransportCandidatesOffer();
            setNegotiatorState(JingleNegotiatorState.PENDING);
        } catch (Exception e) {
            // TODO Auto-generated catch block
            LOGGER.log(Level.WARNING, "exception", e);
        }

    }

    /**
     * Called from above to session-terminate.
     */
    @Override
    public void close() {
        super.close();

    }

    /**
     *  Return a JingleTransport that best reflects this transport negotiator.
     */
    public JingleTransport getJingleTransport() {
        return getJingleTransport(getBestRemoteCandidate());
    }

    public List<TransportCandidate> getOfferedCandidates() {
        return offeredCandidates;
    }

    /**
     * Obtain the best common transport candidate obtained in the negotiation.
     *
     * @return the bestRemoteCandidate
     */
    public abstract TransportCandidate getBestRemoteCandidate();

    /**
     * Get the list of remote candidates.
     *
     * @return the remoteCandidates
     */
    private List<TransportCandidate> getRemoteCandidates() {
        return remoteCandidates;
    }

    /**
     * Add a remote candidate to the list. The candidate will be checked in
     * order to verify if it is usable.
     *
     * @param rc a remote candidate to add and check.
     */
    private void addRemoteCandidate(TransportCandidate rc) {
        // Add the candidate to the list
        if (rc != null) {
            if (acceptableTransportCandidate(rc, offeredCandidates)) {
                synchronized (remoteCandidates) {
                    remoteCandidates.add(rc);
                }

                // Check if the new candidate can be used.
                checkRemoteCandidate(rc);
            }
        }
    }

    /**
     * Add a offered candidate to the list.
     *
     * @param rc a remote candidate we have offered.
     */
    private void addOfferedCandidate(TransportCandidate rc) {
        // Add the candidate to the list
        if (rc != null) {
            synchronized (offeredCandidates) {
                offeredCandidates.add(rc);
            }
        }
    }

    /**
     * Check asynchronously the new transport candidate.
     *
     * @param offeredCandidate a transport candidates to check
     */
    private void checkRemoteCandidate(final TransportCandidate offeredCandidate) {
        offeredCandidate.addListener(new TransportResolverListener.Checker() {
            @Override
            public void candidateChecked(TransportCandidate cand, final boolean validCandidate) {
                if (validCandidate) {
                    if (getNegotiatorState() == JingleNegotiatorState.PENDING)
                        addValidRemoteCandidate(offeredCandidate);
                }
            }

            @Override
            public void candidateChecking(TransportCandidate cand) {
            }

        });
        offeredCandidate.check(resolver.getCandidatesList());
    }

    /**
     * Return true if the transport is established.
     *
     * @return true if the transport is established.
     */
    private boolean isEstablished() {
        return getBestRemoteCandidate() != null && getAcceptedLocalCandidate() != null;
    }

    /**
     * Return true if the transport is fully established.
     *
     * @return true if the transport is fully established.
     */
    public final boolean isFullyEstablished() {
        return (isEstablished() && ((getNegotiatorState() == JingleNegotiatorState.SUCCEEDED) || (getNegotiatorState() == JingleNegotiatorState.FAILED)));
    }

    /**
     * Launch a thread that checks, after some time, if any of the candidates
     * offered by the other endpoint is usable. The thread does not check the
     * candidates: it just checks if we have got a valid one and sends an Accept
     * in that case.
     */
    private void delayedCheckBestCandidate(final JingleSession js, final Jingle jin) {
        //
        // If this is the first insertion in the list, start the thread that
        // will send the result of our checks...
        //
        if (resultThread == null && !getRemoteCandidates().isEmpty()) {
            resultThread = new Thread(new Runnable() {

                @Override
                public void run() {

                    // Sleep for some time, waiting for the candidates checks

                    int totalTime = (CANDIDATES_ACCEPT_PERIOD + TransportResolver.CHECK_TIMEOUT);
                    int tries = (int) Math.ceil(totalTime / 1000);

                    for (int i = 0; i < tries - 1; i++) {
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            LOGGER.log(Level.WARNING, "exception", e);
                        }

                        // Once we are in pending state, look for any valid remote
                        // candidate, and send an "accept" if we have one...
                        TransportCandidate bestRemote = getBestRemoteCandidate();
                        //State state = getState();

                        if ((bestRemote != null)
                                && ((getNegotiatorState() == JingleNegotiatorState.PENDING))) {
                            // Accepting the remote candidate
                            if (!acceptedRemoteCandidates.contains(bestRemote)) {
                                Jingle jout = new Jingle(JingleActionEnum.CONTENT_ACCEPT);
                                JingleContent content = parentNegotiator.getJingleContent();
                                content.addJingleTransport(getJingleTransport(bestRemote));
                                jout.addContent(content);

                                // Send the packet
                                try {
                                    js.sendFormattedJingle(jin, jout);
                                }
                                catch (InterruptedException | NotConnectedException e) {
                                    throw new IllegalStateException(e);
                                }
                                acceptedRemoteCandidates.add(bestRemote);
                            }
                            if ((isEstablished()) && (getNegotiatorState() == JingleNegotiatorState.PENDING)) {
                                setNegotiatorState(JingleNegotiatorState.SUCCEEDED);
                                try {
                                    triggerTransportEstablished(getAcceptedLocalCandidate(), bestRemote);
                                }
                                catch (InterruptedException | NotConnectedException e) {
                                    throw new IllegalStateException(e);
                                }
                                break;
                            }
                        }
                    }

                    // Once we are in pending state, look for any valid remote
                    // candidate, and send an "accept" if we have one...
                    TransportCandidate bestRemote = getBestRemoteCandidate();

                    if (bestRemote == null) {
                        boolean foundRemoteRelay = false;
                        for (TransportCandidate candidate : remoteCandidates) {
                            if (candidate instanceof ICECandidate) {
                                ICECandidate iceCandidate = (ICECandidate) candidate;
                                if (iceCandidate.getType().equals(Type.relay)) {
                                    //TODO Check if the relay is reacheable
                                    addValidRemoteCandidate(iceCandidate);
                                    foundRemoteRelay = true;
                                }
                            }
                        }

                        // If not found, check if we offered a relay. If yes, we should accept any remote candidate.
                        // We should accept the Public One if we received it, otherwise, accepts any.
                        if (!foundRemoteRelay) {
                            boolean foundLocalRelay = false;
                            for (TransportCandidate candidate : offeredCandidates) {
                                if (candidate instanceof ICECandidate) {
                                    ICECandidate iceCandidate = (ICECandidate) candidate;
                                    if (iceCandidate.getType().equals(Type.relay)) {
                                        foundLocalRelay = true;
                                    }
                                }
                            }
                            if (foundLocalRelay) {
                                boolean foundRemotePublic = false;
                                for (TransportCandidate candidate : remoteCandidates) {
                                    if (candidate instanceof ICECandidate) {
                                        ICECandidate iceCandidate = (ICECandidate) candidate;
                                        if (iceCandidate.getType().equals(ICECandidate.Type.srflx)) {
                                            addValidRemoteCandidate(iceCandidate);
                                            foundRemotePublic = true;
                                        }
                                    }
                                }
                                if (!foundRemotePublic) {
                                    for (TransportCandidate candidate : remoteCandidates) {
                                        if (candidate instanceof ICECandidate) {
                                            ICECandidate iceCandidate = (ICECandidate) candidate;
                                            addValidRemoteCandidate(iceCandidate);
                                        }
                                    }
                                }
                            }
                        }
                    }

                    for (int i = 0; i < 6; i++) {
                        try {
                            Thread.sleep(500);
                        } catch (InterruptedException e) {
                            LOGGER.log(Level.WARNING, "exception", e);
                        }

                        bestRemote = getBestRemoteCandidate();
                        //State state = getState();
                        if ((bestRemote != null)
                                && ((getNegotiatorState() == JingleNegotiatorState.PENDING))) {
                            if (!acceptedRemoteCandidates.contains(bestRemote)) {
                                Jingle jout = new Jingle(JingleActionEnum.CONTENT_ACCEPT);
                                JingleContent content = parentNegotiator.getJingleContent();
                                content.addJingleTransport(getJingleTransport(bestRemote));
                                jout.addContent(content);

                                // Send the packet
                                try {
                                    js.sendFormattedJingle(jin, jout);
                                }
                                catch (InterruptedException | NotConnectedException e) {
                                    throw new IllegalStateException(e);
                                }
                                acceptedRemoteCandidates.add(bestRemote);
                            }
                            if (isEstablished()) {
                                setNegotiatorState(JingleNegotiatorState.SUCCEEDED);
                                break;
                            }
                        }
                    }

                    if (getNegotiatorState() != JingleNegotiatorState.SUCCEEDED) {
                        try {
                            session
                                    .terminate("Unable to negotiate session. This may be caused by firewall configuration problems.");
                        } catch (Exception e) {
                            LOGGER.log(Level.WARNING, "exception", e);
                        }
                    }
                }
            }, "Waiting for all the transport candidates checks...");

            resultThread.setName("Transport Resolver Result");
            resultThread.start();
        }
    }

    /**
     * Add a valid remote candidate to the list. The remote candidate has been
     * checked, and the remote
     *
     * @param remoteCandidate a remote candidate to add
     */
    private void addValidRemoteCandidate(TransportCandidate remoteCandidate) {
        // Add the candidate to the list
        if (remoteCandidate != null) {
            synchronized (validRemoteCandidates) {
                LOGGER.fine("Added valid candidate: " + remoteCandidate.getIp() + ":" + remoteCandidate.getPort());
                validRemoteCandidates.add(remoteCandidate);
            }
        }
    }

    /**
     * Get the list of valid (ie, checked) remote candidates.
     *
     * @return The list of valid (ie, already checked) remote candidates.
     */
    final ArrayList<TransportCandidate> getValidRemoteCandidatesList() {
        synchronized (validRemoteCandidates) {
            return new ArrayList<TransportCandidate>(validRemoteCandidates);
        }
    }

    /**
     * Get an iterator for the list of valid (ie, checked) remote candidates.
     *
     * @return The iterator for the list of valid (ie, already checked) remote
     *         candidates.
     */
    public final Iterator<TransportCandidate> getValidRemoteCandidates() {
        return Collections.unmodifiableList(getRemoteCandidates()).iterator();
    }

    /**
     * Add an offered remote candidate. The transport candidate can be unusable:
     * we must check if we can use it.
     *
     * @param rc the remote candidate to add.
     */
    private void addRemoteCandidates(List<TransportCandidate> rc) {
        if (rc != null) {
            if (rc.size() > 0) {
                for (TransportCandidate aRc : rc) {
                    addRemoteCandidate(aRc);
                }
            }
        }
    }

    /**
     * Parse the list of transport candidates from a Jingle packet.
     *
     * @param jin The input jingle packet
     */
    private List<TransportCandidate> obtainCandidatesList(Jingle jingle) {
        List<TransportCandidate> result = new ArrayList<TransportCandidate>();

        if (jingle != null) {
            // Get the list of candidates from the packet
            for (JingleContent jingleContent : jingle.getContentsList()) {
                if (jingleContent.getName().equals(parentNegotiator.getName())) {
                    for (JingleTransport jingleTransport : jingleContent.getJingleTransportsList()) {
                        for (JingleTransportCandidate jingleTransportCandidate : jingleTransport.getCandidatesList()) {
                            TransportCandidate transCand = jingleTransportCandidate.getMediaTransport();
                            result.add(transCand);
                        }
                    }
                }
            }
        }

        return result;
    }

    /**
     * Send an offer for a transport candidate
     *
     * @param cand
     * @throws NotConnectedException 
     * @throws InterruptedException 
     */
    private synchronized void sendTransportCandidateOffer(TransportCandidate cand) throws NotConnectedException, InterruptedException {
        if (!cand.isNull()) {
            // Offer our new candidate...
            addOfferedCandidate(cand);
            JingleContent content = parentNegotiator.getJingleContent();
            content.addJingleTransport(getJingleTransport(cand));
            Jingle jingle = new Jingle(JingleActionEnum.TRANSPORT_INFO);
            jingle.addContent(content);

            // We SHOULD NOT be sending packets directly.
            // This circumvents the state machinery.
            // TODO - work this into the state machinery.
            session.sendFormattedJingle(jingle);
        }
    }

    /**
     * Create a Jingle stanza(/packet) where we announce our transport candidates.
     *
     * @throws XMPPException
     * @throws SmackException 
     * @throws InterruptedException 
     */
    private void sendTransportCandidatesOffer() throws XMPPException, SmackException, InterruptedException {
        List<TransportCandidate> notOffered = resolver.getCandidatesList();

        notOffered.removeAll(offeredCandidates);

        // Send any unset candidate
        for (Object aNotOffered : notOffered) {
            sendTransportCandidateOffer((TransportCandidate) aNotOffered);
        }

        // .. and start a listener that will send any future candidate
        if (resolverListener == null) {
            // Add a listener that sends the offer when the resolver finishes...
            resolverListener = new TransportResolverListener.Resolver() {
                @Override
                public void candidateAdded(TransportCandidate cand) throws NotConnectedException, InterruptedException {
                    sendTransportCandidateOffer(cand);
                }

                @Override
                public void end() {
                }

                @Override
                public void init() {
                }
            };

            resolver.addListener(resolverListener);
        }

        if (!(resolver.isResolving() || resolver.isResolved())) {
            // Resolve our IP and port
            LOGGER.fine("RESOLVER CALLED");
            resolver.resolve(session);
        }
    }

    /**
     * Dispatch an incoming packet. The method is responsible for recognizing
     * the stanza(/packet) type and, depending on the current state, deliverying the
     * stanza(/packet) to the right event handler and wait for a response.
     *
     * @param iq the stanza(/packet) received
     * @return the new Jingle stanza(/packet) to send.
     * @throws XMPPException
     * @throws SmackException 
     * @throws InterruptedException 
     */
    @Override
    public final List<IQ> dispatchIncomingPacket(IQ iq, String id) throws XMPPException, SmackException, InterruptedException {
        List<IQ> responses = new ArrayList<IQ>();
        IQ response = null;

        if (iq != null) {
            if (iq.getType().equals(IQ.Type.error)) {
                // Process errors
                setNegotiatorState(JingleNegotiatorState.FAILED);
                triggerTransportClosed(null);
                // This next line seems wrong, and may subvert the normal closing process.
                throw new JingleException(iq.getError().getDescriptiveText());
            } else if (iq.getType().equals(IQ.Type.result)) {
                // Process ACKs
                if (isExpectedId(iq.getStanzaId())) {
                    response = receiveResult(iq);
                    removeExpectedId(iq.getStanzaId());
                }
            } else if (iq instanceof Jingle) {
                // Get the action from the Jingle packet
                Jingle jingle = (Jingle) iq;
                JingleActionEnum action = jingle.getAction();

                switch (action) {
                    case CONTENT_ACCEPT:
                        response = receiveContentAcceptAction(jingle);
                        break;

                    case CONTENT_MODIFY:
                        break;

                    case CONTENT_REMOVE:
                        break;

                    case SESSION_INFO:
                        break;

                    case SESSION_INITIATE:
                        response = receiveSessionInitiateAction(jingle);
                        break;

                    case SESSION_ACCEPT:
                        response = receiveSessionAcceptAction(jingle);
                        break;

                    case TRANSPORT_INFO:
                        response = receiveTransportInfoAction(jingle);
                        break;

                    default:
                        break;
                }
            }
        }

        if (response != null) {
            addExpectedId(response.getStanzaId());
            responses.add(response);
        }

        return responses;
    }

    /**
     * The other endpoint has partially accepted our invitation: start
     * offering a list of candidates.
     *
     * @return an IQ packet
     * @throws XMPPException
     * @throws SmackException 
     * @throws InterruptedException 
     */
    private Jingle receiveResult(IQ iq) throws XMPPException, SmackException, InterruptedException {
        Jingle response = null;

        sendTransportCandidatesOffer();
        setNegotiatorState(JingleNegotiatorState.PENDING);

        return response;
    }

    /**
     *  @param jingle
     *  @param jingleTransport
     *  @return the iq
     * @throws SmackException 
     * @throws InterruptedException 
     */
    private IQ receiveSessionInitiateAction(Jingle jingle) throws XMPPException, SmackException, InterruptedException {
        IQ response = null;

        // Parse the Jingle and get any proposed transport candidates
        //addRemoteCandidates(obtainCandidatesList(jin));

        // Start offering candidates
        sendTransportCandidatesOffer();

        // All these candidates will be checked asyncronously. Wait for some
        // time and check if we have a valid candidate to use...
        delayedCheckBestCandidate(session, jingle);

        // Set the next state
        setNegotiatorState(JingleNegotiatorState.PENDING);

        return response;
    }

    /**
     *  @param jingle
     *  @param jingleTransport
     *  @return the iq
     */
    private IQ receiveTransportInfoAction(Jingle jingle) {
        IQ response = null;

        // Parse the Jingle and get any proposed transport candidates
        //addRemoteCandidates(obtainCandidatesList(jin));

        //        // Start offering candidates
        //        sendTransportCandidatesOffer();
        //
        //        // All these candidates will be checked asyncronously. Wait for some
        //        // time and check if we have a valid candidate to use...
        //        delayedCheckBestCandidate(session, jingle);
        //
        //        // Set the next state
        //        setNegotiatorState(JingleNegotiatorState.PENDING);

        // Parse the Jingle and get any proposed transport candidates
        addRemoteCandidates(obtainCandidatesList(jingle));

        // Wait for some time and check if we have a valid candidate to
        // use...
        delayedCheckBestCandidate(session, jingle);

        response = session.createAck(jingle);

        return response;
    }

    /**
     * One of our transport candidates has been accepted.
     *
     * @param jin The input packet
     * @return a Jingle packet
     * @throws XMPPException an exception
     * @see org.jivesoftware.smackx.jingleold.JingleNegotiator.State#eventAccept(org.jivesoftware.smackx.jingleold.packet.Jingle)
     */
    private IQ receiveContentAcceptAction(Jingle jingle) throws XMPPException {
        IQ response = null;

        // Parse the Jingle and get the accepted candidate
        List<TransportCandidate> accepted = obtainCandidatesList(jingle);
        if (!accepted.isEmpty()) {

            for (TransportCandidate cand : accepted) {
                LOGGER.fine("Remote acccepted candidate addr: " + cand.getIp());
            }

            TransportCandidate cand = accepted.get(0);
            setAcceptedLocalCandidate(cand);

            if (isEstablished()) {
                LOGGER.fine(cand.getIp() + " is set active");
                //setNegotiatorState(JingleNegotiatorState.SUCCEEDED);
            }
        }
        return response;
    }

    /**
     *  @param jingle
     *  @return the iq
     */
    private static IQ receiveSessionAcceptAction(Jingle jingle) {
        IQ response = null;

        LOGGER.fine("Transport stabilished");
        //triggerTransportEstablished(getAcceptedLocalCandidate(), getBestRemoteCandidate());

        //setNegotiatorState(JingleNegotiatorState.SUCCEEDED);

        return response;
    }

    /**
     * Trigger a Transport session established event.
     *
     * @param local  TransportCandidate that has been agreed.
     * @param remote TransportCandidate that has been agreed.
     * @throws NotConnectedException 
     * @throws InterruptedException 
     */
    private void triggerTransportEstablished(TransportCandidate local, TransportCandidate remote) throws NotConnectedException, InterruptedException {
        List<JingleListener> listeners = getListenersList();
        for (JingleListener li : listeners) {
            if (li instanceof JingleTransportListener) {
                JingleTransportListener mli = (JingleTransportListener) li;
                LOGGER.fine("triggerTransportEstablished " + local.getLocalIp() + ":" + local.getPort() + " <-> "
                        + remote.getIp() + ":" + remote.getPort());
                mli.transportEstablished(local, remote);
            }
        }
    }

    /**
     * Trigger a Transport closed event.
     *
     * @param cand current TransportCandidate that is cancelled.
     */
    private void triggerTransportClosed(TransportCandidate cand) {
        List<JingleListener> listeners = getListenersList();
        for (JingleListener li : listeners) {
            if (li instanceof JingleTransportListener) {
                JingleTransportListener mli = (JingleTransportListener) li;
                mli.transportClosed(cand);
            }
        }
    }

    // Subclasses

    /**
     * Raw-UDP transport negotiator.
     *
     * @author Alvaro Saurin <alvaro.saurin@gmail.com>
     */
    public static final class RawUdp extends TransportNegotiator {

        /**
         * Default constructor, with a JingleSession and transport manager.
         *
         * @param js  The Jingle session this negotiation belongs to.
         * @param res The transport resolver to use.
         */
        public RawUdp(JingleSession js, final TransportResolver res, ContentNegotiator parentNegotiator) {
            super(js, res, parentNegotiator);
        }

        /**
         * Get a TransportNegotiator instance.
         */
        @Override
        public org.jivesoftware.smackx.jingleold.packet.JingleTransport getJingleTransport(TransportCandidate bestRemote) {
            org.jivesoftware.smackx.jingleold.packet.JingleTransport.RawUdp jt = new org.jivesoftware.smackx.jingleold.packet.JingleTransport.RawUdp();
            jt.addCandidate(new org.jivesoftware.smackx.jingleold.packet.JingleTransport.RawUdp.Candidate(bestRemote));
            return jt;
        }

        /**
         * Obtain the best common transport candidate obtained in the
         * negotiation.
         *
         * @return the bestRemoteCandidate
         */
        @Override
        public TransportCandidate getBestRemoteCandidate() {
            // Hopefully, we only have one validRemoteCandidate
            ArrayList<TransportCandidate> cands = getValidRemoteCandidatesList();
            if (!cands.isEmpty()) {
                LOGGER.fine("RAW CAND");
                return cands.get(0);
            } else {
                LOGGER.fine("No Remote Candidate");
                return null;
            }
        }

        /**
         * Return true for fixed candidates.
         */
        @Override
        public boolean acceptableTransportCandidate(TransportCandidate tc, List<TransportCandidate> localCandidates) {
            return tc instanceof TransportCandidate.Fixed;
        }
    }

    /**
     * Ice transport negotiator.
     *
     * @author Alvaro Saurin <alvaro.saurin@gmail.com>
     */
    public static final class Ice extends TransportNegotiator {

        /**
         * Default constructor, with a JingleSession and transport manager.
         *
         * @param js  The Jingle session this negotiation belongs to.
         * @param res The transport manager to use.
         */
        public Ice(JingleSession js, final TransportResolver res, ContentNegotiator parentNegotiator) {
            super(js, res, parentNegotiator);
        }

        /**
         * Get a TransportNegotiator instance.
         *
         * @param candidate
         */
        @Override
        public org.jivesoftware.smackx.jingleold.packet.JingleTransport getJingleTransport(TransportCandidate candidate) {
            org.jivesoftware.smackx.jingleold.packet.JingleTransport.Ice jt = new org.jivesoftware.smackx.jingleold.packet.JingleTransport.Ice();
            jt.addCandidate(new org.jivesoftware.smackx.jingleold.packet.JingleTransport.Ice.Candidate(candidate));
            return jt;
        }

        /**
         * Obtain the best remote candidate obtained in the negotiation so far.
         *
         * @return the bestRemoteCandidate
         */
        @Override
        public TransportCandidate getBestRemoteCandidate() {
            ICECandidate result = null;

            ArrayList<TransportCandidate> cands = getValidRemoteCandidatesList();
            if (!cands.isEmpty()) {
                int highest = -1;
                ICECandidate chose = null;
                for (TransportCandidate transportCandidate : cands) {
                    if (transportCandidate instanceof ICECandidate) {
                        ICECandidate icecandidate = (ICECandidate) transportCandidate;
                        if (icecandidate.getPreference() > highest) {
                            chose = icecandidate;
                            highest = icecandidate.getPreference();
                        }
                    }
                }
                result = chose;
            }

            if (result != null && result.getType().equals(Type.relay))
                LOGGER.fine("Relay Type");

            return result;
        }

        /**
         * Return true for ICE candidates.
         */
        @Override
        public boolean acceptableTransportCandidate(TransportCandidate tc, List<TransportCandidate> localCandidates) {
            return tc instanceof ICECandidate;
        }
    }
}
