package com.limegroup.gnutella.altlocs;


import org.limewire.util.Objects;

import com.limegroup.gnutella.RemoteFileDesc;
import com.limegroup.gnutella.URN;
import com.limegroup.gnutella.downloader.RemoteFileDescFactory;
import com.limegroup.gnutella.settings.UploadSettings;

/**
 * Abstract super class that encompasses common functionality shared by most implementations
 * of {@link AlternateLocation}.
 */
public abstract class AbstractAlternateLocation implements AlternateLocation {
    
    /**
	 * 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;
	
	protected AbstractAlternateLocation(URN sha1) {
		SHA1_URN= Objects.nonNull(sha1, "sha1");
        legacy = new Average();
        ping = new Average();
        response = new Average();
	}
	

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

	

	/* (non-Javadoc)
     * @see com.limegroup.gnutella.altlocs.AlternateLocation#getSHA1Urn()
     */
	public URN getSHA1Urn() { return SHA1_URN; }	
    
    /* (non-Javadoc)
     * @see com.limegroup.gnutella.altlocs.AlternateLocation#getCount()
     */
    public synchronized int getCount() { return _count; }
    

    
    /* (non-Javadoc)
     * @see com.limegroup.gnutella.altlocs.AlternateLocation#isDemoted()
     */ 
    public abstract boolean isDemoted();
    
    ////////////////////////////Mesh utility methods////////////////////////////

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

	
	/* (non-Javadoc)
     * @see com.limegroup.gnutella.altlocs.AlternateLocation#createRemoteFileDesc(long)
     */

	public abstract RemoteFileDesc createRemoteFileDesc(long size, RemoteFileDescFactory remoteFileDescFactory);
	
	/* (non-Javadoc)
     * @see com.limegroup.gnutella.altlocs.AlternateLocation#isMe()
     */
	public abstract boolean isMe();
	
	

    /* (non-Javadoc)
     * @see com.limegroup.gnutella.altlocs.AlternateLocation#increment()
     */
    public synchronized void increment() { _count++; }

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

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

    /* (non-Javadoc)
     * @see com.limegroup.gnutella.altlocs.AlternateLocation#createClone()
     */ 
    public abstract AlternateLocation createClone();
    
    
    /* (non-Javadoc)
     * @see com.limegroup.gnutella.altlocs.AlternateLocation#send(long, int)
     */
    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");
        }
    }
    
    /* (non-Javadoc)
     * @see com.limegroup.gnutella.altlocs.AlternateLocation#canBeSent(int)
     */
    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");
        }
    }
    
    /* (non-Javadoc)
     * @see com.limegroup.gnutella.altlocs.AlternateLocation#canBeSentAny()
     */
    public synchronized boolean canBeSentAny() {
        return canBeSent(MESH_LEGACY) || canBeSent(MESH_PING) || canBeSent(MESH_RESPONSE);
    }
    
    synchronized void resetSent() {
        ping.reset();
        legacy.reset();
        response.reset();
    }
    
    ///////////////////////////////helpers////////////////////////////////
	
	/**
	 * 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>
	 */
	@Override
    public boolean equals(Object obj) {
		if(obj == this) return true;
		if(!(obj instanceof AbstractAlternateLocation)) return false;
		AbstractAlternateLocation other = (AbstractAlternateLocation)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(AlternateLocation other) {
        int ret = _count - other.getCount();
        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
	 */
	@Override
    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;
        }
    }
}
