/*
 *  PHEX - The pure-java Gnutella-servent.
 *  Copyright (C) 2001 - 2006 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
 * 
 *  --- CVS Information ---
 *  $Id: AltLocContainer.java 3859 2007-07-01 20:15:19Z gregork $
 */
package phex.common;

import java.util.*;

import org.apache.commons.collections.SequencedHashMap;

import phex.common.address.DestAddress;
import phex.http.HTTPHeader;
import phex.utils.NLogger;
import phex.utils.NLoggerNames;
import phex.xml.sax.share.DAlternateLocation;

/**
 * A AlternateLocationContainer that helps holder to add, sort and access
 * AlternateLocations.
 */
public class AltLocContainer
{
    public static final int MAX_ALT_LOC_COUNT = 100;
    public static final int MAX_ALT_LOC_FOR_QUERY_COUNT = 10;
    
    // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();

    /**
     * The urn each alternate location must match to be accepted.
     */
    private URN urn;

    /**
     * The map of AlternateLocations. It is lazy initialized on first use and
     * sorted by timestamp.<br>
     * We are using a SequencedHashMap that allows use to remove the alt loc
     * which has not been seen (put) into the map for the longest time.
     */
    private SequencedHashMap altLocationMap;

    public AltLocContainer( URN urn )
    {
        if( urn == null )
        {
            throw new NullPointerException( "URN must be provided" );
        }
        this.urn = urn;
    }

    public void addContainer( AltLocContainer cont )
    {
        if ( cont.urn == null || !cont.urn.equals( urn ) )
        {
            // dont add container with wrong or not existing sha1.
            // This is a implementation error.
            throw new IllegalArgumentException(
                "Trying to add container with not matching urns" );

        }

        // this check prevents a NullPointerException during putAll();
        if ( cont.altLocationMap == null )
        {// nothing to add..
            return;
        }
        initMap();
        synchronized( cont.altLocationMap )
        {
            Set<AlternateLocation> keySet = cont.altLocationMap.keySet();
            for( AlternateLocation tempAltLoc : keySet )
            {
                addAlternateLocation( tempAltLoc );
            }
        }
    }

    public void addAlternateLocation( AlternateLocation altLoc )
    {
        URN altLocURN = altLoc.getURN();
        if ( altLocURN == null || !altLocURN.equals( urn ) )
        {// dont add alt location with wrong or not existing sha1
            NLogger.warn( AltLocContainer.class,
                "Cant add alt-location with not matching URN to container." );
            assert false : "Cant add alt-location with not matching URN to container.";
            return;
        }

        initMap();
        synchronized( altLocationMap )
        {
            // thanks to AlternateLocation hashCode implementation the map ensures
            // that each alt loc is only once present. The put operation of a 
            // duplicate alt loc is required here to update the SequencedHashMap
            // internal order of elements. This ensures that alt loc not seen
            // for a long time get removed from the map.
            altLocationMap.put( altLoc, PRESENT );
            // make sure we have not more then 100 alt locations.
            if ( altLocationMap.size() > MAX_ALT_LOC_COUNT )
            {// drop last element
                Object firstKey = altLocationMap.getFirstKey();
                altLocationMap.remove( firstKey );
            }
        }
    }
    
    public void removeAlternateLocation( AlternateLocation altLoc )
    {
        URN altLocURN = altLoc.getURN();
        if ( altLocURN == null || !altLocURN.equals( urn ) )
        {// dont remove alt location with wrong or not existing sha1
            NLogger.warn( AltLocContainer.class,
                "Cant remove alt-location with not matching URN from container." );
            assert false : "Cant remove alt-location with not matching URN from container.";
            return;
        }
        initMap();
        synchronized( altLocationMap )
        {
            altLocationMap.remove( altLoc );
        }
    }
    
    public Set<DestAddress> getAltLocsForExport()
    {
        if ( isEmpty() )
        {
            return Collections.emptySet();
        }
        Iterator iterator = altLocationMap.keySet().iterator();
        Set<DestAddress> result = new HashSet<DestAddress>();
        while( iterator.hasNext() )
        {
            AlternateLocation altLoc = (AlternateLocation)iterator.next();
            DestAddress destAddress = altLoc.getHostAddress();
            if ( !destAddress.isIpHostName() )
            {
                continue;
            }
            if ( !destAddress.isLocalHost() )
            {
                continue;
            }
            result.add(destAddress);
        }
        return result;
    }
    
    /**
     * Returns a DestAddress array of alt locs for the query response record.
     * 
     * @return a DestAddress array of alt locs
     */
    public Set<DestAddress> getAltLocForQueryResponseRecord( )
    {
        if ( isEmpty() )
        {
            return Collections.emptySet();
        }
        synchronized ( altLocationMap )
        {
            Iterator iterator = altLocationMap.keySet().iterator();
            Set<DestAddress> result = new HashSet<DestAddress>();
            while( iterator.hasNext() && result.size() < MAX_ALT_LOC_FOR_QUERY_COUNT )
            {
                AlternateLocation altLoc = (AlternateLocation)iterator.next();
                DestAddress destAddress = altLoc.getHostAddress();
                if ( !destAddress.isIpHostName() )
                {
                    continue;
                }
                if ( !destAddress.isLocalHost() )
                {
                    continue;
                }
                result.add(destAddress);
            }
            return result;
        }
    }

    /**
     * Returns a HTTPHeader array for this host address. This will include all
     * alternate locations except the one that equals the host address and the
     * one contained in the sendAltLocList. The newly send alt locs are added
     * to the sendAltLocList inside this call.
     * 
     * @param the name of the http header to generate, use X-ALT or X-NAlt.
     * @param hostAddress the host address that gets the alternate locations.
     * @param sendAltLocList a list of already send alt locs.
     * @return a HTTPHeader array for this host address
     */
    public HTTPHeader getAltLocHTTPHeaderForAddress( String headerName,
        DestAddress hostAddress, Set<AlternateLocation> sendAltLocSet )
    {
        if ( isEmpty() )
        {
            return null;
        }
        int count = 0;
        StringBuffer headerValue = new StringBuffer();
        synchronized ( altLocationMap )
        {
            Iterator iterator = altLocationMap.keySet().iterator();
            while( iterator.hasNext() )
            {
                AlternateLocation altLoc = (AlternateLocation)iterator.next();
                
                // filter out alt locations to given host address..
                if ( hostAddress.getHostName().equals( altLoc.getHostAddress().getHostName() ) )
                {
                    continue;
                }
                // filter out already send alt locs...
                if ( sendAltLocSet.contains( altLoc ) )
                {
                    continue;
                }
                
                if ( count > 0 )
                {
                    headerValue.append( "," );
                }
                headerValue.append( altLoc.getHTTPString() );
                sendAltLocSet.add( altLoc );
                count ++;
                if ( count == 10 )
                {
                    break;
                }
            }
        }
        if ( headerValue.length() > 0 )
        {
            HTTPHeader altLocHeader = new HTTPHeader( headerName,
                headerValue.toString() );
            return altLocHeader;
        }
        else
        {
            return null;
        }
    }

    /**
     * Returns the number of alternate locations in the container.
     * @return the number of alternate locations in the container.
     */
    public synchronized int getSize()
    {
        if ( altLocationMap == null )
        {
            return 0;
        }
        else
        {
            return altLocationMap.size();
        }
    }

    /**
     * Indicates whether the container is empty or not.
     * @return <code>true</code> if the container is empty, <code>false</code>
     *         otherwise.
     */
    public synchronized boolean isEmpty()
    {
        if ( altLocationMap == null )
        {
            return true;
        }
        else
        {
            return altLocationMap.isEmpty();
        }
    }

    public synchronized void createDAlternateLocationList( List<DAlternateLocation> list )
    {
        if ( altLocationMap == null )
        {
            return;
        }
        Iterator iterator = altLocationMap.keySet().iterator();
        while( iterator.hasNext() )
        {
            AlternateLocation altLoc = (AlternateLocation)iterator.next();
            DAlternateLocation dAltLoc =  new DAlternateLocation();
            dAltLoc.setHostAddress( altLoc.getHostAddress().getFullHostName() );
            dAltLoc.setUrn( altLoc.getURN().getAsString() );
            list.add( dAltLoc );
        }
    }

    /**
     * Returns a string representation of the object.
     * @return a string representation of the object.
     */
    @Override
    public synchronized String toString()
    {
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append("Alt-Locations(SHA1: ");
        stringBuffer.append( urn.getAsString() );
        stringBuffer.append(")=[ ");
        if ( altLocationMap != null )
        {
            Iterator iterator = altLocationMap.keySet().iterator();
            AlternateLocation altLoc;
            while( iterator.hasNext() )
            {
                altLoc = (AlternateLocation)iterator.next();
                stringBuffer.append( altLoc.toString() );
                stringBuffer.append(", ");
            }
        }
        stringBuffer.append( " ]" );
        return stringBuffer.toString();
    }


    private void initMap()
    {
        if ( altLocationMap == null )
        {
            altLocationMap = new SequencedHashMap();
        }
    }
    
    /**
     * Parses AlternateLocations from the values of the given http headers and
     * returns a List containing the results. 
     * @param headers HTTPHeaderGroup to parse the AlternateLocations from
     * @return List containing all AlternateLocations found.
     */
    public static List<AlternateLocation> parseUriResAltLocFromHeaders( HTTPHeader[] headers )
    {
        if ( headers.length == 0 )
        {
            return Collections.emptyList();
        }
        List<AlternateLocation> altLocList = new ArrayList<AlternateLocation>();
        StringTokenizer tokenizer;
        for ( int i = 0; i < headers.length; i++ )
        {
            HTTPHeader header = headers[i];
            tokenizer = new StringTokenizer( header.getValue(), ",");

            while( tokenizer.hasMoreTokens() )
            {
                try
                {
                    String altLocationStr = tokenizer.nextToken().trim();
                    AlternateLocation altLocation = AlternateLocation.parseUriResAltLoc(
                        altLocationStr );
                    if ( altLocation == null )
                    {
                        continue;
                    }
                    altLocList.add( altLocation );
                }
                // TODO filter this out
                // this is currently just to make sure we have no untested error cases.
                // and the download will not fail because of this.
                catch ( Exception exp )
                {
                    NLogger.error( NLoggerNames.GLOBAL, exp, exp );
                }
            }
        }
        return altLocList;
    }
    
    /**
     * Parses AlternateLocations from the values of the given http headers and
     * returns a List containing the results. 
     * @param headers HTTPHeaderGroup to parse the AlternateLocations from
     * @return List containing all AlternateLocations found.
     */
    public static List<AlternateLocation> parseCompactIpAltLocFromHeaders( HTTPHeader[] headers, URN urn )
    {
        if ( headers.length == 0 )
        {
            return Collections.emptyList();
        }
        List<AlternateLocation> altLocList = new ArrayList<AlternateLocation>();
        StringTokenizer tokenizer;
        for ( int i = 0; i < headers.length; i++ )
        {
            HTTPHeader header = headers[i];
            tokenizer = new StringTokenizer( header.getValue(), ",");

            while( tokenizer.hasMoreTokens() )
            {
                try
                {
                    String altLocationStr = tokenizer.nextToken().trim();
                    AlternateLocation altLocation = AlternateLocation.parseCompactIpAltLoc(
                        altLocationStr, urn );
                    if ( altLocation == null )
                    {
                        continue;
                    }
                    altLocList.add( altLocation );
                }
                // TODO filter this out
                // this is currently just to make sure we have no untested error cases.
                // and the download will not fail because of this.
                catch ( Exception exp )
                {
                    NLogger.error( NLoggerNames.GLOBAL, exp, exp );
                }
            }
        }
        return altLocList;
    }
}