/**
 * $Revision $
 * $Date $
 *
 * Copyright (C) 2005-2010 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.ifsoft.rayo;

import java.io.*;
import java.lang.reflect.*;
import java.net.*;
import java.util.*;
import java.util.jar.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;

import org.jivesoftware.openfire.container.Plugin;
import org.jivesoftware.openfire.container.PluginManager;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.openfire.session.LocalClientSession;
import org.jivesoftware.openfire.session.ClientSession;
import org.jivesoftware.openfire.session.Session;
import org.jivesoftware.openfire.event.SessionEventDispatcher;
import org.jivesoftware.openfire.event.SessionEventListener;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.xmpp.component.ComponentException;
import org.xmpp.component.ComponentManager;
import org.xmpp.component.ComponentManagerFactory;
import org.xmpp.jnodes.*;
import org.xmpp.jnodes.nio.LocalIPResolver;
import org.xmpp.jnodes.nio.PublicIPResolver;
import org.xmpp.packet.*;

import org.jitsi.util.*;

import com.rayo.core.verb.*;

import org.voicebridge.*;

import com.sun.voip.server.*;
import com.sun.voip.*;



public class RayoPlugin implements Plugin, SessionEventListener  {

    private static final Logger Log = LoggerFactory.getLogger(RayoPlugin.class);

    public static final String JN_PUB_IP_PROPERTY = "rayo.publicip";
    private ComponentManager componentManager;

   	private final ConcurrentHashMap<String, RelayChannel> channels = new ConcurrentHashMap<String, RelayChannel>();
    private final long timeout = 60000;
    private final AtomicInteger ids = new AtomicInteger(0);
    private final ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);
    private final String serviceName = "rayo";

    private final boolean bindAllInterfaces;

    private boolean hasPublicIP = false;

    private Application bridge = new Application();
    private RayoComponent component = null;

    public RayoPlugin() {
        final String os = System.getProperty("os.name");
        bindAllInterfaces = !(os != null && os.toLowerCase().indexOf("win") > -1);
    }

    public String getName() {
        return "rayo";
    }

    public String getDescription() {
        return "Rayo Plugin";
    }

    public void initializePlugin(PluginManager manager, File pluginDirectory) {
        componentManager = ComponentManagerFactory.getComponentManager();
        component = new RayoComponent(this);
        try {
            componentManager.addComponent(serviceName, component);
        	bridge.appStart(pluginDirectory);
        	checkNatives(pluginDirectory);
        	checkRecordingFolder(pluginDirectory);
        	SessionEventDispatcher.addListener(this);
        } catch (ComponentException e) {
            Log.error("Could NOT load " + component.getName());
        }
        setup();

        component.doStart();
    }

    private void setup() {
        executor.scheduleWithFixedDelay(new Runnable() {
            public void run() {
                for (final RelayChannel c : channels.values()) {
                    final long current = System.currentTimeMillis();
                    final long da = current - c.getLastReceivedTimeA();
                    final long db = current - c.getLastReceivedTimeB();

                    if (da > timeout || db > timeout) {
                        removeChannel(c);
                    }
                }
            }
        }, timeout, timeout, TimeUnit.MILLISECONDS);
        Log.info("Jingle Nodes Loaded.");

        verifyNetwork();
    }

    public void verifyNetwork() {
        final String localAddress = JiveGlobals.getProperty(JN_PUB_IP_PROPERTY, LocalIPResolver.getLocalIP());
        LocalIPResolver.setOverrideIp(localAddress);
        final InetSocketAddress publicAddress = PublicIPResolver.getPublicAddress("stun.l.google.com", 19302);
        hasPublicIP = publicAddress != null && publicAddress.getAddress().getHostAddress().equals(localAddress);
    }

    private void closeAllChannels() {
        for (final RelayChannel c : channels.values()) {
            removeChannel(c);
        }
    }

    public RelayChannel getRelayChannel(String id)
    {
		if (channels.containsKey(id))
			return channels.get(id);
		else
			return null;
	}

    public void handleVoiceBridge(String id, String operation, String parameter)
    {
			Log.info("handleVoiceBridge " + id);
			bridge.manageCallParticipant(id, operation, parameter);
	}


    public RelayChannel createRelayChannel(JID jid, Handset handset) {
        RelayChannel rc = null;

        try {
            rc = RelayChannel.createLocalRelayChannel(bindAllInterfaces ? "0.0.0.0" : LocalIPResolver.getLocalIP(), 30000, 50000);
            final int id = ids.incrementAndGet();
            final String sId = JID.escapeNode(jid.toString());
            rc.setAttachment(sId);
            rc.setFrom(jid, component);
            rc.setCrypto(handset);
            channels.put(sId, rc);

        } catch (IOException e) {
            Log.error("Could Not Create Channel.", e);
        }

        return rc;
    }

    public void removeChannel(final RelayChannel c) {
        channels.remove(c.getAttachment());
        c.close();
    }

    public String getServiceName() {
        return serviceName;
    }

    public void destroyPlugin() {
        try {
            componentManager.removeComponent(serviceName);
            bridge.appStop();
            SessionEventDispatcher.removeListener(this);
        } catch (ComponentException e) {
            Log.error("Could NOT Remove " + serviceName + " Component");
        }
        closeAllChannels();
        executor.shutdownNow();
        component.doStop();
    }

    public boolean hasPublicIP() {
        return hasPublicIP;
    }

    public int getActiveChannelCount() {
        return channels.size();
    }

    /**
     * Checks whether we have folder with extracted natives, if missing
     * find the appropriate jar file and extract them. Normally this is
     * done once when plugin is installed or updated.
     * If folder with natives exist add it to the java.library.path so
     * rayo can use those native libs.
     */
    private void checkNatives(File pluginDirectory)
    {
        // Find the root path of the class that will be our plugin lib folder.
        try
        {
			String nativeLibsJarPath = pluginDirectory.getAbsolutePath() + File.separator + "lib";
 			Log.info("checkNatives." + nativeLibsJarPath);

            File nativeLibFolder = new File(nativeLibsJarPath, "native");

            if(!nativeLibFolder.exists())
            {
                nativeLibFolder.mkdirs();

                // lets find the appropriate jar file to extract and
                // extract it
                String jarFileSuffix = null;
                if(OSUtils.IS_LINUX32)
                {
                    jarFileSuffix = "rayo-native-linux-32.jar";
                }
                else if(OSUtils.IS_LINUX64)
                {
                    jarFileSuffix = "rayo-native-linux-64.jar";
                }
                else if(OSUtils.IS_WINDOWS32)
                {
                    jarFileSuffix = "rayo-native-windows-32.jar";
                }
                else if(OSUtils.IS_WINDOWS64)
                {
                    jarFileSuffix = "rayo-native-windows-64.jar";
                }
                else if(OSUtils.IS_MAC)
                {
                    jarFileSuffix = "rayo-native-macosx.jar";
                }

                JarFile jar = new JarFile(nativeLibsJarPath + File.separator + jarFileSuffix);
                Enumeration en = jar.entries();

                while (en.hasMoreElements())
                {
                    try
                    {
                        JarEntry file = (JarEntry) en.nextElement();
                        File f = new File(nativeLibFolder, file.getName());
                        if (file.isDirectory())
                        {
                            continue;
                        }

                        InputStream is = jar.getInputStream(file);
                        FileOutputStream fos = new FileOutputStream(f);
                        while (is.available() > 0)
                        {
                            fos.write(is.read());
                        }
                        fos.close();
                        is.close();
                    }
                    catch(Throwable t)
                    {}
                }

                Log.info("Native lib folder created and natives extracted");
            }
            else
                Log.info("Native lib folder already exist.");

            String newLibPath = nativeLibFolder.getCanonicalPath() + File.pathSeparator + System.getProperty("java.library.path");
            System.setProperty("java.library.path", newLibPath);

            // this will reload the new setting
            Field fieldSysPath = ClassLoader.class.getDeclaredField("sys_paths");
            fieldSysPath.setAccessible(true);
            fieldSysPath.set(System.class.getClassLoader(), null);
        }
        catch (Exception e)
        {
            Log.error(e.getMessage(), e);
        }
    }

    private void checkRecordingFolder(File pluginDirectory)
    {
		String rayoHome = JiveGlobals.getHomeDirectory() + File.separator + "resources" + File.separator + "spank" + File.separator + "rayo";

        try
        {
			File rayoFolderPath = new File(rayoHome);

            if(!rayoFolderPath.exists())
            {
                rayoFolderPath.mkdirs();

			}

			File recordingFolderPath = new File(rayoHome + File.separator + "recordings");

            if(!recordingFolderPath.exists())
            {
                recordingFolderPath.mkdirs();

			}

			File soundsFolderPath = new File(rayoHome + File.separator + "sounds");

            if(!soundsFolderPath.exists())
            {
                soundsFolderPath.mkdirs();

			}
        }
        catch (Exception e)
        {
            Log.error(e.getMessage(), e);
        }
	}

	public void anonymousSessionCreated(Session session)
	{
		Log.debug("RayoPlugin anonymousSessionCreated "+ session.getAddress().toString() + "\n" + ((ClientSession) session).getPresence().toXML());
	}

	public void anonymousSessionDestroyed(Session session)
	{
		Log.debug("RayoPlugin anonymousSessionDestroyed "+ session.getAddress().toString() + "\n" + ((ClientSession) session).getPresence().toXML());

		CallHandler.hangupOwner(session.getAddress().toString(), "User has ended session");
	}

	public void resourceBound(Session session)
	{
		Log.debug("RayoPlugin resourceBound "+ session.getAddress().toString() + "\n" + ((ClientSession) session).getPresence().toXML());
	}

	public void sessionCreated(Session session)
	{
		Log.debug("RayoPlugin sessionCreated "+ session.getAddress().toString() + "\n" + ((ClientSession) session).getPresence().toXML());
	}

	public void sessionDestroyed(Session session)
	{
		Log.debug("RayoPlugin sessionDestroyed "+ session.getAddress().toString() + "\n" + ((ClientSession) session).getPresence().toXML());

		CallHandler.hangupOwner(session.getAddress().toString(), "User has ended session");
	}
}
