/*
 * Copyright (C) 2014 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.tv.settings.widget;

import android.app.ActivityManager;
import android.content.Context;
import android.graphics.Bitmap;
import android.util.Log;
import android.util.LruCache;
import android.widget.ImageView;

import com.android.tv.settings.util.AccountImageChangeObserver;
import com.android.tv.settings.util.UriUtils;
import com.android.tv.settings.R;

import java.lang.ref.SoftReference;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

/**
 * Downloader class which loads a resource URI into an image view.
 * <p>
 * This class adds a cache over BitmapWorkerTask.
 */
public class BitmapDownloader {

    private static final String TAG = "BitmapDownloader";

    private static final boolean DEBUG = false;

    private static final int CORE_POOL_SIZE = 5;

    private static final Executor BITMAP_DOWNLOADER_THREAD_POOL_EXECUTOR =
            Executors.newFixedThreadPool(CORE_POOL_SIZE);

    // 1/4 of max memory is used for bitmap mem cache
    private static final int MEM_TO_CACHE = 4;

    // hard limit for bitmap mem cache in MB
    private static final int CACHE_HARD_LIMIT = 32;

    /**
     * bitmap cache item structure saved in LruCache
     */
    private static class BitmapItem {
        /**
         * cached bitmap
         */
        Bitmap mBitmap;
        /**
         * indicate if the bitmap is scaled down from original source (never scale up)
         */
        boolean mScaled;

        public BitmapItem(Bitmap bitmap, boolean scaled) {
            mBitmap = bitmap;
            mScaled = scaled;
        }
    }

    private LruCache<String, BitmapItem> mMemoryCache;

    private static BitmapDownloader sBitmapDownloader;

    private static final Object sBitmapDownloaderLock = new Object();

    // Bitmap cache also uses size of Bitmap as part of key.
    // Bitmap cache is divided into following buckets by height:
    // TODO: we currently care more about height, what about width in key?
    // height <= 128, 128 < height <= 512, height > 512
    // Different bitmap cache buckets save different bitmap cache items.
    // Bitmaps within same bucket share the largest cache item.
    private static final int[] SIZE_BUCKET = new int[]{128, 512, Integer.MAX_VALUE};

    public static abstract class BitmapCallback {
        SoftReference<BitmapWorkerTask> mTask;

        public abstract void onBitmapRetrieved(Bitmap bitmap);
    }

    /**
     * get the singleton BitmapDownloader for the application
     */
    public final static BitmapDownloader getInstance(Context context) {
        if (sBitmapDownloader == null) {
            synchronized(sBitmapDownloaderLock) {
                if (sBitmapDownloader == null) {
                    sBitmapDownloader = new BitmapDownloader(context);
                }
            }
        }
        return sBitmapDownloader;
    }

    public BitmapDownloader(Context context) {
        int memClass = ((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE))
                .getMemoryClass();
        memClass = memClass / MEM_TO_CACHE;
        if (memClass > CACHE_HARD_LIMIT) {
            memClass = CACHE_HARD_LIMIT;
        }
        int cacheSize = 1024 * 1024 * memClass;
        mMemoryCache = new LruCache<String, BitmapItem>(cacheSize) {
            @Override
            protected int sizeOf(String key, BitmapItem bitmap) {
                return bitmap.mBitmap.getByteCount();
            }
        };
    }

    /**
     * load bitmap in current thread, will *block* current thread.
     * FIXME: Should avoid using this function at all cost.
     * @deprecated
     */
    @Deprecated
    public final Bitmap loadBitmapBlocking(BitmapWorkerOptions options) {
        final boolean hasAccountImageUri = UriUtils.isAccountImageUri(options.getResourceUri());
        Bitmap bitmap = null;
        if (hasAccountImageUri) {
            AccountImageChangeObserver.getInstance().registerChangeUriIfPresent(options);
        } else {
            bitmap = getBitmapFromMemCache(options);
        }

        if (bitmap == null) {
            BitmapWorkerTask task = new BitmapWorkerTask(null) {
                @Override
                protected Bitmap doInBackground(BitmapWorkerOptions... params) {
                    final Bitmap bitmap = super.doInBackground(params);
                    if (bitmap != null && !hasAccountImageUri) {
                        addBitmapToMemoryCache(params[0], bitmap, isScaled());
                    }
                    return bitmap;
                }
            };

            return task.doInBackground(options);
        }
        return bitmap;
    }

    /**
     * Loads the bitmap into the image view.
     */
    public void loadBitmap(BitmapWorkerOptions options, final ImageView imageView) {
        cancelDownload(imageView);
        final boolean hasAccountImageUri = UriUtils.isAccountImageUri(options.getResourceUri());
        Bitmap bitmap = null;
        if (hasAccountImageUri) {
            AccountImageChangeObserver.getInstance().registerChangeUriIfPresent(options);
        } else {
            bitmap = getBitmapFromMemCache(options);
        }

        if (bitmap != null) {
            imageView.setImageBitmap(bitmap);
        } else {
            BitmapWorkerTask task = new BitmapWorkerTask(imageView) {
                @Override
                protected Bitmap doInBackground(BitmapWorkerOptions... params) {
                    Bitmap bitmap = super.doInBackground(params);
                    if (bitmap != null && !hasAccountImageUri) {
                        addBitmapToMemoryCache(params[0], bitmap, isScaled());
                    }
                    return bitmap;
                }
            };
            imageView.setTag(R.id.imageDownloadTask, new SoftReference<BitmapWorkerTask>(task));
            task.execute(options);
        }
    }

    /**
     * Loads the bitmap.
     * <p>
     * This will be sent back to the callback object.
     */
    public void getBitmap(BitmapWorkerOptions options, final BitmapCallback callback) {
        cancelDownload(callback);
        final boolean hasAccountImageUri = UriUtils.isAccountImageUri(options.getResourceUri());
        final Bitmap bitmap = hasAccountImageUri ? null : getBitmapFromMemCache(options);
        if (hasAccountImageUri) {
            AccountImageChangeObserver.getInstance().registerChangeUriIfPresent(options);
        }

        BitmapWorkerTask task = new BitmapWorkerTask(null) {
            @Override
            protected Bitmap doInBackground(BitmapWorkerOptions... params) {
                if (bitmap != null) {
                    return bitmap;
                }
                final Bitmap bitmap = super.doInBackground(params);
                if (bitmap != null && !hasAccountImageUri) {
                    addBitmapToMemoryCache(params[0], bitmap, isScaled());
                }
                return bitmap;
            }

            @Override
            protected void onPostExecute(Bitmap bitmap) {
                callback.onBitmapRetrieved(bitmap);
            }
        };
        callback.mTask = new SoftReference<BitmapWorkerTask>(task);
        task.executeOnExecutor(BITMAP_DOWNLOADER_THREAD_POOL_EXECUTOR, options);
    }

    /**
     * Cancel download<p>
     * @param key {@link BitmapCallback} or {@link ImageView}
     */
    public boolean cancelDownload(Object key) {
        BitmapWorkerTask task = null;
        if (key instanceof ImageView) {
            ImageView imageView = (ImageView)key;
            SoftReference<BitmapWorkerTask> softReference =
                    (SoftReference<BitmapWorkerTask>) imageView.getTag(R.id.imageDownloadTask);
            if (softReference != null) {
                task = softReference.get();
                softReference.clear();
            }
        } else if (key instanceof BitmapCallback) {
            BitmapCallback callback = (BitmapCallback)key;
            if (callback.mTask != null) {
                task = callback.mTask.get();
                callback.mTask = null;
            }
        }
        if (task != null) {
            return task.cancel(true);
        }
        return false;
    }

    private static String getBucketKey(String baseKey, Bitmap.Config bitmapConfig, int width) {
        for (int i = 0; i < SIZE_BUCKET.length; i++) {
            if (width <= SIZE_BUCKET[i]) {
                return new StringBuilder(baseKey.length() + 16).append(baseKey)
                        .append(":").append(bitmapConfig == null ? "" : bitmapConfig.ordinal())
                        .append(":").append(SIZE_BUCKET[i]).toString();
            }
        }
        // should never happen because last bucket is Integer.MAX_VALUE
        throw new RuntimeException();
    }

    private void addBitmapToMemoryCache(BitmapWorkerOptions key, Bitmap bitmap, boolean isScaled) {
        if (!key.isMemCacheEnabled()) {
            return;
        }
        String bucketKey = getBucketKey(
                key.getCacheKey(), key.getBitmapConfig(), bitmap.getHeight());
        BitmapItem bitmapItem = mMemoryCache.get(bucketKey);
        if (bitmapItem != null) {
            Bitmap currentBitmap = bitmapItem.mBitmap;
            // If somebody else happened to get a larger one in the bucket, discard our bitmap.
            // TODO: need a better way to prevent current downloading for the same Bitmap
            if (currentBitmap.getWidth() >= bitmap.getWidth() && currentBitmap.getHeight()
                    >= bitmap.getHeight()) {
                return;
            }
        }
        if (DEBUG) {
            Log.d(TAG, "add cache "+bucketKey+" isScaled = "+isScaled);
        }
        bitmapItem = new BitmapItem(bitmap, isScaled);
        mMemoryCache.put(bucketKey, bitmapItem);
    }

    private Bitmap getBitmapFromMemCache(BitmapWorkerOptions key) {
        if (key.getHeight() != BitmapWorkerOptions.MAX_IMAGE_DIMENSION_PX) {
            // 1. find the bitmap in the size bucket
            String bucketKey =
                    getBucketKey(key.getCacheKey(), key.getBitmapConfig(), key.getHeight());
            BitmapItem bitmapItem = mMemoryCache.get(bucketKey);
            if (bitmapItem != null) {
                Bitmap bitmap = bitmapItem.mBitmap;
                // now we have the bitmap in the bucket, use it when the bitmap is not scaled or
                // if the size is larger than or equals to the output size
                if (!bitmapItem.mScaled) {
                    return bitmap;
                }
                if (bitmap.getHeight() >= key.getHeight()) {
                    return bitmap;
                }
            }
            // 2. find un-scaled bitmap in smaller buckets.  If the un-scaled bitmap exists
            // in higher buckets,  we still need to scale it down.  Right now we just
            // return null and let the BitmapWorkerTask to do the same job again.
            // TODO: use the existing unscaled bitmap and we don't need to load it from resource
            // or network again.
            for (int i = SIZE_BUCKET.length - 1; i >= 0; i--) {
                if (SIZE_BUCKET[i] >= key.getHeight()) {
                    continue;
                }
                bucketKey = getBucketKey(key.getCacheKey(), key.getBitmapConfig(), SIZE_BUCKET[i]);
                bitmapItem = mMemoryCache.get(bucketKey);
                if (bitmapItem != null && !bitmapItem.mScaled) {
                    return bitmapItem.mBitmap;
                }
            }
            return null;
        }
        // 3. find un-scaled bitmap if size is not specified
        for (int i = SIZE_BUCKET.length - 1; i >= 0; i--) {
            String bucketKey =
                    getBucketKey(key.getCacheKey(), key.getBitmapConfig(), SIZE_BUCKET[i]);
            BitmapItem bitmapItem = mMemoryCache.get(bucketKey);
            if (bitmapItem != null && !bitmapItem.mScaled) {
                return bitmapItem.mBitmap;
            }
        }
        return null;
    }

    public Bitmap getLargestBitmapFromMemCache(BitmapWorkerOptions key) {
        // find largest bitmap matching the key
        for (int i = SIZE_BUCKET.length - 1; i >= 0; i--) {
            String bucketKey =
                    getBucketKey(key.getCacheKey(), key.getBitmapConfig(), SIZE_BUCKET[i]);
            BitmapItem bitmapItem = mMemoryCache.get(bucketKey);
            if (bitmapItem != null) {
                return bitmapItem.mBitmap;
            }
        }
        return null;
    }
}
