package com.limegroup.gnutella.altlocs;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import com.limegroup.gnutella.URN;

public class AltLocManager {

    private static final AltLocManager INSTANCE = new AltLocManager();
    public static AltLocManager instance() {
        return INSTANCE;
    }
    
    /**
     * Mapping of URN - > array of three alternate locations.
     * LOCKING: itself for all map operations as well as operations on the contained arrays
     */
    private final Map urnMap = Collections.synchronizedMap(new HashMap());
    
    private AltLocManager() {}
    
    /**
     * adds a given altloc to the manager
     * @return whether the manager already knew about this altloc
     */
    public boolean add(AlternateLocation al, Object source) {
        URN sha1 = al.getSHA1Urn();
        AlternateLocationCollection col = null;
        
        URNData data;
        synchronized(urnMap) {
            data = (URNData) urnMap.get(sha1);
            
            if (data == null) {
                data = new URNData();
                urnMap.put(sha1,data);
            }
        }
        
        synchronized(data) {    
            if (al instanceof DirectAltLoc) { 
                if (data.direct == AlternateLocationCollection.EMPTY)
                    data.direct = AlternateLocationCollection.create(sha1);
                col = data.direct;
            }
            else {
                PushAltLoc push = (PushAltLoc) al;
                if (push.supportsFWTVersion() < 1) { 
                    if (data.push == AlternateLocationCollection.EMPTY)
                        data.push = AlternateLocationCollection.create(sha1);
                    col = data.push;
                } else { 
                    if (data.fwt == AlternateLocationCollection.EMPTY)
                        data.fwt = AlternateLocationCollection.create(sha1);
                    col = data.fwt;
                }
            }
        }
        
        boolean ret = col.add(al);
        
        // notify any listeners other than the source
        for (Iterator iter = data.getListeners().iterator(); iter.hasNext();) {
            AltLocListener listener = (AltLocListener) iter.next();
            if (listener == source)
                continue;
            listener.locationAdded(al);
        }
        
        return ret;
    }
    
    /**
     * removes the given altloc (implementations may demote)
     */
    public boolean remove(AlternateLocation al, Object source) {
        URN sha1 = al.getSHA1Urn();
        URNData data = (URNData) urnMap.get(sha1);
        if (data == null)
            return false;
        
        AlternateLocationCollection col;
        synchronized(data) {
            if (al instanceof DirectAltLoc) 
                col = data.direct;
            else {
                PushAltLoc push = (PushAltLoc) al;
                if (push.supportsFWTVersion() < 1)
                    col = data.push;
                else
                    col = data.fwt;
            }
        }
        
        if (col == null)
            return false;
        
        boolean ret = col.remove(al);
        
        // if we emptied the current collection, see if the rest are empty as well
        if (!col.hasAlternateLocations())
            removeIfEmpty(sha1,data);
        
        return ret;
    }

    private void removeIfEmpty(URN sha1, URNData data) {
        boolean empty = false;
        synchronized(data) {
            if (!data.direct.hasAlternateLocations() &&
                    !data.push.hasAlternateLocations() &&
                    !data.fwt.hasAlternateLocations() &&
                    data.getListeners().isEmpty())
                empty = true;
        }
        
        if (empty)
            urnMap.remove(sha1);
    }
    
    /**
     * @param sha1 the URN for which to get altlocs
     * @param size the maximum number of altlocs to return
     */
    public AlternateLocationCollection getDirect(URN sha1) {
        URNData data = (URNData) urnMap.get(sha1);
        if (data == null)
            return AlternateLocationCollection.EMPTY;
        
        synchronized(data) {
            return data.direct;
        }
    }
    
    /**
     * @param sha1 the URN for which to get altlocs
     * @param size the maximum number of altlocs to return
     * @param FWTOnly whether the altlocs must support FWT
     */
    public AlternateLocationCollection getPush(URN sha1, boolean FWTOnly) {
        URNData data = (URNData) urnMap.get(sha1);
        if (data == null)
            return AlternateLocationCollection.EMPTY;
        
        synchronized(data) {
            return FWTOnly ? data.fwt : data.push;
        }
    }
    
    public void purge(){
        urnMap.clear();
    }
    
    public void purge(URN sha1) {
        urnMap.remove(sha1);
    }
    
    public boolean hasAltlocs(URN sha1) {
        URNData data = (URNData) urnMap.get(sha1);
        if (data == null)
            return false;
        
        return data.hasAltLocs();
    }
    
    public int getNumLocs(URN sha1) {
        URNData data = (URNData) urnMap.get(sha1);
        if (data == null)
            return 0;
        return data.getNumLocs();
    }
    
    public void addListener(URN sha1, AltLocListener listener) {
        URNData data; 
        synchronized(urnMap){
            data = (URNData) urnMap.get(sha1);
            
            if (data == null) {
                data = new URNData();
                urnMap.put(sha1,data);
            }
        }
        data.addListener(listener);
    }
    
    public void removeListener(URN sha1, AltLocListener listener) {
        URNData data = (URNData) urnMap.get(sha1);
        if (data == null)
            return;
        data.removeListener(listener);
        removeIfEmpty(sha1,data);
    }

    private static class URNData {
        /** 
         * The three alternate locations we keep with this urn.
         * LOCKING: this
         */
        public AlternateLocationCollection direct = AlternateLocationCollection.EMPTY;
        public AlternateLocationCollection push = AlternateLocationCollection.EMPTY;
        public AlternateLocationCollection fwt = AlternateLocationCollection.EMPTY;
        
        private volatile List listeners = Collections.EMPTY_LIST;
        
        public synchronized boolean hasAltLocs() {
            return direct.hasAlternateLocations() || 
            push.hasAlternateLocations() || 
            fwt.hasAlternateLocations();
        }
        
        public synchronized int getNumLocs() {
            return direct.getAltLocsSize() + push.getAltLocsSize() + fwt.getAltLocsSize();
        }
        
        public synchronized void addListener(AltLocListener listener) {
            List updated = new ArrayList(listeners);
            updated.add(listener);
            listeners = updated;
        }
        
        public synchronized void removeListener(AltLocListener listener) {
            List updated = new ArrayList(listeners);
            updated.remove(listener);
            listeners = updated;
        }
        
        public List getListeners() {
            return listeners;
        }
    }
}
