package com.limegroup.gnutella.spam;

import java.util.Arrays;

import com.limegroup.gnutella.Assert;
import com.limegroup.gnutella.filters.IP;
import com.limegroup.gnutella.filters.IPFilter;

/**
 * This is a simple token holding a 4-byte-ip / port pair
 */
public class AddressToken extends AbstractToken {
	private static final long serialVersionUID = 3257568416670824244L;

	private static final int TYPE = TYPE_ADDRESS;

	/**
	 * must be positive
	 * 
	 * This is a heuristic value to prevent an IP address from becoming bad
	 * after only a small number of bad ratings.
	 */
	private static final byte INITIAL_GOOD = 20;

	/**
	 * must be positive
	 * 
	 * This value determines how dynamic the filter is. A low MAX value will
	 * allow this Token to get a bad rating after occourring only a few times in
	 * spam, a high value will make it very improbable that the filter will
	 * change its mind about a certain token without user-intervention
	 */
	private static final int MAX = 50;

	private final byte[] _address;

	private final short _port;
    
    private boolean ratingInitialized;

	private byte _good;

	private byte _bad;
    
    private final int _hashCode;

	public AddressToken(byte[] address, int port) {
		Assert.that(address.length == 4);
		_address = address;
		_port = (short) port;
        _hashCode = getHashCode();
        ratingInitialized = false;
	}
    
    /* Rating initialization is costly, and many Tokens are created 
     * simply to be replaced by existing tokens in the RatingTable,
     * so initialize rating lazily. */
    private synchronized void initializeRating() {
        if (ratingInitialized) {
            return;
        }
        // this initial value is an
        _good = INITIAL_GOOD;
        IP ip = new IP(_address);
        int logDistance = IPFilter.instance().getBadHosts().logMinDistanceTo(ip);
        
        // Constants 1600 and 3.3 chosen such that:
        // Same /24 subnet as a banned IP results in a rating of 0.07
        // Same /16 subnet as a banned IP results in a rating of 0.01
        int bad = (int) (1600 * Math.pow(1+logDistance, -3.3));
        while (bad > MAX) {
            bad /= 2;
            _good /= 2;
        }
        _bad = (byte) bad;
        
        ratingInitialized = true;
    }
    
    private int getHashCode() {
        int hashCode = TYPE;
        hashCode = (TYPE * hashCode) + _address[0];
        hashCode = (TYPE * hashCode) + _address[1];
        hashCode = (TYPE * hashCode) + _address[2];
        hashCode = (TYPE * hashCode) + _address[3];
        hashCode = (TYPE * hashCode) + _port;
        return hashCode;
    }

	/**
	 * implements interface <tt>Token</tt>
	 */
	public float getRating() {
        if (!ratingInitialized) {
            initializeRating();
        }
		return ((float) _bad )/(_good + _bad + 1);
	}

    /**
     * implements interface <tt>Token</tt>
     */
    public double getImportance() {
        // Throw out AddressTokens first, since they
        // can mostly be regenerated from the list of
        // blocked IP addresses.
        return Double.NEGATIVE_INFINITY;
    }
    
	/**
	 * implements interface <tt>Token</tt>
	 */
	public void rate(int rating) {
        if (!ratingInitialized) {
            initializeRating();
        }
		_age = 0;
		switch (rating) {
		case RATING_GOOD:
			_good++;
			break;
		case RATING_SPAM:
			_bad++;
			break;
		case RATING_USER_MARKED_GOOD:
			_bad = 0;
			break;
		case RATING_USER_MARKED_SPAM:
			_bad = (byte) Math.min(_bad + 10, MAX);
			break;
		case RATING_CLEARED:
			_bad = 0;
			_good = INITIAL_GOOD;
			break;
		default:
			throw new IllegalArgumentException("unknown type of rating");
		}

		if (_good >= MAX || _bad >= MAX) {
			_good = (byte) (_good * 9 / 10);
			_bad = (byte) (_bad * 9 / 10);
		}
	}

	/**
	 * implements interface <tt>Token</tt>
	 */
	public int getType() {
		return TYPE;
	}

    public final int hashCode() {
        return _hashCode;
    }
    
    public final boolean equals(Object o) {
        if (o == null)
            return false;
        if ( ! (o instanceof AddressToken))
            return false;
        
        if (_hashCode != o.hashCode()) {
            return false;
        }
        
        return _port == ((AddressToken)o)._port 
                && Arrays.equals(_address, ((AddressToken)o)._address);
    }

	/**
	 * overrides method from <tt>Object</tt>
	 */
	public String toString() {
		return "" + (0xFF & _address[0]) + "." + (0xFF & _address[1]) + "."
				+ (0xFF & _address[2]) + "." + (0xFF & _address[3]) + ":"
				+ (0xFFFF & _port) + " " + _bad;
	}
}
