/*
 * Copyright (C) 2015 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.messaging.datamodel;

import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import androidx.annotation.NonNull;
import android.text.TextUtils;
import android.util.SparseArray;

import com.android.messaging.datamodel.MemoryCacheManager.MemoryCache;
import com.android.messaging.util.Assert;
import com.android.messaging.util.LogUtil;

import java.io.InputStream;

/**
 * Class for creating / loading / reusing bitmaps. This class allow the user to create a new bitmap,
 * reuse an bitmap from the pool and to return a bitmap for future reuse.  The pool of bitmaps
 * allows for faster decode and more efficient memory usage.
 * Note: consumers should not create BitmapPool directly, but instead get the pool they want from
 * the BitmapPoolManager.
 */
public class BitmapPool implements MemoryCache {
    public static final int MAX_SUPPORTED_IMAGE_DIMENSION = 0xFFFF;

    protected static final boolean VERBOSE = false;

    /**
     * Number of reuse failures to skip before reporting.
     */
    private static final int FAILED_REPORTING_FREQUENCY = 100;

    /**
     * Count of reuse failures which have occurred.
     */
    private static volatile int sFailedBitmapReuseCount = 0;

    /**
     * Overall pool data structure which currently only supports rectangular bitmaps. The size of
     * one of the sides is used to index into the SparseArray.
     */
    private final SparseArray<SingleSizePool> mPool;
    private final Object mPoolLock = new Object();
    private final String mPoolName;
    private final int mMaxSize;

    /**
     * Inner structure which holds a pool of bitmaps all the same size (i.e. all have the same
     * width as each other and height as each other, but not necessarily the same).
     */
    private class SingleSizePool {
        int mNumItems;
        final Bitmap[] mBitmaps;

        SingleSizePool(final int maxPoolSize) {
            mNumItems = 0;
            mBitmaps = new Bitmap[maxPoolSize];
        }
    }

    /**
     * Creates a pool of reused bitmaps with helper decode methods which will attempt to use the
     * reclaimed bitmaps. This will help speed up the creation of bitmaps by using already allocated
     * bitmaps.
     * @param maxSize The overall max size of the pool. When the pool exceeds this size, all calls
     * to reclaimBitmap(Bitmap) will result in recycling the bitmap.
     * @param name Name of the bitmap pool and only used for logging. Can not be null.
     */
    BitmapPool(final int maxSize, @NonNull final String name) {
        Assert.isTrue(maxSize > 0);
        Assert.isTrue(!TextUtils.isEmpty(name));
        mPoolName = name;
        mMaxSize = maxSize;
        mPool = new SparseArray<SingleSizePool>();
    }

    @Override
    public void reclaim() {
        synchronized (mPoolLock) {
            for (int p = 0; p < mPool.size(); p++) {
                final SingleSizePool singleSizePool = mPool.valueAt(p);
                for (int i = 0; i < singleSizePool.mNumItems; i++) {
                    singleSizePool.mBitmaps[i].recycle();
                    singleSizePool.mBitmaps[i] = null;
                }
                singleSizePool.mNumItems = 0;
            }
            mPool.clear();
        }
    }

    /**
     * Creates a new BitmapFactory.Options.
     */
    public static BitmapFactory.Options getBitmapOptionsForPool(final boolean scaled,
            final int inputDensity, final int targetDensity) {
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inScaled = scaled;
        options.inDensity = inputDensity;
        options.inTargetDensity = targetDensity;
        options.inSampleSize = 1;
        options.inJustDecodeBounds = false;
        options.inMutable = true;
        return options;
    }

    /**
     * @return The pool key for the provided image dimensions or 0 if either width or height is
     * greater than the max supported image dimension.
     */
    private int getPoolKey(final int width, final int height) {
        if (width > MAX_SUPPORTED_IMAGE_DIMENSION || height > MAX_SUPPORTED_IMAGE_DIMENSION) {
            return 0;
        }
        return (width << 16) | height;
    }

    /**
     *
     * @return A bitmap in the pool with the specified dimensions or null if no bitmap with the
     * specified dimension is available.
     */
    private Bitmap findPoolBitmap(final int width, final int height) {
        final int poolKey = getPoolKey(width, height);
        if (poolKey != 0) {
            synchronized (mPoolLock) {
                // Take a bitmap from the pool if one is available
                final SingleSizePool singlePool = mPool.get(poolKey);
                if (singlePool != null && singlePool.mNumItems > 0) {
                    singlePool.mNumItems--;
                    final Bitmap foundBitmap = singlePool.mBitmaps[singlePool.mNumItems];
                    singlePool.mBitmaps[singlePool.mNumItems] = null;
                    return foundBitmap;
                }
            }
        }
        return null;
    }

    /**
     * Internal function to try and find a bitmap in the pool which matches the desired width and
     * height and then set that in the bitmap options properly.
     *
     * TODO: Why do we take a width/height? Shouldn't this already be in the
     * BitmapFactory.Options instance? Can we assert that they match?
     * @param optionsTmp The BitmapFactory.Options to update with the bitmap for the system to try
     * to reuse.
     * @param width The width of the reusable bitmap.
     * @param height The height of the reusable bitmap.
     */
    private void assignPoolBitmap(final BitmapFactory.Options optionsTmp, final int width,
            final int height) {
        if (optionsTmp.inJustDecodeBounds) {
            return;
        }
        optionsTmp.inBitmap = findPoolBitmap(width, height);
    }

    /**
     * Load a resource into a bitmap. Uses a bitmap from the pool if possible to reduce memory
     * turnover.
     * @param resourceId Resource id to load.
     * @param resources Application resources. Cannot be null.
     * @param optionsTmp Should be the same options returned from getBitmapOptionsForPool(). Cannot
     * be null.
     * @param width The width of the bitmap.
     * @param height The height of the bitmap.
     * @return The decoded Bitmap with the resource drawn in it.
     */
    public Bitmap decodeSampledBitmapFromResource(final int resourceId,
            @NonNull final Resources resources, @NonNull final BitmapFactory.Options optionsTmp,
            final int width, final int height) {
        Assert.notNull(resources);
        Assert.notNull(optionsTmp);
        Assert.isTrue(width > 0);
        Assert.isTrue(height > 0);
        assignPoolBitmap(optionsTmp, width, height);
        Bitmap b = null;
        try {
            b = BitmapFactory.decodeResource(resources, resourceId, optionsTmp);
        } catch (final IllegalArgumentException e) {
            // BitmapFactory couldn't decode the file, try again without an inputBufferBitmap.
            if (optionsTmp.inBitmap != null) {
                optionsTmp.inBitmap = null;
                b = BitmapFactory.decodeResource(resources, resourceId, optionsTmp);
                sFailedBitmapReuseCount++;
                if (sFailedBitmapReuseCount % FAILED_REPORTING_FREQUENCY == 0) {
                    LogUtil.w(LogUtil.BUGLE_TAG,
                            "Pooled bitmap consistently not being reused count = " +
                            sFailedBitmapReuseCount);
                }
            }
        } catch (final OutOfMemoryError e) {
            LogUtil.w(LogUtil.BUGLE_TAG, "Oom decoding resource " + resourceId);
            reclaim();
        }
        return b;
    }

    /**
     * Load an input stream into a bitmap. Uses a bitmap from the pool if possible to reduce memory
     * turnover.
     * @param inputStream InputStream load. Cannot be null.
     * @param optionsTmp Should be the same options returned from getBitmapOptionsForPool(). Cannot
     * be null.
     * @param width The width of the bitmap.
     * @param height The height of the bitmap.
     * @return The decoded Bitmap with the resource drawn in it.
     */
    public Bitmap decodeSampledBitmapFromInputStream(@NonNull final InputStream inputStream,
            @NonNull final BitmapFactory.Options optionsTmp,
            final int width, final int height) {
        Assert.notNull(inputStream);
        Assert.isTrue(width > 0);
        Assert.isTrue(height > 0);
        assignPoolBitmap(optionsTmp, width, height);
        Bitmap b = null;
        try {
            b = BitmapFactory.decodeStream(inputStream, null, optionsTmp);
        } catch (final IllegalArgumentException e) {
            // BitmapFactory couldn't decode the file, try again without an inputBufferBitmap.
            if (optionsTmp.inBitmap != null) {
                optionsTmp.inBitmap = null;
                b = BitmapFactory.decodeStream(inputStream, null, optionsTmp);
                sFailedBitmapReuseCount++;
                if (sFailedBitmapReuseCount % FAILED_REPORTING_FREQUENCY == 0) {
                    LogUtil.w(LogUtil.BUGLE_TAG,
                            "Pooled bitmap consistently not being reused count = " +
                            sFailedBitmapReuseCount);
                }
            }
        } catch (final OutOfMemoryError e) {
            LogUtil.w(LogUtil.BUGLE_TAG, "Oom decoding inputStream");
            reclaim();
        }
        return b;
    }

    /**
     * Turn encoded bytes into a bitmap. Uses a bitmap from the pool if possible to reduce memory
     * turnover.
     * @param bytes Encoded bytes to draw on the bitmap. Cannot be null.
     * @param optionsTmp The bitmap will set here and the input should be generated from
     * getBitmapOptionsForPool(). Cannot be null.
     * @param width The width of the bitmap.
     * @param height The height of the bitmap.
     * @return A Bitmap with the encoded bytes drawn in it.
     */
    public Bitmap decodeByteArray(@NonNull final byte[] bytes,
            @NonNull final BitmapFactory.Options optionsTmp, final int width,
            final int height) throws OutOfMemoryError {
        Assert.notNull(bytes);
        Assert.notNull(optionsTmp);
        Assert.isTrue(width > 0);
        Assert.isTrue(height > 0);
        assignPoolBitmap(optionsTmp, width, height);
        Bitmap b = null;
        try {
            b = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, optionsTmp);
        } catch (final IllegalArgumentException e) {
            if (VERBOSE) {
                LogUtil.v(LogUtil.BUGLE_TAG, "BitmapPool(" + mPoolName +
                        ") Unable to use pool bitmap");
            }
            // BitmapFactory couldn't decode the file, try again without an inputBufferBitmap.
            // (i.e. without the bitmap from the pool)
            if (optionsTmp.inBitmap != null) {
                optionsTmp.inBitmap = null;
                b = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, optionsTmp);
                sFailedBitmapReuseCount++;
                if (sFailedBitmapReuseCount % FAILED_REPORTING_FREQUENCY == 0) {
                    LogUtil.w(LogUtil.BUGLE_TAG,
                            "Pooled bitmap consistently not being reused count = " +
                            sFailedBitmapReuseCount);
                }
            }
        }
        return b;
    }

    /**
     * Creates a bitmap with the given size, this will reuse a bitmap in the pool, if one is
     * available, otherwise this will create a new one.
     * @param width The desired width of the bitmap.
     * @param height The desired height of the bitmap.
     * @return A bitmap with the desired width and height, this maybe a reused bitmap from the pool.
     */
    public Bitmap createOrReuseBitmap(final int width, final int height) {
        Bitmap b = findPoolBitmap(width, height);
        if (b == null) {
            b = createBitmap(width, height);
        }
        return b;
    }

    /**
     * This will create a new bitmap regardless of pool state.
     * @param width The desired width of the bitmap.
     * @param height The desired height of the bitmap.
     * @return A bitmap with the desired width and height.
     */
    private Bitmap createBitmap(final int width, final int height) {
        return Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    }

    /**
     * Called when a bitmap is finished being used so that it can be used for another bitmap in the
     * future or recycled. Any bitmaps returned should not be used by the caller again.
     * @param b The bitmap to return to the pool for future usage or recycled. This cannot be null.
     */
    public void reclaimBitmap(@NonNull final Bitmap b) {
        Assert.notNull(b);
        final int poolKey = getPoolKey(b.getWidth(), b.getHeight());
        if (poolKey == 0 || !b.isMutable()) {
            // Unsupported image dimensions or a immutable bitmap.
            b.recycle();
            return;
        }
        synchronized (mPoolLock) {
            SingleSizePool singleSizePool = mPool.get(poolKey);
            if (singleSizePool == null) {
                singleSizePool = new SingleSizePool(mMaxSize);
                mPool.append(poolKey, singleSizePool);
            }
            if (singleSizePool.mNumItems < singleSizePool.mBitmaps.length) {
                singleSizePool.mBitmaps[singleSizePool.mNumItems] = b;
                singleSizePool.mNumItems++;
            } else {
                b.recycle();
            }
        }
    }

    /**
     * @return whether the pool is full for a given width and height.
     */
    public boolean isFull(final int width, final int height) {
        final int poolKey = getPoolKey(width, height);
        synchronized (mPoolLock) {
            final SingleSizePool singleSizePool = mPool.get(poolKey);
            if (singleSizePool != null &&
                    singleSizePool.mNumItems >= singleSizePool.mBitmaps.length) {
                return true;
            }
            return false;
        }
    }
}
