/*
 *  PHEX - The pure-java Gnutella-servent.
 *  Copyright (C) 2001 - 2007 Phex Development Group
 *
 *  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
 *
 *  --- SVN Information ---
 *  $Id: OutgoingConnectionDispatcher.java 3859 2007-07-01 20:15:19Z gregork $
 */
package phex.connection;

import java.io.IOException;

import phex.common.ThreadPool;
import phex.common.address.DestAddress;
import phex.host.CaughtHostsContainer;
import phex.host.Host;
import phex.host.HostManager;
import phex.host.HostStatus;
import phex.host.NetworkHostsContainer;
import phex.net.OnlineObserver;
import phex.net.connection.Connection;
import phex.net.connection.ConnectionFactory;
import phex.utils.NLogger;

/**
 * This class is responsible to dispatch an outgoing Gnutella network 
 * connection to a specific host or to the next best host from the host catcher.
 */
public class OutgoingConnectionDispatcher implements Runnable
{
    /**
     * Dispatches a outgoing Gnutella network connection to the next 
     * best host from the host catcher.
     */
    public static void dispatchConnectToNextHost()
    {
        HostManager hostMgr = HostManager.getInstance();
        OutgoingConnectionDispatcher dispatcher = new OutgoingConnectionDispatcher(
            hostMgr.getCaughtHostsContainer(),
            hostMgr.getNetworkHostsContainer() );
        
        ThreadPool.getInstance().addJob( dispatcher,
            "OutgoingConnectionDispatcher-" + Integer.toHexString( dispatcher.hashCode() ) );
    }
    
    /**
     * Dispatches <tt>count</tt> number of outgoing Gnutella network connection 
     * to the next best hosts from the host catcher.
     */
    public static void dispatchConnectToNextHosts( int count )
    {
        HostManager hostMgr = HostManager.getInstance();
        CaughtHostsContainer caughtHostsCont = hostMgr.getCaughtHostsContainer();
        NetworkHostsContainer networkHostsCont = hostMgr.getNetworkHostsContainer();
        
        for ( int i = 0; i < count; i++ )
        {
            OutgoingConnectionDispatcher dispatcher = new OutgoingConnectionDispatcher(
                caughtHostsCont, networkHostsCont );
            ThreadPool.getInstance().addJob( dispatcher,
                "OutgoingConnectionDispatcher-" + Integer.toHexString( dispatcher.hashCode() ) );
        }
    }
    
    /**
     * Dispatches a outgoing Gnutella network connection to the specified
     * <tt>hostAddress</tt>
     * @param hostAddress the hostAddress to connect to.
     */
    public static void dispatchConnectToHost( DestAddress hostAddress )
    {
        OutgoingConnectionDispatcher dispatcher = new OutgoingConnectionDispatcher(
            hostAddress, HostManager.getInstance().getNetworkHostsContainer() );
        
        ThreadPool.getInstance().addJob( dispatcher,
            "OutgoingConnectionDispatcher-" + Integer.toHexString( dispatcher.hashCode() ) );
    }
    
    
    
    private CaughtHostsContainer caughtHostsContainer;
    private NetworkHostsContainer networkHostsContainer;
    private DestAddress hostAddress;

    private OutgoingConnectionDispatcher( CaughtHostsContainer caughtHostContainer, 
        NetworkHostsContainer networkHostsContainer )
    {
        this( (DestAddress)null, networkHostsContainer );
        this.caughtHostsContainer = caughtHostContainer; 
    }
    
    private OutgoingConnectionDispatcher( DestAddress hostAddress, NetworkHostsContainer networkHostsContainer )
    {
        this.hostAddress = hostAddress;
        this.networkHostsContainer = networkHostsContainer;
    }
    
    public void run()
    {
        try
        {
            if ( hostAddress == null )
            {// fetch new host
                hostAddress = lookupHostToConnect();
            }
            if ( hostAddress == null )
            {// no host found... nothing we can do about it...
                return;
            }

            connectToHostAddress();
        }
        catch ( Throwable th )
        {
            NLogger.error( OutgoingConnectionDispatcher.class, th, th);
        }
    }
    
    private DestAddress lookupHostToConnect()
    {
        assert( caughtHostsContainer != null );
        DestAddress caughtHost;
        do
        {
            caughtHost = caughtHostsContainer.getNextCaughtHost();
            if ( caughtHost == null )
            {
                // nothing is available...
                return null;
            }
        }
        while ( networkHostsContainer.isConnectedToHost( caughtHost ) );
        
        return caughtHost;
    }
    
    private void connectToHostAddress()
    {
        Host host = new Host( hostAddress );
        host.setType( Host.Type.OUTGOING );
        host.setStatus( HostStatus.CONNECTING );
        
        networkHostsContainer.addNetworkHost( host );

        ConnectionEngine engine;
        try
        {
            Connection connection = ConnectionFactory.createConnection( 
                hostAddress );
            // I am connected to the remote host at this point.
            host.setConnection( connection );
                
            OnlineObserver onlineObserver = NetworkManager.getInstance().getOnlineObserver();
            onlineObserver.markSuccessfulConnection();
            
            engine = new ConnectionEngine( host );
            engine.initHostHandshake();
        }
        catch ( ConnectionRejectedException exp )
        {
            reportConnectionStatus( true );
            host.setStatus( HostStatus.ERROR, exp.getMessage() );
            HostManager.getInstance().disconnectHost( host );
            NLogger.debug( OutgoingConnectionDispatcher.class, exp);
            return;
        }
        catch ( IOException exp )
        {
            reportConnectionStatus( false );
            host.setStatus( HostStatus.ERROR, exp.getMessage() );
            HostManager.getInstance().disconnectHost( host );
            NLogger.debug( OutgoingConnectionDispatcher.class, exp);
            return;
        }
        catch (Exception exp)
        {
            reportConnectionStatus( false );
            host.setStatus(HostStatus.ERROR, exp.getMessage());
            HostManager.getInstance().disconnectHost( host );
            NLogger.warn( OutgoingConnectionDispatcher.class, exp, exp);
            return;
        }
        
        reportConnectionStatus( true );
        
        try
        {
            engine.processIncomingData();
        }
        catch ( IOException exp )
        {
            host.setStatus( HostStatus.ERROR, exp.getMessage() );
            HostManager.getInstance().disconnectHost( host );
            NLogger.debug( OutgoingConnectionDispatcher.class, exp);
        }
        catch (Exception exp)
        {
            host.setStatus(HostStatus.ERROR, exp.getMessage());
            HostManager.getInstance().disconnectHost( host );
            NLogger.warn( OutgoingConnectionDispatcher.class, exp, exp);
        }
    }
    
    private void reportConnectionStatus( boolean hasConnected )
    {
        if ( caughtHostsContainer != null )
        {
            caughtHostsContainer.reportConnectionStatus( hostAddress, hasConnected );
        }
        if ( !hasConnected )
        {
            NetworkManager.getInstance().getOnlineObserver().markFailedConnection();
        }
    }
}