package com.bumptech.glide.load.resource.bitmap;

import android.annotation.TargetApi;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.os.Build;
import android.support.annotation.Nullable;
import android.util.DisplayMetrics;
import android.util.Log;

import com.bumptech.glide.load.DecodeFormat;
import com.bumptech.glide.load.ImageHeaderParser;
import com.bumptech.glide.load.ImageHeaderParser.ImageType;
import com.bumptech.glide.load.ImageHeaderParserUtils;
import com.bumptech.glide.load.Option;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.engine.Resource;
import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy.SampleSizeRounding;
import com.bumptech.glide.request.RequestOptions;
import com.bumptech.glide.request.target.Target;
import com.bumptech.glide.util.LogTime;
import com.bumptech.glide.util.Preconditions;
import com.bumptech.glide.util.Util;

import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Queue;
import java.util.Set;

/**
 * Downsamples, decodes, and rotates images according to their exif orientation.
 */
public final class Downsampler {
    static final String TAG = "Downsampler";
    /**
     * Indicates the {@link com.bumptech.glide.load.DecodeFormat} that will be used in conjunction
     * with the image format to determine the {@link android.graphics.Bitmap.Config} to provide to
     * {@link android.graphics.BitmapFactory.Options#inPreferredConfig} when decoding the image.
     */
    public static final Option<DecodeFormat> DECODE_FORMAT = Option.memory(
            "com.bumptech.glide.load.resource.bitmap.Downsampler.DecodeFormat", DecodeFormat.DEFAULT);
    /**
     * Indicates the {@link com.bumptech.glide.load.resource.bitmap.DownsampleStrategy} option that
     * will be used to calculate the sample size to use to downsample an image given the original
     * and target dimensions of the image.
     *
     * @deprecated Use {@link DownsampleStrategy#OPTION} directly instead.
     */
    @Deprecated
    public static final Option<DownsampleStrategy> DOWNSAMPLE_STRATEGY = DownsampleStrategy.OPTION;
    /**
     * Ensure that the size of the bitmap is fixed to the requested width and height of the
     * resource from the caller.  The final resource dimensions may differ from the requested
     * width and height, and thus setting this to true may result in the bitmap size differing
     * from the resource dimensions.
     * <p>
     * This can be used as a performance optimization for KitKat and above by fixing the size of the
     * bitmap for a collection of requested resources so that the bitmap pool will not need to
     * allocate new bitmaps for images of different sizes.
     */
    // Public API
    @SuppressWarnings("WeakerAccess")
    public static final Option<Boolean> FIX_BITMAP_SIZE_TO_REQUESTED_DIMENSIONS =
            Option.memory("com.bumptech.glide.load.resource.bitmap.Downsampler.FixBitmapSize", false);

    /**
     * Indicates that it's safe or unsafe to decode {@link Bitmap}s with
     * {@link Bitmap.Config#HARDWARE}.
     *
     * <p>Callers should almost never set this value to {@code true} manually. Glide will already do
     * so when Glide believes it's safe to do (when no transformations are applied). Instead, callers
     * can set this value to {@code false} to prevent Glide from decoding hardware bitmaps if Glide
     * is unable to detect that hardware bitmaps are unsafe. For example, you should set this to
     * {@code false} if you plan to draw it to a software {@link android.graphics.Canvas} or if you
     * plan to inspect the {@link Bitmap}s pixels with {@link Bitmap#getPixel(int, int)} or
     * {@link Bitmap#getPixels(int[], int, int, int, int, int, int)}.
     *
     * <p>Callers can disable hardware {@link Bitmap}s for all loads using
     * {@link com.bumptech.glide.GlideBuilder#setDefaultRequestOptions(RequestOptions)}.
     *
     * <p>This option is ignored unless we're on Android O+.
     */
    public static final Option<Boolean> ALLOW_HARDWARE_CONFIG =
            Option.memory(
                    "com.bumptech.glide.load.resource.bitmap.Downsampler.AllowHardwareDecode", false);

    private static final String WBMP_MIME_TYPE = "image/vnd.wap.wbmp";
    private static final String ICO_MIME_TYPE = "image/x-ico";
    private static final Set<String> NO_DOWNSAMPLE_PRE_N_MIME_TYPES =
            Collections.unmodifiableSet(
                    new HashSet<>(
                            Arrays.asList(
                                    WBMP_MIME_TYPE,
                                    ICO_MIME_TYPE
                            )
                    )
            );
    private static final DecodeCallbacks EMPTY_CALLBACKS = new DecodeCallbacks() {
        @Override
        public void onObtainBounds() {
            // Do nothing.
        }

        @Override
        public void onDecodeComplete(BitmapPool bitmapPool, Bitmap downsampled) {
            // Do nothing.
        }
    };
    private static final Set<ImageHeaderParser.ImageType> TYPES_THAT_USE_POOL_PRE_KITKAT =
            Collections.unmodifiableSet(
                    EnumSet.of(
                            ImageHeaderParser.ImageType.JPEG,
                            ImageHeaderParser.ImageType.PNG_A,
                            ImageHeaderParser.ImageType.PNG
                    )
            );
    private static final Queue<BitmapFactory.Options> OPTIONS_QUEUE = Util.createQueue(0);
    // 10MB. This is the max image header size we can handle, we preallocate a much smaller buffer
    // but will resize up to this amount if necessary.
    private static final int MARK_POSITION = 10 * 1024 * 1024;

    private final BitmapPool bitmapPool;
    private final DisplayMetrics displayMetrics;
    private final ArrayPool byteArrayPool;
    private final List<ImageHeaderParser> parsers;
    private final HardwareConfigState hardwareConfigState = HardwareConfigState.getInstance();

    public Downsampler(List<ImageHeaderParser> parsers, DisplayMetrics displayMetrics,
                       BitmapPool bitmapPool, ArrayPool byteArrayPool) {
        this.parsers = parsers;
        this.displayMetrics = Preconditions.checkNotNull(displayMetrics);
        this.bitmapPool = Preconditions.checkNotNull(bitmapPool);
        this.byteArrayPool = Preconditions.checkNotNull(byteArrayPool);
    }

    public boolean handles(@SuppressWarnings("unused") InputStream is) {
        // We expect Downsampler to handle any available type Android supports.
        return true;
    }

    public boolean handles(@SuppressWarnings("unused") ByteBuffer byteBuffer) {
        // We expect downsampler to handle any available type Android supports.
        return true;
    }

    /**
     * Returns a Bitmap decoded from the given {@link InputStream} that is rotated to match any EXIF
     * data present in the stream and that is downsampled according to the given dimensions and any
     * provided  {@link com.bumptech.glide.load.resource.bitmap.DownsampleStrategy} option.
     *
     * @see #decode(InputStream, int, int, Options, DecodeCallbacks)
     */
    public Resource<Bitmap> decode(InputStream is, int outWidth, int outHeight,
                                   Options options) throws IOException {
        return decode(is, outWidth, outHeight, options, EMPTY_CALLBACKS);
    }

    /**
     * Returns a Bitmap decoded from the given {@link InputStream} that is rotated to match any EXIF
     * data present in the stream and that is downsampled according to the given dimensions and any
     * provided  {@link com.bumptech.glide.load.resource.bitmap.DownsampleStrategy} option.
     *
     * <p> If a Bitmap is present in the
     * {@link com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool} whose dimensions exactly match
     * those of the image for the given InputStream is available, the operation is much less expensive
     * in terms of memory. </p>
     *
     * <p> The provided {@link java.io.InputStream} must return <code>true</code> from
     * {@link java.io.InputStream#markSupported()} and is expected to support a reasonably large
     * mark limit to accommodate reading large image headers (~5MB). </p>
     *
     * @param is              An {@link InputStream} to the data for the image.
     * @param requestedWidth  The width the final image should be close to.
     * @param requestedHeight The height the final image should be close to.
     * @param options         A set of options that may contain one or more supported options that influence
     *                        how a Bitmap will be decoded from the given stream.
     * @param callbacks       A set of callbacks allowing callers to optionally respond to various
     *                        significant events during the decode process.
     * @return A new bitmap containing the image from the given InputStream, or recycle if recycle is
     * not null.
     */
    @SuppressWarnings({"resource", "deprecation"})
    public Resource<Bitmap> decode(InputStream is, int requestedWidth, int requestedHeight,
                                   Options options, DecodeCallbacks callbacks) throws IOException {
        Preconditions.checkArgument(is.markSupported(), "You must provide an InputStream that supports"
                + " mark()");

        byte[] bytesForOptions = byteArrayPool.get(ArrayPool.STANDARD_BUFFER_SIZE_BYTES, byte[].class);
        BitmapFactory.Options bitmapFactoryOptions = getDefaultOptions();
        bitmapFactoryOptions.inTempStorage = bytesForOptions;

        DecodeFormat decodeFormat = options.get(DECODE_FORMAT);
        DownsampleStrategy downsampleStrategy = options.get(DownsampleStrategy.OPTION);
        boolean fixBitmapToRequestedDimensions = options.get(FIX_BITMAP_SIZE_TO_REQUESTED_DIMENSIONS);
        boolean isHardwareConfigAllowed =
                options.get(ALLOW_HARDWARE_CONFIG) != null && options.get(ALLOW_HARDWARE_CONFIG);

        try {
            Bitmap result = decodeFromWrappedStreams(is, bitmapFactoryOptions,
                    downsampleStrategy, decodeFormat, isHardwareConfigAllowed, requestedWidth,
                    requestedHeight, fixBitmapToRequestedDimensions, callbacks);
            return BitmapResource.obtain(result, bitmapPool);
        } finally {
            releaseOptions(bitmapFactoryOptions);
            byteArrayPool.put(bytesForOptions);
        }
    }

    private Bitmap decodeFromWrappedStreams(InputStream is,
                                            BitmapFactory.Options options, DownsampleStrategy downsampleStrategy,
                                            DecodeFormat decodeFormat, boolean isHardwareConfigAllowed, int requestedWidth,
                                            int requestedHeight, boolean fixBitmapToRequestedDimensions,
                                            DecodeCallbacks callbacks) throws IOException {
        long startTime = LogTime.getLogTime();

        int[] sourceDimensions = getDimensions(is, options, callbacks, bitmapPool);
        int sourceWidth = sourceDimensions[0];
        int sourceHeight = sourceDimensions[1];
        String sourceMimeType = options.outMimeType;

        // If we failed to obtain the image dimensions, we may end up with an incorrectly sized Bitmap,
        // so we want to use a mutable Bitmap type. One way this can happen is if the image header is so
        // large (10mb+) that our attempt to use inJustDecodeBounds fails and we're forced to decode the
        // full size image.
        if (sourceWidth == -1 || sourceHeight == -1) {
            isHardwareConfigAllowed = false;
        }

        int orientation = ImageHeaderParserUtils.getOrientation(parsers, is, byteArrayPool);
        int degreesToRotate = TransformationUtils.getExifOrientationDegrees(orientation);
        boolean isExifOrientationRequired = TransformationUtils.isExifOrientationRequired(orientation);

        int targetWidth = requestedWidth == Target.SIZE_ORIGINAL ? sourceWidth : requestedWidth;
        int targetHeight = requestedHeight == Target.SIZE_ORIGINAL ? sourceHeight : requestedHeight;

        ImageType imageType = ImageHeaderParserUtils.getType(parsers, is, byteArrayPool);

        calculateScaling(
                imageType,
                is,
                callbacks,
                bitmapPool,
                downsampleStrategy,
                degreesToRotate,
                sourceWidth,
                sourceHeight,
                targetWidth,
                targetHeight,
                options);
        calculateConfig(
                is,
                decodeFormat,
                isHardwareConfigAllowed,
                isExifOrientationRequired,
                options,
                targetWidth,
                targetHeight);

        boolean isKitKatOrGreater = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
        // Prior to KitKat, the inBitmap size must exactly match the size of the bitmap we're decoding.
        if ((options.inSampleSize == 1 || isKitKatOrGreater) && shouldUsePool(imageType)) {
            int expectedWidth;
            int expectedHeight;
            if (sourceWidth >= 0 && sourceHeight >= 0
                    && fixBitmapToRequestedDimensions && isKitKatOrGreater) {
                expectedWidth = targetWidth;
                expectedHeight = targetHeight;
            } else {
                float densityMultiplier = isScaling(options)
                        ? (float) options.inTargetDensity / options.inDensity : 1f;
                int sampleSize = options.inSampleSize;
                int downsampledWidth = (int) Math.ceil(sourceWidth / (float) sampleSize);
                int downsampledHeight = (int) Math.ceil(sourceHeight / (float) sampleSize);
                expectedWidth = Math.round(downsampledWidth * densityMultiplier);
                expectedHeight = Math.round(downsampledHeight * densityMultiplier);

                if (Log.isLoggable(TAG, Log.VERBOSE)) {
                    Log.v(TAG, "Calculated target [" + expectedWidth + "x" + expectedHeight + "] for source"
                            + " [" + sourceWidth + "x" + sourceHeight + "]"
                            + ", sampleSize: " + sampleSize
                            + ", targetDensity: " + options.inTargetDensity
                            + ", density: " + options.inDensity
                            + ", density multiplier: " + densityMultiplier);
                }
            }
            // If this isn't an image, or BitmapFactory was unable to parse the size, width and height
            // will be -1 here.
            if (expectedWidth > 0 && expectedHeight > 0) {
                setInBitmap(options, bitmapPool, expectedWidth, expectedHeight);
            }
        }
        Bitmap downsampled = decodeStream(is, options, callbacks, bitmapPool);
        callbacks.onDecodeComplete(bitmapPool, downsampled);

        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logDecode(sourceWidth, sourceHeight, sourceMimeType, options, downsampled,
                    requestedWidth, requestedHeight, startTime);
        }

        Bitmap rotated = null;
        if (downsampled != null) {
            // If we scaled, the Bitmap density will be our inTargetDensity. Here we correct it back to
            // the expected density dpi.
            downsampled.setDensity(displayMetrics.densityDpi);

            rotated = TransformationUtils.rotateImageExif(bitmapPool, downsampled, orientation);
            if (!downsampled.equals(rotated)) {
                bitmapPool.put(downsampled);
            }
        }

        return rotated;
    }

    private static void calculateScaling(
            ImageType imageType,
            InputStream is,
            DecodeCallbacks decodeCallbacks,
            BitmapPool bitmapPool,
            DownsampleStrategy downsampleStrategy,
            int degreesToRotate,
            int sourceWidth,
            int sourceHeight,
            int targetWidth,
            int targetHeight,
            BitmapFactory.Options options) throws IOException {
        // We can't downsample source content if we can't determine its dimensions.
        if (sourceWidth <= 0 || sourceHeight <= 0) {
            if (Log.isLoggable(TAG, Log.DEBUG)) {
                Log.d(TAG, "Unable to determine dimensions for: " + imageType
                        + " with target [" + targetWidth + "x" + targetHeight + "]");
            }
            return;
        }

        final float exactScaleFactor;
        if (degreesToRotate == 90 || degreesToRotate == 270) {
            // If we're rotating the image +-90 degrees, we need to downsample accordingly so the image
            // width is decreased to near our target's height and the image height is decreased to near
            // our target width.
            //noinspection SuspiciousNameCombination
            exactScaleFactor = downsampleStrategy.getScaleFactor(sourceHeight, sourceWidth,
                    targetWidth, targetHeight);
        } else {
            exactScaleFactor =
                    downsampleStrategy.getScaleFactor(sourceWidth, sourceHeight, targetWidth, targetHeight);
        }

        if (exactScaleFactor <= 0f) {
            throw new IllegalArgumentException("Cannot scale with factor: " + exactScaleFactor
                    + " from: " + downsampleStrategy
                    + ", source: [" + sourceWidth + "x" + sourceHeight + "]"
                    + ", target: [" + targetWidth + "x" + targetHeight + "]");
        }
        SampleSizeRounding rounding = downsampleStrategy.getSampleSizeRounding(sourceWidth,
                sourceHeight, targetWidth, targetHeight);
        if (rounding == null) {
            throw new IllegalArgumentException("Cannot round with null rounding");
        }

        int outWidth = round(exactScaleFactor * sourceWidth);
        int outHeight = round(exactScaleFactor * sourceHeight);

        int widthScaleFactor = sourceWidth / outWidth;
        int heightScaleFactor = sourceHeight / outHeight;

        int scaleFactor = rounding == SampleSizeRounding.MEMORY
                ? Math.max(widthScaleFactor, heightScaleFactor)
                : Math.min(widthScaleFactor, heightScaleFactor);

        int powerOfTwoSampleSize;
        // BitmapFactory does not support downsampling wbmp files on platforms <= M. See b/27305903.
        if (Build.VERSION.SDK_INT <= 23
                && NO_DOWNSAMPLE_PRE_N_MIME_TYPES.contains(options.outMimeType)) {
            powerOfTwoSampleSize = 1;
        } else {
            powerOfTwoSampleSize = Math.max(1, Integer.highestOneBit(scaleFactor));
            if (rounding == SampleSizeRounding.MEMORY
                    && powerOfTwoSampleSize < (1.f / exactScaleFactor)) {
                powerOfTwoSampleSize = powerOfTwoSampleSize << 1;
            }
        }

        // Here we mimic framework logic for determining how inSampleSize division is rounded on various
        // versions of Android. The logic here has been tested on emulators for Android versions 15-26.
        // PNG - Always uses floor
        // JPEG - Always uses ceiling
        // Webp - Prior to N, always uses floor. At and after N, always uses round.
        options.inSampleSize = powerOfTwoSampleSize;
        int powerOfTwoWidth;
        int powerOfTwoHeight;
        if (imageType == ImageType.JPEG) {
            // libjpegturbo can downsample up to a sample size of 8. libjpegturbo uses ceiling to round.
            // After libjpegturbo's native rounding, skia does a secondary scale using floor
            // (integer division). Here we replicate that logic.
            int nativeScaling = Math.min(powerOfTwoSampleSize, 8);
            powerOfTwoWidth = (int) Math.ceil(sourceWidth / (float) nativeScaling);
            powerOfTwoHeight = (int) Math.ceil(sourceHeight / (float) nativeScaling);
            int secondaryScaling = powerOfTwoSampleSize / 8;
            if (secondaryScaling > 0) {
                powerOfTwoWidth = powerOfTwoWidth / secondaryScaling;
                powerOfTwoHeight = powerOfTwoHeight / secondaryScaling;
            }
        } else if (imageType == ImageType.PNG || imageType == ImageType.PNG_A) {
            powerOfTwoWidth = (int) Math.floor(sourceWidth / (float) powerOfTwoSampleSize);
            powerOfTwoHeight = (int) Math.floor(sourceHeight / (float) powerOfTwoSampleSize);
        } else if (imageType == ImageType.WEBP || imageType == ImageType.WEBP_A) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                powerOfTwoWidth = Math.round(sourceWidth / (float) powerOfTwoSampleSize);
                powerOfTwoHeight = Math.round(sourceHeight / (float) powerOfTwoSampleSize);
            } else {
                powerOfTwoWidth = (int) Math.floor(sourceWidth / (float) powerOfTwoSampleSize);
                powerOfTwoHeight = (int) Math.floor(sourceHeight / (float) powerOfTwoSampleSize);
            }
        } else if (
                sourceWidth % powerOfTwoSampleSize != 0 || sourceHeight % powerOfTwoSampleSize != 0) {
            // If we're not confident the image is in one of our types, fall back to checking the
            // dimensions again. inJustDecodeBounds decodes do obey inSampleSize.
            int[] dimensions = getDimensions(is, options, decodeCallbacks, bitmapPool);
            // Power of two downsampling in BitmapFactory uses a variety of random factors to determine
            // rounding that we can't reliably replicate for all image formats. Use ceiling here to make
            // sure that we at least provide a Bitmap that's large enough to fit the content we're going
            // to load.
            powerOfTwoWidth = dimensions[0];
            powerOfTwoHeight = dimensions[1];
        } else {
            powerOfTwoWidth = sourceWidth / powerOfTwoSampleSize;
            powerOfTwoHeight = sourceHeight / powerOfTwoSampleSize;
        }

        double adjustedScaleFactor = downsampleStrategy.getScaleFactor(
                powerOfTwoWidth, powerOfTwoHeight, targetWidth, targetHeight);

        // Density scaling is only supported if inBitmap is null prior to KitKat. Avoid setting
        // densities here so we calculate the final Bitmap size correctly.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            options.inTargetDensity = adjustTargetDensityForError(adjustedScaleFactor);
            options.inDensity = getDensityMultiplier(adjustedScaleFactor);
        }
        if (isScaling(options)) {
            options.inScaled = true;
        } else {
            options.inDensity = options.inTargetDensity = 0;
        }

        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            Log.v(TAG, "Calculate scaling"
                    + ", source: [" + sourceWidth + "x" + sourceHeight + "]"
                    + ", target: [" + targetWidth + "x" + targetHeight + "]"
                    + ", power of two scaled: [" + powerOfTwoWidth + "x" + powerOfTwoHeight + "]"
                    + ", exact scale factor: " + exactScaleFactor
                    + ", power of 2 sample size: " + powerOfTwoSampleSize
                    + ", adjusted scale factor: " + adjustedScaleFactor
                    + ", target density: " + options.inTargetDensity
                    + ", density: " + options.inDensity);
        }
    }

    /**
     * BitmapFactory calculates the density scale factor as a float. This introduces some non-trivial
     * error. This method attempts to account for that error by adjusting the inTargetDensity so that
     * the final scale factor is as close to our target as possible.
     */
    private static int adjustTargetDensityForError(double adjustedScaleFactor) {
        int densityMultiplier = getDensityMultiplier(adjustedScaleFactor);
        int targetDensity = round(densityMultiplier * adjustedScaleFactor);
        float scaleFactorWithError = targetDensity / (float) densityMultiplier;
        double difference = adjustedScaleFactor / scaleFactorWithError;
        return round(difference * targetDensity);
    }

    private static int getDensityMultiplier(double adjustedScaleFactor) {
        return (int) Math.round(
                Integer.MAX_VALUE
                        * (adjustedScaleFactor <= 1D ? adjustedScaleFactor : 1 / adjustedScaleFactor));
    }

    // This is weird, but it matches the logic in a bunch of Android views/framework classes for
    // rounding.
    private static int round(double value) {
        return (int) (value + 0.5d);
    }

    private boolean shouldUsePool(ImageType imageType) {
        // On KitKat+, any bitmap (of a given config) can be used to decode any other bitmap
        // (with the same config).
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            return true;
        }

        // We cannot reuse bitmaps when decoding images that are not PNG or JPG prior to KitKat.
        // See: https://groups.google.com/forum/#!msg/android-developers/Mp0MFVFi1Fo/e8ZQ9FGdWdEJ
        return TYPES_THAT_USE_POOL_PRE_KITKAT.contains(imageType);
    }

    @SuppressWarnings("deprecation")
    private void calculateConfig(
            InputStream is,
            DecodeFormat format,
            boolean isHardwareConfigAllowed,
            boolean isExifOrientationRequired,
            BitmapFactory.Options optionsWithScaling,
            int targetWidth,
            int targetHeight) {

        if (hardwareConfigState.setHardwareConfigIfAllowed(
                targetWidth,
                targetHeight,
                optionsWithScaling,
                format,
                isHardwareConfigAllowed,
                isExifOrientationRequired)) {
            return;
        }

        // Changing configs can cause skewing on 4.1, see issue #128.
        if (format == DecodeFormat.PREFER_ARGB_8888
                || Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN) {
            optionsWithScaling.inPreferredConfig = Bitmap.Config.ARGB_8888;
            return;
        }

        boolean hasAlpha = false;
        try {
            hasAlpha = ImageHeaderParserUtils.getType(parsers, is, byteArrayPool).hasAlpha();
        } catch (IOException e) {
            if (Log.isLoggable(TAG, Log.DEBUG)) {
                Log.d(TAG, "Cannot determine whether the image has alpha or not from header"
                        + ", format " + format, e);
            }
        }

        optionsWithScaling.inPreferredConfig =
                hasAlpha ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565;
        if (optionsWithScaling.inPreferredConfig == Config.RGB_565) {
            optionsWithScaling.inDither = true;
        }
    }

    /**
     * A method for getting the dimensions of an image from the given InputStream.
     *
     * @param is      The InputStream representing the image.
     * @param options The options to pass to {@link BitmapFactory#decodeStream(java.io.InputStream,
     *                android.graphics.Rect, android.graphics.BitmapFactory.Options)}.
     * @return an array containing the dimensions of the image in the form {width, height}.
     */
    private static int[] getDimensions(InputStream is, BitmapFactory.Options options,
                                       DecodeCallbacks decodeCallbacks, BitmapPool bitmapPool) throws IOException {
        options.inJustDecodeBounds = true;
        decodeStream(is, options, decodeCallbacks, bitmapPool);
        options.inJustDecodeBounds = false;
        return new int[]{options.outWidth, options.outHeight};
    }

    private static Bitmap decodeStream(InputStream is, BitmapFactory.Options options,
                                       DecodeCallbacks callbacks, BitmapPool bitmapPool) throws IOException {
        if (options.inJustDecodeBounds) {
            is.mark(MARK_POSITION);
        } else {
            // Once we've read the image header, we no longer need to allow the buffer to expand in
            // size. To avoid unnecessary allocations reading image data, we fix the mark limit so that it
            // is no larger than our current buffer size here. We need to do so immediately before
            // decoding the full image to avoid having our mark limit overridden by other calls to
            // mark and reset. See issue #225.
            callbacks.onObtainBounds();
        }
        // BitmapFactory.Options out* variables are reset by most calls to decodeStream, successful or
        // otherwise, so capture here in case we log below.
        int sourceWidth = options.outWidth;
        int sourceHeight = options.outHeight;
        String outMimeType = options.outMimeType;
        final Bitmap result;
        TransformationUtils.getBitmapDrawableLock().lock();
        try {
            result = BitmapFactory.decodeStream(is, null, options);
        } catch (IllegalArgumentException e) {
            IOException bitmapAssertionException =
                    newIoExceptionForInBitmapAssertion(e, sourceWidth, sourceHeight, outMimeType, options);
            if (Log.isLoggable(TAG, Log.DEBUG)) {
                Log.d(TAG, "Failed to decode with inBitmap, trying again without Bitmap re-use",
                        bitmapAssertionException);
            }
            if (options.inBitmap != null) {
                try {
                    is.reset();
                    bitmapPool.put(options.inBitmap);
                    options.inBitmap = null;
                    return decodeStream(is, options, callbacks, bitmapPool);
                } catch (IOException resetException) {
                    throw bitmapAssertionException;
                }
            }
            throw bitmapAssertionException;
        } finally {
            TransformationUtils.getBitmapDrawableLock().unlock();
        }

        if (options.inJustDecodeBounds) {
            is.reset();

        }
        return result;
    }

    private static boolean isScaling(BitmapFactory.Options options) {
        return options.inTargetDensity > 0 && options.inDensity > 0
                && options.inTargetDensity != options.inDensity;
    }

    private static void logDecode(int sourceWidth, int sourceHeight, String outMimeType,
                                  BitmapFactory.Options options, Bitmap result, int requestedWidth, int requestedHeight,
                                  long startTime) {
        Log.v(TAG, "Decoded " + getBitmapString(result)
                + " from [" + sourceWidth + "x" + sourceHeight + "] " + outMimeType
                + " with inBitmap " + getInBitmapString(options)
                + " for [" + requestedWidth + "x" + requestedHeight + "]"
                + ", sample size: " + options.inSampleSize
                + ", density: " + options.inDensity
                + ", target density: " + options.inTargetDensity
                + ", thread: " + Thread.currentThread().getName()
                + ", duration: " + LogTime.getElapsedMillis(startTime));
    }

    private static String getInBitmapString(BitmapFactory.Options options) {
        return getBitmapString(options.inBitmap);
    }

    @Nullable
    @TargetApi(Build.VERSION_CODES.KITKAT)
    private static String getBitmapString(Bitmap bitmap) {
        if (bitmap == null) {
            return null;
        }

        String sizeString = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
                ? " (" + bitmap.getAllocationByteCount() + ")" : "";
        return "[" + bitmap.getWidth() + "x" + bitmap.getHeight() + "] " + bitmap.getConfig()
                + sizeString;
    }

    // BitmapFactory throws an IllegalArgumentException if any error occurs attempting to decode a
    // file when inBitmap is non-null, including those caused by partial or corrupt data. We still log
    // the error because the IllegalArgumentException is supposed to catch errors reusing Bitmaps, so
    // want some useful log output. In most cases this can be safely treated as a normal IOException.
    private static IOException newIoExceptionForInBitmapAssertion(IllegalArgumentException e,
                                                                  int outWidth, int outHeight, String outMimeType, BitmapFactory.Options options) {
        return new IOException("Exception decoding bitmap"
                + ", outWidth: " + outWidth
                + ", outHeight: " + outHeight
                + ", outMimeType: " + outMimeType
                + ", inBitmap: " + getInBitmapString(options), e);
    }

    @SuppressWarnings("PMD.CollapsibleIfStatements")
    @TargetApi(Build.VERSION_CODES.O)
    private static void setInBitmap(
            BitmapFactory.Options options, BitmapPool bitmapPool, int width, int height) {
        @Nullable Bitmap.Config expectedConfig = null;
        // Avoid short circuiting, it appears to break on some devices.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            if (options.inPreferredConfig == Config.HARDWARE) {
                return;
            }
            // On API 26 outConfig may be null for some images even if the image is valid, can be decoded
            // and outWidth/outHeight/outColorSpace are populated (see b/71513049).
            expectedConfig = options.outConfig;
        }

        if (expectedConfig == null) {
            // We're going to guess that BitmapFactory will return us the config we're requesting. This
            // isn't always the case, even though our guesses tend to be conservative and prefer configs
            // of larger sizes so that the Bitmap will fit our image anyway. If we're wrong here and the
            // config we choose is too small, our initial decode will fail, but we will retry with no
            // inBitmap which will succeed so if we're wrong here, we're less efficient but still correct.
            expectedConfig = options.inPreferredConfig;
        }
        // BitmapFactory will clear out the Bitmap before writing to it, so getDirty is safe.
        options.inBitmap = bitmapPool.getDirty(width, height, expectedConfig);
    }

    private static synchronized BitmapFactory.Options getDefaultOptions() {
        BitmapFactory.Options decodeBitmapOptions;
        synchronized (OPTIONS_QUEUE) {
            decodeBitmapOptions = OPTIONS_QUEUE.poll();
        }
        if (decodeBitmapOptions == null) {
            decodeBitmapOptions = new BitmapFactory.Options();
            resetOptions(decodeBitmapOptions);
        }

        return decodeBitmapOptions;
    }

    private static void releaseOptions(BitmapFactory.Options decodeBitmapOptions) {
        resetOptions(decodeBitmapOptions);
        synchronized (OPTIONS_QUEUE) {
            OPTIONS_QUEUE.offer(decodeBitmapOptions);
        }
    }

    @SuppressWarnings("deprecation")
    private static void resetOptions(BitmapFactory.Options decodeBitmapOptions) {
        decodeBitmapOptions.inTempStorage = null;
        decodeBitmapOptions.inDither = false;
        decodeBitmapOptions.inScaled = false;
        decodeBitmapOptions.inSampleSize = 1;
        decodeBitmapOptions.inPreferredConfig = null;
        decodeBitmapOptions.inJustDecodeBounds = false;
        decodeBitmapOptions.inDensity = 0;
        decodeBitmapOptions.inTargetDensity = 0;
        decodeBitmapOptions.outWidth = 0;
        decodeBitmapOptions.outHeight = 0;
        decodeBitmapOptions.outMimeType = null;
        decodeBitmapOptions.inBitmap = null;
        decodeBitmapOptions.inMutable = true;
    }

    /**
     * Callbacks for key points during decodes.
     */
    public interface DecodeCallbacks {
        void onObtainBounds();

        void onDecodeComplete(BitmapPool bitmapPool, Bitmap downsampled) throws IOException;
    }
}
