/*
 * Copyright (C) 2014 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.server.telecom;

import android.app.Notification;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.telecom.Log;
import android.net.Uri;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;

// TODO: Needed for move to system service: import com.android.internal.R;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

/**
 * Helper class for loading contacts photo asynchronously.
 */
public class ContactsAsyncHelper {
    private static final String LOG_TAG = ContactsAsyncHelper.class.getSimpleName();

    public static class Factory {
        public ContactsAsyncHelper create(ContentResolverAdapter adapter) {
            return new ContactsAsyncHelper(adapter);
        }
    }

    /**
     * Interface for a WorkerHandler result return.
     */
    public interface OnImageLoadCompleteListener {
        /**
         * Called when the image load is complete.
         *
         * @param token Integer passed in {@link ContactsAsyncHelper#startObtainPhotoAsync(int,
         * Context, Uri, OnImageLoadCompleteListener, Object)}.
         * @param photo Drawable object obtained by the async load.
         * @param photoIcon Bitmap object obtained by the async load.
         * @param cookie Object passed in {@link ContactsAsyncHelper#startObtainPhotoAsync(int,
         * Context, Uri, OnImageLoadCompleteListener, Object)}. Can be null iff. the original
         * cookie is null.
         */
        public void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon,
                Object cookie);
    }

    /**
     * Interface to enable stubbing of the call to openInputStream
     */
    public interface ContentResolverAdapter {
        InputStream openInputStream(Context context, Uri uri) throws FileNotFoundException;
    }

    // constants
    private static final int EVENT_LOAD_IMAGE = 1;

    /** Handler run on a worker thread to load photo asynchronously. */
    private Handler mThreadHandler;
    private final ContentResolverAdapter mContentResolverAdapter;

    public ContactsAsyncHelper(ContentResolverAdapter contentResolverAdapter) {
        mContentResolverAdapter = contentResolverAdapter;
    }

    public ContactsAsyncHelper(ContentResolverAdapter contentResolverAdapter, Looper looper) {
        mContentResolverAdapter = contentResolverAdapter;
        mThreadHandler = new WorkerHandler(looper);
    }

    private static final class WorkerArgs {
        public Context context;
        public Uri displayPhotoUri;
        public Drawable photo;
        public Bitmap photoIcon;
        public Object cookie;
        public OnImageLoadCompleteListener listener;
    }

    /**
     * Thread worker class that handles the task of opening the stream and loading
     * the images.
     */
    private class WorkerHandler extends Handler {
        public WorkerHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            WorkerArgs args = (WorkerArgs) msg.obj;

            switch (msg.arg1) {
                case EVENT_LOAD_IMAGE:
                    InputStream inputStream = null;
                    try {
                        try {
                            inputStream = mContentResolverAdapter.openInputStream(
                                    args.context, args.displayPhotoUri);
                        } catch (Exception e) {
                            Log.e(this, e, "Error opening photo input stream");
                        }

                        if (inputStream != null) {
                            args.photo = Drawable.createFromStream(inputStream,
                                    args.displayPhotoUri.toString());

                            // This assumes Drawable coming from contact database is usually
                            // BitmapDrawable and thus we can have (down)scaled version of it.
                            args.photoIcon = getPhotoIconWhenAppropriate(args.context, args.photo);

                            Log.d(this, "Loading image: " + msg.arg1 +
                                    " token: " + msg.what + " image URI: " + args.displayPhotoUri);
                        } else {
                            args.photo = null;
                            args.photoIcon = null;
                            Log.d(this, "Problem with image: " + msg.arg1 +
                                    " token: " + msg.what + " image URI: " + args.displayPhotoUri +
                                    ", using default image.");
                        }
                    } finally {
                        if (inputStream != null) {
                            try {
                                inputStream.close();
                            } catch (IOException e) {
                                Log.e(this, e, "Unable to close input stream.");
                            }
                        }
                    }

                    // Listener will synchronize as needed
                    Log.d(this, "Notifying listener: " + args.listener.toString() +
                            " image: " + args.displayPhotoUri + " completed");
                    args.listener.onImageLoadComplete(msg.what, args.photo, args.photoIcon,
                                args.cookie);
                    break;
                default:
                    break;
            }
        }

        /**
         * Returns a Bitmap object suitable for {@link Notification}'s large icon. This might
         * return null when the given Drawable isn't BitmapDrawable, or if the system fails to
         * create a scaled Bitmap for the Drawable.
         */
        private Bitmap getPhotoIconWhenAppropriate(Context context, Drawable photo) {
            if (!(photo instanceof BitmapDrawable)) {
                return null;
            }
            int iconSize = context.getResources()
                    .getDimensionPixelSize(R.dimen.notification_icon_size);
            Bitmap orgBitmap = ((BitmapDrawable) photo).getBitmap();
            int orgWidth = orgBitmap.getWidth();
            int orgHeight = orgBitmap.getHeight();
            int longerEdge = orgWidth > orgHeight ? orgWidth : orgHeight;
            // We want downscaled one only when the original icon is too big.
            if (longerEdge > iconSize) {
                float ratio = ((float) longerEdge) / iconSize;
                int newWidth = (int) (orgWidth / ratio);
                int newHeight = (int) (orgHeight / ratio);
                // If the longer edge is much longer than the shorter edge, the latter may
                // become 0 which will cause a crash.
                if (newWidth <= 0 || newHeight <= 0) {
                    Log.w(this, "Photo icon's width or height become 0.");
                    return null;
                }

                // It is sure ratio >= 1.0f in any case and thus the newly created Bitmap
                // should be smaller than the original.
                return Bitmap.createScaledBitmap(orgBitmap, newWidth, newHeight, true);
            } else {
                return orgBitmap;
            }
        }
    }

    /**
     * Starts an asynchronous image load. After finishing the load,
     * {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)}
     * will be called.
     *
     * @param token Arbitrary integer which will be returned as the first argument of
     * {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)}
     * @param context Context object used to do the time-consuming operation.
     * @param displayPhotoUri Uri to be used to fetch the photo
     * @param listener Callback object which will be used when the asynchronous load is done.
     * Can be null, which means only the asynchronous load is done while there's no way to
     * obtain the loaded photos.
     * @param cookie Arbitrary object the caller wants to remember, which will become the
     * fourth argument of {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable,
     * Bitmap, Object)}. Can be null, at which the callback will also has null for the argument.
     */
    public void startObtainPhotoAsync(int token, Context context, Uri displayPhotoUri,
            OnImageLoadCompleteListener listener, Object cookie) {
        ensureAsyncHandlerStarted();

        // in case the source caller info is null, the URI will be null as well.
        // just update using the placeholder image in this case.
        if (displayPhotoUri == null) {
            Log.wtf(LOG_TAG, "Uri is missing");
            return;
        }

        // Added additional Cookie field in the callee to handle arguments
        // sent to the callback function.

        // setup arguments
        WorkerArgs args = new WorkerArgs();
        args.cookie = cookie;
        args.context = context;
        args.displayPhotoUri = displayPhotoUri;
        args.listener = listener;

        // setup message arguments
        Message msg = mThreadHandler.obtainMessage(token);
        msg.arg1 = EVENT_LOAD_IMAGE;
        msg.obj = args;

        Log.d(LOG_TAG, "Begin loading image: " + args.displayPhotoUri +
                ", displaying default image for now.");

        // notify the thread to begin working
        mThreadHandler.sendMessage(msg);
    }

    private void ensureAsyncHandlerStarted() {
        if (mThreadHandler == null) {
            HandlerThread thread = new HandlerThread("ContactsAsyncWorker");
            thread.start();
            mThreadHandler = new WorkerHandler(thread.getLooper());
        }
    }
}
