package com.limegroup.gnutella.altlocs;

import java.io.IOException;
import java.net.URL;
import java.util.Collections;
import java.util.StringTokenizer;

import com.limegroup.gnutella.Endpoint;
import com.limegroup.gnutella.ErrorService;
import com.limegroup.gnutella.PushEndpoint;
import com.limegroup.gnutella.RemoteFileDesc;
import com.limegroup.gnutella.RouterService;
import com.limegroup.gnutella.URN;
import com.limegroup.gnutella.filters.IP;
import com.limegroup.gnutella.http.HTTPHeaderValue;
import com.limegroup.gnutella.settings.ConnectionSettings;
import com.limegroup.gnutella.settings.UploadSettings;
import com.limegroup.gnutella.util.IpPort;
import com.limegroup.gnutella.util.IpPortForSelf;
import com.limegroup.gnutella.util.NetworkUtils;

/**
 * This class encapsulates the data for an alternate resource location, as 
 * specified in HUGE v0.93.  This also provides utility methods for such 
 * operations as comparing alternate locations based on the date they were 
 * stored.
 * 
 * Firewalled hosts can also be alternate locations, although the format is
 * slightly different.
 */
public abstract class AlternateLocation implements HTTPHeaderValue, 
	Comparable {
    
    /**
     * The vendor to use.
     */
    public static final String ALT_VENDOR = "ALT";

    /**
     * The three types of medium altlocs travel through
     */
	public static final int MESH_PING = 0;
    public static final int MESH_LEGACY = 1;
    public static final int MESH_RESPONSE = 2;
    
	/**
	 * Constant for the sha1 urn for this <tt>AlternateLocation</tt> --
	 * can be <tt>null</tt>.
	 */
	protected final URN SHA1_URN;
	
	/**
	 * Constant for the string to display as the httpStringValue.
	 */
	private String DISPLAY_STRING;
	


	/**
	 * Cached hash code that is lazily initialized.
	 */
	protected volatile int hashCode = 0;
	


    /**
     * LOCKING: obtain this' monitor while changing/accessing _count and 
     * _demoted as multiple threads could be accessing them.
     */
    
    /**
     * maintins a count of how many times this alternate location has been seen.
     * A value of 0 means this alternate location was failed one more time that
     * it has succeeded. Newly created AlternateLocations start out wit a value
     * of 1.
     */
    protected volatile int _count = 0;
    
    /**
     * Two counter objects to keep track of altloc expiration
     */
    private final Average legacy, ping, response;
    
    ////////////////////////"Constructors"//////////////////////////////
    
	/**
	 * Constructs a new <tt>AlternateLocation</tt> instance based on the
	 * specified string argument.  
	 *
	 * @param location a string containing a single alternate location,
	 *  including a full URL for a file and an optional date
	 * @throws <tt>IOException</tt> if there is any problem constructing
	 *  the new instance from the specified string, or if the <tt<location</tt>
	 *  argument is either null or the empty string -- we could (should?) 
	 *  throw NullPointerException here, but since we're already forcing the
	 *  caller to catch IOException, we might as well throw in in both cases
	 */
	public static AlternateLocation create(final String location) 
                                                           throws IOException {
		if(location == null || location.equals(""))
			throw new IOException("null or empty location");

		URL url = AlternateLocation.createUrl(location);
		URN sha1 = URN.createSHA1UrnFromURL(url);
		return new DirectAltLoc(url,sha1);
	}
	
	/**
	 * Constructs a new <tt>AlternateLocation</tt> instance based on the
	 * specified string argument and URN.  The location created this way
	 * assumes the name "ALT" for the file.
	 *
	 * @param location a string containing one of the following:
	 *  "http://my.address.com:port#/uri-res/N2R?urn:sha:SHA1LETTERS" or
	 *  "1.2.3.4[:6346]" or
	 *  http representation of a PushEndpoint.
	 * 
	 * If the first is given, then the SHA1 in the string MUST match
	 * the SHA1 given.
	 * 
	 * @param good whether the proxies contained in the string representation
	 * should be added to or removed from the current set of proxies
	 *
	 * @throws <tt>IOException</tt> if there is any problem constructing
	 *  the new instance.
	 */
	public static AlternateLocation create(final String location,
	                                       final URN urn) throws IOException {
	    if(location == null || location.equals(""))
            throw new IOException("null or empty location");
        if(urn == null)
            throw new IOException("null URN.");
         
        // Case 1. Old-Style direct alt loc.
        if(location.toLowerCase().startsWith("http")) {
            URL url = createUrl(location);
            URN sha1 = URN.createSHA1UrnFromURL(url);
            AlternateLocation al = new DirectAltLoc(url,sha1);
            if(!al.SHA1_URN.equals(urn))
                throw new IOException("mismatched URN");
            return al;
        }
        
        // Case 2. Direct Alt Loc
        if (location.indexOf(";")==-1) {
        	IpPort addr = AlternateLocation.createUrlFromMini(location, urn);
			return new DirectAltLoc(addr, urn);
        }
        
        //Case 3. Push Alt loc
        PushEndpoint pe = new PushEndpoint(location);
        return new PushAltLoc(pe,urn);
    }
	


	/**
	 * Creates a new <tt>AlternateLocation</tt> for the data stored in
	 * a <tt>RemoteFileDesc</tt>.
	 *
	 * @param rfd the <tt>RemoteFileDesc</tt> to use in creating the 
	 *  <tt>AlternateLocation</tt>
	 * @return a new <tt>AlternateLocation</tt>
	 * @throws <tt>IOException</tt> if the <tt>rfd</tt> does not contain
	 *  a valid urn or if it's a private address
	 * @throws <tt>NullPointerException</tt> if the <tt>rfd</tt> is 
	 *  <tt>null</tt>
     * @throws <tt>IOException</tt> if the port is invalid
	 */
	public static AlternateLocation create(final RemoteFileDesc rfd) 
		                                                    throws IOException {
		if(rfd == null)
			throw new NullPointerException("cannot accept null RFD");

		URN urn = rfd.getSHA1Urn();
		if(urn == null)
		    throw new NullPointerException("cannot accept null URN");
		int port = rfd.getPort();

		if (!rfd.needsPush()) {
			return new DirectAltLoc(new Endpoint(rfd.getHost(),rfd.getPort()), urn);
		} else {
		    PushEndpoint copy;
            if (rfd.getPushAddr() != null) 
                copy = rfd.getPushAddr();
            else 
                copy = new PushEndpoint(rfd.getClientGUID(),Collections.EMPTY_SET,0,0,null);
		    return new PushAltLoc(copy,urn);
		} 
	}

	/**
	 * Creates a new <tt>AlternateLocation</tt> for a file stored locally 
	 * with the specified <tt>URN</tt>.
	 * 
	 * Note: the altloc created this way does not know the name of the file.
	 *
	 * @param urn the <tt>URN</tt> of the locally stored file
	 */
	public static AlternateLocation create(URN urn) {
		if(urn == null) throw new NullPointerException("null sha1");
        
		try {
		    
		    // We try to guess whether we are firewalled or not.  If the node
		    // has just started up and has not yet received an incoming connection
		    // our best bet is to see if we have received a connection in the past.
		    //
		    // However it is entirely possible that we have received connection in 
		    // the past but are firewalled this session, so if we are connected
		    // we see if we received a conn this session only.
		    
		    boolean open;
		    
		    if (RouterService.isConnected())
		        open = RouterService.acceptedIncomingConnection();
		    else
		        open = ConnectionSettings.EVER_ACCEPTED_INCOMING.getValue();
		    
		    
			if (open && NetworkUtils.isValidExternalIpPort(IpPortForSelf.instance()))
				return new DirectAltLoc(urn);
			else 
				return new PushAltLoc(urn);
			
		}catch(IOException bad) {
			ErrorService.error(bad);
			return null;
		}
	}


	protected AlternateLocation(URN sha1) throws IOException {
		if(sha1 == null)
            throw new IOException("null sha1");	
		SHA1_URN=sha1;
        legacy = new Average();
        ping = new Average();
        response = new Average();
	}
	

    //////////////////////////////accessors////////////////////////////

	

	/**
	 * Accessor for the SHA1 urn for this <tt>AlternateLocation</tt>.
     * <p>
	 * @return the SHA1 urn for the this <tt>AlternateLocation</tt>
	 */
	public URN getSHA1Urn() { return SHA1_URN; }	
    
    /**
     * Accessor to find if this has been demoted
     */
    public synchronized int getCount() { return _count; }
    

    
    /**
     * package access, accessor to the value of _demoted
     */ 
    public abstract boolean isDemoted();
    
    ////////////////////////////Mesh utility methods////////////////////////////

	public String httpStringValue() {
		if (DISPLAY_STRING == null) 
			DISPLAY_STRING = generateHTTPString();
	    return DISPLAY_STRING;
    }

	
	/**
	 * Creates a new <tt>RemoteFileDesc</tt> from this AlternateLocation
     *
	 * @param size the size of the file for the new <tt>RemoteFileDesc</tt> 
	 *  -- this is necessary to make sure the download bucketing works 
	 *  correctly
	 * @return new <tt>RemoteFileDesc</tt> based off of this, or 
	 *  <tt>null</tt> if the <tt>RemoteFileDesc</tt> could not be created
	 */

	public abstract RemoteFileDesc createRemoteFileDesc(int size);
	
	/**
	 * 
	 * @return whether this is an alternate location pointing to myself.
	 */
	public abstract boolean isMe();
	
	

    /**
     * increment the count.
     * @see demote
     */
    public synchronized void increment() { _count++; }

    /**
     * package access for demoting this.
     */
    abstract void  demote(); 

    /**
     * package access for promoting this.
     */
    abstract void promote(); 

    /**
     * could return null
     */ 
    public abstract AlternateLocation createClone();
    
    
    public synchronized void send(long now, int meshType) {
        switch(meshType) {
        case MESH_LEGACY :
            legacy.send(now);return;
        case MESH_PING :
            ping.send(now);return;
        case MESH_RESPONSE :
            response.send(now);return;
        default :
            throw new IllegalArgumentException("unknown mesh type");
        }
    }
    
    public synchronized boolean canBeSent(int meshType) {
        switch(meshType) {
        case MESH_LEGACY :
            if (!UploadSettings.EXPIRE_LEGACY.getValue())
                return true;
            return  legacy.canBeSent(UploadSettings.LEGACY_BIAS.getValue(), 
                    UploadSettings.LEGACY_EXPIRATION_DAMPER.getValue());
        case MESH_PING :
            if (!UploadSettings.EXPIRE_PING.getValue())
                return true;
            return ping.canBeSent(UploadSettings.PING_BIAS.getValue(),
                    UploadSettings.PING_EXPIRATION_DAMPER.getValue());
        case MESH_RESPONSE :
            if (!UploadSettings.EXPIRE_RESPONSE.getValue())
                return true; 
            return response.canBeSent(UploadSettings.RESPONSE_BIAS.getValue(),
                    UploadSettings.RESPONSE_EXPIRATION_DAMPER.getValue());
            
        default :
            throw new IllegalArgumentException("unknown mesh type");
        }
    }
    
    public synchronized boolean canBeSentAny() {
        return canBeSent(MESH_LEGACY) || canBeSent(MESH_PING) || canBeSent(MESH_RESPONSE);
    }
    
    synchronized void resetSent() {
        ping.reset();
        legacy.reset();
        response.reset();
    }
    
    ///////////////////////////////helpers////////////////////////////////

	/**
	 * Creates a new <tt>URL</tt> instance based on the URL specified in
	 * the alternate location header.
	 * 
	 * @param locationHeader the alternate location header from an HTTP
	 *  header
	 * @return a new <tt>URL</tt> instance for the URL in the alternate
	 *  location header
	 * @throws <tt>IOException</tt> if the url could not be extracted from
	 *  the header in the expected format
	 * @throws <tt>MalformedURLException</tt> if the enclosed URL is not
	 *  formatted correctly
	 */
	private static URL createUrl(final String locationHeader) 
		throws IOException {
		String locHeader = locationHeader.toLowerCase();
		
		//Doesn't start with http? Bad.
		if(!locHeader.startsWith("http"))
		    throw new IOException("invalid location: " + locationHeader);
		
		//Had multiple http's in it? Bad.
		if(locHeader.lastIndexOf("http://") > 4) 
            throw new IOException("invalid location: " + locationHeader);
            
        String urlStr = AlternateLocation.removeTimestamp(locHeader);
        URL url = new URL(urlStr);
        String host = url.getHost();
        
        // Invalid host? Bad.
        if(host == null || host.equals(""))
            throw new IOException("invalid location: " + locationHeader);        
        // If no port, fake it at 80.
        if(url.getPort()==-1)
            url = new URL("http",url.getHost(),80,url.getFile());

		return url;
	}
	
	/**
	 * Creates a new <tt>URL</tt> based on the IP and port in the location
	 * The location MUST be a dotted IP address.
	 */
	private static IpPort createUrlFromMini(final String location, URN urn)
	  throws IOException {
	    int port = location.indexOf(':');
	    final String loc =
	        (port == -1 ? location : location.substring(0, port));
        //Use the IP class as a quick test to make sure it numeric
        try {
            new IP(loc);
        } catch(IllegalArgumentException iae) {
            throw new IOException("invalid location: " + location);
        }
        //But, IP still could have passed if it thought there was a submask
        if( loc.indexOf('/') != -1 )
            throw new IOException("invalid location: " + location);

        //Then make sure it's a valid IP addr.
        if(!NetworkUtils.isValidAddress(loc))
            throw new IOException("invalid location: " + location);
        
        if( port == -1 )
            port = 6346; // default port if not included.
        else {
            // Not enough room for a port.
            if(location.length() < port+1)
                throw new IOException("invalid location: " + location);
            try {
                port = Short.parseShort(location.substring(port+1));
            } catch(NumberFormatException nfe) {
                throw new IOException("invalid location: " + location);
            }
        }
        
        if(!NetworkUtils.isValidPort(port))
            throw new IOException("invalid port: " + port);
	    
	    return new Endpoint(loc,port);
    }

	/**
	 * Removes the timestamp from an alternate location header.  This will
	 * remove the timestamp from an alternate location header string that 
	 * includes the header name, or from an alternate location string that
	 * only contains the alternate location header value.
	 *
	 * @param locationHeader the string containing the full header, or only
	 *  the header value
	 * @return the same string as supplied in the <tt>locationHeader</tt> 
	 *  argument, but with the timestamp removed
	 */
	private static String removeTimestamp(final String locationHeader) {
		StringTokenizer st = new StringTokenizer(locationHeader);
		int numToks = st.countTokens();
		if(numToks == 1) {
			return locationHeader;
		}
		String curTok = null;
		for(int i=0; i<numToks; i++) {
			curTok = st.nextToken();
		}
		
		int tsIndex = locationHeader.indexOf(curTok);
		if(tsIndex == -1) return null;
		return locationHeader.substring(0, tsIndex);
	}

    /////////////////////Object's overridden methods////////////////

	/**
	 * Overrides the equals method to accurately compare 
	 * <tt>AlternateLocation</tt> instances.  <tt>AlternateLocation</tt>s 
	 * are equal if their <tt>URL</tt>s are equal.
	 *
	 * @param obj the <tt>Object</tt> instance to compare to
	 * @return <tt>true</tt> if the <tt>URL</tt> of this
	 *  <tt>AlternateLocation</tt> is equal to the <tt>URL</tt>
	 *  of the <tt>AlternateLocation</tt> location argument,
	 *  and otherwise returns <tt>false</tt>
	 */
	public boolean equals(Object obj) {
		if(obj == this) return true;
		if(!(obj instanceof AlternateLocation)) return false;
		AlternateLocation other = (AlternateLocation)obj;
		
		return SHA1_URN.equals(other.SHA1_URN);
		
	}

    /**
     * The idea is that this is smaller than any AlternateLocation who has a
     * greater value of _count. There is one exception to this rule -- a demoted
     * AlternateLocation has a higher value irrespective of count.
     * <p> 
     * This is because we want to have a sorted set of AlternateLocation where
     * any demoted AlternateLocation is put  at the end of the list
     * because it probably does not work.  
     * <p> 
     * Further we want to get AlternateLocations with smaller counts to be
     * propogated more, since this will serve to get better load balancing of
     * uploader. 
     */
    public int compareTo(Object obj) {
        
        AlternateLocation other = (AlternateLocation) obj;
        
        int ret = _count - other._count;
        if(ret!=0) 
            return ret;
        
        return ret;
 
    }
    
    protected abstract String generateHTTPString();

	/**
	 * Overrides the hashCode method of Object to meet the contract of 
	 * hashCode.  Since we override equals, it is necessary to also 
	 * override hashcode to ensure that two "equal" alternate locations
	 * return the same hashCode, less we unleash unknown havoc on the
	 * hash-based collections.
	 *
	 * @return a hash code value for this object
	 */
	public int hashCode() {
		
        return 17*37+this.SHA1_URN.hashCode();        
	}

    private static class Average {
        /** The number of times this altloc was given out */
        private int numTimes;
        /** The average time in ms between giving out the altloc */
        private double average;
        /** The last time the altloc was given out */
        private long lastSentTime;
        /** The last calculated threshold, -1 if dirty */
        private double cachedTreshold = -1;
        
        public void send(long now) {
            if (lastSentTime == 0)
                lastSentTime = now;
            
            average =  ( (average * numTimes) + (now - lastSentTime) ) / ++numTimes;
            lastSentTime = now;
            cachedTreshold = -1;
        }
        
        public boolean canBeSent(float bias, float damper) {
            if (numTimes < 2 || average == 0)
                return true;
            
            if (cachedTreshold == -1)
                cachedTreshold = Math.abs(Math.log(average) / Math.log(damper));
            
            return numTimes < cachedTreshold * bias;
        }
        
        public void reset() {
            numTimes = 0;
            average = 0;
            lastSentTime = 0;
        }
    }
}









