package com.bumptech.glide.load.engine.cache;

import android.annotation.TargetApi;
import android.app.ActivityManager;
import android.content.Context;
import android.os.Build;
import android.support.annotation.VisibleForTesting;
import android.text.format.Formatter;
import android.util.DisplayMetrics;
import android.util.Log;

import com.bumptech.glide.util.Preconditions;
import com.bumptech.glide.util.Synthetic;

/**
 * A calculator that tries to intelligently determine cache sizes for a given device based on some
 * constants and the devices screen density, width, and height.
 */
public final class MemorySizeCalculator {
    private static final String TAG = "MemorySizeCalculator";
    @VisibleForTesting
    static final int BYTES_PER_ARGB_8888_PIXEL = 4;
    private static final int LOW_MEMORY_BYTE_ARRAY_POOL_DIVISOR = 2;

    private final int bitmapPoolSize;
    private final int memoryCacheSize;
    private final Context context;
    private final int arrayPoolSize;

    interface ScreenDimensions {
        int getWidthPixels();

        int getHeightPixels();
    }

    // Package private to avoid PMD warning.
    MemorySizeCalculator(MemorySizeCalculator.Builder builder) {
        this.context = builder.context;

        //默认为 4MB，如果是低内存设备则在此基础上除以二
        arrayPoolSize =
                isLowMemoryDevice(builder.activityManager)
                        ? builder.arrayPoolSizeBytes / LOW_MEMORY_BYTE_ARRAY_POOL_DIVISOR
                        : builder.arrayPoolSizeBytes;
        //其中会先获取当前进程可使用内存大小，
        //然后通过判断是否是否为低内存设备乘以相应的系数，
        //普通设备是乘以 0.4，低内存为 0.33，这样得到的是 Glide 可使用的最大内存阈值 maxSize
        int maxSize =
                getMaxSize(
                        builder.activityManager, builder.maxSizeMultiplier, builder.lowMemoryMaxSizeMultiplier);

        int widthPixels = builder.screenDimensions.getWidthPixels();
        int heightPixels = builder.screenDimensions.getHeightPixels();
        //计算一张格式为 ARGB_8888 ，大小为屏幕大小的图片的占用内存大小
        //BYTES_PER_ARGB_8888_PIXEL 值为 4
        int screenSize = widthPixels * heightPixels * BYTES_PER_ARGB_8888_PIXEL;

        int targetBitmapPoolSize = Math.round(screenSize * builder.bitmapPoolScreens);

        int targetMemoryCacheSize = Math.round(screenSize * builder.memoryCacheScreens);
        //去掉 ArrayPool 占用的内存后还剩余的内存
        int availableSize = maxSize - arrayPoolSize;

        if (targetMemoryCacheSize + targetBitmapPoolSize <= availableSize) {
            //未超出内存限制
            memoryCacheSize = targetMemoryCacheSize;
            bitmapPoolSize = targetBitmapPoolSize;
        } else {
            //超出内存限制
            float part = availableSize / (builder.bitmapPoolScreens + builder.memoryCacheScreens);
            memoryCacheSize = Math.round(part * builder.memoryCacheScreens);
            bitmapPoolSize = Math.round(part * builder.bitmapPoolScreens);
        }

        if (Log.isLoggable(TAG, Log.DEBUG)) {
            Log.d(
                    TAG,
                    "Calculation complete"
                            + ", Calculated memory cache size: "
                            + toMb(memoryCacheSize)
                            + ", pool size: "
                            + toMb(bitmapPoolSize)
                            + ", byte array size: "
                            + toMb(arrayPoolSize)
                            + ", memory class limited? "
                            + (targetMemoryCacheSize + targetBitmapPoolSize > maxSize)
                            + ", max size: "
                            + toMb(maxSize)
                            + ", memoryClass: "
                            + builder.activityManager.getMemoryClass()
                            + ", isLowMemoryDevice: "
                            + isLowMemoryDevice(builder.activityManager));
        }
    }

    /**
     * Returns the recommended memory cache size for the device it is run on in bytes.
     */
    public int getMemoryCacheSize() {
        return memoryCacheSize;
    }

    /**
     * Returns the recommended bitmap pool size for the device it is run on in bytes.
     */
    public int getBitmapPoolSize() {
        return bitmapPoolSize;
    }

    /**
     * Returns the recommended array pool size for the device it is run on in bytes.
     */
    public int getArrayPoolSizeInBytes() {
        return arrayPoolSize;
    }

    private static int getMaxSize(ActivityManager activityManager, float maxSizeMultiplier,
                                  float lowMemoryMaxSizeMultiplier) {
        final int memoryClassBytes = activityManager.getMemoryClass() * 1024 * 1024;
        final boolean isLowMemoryDevice = isLowMemoryDevice(activityManager);
        return Math.round(memoryClassBytes * (isLowMemoryDevice ? lowMemoryMaxSizeMultiplier
                : maxSizeMultiplier));
    }

    private String toMb(int bytes) {
        return Formatter.formatFileSize(context, bytes);
    }

    @TargetApi(Build.VERSION_CODES.KITKAT)
    @Synthetic
    static boolean isLowMemoryDevice(ActivityManager activityManager) {
        // Explicitly check with an if statement, on some devices both parts of boolean expressions
        // can be evaluated even if we'd normally expect a short circuit.
        //noinspection SimplifiableIfStatement
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            return activityManager.isLowRamDevice();
        } else {
            return true;
        }
    }

    /**
     * Constructs an {@link MemorySizeCalculator} with reasonable defaults that can be optionally
     * overridden.
     */
    // Public API.
    @SuppressWarnings({"WeakerAccess", "unused"})
    public static final class Builder {
        @VisibleForTesting
        static final int MEMORY_CACHE_TARGET_SCREENS = 2;

        /**
         * On Android O+, we use {@link android.graphics.Bitmap.Config#HARDWARE} for all reasonably
         * sized images unless we're creating thumbnails for the first time. As a result, the Bitmap
         * pool is much less important on O than it was on previous versions.
         */
        static final int BITMAP_POOL_TARGET_SCREENS =
                Build.VERSION.SDK_INT < Build.VERSION_CODES.O ? 4 : 1;

        static final float MAX_SIZE_MULTIPLIER = 0.4f;
        static final float LOW_MEMORY_MAX_SIZE_MULTIPLIER = 0.33f;
        // 4MB.
        static final int ARRAY_POOL_SIZE_BYTES = 4 * 1024 * 1024;

        @Synthetic
        final Context context;

        // Modifiable (non-final) for testing.
        @Synthetic
        ActivityManager activityManager;
        @Synthetic
        ScreenDimensions screenDimensions;

        @Synthetic
        float memoryCacheScreens = MEMORY_CACHE_TARGET_SCREENS;
        @Synthetic
        float bitmapPoolScreens = BITMAP_POOL_TARGET_SCREENS;
        @Synthetic
        float maxSizeMultiplier = MAX_SIZE_MULTIPLIER;
        @Synthetic
        float lowMemoryMaxSizeMultiplier = LOW_MEMORY_MAX_SIZE_MULTIPLIER;
        @Synthetic
        int arrayPoolSizeBytes = ARRAY_POOL_SIZE_BYTES;

        public Builder(Context context) {
            this.context = context;
            activityManager =
                    (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
            screenDimensions =
                    new DisplayMetricsScreenDimensions(context.getResources().getDisplayMetrics());

            // On Android O+ Bitmaps are allocated natively, ART is much more efficient at managing
            // garbage and we rely heavily on HARDWARE Bitmaps, making Bitmap re-use much less important.
            // We prefer to preserve RAM on these devices and take the small performance hit of not
            // re-using Bitmaps and textures when loading very small images or generating thumbnails.
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isLowMemoryDevice(activityManager)) {
                bitmapPoolScreens = 0;
            }
        }

        /**
         * Sets the number of device screens worth of pixels the
         * {@link com.bumptech.glide.load.engine.cache.MemoryCache} should be able to hold and
         * returns this Builder.
         */
        public Builder setMemoryCacheScreens(float memoryCacheScreens) {
            Preconditions.checkArgument(memoryCacheScreens >= 0,
                    "Memory cache screens must be greater than or equal to 0");
            this.memoryCacheScreens = memoryCacheScreens;
            return this;
        }

        /**
         * Sets the number of device screens worth of pixels the
         * {@link com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool} should be able to hold and
         * returns this Builder.
         */
        public Builder setBitmapPoolScreens(float bitmapPoolScreens) {
            Preconditions.checkArgument(bitmapPoolScreens >= 0,
                    "Bitmap pool screens must be greater than or equal to 0");
            this.bitmapPoolScreens = bitmapPoolScreens;
            return this;
        }

        /**
         * Sets the maximum percentage of the device's memory class for standard devices that can be
         * taken up by Glide's {@link com.bumptech.glide.load.engine.cache.MemoryCache} and
         * {@link com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool} put together, and returns
         * this builder.
         */
        public Builder setMaxSizeMultiplier(float maxSizeMultiplier) {
            Preconditions.checkArgument(maxSizeMultiplier >= 0 && maxSizeMultiplier <= 1,
                    "Size multiplier must be between 0 and 1");
            this.maxSizeMultiplier = maxSizeMultiplier;
            return this;
        }

        /**
         * Sets the maximum percentage of the device's memory class for low ram devices that can be
         * taken up by Glide's {@link com.bumptech.glide.load.engine.cache.MemoryCache} and
         * {@link com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool} put together, and returns
         * this builder.
         *
         * @see ActivityManager#isLowRamDevice()
         */
        public Builder setLowMemoryMaxSizeMultiplier(float lowMemoryMaxSizeMultiplier) {
            Preconditions.checkArgument(
                    lowMemoryMaxSizeMultiplier >= 0 && lowMemoryMaxSizeMultiplier <= 1,
                    "Low memory max size multiplier must be between 0 and 1");
            this.lowMemoryMaxSizeMultiplier = lowMemoryMaxSizeMultiplier;
            return this;
        }

        /**
         * Sets the size in bytes of the {@link
         * com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool} to use to store temporary
         * arrays while decoding data and returns this builder.
         *
         * <p>This number will be halved on low memory devices that return {@code true} from
         * {@link ActivityManager#isLowRamDevice()}.
         */
        public Builder setArrayPoolSize(int arrayPoolSizeBytes) {
            this.arrayPoolSizeBytes = arrayPoolSizeBytes;
            return this;
        }

        @VisibleForTesting
        Builder setActivityManager(ActivityManager activityManager) {
            this.activityManager = activityManager;
            return this;
        }

        @VisibleForTesting
        Builder setScreenDimensions(ScreenDimensions screenDimensions) {
            this.screenDimensions = screenDimensions;
            return this;
        }

        public MemorySizeCalculator build() {
            return new MemorySizeCalculator(this);
        }
    }

    private static final class DisplayMetricsScreenDimensions implements ScreenDimensions {
        private final DisplayMetrics displayMetrics;

        DisplayMetricsScreenDimensions(DisplayMetrics displayMetrics) {
            this.displayMetrics = displayMetrics;
        }

        @Override
        public int getWidthPixels() {
            return displayMetrics.widthPixels;
        }

        @Override
        public int getHeightPixels() {
            return displayMetrics.heightPixels;
        }
    }
}
