/**
 *
 * 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.jingle.nat;

import de.javawi.jstun.test.demo.StunServer;
import de.javawi.jstun.test.demo.ice.Candidate;
import de.javawi.jstun.test.demo.ice.ICENegociator;
import de.javawi.jstun.util.UtilityException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.test.SmackTestCase;
import org.jivesoftware.smackx.jingle.JingleManager;
import org.jivesoftware.smackx.jingle.JingleSession;
import org.jivesoftware.smackx.jingle.JingleSessionRequest;
import org.jivesoftware.smackx.jingle.listeners.JingleSessionListener;
import org.jivesoftware.smackx.jingle.listeners.JingleSessionRequestListener;
import org.jivesoftware.smackx.jingle.media.JingleMediaManager;
import org.jivesoftware.smackx.jingle.media.PayloadType;
import org.jivesoftware.smackx.jingle.mediaimpl.test.TestMediaManager;
import org.jivesoftware.smackx.jingle.nat.STUNResolver.STUNService;

import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;

/**
 * Test the STUN IP resolver.
 *
 * @author Thiago Camargo
 */
public class STUNResolverTest extends SmackTestCase {

    // Counter management

    public STUNResolverTest(final String arg) {
        super(arg);
    }

    private int counter;

    private final Object mutex = new Object();

    private void resetCounter() {
        synchronized (mutex) {
            counter = 0;
        }
    }

    private void incCounter() {
        synchronized (mutex) {
            counter++;
        }
    }

    private int valCounter() {
        int val;
        synchronized (mutex) {
            val = counter;
        }
        return val;
    }

    /**
     * Test for getPreferredCandidate()
     *
     * @throws Exception if an exception occurs.
     */
    public void testGetPreferredCandidate() throws Exception {
        int highestPref = 100;

        TransportCandidate cand1 = new ICECandidate("192.168.2.1", 3, 2, "password", 3468, "username1", 1, ICECandidate.Type.prflx);
        TransportCandidate cand2 = new ICECandidate("192.168.5.1", 2, 10, "password", 3469, "username2", 15,
                ICECandidate.Type.prflx);
        TransportCandidate candH = new ICECandidate("192.168.2.11", 1, 2, "password", 3468, "usernameH", highestPref,
                ICECandidate.Type.prflx);
        TransportCandidate cand3 = new ICECandidate("192.168.2.10", 2, 10, "password", 3469, "username3", 2,
                ICECandidate.Type.prflx);
        TransportCandidate cand4 = new ICECandidate("192.168.4.1", 3, 2, "password", 3468, "username4", 78, ICECandidate.Type.prflx);

        STUNResolver stunResolver = new STUNResolver() {
        };
        stunResolver.addCandidate(cand1);
        stunResolver.addCandidate(cand2);
        stunResolver.addCandidate(candH);
        stunResolver.addCandidate(cand3);
        stunResolver.addCandidate(cand4);

        assertEquals(stunResolver.getPreferredCandidate(), candH);
    }

    /**
     * Test for getPreferredCandidate()
     *
     * @throws Exception if an exception occurs.
     */
    public void testGetPreferredCandidateICE() throws Exception {
        int highestPref = 100;

        TransportCandidate cand1 = new ICECandidate("192.168.2.1", 3, 2, "password", 3468, "username1", 1, ICECandidate.Type.prflx);
        TransportCandidate cand2 = new ICECandidate("192.168.5.1", 2, 10, "password", 3469, "username2", 15,
                ICECandidate.Type.prflx);
        TransportCandidate candH = new ICECandidate("192.168.2.11", 1, 2, "password", 3468, "usernameH", highestPref,
                ICECandidate.Type.prflx);
        TransportCandidate cand3 = new ICECandidate("192.168.2.10", 2, 10, "password", 3469, "username3", 2,
                ICECandidate.Type.prflx);
        TransportCandidate cand4 = new ICECandidate("192.168.4.1", 3, 2, "password", 3468, "username4", 78, ICECandidate.Type.prflx);

        ICEResolver iceResolver = new ICEResolver(getConnection(0), "stun.xten.net", 3478) {
        };
        iceResolver.addCandidate(cand1);
        iceResolver.addCandidate(cand2);
        iceResolver.addCandidate(candH);
        iceResolver.addCandidate(cand3);
        iceResolver.addCandidate(cand4);

        assertEquals(iceResolver.getPreferredCandidate(), candH);
    }

    /**
     * Test priority generated by STUN lib
     *
     * @throws Exception if an exception occurs.
     */
    public void testICEPriority() throws Exception {

        String first = "";

        for (int i = 0; i < 100; i++) {

            ICENegociator cc = new ICENegociator((short) 1);
            // gather candidates
            cc.gatherCandidateAddresses();
            // priorize candidates
            cc.prioritizeCandidates();
            // get SortedCandidates

            for (Candidate candidate : cc.getSortedCandidates()) {
                short nicNum = 0;
				try {
					Enumeration<NetworkInterface> nics = NetworkInterface.getNetworkInterfaces();
					short tempNic = 0;
					NetworkInterface nic = NetworkInterface.getByInetAddress(candidate.getAddress().getInetAddress());
					while(nics.hasMoreElements()) {
						NetworkInterface checkNIC = nics.nextElement();
						if (checkNIC.equals(nic)) {
							nicNum = tempNic;
							break;
						}
						i++;
					}
				} catch (SocketException e1) {
					// TODO Auto-generated catch block
					e1.printStackTrace();
				}

                try {
                    TransportCandidate transportCandidate = new ICECandidate(candidate.getAddress().getInetAddress()
                            .getHostAddress(), 1, nicNum, "1", candidate.getPort(), "1", candidate.getPriority(),
                            ICECandidate.Type.prflx);
                    transportCandidate.setLocalIp(candidate.getBase().getAddress().getInetAddress().getHostAddress());
                    System.out.println("C: " + candidate.getAddress().getInetAddress() + "|"
                            + candidate.getBase().getAddress().getInetAddress() + " p:" + candidate.getPriority());
                } catch (UtilityException e) {
                    LOGGER.log(Level.WARNING, "exception", e);
                } catch (UnknownHostException e) {
                    LOGGER.log(Level.WARNING, "exception", e);
                }
            }
            Candidate candidate = cc.getSortedCandidates().get(0);
            String temp = "C: " + candidate.getAddress().getInetAddress() + "|" + candidate.getBase().getAddress().getInetAddress()
                    + " p:" + candidate.getPriority();
            if (first.equals(""))
                first = temp;
            assertEquals(first, temp);
            first = temp;
        }
    }

    /**
     * Test for loadSTUNServers()
     *
     * @throws Exception if an exception occurs.
     */
    public void testLoadSTUNServers() throws Exception {
        STUNResolver stunResolver = new STUNResolver() {
        };
        ArrayList<STUNService> stunServers = stunResolver.loadSTUNServers();

        assertTrue(stunServers.size() > 0);
        System.out.println(stunServers.size() + " servers loaded");
    }

    public void testGetSTUNServer() {

        System.out.println(STUN.serviceAvailable(getConnection(0)));
        STUN stun = STUN.getSTUNServer(getConnection(0));
        for (STUN.StunServerAddress stunServerAddress : stun.getServers())
            System.out.println(stunServerAddress.getServer() + ":" + stunServerAddress.getPort());

        System.out.println(stun.getPublicIp());

    }

    /**
     * Test for resolve()
     *
     * @throws Exception if an exception occurs.
     */
    public void testResolve() throws Exception {

        final STUNResolver stunResolver = new STUNResolver() {
        };

        stunResolver.addListener(new TransportResolverListener.Resolver() {

            public void candidateAdded(final TransportCandidate cand) {
                incCounter();

                String addr = cand.getIp();
                int port = cand.getPort();

                System.out.println("Addr: " + addr + " port:" + port);

            }

            public void init() {
                System.out.println("Resolution started");
            }

            public void end() {
                System.out.println("Resolution finished");
            }
        });

        try {
            stunResolver.initializeAndWait();
            Thread.sleep(55000);
            assertTrue(valCounter() > 0);
        } catch (Exception e) {
            LOGGER.log(Level.WARNING, "exception", e);
        }
    }

    /**
     * Generate a list of payload types
     *
     * @return A testing list
     */
    private ArrayList<PayloadType> getTestPayloads1() {
        ArrayList<PayloadType> result = new ArrayList<PayloadType>();

        result.add(new PayloadType.Audio(34, "supercodec-1", 2, 14000));
        result.add(new PayloadType.Audio(56, "supercodec-2", 1, 44000));
        result.add(new PayloadType.Audio(36, "supercodec-3", 2, 28000));
        result.add(new PayloadType.Audio(45, "supercodec-4", 1, 98000));

        return result;
    }

    private ArrayList<PayloadType> getTestPayloads2() {
        ArrayList<PayloadType> result = new ArrayList<PayloadType>();

        result.add(new PayloadType.Audio(27, "supercodec-3", 2, 28000));
        result.add(new PayloadType.Audio(56, "supercodec-2", 1, 44000));
        result.add(new PayloadType.Audio(32, "supercodec-4", 1, 98000));
        result.add(new PayloadType.Audio(34, "supercodec-1", 2, 14000));

        return result;
    }

    /**
     * This is a simple test where the user_2 rejects the Jingle session.
     */
    public void testSTUNJingleSession() {

        resetCounter();

        try {
            TransportResolver tr1 = new STUNResolver() {
            };
            TransportResolver tr2 = new STUNResolver() {
            };

            // Explicit resolution
            tr1.resolve(null);
            tr2.resolve(null);

            STUNTransportManager stm0 = new STUNTransportManager();
            TestMediaManager tmm0 = new TestMediaManager(stm0);
            tmm0.setPayloads(getTestPayloads1());
            List<JingleMediaManager> trl0 = new ArrayList<JingleMediaManager>();
            trl0.add(tmm0);

            STUNTransportManager stm1 = new STUNTransportManager();
            TestMediaManager tmm1 = new TestMediaManager(stm1);
            tmm1.setPayloads(getTestPayloads2());
            List<JingleMediaManager> trl1 = new ArrayList<JingleMediaManager>();
            trl1.add(tmm1);

            final JingleManager man0 = new JingleManager(getConnection(0), trl0);
            final JingleManager man1 = new JingleManager(getConnection(1), trl1);

            man1.addJingleSessionRequestListener(new JingleSessionRequestListener() {
                /**
                 * Called when a new session request is detected
                 */
                public void sessionRequested(final JingleSessionRequest request) {
                    System.out.println("Session request detected, from " + request.getFrom() + ": accepting.");

                    // We accept the request
                    JingleSession session1;
                    try {
                        session1 = request.accept();
                        session1.addListener(new JingleSessionListener() {
                            public void sessionClosed(String reason, JingleSession jingleSession) {
                            }

                            public void sessionClosedOnError(XMPPException e, JingleSession jingleSession) {
                            }

                            public void sessionDeclined(String reason, JingleSession jingleSession) {
                            }

                            public void sessionEstablished(PayloadType pt, TransportCandidate rc, TransportCandidate lc,
                                    JingleSession jingleSession) {
                                incCounter();
                                System.out.println("Responder: the session is fully established.");
                                System.out.println("+ Payload Type: " + pt.getId());
                                System.out.println("+ Local IP/port: " + lc.getIp() + ":" + lc.getPort());
                                System.out.println("+ Remote IP/port: " + rc.getIp() + ":" + rc.getPort());
                            }

                            public void sessionRedirected(String redirection, JingleSession jingleSession) {
                            }

                            public void sessionMediaReceived(JingleSession jingleSession, String participant) {
                                // Do Nothing
                            }
                        });
                        session1.startIncoming();
                    } catch (XMPPException e) {
                        LOGGER.log(Level.WARNING, "exception", e);
                    }
                }
            });

            // Session 0 starts a request
            System.out.println("Starting session request, to " + getFullJID(1) + "...");
            JingleSession session0 = man0.createOutgoingJingleSession(getFullJID(1));

            session0.addListener(new JingleSessionListener() {
                public void sessionClosed(String reason, JingleSession jingleSession) {
                }

                public void sessionClosedOnError(XMPPException e, JingleSession jingleSession) {
                }

                public void sessionDeclined(String reason, JingleSession jingleSession) {
                }

                public void sessionEstablished(PayloadType pt, TransportCandidate rc, TransportCandidate lc,
                        JingleSession jingleSession) {
                    incCounter();
                    System.out.println("Initiator: the session is fully established.");
                    System.out.println("+ Payload Type: " + pt.getId());
                    System.out.println("+ Local IP/port: " + lc.getIp() + ":" + lc.getPort());
                    System.out.println("+ Remote IP/port: " + rc.getIp() + ":" + rc.getPort());
                }

                public void sessionMediaReceived(JingleSession jingleSession, String participant) {
                    // Do Nothing
                }

                public void sessionRedirected(String redirection, JingleSession jingleSession) {
                }
            });
            session0.startOutgoing();

            Thread.sleep(60000);

            assertTrue(valCounter() == 2);

        } catch (Exception e) {
            LOGGER.log(Level.WARNING, "exception", e);
            fail("An error occurred with Jingle");
        }
    }

    protected int getMaxConnections() {
        return 2;
    }
}
