/*
 * Copyright (C) 2017 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.wallpaper.asset;

import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.BitmapRegionDecoder;
import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.Rect;
import android.media.ExifInterface;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;

import androidx.annotation.Nullable;

import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * Represents Asset types for which bytes can be read directly, allowing for flexible bitmap
 * decoding.
 */
public abstract class StreamableAsset extends Asset {
    private static final ExecutorService sExecutorService = Executors.newCachedThreadPool();
    private static final String TAG = "StreamableAsset";

    private BitmapRegionDecoder mBitmapRegionDecoder;
    private Point mDimensions;

    /**
     * Scales and returns a new Rect from the given Rect by the given scaling factor.
     */
    public static Rect scaleRect(Rect rect, float scale) {
        return new Rect(
                Math.round((float) rect.left * scale),
                Math.round((float) rect.top * scale),
                Math.round((float) rect.right * scale),
                Math.round((float) rect.bottom * scale));
    }

    /**
     * Maps from EXIF orientation tag values to counterclockwise degree rotation values.
     */
    private static int getDegreesRotationForExifOrientation(int exifOrientation) {
        switch (exifOrientation) {
            case ExifInterface.ORIENTATION_NORMAL:
                return 0;
            case ExifInterface.ORIENTATION_ROTATE_90:
                return 90;
            case ExifInterface.ORIENTATION_ROTATE_180:
                return 180;
            case ExifInterface.ORIENTATION_ROTATE_270:
                return 270;
            default:
                Log.w(TAG, "Unsupported EXIF orientation " + exifOrientation);
                return 0;
        }
    }

    @Override
    public void decodeBitmap(int targetWidth, int targetHeight, boolean useHardwareBitmapIfPossible,
                             BitmapReceiver receiver) {
        sExecutorService.execute(() -> {
            int newTargetWidth = targetWidth;
            int newTargetHeight = targetHeight;
            int exifOrientation = getExifOrientation();
            // Switch target height and width if image is rotated 90 or 270 degrees.
            if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_90
                    || exifOrientation == ExifInterface.ORIENTATION_ROTATE_270) {
                int tempHeight = newTargetHeight;
                newTargetHeight = newTargetWidth;
                newTargetWidth = tempHeight;
            }

            BitmapFactory.Options options = new BitmapFactory.Options();

            Point rawDimensions = calculateRawDimensions();
            // Raw dimensions may be null if there was an error opening the underlying input stream.
            if (rawDimensions == null) {
                decodeBitmapCompleted(receiver, null);
                return;
            }
            options.inSampleSize = BitmapUtils.calculateInSampleSize(
                    rawDimensions.x, rawDimensions.y, newTargetWidth, newTargetHeight);
            if (useHardwareBitmapIfPossible) {
                options.inPreferredConfig = Config.HARDWARE;
            }

            InputStream inputStream = openInputStream();
            Bitmap bitmap = null;
            if (inputStream != null) {
                bitmap = BitmapFactory.decodeStream(inputStream, null, options);
                closeInputStream(
                        inputStream, "Error closing the input stream used "
                                + "to decode the full bitmap");

                // Rotate output bitmap if necessary because of EXIF orientation tag.
                int matrixRotation = getDegreesRotationForExifOrientation(exifOrientation);
                if (matrixRotation > 0) {
                    Matrix rotateMatrix = new Matrix();
                    rotateMatrix.setRotate(matrixRotation);
                    bitmap = Bitmap.createBitmap(
                            bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(),
                            rotateMatrix, false);
                }
            }
            decodeBitmapCompleted(receiver, bitmap);
        });
    }

    @Override
    public void decodeBitmap(BitmapReceiver receiver) {
        sExecutorService.execute(() -> {
            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inPreferredConfig = Config.HARDWARE;
            InputStream inputStream = openInputStream();
            Bitmap bitmap = null;
            if (inputStream != null) {
                bitmap = BitmapFactory.decodeStream(inputStream, null, options);
                closeInputStream(inputStream,
                        "Error closing the input stream used to decode the full bitmap");

                // Rotate output bitmap if necessary because of EXIF orientation tag.
                int exifOrientation = getExifOrientation();
                int matrixRotation = getDegreesRotationForExifOrientation(exifOrientation);
                if (matrixRotation > 0) {
                    Matrix rotateMatrix = new Matrix();
                    rotateMatrix.setRotate(matrixRotation);
                    bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
                            bitmap.getHeight(), rotateMatrix, false);
                }
            }
            decodeBitmapCompleted(receiver, bitmap);
        });
    }

    @Override
    public void decodeRawDimensions(Activity unused, DimensionsReceiver receiver) {
        sExecutorService.execute(() -> {
            Point result = calculateRawDimensions();
            new Handler(Looper.getMainLooper()).post(() -> {
                receiver.onDimensionsDecoded(result);
            });
        });
    }

    @Override
    public void decodeBitmapRegion(Rect rect, int targetWidth, int targetHeight,
            boolean shouldAdjustForRtl, BitmapReceiver receiver) {
        runDecodeBitmapRegionTask(rect, targetWidth, targetHeight, shouldAdjustForRtl, receiver);
    }

    @Override
    public boolean supportsTiling() {
        return true;
    }

    /**
     * Fetches an input stream of bytes for the wallpaper image asset and provides the stream
     * asynchronously back to a {@link StreamReceiver}.
     */
    public void fetchInputStream(final StreamReceiver streamReceiver) {
        sExecutorService.execute(() -> {
            InputStream result = openInputStream();
            new Handler(Looper.getMainLooper()).post(() -> {
                streamReceiver.onInputStreamOpened(result);
            });
        });
    }

    /**
     * Returns an InputStream representing the asset. Should only be called off the main UI thread.
     */
    @Nullable
    protected abstract InputStream openInputStream();

    /**
     * Gets the EXIF orientation value of the asset. This method should only be called off the main UI
     * thread.
     */
    public int getExifOrientation() {
        // By default, assume that the EXIF orientation is normal (i.e., bitmap is rotated 0 degrees
        // from how it should be rendered to a viewer).
        return ExifInterface.ORIENTATION_NORMAL;
    }

    /**
     * Decodes and downscales a bitmap region off the main UI thread.
     *
     * @param rect         Rect representing the crop region in terms of the original image's resolution.
     * @param targetWidth  Width of target view in physical pixels.
     * @param targetHeight Height of target view in physical pixels.
     * @param isRtl
     * @param receiver     Called with the decoded bitmap region or null if there was an error decoding
     *                     the bitmap region.
     */
    public void runDecodeBitmapRegionTask(Rect rect, int targetWidth, int targetHeight,
            boolean isRtl, BitmapReceiver receiver) {
        sExecutorService.execute(() -> {
            int newTargetWidth = targetWidth;
            int newTargetHeight = targetHeight;
            Rect cropRect = rect;
            int exifOrientation = getExifOrientation();
            // Switch target height and width if image is rotated 90 or 270 degrees.
            if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_90
                    || exifOrientation == ExifInterface.ORIENTATION_ROTATE_270) {
                int tempHeight = newTargetHeight;
                newTargetHeight = newTargetWidth;
                newTargetWidth = tempHeight;
            }

            // Rotate crop rect if image is rotated more than 0 degrees.
            Point dimensions = calculateRawDimensions();
            cropRect = CropRectRotator.rotateCropRectForExifOrientation(
                    dimensions, cropRect, exifOrientation);

            // If we're in RTL mode, center in the rightmost side of the image
            if (isRtl) {
                cropRect.set(dimensions.x - cropRect.right, cropRect.top,
                        dimensions.x - cropRect.left, cropRect.bottom);
            }

            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inSampleSize = BitmapUtils.calculateInSampleSize(
                    cropRect.width(), cropRect.height(), newTargetWidth, newTargetHeight);

            if (mBitmapRegionDecoder == null) {
                mBitmapRegionDecoder = openBitmapRegionDecoder();
            }

            // Bitmap region decoder may have failed to open if there was a problem with the
            // underlying InputStream.
            if (mBitmapRegionDecoder != null) {
                try {
                    Bitmap bitmap = mBitmapRegionDecoder.decodeRegion(cropRect, options);

                    // Rotate output bitmap if necessary because of EXIF orientation.
                    int matrixRotation = getDegreesRotationForExifOrientation(exifOrientation);
                    if (matrixRotation > 0) {
                        Matrix rotateMatrix = new Matrix();
                        rotateMatrix.setRotate(matrixRotation);
                        bitmap = Bitmap.createBitmap(
                                bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), rotateMatrix,
                                false);
                    }
                    decodeBitmapCompleted(receiver, bitmap);
                    return;
                } catch (OutOfMemoryError e) {
                    Log.e(TAG, "Out of memory and unable to decode bitmap region", e);
                } catch (IllegalArgumentException e) {
                    Log.e(TAG, "Illegal argument for decoding bitmap region", e);
                }
            }
            decodeBitmapCompleted(receiver, null);
        });
    }

    /**
     * Decodes the raw dimensions of the asset without allocating memory for the entire asset. Adjusts
     * for the EXIF orientation if necessary.
     *
     * @return Dimensions as a Point where width is represented by "x" and height by "y".
     */
    @Nullable
    public Point calculateRawDimensions() {
        if (mDimensions != null) {
            return mDimensions;
        }

        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        InputStream inputStream = openInputStream();
        // Input stream may be null if there was an error opening it.
        if (inputStream == null) {
            return null;
        }
        BitmapFactory.decodeStream(inputStream, null, options);
        closeInputStream(inputStream, "There was an error closing the input stream used to calculate "
                + "the image's raw dimensions");

        int exifOrientation = getExifOrientation();
        // Swap height and width if image is rotated 90 or 270 degrees.
        if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_90
                || exifOrientation == ExifInterface.ORIENTATION_ROTATE_270) {
            mDimensions = new Point(options.outHeight, options.outWidth);
        } else {
            mDimensions = new Point(options.outWidth, options.outHeight);
        }

        return mDimensions;
    }

    /**
     * Returns a BitmapRegionDecoder for the asset.
     */
    @Nullable
    private BitmapRegionDecoder openBitmapRegionDecoder() {
        InputStream inputStream = null;
        BitmapRegionDecoder brd = null;

        try {
            inputStream = openInputStream();
            // Input stream may be null if there was an error opening it.
            if (inputStream == null) {
                return null;
            }
            brd = BitmapRegionDecoder.newInstance(inputStream, true);
        } catch (IOException e) {
            Log.w(TAG, "Unable to open BitmapRegionDecoder", e);
        } finally {
            closeInputStream(inputStream, "Unable to close input stream used to create "
                    + "BitmapRegionDecoder");
        }

        return brd;
    }

    /**
     * Closes the provided InputStream and if there was an error, logs the provided error message.
     */
    private void closeInputStream(InputStream inputStream, String errorMessage) {
        try {
            inputStream.close();
        } catch (IOException e) {
            Log.e(TAG, errorMessage);
        }
    }

    /**
     * Interface for receiving unmodified input streams of the underlying asset without any
     * downscaling or other decoding options.
     */
    public interface StreamReceiver {

        /**
         * Called with an opened input stream of bytes from the underlying image asset. Clients must
         * close the input stream after it has been read. Returns null if there was an error opening the
         * input stream.
         */
        void onInputStreamOpened(@Nullable InputStream inputStream);
    }
}



