/*
 *  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: NetworkHostsContainer.java 3859 2007-07-01 20:15:19Z gregork $
 */
package phex.host;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;

import phex.common.address.DestAddress;
import phex.event.AsynchronousDispatcher;
import phex.event.NetworkHostsChangeListener;
import phex.event.NetworkListener;
import phex.gui.prefs.NetworkTabPrefs;
import phex.msg.MsgManager;
import phex.prefs.core.ConnectionPrefs;
import phex.prefs.core.NetworkPrefs;
import phex.utils.Localizer;
import phex.utils.NLogger;

/**
 * Responsible for holding all hosts of the current network neighbour hood.
 *
 * For performance there where two different implementation decision to be made:
 * 1) When accessing the lists, return the original internaly maintaind list.
 *    When updating the lists dont mutate the original list. Instead create a new
 *    list and copy the contents of the old list into the new list, then replace
 *    the new list.
 *    This would mean fast access (227ms) against slow updates (799ms) but we
 *    would reveal our internal data structure for mutation. Hidding this
 *    structure through the use of a unmodifiable List would give slightly slower
 *    access (414ms).
 * 2) When accessing the lists, return a array containing the elements of the
 *    internal list. When updating the lists mutate the original list.
 *    This would resut in slow access (430ms) against fast updates (146ms).
 *    This concept would not reveal the internal data structure for mutation.
 *
 *    Performance times are based on a 15 element ArrayList. Access was measured
 *    by iterating 10000000 times over the list. Updates are measured by performing
 *    10000000 add and remove operations.
 *
 * Since I would think not revealing the internal data structure for mutation is
 * a high design goal, the option would be to publish a unmodifiable List (1) or
 * a array (2). Since access times are very close for these two options, the update
 * times go in favour of concept 2. Therefore concept two is used in this container
 * implementation.
 */
final public class NetworkHostsContainer implements NetworkListener
{
    private HostManager hostMgr;

    /**
     * The complete neighbour hood. Contains all connected and not connected
     * hosts independent from its connection type.
     * This collection is mainly used for GUI representation.
     */
    private List<Host> networkHosts;

    /**
     * Contains a list of connected peer connections.
     */
    private List<Host> peerConnections;

    /**
     * Contains a list of connected ultrapeer connections.
     */
    private List<Host> ultrapeerConnections;

    /**
     * The number of connections that are leafUltrapeerConnections inside the
     * ultrapeerConnections list.
     */
    private int leafUltrapeerConnectionCount;

    /**
     * Contains a list of connected leaf connections, in case we act as there
     * Ultrapeer.
     */
    private List<Host> leafConnections;

    /**
     * All listeners interested in events.
     */
    private List<NetworkHostsChangeListener> listenerList = new ArrayList<NetworkHostsChangeListener>( 3 );    
    
    public NetworkHostsContainer()
    {
        networkHosts = new ArrayList<Host>();
        peerConnections = new ArrayList<Host>();
        ultrapeerConnections = new ArrayList<Host>();
        leafConnections = new ArrayList<Host>();
        hostMgr = HostManager.getInstance();
    }

    /**
     * Returns true if the local host is a shielded leaf node ( has a connection
     * to a ultrapeer).
     */
    public synchronized boolean isShieldedLeafNode()
    {
        return leafUltrapeerConnectionCount > 0;
    }

    public synchronized boolean hasLeafConnections()
    {
        // we are a ultrapeer if we have any leaf slots filled.
        return !leafConnections.isEmpty();
    }

    public synchronized boolean hasUltrapeerConnections()
    {
        return !ultrapeerConnections.isEmpty();
    }

    /**
     * Used to chek if we have anymore ultrapeer slots. Usually this method
     * should only be used used as a Ultrapeer.
     * @return true if ultrapeer slots are available, false otherwise.
     */
    public boolean hasUltrapeerSlotsAvailable()
    {
        // Note: That we don't response on pings when the slots are full this
        // results in not getting that many incomming requests.
        return ultrapeerConnections.size() < ConnectionPrefs.Up2UpConnections.get().intValue();
    }
    
    /**
     * Returns the number of open slots for leaf nodes. Usually this method
     * should only be used used as a Ultrapeer.
     * @return the number of open slots for leaf nodes.
     */
    public int getOpenUltrapeerSlotsCount()
    {
        return ConnectionPrefs.Up2UpConnections.get().intValue() - ultrapeerConnections.size();
    }


    /**
     * Used to check if we would provide a Ultrapeer that will become a possible
     * leaf through leaf guidance a slot. This is only the case if we have not
     * already a ultrapeer too much and if we have a leaf slot available.
     * @return true if we have a leaf slot available to guide a ultrapeer, 
     *         false otherwise
     */
    public boolean hasLeafSlotForUltrapeerAvailable()
    {
        return hasLeafSlotsAvailable() &&
            // alow one more up2up connection to accept this possibly leaf guidanced
            // ultrapeer
            ultrapeerConnections.size() < ConnectionPrefs.Up2UpConnections.get().intValue() + 1;
    }

    /**
     * Used to chek if we have anymore leaf slots. Usually this method
     * is only used used as a Ultrapeer.
     * @return true if we have leaf slots available, false otherwise.
     */
    public boolean hasLeafSlotsAvailable()
    {
        // Note: That we don't response on pings when the slots are full this
        // results in not getting that many incomming requests.
        return leafConnections.size() < ConnectionPrefs.Up2LeafConnections.get().intValue();
    }
    
    /**
     * Returns the number of open slots for leaf nodes.
     * @return the number of open slots for leaf nodes.
     */
    public int getOpenLeafSlotsCount()
    {
        if ( hostMgr.isUltrapeer() )
        {
            return ConnectionPrefs.Up2LeafConnections.get().intValue() - leafConnections.size();
        }
        return 0;
    }

    /**
     * Returns all available connected ultrapeers.
     * @return all available connected ultrapeers.
     */
    public synchronized Host[] getUltrapeerConnections()
    {
        Host[] hosts = new Host[ ultrapeerConnections.size() ];
        ultrapeerConnections.toArray( hosts );
        return hosts;
    }

    /**
     * Returns all available connected leafs.
     * @return all available connected leafs.
     */
    public synchronized Host[] getLeafConnections()
    {
        Host[] hosts = new Host[ leafConnections.size() ];
        leafConnections.toArray( hosts );
        return hosts;
    }

    /**
     * Returns all available connected peers.
     * @return all available connected peers.
     */
    public synchronized Host[] getPeerConnections()
    {
        Host[] hosts = new Host[ peerConnections.size() ];
        peerConnections.toArray( hosts );
        return hosts;
    }

    public synchronized int getTotalConnectionCount()
    {
        return ultrapeerConnections.size() +
            leafConnections.size() +
            peerConnections.size();
    }
    
    public int getLeafConnectionCount()
    {
        return leafConnections.size();
    }

    public synchronized int getUltrapeerConnectionCount()
    {
        return ultrapeerConnections.size();
    }
    
    /**
     * Returns a array of push proxy addresses or null if 
     * this is not a shielded leaf node.
     * @return a array of push proxy addresses or null.
     */
    public DestAddress[] getPushProxies() 
    {
        if ( isShieldedLeafNode() )
        {
            HashSet<DestAddress> pushProxies = new HashSet<DestAddress>();
            for ( Host host : ultrapeerConnections )
            {
                DestAddress pushProxyAddress = host.getPushProxyAddress();
                if ( pushProxyAddress != null )
                {
                    pushProxies.add( pushProxyAddress );
                    if ( pushProxies.size() == 4 )
                    {
                        break;
                    }
                }
            }
            DestAddress[] addresses = new DestAddress[ pushProxies.size() ];
            pushProxies.toArray( addresses );
            return addresses;
        }
        return null;
    }

    /**
     * Adds a connected host to the connected host list. But only if its already
     * in the network host list.
     * @param host the host to add to the connected host list.
     */
    public synchronized void addConnectedHost( Host host )
    {
        // make sure host is still in network and not already removed
        if ( !networkHosts.contains( host ) )
        {// host is already removed by user action...
            disconnectHost( host );
            return;
        }

        if ( host.isUltrapeer() )
        {
            ultrapeerConnections.add( host );
            if ( host.isLeafUltrapeerConnection() )
            {
                leafUltrapeerConnectionCount++;
            }
        }
        else if ( host.isUltrapeerLeafConnection() )
        {
            leafConnections.add( host );
        }
        else
        {
            peerConnections.add( host );
        }

        /* We keep the connections it will be usefull to have one...
        // if the host is a ultrapeer we are a shielded leaf...
        // therefor drop all none ultrapeer connections!
        if ( host.isUltrapeer() )
        {
            dropAllNonUltrapeers();
        }*/
        //dump();
    }

    public synchronized void disconnectHost( Host host )
    {
        if ( host == null )
        {
            return;
        }

        if ( host.isUltrapeer() )
        {
            boolean isRemoved = ultrapeerConnections.remove( host );
            if ( isRemoved && host.isLeafUltrapeerConnection() )
            {
                leafUltrapeerConnectionCount--;
            }
        }
        else if ( host.isUltrapeerLeafConnection() )
        {
            leafConnections.remove( host );
        }
        else
        {
            peerConnections.remove( host );
        }

        // first disconnect to make sure that no disconnected host are added
        // to the routing.
        host.disconnect();
        // then clean routings
        MsgManager.getInstance().removeHost( host );

        fireNetworkHostChanged( host );

        //dump();
        // This is only for testing!!!
        /*if ( connectedHosts.contains( host ) )
        {
            // go crazy
            throw new RuntimeException ("notrem");
        }*/
    }

    /**
     * Checks for hosts that have a connection timeout...
     * Checks if a connected host is able to keep up...
     * if not it will be removed...
     */
    public synchronized void periodicallyCheckHosts()
    {
        HostStatus status;
        long currentTime = System.currentTimeMillis();

        Host[] badHosts = new Host[ networkHosts.size() ];
        int badHostsPos = 0;
        //boolean isShieldedLeafNode = isShieldedLeafNode();

        for( Host host : networkHosts )
        {
            status = host.getStatus();
            if ( status == HostStatus.CONNECTED )
            {
                host.checkForStableConnection( currentTime );

                String policyInfraction = null;
                if ( host.tooManyDropPackets() )
                {
                    policyInfraction = Localizer.getString( "TooManyDroppedPackets" );
                }
                else if ( host.isSendQueueTooLong() )
                {
                    policyInfraction = Localizer.getString( "SendQueueTooLong" );
                }
                else if ( host.isNoVendorDisconnectApplying() )
                {
                    policyInfraction = Localizer.getString( "NoVendorString" );
                }
                // freeloaders are no real problem
                // else if ( host.isFreeloader( currentTime ) )
                // {
                //     policyInfraction = Localizer.getString( "FreeloaderNotSharing" );
                // }
                if ( policyInfraction != null )
                {
                    //Logger.logMessage( Logger.FINE, "log.core.msg",
                    //    "Applying disconnect policy to host: " + host +
                    //    " drops: " + host.tooManyDropPackets() +
                    //    " queue: " + host.sendQueueTooLong() );
                    host.setStatus( HostStatus.ERROR, policyInfraction, currentTime );
                    disconnectHost( host );
                }
            }
            if ( NetworkPrefs.AutoRemoveBadHosts.get().booleanValue() )
            {
                // first collect...
                if ( status != HostStatus.CONNECTED &&
                     status != HostStatus.CONNECTING &&
                     status != HostStatus.ACCEPTING )
                {
                    if ( host.isErrorStatusExpired( currentTime, 
                        NetworkTabPrefs.HostErrorDisplayTime.get().intValue() ) )
                    {
                        //Logger.logMessage( Logger.DEBUG, "log.core.msg",
                        //    "Cleaning up network host: " + host + " Status: " + status );
                        badHosts[ badHostsPos ] = host;
                        badHostsPos ++;
                        continue;
                    }
                }

                /* we dont drop non-ultrapeers in leaf mode anymore

                // actually this should never be a problem... but to make sure...
                if ( status == Host.STATUS_HOST_CONNECTED &&
                     isShieldedLeafNode && !host.isUltrapeer() )
                {
                    Logger.logMessage( Logger.FINER, Logger.NETWORK,
                        "Dropping none Ultrapeer: " + host );
                    badHosts[ badHostsPos ] = host;
                    badHostsPos ++;
                    continue;
                }*/
            }
        }
        // kill all bad hosts...
        if ( badHostsPos > 0 )
        {
            removeNetworkHosts( badHosts );
        }
    }

    public synchronized Host getNetworkHostAt( int index )
    {
        if ( index < 0 || index >= networkHosts.size() )
        {
            return null;
        }
        return networkHosts.get( index );
    }

    public synchronized Host[] getNetworkHostsAt( int[] indices )
    {
        int length = indices.length;
        Host[] hosts = new Host[ length ];
        for ( int i = 0; i < length; i++ )
        {
            hosts[i] = networkHosts.get( indices[i] );
        }
        return hosts;
    }
    
    public synchronized Host getNetworkHost( DestAddress address )
    {
        for ( Host networkHost : networkHosts )
        {
            DestAddress networkAddress = networkHost.getHostAddress();
            if ( networkAddress.equals( address ) )
            {
                return networkHost;
            }            
        }
        //not found
        return null;
    }

    /**
     * Returns the count of the complete neighbour hood, containing all 
     * connected and not connected hosts independent from its connection type.
     */
    public synchronized int getNetworkHostCount()
    {
        return networkHosts.size();
    }

    /**
     * Returns the count of the networks hosts with the given status.
     */
    public synchronized int getNetworkHostCount( HostStatus status )
    {
        int count = 0;
        for( Host host : networkHosts )
        {
            if ( host.getStatus() == status )
            {
                count ++;
            }
        }
        return count;
    }
    
    /**
     * Adds a host to the network host list.
     * @param host the host to add to the network host list.
     */
    public synchronized void addNetworkHost( Host host )
    {
        int position = networkHosts.size();
        networkHosts.add( host );
        fireNetworkHostAdded( position );
        //dump();
    }
    
    public synchronized boolean isConnectedToHost( DestAddress address )
    {
        // Check for duplicate.
        for (int i = 0; i < networkHosts.size(); i++)
        {
            Host host = networkHosts.get( i );
            if ( host.getHostAddress().equals( address ) )
            {// already connected
                return true;
            }
        }
        return false;
    }

    public synchronized void addIncomingHost( Host host )
    {
        // a incoming host is new for the network and is connected
        addNetworkHost( host );
        addConnectedHost( host );
        //dump();
    }

    public synchronized void removeAllNetworkHosts()
    {
        Host host;
        while ( networkHosts.size() > 0 )
        {
            host = networkHosts.get( 0 );
            internalRemoveNetworkHost( host );
        }
    }

    public synchronized void removeNetworkHosts( Host[] hosts )
    {
        Host host;
        int length = hosts.length;
        for ( int i = 0; i < length; i++ )
        {
            host = hosts[ i ];
            internalRemoveNetworkHost( host );
        }
    }

    /**
     * This is the only way a Host gets diconnected right!
     * The Host disconnect method is only used to clean up the connection.
     */
    public synchronized void removeNetworkHost( Host host )
    {
        internalRemoveNetworkHost( host );
    }

    /*
    // Not used anymore but might be used again later...
    public synchronized void dropAllNonUltrapeers()
    {
        Logger.logMessage( Logger.FINE, Logger.NETWORK, "Dropping all non-Ultrapeers" );
        Host host;
        for ( int i = connectedHosts.size() - 1; i >= 0; i-- )
        {
            host = (Host) connectedHosts.get( i );
            if ( !host.isUltrapeer() )
            {
                Logger.logMessage( Logger.FINER, Logger.NETWORK,
                    "Dropping non-Ultrapeer: " + host );
                internalRemoveNetworkHost( host );
            }
        }
    }*/

    /**
     * Disconnects from host.
     */
    private synchronized void internalRemoveNetworkHost( Host host )
    {
        if ( host == null )
        {
            return;
        }
        disconnectHost( host );
        int position = networkHosts.indexOf( host );
        if ( position >= 0 )
        {
            networkHosts.remove( position );
            fireNetworkHostRemoved( position );
        }
        //dump();
        // This is only for testing!!!
        /*if ( connectedHosts.contains( host ) )
        {
            // go crazy
            throw new RuntimeException ("notrem");
        }*/
    }

    ///////////////////// START event handling methods ////////////////////////
    public void addNetworkHostsChangeListener( NetworkHostsChangeListener listener )
    {
        synchronized ( listenerList )
        {
            listenerList.add( listener );
        }
    }

    public void removeNetworkHostsChangeListener( NetworkHostsChangeListener listener )
    {
        synchronized ( listenerList )
        {
            listenerList.remove( listener );
        }
    }


    private void fireNetworkHostChanged( final int position )
    {
        // invoke update in event dispatcher
        AsynchronousDispatcher.invokeLater(
        new Runnable()
        {
            public void run()
            {
                Object[] listeners = listenerList.toArray();
                NetworkHostsChangeListener listener;
                // Process the listeners last to first, notifying
                // those that are interested in this event
                for ( int i = listeners.length - 1; i >= 0; i-- )
                {
                    listener = (NetworkHostsChangeListener)listeners[ i ];
                    listener.networkHostChanged( position );
                }
            }
        });
    }

    private void fireNetworkHostAdded( final int position )
    {
        // invoke update in event dispatcher
        AsynchronousDispatcher.invokeLater(
        new Runnable()
        {
            public void run()
            {
                Object[] listeners = listenerList.toArray();
                NetworkHostsChangeListener listener;
                // Process the listeners last to first, notifying
                // those that are interested in this event
                for ( int i = listeners.length - 1; i >= 0; i-- )
                {
                    listener = (NetworkHostsChangeListener)listeners[ i ];
                    listener.networkHostAdded( position );
                }
            }
        });
    }

    private void fireNetworkHostRemoved( final int position )
    {
        // invoke update in event dispatcher
        AsynchronousDispatcher.invokeLater(
        new Runnable()
        {
            public void run()
            {
                try
                {
                    Object[] listeners = listenerList.toArray();
                    NetworkHostsChangeListener listener;
                    // Process the listeners last to first, notifying
                    // those that are interested in this event
                    for ( int i = listeners.length - 1; i >= 0; i-- )
                    {
                        listener = (NetworkHostsChangeListener)listeners[ i ];
                        listener.networkHostRemoved( position );
                    }
                }
                catch ( Throwable th )
                {// catch all
                    NLogger.error( NetworkHostsContainer.class, th, th );
                }
            }
        });
    }

    public void fireNetworkHostChanged( Host host )
    {
        int position = networkHosts.indexOf( host );
        if ( position >= 0 )
        {
            fireNetworkHostChanged( position );
        }
    }

    // BEGIN NetworkListener handler methods
    public void connectedToNetwork()
    {
    }

    public void disconnectedFromNetwork()
    {
        // Disconnect all hosts
        removeAllNetworkHosts();
    }

    public void networkIPChanged(DestAddress hostAddress)
    { /* not implemented */ }
    public void joinedNetwork()
    { /* not implemented */ }
    public void leftNetwork()
    { /* not implemented */ }
    
    // END NetworkListener handler methods

    ///////////////////// END event handling methods ////////////////////////


    /////////////////////////// debug ///////////////////////////////////////
    /*private synchronized void dump()
    {
        System.out.println( "-------------- network -----------------" );
        Iterator iterator = networkHosts.iterator();
        while ( iterator.hasNext() )
        {
            System.out.println( iterator.next() );
        }
        System.out.println( "-------------- connected ---------------" );
        iterator = connectedHosts.iterator();
        while ( iterator.hasNext() )
        {
            System.out.println( iterator.next() );
        }
        System.out.println( "----------------------------------------" );
    }*/
}