/*
 * Copyright (C) 2019 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 android.car.cluster;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.car.cluster.navigation.NavigationState.ImageReference;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.net.Uri;
import android.util.Log;

import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;

/**
 * Class for retrieving bitmap images from a ContentProvider
 *
 * @hide
 */
public class ImageResolver {
    private static final String TAG = "Cluster.ImageResolver";
    private final BitmapFetcher mFetcher;

    /**
     * Interface used for fetching bitmaps from a content resolver
     */
    public interface BitmapFetcher {
        /**
         * Returns a {@link Bitmap} given a request Uri and dimensions
         */
        Bitmap getBitmap(@NonNull Uri uri, int width, int height);

        /**
         * Returns a {@link Bitmap} given a request Uri, dimensions, and offLanesAlpha value
         */
        Bitmap getBitmap(@NonNull Uri uri, int width, int height, float offLanesAlpha);
    }

    /**
     * Creates a resolver that delegate the image retrieval to the given fetcher.
     */
    public ImageResolver(BitmapFetcher fetcher) {
        mFetcher = fetcher;
    }

    /**
     * Returns a {@link CompletableFuture} that provides a bitmap from a {@link ImageReference}.
     * This image would fit inside the provided size. Either width, height or both should be greater
     * than 0.
     *
     * @param width  required width, or 0 if width is flexible based on height.
     * @param height required height, or 0 if height is flexible based on width.
     */
    @NonNull
    public CompletableFuture<Bitmap> getBitmap(@NonNull ImageReference img, int width, int height) {
        return getBitmap(img, width, height, 1f);
    }

    /**
     * Returns a {@link CompletableFuture} that provides a bitmap from a {@link ImageReference}.
     * This image would fit inside the provided size. Either width, height or both should be greater
     * than 0.
     *
     * @param width         required width, or 0 if width is flexible based on height.
     * @param height        required height, or 0 if height is flexible based on width.
     * @param offLanesAlpha opacity value for off lane guidance images. Only applies to lane
     *                      guidance images. 0 (transparent) <= offLanesAlpha <= 1 (opaque).
     */
    @NonNull
    public CompletableFuture<Bitmap> getBitmap(@NonNull ImageReference img, int width, int height,
            float offLanesAlpha) {
        if (Log.isLoggable(TAG, Log.DEBUG)) {
            Log.d(TAG, String.format("Requesting image %s (width: %d, height: %d)",
                    img.getContentUri(), width, height));
        }

        return CompletableFuture.supplyAsync(() -> {
            // Adjust the size to fit in the requested box.
            Point adjusted = getAdjustedSize(img.getAspectRatio(), width, height);
            if (adjusted == null) {
                Log.e(TAG, "The provided image has no aspect ratio: " + img.getContentUri());
                return null;
            }

            Uri uri = Uri.parse(img.getContentUri());
            Bitmap bitmap = null;
            try {
                bitmap = mFetcher.getBitmap(uri, adjusted.x, adjusted.y, offLanesAlpha);
            } catch (IllegalArgumentException e) {
                Log.e(TAG, e.getMessage());
            }
            if (bitmap == null) {
                if (Log.isLoggable(TAG, Log.DEBUG)) {
                    Log.d(TAG, "Unable to fetch image: " + uri);
                }
                return null;
            }

            if (Log.isLoggable(TAG, Log.DEBUG)) {
                Log.d(TAG, String.format("Returning image %s (width: %d, height: %d)",
                        img.getContentUri(), width, height));
            }
            return bitmap;
        });
    }

    /**
     * Same as {@link #getBitmap(ImageReference, int, int)} but it works on a list of images. The
     * returning {@link CompletableFuture} will contain a map from each {@link ImageReference} to
     * its bitmap. If any image fails to be fetched, the whole future completes exceptionally.
     *
     * @param width  required width, or 0 if width is flexible based on height.
     * @param height required height, or 0 if height is flexible based on width.
     */
    @NonNull
    public CompletableFuture<Map<ImageReference, Bitmap>> getBitmaps(
            @NonNull List<ImageReference> imgs, int width, int height) {
        return getBitmaps(imgs, width, height, 1f);
    }

    /**
     * Same as {@link #getBitmap(ImageReference, int, int)} but it works on a list of images. The
     * returning {@link CompletableFuture} will contain a map from each {@link ImageReference} to
     * its bitmap. If any image fails to be fetched, the whole future completes exceptionally.
     *
     * @param width         required width, or 0 if width is flexible based on height.
     * @param height        required height, or 0 if height is flexible based on width.
     * @param offLanesAlpha opacity value for off lane guidance images. Only applies to lane
     *                      guidance images. 0 (transparent) <= offLanesAlpha <= 1 (opaque).
     */
    @NonNull
    public CompletableFuture<Map<ImageReference, Bitmap>> getBitmaps(
            @NonNull List<ImageReference> imgs, int width, int height, float offLanesAlpha) {
        CompletableFuture<Map<ImageReference, Bitmap>> future = new CompletableFuture<>();

        Map<ImageReference, CompletableFuture<Bitmap>> bitmapFutures = imgs.stream().collect(
                Collectors.toMap(
                        img -> img,
                        img -> getBitmap(img, width, height, offLanesAlpha)));

        CompletableFuture.allOf(bitmapFutures.values().toArray(new CompletableFuture[0]))
                .thenAccept(v -> {
                    Map<ImageReference, Bitmap> bitmaps = bitmapFutures.entrySet().stream()
                            .collect(Collectors.toMap(Map.Entry::getKey, entry -> entry
                                    .getValue().join()));
                    future.complete(bitmaps);
                })
                .exceptionally(ex -> {
                    future.completeExceptionally(ex);
                    return null;
                });

        return future;
    }

    /**
     * Returns an image size that exactly fits inside a requested box, maintaining an original size
     * aspect ratio.
     *
     * @param imageRatio      original aspect ratio (must be > 0)
     * @param requestedWidth  required width, or 0 if width is flexible based on height.
     * @param requestedHeight required height, or 0 if height is flexible based on width.
     */
    @Nullable
    public Point getAdjustedSize(double imageRatio, int requestedWidth,
            int requestedHeight) {
        if (imageRatio <= 0) {
            return null;
        } else if (requestedWidth == 0 && requestedHeight == 0) {
            throw new IllegalArgumentException("At least one of width or height must be != 0");
        }
        // If width is flexible or if both width and height are set and the original image is wider
        // than the space provided, then scale the width.
        float requiredRatio = requestedHeight > 0 ? ((float) requestedWidth) / requestedHeight : 0;
        Point res = new Point(requestedWidth, requestedHeight);
        if (requestedWidth == 0 || (requestedHeight != 0 && imageRatio < requiredRatio)) {
            res.x = (int) (imageRatio * requestedHeight);
        } else {
            res.y = (int) (requestedWidth / imageRatio);
        }
        return res;
    }
}
