package com.crossbow.volley;

import android.content.Context;

import com.crossbow.volley.toolbox.BasicFileStack;

import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;

/**

 *
 * The file queue handles the creation of a queue and the dispatching of the file read/writes to
 * separate threads. This is run independently of the normal {@link com.android.volley.RequestQueue RequestQueue}
 *
 * The default thread count is 3.
 */
public class FileQueue {

    private PriorityBlockingQueue<FileRequest<?>>  requestQueue = new PriorityBlockingQueue<>();

    private FileStack fileStack;

    private List<FileRequest> inFlightRequests = new LinkedList<>();

    private AtomicInteger sequenceNumber = new AtomicInteger();

    private static final int THREAD_COUNT = 3;

    private FileDispatcher[] fileDispatchers = new FileDispatcher[THREAD_COUNT];

    private FileDelivery fileDelivery;

    private boolean running = false;

    private Map<String, LinkedList<FileRequest<?>>> blockedRequests = new HashMap<>();

    private Set<String> inUseFiles = new HashSet<>();

    /**
     * Creates a new file queue
     * @param fileDelivery the File deliver to use
     * @param fileStack the file stack to use for reading a writing
     * @param threadCount the size of the thread pool. Defaults to 3.
     */
    public FileQueue(FileDelivery fileDelivery, FileStack fileStack, int threadCount) {
        this.fileDelivery = fileDelivery;
        this.fileStack = fileStack;
        fileDispatchers = new FileDispatcher[threadCount];
    }

    /**
     * Creates a new file queue with the default thread pool size of 3
     */
    public FileQueue(FileDelivery fileDelivery, FileStack fileStack) {
        this(fileDelivery, fileStack, THREAD_COUNT);
    }

    /**
     * Creates a new file queue with the default File delivery, Reader and thread pool size.
     */
    public FileQueue(Context context) {
        this(new BasicFileDelivery(), new BasicFileStack(context), THREAD_COUNT);
    }

    /**
     * Adds a request to the queue. {@link #start()} has to be called inorder for requests to be processed
     * @param fileRequest
     */
    public void add(FileRequest fileRequest) {

        if(fileRequest == null) {
            return;
        }

        //Set the sequence number
        int sequence = sequenceNumber.incrementAndGet();
        fileRequest.sequence(sequence);

        fileRequest.setFileQueue(this);

        String filePath = fileRequest.getFilePath();

        //add to the inFlight requests
        synchronized (inFlightRequests) {
            inFlightRequests.add(fileRequest);
        }

        synchronized (inUseFiles) {
            if(inUseFiles.contains(filePath)) {

                if(blockedRequests.containsKey(filePath)) {
                    List<FileRequest<?>> list = blockedRequests.get(filePath);
                    list.add(fileRequest);
                }
                else {
                    LinkedList<FileRequest<?>> list = new LinkedList<>();
                    list.add(fileRequest);
                    blockedRequests.put(filePath, list);
                }
                fileRequest.mark("file-in-use-blocking");
            }
            else {
                inUseFiles.add(filePath);
                requestQueue.add(fileRequest);
                fileRequest.mark("add-to-queue");
            }
        }
    }

    //Visible for testing
    PriorityBlockingQueue<FileRequest<?>> getRequestQueue() {
        return requestQueue;
    }

    /**
     * Start the request queue processing requests
     */
    public void start() {
        if(running) {
            return;
        }
        running = true;

        for (int i = 0; i < fileDispatchers.length; i++) {
            fileDispatchers[i] = new FileDispatcher(requestQueue, fileStack, fileDelivery);
            fileDispatchers[i].start();
        }
    }

    /**
     * Stops the queue from processing any new requests
     */
    public void stop() {
        running = false;
        for (int i = 0; i < fileDispatchers.length; i++) {
            fileDispatchers[i] = new FileDispatcher(requestQueue, fileStack, fileDelivery);
            fileDispatchers[i].quit();
        }
    }

    /**
     * Cancels any requests that match the current request filter
     * @param filter FileRequestFilter to use to cancel the requests
     */
    public void cancelAll(FileRequestFilter filter) {
        synchronized (inFlightRequests) {
            for(FileRequest<?> request : inFlightRequests) {
                if(filter.apply(request)) request.cancel();
            }
        }
    }

    /**
     * Cancels every request  in the queue
     */
    public void cancelAll() {
        synchronized (inFlightRequests) {
            for(FileRequest<?> request : inFlightRequests) {
                request.cancel();
            }
        }
    }

    /**
     * Cancel all the requests that have a matching tag. The tag is set using {@link FileRequest#setTag(Object) FileRequest.setTag}
     * @param tag to match against
     */
    public void cancelAll(Object tag) {

        if(tag == null) {
            throw new IllegalArgumentException("Cant cancel for a null tag");
        }

        synchronized (inFlightRequests) {
            for(FileRequest<?> request : inFlightRequests) {
                if(tag.equals(request.getTag())) {
                    request.cancel();
                }
            }
        }
    }

    protected void finishRequest(FileRequest fileRequest) {
        synchronized (inFlightRequests) {
            inFlightRequests.remove(fileRequest);
        }

        synchronized (inUseFiles) {
            String filePath = fileRequest.getFilePath();
            inUseFiles.remove(filePath);
            //get any blocked requests nad add to the queue
            if(blockedRequests.containsKey(filePath)) {
                LinkedList<FileRequest<?>> blocked = blockedRequests.get(filePath);
                FileRequest<?> request = blocked.pop();
                if(!request.isCanceled()) {
                    requestQueue.add(request);
                    inUseFiles.add(filePath);
                    fileRequest.mark("request-unblocked");
                }
                else {
                    fileRequest.mark("request-unblocked-canceled");
                }


                if (blocked.isEmpty()) {
                    blockedRequests.remove(filePath);
                }
            }
        }
    }
}
