package com.limegroup.gnutella.altlocs;

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

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.limewire.inspection.Inspectable;
import org.limewire.inspection.InspectableContainer;
import org.limewire.inspection.InspectionPoint;

import com.google.inject.Singleton;
import com.limegroup.gnutella.URN;
import com.limegroup.gnutella.util.ClassCNetworks;

@Singleton
public class AltLocManager {

    private static final Log LOG = LogFactory.getLog(AltLocManager.class);
    
    /**
     * Map of the alternate location collections for each URN.
     * LOCKING: itself for all map operations as well as operations on the contained arrays
     */
    private final Map<URN, URNData> urnMap = Collections.synchronizedMap(new HashMap<URN, URNData>());
    
    /**
     * adds a given altloc to the manager
     * @return whether the manager already knew about this altloc
     */
    public boolean add(AlternateLocation al, Object source) {
        if (LOG.isTraceEnabled()) {
            LOG.trace("alternate location added: " + al);
        }
        
        URN sha1 = al.getSHA1Urn();
        AlternateLocationCollection<DirectAltLoc> dCol = null;
        AlternateLocationCollection<PushAltLoc>   pCol = null;
        
        URNData data;
        synchronized(urnMap) {
            data = 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);
                dCol = data.direct;
            } else if(al instanceof PushAltLoc) {
                PushAltLoc push = (PushAltLoc) al;
                if (push.supportsFWTVersion() < 1) { 
                    if (data.push == AlternateLocationCollection.EMPTY)
                        data.push = AlternateLocationCollection.create(sha1);
                    pCol = data.push;
                } else { 
                    if (data.fwt == AlternateLocationCollection.EMPTY)
                        data.fwt = AlternateLocationCollection.create(sha1);
                    pCol = data.fwt;
                }
            } else {
                throw new IllegalStateException("unknown loc class: " + al.getClass());
            }
        }
        
        boolean ret = false;
        if(dCol != null) {
            ret = dCol.add((DirectAltLoc)al);
        } else if(pCol != null) {
            ret = pCol.add((PushAltLoc)al);
        } else {
            throw new IllegalStateException("didn't set a collection!");
        }
        
        // notify any listeners other than the source
        for(AltLocListener listener : data.getListeners()) {
            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 = urnMap.get(sha1);
        if (data == null)
            return false;

        AlternateLocationCollection<DirectAltLoc> dCol = null;
        AlternateLocationCollection<PushAltLoc>   pCol = null;
        synchronized(data) {
            if (al instanceof DirectAltLoc) { 
                dCol = data.direct;
            } else {
                PushAltLoc push = (PushAltLoc) al;
                if (push.supportsFWTVersion() < 1)
                    pCol = data.push;
                else
                    pCol = data.fwt;
            }
        }

        AlternateLocationCollection col = null;
        boolean ret = false;
        if(dCol != null) {
            ret = dCol.remove((DirectAltLoc)al);
            col = dCol;
        } else if(pCol != null) {
            ret = pCol.remove((PushAltLoc)al);
            col = pCol;
        } else {
            return false;
        }
        
        // 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
     */
    public AlternateLocationCollection<DirectAltLoc> getDirect(URN sha1) {
        URNData data = urnMap.get(sha1);
        if (data == null)
            return AlternateLocationCollection.getEmptyCollection();
        
        synchronized(data) {
            return data.direct;
        }
    }
    
    /**
     * Returns push alternate locations that do not support FWT.
     * @param sha1 the URN for which to get altlocs
     */
    public AlternateLocationCollection<PushAltLoc> getPushNoFWT(URN sha1) {
        URNData data = urnMap.get(sha1);
        if (data == null)
            return AlternateLocationCollection.getEmptyCollection();
        
        synchronized(data) {
            return data.push;
        }
    }
    
    /**
     * Returns push alternate locations that support FWT.
     * @param sha1 the URN for which to get altlocs
     */
    public AlternateLocationCollection<PushAltLoc> getPushFWT(URN sha1) {
        URNData data = urnMap.get(sha1);
        if (data == null)
            return AlternateLocationCollection.getEmptyCollection();
        
        synchronized(data) {
            return data.fwt;
        }
    }
    
    public void purge(){
        urnMap.clear();
    }
    
    public void purge(URN sha1) {
        urnMap.remove(sha1);
    }
    
    public boolean hasAltlocs(URN sha1) {
        URNData data = urnMap.get(sha1);
        if (data == null)
            return false;
        
        return data.hasAltLocs();
    }
    
    public int getNumLocs(URN sha1) {
        URNData data = urnMap.get(sha1);
        if (data == null)
            return 0;
        return data.getNumLocs();
    }
    
    public void addListener(URN sha1, AltLocListener listener) {
        URNData data; 
        synchronized(urnMap){
            data = 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 =  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<DirectAltLoc> direct = AlternateLocationCollection.getEmptyCollection();
        public AlternateLocationCollection<PushAltLoc> push = AlternateLocationCollection.getEmptyCollection();
        public AlternateLocationCollection<PushAltLoc> fwt = AlternateLocationCollection.getEmptyCollection();
        
        private volatile List<AltLocListener> listeners = Collections.emptyList();
        
        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<AltLocListener> updated = new ArrayList<AltLocListener>(listeners);
            updated.add(listener);
            listeners = updated;
        }
        
        public synchronized void removeListener(AltLocListener listener) {
            List<AltLocListener> updated = new ArrayList<AltLocListener>(listeners);
            updated.remove(listener);
            listeners = updated;
        }
        
        public List<AltLocListener> getListeners() {
            return listeners;
        }
    }
    
    @SuppressWarnings("unused")
    @InspectableContainer
    private class ALMInspectables {
        /** Inspectable for class C networks of alternate locations */
        @InspectionPoint("alternate locations class C stats")
        public final Inspectable classC = new Inspectable() {
            public Object inspect() {
                ClassCNetworks direct = new ClassCNetworks();
                ClassCNetworks pushProxies = new ClassCNetworks();
                synchronized(urnMap) {
                    for (URNData data : urnMap.values()) {
                        for( DirectAltLoc dal : data.direct)
                            direct.add(dal.getHost().getInetAddress(), 1);
                        for (PushAltLoc push : data.push) 
                            pushProxies.addAll(push.getPushAddress().getProxies());
                        for (PushAltLoc push : data.fwt) 
                            pushProxies.addAll(push.getPushAddress().getProxies());
                    }
                }
                Map<String, Object> ret = new HashMap<String, Object>();
                ret.put("direct", direct.getTopInspectable(10));
                ret.put("proxies", pushProxies.getTopInspectable(10));
                return ret;
            }
        };
    }
}
