package com.limegroup.gnutella.filters;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.limewire.collection.PatriciaTrie;
import org.limewire.collection.Trie;
import org.limewire.collection.PatriciaTrie.KeyAnalyzer;
import org.limewire.collection.Trie.Cursor;
import org.limewire.io.IP;
import org.limewire.io.NetworkInstanceUtils;
import org.limewire.io.NetworkUtils;
import org.limewire.util.ByteUtils;


/**
 * A mutable list of IP addresses.  More specifically, a list of sets of
 * addresses, like "18.239.0.*".  Provides fast operations to find if an address
 * is in the list.  Used to implement IPFilter.  Not synchronized. 
 * <p>
 * This class is optimized by the use of a PATRICIA Trie to store the ranges.
 * Many of the optimizations work because of two key properties that we use
 * when inserting items into the Trie.
 * <pre>
 *   1) If the item to be inserted is within a range already in the Trie,
 *      the item is not inserted.
 *   2) If the item to be inserted contains any items that are within the
 *      Trie, those items are removed.
 * </pre>     
 * Maintaining these properties allows certain necessary optimizations, such as
 * looking at only the closest node when performing a lookup.  If these
 * optimizations were not done, then certain items would appear closer within
 * the Trie, despite there being a range further away that encompassed a given IP.
 * <p>
 * Using a PATRICIA allows an intelligent traversal to be done, so that at most
 * 32 comparisons (the number of bits in an address) are performed regardless 
 * of the number of items inserted into the Trie.  It also allows very efficient
 * means of calculating the minimum distance (using an xor metric), because the
 * Trie orders the IPs by distance.  
 */
public class IPList {
    
    /** A null IP, to use as a comparison when adding. */
    private static final IP NULL_IP = new IP("*.*.*.*");
    
    /** The list of IPs. */
    private Trie<IP, IP> ips = new PatriciaTrie<IP, IP>(new IPKeyAnalyzer());

    public IPList () {}
    
    /**
     * Determines if any hosts exist in this list.
     */
    public synchronized boolean isEmpty() {
        return ips.isEmpty();
    }
    
    /** Gets the number of addresses loaded. */
    public synchronized int size() {
        return ips.size();
    }
    
    public void add(String ipStr) {
        IP ip;
        try {
            ip = new IP(ipStr);
        } catch (IllegalArgumentException e) {
            return;
        }
        add(ip);
    }
    
    /** 
     * Adds a certain IP to the IPList.
     * @param ipStr a String containing the IP, see IP.java for formatting
     */
    public synchronized void add(IP ip) {
        // SPECIAL-CASE:
        // If the IP we're adding is the 'null' key (0.0.0.0/0.0.0.0)
        // then we must clear the trie.  The AddFilter trick will not
        // work in this case.
        if(ip.equals(NULL_IP)) {
            ips.clear();
            ips.put(ip, ip);
            return;
        }
        
        if(!NetworkUtils.isValidAddress(ip)) {
            return;
        }
                
        // If we already had it (or an address that contained it),
        // then don't include.  Also remove any IPs we encountered
        // that are contained by this new IP.
        // These two properties are necessary to allow the optimization
        // in Lookup to exit when the distance is greater than 1.
        AddFilter filter = new AddFilter(ip);
        Map.Entry<IP, IP> entry = ips.select(ip, filter);
        if(entry != null) {
            if(!entry.getKey().contains(ip)) {
                for(IP obsolete : filter.getContained()) {
                    ips.remove(obsolete);
                }
                ips.put(ip, ip);
            }
        } else {
            ips.put(ip, ip);
        }
    }

    /**
     * @param String equal to an IP
     * @returns true if ip_address is contained somewhere in the list of IPs
     */
    public synchronized boolean contains(IP lookup) {
        IP ip = ips.select(lookup);        
        return ip != null && ip.contains(lookup);
    }
    
    /**
     * Determines if this filter is valid.  If private IPs are not allowed,
     * NetworkInstanceUtils must be non-null in order to check if an address
     * is considered private.  If allowPrivateIPs is true, networkInstanceUtils
     * can be null.
     */
    public synchronized boolean isValidFilter(boolean allowPrivateIPs, NetworkInstanceUtils networkInstanceUtils) {
        ValidFilter filter = new ValidFilter(allowPrivateIPs, networkInstanceUtils);
        ips.traverse(filter);
        return filter.isValid();
    }
    
    /**
     * Calculates the first set bit in the distance between an IPv4 address and
     * the ranges represented by this list.
     * 
     * This is equivalent to floor(log2(distance)) + 1.
     *  
     * @param ip an IPv4 address, represented as an IP object with a /32 netmask.
     * @return an int on the interval [0,31].
     */
    public int logMinDistanceTo(IP ip) {
        int distance = minDistanceTo(ip);
        int logDistance = 0;
        int testMask = -1; // All bits set
        // Guaranteed to terminate since testMask will reach zero
        while ((distance & testMask) != 0) {
            testMask <<= 1;
            ++logDistance;
        }
        return logDistance;
    }
    
    
    /**
     * Calculates the minimum distance between an IPv4 address and this list of IPv4 address ranges.
     * 
     * @param lookup an IPv4 address, represented as an IP object with a /32 netmask.
     * @return 32-bit unsigned distance (using xor metric), represented as Java int
     */
    public synchronized int minDistanceTo(IP lookup) {
        if (lookup.mask != -1) {
            throw new IllegalArgumentException("Expected single IP, not an IP range.");
        }
        
        // The nature of the PATRICIA Trie & the distance (using an xor metric)
        // work well in that the closest item within the trie is also the shortest
        // distance.
        IP ip = ips.select(lookup);
        if(ip == null)
            return Integer.MAX_VALUE;
        else
            return ip.getDistanceTo(lookup);
    }
    
    /**
     *  A trie cursor that determines if the IP list contained in the
     *  trie is valid or not. 
     *  A list is considered invalid if :
     *  1) It contains a private IP
     *  2) It contains an invalid IP
     *  3) It spans a range of hosts larger than the MAX_LIST_SPACE constant
     *
     */
    private static class ValidFilter implements Trie.Cursor<IP, IP> {
        
        /** The space covered by private addresses */
        private static final int INVALID_SPACE = 60882944;
        
        /** The total IP space available */
        private static final long TOTAL_SPACE = (long)Math.pow(2,32) - INVALID_SPACE;
        
        /** The maximum IP space (in percent) for this IPList to be valid */
        private static final float MAX_LIST_SPACE = 0.025f;
        
        private boolean isInvalid;
        private long counter;
        
        private final boolean allowPrivateIPs;
        private final NetworkInstanceUtils networkInstanceUtils;
        
        public boolean isValid() {
            return !isInvalid && ((counter/(float)TOTAL_SPACE) < MAX_LIST_SPACE) ;
        }
        
        public ValidFilter(boolean allowPrivateIPs, NetworkInstanceUtils networkInstanceUtils) {
            this.allowPrivateIPs = allowPrivateIPs;
            this.networkInstanceUtils = networkInstanceUtils;
        }
        
        public SelectStatus select(Entry<? extends IP, ? extends IP> entry) {
            IP key = entry.getKey();
            byte[] buf = new byte[4];
            ByteUtils.int2beb(key.addr,buf,0);
            
            if(!allowPrivateIPs && networkInstanceUtils.isPrivateAddress(buf)) {
                isInvalid = true;
                return SelectStatus.EXIT;
            }
            
            counter += Math.pow(2,countBits(~key.mask));
            return SelectStatus.CONTINUE;
        }
        
        /**
         * Counts number of 1 bits in a 32 bit unsigned number.
         *
         * @param x unsigned 32 bit number whose bits you wish to count.
         *
         * @return number of 1 bits in x.
         * @author Roedy Green
         */
        private int countBits( int x ) {
           // collapsing partial parallel sums method
           // collapse 32x1 bit counts to 16x2 bit counts, mask 01010101
           x = (x >>> 1 & 0x55555555) + (x & 0x55555555);
           // collapse 16x2 bit counts to 8x4 bit counts, mask 00110011
           x = (x >>> 2 & 0x33333333) + (x & 0x33333333);
           // collapse 8x4 bit counts to 4x8 bit counts, mask 00001111
           x = (x >>> 4 & 0x0f0f0f0f) + (x & 0x0f0f0f0f);
           // collapse 4x8 bit counts to 2x16 bit counts
           x = (x >>> 8 & 0x00ff00ff) + (x & 0x00ff00ff);
           // collapse 2x16 bit counts to 1x32 bit count
           return(x >>> 16) + (x & 0x0000ffff);
       }
        
    }
    
    /**
     * A filter for adding IPs -- stores IPs we encountered that
     * are contained by the to-be-added IP, so they can later
     * be removed.
     */
    private static class AddFilter implements Trie.Cursor<IP, IP> {
        private final IP lookup;
        private List<IP> contained;
        
        AddFilter(IP lookup) {
            this.lookup = lookup;
        }
        
        /**
         * Returns all the IPs we encountered while selecting
         * that were contained by the IP being added.
         */
        public List<IP> getContained() {
            if(contained == null)
                return Collections.emptyList();
            else
                return contained;
        }
        
        public Cursor.SelectStatus select(Map.Entry<? extends IP, ? extends IP> entry) {
            IP compare = entry.getKey();
            if (compare.contains(lookup)) {
                return Cursor.SelectStatus.EXIT; // Terminate
            }
            
            if(lookup.contains(compare)) {
                if(contained == null)
                    contained = new ArrayList<IP>();
                contained.add(compare);
                return SelectStatus.CONTINUE;
            } else {
                // Because select traverses in XOR closeness,
                // the first time we encounter an item that's
                // not contained, we know we've exhausted all
                // possible containing values.
                return SelectStatus.EXIT;
            }
        }
    };
    
    private static class IPKeyAnalyzer implements KeyAnalyzer<IP> {

        private static final int[] createIntBitMask(final int bitCount) {
            int[] bits = new int[bitCount];
            for(int i = 0; i < bitCount; i++) {
                bits[i] = 1 << (bitCount - i - 1);
            }
            return bits;
        }
        
        private static final int[] BITS = createIntBitMask(32);
        
        public int length(IP key) {
            return 32;
        }

        public boolean isBitSet(IP key, int keyLength, int bitIndex) {
            int maddr = key.addr & key.mask;
            return (maddr & BITS[bitIndex]) != 0;
        }
        
        public int bitIndex(IP key,   int keyOff, int keyLength,
                            IP found, int foundOff, int foundKeyLength) {
            int maddr1 = key.addr & key.mask;
            int maddr2 = (found != null) ? found.addr & found.mask : 0;
            
            if(keyOff != 0 || foundOff != 0)
                throw new IllegalArgumentException("offsets must be 0 for fixed-size keys");
            
            int length = Math.max(keyLength, foundKeyLength);
            
            boolean allNull = true;
            for (int i = 0; i < length; i++) {
                int a = maddr1 & BITS[i];
                int b = maddr2 & BITS[i];
                
                if (allNull && a != 0) {
                    allNull = false;
                }
                
                if (a != b) {
                    return i;
                }
            }
            
            if (allNull) {
                return KeyAnalyzer.NULL_BIT_KEY;
            }
            
            return KeyAnalyzer.EQUAL_BIT_KEY;
        }

        public int compare(IP o1, IP o2) {
            int addr1 = o1.addr & o1.mask;
            int addr2 = o2.addr & o2.mask;
            if(addr1 > addr2)
                return 1;
            else if(addr1 < addr2)
                return -1;
            else
                return 0;
                
        }

        // This method is generally intended for variable length keys.
        // Fixed-length keys, such as an IP address (32 bits) tend to
        // look at each element as a bit, thus 1 element == 1 bit.
        public int bitsPerElement() {
            return 1;
        }

        public boolean isPrefix(IP prefix, int offset, int length, IP key) {
            int addr1 = prefix.addr & prefix.mask;
            int addr2 = key.addr & key.mask;
            addr1 = addr1 << offset;
            
            int mask = 0;
            for(int i = 0; i < length; i++) {
                mask |= (0x1 << i);
            }
            
            addr1 &= mask;
            addr2 &= mask;
            
            return addr1 == addr2;
        }
    }
}
