/*
 *  PHEX - The pure-java Gnutella-servent.
 *  Copyright (C) 2001 - 2004 Gregor Koukkoullis ( phex <at> kouk <dot> de )
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
package phex.connection;

import java.io.*;
import java.net.*;
import java.util.TimerTask;


import phex.common.*;
import phex.host.*;
import phex.msg.MsgManager;
import phex.security.*;
import phex.utils.*;

public class IncomingListener implements Runnable
{
    private ServerSocket listeningSocket;
    private boolean isRequestedToDie;
    private boolean isRunning;
    
    /**
     * Indicates if a incomming connection was seen
     */
    private boolean hasConnectedIncomming;
    
    /**
     * The time the last incomming connection was seen.
     */
    private long lastInConnectionTime;
    
    /**
     * The last time a TCP connect back reqest check was sent.
     */
    private long lastFirewallCheckTime;

    public IncomingListener()
    {
        hasConnectedIncomming = false;
        isRequestedToDie = false;
        isRunning = false;
    }

    public synchronized void initialize()
        throws IOException
    {
        if ( isRunning )
        {
            return;
        }
        int port = ServiceManager.sCfg.mListeningPort;
        // Create a listening socket at the port.
        int tries = 0;
        boolean error;
        // try to find new port if port not valid
        do
        {
            error = false;

            try
            {
                listeningSocket = new ServerSocket( port, 50 );
            }
            catch ( BindException exp )
            {
                if ( tries > 10 )
                {
                    throw exp;
                }
                error = true;
                port ++;
                tries ++;
            }
        }
        while ( error == true );

        if ( ServiceManager.sCfg.mMyIP.length() > 0 )
        {
            NetworkManager.getInstance().setForcedHostIP(
                HostAddress.parseIP( ServiceManager.sCfg.mMyIP ) );
        }
        else
        {
            byte[] hostIP = resolveLocalHostIP();
            port = listeningSocket.getLocalPort();
            NetworkManager.getInstance().updateLocalAddress( hostIP, port );
        }
    }

    public synchronized void startup()
    {
        if ( isRunning )
        {
            return;
        }

        Logger.logMessage( Logger.FINER, Logger.GLOBAL, "Starting Listener");

        isRequestedToDie = false;
        isRunning = true;

        ThreadPool.getInstance().addJob( this,
            "IncommingListener-" + Integer.toHexString( hashCode() ) );
    }

    public synchronized void restart()
        throws IOException
    {
        shutdown(true);
        initialize();
        startup();
    }

    public synchronized void shutdown( boolean waitForCompleted )
    {
        // not running, already dead or been requested to die.
        if ( !isRunning || isRequestedToDie)
        {
            return;
        }

        Logger.logMessage( Logger.FINER, Logger.GLOBAL, "Shutting down Listener");

        // Set flag to die.
        isRequestedToDie = true;

        // Dummy connection to the listener to wake it up to die.
        try
        {
            Socket tmpSock = new Socket( HostAddress.LOCAL_HOST_NAME,
                listeningSocket.getLocalPort() );
            tmpSock.close();
        }
        catch(IOException e)
        {
            // Don't care.
        }

        if (waitForCompleted)
        {
            // Wait until the thread is dead.
            while ( isRunning )
            {
                try
                {
                    wait();
                }
                catch (InterruptedException e)
                {
                    break;
                }
            }
        }
    }

    public boolean getRunning()
    {
        return isRunning;
    }

    public int getListeningLocalPort()
    {
        return listeningSocket.getLocalPort();
    }

    public boolean hasConnectedIncoming()
    {
        return hasConnectedIncomming;
    }

    // The listening thread.
    public void run()
    {
        Logger.logMessage( Logger.FINER, Logger.GLOBAL, "Listener started. Listening on: "
            + listeningSocket.getInetAddress().getHostAddress()
            + ':' + listeningSocket.getLocalPort() );

        Socket incoming;
        HostAddress address;
        while ( !isRequestedToDie )
        {
            try
            {
                // Waiting for incoming connection.
                incoming = listeningSocket.accept();
                incoming.setSoTimeout(ServiceManager.sCfg.mSocketTimeout);
                
                address = new HostAddress(
                    incoming.getInetAddress().getAddress(), 
                    incoming.getPort() );
                NetworkHostsContainer netHostsContainer = HostManager.getInstance().getNetworkHostsContainer();
                
                // if not already connected and connection is not from a private address.
                if ( !netHostsContainer.isConnectedToHost( address ) && !address.isPrivateIP() )
                {
                    hasConnectedIncomming = true;
                    lastInConnectionTime = System.currentTimeMillis();
                }
            }
            catch( IOException e )
            {
                Logger.logError( e );
                break;
            }

            // See if I have been asked to die.
            if ( isRequestedToDie )
            {
                break;
            }

            try
            {
                // Set this will defeat the Nagle Algorithm, making short bursts of
                // transmission faster, but will be worse for the overall network.
                // incoming.setTcpNoDelay(true);

                // Create a Host object for the incoming connection
                // and hand it off to a ReadWorker to handle.

                byte access = PhexSecurityManager.getInstance().controlHostAddressAccess(
                    address );
                switch ( access )
                {
                    case PhexSecurityManager.ACCESS_DENIED:
                    case PhexSecurityManager.ACCESS_STRONGLY_DENIED:
                        throw new IOException( "Host access denied: "
                        + incoming.getInetAddress().getHostAddress() );
                }

                Logger.logMessage( Logger.FINE, Logger.NETWORK,
                    "Accepted incoming connection from: " +
                    address.getFullHostName() );

                IncomingConnectionDispatcher dispatcher =
                    new IncomingConnectionDispatcher( incoming );

                ThreadPool.getInstance().addJob( dispatcher,
                    "IncomingConnectionDispatcher-" + Integer.toHexString( hashCode() ) );
            }
            catch ( IOException exp )
            {
                Logger.logMessage( Logger.FINE, Logger.NETWORK, exp );
            }
            catch (Exception e)
            {
                Logger.logError( e );
            }
        }

        try
        {
            listeningSocket.close();
        }
        catch(IOException e)
        {// ignore during shoutdown
        }
        isRunning = false;
        Logger.logMessage( Logger.FINER, Logger.GLOBAL, "Listener stopped.");
        NetworkManager.getInstance().updateLocalAddress(
            HostAddress.LOCAL_HOST_IP, HostAddress.DEFAULT_PORT );
        synchronized ( this )
        {
            notifyAll();
        }
    }

    public byte[] resolveLocalHostIP()
    {
        byte[] ip = null;
        InetAddress addr = listeningSocket.getInetAddress();
        ip = addr.getAddress();
        if ( ip[0] == 0 && ip[1] == 0 && ip[2] == 0 && ip[3] == 0 )
        {
            ip = HostAddress.LOCAL_HOST_IP;
        }
        return ip;
    }
    
    public void resetFirewallCheck()
    {
        lastFirewallCheckTime = 0;
    }
    
    private class FirewallCheckTimer extends TimerTask
    {
        // once per 10 minutes
        public static final long TIMER_PERIOD = 1000 * 60 * 5;
        private static final long CHECK_TIME = 1000 * 60 * 15;
        
        public void run() 
        {
            try
            {
                long now = System.currentTimeMillis();
                
                if ( (hasConnectedIncomming && now - lastInConnectionTime > CHECK_TIME )
                    || (!hasConnectedIncomming && now - lastFirewallCheckTime > CHECK_TIME ) )
                {
                    NetworkHostsContainer netHostsContainer = HostManager.getInstance().getNetworkHostsContainer();
                    if ( netHostsContainer.getUltrapeerConnectionCount() <= 2)
                    {
                        return;
                    }
                    lastFirewallCheckTime = now;
                    MsgManager.getInstance().requestTCPConnectBack();
                    Environment.getInstance().scheduleTimerTask( 
                        new IncommingCheckRunner(), IncommingCheckRunner.TIMER_PERIOD, 0 );
                }
            }
            catch ( Throwable th )
            {
                Logger.logError(th);
            }
        }
    }
    
    private class IncommingCheckRunner extends TimerTask
    {
        // after 45 sec.
        public static final long TIMER_PERIOD = 1000 * 45;
        
        public void run()
        {
            long now = System.currentTimeMillis();
            if ( now - lastInConnectionTime > TIMER_PERIOD )
            {
                hasConnectedIncomming = false;
            }
        }
    }
}