package vandy.mooc.utils.loader;

import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.locks.ReentrantLock;

import vandy.mooc.common.GenericAsyncTask;
import vandy.mooc.common.GenericAsyncTaskOps;
import vandy.mooc.utils.BitmapUtils;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.util.Log;
import android.util.LruCache;
import android.widget.ImageView;

/**
 * This class loads and displays images in the background. It maintains 
 * a cache of bitmaps to make displaying the same image multiple times
 * more efficient. 
 */
public class ImageLoader 
    implements GenericAsyncTaskOps<ImageLoaderWorkOrder,
                                   Void,
                                   ImageLoaderWorkResult>{
    /**
     * Logcat Tag
     */
    public static final String TAG =
        ImageLoader.class.getCanonicalName();

    /**
     * Executor used to load the images from disk in the background.
     */
    private final Executor mDisplayThreadPoolExecutor =
        ImageLoaderThreadPool.MY_THREAD_POOL_EXECUTOR;

    /**
     * Map of ImageView's (using their hash codes) to the filepath of 
     * the image they are currently displaying. This is used to ensure 
     * that the ImageView is displaying the image that is being loaded 
     * for it. 
     */
    private final Map<Integer, String> mCacheKeysForImageView =
        new ConcurrentHashMap<>();

    /**
     * Map storing ReentrantLocks for each file.
     */
    private final Map<String, ReentrantLock> fileLocks =
        new WeakHashMap<String, ReentrantLock>();

    /**
     *  Drawable that is displayed while the image is loading
     */
    private final Drawable mLoadingDrawable;

    /**
     * Cache storing the bitmaps in memory.
     */
    private LruCache<String, Bitmap> mBitmapCache;

    /**
     * Constructor initializes the fields.
     */
    public ImageLoader(Drawable loadingDrawable) {
        mLoadingDrawable = loadingDrawable;

        initCache();
    }

    /**
     * Initialize the image cache.
     */
    private void initCache() {
        // Maximum memory allowed for this application in Mb.
        final int maxMemory =
            (int) (Runtime.getRuntime().maxMemory() / 1024);

        // Maximum size of the cache - set to 1/4th of the memory
        // allowed for this application.
        final int cacheSize = maxMemory / 4;

        // Create the cache using the cache size above
        mBitmapCache = new LruCache<String, Bitmap>(cacheSize) {
            // Returns the size of an item in the cache.
            @Override
            protected int sizeOf(String key,
                                 Bitmap value) {
                return value.getAllocationByteCount() / 1024;
            }
        };
    }
    
    /**
     * Adds the bitmap to the image cache.
     */
    private void addBitmapToCache(String filename,
                                 Bitmap bitmap) {
        Log.d(TAG, "added BM to cache for position: " + filename);
        mBitmapCache.put(filename,
                         bitmap);
    }
    
    /**
     * Returns the ReentrantLock associated with the
     * file path parameter.
     */
    private ReentrantLock getFileLock(String filepath) {
        ReentrantLock lock =
            fileLocks.get(filepath);

        if (lock == null) {
            lock = new ReentrantLock();
            fileLocks.put(filepath, 
                          lock);
        }
        return lock;
    }
    
    /**
     * Sets the ImageView parameter to the bitmap at the filepath
     * parameter by loading it in a background thread. A cache is
     * maintained to make loading bitmaps multiple times more
     * efficient.
     */
    public void loadAndDisplayImage(ImageView view,
                String imageFilePath,
                int colWidth) {
        // Store the holder and its current filepath in the cache
        mCacheKeysForImageView.put(view.hashCode(),
                                   imageFilePath);

        Bitmap cachedBitmap =
            mBitmapCache.get(imageFilePath);

        if (cachedBitmap != null)
            // If the bitmap is in the cache, simply place it in the
            // ImageView.
            view.setImageBitmap(cachedBitmap);
        else {
            // If the bitmap isn't in the cache, set the ImageView to
            // display the loading drawable
            view.setImageDrawable(mLoadingDrawable);

            // Create the work order that contains the info
            // needed to load and display the image.
            ImageLoaderWorkOrder wo = 
                new ImageLoaderWorkOrder(new ImageViewHolder(view), 
                                         imageFilePath, 
                                         colWidth,
                                         colWidth);
            
            // Create a new GenericAsyncTask
            GenericAsyncTask<ImageLoaderWorkOrder, 
                             Void, 
                             ImageLoaderWorkResult, 
                             ImageLoader> imageLoaderTask = 
                new GenericAsyncTask<> (this);
            
            // Use the task to load the image in the background.
            imageLoaderTask
                .executeOnExecutor(mDisplayThreadPoolExecutor,
                                   wo);
        }
    }

    /**
     * Checks if the ImageView wrapped by the ImageViewHolder has
     * been garbage collected or reused to display a different
     * image. To ensure that the wrapped view is not GC'd while
     * this check is being performed, we first need to grab a
     * reference to that view.
     */
    private void checkImageView(ImageViewHolder imgView,
                                String filepath)
            throws ViewChangedException {
        // Grab a reference to the wrapped view so that
        // it won't be GC'd between the two checks below.
        ImageView view = imgView.getWrappedImageView();
        checkViewCollected(imgView);
        checkViewReused(imgView,
                        filepath);
    }

    /**
     * Checks if the view has been collected by 
     * the garbage collector
     */
    private void checkViewCollected(ImageViewHolder imgView)
        throws ViewChangedException {
            if (imgView.isCollected())
                throw new ViewChangedException();
    }

    /**
     * Checks if the view has been reused to display 
     * a different image.
     */
    private void checkViewReused(ImageViewHolder imgView,
                                 String filepath)
        throws ViewChangedException {
            if (isViewReused(imgView, filepath))
                throw new ViewChangedException();
    }

    /**
     * Checks if the view has been reused without
     * throwing an exception. If the wrapped image
     * view has been garbage collected, this method
     * will return true.
     */
    private boolean isViewReused(ImageViewHolder imgView,
                                 String filepath) {
        // Get a reference to the wrapped view to prevent
        // garbage collection.
        ImageView view = imgView.getWrappedImageView();
        if (view == null) {
            // Must have been collected.
            return true;
        }

        final String currCachedKey =
                mCacheKeysForImageView.get(view.hashCode());

        return currCachedKey != filepath;
    }

    /**
     * The Exception that is used to cancel the 
     * loading if the view has been collected or
     * reused.
     */
    @SuppressWarnings("serial")
    class ViewChangedException extends Exception {}
    
    /**
     * Hook method called by the GenericAsyncTask framework 
     * to perform the background processing. It loads the image
     * from disk while periodically checking if the view has been 
     * reused. 
     */
    @Override
    public ImageLoaderWorkResult doInBackground(ImageLoaderWorkOrder... param) {
        
        ImageLoaderWorkOrder wo = param[0];

        // Obtain a lock on this image file.
        ReentrantLock lock = getFileLock(wo.getmFilePath());
        lock.lock();

        Bitmap result = null;
        
        try {
            
            // Retrieve data from the work order
            ImageViewHolder holder = wo.getmImageViewHolder();
            String filepath = wo.getmFilePath();
            int width = wo.getmTargetWidth();
            int height = wo.getmTargetHeight();
            
            // Check that the view is still valid
            checkImageView(holder, filepath);
            
            // Load the bitmap
            result = 
                BitmapUtils
                    .decodeSampledBitmapFromFile(filepath,
                                                 width,
                                                 height);
            
            // re-check the view's validity
            checkImageView(holder, filepath);
            
        } catch (ViewChangedException e) {
            // Caught if the view is no longer valid. 
            // Halt loading the image.
            return null;
        } finally {
            lock.unlock();
        }

        return new ImageLoaderWorkResult
                    (wo.getmImageViewHolder(), 
                     wo.getmFilePath(), 
                     result);
    }
    
    /**
     * Hook method called by the GenericAsyncTask framework when the 
     * background processing has completed. This will check that the 
     * ImageView is still set to display the loaded bitmap, and display 
     * it if so. 
     */
    @Override
    public void onPostExecute(ImageLoaderWorkResult result) {

        if (result != null) {
            ImageViewHolder holder = result.getmImageViewHolder();
            String filepath = result.getmFilePath();

            // Check that the ImageView is still valid
            if (!holder.isCollected()
                    && !isViewReused(holder, filepath)) {
                addBitmapToCache(filepath,
                        result.getmBitmap());

                // Display the loaded bitmap
                holder.getWrappedImageView()
                        .setImageBitmap(result.getmBitmap());
            }
        }
    }
}
