/*
 * Copyright (C) 2015 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.tv.util.images;

import android.content.ContentResolver;
import android.content.Context;
import android.database.sqlite.SQLiteException;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.net.TrafficStats;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
import com.android.tv.common.util.NetworkTrafficTags;
import java.io.BufferedInputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;

public final class BitmapUtils {
    private static final String TAG = "BitmapUtils";
    private static final boolean DEBUG = false;

    // The value of 64K, for MARK_READ_LIMIT, is chosen to be eight times the default buffer size
    // of BufferedInputStream (8K) allowing it to double its buffers three times. Also it is a
    // fairly reasonable value, not using too much memory and being large enough for most cases.
    private static final int MARK_READ_LIMIT = 64 * 1024; // 64K

    private static final int CONNECTION_TIMEOUT_MS_FOR_URLCONNECTION = 3000; // 3 sec
    private static final int READ_TIMEOUT_MS_FOR_URLCONNECTION = 10000; // 10 sec

    private BitmapUtils() {
        /* cannot be instantiated */
    }

    public static Bitmap scaleBitmap(Bitmap bm, int maxWidth, int maxHeight) {
        Rect rect = calculateNewSize(bm, maxWidth, maxHeight);
        return Bitmap.createScaledBitmap(bm, rect.right, rect.bottom, false);
    }

    public static Bitmap getScaledMutableBitmap(Bitmap bm, int maxWidth, int maxHeight) {
        Bitmap scaledBitmap = scaleBitmap(bm, maxWidth, maxHeight);
        return scaledBitmap.isMutable()
                ? scaledBitmap
                : scaledBitmap.copy(Bitmap.Config.ARGB_8888, true);
    }

    private static Rect calculateNewSize(Bitmap bm, int maxWidth, int maxHeight) {
        final double ratio = maxHeight / (double) maxWidth;
        final double bmRatio = bm.getHeight() / (double) bm.getWidth();
        Rect rect = new Rect();
        if (ratio > bmRatio) {
            rect.right = maxWidth;
            rect.bottom = Math.round((float) bm.getHeight() * maxWidth / bm.getWidth());
        } else {
            rect.right = Math.round((float) bm.getWidth() * maxHeight / bm.getHeight());
            rect.bottom = maxHeight;
        }
        return rect;
    }

    public static ScaledBitmapInfo createScaledBitmapInfo(
            String id, Bitmap bm, int maxWidth, int maxHeight) {
        return new ScaledBitmapInfo(
                id,
                scaleBitmap(bm, maxWidth, maxHeight),
                calculateInSampleSize(bm.getWidth(), bm.getHeight(), maxWidth, maxHeight));
    }

    @Nullable
    public static Bitmap drawableToBitmap(Drawable drawable) {
        if (drawable == null) {
            return null;
        }
        Bitmap bm = Bitmap.createBitmap(
                drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Config.ARGB_8888);
        Canvas canvas = new Canvas(bm);
        drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
        drawable.draw(canvas);
        return bm;
    }

    /** Decode large sized bitmap into requested size. */
    public static ScaledBitmapInfo decodeSampledBitmapFromUriString(
            Context context, String uriString, int reqWidth, int reqHeight) {
        if (TextUtils.isEmpty(uriString)) {
            return null;
        }

        Uri uri = Uri.parse(uriString).normalizeScheme();
        boolean isResourceUri = isContentResolverUri(uri);
        URLConnection urlConnection = null;
        InputStream inputStream = null;
        final int oldTag = TrafficStats.getThreadStatsTag();
        TrafficStats.setThreadStatsTag(NetworkTrafficTags.LOGO_FETCHER);
        try {
            if (isResourceUri) {
                inputStream = context.getContentResolver().openInputStream(uri);
            } else {
                // If the URLConnection is HttpURLConnection, disconnect() should be called
                // explicitly.
                urlConnection = getUrlConnection(uriString);
                inputStream = urlConnection.getInputStream();
            }
            inputStream = new BufferedInputStream(inputStream);
            inputStream.mark(MARK_READ_LIMIT);

            // Check the bitmap dimensions.
            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inJustDecodeBounds = true;
            BitmapFactory.decodeStream(inputStream, null, options);

            // Rewind the stream in order to restart bitmap decoding.
            try {
                inputStream.reset();
            } catch (IOException e) {
                if (DEBUG) Log.i(TAG, "Failed to rewind stream: " + uriString, e);

                // Failed to rewind the stream, try to reopen it.
                close(inputStream, urlConnection);
                if (isResourceUri) {
                    inputStream = context.getContentResolver().openInputStream(uri);
                } else {
                    urlConnection = getUrlConnection(uriString);
                    inputStream = urlConnection.getInputStream();
                }
            }

            // Decode the bitmap possibly resizing it.
            options.inJustDecodeBounds = false;
            options.inPreferredConfig = Bitmap.Config.RGB_565;
            options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
            Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null, options);
            if (bitmap == null) {
                return null;
            }
            return new ScaledBitmapInfo(uriString, bitmap, options.inSampleSize);
        } catch (IOException e) {
            if (DEBUG) {
                // It can happens in normal cases like when a channel doesn't have any logo.
                Log.w(TAG, "Failed to open stream: " + uriString, e);
            }
            return null;
        } catch (SQLiteException e) {
            Log.e(TAG, "Failed to open stream: " + uriString, e);
            return null;
        } finally {
            close(inputStream, urlConnection);
            TrafficStats.setThreadStatsTag(oldTag);
        }
    }

    private static URLConnection getUrlConnection(String uriString) throws IOException {
        URLConnection urlConnection = new URL(uriString).openConnection();
        urlConnection.setConnectTimeout(CONNECTION_TIMEOUT_MS_FOR_URLCONNECTION);
        urlConnection.setReadTimeout(READ_TIMEOUT_MS_FOR_URLCONNECTION);
        return urlConnection;
    }

    private static int calculateInSampleSize(
            BitmapFactory.Options options, int reqWidth, int reqHeight) {
        return calculateInSampleSize(options.outWidth, options.outHeight, reqWidth, reqHeight);
    }

    private static int calculateInSampleSize(int width, int height, int reqWidth, int reqHeight) {
        // Calculates the largest inSampleSize that, is a power of two and, keeps either width or
        // height larger or equal to the requested width and height.
        int ratio = Math.max(width / reqWidth, height / reqHeight);
        return Math.max(1, Integer.highestOneBit(ratio));
    }

    private static boolean isContentResolverUri(Uri uri) {
        String scheme = uri.getScheme();
        return ContentResolver.SCHEME_CONTENT.equals(scheme)
                || ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)
                || ContentResolver.SCHEME_FILE.equals(scheme);
    }

    private static void close(Closeable closeable, URLConnection urlConnection) {
        if (closeable != null) {
            try {
                closeable.close();
            } catch (IOException e) {
                // Log and continue.
                Log.w(TAG, "Error closing " + closeable, e);
            }
        }
        if (urlConnection instanceof HttpURLConnection) {
            ((HttpURLConnection) urlConnection).disconnect();
        }
    }

    /** A wrapper class which contains the loaded bitmap and the scaling information. */
    public static class ScaledBitmapInfo {
        /** The id of bitmap, usually this is the URI of the original. */
        @NonNull public final String id;

        /** The loaded bitmap object. */
        @NonNull public final Bitmap bitmap;

        /**
         * The scaling factor to the original bitmap. It should be an positive integer.
         *
         * @see android.graphics.BitmapFactory.Options#inSampleSize
         */
        public final int inSampleSize;

        /**
         * A constructor.
         *
         * @param bitmap The loaded bitmap object.
         * @param inSampleSize The sampling size. See {@link
         *     android.graphics.BitmapFactory.Options#inSampleSize}
         */
        public ScaledBitmapInfo(@NonNull String id, @NonNull Bitmap bitmap, int inSampleSize) {
            this.id = id;
            this.bitmap = bitmap;
            this.inSampleSize = inSampleSize;
        }

        /**
         * Checks if the bitmap needs to be reloaded. The scaling is performed by power 2. The
         * bitmap can be reloaded only if the required width or height is greater then or equal to
         * the existing bitmap. If the full sized bitmap is already loaded, returns {@code false}.
         *
         * @see android.graphics.BitmapFactory.Options#inSampleSize
         */
        public boolean needToReload(int reqWidth, int reqHeight) {
            if (inSampleSize <= 1) {
                if (DEBUG) Log.d(TAG, "Reload not required " + this + " already full size.");
                return false;
            }
            Rect size = calculateNewSize(this.bitmap, reqWidth, reqHeight);
            boolean reload =
                    (size.right >= bitmap.getWidth() * 2 || size.bottom >= bitmap.getHeight() * 2);
            if (DEBUG) {
                Log.d(
                        TAG,
                        "needToReload("
                                + reqWidth
                                + ", "
                                + reqHeight
                                + ")="
                                + reload
                                + " because the new size would be "
                                + size
                                + " for "
                                + this);
            }
            return reload;
        }

        /** Returns {@code true} if a request the size of {@code other} would need a reload. */
        public boolean needToReload(ScaledBitmapInfo other) {
            return needToReload(other.bitmap.getWidth(), other.bitmap.getHeight());
        }

        @Override
        public String toString() {
            return "ScaledBitmapInfo["
                    + id
                    + "](in="
                    + inSampleSize
                    + ", w="
                    + bitmap.getWidth()
                    + ", h="
                    + bitmap.getHeight()
                    + ")";
        }
    }

    /**
     * Applies a color filter to the {@code drawable}. The color filter is made with the given
     * {@code color} and {@link android.graphics.PorterDuff.Mode#SRC_ATOP}.
     *
     * @see Drawable#setColorFilter
     */
    public static void setColorFilterToDrawable(int color, Drawable drawable) {
        if (drawable != null) {
            drawable.mutate().setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
        }
    }
}
