/*
 * 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.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.TransitionDrawable;
import android.os.Handler;
import android.os.Looper;
import android.view.Display;
import android.view.View;
import android.widget.ImageView;

import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;

import com.android.wallpaper.module.BitmapCropper;
import com.android.wallpaper.module.InjectorProvider;
import com.android.wallpaper.picker.preview.ui.util.CropSizeUtil;
import com.android.wallpaper.util.RtlUtils;
import com.android.wallpaper.util.ScreenSizeCalculator;
import com.android.wallpaper.util.WallpaperCropUtils;

import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;

import java.io.File;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * Interface representing an image asset.
 */
public abstract class Asset {
    private static final ExecutorService sExecutorService = Executors.newSingleThreadExecutor();
    /**
     * Creates and returns a placeholder Drawable instance sized exactly to the target ImageView and
     * filled completely with pixels of the provided placeholder color.
     */
    protected static Drawable getPlaceholderDrawable(
            Context context, ImageView imageView, int placeholderColor) {
        Point imageViewDimensions = getViewDimensions(imageView);
        Bitmap placeholderBitmap =
                Bitmap.createBitmap(imageViewDimensions.x, imageViewDimensions.y, Config.ARGB_8888);
        placeholderBitmap.eraseColor(placeholderColor);
        return new BitmapDrawable(context.getResources(), placeholderBitmap);
    }

    /**
     * Returns the visible height and width in pixels of the provided ImageView, or if it hasn't
     * been laid out yet, then gets the absolute value of the layout params.
     */
    private static Point getViewDimensions(View view) {
        int width = view.getWidth() > 0 ? view.getWidth() : Math.abs(view.getLayoutParams().width);
        int height = view.getHeight() > 0 ? view.getHeight()
                : Math.abs(view.getLayoutParams().height);

        return new Point(width, height);
    }

    /**
     * Decodes a bitmap sized for the destination view's dimensions off the main UI thread.
     *
     * @param targetWidth  Width of target view in physical pixels.
     * @param targetHeight Height of target view in physical pixels.
     * @param receiver     Called with the decoded bitmap or null if there was an error decoding the
     *                     bitmap.
     */
    public final void decodeBitmap(int targetWidth, int targetHeight, BitmapReceiver receiver) {
        decodeBitmap(targetWidth, targetHeight, true, receiver);
    }


    /**
     * Decodes a bitmap sized for the destination view's dimensions off the main UI thread.
     *
     * @param targetWidth  Width of target view in physical pixels.
     * @param targetHeight Height of target view in physical pixels.
     * @param hardwareBitmapAllowed if true and it's possible, we'll try to decode into a HARDWARE
     *                              bitmap
     * @param receiver     Called with the decoded bitmap or null if there was an error decoding the
     *                     bitmap.
     */
    public abstract void decodeBitmap(int targetWidth, int targetHeight,
            boolean hardwareBitmapAllowed, BitmapReceiver receiver);

    /**
     * Copies the asset file to another place.
     * @param dest  The destination file.
     */
    public void copy(File dest) {
        // no op
    }

    /**
     * Decodes a full bitmap.
     *
     * @param receiver     Called with the decoded bitmap or null if there was an error decoding the
     *                     bitmap.
     */
    public abstract void decodeBitmap(BitmapReceiver receiver);

    /**
     * For {@link #decodeBitmap(int, int, BitmapReceiver)} to use when it is done. It then call
     * the receiver with decoded bitmap in the main thread.
     *
     * @param receiver The receiver to handle decoded bitmap or null if decoding failed.
     * @param decodedBitmap The bitmap which is already decoded.
     */
    protected void decodeBitmapCompleted(BitmapReceiver receiver, Bitmap decodedBitmap) {
        new Handler(Looper.getMainLooper()).post(() -> receiver.onBitmapDecoded(decodedBitmap));
    }

    /**
     * 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 shouldAdjustForRtl whether the region selected should be adjusted for RTL (that is,
     *                           the crop region will be considered starting from the right)
     * @param receiver     Called with the decoded bitmap region or null if there was an error
     */
    public abstract void decodeBitmapRegion(Rect rect, int targetWidth, int targetHeight,
            boolean shouldAdjustForRtl, BitmapReceiver receiver);

    /**
     * Calculates the raw dimensions of the asset at its original resolution off the main UI thread.
     * Avoids decoding the entire bitmap if possible to conserve memory.
     *
     * @param activity Activity in which this decoding request is made. Allows for early termination
     *                 of fetching image data and/or decoding to a bitmap. May be null, in which
     *                 case the request is made in the application context instead.
     * @param receiver Called with the decoded raw dimensions of the whole image or null if there
     *                 was an error decoding the dimensions.
     */
    public abstract void decodeRawDimensions(@Nullable Activity activity,
            DimensionsReceiver receiver);

    /**
     * Returns whether this asset has access to a separate, lower fidelity source of image data
     * (that may be able to be loaded more quickly to simulate progressive loading).
     */
    public boolean hasLowResDataSource() {
        return false;
    }

    /**
     * Loads the asset from the separate low resolution data source (if there is one) into the
     * provided ImageView with the placeholder color and bitmap transformation.
     *
     * @param transformation Bitmap transformation that can transform the thumbnail image
     *                       post-decoding.
     */
    public void loadLowResDrawable(Activity activity, ImageView imageView, int placeholderColor,
            BitmapTransformation transformation) {
        // No op
    }

    /**
     * Returns a Bitmap from the separate low resolution data source (if there is one) or
     * {@code null} otherwise.
     * This could be an I/O operation so DO NOT CALL ON UI THREAD
     */
    @WorkerThread
    @Nullable
    public Bitmap getLowResBitmap(Context context) {
        return null;
    }

    /**
     * Returns whether the asset supports rendering tile regions at varying pixel densities.
     */
    public abstract boolean supportsTiling();

    /**
     * Loads a Drawable for this asset into the provided ImageView. While waiting for the image to
     * load, first loads a ColorDrawable based on the provided placeholder color.
     *
     * @param context          Activity hosting the ImageView.
     * @param imageView        ImageView which is the target view of this asset.
     * @param placeholderColor Color of placeholder set to ImageView while waiting for image to
     *                         load.
     */
    public void loadDrawable(final Context context, final ImageView imageView,
            int placeholderColor) {
        // Transition from a placeholder ColorDrawable to the decoded bitmap when the ImageView in
        // question is empty.
        final boolean needsTransition = imageView.getDrawable() == null;
        final Drawable placeholderDrawable = new ColorDrawable(placeholderColor);
        if (needsTransition) {
            imageView.setImageDrawable(placeholderDrawable);
        }

        // Set requested height and width to the either the actual height and width of the view in
        // pixels, or if it hasn't been laid out yet, then to the absolute value of the layout
        // params.
        int width = imageView.getWidth() > 0
                ? imageView.getWidth()
                : Math.abs(imageView.getLayoutParams().width);
        int height = imageView.getHeight() > 0
                ? imageView.getHeight()
                : Math.abs(imageView.getLayoutParams().height);

        decodeBitmap(width, height, new BitmapReceiver() {
            @Override
            public void onBitmapDecoded(Bitmap bitmap) {
                if (!needsTransition) {
                    imageView.setImageBitmap(bitmap);
                    return;
                }

                Resources resources = context.getResources();

                Drawable[] layers = new Drawable[2];
                layers[0] = placeholderDrawable;
                layers[1] = new BitmapDrawable(resources, bitmap);

                TransitionDrawable transitionDrawable = new TransitionDrawable(layers);
                transitionDrawable.setCrossFadeEnabled(true);

                imageView.setImageDrawable(transitionDrawable);
                transitionDrawable.startTransition(resources.getInteger(
                        android.R.integer.config_shortAnimTime));
            }
        });
    }

    /**
     * Loads a Drawable for this asset into the provided ImageView, providing a crossfade transition
     * with the given duration from the Drawable previously set on the ImageView.
     *
     * @param context                  Activity hosting the ImageView.
     * @param imageView                ImageView which is the target view of this asset.
     * @param transitionDurationMillis Duration of the crossfade, in milliseconds.
     * @param drawableLoadedListener   Listener called once the transition has begun.
     * @param placeholderColor         Color of the placeholder if the provided ImageView is empty
     *                                 before the
     */
    public void loadDrawableWithTransition(
            final Context context,
            final ImageView imageView,
            final int transitionDurationMillis,
            @Nullable final DrawableLoadedListener drawableLoadedListener,
            int placeholderColor) {
        Point imageViewDimensions = getViewDimensions(imageView);

        // Transition from a placeholder ColorDrawable to the decoded bitmap when the ImageView in
        // question is empty.
        boolean needsPlaceholder = imageView.getDrawable() == null;
        if (needsPlaceholder) {
            imageView.setImageDrawable(
                    getPlaceholderDrawable(context, imageView, placeholderColor));
        }

        decodeBitmap(imageViewDimensions.x, imageViewDimensions.y, new BitmapReceiver() {
            @Override
            public void onBitmapDecoded(Bitmap bitmap) {
                final Resources resources = context.getResources();

                centerCropBitmap(bitmap, imageView, new BitmapReceiver() {
                    @Override
                    public void onBitmapDecoded(@Nullable Bitmap newBitmap) {
                        Drawable[] layers = new Drawable[2];
                        Drawable existingDrawable = imageView.getDrawable();

                        if (existingDrawable instanceof TransitionDrawable) {
                            // Take only the second layer in the existing TransitionDrawable so
                            // we don't keep
                            // around a reference to older layers which are no longer shown (this
                            // way we avoid a
                            // memory leak).
                            TransitionDrawable existingTransitionDrawable =
                                    (TransitionDrawable) existingDrawable;
                            int id = existingTransitionDrawable.getId(1);
                            layers[0] = existingTransitionDrawable.findDrawableByLayerId(id);
                        } else {
                            layers[0] = existingDrawable;
                        }
                        layers[1] = new BitmapDrawable(resources, newBitmap);

                        TransitionDrawable transitionDrawable = new TransitionDrawable(layers);
                        transitionDrawable.setCrossFadeEnabled(true);

                        imageView.setImageDrawable(transitionDrawable);
                        transitionDrawable.startTransition(transitionDurationMillis);

                        if (drawableLoadedListener != null) {
                            drawableLoadedListener.onDrawableLoaded();
                        }
                    }
                });
            }
        });
    }

    /**
     * Loads the image for this asset into the provided ImageView which is used for the preview.
     * While waiting for the image to load, first loads a ColorDrawable based on the provided
     * placeholder color.
     *
     * @param activity         Activity hosting the ImageView.
     * @param imageView        ImageView which is the target view of this asset.
     * @param placeholderColor Color of placeholder set to ImageView while waiting for image to
     *                         load.
     * @param offsetToStart    true to let the preview show from the start of the image, false to
     *                         center-aligned to the image.
     */
    public void loadPreviewImage(Activity activity, ImageView imageView, int placeholderColor,
            boolean offsetToStart) {
        loadPreviewImage(activity, imageView, placeholderColor, offsetToStart, null);
    }

    /**
     * Loads the image for this asset into the provided ImageView which is used for the preview.
     * While waiting for the image to load, first loads a ColorDrawable based on the provided
     * placeholder color.
     *
     * @param activity         Activity hosting the ImageView.
     * @param imageView        ImageView which is the target view of this asset.
     * @param placeholderColor Color of placeholder set to ImageView while waiting for image to
     *                         load.
     * @param offsetToStart    true to let the preview show from the start of the image, false to
     *                         center-aligned to the image.
     * @param cropHints        A Map of display size to crop rect
     */
    public void loadPreviewImage(Activity activity, ImageView imageView, int placeholderColor,
            boolean offsetToStart, @Nullable Map<Point, Rect> cropHints) {
        boolean needsTransition = imageView.getDrawable() == null;
        Drawable placeholderDrawable = new ColorDrawable(placeholderColor);
        if (needsTransition) {
            imageView.setImageDrawable(placeholderDrawable);
        }

        decodeRawDimensions(activity, dimensions -> {
            // TODO (b/286404249): A proper fix here would be to find out why the
            //  leak happens in first place
            if (activity.isDestroyed()) {
                return;
            }
            if (dimensions == null) {
                loadDrawable(activity, imageView, placeholderColor);
                return;
            }

            boolean isRtl = RtlUtils.isRtl(activity);
            Display defaultDisplay = activity.getWindowManager().getDefaultDisplay();
            Point screenSize = ScreenSizeCalculator.getInstance().getScreenSize(defaultDisplay);
            Rect visibleRawWallpaperRect =
                    WallpaperCropUtils.calculateVisibleRect(dimensions, screenSize);
            if (cropHints != null && cropHints.containsKey(screenSize)) {
                visibleRawWallpaperRect = CropSizeUtil.INSTANCE.fitCropRectToLayoutDirection(
                        cropHints.get(screenSize), screenSize, RtlUtils.isRtl(activity));
                // For multi-crop, the visibleRawWallpaperRect above is already the exact size of
                // the part of wallpaper we should show on the screen, turning off the old RTL
                // logic by assigning false.
                isRtl = false;
            }

            // TODO(b/264234793): Make offsetToStart general support or for the specific asset.
            adjustCropRect(activity, dimensions, visibleRawWallpaperRect, offsetToStart);

            BitmapCropper bitmapCropper = InjectorProvider.getInjector().getBitmapCropper();
            bitmapCropper.cropAndScaleBitmap(this, /* scale= */ 1f, visibleRawWallpaperRect,
                    isRtl,
                    new BitmapCropper.Callback() {
                        @Override
                        public void onBitmapCropped(Bitmap croppedBitmap) {
                            // Since the size of the cropped bitmap may not exactly the same with
                            // image view(maybe has 1px or 2px difference),
                            // so set CENTER_CROP to let the bitmap to fit the image view.
                            if (!activity.isDestroyed()) {
                                imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
                                if (!needsTransition) {
                                    imageView.setImageBitmap(croppedBitmap);
                                    return;
                                }

                                Resources resources = activity.getResources();

                                Drawable[] layers = new Drawable[2];
                                layers[0] = placeholderDrawable;
                                layers[1] = new BitmapDrawable(resources, croppedBitmap);

                                TransitionDrawable transitionDrawable = new
                                        TransitionDrawable(layers);
                                transitionDrawable.setCrossFadeEnabled(true);

                                imageView.setImageDrawable(transitionDrawable);
                                transitionDrawable.startTransition(resources.getInteger(
                                        android.R.integer.config_shortAnimTime));
                            }
                        }

                        @Override
                        public void onError(@Nullable Throwable e) {
                            if (!activity.isDestroyed()) {
                                loadDrawable(activity, imageView, placeholderColor);
                            }
                        }
                    });
        });
    }

    /**
     * Interface for receiving decoded Bitmaps.
     */
    public interface BitmapReceiver {

        /**
         * Called with a decoded Bitmap object or null if there was an error decoding the bitmap.
         */
        void onBitmapDecoded(@Nullable Bitmap bitmap);
    }

    /**
     * Interface for receiving raw asset dimensions.
     */
    public interface DimensionsReceiver {

        /**
         * Called with raw dimensions of asset or null if the asset is unable to decode the raw
         * dimensions.
         *
         * @param dimensions Dimensions as a Point where width is represented by "x" and height by
         *                   "y".
         */
        void onDimensionsDecoded(@Nullable Point dimensions);
    }

    /**
     * Interface for being notified when a drawable has been loaded.
     */
    public interface DrawableLoadedListener {
        void onDrawableLoaded();
    }

    protected void adjustCropRect(Context context, Point assetDimensions, Rect cropRect,
            boolean offsetToStart) {
        WallpaperCropUtils.adjustCropRect(context, cropRect, true /* zoomIn */);
    }

    /**
     * Returns a copy of the given bitmap which is center cropped and scaled
     * to fit in the given ImageView and the thread runs on ExecutorService.
     */
    public void centerCropBitmap(Bitmap bitmap, View view, BitmapReceiver bitmapReceiver) {
        Point imageViewDimensions = getViewDimensions(view);
        sExecutorService.execute(() -> {
            int measuredWidth = imageViewDimensions.x;
            int measuredHeight = imageViewDimensions.y;

            int bitmapWidth = bitmap.getWidth();
            int bitmapHeight = bitmap.getHeight();

            float scale = Math.min(
                    (float) bitmapWidth / measuredWidth,
                    (float) bitmapHeight / measuredHeight);

            Bitmap scaledBitmap = Bitmap.createScaledBitmap(
                    bitmap, Math.round(bitmapWidth / scale), Math.round(bitmapHeight / scale),
                    true);

            int horizontalGutterPx = Math.max(0, (scaledBitmap.getWidth() - measuredWidth) / 2);
            int verticalGutterPx = Math.max(0, (scaledBitmap.getHeight() - measuredHeight) / 2);
            Bitmap result = Bitmap.createBitmap(
                    scaledBitmap,
                    horizontalGutterPx,
                    verticalGutterPx,
                    scaledBitmap.getWidth() - (2 * horizontalGutterPx),
                    scaledBitmap.getHeight() - (2 * verticalGutterPx));
            decodeBitmapCompleted(bitmapReceiver, result);
        });
    }
}
