/*
 * Copyright (c) 2013. wyouflf ([email protected])
 *
 * 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.lidroid.xutils.bitmap.core;

import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.media.ExifInterface;
import com.lidroid.xutils.BitmapUtils;
import com.lidroid.xutils.bitmap.BitmapDisplayConfig;
import com.lidroid.xutils.bitmap.BitmapGlobalConfig;
import com.lidroid.xutils.bitmap.factory.BitmapFactory;
import com.lidroid.xutils.cache.FileNameGenerator;
import com.lidroid.xutils.cache.LruDiskCache;
import com.lidroid.xutils.cache.LruMemoryCache;
import com.lidroid.xutils.util.IOUtils;
import com.lidroid.xutils.util.LogUtils;
import com.lidroid.xutils.util.OtherUtils;

import java.io.*;


public class BitmapCache {

    private final int DISK_CACHE_INDEX = 0;

    private LruDiskCache mDiskLruCache;
    private LruMemoryCache<MemoryCacheKey, Bitmap> mMemoryCache;

    private final Object mDiskCacheLock = new Object();

    private BitmapGlobalConfig globalConfig;

    /**
     * Creating a new ImageCache object using the specified parameters.
     *
     * @param globalConfig The cache parameters to use to initialize the cache
     */
    public BitmapCache(BitmapGlobalConfig globalConfig) {
        if (globalConfig == null) throw new IllegalArgumentException("globalConfig may not be null");
        this.globalConfig = globalConfig;
    }


    /**
     * Initialize the memory cache
     */
    public void initMemoryCache() {
        if (!globalConfig.isMemoryCacheEnabled()) return;

        // Set up memory cache
        if (mMemoryCache != null) {
            try {
                clearMemoryCache();
            } catch (Throwable e) {
            }
        }
        mMemoryCache = new LruMemoryCache<MemoryCacheKey, Bitmap>(globalConfig.getMemoryCacheSize()) {
            /**
             * Measure item size in bytes rather than units which is more practical
             * for a bitmap cache
             */
            @Override
            protected int sizeOf(MemoryCacheKey key, Bitmap bitmap) {
                if (bitmap == null) return 0;
                return bitmap.getRowBytes() * bitmap.getHeight();
            }
        };
    }

    /**
     * Initializes the disk cache.  Note that this includes disk access so this should not be
     * executed on the main/UI thread. By default an ImageCache does not initialize the disk
     * cache when it is created, instead you should call initDiskCache() to initialize it on a
     * background thread.
     */
    public void initDiskCache() {
        // Set up disk cache
        synchronized (mDiskCacheLock) {
            if (globalConfig.isDiskCacheEnabled() && (mDiskLruCache == null || mDiskLruCache.isClosed())) {
                File diskCacheDir = new File(globalConfig.getDiskCachePath());
                if (diskCacheDir.exists() || diskCacheDir.mkdirs()) {
                    long availableSpace = OtherUtils.getAvailableSpace(diskCacheDir);
                    long diskCacheSize = globalConfig.getDiskCacheSize();
                    diskCacheSize = availableSpace > diskCacheSize ? diskCacheSize : availableSpace;
                    try {
                        mDiskLruCache = LruDiskCache.open(diskCacheDir, 1, 1, diskCacheSize);
                        mDiskLruCache.setFileNameGenerator(globalConfig.getFileNameGenerator());
                        LogUtils.d("create disk cache success");
                    } catch (Throwable e) {
                        mDiskLruCache = null;
                        LogUtils.e("create disk cache error", e);
                    }
                }
            }
        }
    }

    public void setMemoryCacheSize(int maxSize) {
        if (mMemoryCache != null) {
            mMemoryCache.setMaxSize(maxSize);
        }
    }

    public void setDiskCacheSize(int maxSize) {
        synchronized (mDiskCacheLock) {
            if (mDiskLruCache != null) {
                mDiskLruCache.setMaxSize(maxSize);
            }
        }
    }

    public void setDiskCacheFileNameGenerator(FileNameGenerator fileNameGenerator) {
        synchronized (mDiskCacheLock) {
            if (mDiskLruCache != null && fileNameGenerator != null) {
                mDiskLruCache.setFileNameGenerator(fileNameGenerator);
            }
        }
    }

    /**
     * 下载图片
     * @param uri
     * @param config
     * @param task
     * @return
     */
    public Bitmap downloadBitmap(String uri, BitmapDisplayConfig config, final BitmapUtils.BitmapLoadTask<?> task) {

        BitmapMeta bitmapMeta = new BitmapMeta();

        OutputStream outputStream = null;
        LruDiskCache.Snapshot snapshot = null;

        try {
            Bitmap bitmap = null;

            // try download to disk
            if (globalConfig.isDiskCacheEnabled()) {
                if (mDiskLruCache == null) {
                    initDiskCache();
                }

                if (mDiskLruCache != null) {
                    try {
                        snapshot = mDiskLruCache.get(uri);
                        if (snapshot == null) {
                        	//先将数据写入闪存中
                            LruDiskCache.Editor editor = mDiskLruCache.edit(uri);
                            if (editor != null) {
                                outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
                                bitmapMeta.expiryTimestamp = globalConfig.getDownloader().downloadToStream(uri, outputStream, task);
                                if (bitmapMeta.expiryTimestamp < 0) {
                                    editor.abort();
                                    return null;
                                } else {
                                    editor.setEntryExpiryTimestamp(bitmapMeta.expiryTimestamp);
                                    editor.commit();
                                }
                                //存闪存中读出
                                snapshot = mDiskLruCache.get(uri);
                            }
                        }
                        if (snapshot != null) {
                            bitmapMeta.inputStream = snapshot.getInputStream(DISK_CACHE_INDEX);
                            bitmap = decodeBitmapMeta(bitmapMeta, config);
                            if (bitmap == null) {
                                bitmapMeta.inputStream = null;
                                mDiskLruCache.remove(uri);
                            }
                        }
                    } catch (Throwable e) {
                        LogUtils.e(e.getMessage(), e);
                    }
                }
            }

            // try download to memory stream
            if (bitmap == null) {
                outputStream = new ByteArrayOutputStream();
                bitmapMeta.expiryTimestamp = globalConfig.getDownloader().downloadToStream(uri, outputStream, task);
                if (bitmapMeta.expiryTimestamp < 0) {
                    return null;
                } else {
                    bitmapMeta.data = ((ByteArrayOutputStream) outputStream).toByteArray();
                    bitmap = decodeBitmapMeta(bitmapMeta, config);
                }
            }

            if (bitmap != null) {
                bitmap = rotateBitmapIfNeeded(uri, config, bitmap);
                //添加到运行内存的缓存中
                bitmap = addBitmapToMemoryCache(uri, config, bitmap, bitmapMeta.expiryTimestamp);
            }
            return bitmap;
        } catch (Throwable e) {
            LogUtils.e(e.getMessage(), e);
        } finally {
            IOUtils.closeQuietly(outputStream);
            IOUtils.closeQuietly(snapshot);
        }

        return null;
    }

    private Bitmap addBitmapToMemoryCache(String uri, BitmapDisplayConfig config, Bitmap bitmap, long expiryTimestamp) throws IOException {
        if (config != null) {
            BitmapFactory bitmapFactory = config.getBitmapFactory();
            if (bitmapFactory != null) {
                bitmap = bitmapFactory.cloneNew().createBitmap(bitmap);
            }
        }
        if (uri != null && bitmap != null && globalConfig.isMemoryCacheEnabled() && mMemoryCache != null) {
            MemoryCacheKey key = new MemoryCacheKey(uri, config);
            mMemoryCache.put(key, bitmap, expiryTimestamp);
        }
        return bitmap;
    }

    /**
     * Get the bitmap from memory cache.
     *
     * @param uri    Unique identifier for which item to get
     * @param config
     * @return The bitmap if found in cache, null otherwise
     */
    public Bitmap getBitmapFromMemCache(String uri, BitmapDisplayConfig config) {
        if (mMemoryCache != null && globalConfig.isMemoryCacheEnabled()) {
            MemoryCacheKey key = new MemoryCacheKey(uri, config);
            return mMemoryCache.get(key);
        }
        return null;
    }

    /**
     * Get the bitmap file from disk cache.
     *
     * @param uri Unique identifier for which item to get
     * @return The file if found in cache.
     */
    public File getBitmapFileFromDiskCache(String uri) {
        synchronized (mDiskCacheLock) {
            if (mDiskLruCache != null) {
                return mDiskLruCache.getCacheFile(uri, DISK_CACHE_INDEX);
            } else {
                return null;
            }
        }
    }

    /**
     * Get the bitmap from disk cache.
     *
     * @param uri
     * @param config
     * @return
     */
    public Bitmap getBitmapFromDiskCache(String uri, BitmapDisplayConfig config) {
        if (uri == null || !globalConfig.isDiskCacheEnabled()) return null;
        if (mDiskLruCache == null) {
            initDiskCache();
        }
        if (mDiskLruCache != null) {
            LruDiskCache.Snapshot snapshot = null;
            try {
                snapshot = mDiskLruCache.get(uri);
                if (snapshot != null) {
                    Bitmap bitmap = null;
                    if (config == null || config.isShowOriginal()) {
                        bitmap = BitmapDecoder.decodeFileDescriptor(
                                snapshot.getInputStream(DISK_CACHE_INDEX).getFD());
                    } else {
                        bitmap = BitmapDecoder.decodeSampledBitmapFromDescriptor(
                                snapshot.getInputStream(DISK_CACHE_INDEX).getFD(),
                                config.getBitmapMaxSize(),
                                config.getBitmapConfig());
                    }

                    bitmap = rotateBitmapIfNeeded(uri, config, bitmap);
                    bitmap = addBitmapToMemoryCache(uri, config, bitmap, mDiskLruCache.getExpiryTimestamp(uri));
                    return bitmap;
                }
            } catch (Throwable e) {
                LogUtils.e(e.getMessage(), e);
            } finally {
                IOUtils.closeQuietly(snapshot);
            }
        }
        return null;
    }

    /**
     * Clears both the memory and disk cache associated with this ImageCache object. Note that
     * this includes disk access so this should not be executed on the main/UI thread.
     */
    public void clearCache() {
        clearMemoryCache();
        clearDiskCache();
    }

    public void clearMemoryCache() {
        if (mMemoryCache != null) {
            mMemoryCache.evictAll();
        }
    }

    public void clearDiskCache() {
        synchronized (mDiskCacheLock) {
            if (mDiskLruCache != null && !mDiskLruCache.isClosed()) {
                try {
                    mDiskLruCache.delete();
                    mDiskLruCache.close();
                } catch (Throwable e) {
                    LogUtils.e(e.getMessage(), e);
                }
                mDiskLruCache = null;
            }
        }
        initDiskCache();
    }


    public void clearCache(String uri) {
        clearMemoryCache(uri);
        clearDiskCache(uri);
    }

    public void clearMemoryCache(String uri) {
        MemoryCacheKey key = new MemoryCacheKey(uri, null);
        if (mMemoryCache != null) {
            while (mMemoryCache.containsKey(key)) {
                mMemoryCache.remove(key);
            }
        }
    }

    public void clearDiskCache(String uri) {
        synchronized (mDiskCacheLock) {
            if (mDiskLruCache != null && !mDiskLruCache.isClosed()) {
                try {
                    mDiskLruCache.remove(uri);
                } catch (Throwable e) {
                    LogUtils.e(e.getMessage(), e);
                }
            }
        }
    }

    /**
     * Flushes the disk cache associated with this ImageCache object. Note that this includes
     * disk access so this should not be executed on the main/UI thread.
     */
    public void flush() {
        synchronized (mDiskCacheLock) {
            if (mDiskLruCache != null) {
                try {
                    mDiskLruCache.flush();
                } catch (Throwable e) {
                    LogUtils.e(e.getMessage(), e);
                }
            }
        }
    }

    /**
     * Closes the disk cache associated with this ImageCache object. Note that this includes
     * disk access so this should not be executed on the main/UI thread.
     */
    public void close() {
        synchronized (mDiskCacheLock) {
            if (mDiskLruCache != null) {
                try {
                    if (!mDiskLruCache.isClosed()) {
                        mDiskLruCache.close();
                    }
                } catch (Throwable e) {
                    LogUtils.e(e.getMessage(), e);
                }
                mDiskLruCache = null;
            }
        }
    }

    /**
     * @author Caij
     * 存放图片数据的包装对象
     */
    private class BitmapMeta {
        public FileInputStream inputStream;
        public byte[] data;
        public long expiryTimestamp;
    }

    /**
     * 流转化为bitmap, 而且根据config压缩图片
     * @param bitmapMeta
     * @param config
     * @return
     * @throws IOException
     */
    private Bitmap decodeBitmapMeta(BitmapMeta bitmapMeta, BitmapDisplayConfig config) throws IOException {
        if (bitmapMeta == null) return null;
        Bitmap bitmap = null;
        if (bitmapMeta.inputStream != null) {
        	//BitmapFromDescriptor这一块使用的是这个方法而不是直接用流, 在网上找了一些
        	//资料, 这种转化能避免一些莫名其妙的图片加载不出来的问题
            if (config == null || config.isShowOriginal()) {
            	//原图
                bitmap = BitmapDecoder.decodeFileDescriptor(bitmapMeta.inputStream.getFD());
            } else {
            	//压缩图
                bitmap = BitmapDecoder.decodeSampledBitmapFromDescriptor(
                        bitmapMeta.inputStream.getFD(),
                        config.getBitmapMaxSize(),
                        config.getBitmapConfig());
            }
        } else if (bitmapMeta.data != null) {
            if (config == null || config.isShowOriginal()) {
                bitmap = BitmapDecoder.decodeByteArray(bitmapMeta.data);
            } else {
                bitmap = BitmapDecoder.decodeSampledBitmapFromByteArray(
                        bitmapMeta.data,
                        config.getBitmapMaxSize(),
                        config.getBitmapConfig());
            }
        }
        return bitmap;
    }

    private synchronized Bitmap rotateBitmapIfNeeded(String uri, BitmapDisplayConfig config, Bitmap bitmap) {
        Bitmap result = bitmap;
        if (config != null && config.isAutoRotation()) {
            File bitmapFile = this.getBitmapFileFromDiskCache(uri);
            if (bitmapFile != null && bitmapFile.exists()) {
                ExifInterface exif = null;
                try {
                    exif = new ExifInterface(bitmapFile.getPath());
                } catch (Throwable e) {
                    return result;
                }
                int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED);
                int angle = 0;
                switch (orientation) {
                    case ExifInterface.ORIENTATION_ROTATE_90:
                        angle = 90;
                        break;
                    case ExifInterface.ORIENTATION_ROTATE_180:
                        angle = 180;
                        break;
                    case ExifInterface.ORIENTATION_ROTATE_270:
                        angle = 270;
                        break;
                    default:
                        angle = 0;
                        break;
                }
                if (angle != 0) {
                    Matrix m = new Matrix();
                    m.postRotate(angle);
                    result = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), m, true);
                    bitmap.recycle();
                    bitmap = null;
                }
            }
        }
        return result;
    }

    public class MemoryCacheKey {
        private String uri;
        private String subKey;

        private MemoryCacheKey(String uri, BitmapDisplayConfig config) {
            this.uri = uri;
            this.subKey = config == null ? null : config.toString();
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof MemoryCacheKey)) return false;

            MemoryCacheKey that = (MemoryCacheKey) o;

            if (!uri.equals(that.uri)) return false;

            if (subKey != null && that.subKey != null) {
                return subKey.equals(that.subKey);
            }

            return true;
        }

        @Override
        public int hashCode() {
            return uri.hashCode();
        }
    }
}