package com.limegroup.gnutella;

import java.lang.ref.WeakReference;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.limewire.io.Connectable;
import org.limewire.io.IpPort;
import org.limewire.io.IpPortSet;
import org.limewire.io.NetworkInstanceUtils;

import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.google.inject.name.Named;
import com.limegroup.gnutella.dht.db.SearchListener;
import com.limegroup.gnutella.uploader.HTTPHeaderUtils;

@Singleton
class PushEndpointCacheImpl implements PushEndpointCache {
    
    private static final Log LOG = LogFactory.getLog(PushEndpointCacheImpl.class);
    
    /**
     * A mapping from GUID to a CachedPushEndpoint.  This is used to ensure
     * that all PE's will have access to the same PushProxies, even if
     * multiple PE's exist for a single GUID.  Because access to the proxies
     * is referenced from this GUID_PROXY_MAP, the PE will always receive the
     * most up-to-date set of proxies.
     *
     * Insertion to this map must be manually performed, to allow for temporary
     * PE objects that are used to update pre-existing ones.
     *
     * There is no explicit removal from the map -- the Weak nature of it will
     * automatically remove the key/value pairs when the key is garbage
     * collected.  For this reason, all PEs must reference the exact GUID
     * object that is stored in the map -- to ensure that the map will not GC
     * the GUID while it is still in use by a PE.
     *
     * The value is a CachedPushEndpoint SetWrapper (containing a WeakReference to the
     * GUID key as well as the Set of proxies) so that subsequent PEs can 
     * reference the true key object.  A WeakReference is used to allow
     * GC'ing to still work and the map to ultimately remove unused keys.
     */
    private final Map<GUID, CachedPushEndpoint> GUID_PROXY_MAP = 
        Collections.synchronizedMap(new WeakHashMap<GUID, CachedPushEndpoint>());
    
    private final HTTPHeaderUtils httpHeaderUtils;

    private final NetworkInstanceUtils networkInstanceUtils;
    
    @Inject
    PushEndpointCacheImpl(@Named("backgroundExecutor") ScheduledExecutorService backgroundExecutor,
                          HTTPHeaderUtils httpHeaderUtils, NetworkInstanceUtils networkInstanceUtils) {
        this.httpHeaderUtils = httpHeaderUtils;
        this.networkInstanceUtils = networkInstanceUtils;
        backgroundExecutor.scheduleWithFixedDelay(new WeakCleaner(),30*1000,30*1000, TimeUnit.MILLISECONDS);
    }

    /**
     * Overwrites the current known push proxies for the host specified
     * by the GUID, using the the proxies as written in the httpString.
     * 
     * @param guid the guid whose proxies to overwrite
     * @param httpString comma-separated list of proxies and possible proxy features
     */
    public void overwriteProxies(byte [] guid, String httpString) {
        Set<? extends Connectable> newSet = httpHeaderUtils.decodePushProxies(httpString, ",");
        overwriteProxies(guid, newSet);
    }

    /**
     * Overwrites any stored proxies for the host specified by the guid.
     * 
     * @param guid the guid whose proxies to overwrite
     * @param newSet the proxies to overwrite with
     */
    public void overwriteProxies(byte[] guid, Set<? extends IpPort> newSet) {
        GUID g = new GUID(guid);
        CachedPushEndpoint wrapper ;
        synchronized(GUID_PROXY_MAP) {
            wrapper = GUID_PROXY_MAP.get(g);
            if (wrapper==null) {
                wrapper = new CachedPushEndpoint(g, newSet);
                GUID_PROXY_MAP.put(g, wrapper);
            } else {
                wrapper.overwriteProxies(newSet);
            }
        }
    }
    
    public void removePushProxy(byte[] bytes, IpPort pushProxy) {
        if (LOG.isDebugEnabled()) {
            LOG.debug("Removing push proxy: " + pushProxy + " for " + new GUID(bytes));
        }
        GUID guid = new GUID(bytes);
        CachedPushEndpoint cachedPushEndpoint = GUID_PROXY_MAP.get(guid);
        if (cachedPushEndpoint != null) {
            cachedPushEndpoint.removePushProxy(pushProxy);
        }
    }

    /**
     * updates the external address of all PushEndpoints for the given guid
     */
    public void setAddr(byte [] guid, IpPort addr) {
        GUID g = new GUID(guid);
        CachedPushEndpoint current = getCached(g);
        if (current!=null)
            current.setIpPort(addr);
    }

    /**
     * Sets the fwt version supported for all PEs pointing to the
     * given client guid.
     */
    public void setFWTVersionSupported(byte[] guid,int version){
    	GUID g = new GUID(guid);
    	CachedPushEndpoint current = getCached(g);
    	if (current!=null)
    		current.setFWTVersion(version);
    }
    
    public CachedPushEndpoint getCached(GUID guid) {
        return GUID_PROXY_MAP.get(guid);
    }

    public PushEndpoint getPushEndpoint(GUID guid) {
        CachedPushEndpoint cached = GUID_PROXY_MAP.get(guid);
        return cached != null ? cached.createClone() : null;
    }
    
    public void findPushEndpoint(GUID guid, SearchListener<PushEndpoint> listener) {
        PushEndpoint pushEndpoint = getPushEndpoint(guid);
        if (pushEndpoint != null) {
            listener.handleResult(pushEndpoint);
        } else {
            listener.searchFailed();
        }
    }

    public GUID updateProxiesFor(GUID guid, PushEndpoint pushEndpoint, boolean valid) {
        if (LOG.isDebugEnabled()) {
            LOG.debug("Updating proxies for: " + guid + " with: " + pushEndpoint + ", valid: " + valid);
        }
        
        CachedPushEndpoint existing;
        GUID guidRef = null;
        
        synchronized(GUID_PROXY_MAP) {
            existing = GUID_PROXY_MAP.get(guid);

            // try to get a hard ref so that the mapping won't expire
            if (existing!=null)
                guidRef=existing.getGuid();         

            // if we do not have a mapping for this guid, or it just expired,
            // add a new one atomically
            // (we don't care about the proxies of the expired mapping)
            if (existing == null || guidRef==null) {                
                existing = new CachedPushEndpoint(guid, pushEndpoint.getFeatures(), pushEndpoint.getFWTVersion(), 
                        valid ? pushEndpoint.getProxies() : IpPort.EMPTY_SET);
                GUID_PROXY_MAP.put(guid, existing);
                return guid;
            }
        }
        
        // if we got here, means we did have a mapping.  no need to
        // hold the map mutex when updating just the set
        existing.updateProxies(pushEndpoint.getProxies(), valid);
        return guidRef;
    }
    
    public void clear() {
        GUID_PROXY_MAP.clear();
    }
    
    private final class WeakCleaner implements Runnable {
        public void run() {
            GUID_PROXY_MAP.size();
        }
    }
    
    class CachedPushEndpoint extends AbstractPushEndpoint {
        
        private final WeakReference<GUID> _guidRef;
        /**
         * Class invariant: never null
         */
        private Set<IpPort> _proxies;
        private byte _features;
        private int _fwtVersion;
        private IpPort _externalAddr;
        private final byte[] guid;
        
        CachedPushEndpoint(GUID guid, Set<? extends IpPort> proxies) {
            this(guid, (byte)0, 0, proxies);
        }
        
        CachedPushEndpoint(GUID guid, byte features, int version, Set<? extends IpPort> proxies) {
            this.guid = guid.bytes();
            _guidRef = new WeakReference<GUID>(guid);
            _features=features;
            _fwtVersion=version;
            overwriteProxies(proxies);
        }
        
        public synchronized void removePushProxy(IpPort pushProxy) {
            // It's imortant to use IPortSet instead of Collections.singleton for
            // comparator correctness based on IpPort.COMPARATOR
            updateProxies(new IpPortSet(pushProxy), false);
        }
        
        public synchronized void updateProxies(Set<? extends IpPort> s, boolean add){
            Set<IpPort> existing = new IpPortSet(_proxies);
            if (add)
                existing.addAll(s);
            else
                existing.removeAll(s);
            
            _proxies = Collections.unmodifiableSet(existing);
        }
        
        public synchronized void overwriteProxies(Set<? extends IpPort> proxies) {
            _proxies = Collections.unmodifiableSet(new IpPortSet(proxies));
        }
        
        public synchronized Set<IpPort> getProxies() {
            return _proxies;
        }
        
        public synchronized byte getFeatures() {
            return _features;
        }
        
        public synchronized int getFWTVersion() {
            return _fwtVersion;
        }
        
        public synchronized void setFeatures(byte features) {
            _features=features;
        }
        
        public synchronized void setFWTVersion(int version){
            _fwtVersion=version;
        }
        
        public synchronized void setIpPort(IpPort addr) {
            _externalAddr = addr;
        }
        
        public synchronized IpPort getIpPort() {
            return _externalAddr;
        }
        
        public GUID getGuid() {
            return _guidRef.get();
        }

        public synchronized PushEndpoint createClone() {
            return new PushEndpointImpl(guid, getProxies(), 
                    getFeatures(), getFWTVersion(), 
                    getValidExternalAddress(), PushEndpointCacheImpl.this, networkInstanceUtils);
        }

        public byte[] getClientGUID() {
            return guid;
        }

        public synchronized int getPort() {
            IpPort address = _externalAddr;
            return address != null ? address.getPort() : 6346;
        }

        public synchronized IpPort getValidExternalAddress() {
            return _externalAddr;
        }

        public boolean isLocal() {
            return false;
        }

        public synchronized void updateProxies(boolean good) {
            if (!good) {
                _proxies = Collections.emptySet();
            }
        }

        public synchronized String getAddress() {
            IpPort address = _externalAddr;
            return address != null ? _externalAddr.getAddress() : RemoteFileDesc.BOGUS_IP;
        }

        public InetAddress getInetAddress() {
            return null;
        }

        public InetSocketAddress getInetSocketAddress() {
            return null;
        }
    }

}
