/*
 *  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
 * 
 *  --- CVS Information ---
 *  $Id: GWebCacheContainer.java,v 1.2 2004/04/13 10:52:38 gregork Exp $
 */
package phex.gwebcache;

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


import phex.common.*;
import phex.connection.*;
import phex.host.*;
import phex.utils.*;

public class GWebCacheContainer
{
    private static int MIN_G_WEB_CACHES_SIZE = 20;

    private ArrayList allGWebCaches;
    private ArrayList functionalGWebCaches;

    /**
     * Stores the URL.toExternalForm(). The URL object itself is not stored
     * since the hash function of the URL is way slow, it could lead to doing a
     * IP lookup.
     */
    private HashSet uniqueGWebCaches;
    private Random random;

    public GWebCacheContainer()
    {
        Logger.logMessage( Logger.FINEST, Logger.GLOBAL, "Initializing GWebCacheContainer" );
        allGWebCaches = new ArrayList();
        functionalGWebCaches = new ArrayList();
        uniqueGWebCaches = new HashSet();
        random = new Random();
        Logger.logMessage( Logger.FINEST, Logger.GLOBAL, "Initialized GWebCacheContainer" );
    }
    
    /**
     *
     */
    public void initializeGWebCacheContainer()
    {
        allGWebCaches.clear();
        functionalGWebCaches.clear();
        uniqueGWebCaches.clear();
        loadGWebCacheFromFile();
    }

    /**
     * Connects to a random GWebCache to fetch more hosts. This should be triggered
     * on startup and when there are not enough hosts in the catcher. The queryed
     * hosts are directly put into the CaughtHostsContainer with high priority.
     */
    public boolean queryMoreHosts()
    {
        int retrys = 0;
        boolean succ = false;
        do
        {
            retrys ++;
            GWebCacheConnection connection = getRandomGWebCacheConnection();
            // continue if no connection...
            if ( connection == null )
            {
                continue;
            }
            HostAddress[] hosts = connection.sendHostFileRequest();
            // continue if cache is bad or data is null...
            if ( !verifyGWebCache( connection ) || hosts == null )
            {
                continue;
            }
            // everything looks good add data..
            CaughtHostsContainer container =
                HostManager.getInstance().getCaughtHostsContainer();
            for ( int i = 0; i < hosts.length; i++ )
            {
                // gwebcache should only return Ultrapeers therefore we have
                // high priority.
                container.addCaughtHost( hosts[i], CaughtHostsContainer.HIGH_PRIORITY );
            }
            succ = true;
        }
        // do this max 5 times or until we where successful
        while ( !succ && retrys < 5 );
        return succ;
    }

    /**
     * Updates the remote GWebCache. By the specification clients should only
     * send updates if they accept incoming connections - i.e. clients behind
     * firewalls should not send updates. Also, if supported by clients, only
     * Ultrapeers/Supernodes should send updates. After a client has been up for
     * an hour, it should begin sending an Update request periodically - every
     * 60 minutes. It sends its own IP address and port in the "ip" parameter
     * and a the URL of a random cache in the "url" parameter.
     * Clients should only submit the URLs of caches that they know are functional!
     */
    public boolean updateRemoteGWebCache( HostAddress myHostAddress )
    {
        String fullHostName = null;
        if ( myHostAddress != null )
        {
            fullHostName = myHostAddress.getFullHostName();
        }

        int retrys = 0;
        boolean succ = false;
        do
        {
            retrys ++;
            GWebCacheConnection connection = getRandomGWebCacheConnection();
            // continue if no connection...
            if ( connection == null )
            {
                continue;
            }
            URL url = getRandomFunctionalGWebCacheURL();
            if ( url != null && url.equals( connection.getGWebCacheURL() ) )
            {// same as connection... try other url..
                url = getRandomFunctionalGWebCacheURL();
                if ( url != null && url.equals( connection.getGWebCacheURL() ) )
                {// same again... dont send url..
                    url = null;
                }
            }
            String urlString = null;
            if ( url != null )
            {
                urlString = url.toExternalForm();
            }

            if ( fullHostName == null && urlString == null )
            {
                // no data to update... try again to determine random GWebCache in loop
                continue;
            }

            succ = connection.updateRequest( fullHostName, urlString );
            // continue if cache is bad or not successful...
            if ( !verifyGWebCache( connection ) || !succ )
            {
                continue;
            }
        }
        // do this max 5 times or until we where successful
        while ( !succ && retrys < 5 );
        return succ;
    }

    /**
     * Connects to a random GWebCache to fetch more GWebCaches.
     */
    public boolean queryMoreGWebCaches()
    {
        int retrys = 0;
        boolean succ = false;
        do
        {
            retrys ++;
            GWebCacheConnection connection = getRandomGWebCacheConnection();
            // continue if no connection...
            if ( connection == null )
            {
                continue;
            }
            URL[] urls = connection.sendURLFileRequest();
            // continue if cache is bad or data is null...
            if ( !verifyGWebCache( connection ) || urls == null )
            {
                continue;
            }
            // everything looks good add data..
            for ( int i = 0; i < urls.length; i++ )
            {
                insertGWebCache( urls[i] );
            }
            if ( urls.length > 0 )
            {// save if changed
                saveGWebCacheToFile();
            }
            succ = true;
        }
        // do this max 5 times or until we where successful
        while ( !succ && retrys < 5 );
        return succ;
    }

    public int getGWebCacheCount()
    {
        return allGWebCaches.size();
    }

    private URL getRandomFunctionalGWebCacheURL()
    {
        URL url = null;
        int count = functionalGWebCaches.size();
        if ( count == 0 )
        {
            return null;
        }
        else if ( count == 1 )
        {
            return (URL)functionalGWebCaches.get( 0 );
        }

        int randomIndex = random.nextInt( count - 1 );
        url = (URL)functionalGWebCaches.get( randomIndex );
        return url;
    }

    private URL getRandomGWebCacheURL()
    {
        URL url = null;
        ensureMinGWebCaches();
        synchronized ( allGWebCaches )
        {
            int count = allGWebCaches.size();
            if ( count == 0 )
            {
                return null;
            }
            else if ( count == 1 )
            {
                return (URL)allGWebCaches.get( 0 );
            }

            int randomIndex = random.nextInt( count - 1 );
            url = (URL)allGWebCaches.get( randomIndex );
        }
        return url;
    }

    /**
     * Single trys to get a random and pinged GWebCacheConnection or null if
     * connection could not be obtained.
     */
    private GWebCacheConnection getRandomGWebCacheConnection()
    {
        GWebCacheConnection connection = null;
        URL url = null;
        try
        {
            url = getRandomGWebCacheURL();
            connection = new GWebCacheConnection( url );
            // we stop pinging GWebCache... this is not necessary since we
            // find out anyway if cache is working on first contact.
            //if ( !connection.sendPingRequest() )
            //{
            //    removeGWebCache( url, false );
            //    return null;
            //}
        }
        catch ( ProtocolNotSupportedException exp )
        {// not valid url.. throw away...
            removeGWebCache( url, true );
            return null;
        }
        // cache is working add it to functional list.
        if ( !functionalGWebCaches.contains( url ) )
        {
            functionalGWebCaches.add( url );
        }
        return connection;
    }

    private boolean verifyGWebCache( GWebCacheConnection connection )
    {
        if ( connection.isCacheBad() )
        {
            removeGWebCache( connection.getGWebCacheURL(), false );
            return false;
        }
        return true;
    }

    /**
     * Inserts a GWebCache...
     */
    private void insertGWebCache( URL gWebCacheURL )
    {
        synchronized( allGWebCaches )
        {
            // The URL object itself is not stored
            // since the hash function of the URL is way slow, it could lead to
            // doing a IP lookup.
            if ( !uniqueGWebCaches.contains( gWebCacheURL.toExternalForm() ) )
            {
                allGWebCaches.add( gWebCacheURL );
                uniqueGWebCaches.add( gWebCacheURL.toExternalForm() );
            }
        }
    }

    /**
     * Removes a GWebCache..
     */
    private void removeGWebCache( URL gWebCacheURL, boolean force )
    {
        synchronized( allGWebCaches )
        {
            // maintain a min number of GWebCaches even if bad.
            if ( allGWebCaches.size() > MIN_G_WEB_CACHES_SIZE || force )
            {
                allGWebCaches.remove( gWebCacheURL );
                functionalGWebCaches.remove( gWebCacheURL );
                uniqueGWebCaches.remove( gWebCacheURL.toExternalForm() );

                // save file...
                saveGWebCacheToFile();
            }
        }
    }

    /**
     * Inserts a GWebCache...
     */
    private void insertGWebCache( String gWebCacheURL )
    {
        // verify URL
        try
        {
            URL url = new URL( gWebCacheURL );
            insertGWebCache( url );
        }
        catch ( MalformedURLException exp )
        {
            Logger.logMessage( Logger.FINE, Logger.NETWORK, exp );
        }
    }

    /**
     * Tries to ensure that there is a minimum number of GWebCaches available.
     * This is done by loading GWebCaches from a districuted GWebCache default
     * file and 1 hard coded emergency GWebCache.
     * If we are not on the General Gnutella Network there is no way to ensure
     * a minimum set of GWebCaches and this call returns without actions.
     *
     */
    private void ensureMinGWebCaches()
    {
        if ( allGWebCaches.size() >= 10 )
        {
            return;
        }
        NetworkManager networkMgr = NetworkManager.getInstance();
        if ( !( networkMgr.getGnutellaNetwork() instanceof GeneralGnutellaNetwork ) )
        {// not on general gnutella network... cant use default list
            return;
        }
        
        Logger.logMessage( Logger.FINE, Logger.NETWORK,
            "Load default GWebCache file." );
        InputStream inStream = ClassLoader.getSystemResourceAsStream(
            "phex/resources/gwebcache.cfg" );
        if ( inStream != null )
        {
            InputStreamReader reader = new InputStreamReader( inStream );
            try
            {
                loadGWebCacheFromReader( reader );
                saveGWebCacheToFile();
            }
            catch ( IOException exp )
            {
                Logger.logWarning( exp );
            }
        }
        else
        {
            Logger.logMessage( Logger.FINE, Logger.NETWORK,
                "Default GWebCache file not found." );
        }
        if ( allGWebCaches.size() < 1 )
        {// emergency case which should never happen since the gwebcache.cfg
         // should contain enough caches.
            insertGWebCache( "http://gwebcache.bearshare.net/gcache.php" );
            saveGWebCacheToFile();
        }
    }

    private void loadGWebCacheFromFile()
    {
        try
        {
            NetworkManager networkMgr = NetworkManager.getInstance();
            File file = networkMgr.getGnutellaNetwork().getGWebCacheFile();
            if ( !file.exists() )
            {
                return;
            }
            loadGWebCacheFromReader( new FileReader(file) );
        }
        catch ( IOException exp )
        {
            Logger.logWarning( exp );
        }
        finally
        {
            ensureMinGWebCaches();
        }
    }

    /**
     * Reads the contents of the reader and closes the reader when done.
     * @param reader the reader to read from.
     * @throws IOException thrown when there are io errors.
     */
    private void loadGWebCacheFromReader( Reader reader )
        throws IOException
    {
        BufferedReader br = new BufferedReader( reader );
        String line;
        synchronized( allGWebCaches )
        {
            while ( (line = br.readLine()) != null)
            {
                if ( line.startsWith("#") )
                {
                    continue;
                }
                insertGWebCache( line );
            }
        }
        br.close();
    }

    private void saveGWebCacheToFile()
    {
        try
        {
            NetworkManager networkMgr = NetworkManager.getInstance();
            File file = networkMgr.getGnutellaNetwork().getGWebCacheFile();
            BufferedWriter writer = new BufferedWriter( new FileWriter( file ) );

            synchronized( allGWebCaches )
            {
                Iterator iterator = allGWebCaches.iterator();
                while( iterator.hasNext() )
                {
                    URL url = (URL)iterator.next();
                    writer.write( url.toExternalForm() );
                    writer.newLine();
                }
            }
            writer.close();
        }
        catch ( IOException exp )
        {
            Logger.logWarning( exp );
        }
    }
}