/**
 * Copyright (C) 2010-2012 Regis Montoya (aka r3gis - www.r3gis.fr)
 * This file is part of CSipSimple.
 *
 *  CSipSimple is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *  If you own a pjsip commercial license you can also redistribute it
 *  and/or modify it under the terms of the GNU Lesser General Public License
 *  as an android library.
 *
 *  CSipSimple is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with CSipSimple.  If not, see <http://www.gnu.org/licenses/>.
 */
/**
 * This file contains relicensed code from Apache copyright of 
 * Copyright (C) 2008 The Android Open Source Project
 */

package com.csipsimple.utils;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.support.v4.util.LruCache;
import android.text.TextUtils;
import android.view.View;
import android.widget.ImageView;

import com.csipsimple.R;
import com.csipsimple.models.CallerInfo;
import com.csipsimple.utils.contacts.ContactsWrapper;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;

public class ContactsAsyncHelper extends Handler {
    private static final String THIS_FILE = "ContactsAsyncHelper";
    
    // TODO : use LRUCache for bitmaps.
    
    LruCache<Uri, Bitmap> photoCache = new LruCache<Uri, Bitmap>(5 * 1024 * 1024 /* 5MiB */) {
        protected int sizeOf(Uri key, Bitmap value) {
            return value.getRowBytes() * value.getWidth();
        }
    };

    /**
     * Interface for a WorkerHandler result return.
     */
    public interface OnImageLoadCompleteListener {
        /**
         * Called when the image load is complete.
         * 
         * @param imagePresent true if an image was found
         */
        public void onImageLoadComplete(int token, Object cookie, ImageView iView,
                                        boolean imagePresent);
    }

    // constants
    private static final int EVENT_LOAD_IMAGE = 1;
    private static final int EVENT_LOAD_IMAGE_URI = 2;
    private static final int EVENT_LOAD_CONTACT_URI = 3;
    private static final int DEFAULT_TOKEN = -1;
    private static final int TAG_PHOTO_INFOS = R.id.icon;
    private static ContactsWrapper contactsWrapper;

    // static objects
    private static Handler sThreadHandler;

    private static final class WorkerArgs {
        public Context context;
        public ImageView view;
        public int defaultResource;
        public Object result;
        public Uri loadedUri;
        public Object cookie;
        public OnImageLoadCompleteListener listener;
    }

    private static class PhotoViewTag {
        public Uri uri;
    }

    public static final String HIGH_RES_URI_PARAM = "hiRes";
    /**
     * 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);
        }

        public void handleMessage(Message msg) {
            WorkerArgs args = (WorkerArgs) msg.obj;
            Uri uri = null;
            if (msg.arg1 == EVENT_LOAD_IMAGE) {
                PhotoViewTag photoTag = (PhotoViewTag) args.view.getTag(TAG_PHOTO_INFOS);
                if (photoTag != null && photoTag.uri != null) {
                    uri = photoTag.uri;
                    boolean hiRes = false;
                    String p = uri.getQueryParameter(HIGH_RES_URI_PARAM);
                    if(!TextUtils.isEmpty(p) && p.equalsIgnoreCase("1")) {
                        hiRes = true;
                    }
                    Log.v(THIS_FILE, "get : " + uri);
                    Bitmap img = null;
                    synchronized (photoCache) {
                        img = photoCache.get(uri);
                    }
                    if(img == null) {
                        img = contactsWrapper.getContactPhoto(args.context, uri, hiRes,
                                args.defaultResource);
                        synchronized (photoCache) {
                            photoCache.put(uri, img);
                        }
                    }
                    if (img != null) {
                        args.result = img;
                    } else {
                        args.result = null;
                    }
                }
            } else if (msg.arg1 == EVENT_LOAD_IMAGE_URI || msg.arg1 == EVENT_LOAD_CONTACT_URI) {
                PhotoViewTag photoTag = (PhotoViewTag) args.view.getTag(TAG_PHOTO_INFOS);
                if (photoTag != null && photoTag.uri != null) {
                    uri = photoTag.uri;
                    Log.v(THIS_FILE, "get : " + uri);

                    Bitmap img = null;

                    synchronized (photoCache) {
                        img = photoCache.get(uri);
                    }
                    if (img == null) {

                        if (msg.arg1 == EVENT_LOAD_IMAGE_URI) {

                            try {
                                byte[] buffer = new byte[1024 * 16];
                                InputStream is = args.context.getContentResolver().openInputStream(
                                        uri);
                                if (is != null) {
                                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
                                    try {
                                        int size;
                                        while ((size = is.read(buffer)) != -1) {
                                            baos.write(buffer, 0, size);
                                        }
                                    } finally {
                                        is.close();
                                    }
                                    byte[] boasBytes = baos.toByteArray();
                                    img = BitmapFactory.decodeByteArray(boasBytes, 0,
                                            boasBytes.length,
                                            null);
                                }

                            } catch (Exception ex) {
                                Log.v(THIS_FILE, "Cannot load photo " + uri, ex);
                            }

                        } else if (msg.arg1 == EVENT_LOAD_CONTACT_URI) {
                            img = ContactsWrapper.getInstance().getContactPhoto(args.context, uri, false, null);
                        }
                    }
                    
                    if (img != null) {
                        args.result = img;
                        synchronized (photoCache) {
                            photoCache.put(uri, img);
                        }
                    } else {
                        args.result = null;
                    }
                    
                }
            }
            args.loadedUri = uri;

            // send the reply to the enclosing class.
            Message reply = ContactsAsyncHelper.this.obtainMessage(msg.what);
            reply.arg1 = msg.arg1;
            reply.obj = msg.obj;
            reply.sendToTarget();
        }
    }

    /**
     * Private constructor for static class
     */
    private ContactsAsyncHelper() {
        HandlerThread thread = new HandlerThread("ContactsAsyncWorker");
        thread.start();
        sThreadHandler = new WorkerHandler(thread.getLooper());
        contactsWrapper = ContactsWrapper.getInstance();
    }

    /**
     * Convenience method for calls that do not want to deal with listeners and
     * tokens.
     */
    public static final void updateImageViewWithContactPhotoAsync(Context context,
            ImageView imageView, CallerInfo person, int placeholderImageResource) {
        // Added additional Cookie field in the callee.
        updateImageViewWithContactPhotoAsync(DEFAULT_TOKEN, null, null, context,
                imageView, person, placeholderImageResource);
    }

    /**
     * Start an image load, attach the result to the specified CallerInfo
     * object. Note, when the query is started, we make the ImageView INVISIBLE
     * if the placeholderImageResource value is -1. When we're given a valid (!=
     * -1) placeholderImageResource value, we make sure the image is visible.
     */
    public static final void updateImageViewWithContactPhotoAsync(int token,
            OnImageLoadCompleteListener listener, Object cookie, Context context,
            ImageView imageView, CallerInfo callerInfo, int placeholderImageResource) {
        if (sThreadHandler == null) {
            new ContactsAsyncHelper();
        }

        // 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 (callerInfo == null || callerInfo.contactContentUri == null) {
            defaultImage(imageView, placeholderImageResource);
            return;
        }

        // Check that the view is not already loading for same uri
        if (isAlreadyProcessed(imageView, callerInfo.contactContentUri)) {
            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.view = imageView;
        PhotoViewTag photoTag = new PhotoViewTag();
        photoTag.uri = callerInfo.contactContentUri;
        args.view.setTag(TAG_PHOTO_INFOS, photoTag);
        args.defaultResource = placeholderImageResource;
        args.listener = listener;

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

        preloadImage(imageView, placeholderImageResource, msg);
    }

    public static void updateImageViewWithContactPhotoAsync(Context context, ImageView imageView,
            Uri photoUri, int placeholderImageResource) {
       updateImageViewWithUriAsync(context, imageView, photoUri, placeholderImageResource, EVENT_LOAD_IMAGE_URI);
    }
    
    public static void updateImageViewWithContactAsync(Context context, ImageView imageView,
            Uri contactUri, int placeholderImageResource) {
       updateImageViewWithUriAsync(context, imageView, contactUri, placeholderImageResource, EVENT_LOAD_CONTACT_URI);
    }

    private static void updateImageViewWithUriAsync(Context context, ImageView imageView,
            Uri photoUri, int placeholderImageResource, int eventType) {
        if (sThreadHandler == null) {
            Log.v(THIS_FILE, "Update image view with contact async");
            new ContactsAsyncHelper();
        }

        // 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 (photoUri == null) {
            defaultImage(imageView, placeholderImageResource);
            return;
        }
        if (isAlreadyProcessed(imageView, photoUri)) {
            return;
        }

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

        // setup arguments
        WorkerArgs args = new WorkerArgs();
        args.context = context;
        args.view = imageView;
        PhotoViewTag photoTag = new PhotoViewTag();
        photoTag.uri = photoUri;
        args.view.setTag(TAG_PHOTO_INFOS, photoTag);
        args.defaultResource = placeholderImageResource;

        // setup message arguments
        Message msg = sThreadHandler.obtainMessage();
        msg.arg1 = eventType;
        msg.obj = args;

        preloadImage(imageView, placeholderImageResource, msg);
    }

    private static void defaultImage(ImageView imageView, int placeholderImageResource) {
        Log.v(THIS_FILE, "No uri, just display placeholder.");
        PhotoViewTag photoTag = new PhotoViewTag();
        photoTag.uri = null;
        imageView.setTag(TAG_PHOTO_INFOS, photoTag);
        imageView.setVisibility(View.VISIBLE);
        imageView.setImageResource(placeholderImageResource);
    }

    private static void preloadImage(ImageView imageView, int placeholderImageResource, Message msg) {
        // set the default image first, when the query is complete, we will
        // replace the image with the correct one.
        if (placeholderImageResource != -1) {
            imageView.setVisibility(View.VISIBLE);
            imageView.setImageResource(placeholderImageResource);
        } else {
            imageView.setVisibility(View.INVISIBLE);
        }

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

    private static boolean isAlreadyProcessed(ImageView imageView, Uri uri) {
        if(imageView != null) {
            PhotoViewTag vt = (PhotoViewTag) imageView.getTag(TAG_PHOTO_INFOS);
            return (vt != null && UriUtils.areEqual(uri, vt.uri));
        }
        return true;
    }

    /**
     * Called when loading is done.
     */
    @Override
    public void handleMessage(Message msg) {
        WorkerArgs args = (WorkerArgs) msg.obj;
        if (msg.arg1 == EVENT_LOAD_IMAGE || msg.arg1 == EVENT_LOAD_IMAGE_URI || msg.arg1 == EVENT_LOAD_CONTACT_URI) {
            boolean imagePresent = false;
            // Sanity check on image view
            PhotoViewTag photoTag = (PhotoViewTag) args.view.getTag(TAG_PHOTO_INFOS);
            if (photoTag == null) {
                Log.w(THIS_FILE, "Tag has been removed meanwhile");
                return;
            }
            if (!UriUtils.areEqual(args.loadedUri, photoTag.uri)) {
                Log.w(THIS_FILE, "Image view has changed uri meanwhile");
                return;
            }

            // if the image has been loaded then display it, otherwise set
            // default.
            // in either case, make sure the image is visible.
            if (args.result != null) {
                args.view.setVisibility(View.VISIBLE);
                args.view.setImageBitmap((Bitmap) args.result);
                imagePresent = true;
            } else if (args.defaultResource != -1) {
                args.view.setVisibility(View.VISIBLE);
                args.view.setImageResource(args.defaultResource);
            }
            // notify the listener if it is there.
            if (args.listener != null) {
                Log.v(THIS_FILE, "Notifying listener: " + args.listener.toString() +
                        " image: " + args.loadedUri + " completed");
                args.listener.onImageLoadComplete(msg.what, args.cookie, args.view,
                        imagePresent);
            }
        }
    }

}