package com.limegroup.gnutella.uploader;


import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.limewire.collection.MultiIterable;
import org.limewire.collection.NumericBuffer;
import org.limewire.collection.QueueCounter;
import org.limewire.core.settings.UploadSettings;

import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.limegroup.gnutella.InsufficientDataException;


/**
 * This class implements the logic of managing BT uploads and HTTP Uploads.
 * More information available see  
 * http://wiki.limewire.org/index.php?title=UploadSlotsAndBT
 */
@Singleton
public class UploadSlotManagerImpl implements UploadSlotManager {
    
    private static final Log LOG = LogFactory.getLog(UploadSlotManagerImpl.class);
    
    /**
     * The three priority levels
     */
    private static final int BT_SEED = 0; // low priority
    private static final int HTTP = 1; // medium priority
    private static final int HIGH = 2; // torrent downloads and metafiles.
    
    /** 
     * The desired minimum quality of service to provide for uploads, in
     *  B/ms
     */
    private static final float MINIMUM_UPLOAD_SPEED = 3.0f;
    
    /**
     * The list of active upload slot requests
     * INVARIANT: sorted by priority and contains only 
     * requests of the highest priority or non-preemptible requests
     */
    private final CountingList <UploadSlotRequest> active;
    
    /**
     * The list of queued non-resumable requests
     */
    private final CountingList <HTTPSlotRequest> queued;
    
    /**
     * The list of queued resumable requests
     * (currently only Seeding BT Uploaders)
     */
    private final CountingList<BTSlotRequest> queuedResumable;
    
    private final MultiIterable<UploadSlotRequest> allRequests;
    
    private final NumericBuffer<Float> bandwidth = new NumericBuffer<Float>(10);
    
    private float sessionAverage;
    private int numMeasures;
    
    @Inject
    public UploadSlotManagerImpl() {
        active = new CountingList<UploadSlotRequest>();
        queued = new CountingList<HTTPSlotRequest>();
        queuedResumable = new CountingList<BTSlotRequest>();
        allRequests = new MultiIterable<UploadSlotRequest>(active, queued, queuedResumable);
    }

    /**
     * Polls for an available upload slot. (HTTP-style)
     * 
     * @param user the user that will use the upload slot
     * @param queue if the user can enter the queue 
     * @param highPriority if the user needs an upload slot now or never 
     * @return the position in the queue if queued, -1 if rejected,
     * 0 if it can proceed immediately
     */
    public int pollForSlot(UploadSlotUser user, boolean queue, boolean highPriority) {
        if (LOG.isDebugEnabled())
            LOG.debug(user+" polling for slot, queuable "+queue);
        return requestSlot(new HTTPSlotRequest(user, queue, highPriority));
    }
    
    /**
     * Requests an upload slot. (BT-style)
     * 
     * @param listener the listener that should be notified when a slot
     * becomes available
     * @param highPriority if the user needs an upload slot now or never
     * @return the position of the upload if queued, -1 if rejected, 0 if 
     * it can proceed immediately.
     */
    public int requestSlot(UploadSlotListener listener, boolean highPriority) {
        if (LOG.isDebugEnabled())
            LOG.debug(listener+" requesting slot, high priority "+highPriority);
        return requestSlot(new BTSlotRequest(listener, highPriority));
    }

    private synchronized int requestSlot(UploadSlotRequest request) {
        // see if there exists an uploader with higher priority
        boolean existHigherPriority = existActiveHigherPriority(request.getPriority());
        
        // see if this is already in the queue
        int positionInQueue = positionInQueue(request);
        
        // see if there are any uploaders with lower priority
        int freeableSlots = getPreemptible(request.getPriority());
        
        // if there is a higher priority upload or not enough free slots, queue.
        if (existHigherPriority || 
                !hasFreeSlot(active.size() + 
                        Math.max(0,positionInQueue) - 
                        freeableSlots)) {
            
            if (!request.isQueuable()) 
                return -1;
            
            if (positionInQueue >= 0)
                return ++positionInQueue;
            else
                return queueRequest(request);
        }
        
        // free any freeable slots
        if (freeableSlots > 0)
            killPreemptible(request.getPriority());

        // remove from queue if it was there
        if (positionInQueue > -1)
            removeIfQueued(request.getUser());
        
        addActiveRequest(request);
        return 0;
    }
    
    /**
     * @return the position in the appropriate queue of the request
     *    0 if not in the queue
     */
    private int positionInQueue(UploadSlotRequest request) {
        return getQueue(request.getUser()).indexOf(request);
    }
    
    public synchronized int positionInQueue(UploadSlotUser user) {
        List<? extends UploadSlotRequest> queue = getQueue(user);
        for(int i = 0; i < queue.size();i++) {
            UploadSlotRequest request = queue.get(i);
            if (request.getUser() == user)
                return i;
        }
        return -1;
    }
    /**
     * @return the queue where requests from the user would be found.
     */
    private List<? extends UploadSlotRequest> getQueue(UploadSlotUser user) {
        return user instanceof UploadSlotListener ? queuedResumable : queued;
    }
    
    /**
     * @return if there are any active users with higher priority
     */
    private boolean existActiveHigherPriority(int priority) {
        if (priority == HIGH) 
            return false;
        
        if (!active.isEmpty()) {
            UploadSlotRequest max = active.get(0);
            if (max.getPriority() > priority)
                return true;
        }
        return false;
    }
    
    /**
     * @return the number of active uploaders with lower priority
     * that can be preempted.
     */
    private int getPreemptible(int priority) {
        if (priority == BT_SEED)
            return 0;
        
        // iterate backwards
        int ret = 0;
        for(int i = active.size() - 1; i >= 0; i--) {
            UploadSlotRequest request = active.get(i);
            if (request.getPriority() < priority && request.isPreemptible())
                ret++;
        }
        return ret;
    }
    
    /**
     * Kills any active uploaders that can be preempted and have lower priority.
     */
    private void killPreemptible(int priority) {
        for(int i = active.size() - 1; i >= 0; i--) {
            UploadSlotRequest request = active.get(i);
            if (request.getPriority() < priority && request.isPreemptible()) {
                if (LOG.isDebugEnabled())
                    LOG.debug("freeing slot from "+request.getUser());
                active.remove(i);
                request.getUser().releaseSlot();
            }
        }
    }

    /**
     * @return whether there is a free slot for an HTTP uploader.
     */
    public synchronized boolean hasHTTPSlot(int current) {
        // This ignores currently active BT_SEED uploaders since they 
        // can be preempted.
        if (existActiveHigherPriority(HTTP))
            return false;
        
        return hasFreeSlot(current);
    }
    
    /**
     * @return true if there may be a free slot for an HTTP uploader.
     */
    public synchronized boolean hasHTTPSlotForMeta(int current) {
        
        return hasFreeSlot(current);
    }
    
    /**
     * @return whether there would be a free slot if current many were taken.
     */
    private boolean hasFreeSlot(int current) {
        //Allow another upload if (a) we currently have fewer than
        //SOFT_MAX_UPLOADS uploads or (b) some upload has more than
        //MINIMUM_UPLOAD_SPEED KB/s.  But never allow more than MAX_UPLOADS.
        //
        //In other words, we continue to allow uploads until everyone's
        //bandwidth is diluted.  The assumption is that with MAX_UPLOADS
        //uploads, the probability that all just happen to have low capacity
        //(e.g., modems) is small.  This reduces "Try Again Later"'s at the
        //expensive of quality, making swarmed downloads work better.
        
        if (current >= UploadSettings.HARD_MAX_UPLOADS.getValue()) 
            return false;
        else if (current < UploadSettings.SOFT_MAX_UPLOADS.getValue()) 
            return true;
        else {
            float fastest = 0f;
            for (UploadSlotRequest request : active) {
                UploadSlotUser user = request.getUser();
                float speed = 0;
                user.measureBandwidth();
                try {
                    speed = user.getMeasuredBandwidth();
                } catch (InsufficientDataException ide) {}
                fastest = Math.max(fastest,speed);
                if (fastest > MINIMUM_UPLOAD_SPEED) 
                    return true;
                
            }
            return false;
        }
    }
    
    public synchronized void measureBandwidth() {
        float bw = getTotalBandwidth();
        sessionAverage = ((sessionAverage * numMeasures) + bw) / (++numMeasures);
        bandwidth.add(bw);
    }
    
    public synchronized float getMeasuredBandwidth() throws InsufficientDataException {
        if (bandwidth.size() < bandwidth.getCapacity())
            throw new InsufficientDataException();
        return bandwidth.average().floatValue();
    }
    
    public synchronized float getAverageBandwidth() {
        return sessionAverage;
    }
    
    private float getTotalBandwidth() {
        float ret = 0;
        for (UploadSlotRequest request : active) {
            UploadSlotUser user = request.getUser();
            user.measureBandwidth();
            try {
                ret += user.getMeasuredBandwidth();
            } catch (InsufficientDataException ide) {}
        }
        return ret;
    }
    
    /**
     * Adds a request to the appropriate queue if not already there.
     * @return the position in the queue (>= 1)
     */
    @SuppressWarnings("unchecked")
    private <Request_t extends UploadSlotRequest>int queueRequest(Request_t request) {
        List<Request_t> queue = (List<Request_t>)getQueue(request.user);
        if (queue.size() == UploadSettings.UPLOAD_QUEUE_SIZE.getValue())
            return -1;
        queue.add(request);
        
        if (LOG.isDebugEnabled())
            LOG.debug("queued "+request.getUser()+" at position "+queue.size());
        
        return queue.size();
    }
    
    /**
     * Adds an active request.  
     */
    private void addActiveRequest(UploadSlotRequest request) {
        int i = 0;
        for(; i < active.size(); i++) {
            UploadSlotRequest current = active.get(i);
            if (current.getPriority() < request.getPriority()) 
                break;
        }
        
        if (LOG.isDebugEnabled())
            LOG.debug("added active request "+request.getUser()+" at position "+i);
        
        active.add(i,request);
    }
    
    /**
     * Cancels the request issued by this UploadSlotListener.
     */
    public synchronized void cancelRequest(UploadSlotUser user) {
        if (LOG.isDebugEnabled())
            LOG.debug(user +" cancelling request");
        if (!removeIfQueued(user))
            requestDone(user);
    }

    /**
     * Removes an UploadSlotUser from the queue. 
     * @return if the user was in the queue.
     */
    private boolean removeIfQueued(UploadSlotUser user) {
        List<? extends UploadSlotRequest> queue = getQueue(user);
        for (Iterator<? extends UploadSlotRequest> iter = queue.iterator(); iter.hasNext();) {
            UploadSlotRequest request = iter.next();
            if (request.getUser() == user) {
                iter.remove();
                if (LOG.isDebugEnabled())
                    LOG.debug("remove queued request by "+user);
                return true;
            }
        }
        return false;
    }

    /**
     * Notification that the UploadSlotUser is done with its request.
     */
    public synchronized void requestDone(UploadSlotUser user) {
        for (Iterator<? extends UploadSlotRequest> iter = active.iterator(); iter.hasNext();) {
            UploadSlotRequest request = iter.next();
            if (request.getUser() == user) {
                if (LOG.isDebugEnabled())
                    LOG.debug("request finished for "+user);
                iter.remove();
                resumeQueued();
                return;
            }
        }
    }
    
    /**
     * Resumes an uploader from the resumable queue
     * (in this specific case a Seeding BT uploader).
     */
    private void resumeQueued() {
        // can't resume if someone is still active
        if (existActiveHigherPriority(BT_SEED))
            return;
        
        // consider moving this to an external collection
        for(Iterator<BTSlotRequest> iter = queuedResumable.iterator(); iter.hasNext() && hasFreeSlot(active.size());) {
            BTSlotRequest queuedRequest = iter.next();
            iter.remove();
            
            if (LOG.isDebugEnabled())
                LOG.debug("resuming queued request "+queuedRequest.getUser());
            
            active.add(queuedRequest);
            queuedRequest.getListener().slotAvailable();
        }
    }
    
    public synchronized int getNumActive() {
        return active.size();
    }
    
    public synchronized int getNumQueued() {
        return queued.size();
    }
    
    public synchronized int getNumQueuedResumable() {
        return queuedResumable.size();
    }
    
    public synchronized int getNumUsersForHost(String host) {
        int ret = 0;
        for(UploadSlotRequest request : allRequests) {
            if (host.equals(request.getUser().getHost()))
                ret++;
        }
        return ret;
    }
    
    @Override
    public synchronized String toString() {
        StringBuilder ret = new StringBuilder();
        ret.append("active:");
        appendPriorities(active, ret);
        ret.append("queued:");
        appendPriorities(queued, ret);
        ret.append("resumable:");
        appendPriorities(queuedResumable, ret);
        ret.append("bw now:").append(getTotalBandwidth());
        ret.append(" session avg:").append(sessionAverage);
        return ret.toString();
    }
    
    private void appendPriorities(List<? extends UploadSlotRequest> l, StringBuilder dest) {
        int [] priorities = countPriorities(l);
        for (int i = 0; i < priorities.length; i++)
            dest.append(i).append(":").append(priorities[i]).append(" ");
    }
    
    private int[] countPriorities(List<? extends UploadSlotRequest> l) {
        int [] ret = new int[3];
        for (UploadSlotRequest r : l) 
            ret[r.getPriority()]++;
        return ret;
    }
    
    /**
     * A request for an upload slot.
     */
    private abstract class UploadSlotRequest {
        final UploadSlotUser user;
        private final boolean preempt;
        private final int priority;
        
        boolean isPreemptible() {
            return preempt;
        }
        
        int getPriority() {
            return priority;
        }
        
        UploadSlotUser getUser() {
            return user;
        }
        
        abstract boolean isQueuable();
        
        protected UploadSlotRequest(UploadSlotUser listener,
                boolean preempt,
                int priority) {
            this.user = listener;
            this.preempt = preempt;
            this.priority = priority;
        }
        
        @Override
        public boolean equals(Object o) {
            if (! (o instanceof UploadSlotRequest))
                return false;
            UploadSlotRequest other = (UploadSlotRequest) o;
            
            // one request per user at a time.
            return getUser() == other.getUser();
        }
        
        @Override
        public String toString() {
            return getClass().getName() + "[user=" + user + "]";
        }

    }

    /**
     * An HTTP request for an upload slot.
     */
    private class HTTPSlotRequest extends UploadSlotRequest {
        
        private final boolean queuable;
        
        HTTPSlotRequest (UploadSlotUser user, boolean queuable, boolean highPriority) {
            super(user, false, highPriority ? HIGH : HTTP);
            this.queuable = queuable;
        }
        
        @Override
        boolean isQueuable() {
            return queuable;
        }
        
    }

    /**
     * A BT request for an upload slot.
     */
    private class BTSlotRequest extends UploadSlotRequest {
        BTSlotRequest(UploadSlotListener listener, boolean highPriority) {
            super(listener, !highPriority, highPriority ? HIGH : BT_SEED);
        }
        
        UploadSlotListener getListener() {
            return (UploadSlotListener) getUser();
        }
        
        @Override
        boolean isQueuable() {
            return getPriority() == BT_SEED;
        }
    }
    
    /**
     * An ArrayList that keeps some stats on its elements. 
     */
    private static class CountingList<E> extends ArrayList<E> {
        private final QueueCounter counter = new QueueCounter(10);
        private volatile int maxSize;
        
        @Override
        public boolean add(E e) {
            counter.recordArrival();
            maxSize = Math.max(maxSize, 1 + size());
            return super.add(e);
        }
        
        @Override
        public void add(int index, E e) {
            counter.recordArrival();
            maxSize = Math.max(maxSize, 1 + size());
            super.add(index, e);
        }
        
        @Override
        public E remove(int index) {
            counter.recordDeparture();
            return super.remove(index);
        }
        
        @Override
        public boolean remove(Object e) {
            boolean ret = super.remove(e);
            if (ret) {
                counter.recordDeparture();
            }
            return ret;
        }
    }

    public synchronized void cleanup() {
        active.clear();
        queued.clear();
        queuedResumable.clear();        
    }
}
