/*
 * 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.graphics.Bitmap;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.util.Log;
import android.widget.ImageView;

import androidx.annotation.Nullable;

import com.bumptech.glide.Glide;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.MultiTransformation;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.engine.GlideException;
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
import com.bumptech.glide.load.resource.bitmap.FitCenter;
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.RequestOptions;
import com.bumptech.glide.request.target.Target;

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

/**
 * Represents an asset located via an Android content URI.
 */
public final class ContentUriAsset extends StreamableAsset {
    private static final ExecutorService sExecutorService = Executors.newSingleThreadExecutor();
    private static final String TAG = "ContentUriAsset";
    private static final String JPEG_MIME_TYPE = "image/jpeg";
    private static final String PNG_MIME_TYPE = "image/png";

    private final Context mContext;
    private final Uri mUri;
    private final RequestOptions mRequestOptions;

    private ExifInterfaceCompat mExifCompat;
    private int mExifOrientation;

    /**
     * @param context The application's context.
     * @param uri     Content URI locating the asset.
     * @param requestOptions {@link RequestOptions} to be applied when loading the asset.
     * @param uncached If true, {@link #loadDrawable(Context, ImageView, int)} and
     * {@link #loadDrawableWithTransition(Context, ImageView, int, DrawableLoadedListener, int)}
     * will not cache data, and fetch it each time.
     */
    public ContentUriAsset(Context context, Uri uri, RequestOptions requestOptions,
                           boolean uncached) {
        mExifOrientation = ExifInterfaceCompat.EXIF_ORIENTATION_UNKNOWN;
        mContext = context.getApplicationContext();
        mUri = uri;

        if (uncached) {
            mRequestOptions = requestOptions.apply(RequestOptions
                    .diskCacheStrategyOf(DiskCacheStrategy.NONE)
                    .skipMemoryCache(true));
        } else {
            mRequestOptions = requestOptions;
        }
    }

    /**
     * @param context The application's context.
     * @param uri     Content URI locating the asset.
     * @param requestOptions {@link RequestOptions} to be applied when loading the asset.
     */
    public ContentUriAsset(Context context, Uri uri, RequestOptions requestOptions) {
        this(context, uri, requestOptions, /* uncached */ false);
    }

    /**
     * @param context The application's context.
     * @param uri     Content URI locating the asset.
     * @param uncached If true, {@link #loadDrawable(Context, ImageView, int)} and
     * {@link #loadDrawableWithTransition(Context, ImageView, int, DrawableLoadedListener, int)}
     * will not cache data, and fetch it each time.
     */
    public ContentUriAsset(Context context, Uri uri, boolean uncached) {
        this(context, uri, RequestOptions.centerCropTransform(), uncached);
    }

    /**
     * @param context The application's context.
     * @param uri     Content URI locating the asset.
     */
    public ContentUriAsset(Context context, Uri uri) {
            this(context, uri, /* uncached */ false);
    }



    @Override
    public void decodeBitmapRegion(final Rect rect, int targetWidth, int targetHeight,
            boolean shouldAdjustForRtl, final BitmapReceiver receiver) {
        // BitmapRegionDecoder only supports images encoded in either JPEG or PNG, so if the content
        // URI asset is encoded with another format (for example, GIF), then fall back to cropping a
        // bitmap region from the full-sized bitmap.
        if (isJpeg() || isPng()) {
            super.decodeBitmapRegion(rect, targetWidth, targetHeight, shouldAdjustForRtl, receiver);
            return;
        }

        decodeRawDimensions(null /* activity */, new DimensionsReceiver() {
            @Override
            public void onDimensionsDecoded(@Nullable Point dimensions) {
                if (dimensions == null) {
                    Log.e(TAG, "There was an error decoding the asset's raw dimensions with " +
                            "content URI: " + mUri);
                    receiver.onBitmapDecoded(null);
                    return;
                }

                decodeBitmap(dimensions.x, dimensions.y, new BitmapReceiver() {
                    @Override
                    public void onBitmapDecoded(@Nullable Bitmap fullBitmap) {
                        if (fullBitmap == null) {
                            Log.e(TAG, "There was an error decoding the asset's full bitmap with " +
                                    "content URI: " + mUri);
                            decodeBitmapCompleted(receiver, null);
                            return;
                        }
                        sExecutorService.execute(()-> {
                            decodeBitmapCompleted(receiver, Bitmap.createBitmap(
                                    fullBitmap, rect.left, rect.top, rect.width(), rect.height()));
                        });
                    }
                });
            }
        });
    }

    /**
     * Returns whether this image is encoded in the JPEG file format.
     */
    public boolean isJpeg() {
        String mimeType = mContext.getContentResolver().getType(mUri);
        return mimeType != null && mimeType.equals(JPEG_MIME_TYPE);
    }

    /**
     * Returns whether this image is encoded in the PNG file format.
     */
    public boolean isPng() {
        String mimeType = mContext.getContentResolver().getType(mUri);
        return mimeType != null && mimeType.equals(PNG_MIME_TYPE);
    }

    /**
     * Reads the EXIF tag on the asset. Automatically trims leading and trailing whitespace.
     *
     * @return String attribute value for this tag ID, or null if ExifInterface failed to read tags
     * for this asset, if this tag was not found in the image's metadata, or if this tag was
     * empty (i.e., only whitespace).
     */
    public String readExifTag(String tagId) {
        ensureExifInterface();
        if (mExifCompat == null) {
            Log.w(TAG, "Unable to read EXIF tags for content URI asset");
            return null;
        }


        String attribute = mExifCompat.getAttribute(tagId);
        if (attribute == null || attribute.trim().isEmpty()) {
            return null;
        }

        return attribute.trim();
    }

    private void ensureExifInterface() {
        if (mExifCompat == null) {
            try (InputStream inputStream = openInputStream()) {
                if (inputStream != null) {
                    mExifCompat = new ExifInterfaceCompat(inputStream);
                }
            } catch (IOException e) {
                Log.w(TAG, "Couldn't read stream for " + mUri, e);
            }
        }

    }

    @Override
    protected InputStream openInputStream() {
        try {
            return mContext.getContentResolver().openInputStream(mUri);
        } catch (FileNotFoundException e) {
            Log.w(TAG, "Image file not found", e);
            return null;
        }
    }

    @Override
    public int getExifOrientation() {
        if (mExifOrientation != ExifInterfaceCompat.EXIF_ORIENTATION_UNKNOWN) {
            return mExifOrientation;
        }

        mExifOrientation = readExifOrientation();
        return mExifOrientation;
    }

    /**
     * Returns the EXIF rotation for the content URI asset. This method should only be called off
     * the main UI thread.
     */
    private int readExifOrientation() {
        ensureExifInterface();
        if (mExifCompat == null) {
            Log.w(TAG, "Unable to read EXIF rotation for content URI asset with content URI: "
                    + mUri);
            return ExifInterfaceCompat.EXIF_ORIENTATION_NORMAL;
        }

        return mExifCompat.getAttributeInt(ExifInterfaceCompat.TAG_ORIENTATION,
                ExifInterfaceCompat.EXIF_ORIENTATION_NORMAL);
    }

    @Override
    public void loadDrawable(Context context, ImageView imageView,
                             int placeholderColor) {
        Glide.with(context)
                .asDrawable()
                .load(mUri)
                .apply(mRequestOptions
                        .placeholder(new ColorDrawable(placeholderColor)))
                .transition(DrawableTransitionOptions.withCrossFade())
                .into(imageView);
    }

    @Override
    public void loadLowResDrawable(Activity activity, ImageView imageView, int placeholderColor,
            BitmapTransformation transformation) {
        MultiTransformation<Bitmap> multiTransformation =
                new MultiTransformation<>(new FitCenter(), transformation);
        Glide.with(activity)
                .asDrawable()
                .load(mUri)
                .apply(RequestOptions.bitmapTransform(multiTransformation)
                        .placeholder(new ColorDrawable(placeholderColor)))
                .into(imageView);
    }

    @Override
    public void loadDrawableWithTransition(Context context, ImageView imageView,
            int transitionDurationMillis, @Nullable DrawableLoadedListener drawableLoadedListener,
            int placeholderColor) {
        Glide.with(context)
                .asDrawable()
                .load(mUri)
                .apply(mRequestOptions
                        .placeholder(new ColorDrawable(placeholderColor)))
                .transition(DrawableTransitionOptions.withCrossFade(transitionDurationMillis))
                .listener(new RequestListener<Drawable>() {
                    @Override
                    public boolean onLoadFailed(GlideException e, Object model,
                            Target<Drawable> target, boolean isFirstResource) {
                        return false;
                    }

                    @Override
                    public boolean onResourceReady(Drawable resource, Object model,
                            Target<Drawable> target, DataSource dataSource,
                            boolean isFirstResource) {
                        if (drawableLoadedListener != null) {
                            drawableLoadedListener.onDrawableLoaded();
                        }
                        return false;
                    }
                })
                .into(imageView);
    }

    public Uri getUri() {
        return mUri;
    }
}
