/*
 * Copyright (C) 2012 Google Inc.
 * Licensed to 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.mail;

import android.content.AsyncTaskLoader;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.content.Loader;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build.VERSION;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Contacts.Photo;
import android.provider.ContactsContract.Data;
import android.util.Pair;

import com.android.bitmap.util.Trace;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import java.util.Set;

/**
 * A {@link Loader} to look up presence, contact URI, and photo data for a set of email
 * addresses.
 */
public class SenderInfoLoader extends AsyncTaskLoader<ImmutableMap<String, ContactInfo>> {

    private static final String[] DATA_COLS = new String[] {
        Email._ID,                  // 0
        Email.DATA,                 // 1
        Email.CONTACT_ID,           // 2
        Email.PHOTO_ID,             // 3
    };
    private static final int DATA_EMAIL_COLUMN = 1;
    private static final int DATA_CONTACT_ID_COLUMN = 2;
    private static final int DATA_PHOTO_ID_COLUMN = 3;

    private static final String[] PHOTO_COLS = new String[] { Photo._ID, Photo.PHOTO };
    private static final int PHOTO_PHOTO_ID_COLUMN = 0;
    private static final int PHOTO_PHOTO_COLUMN = 1;

    /**
     * Limit the query params to avoid hitting the maximum of 99. We choose a number smaller than
     * 99 since the contacts provider may wrap our query in its own and insert more params.
     */
    private static final int MAX_QUERY_PARAMS = 75;

    private final Set<String> mSenders;

    public SenderInfoLoader(Context context, Set<String> senders) {
        super(context);
        mSenders = senders;
    }

    @Override
    protected void onStartLoading() {
        forceLoad();
    }

    @Override
    protected void onStopLoading() {
        cancelLoad();
    }

    @Override
    public ImmutableMap<String, ContactInfo> loadInBackground() {
        if (mSenders == null || mSenders.isEmpty()) {
            return null;
        }

        return loadContactPhotos(
                getContext().getContentResolver(), mSenders, true /* decodeBitmaps */);
    }

    /**
     * Loads contact photos from the ContentProvider.
     * @param resolver {@link ContentResolver} to use in queries to the ContentProvider.
     * @param emails The email addresses of the sender images to return.
     * @param decodeBitmaps If {@code true}, decode the bitmaps and put them into
     *                      {@link ContactInfo}. Otherwise, just put the raw bytes of the photo
     *                      into the {@link ContactInfo}.
     * @return A mapping of email to {@link ContactInfo}. How to interpret the map:
     * <ul>
     *     <li>The email is missing from the key set or maps to null - The email was skipped. Try
     *     again.</li>
     *     <li>Either {@link ContactInfo#photoBytes} or {@link ContactInfo#photo} is non-null -
     *     Photo loaded successfully.</li>
     *     <li>Both {@link ContactInfo#photoBytes} and {@link ContactInfo#photo} are null -
     *     Photo load failed.</li>
     * </ul>
     */
    public static ImmutableMap<String, ContactInfo> loadContactPhotos(
            final ContentResolver resolver, final Set<String> emails, final boolean decodeBitmaps) {
        Trace.beginSection("load contact photos util");
        Cursor cursor = null;

        Trace.beginSection("build first query");
        Map<String, ContactInfo> results = Maps.newHashMap();

        // temporary structures
        Map<Long, Pair<String, ContactInfo>> photoIdMap = Maps.newHashMap();
        ArrayList<String> photoIdsAsStrings = new ArrayList<String>();
        ArrayList<String> emailsList = getTruncatedQueryParams(emails);

        // Build first query
        StringBuilder query = new StringBuilder()
                .append(Data.MIMETYPE).append("='").append(Email.CONTENT_ITEM_TYPE)
                .append("' AND ").append(Email.DATA).append(" IN (");
        appendQuestionMarks(query, emailsList);
        query.append(')');
        Trace.endSection();

        // Contacts that are designed to be visible outside of search will be returned last.
        // Therefore, these contacts will be given precedence below, if possible.
        final String sortOrder = contactInfoSortOrder();

        try {
            Trace.beginSection("query 1");
            cursor = resolver.query(Data.CONTENT_URI, DATA_COLS,
                    query.toString(), toStringArray(emailsList), sortOrder);
            Trace.endSection();

            if (cursor == null) {
                Trace.endSection();
                return null;
            }

            Trace.beginSection("get photo id");
            int i = -1;
            while (cursor.moveToPosition(++i)) {
                String email = cursor.getString(DATA_EMAIL_COLUMN);
                long contactId = cursor.getLong(DATA_CONTACT_ID_COLUMN);
                Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);

                ContactInfo result = new ContactInfo(contactUri);

                if (!cursor.isNull(DATA_PHOTO_ID_COLUMN)) {
                    long photoId = cursor.getLong(DATA_PHOTO_ID_COLUMN);
                    photoIdsAsStrings.add(Long.toString(photoId));
                    photoIdMap.put(photoId, Pair.create(email, result));
                }
                results.put(email, result);
            }
            cursor.close();
            Trace.endSection();

            // Put empty ContactInfo for all the emails that didn't map to a contact.
            // This allows us to differentiate between lookup failed,
            // and lookup skipped (truncated above).
            for (String email : emailsList) {
                if (!results.containsKey(email)) {
                    results.put(email, new ContactInfo(null));
                }
            }

            if (photoIdsAsStrings.isEmpty()) {
                Trace.endSection();
                return ImmutableMap.copyOf(results);
            }

            Trace.beginSection("build second query");
            // Build second query: photoIDs->blobs
            // based on photo batch-select code in ContactPhotoManager
            photoIdsAsStrings = getTruncatedQueryParams(photoIdsAsStrings);
            query.setLength(0);
            query.append(Photo._ID).append(" IN (");
            appendQuestionMarks(query, photoIdsAsStrings);
            query.append(')');
            Trace.endSection();

            Trace.beginSection("query 2");
            cursor = resolver.query(Data.CONTENT_URI, PHOTO_COLS,
                    query.toString(), toStringArray(photoIdsAsStrings), sortOrder);
            Trace.endSection();

            if (cursor == null) {
                Trace.endSection();
                return ImmutableMap.copyOf(results);
            }

            Trace.beginSection("get photo blob");
            i = -1;
            while (cursor.moveToPosition(++i)) {
                byte[] photoBytes = cursor.getBlob(PHOTO_PHOTO_COLUMN);
                if (photoBytes == null) {
                    continue;
                }

                long photoId = cursor.getLong(PHOTO_PHOTO_ID_COLUMN);
                Pair<String, ContactInfo> prev = photoIdMap.get(photoId);
                String email = prev.first;
                ContactInfo prevResult = prev.second;

                if (decodeBitmaps) {
                    Trace.beginSection("decode bitmap");
                    Bitmap photo = BitmapFactory.decodeByteArray(photoBytes, 0, photoBytes.length);
                    Trace.endSection();
                    // overwrite existing photo-less result
                    results.put(email, new ContactInfo(prevResult.contactUri, photo));
                } else {
                    // overwrite existing photoBytes-less result
                    results.put(email, new ContactInfo(prevResult.contactUri, photoBytes));
                }
            }
            Trace.endSection();
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }

        Trace.endSection();
        return ImmutableMap.copyOf(results);
    }

    private static String contactInfoSortOrder() {
        // The ContactsContract.IN_DEFAULT_DIRECTORY does not exist prior to android L. There is
        // no VERSION.SDK_INT value assigned for android L yet. Therefore, we must gate the
        // following logic on the development codename.
        // TODO: use VERSION.SDK_INT once VERSION.SDK_INT is increased for L.
        if (VERSION.CODENAME.startsWith("L")) {
            return "in_default_directory ASC, " + Data._ID;
        }
        return null;
    }

    private static ArrayList<String> getTruncatedQueryParams(Collection<String> params) {
        int truncatedLen = Math.min(params.size(), MAX_QUERY_PARAMS);
        ArrayList<String> truncated = new ArrayList<String>(truncatedLen);

        int copied = 0;
        for (String param : params) {
            truncated.add(param);
            copied++;
            if (copied >= truncatedLen) {
                break;
            }
        }

        return truncated;
    }

    private static String[] toStringArray(Collection<String> items) {
        return items.toArray(new String[items.size()]);
    }

    private static void appendQuestionMarks(StringBuilder query, Iterable<?> items) {
        boolean first = true;
        for (Object item : items) {
            if (first) {
                first = false;
            } else {
                query.append(',');
            }
            query.append('?');
        }
    }

}
