package com.limegroup.gnutella.downloader;

import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.limewire.collection.ByteArrayCache;
import org.limewire.collection.PowerOf2ByteArrayCache;
import org.limewire.concurrent.ExecutorsHelper;
import org.limewire.concurrent.ManagedThread;

import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.google.inject.name.Named;

/** Manages writing / reading from / to disk. */
@Singleton
public class DiskController {
    
    private final Log LOG = LogFactory.getLog(DiskController.class);

    /** The thread that does the actual verification & writing */
    private final ThreadPoolExecutor QUEUE = ExecutorsHelper.newSingleThreadExecutor(
            new ThreadFactory() {
                public Thread newThread(Runnable r) {
                    Thread t = new ManagedThread(r, "BlockingVF");
                    t.setDaemon(true);
                    t.setPriority(Thread.NORM_PRIORITY+1);
                    return t;
                }
            });
    
    /**
     * A list of DelayedWrites that will write when space becomes available in the cache.
     * LOCKING: Lock on the below CACHE.
     */
    private final List<DelayedWrite> DELAYED = new LinkedList<DelayedWrite>();   
    /**  A cache for byte[]s. */
    private final ByteArrayCache CACHE = new ByteArrayCache(512, HTTPDownloader.BUF_LENGTH);    
    /** a bunch of cached byte[]s for verifiable chunks */
    private final PowerOf2ByteArrayCache CHUNK_CACHE = new PowerOf2ByteArrayCache();
    /** The number of chunks scheduled to be written. */
    private int chunksScheduled = 0;
    /** A lock to use for the queue size + chunksScheduled. */
    private final Object SCHEDULE_LOCK = new Object();
    
    @Inject
    public DiskController(@Named("backgroundExecutor") ScheduledExecutorService backgroundExecutor) {
        backgroundExecutor.scheduleWithFixedDelay(new CacheCleaner(), 10 * 60 * 1000, 10 * 60 * 1000, TimeUnit.MILLISECONDS);
    }
    
    /** Adds a DelayedWrite to the queue of writers. */
    public void addDelayedWrite(DelayedWrite dw) {
        synchronized(CACHE) {
            DELAYED.add(dw);
        }
    }
    
    /** Returns true if no delayed writes are pending. */
    public boolean canWriteNow() {
        synchronized(CACHE) {
            return DELAYED.isEmpty();
        }
    }
    
    /** Returns a chunk for writing.  Will return false if no chunks are available for writing. */
    public byte[] getWriteChunk() {
        return CACHE.getQuick();
    }
    
    /** Adds a job to be performed on the disk. */
    public void addDiskJob(final ChunkDiskJob job) {
        synchronized(SCHEDULE_LOCK) {
            chunksScheduled++;
            QUEUE.execute(new Runnable() {
                public void run() {
                    try {
                        job.runChunkJob(job.getChunk());
                    } finally {
                        synchronized(SCHEDULE_LOCK) {
                            chunksScheduled--;
                        }
                        
                        releaseChunk(job.getChunk(), true);
                        
                        job.finish();
                    }
                }
            });
        }
    }
    
    /** Adds a job to be performed on the disk that doesn't involve chunks. */
    public void addDiskJobWithoutChunk(Runnable job) {
        QUEUE.execute(job);
    }
    
    /** Gets a byte[] to the closest power of 2. */
    public byte[] getPowerOf2Chunk(int size) {
        return CHUNK_CACHE.get(size);
    }
    
    /**
     * A Runnable that clears the cache used for storing byte[]s used for
     * writing data read from network to disk, and schedules a ChunkCacheCleaner.
     */
    private class CacheCleaner implements Runnable {
        public void run() {
            LOG.info("clearing cache");
            CACHE.clear();
            QUEUE.execute(new ChunkCacheCleaner());
        }
    }
    
    /** A Runnable that clears the cache storing byte[]s used for verifying. */
    private class ChunkCacheCleaner implements Runnable {
        public void run() {
            CHUNK_CACHE.clear();
        }
    }

    private void releaseChunk(byte[] buf, boolean runDelayed) {
        CACHE.release(buf);
        if(runDelayed)
            runDelayedWrites();
    }

    private void runDelayedWrites() {
        synchronized(SCHEDULE_LOCK) {
            if(chunksScheduled > 0)
                return;
        }
        
        while(CACHE.isBufferAvailable()) {
            DelayedWrite dw;
            
            synchronized(CACHE) {
                if(DELAYED.isEmpty()) {
                    LOG.debug("Nothing delayed to run.");
                    return;
                }
                dw = DELAYED.get(0);
            }
    
            // write & notify outside of lock
            if(dw.write()) {
                // if we wrote succesfully, remove the item from the cache.
                synchronized(CACHE) {
                    DELAYED.remove(0);
                }
            } else {
                // otherwise, something went wrong, so reschedule another
                // delayed write later on.
                // NOTE: this should be impossible to happen, but it's happening,
                //       and its no huge deal, so we're preparing for it.
                QUEUE.execute(new Runnable() {
                    public void run() {
                        runDelayedWrites();
                    }
                });
            }
        }
    }

    /** Cleans the caches. */
    public void clearCaches() {
        Runnable runner = new CacheCleaner();
        runner.run();
    }

    /** Returns the number of bytes cached in the byte cache. */
    public int getSizeOfByteCache() {
        return CACHE.getCacheSize();
    }

    /** Returns the number of bytes cached in the verifying cache. */
    public int getSizeOfVerifyingCache() {
        return CHUNK_CACHE.getCacheSize();
    }

    public int getNumPendingItems() {
        return QUEUE.getQueue().size();
    }

}
