/*
 * Copyright (C) 2011 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.ex.chips;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import android.accounts.Account;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.Contacts;
import android.text.TextUtils;
import android.text.util.Rfc822Token;
import android.text.util.Rfc822Tokenizer;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CursorAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import com.android.ex.chips.BaseRecipientAdapter.DirectoryListQuery;
import com.android.ex.chips.BaseRecipientAdapter.DirectorySearchParams;
import com.android.ex.chips.Queries.Query;

/**
 * RecipientAlternatesAdapter backs the RecipientEditTextView for managing contacts queried by email or by phone number.
 */
public class RecipientAlternatesAdapter extends CursorAdapter
  {
  static final int                           MAX_LOOKUPS          =50;
  private final LayoutInflater               mLayoutInflater;
  private final long                         mCurrentId;
  private int                                mCheckedItemPosition =-1;
  private final OnCheckedItemChangedListener mCheckedItemChangedListener;
  private static final String                TAG                  ="RecipAlternates";
  public static final int                    QUERY_TYPE_EMAIL     =0;
  public static final int                    QUERY_TYPE_PHONE     =1;
  private Query                              mQuery;

  public interface RecipientMatchCallback
    {
    public void matchesFound(Map<String,RecipientEntry> results);

    /**
     * Called with all addresses that could not be resolved to valid recipients.
     */
    public void matchesNotFound(Set<String> unfoundAddresses);
    }

  public static void getMatchingRecipients(final Context context,final BaseRecipientAdapter adapter,final ArrayList<String> inAddresses,final Account account,final RecipientMatchCallback callback)
    {
    getMatchingRecipients(context,adapter,inAddresses,QUERY_TYPE_EMAIL,account,callback);
    }

  /**
   * Get a HashMap of address to RecipientEntry that contains all contact information for a contact with the provided
   * address, if one exists. This may block the UI, so run it in an async task.
   *
   * @param context
   * Context.
   * @param inAddresses
   * Array of addresses on which to perform the lookup.
   * @param callback
   * RecipientMatchCallback called when a match or matches are found.
   * @return HashMap<String,RecipientEntry>
   */
  public static void getMatchingRecipients(final Context context,final BaseRecipientAdapter adapter,final ArrayList<String> inAddresses,final int addressType,final Account account,final RecipientMatchCallback callback)
    {
    Queries.Query query;
    if(addressType==QUERY_TYPE_EMAIL)
      query=Queries.EMAIL;
    else query=Queries.PHONE;
    final int addressesSize=Math.min(MAX_LOOKUPS,inAddresses.size());
    final HashSet<String> addresses=new HashSet<String>();
    final StringBuilder bindString=new StringBuilder();
    // Create the "?" string and set up arguments.
    for(int i=0;i<addressesSize;i++)
      {
      final Rfc822Token[] tokens=Rfc822Tokenizer.tokenize(inAddresses.get(i).toLowerCase(Locale.getDefault()));
      addresses.add(tokens.length>0 ? tokens[0].getAddress() : inAddresses.get(i));
      bindString.append("?");
      if(i<addressesSize-1)
        bindString.append(",");
      }
    if(Log.isLoggable(TAG,Log.DEBUG))
      Log.d(TAG,"Doing reverse lookup for "+addresses.toString());
    final String[] addressArray=new String[addresses.size()];
    addresses.toArray(addressArray);
    HashMap<String,RecipientEntry> recipientEntries=null;
    Cursor c=null;
    try
      {
      c=context.getContentResolver().query(query.getContentUri(),query.getProjection(),query.getProjection()[Queries.Query.DESTINATION]+" IN ("+bindString.toString()+")",addressArray,null);
      recipientEntries=processContactEntries(c);
      callback.matchesFound(recipientEntries);
      }
    finally
      {
      if(c!=null)
        c.close();
      }
    // See if any entries did not resolve; if so, we need to check other
    // directories
    final Set<String> matchesNotFound=new HashSet<String>();
    if(recipientEntries.size()<addresses.size())
      {
      final List<DirectorySearchParams> paramsList;
      Cursor directoryCursor=null;
      try
        {
        directoryCursor=context.getContentResolver().query(DirectoryListQuery.URI,DirectoryListQuery.PROJECTION,null,null,null);
        if(directoryCursor==null)
          paramsList=null;
        else paramsList=BaseRecipientAdapter.setupOtherDirectories(context,directoryCursor,account);
        }
      finally
        {
        if(directoryCursor!=null)
          directoryCursor.close();
        }
      // Run a directory query for each unmatched recipient.
      final HashSet<String> unresolvedAddresses=new HashSet<String>();
      for(final String address : addresses)
        if(!recipientEntries.containsKey(address))
          unresolvedAddresses.add(address);
      matchesNotFound.addAll(unresolvedAddresses);
      if(paramsList!=null)
        {
        Cursor directoryContactsCursor=null;
        for(final String unresolvedAddress : unresolvedAddresses)
          for(int i=0;i<paramsList.size();i++)
            try
              {
              directoryContactsCursor=doQuery(unresolvedAddress,1,paramsList.get(i).directoryId,account,context.getContentResolver(),query);
              }
            finally
              {
              if(directoryContactsCursor!=null&&directoryContactsCursor.getCount()==0)
                {
                directoryContactsCursor.close();
                directoryContactsCursor=null;
                }
              else break;
              }
        }
      }
    // If no matches found in contact provider or the directories, try the extension
    // matcher.
    // todo (aalbert): This whole method needs to be in the adapter?
    if(adapter!=null)
      {
      final Map<String,RecipientEntry> entries=adapter.getMatchingRecipients(matchesNotFound);
      if(entries!=null&&entries.size()>0)
        {
        callback.matchesFound(entries);
        for(final String address : entries.keySet())
          matchesNotFound.remove(address);
        }
      }
    callback.matchesNotFound(matchesNotFound);
    }

  private static HashMap<String,RecipientEntry> processContactEntries(final Cursor c)
    {
    final HashMap<String,RecipientEntry> recipientEntries=new HashMap<String,RecipientEntry>();
    if(c!=null&&c.moveToFirst())
      do
        {
        final String address=c.getString(Queries.Query.DESTINATION);
        final RecipientEntry newRecipientEntry=RecipientEntry.constructTopLevelEntry(c.getString(Queries.Query.NAME),c.getInt(Queries.Query.DISPLAY_NAME_SOURCE),c.getString(Queries.Query.DESTINATION),c.getInt(Queries.Query.DESTINATION_TYPE),c.getString(Queries.Query.DESTINATION_LABEL),c.getLong(Queries.Query.CONTACT_ID),c.getLong(Queries.Query.DATA_ID),c.getString(Queries.Query.PHOTO_THUMBNAIL_URI),true,false /*
                                                                                                                                                                                                                                                                                                                                                                                                                              * isGalContact
                                                                                                                                                                                                                                                                                                                                                                                                                              * TODO(skennedy) We should
                                                                                                                                                                                                                                                                                                                                                                                                                              * look these up eventually
                                                                                                                                                                                                                                                                                                                                                                                                                              */);
        /*
         * In certain situations, we may have two results for one address, where one of the results is just the
         * email address, and the other has a name and photo, so we want to use the better one.
         */
        final RecipientEntry recipientEntry=getBetterRecipient(recipientEntries.get(address),newRecipientEntry);
        recipientEntries.put(address,recipientEntry);
        if(Log.isLoggable(TAG,Log.DEBUG))
          Log.d(TAG,"Received reverse look up information for "+address+" RESULTS: "+" NAME : "+c.getString(Queries.Query.NAME)+" CONTACT ID : "+c.getLong(Queries.Query.CONTACT_ID)+" ADDRESS :"+c.getString(Queries.Query.DESTINATION));
        }
      while(c.moveToNext());
    return recipientEntries;
    }

  /**
   * Given two {@link RecipientEntry}s for the same email address, this will return the one that contains more
   * complete information for display purposes. Defaults to <code>entry2</code> if no significant differences are
   * found.
   */
  static RecipientEntry getBetterRecipient(final RecipientEntry entry1,final RecipientEntry entry2)
    {
    // If only one has passed in, use it
    if(entry2==null)
      return entry1;
    if(entry1==null)
      return entry2;
    // If only one has a display name, use it
    if(!TextUtils.isEmpty(entry1.getDisplayName())&&TextUtils.isEmpty(entry2.getDisplayName()))
      return entry1;
    if(!TextUtils.isEmpty(entry2.getDisplayName())&&TextUtils.isEmpty(entry1.getDisplayName()))
      return entry2;
    // If only one has a display name that is not the same as the destination, use it
    if(!TextUtils.equals(entry1.getDisplayName(),entry1.getDestination())&&TextUtils.equals(entry2.getDisplayName(),entry2.getDestination()))
      return entry1;
    if(!TextUtils.equals(entry2.getDisplayName(),entry2.getDestination())&&TextUtils.equals(entry1.getDisplayName(),entry1.getDestination()))
      return entry2;
    // If only one has a photo, use it
    if((entry1.getPhotoThumbnailUri()!=null||entry1.getPhotoBytes()!=null)&&entry2.getPhotoThumbnailUri()==null&&entry2.getPhotoBytes()==null)
      return entry1;
    if((entry2.getPhotoThumbnailUri()!=null||entry2.getPhotoBytes()!=null)&&entry1.getPhotoThumbnailUri()==null&&entry1.getPhotoBytes()==null)
      return entry2;
    // Go with the second option as a default
    return entry2;
    }

  private static Cursor doQuery(final CharSequence constraint,final int limit,final Long directoryId,final Account account,final ContentResolver resolver,final Query query)
    {
    String constraintStr=constraint.toString();
    final Uri.Builder builder;
    String selection=null;
    String[] selectionArgs=null;
    if(query!=Queries.PHONE)
      builder=query.getContentFilterUri().buildUpon().appendPath(constraintStr).appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,String.valueOf(limit+BaseRecipientAdapter.ALLOWANCE_FOR_DUPLICATES));
    else
      {
      builder=query.getContentUri().buildUpon().appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,String.valueOf(limit+BaseRecipientAdapter.ALLOWANCE_FOR_DUPLICATES));
      selection=Contacts.DISPLAY_NAME+" LIKE ? OR "+Phone.NUMBER+" LIKE ?";
      constraintStr="%"+constraintStr+"%";
      selectionArgs=new String[] {constraintStr,constraintStr};
      }
    if(directoryId!=null)
      builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,String.valueOf(directoryId));
    if(account!=null)
      {
      builder.appendQueryParameter(BaseRecipientAdapter.PRIMARY_ACCOUNT_NAME,account.name);
      builder.appendQueryParameter(BaseRecipientAdapter.PRIMARY_ACCOUNT_TYPE,account.type);
      }
    // final long start = System.currentTimeMillis();
    final Uri uri=builder.build();
    final Cursor cursor=resolver.query(uri,query.getProjection(),selection,selectionArgs,null);
    // final long end = System.currentTimeMillis();
    // if (DEBUG) {
    // Log.d(TAG, "Time for autocomplete (query: " + constraint + ", directoryId: " + directoryId
    // + ", num_of_results: " + (cursor != null ? cursor.getCount() : "null") + "): " + (end - start)
    // + " ms");
    // }
    return cursor;
    }

  public RecipientAlternatesAdapter(final Context context,final long contactId,final long currentId,final OnCheckedItemChangedListener listener)
    {
    this(context,contactId,currentId,QUERY_TYPE_EMAIL,listener);
    }

  public RecipientAlternatesAdapter(final Context context,final long contactId,final long currentId,//
      final int queryMode,final OnCheckedItemChangedListener listener)
    {
    super(context,getCursorForConstruction(context,contactId,queryMode),0);
    mLayoutInflater=LayoutInflater.from(context);
    mCurrentId=currentId;
    mCheckedItemChangedListener=listener;
    if(queryMode==QUERY_TYPE_EMAIL)
      mQuery=Queries.EMAIL;
    else if(queryMode==QUERY_TYPE_PHONE)
      mQuery=Queries.PHONE;
    else
      {
      mQuery=Queries.EMAIL;
      Log.e(TAG,"Unsupported query type: "+queryMode);
      }
    }

  public RecipientAlternatesAdapter(final Context context,final Cursor c,final long currentId,final int queryMode,final OnCheckedItemChangedListener listener)
    {
    super(context,c,0);
    mLayoutInflater=LayoutInflater.from(context);
    mCurrentId=currentId;
    mCheckedItemChangedListener=listener;
    if(queryMode==QUERY_TYPE_EMAIL)
      mQuery=Queries.EMAIL;
    else if(queryMode==QUERY_TYPE_PHONE)
      mQuery=Queries.PHONE;
    else
      {
      mQuery=Queries.EMAIL;
      Log.e(TAG,"Unsupported query type: "+queryMode);
      }
    }

  protected static Cursor getCursorForConstruction(final Context context,final long contactId,final int queryType)
    {
    final Cursor cursor;
    if(queryType==QUERY_TYPE_EMAIL)
      cursor=context.getContentResolver().query(Queries.EMAIL.getContentUri(),Queries.EMAIL.getProjection(),Queries.EMAIL.getProjection()[Queries.Query.CONTACT_ID]+" =?",new String[] {String.valueOf(contactId)},null);
    else cursor=context.getContentResolver().query(Queries.PHONE.getContentUri(),Queries.PHONE.getProjection(),Queries.PHONE.getProjection()[Queries.Query.CONTACT_ID]+" =?",new String[] {String.valueOf(contactId)},null);
    return removeDuplicateDestinations(cursor);
    }

  /**
   * @return a new cursor based on the given cursor with all duplicate destinations removed.
   * It's only intended to use for the alternate list, so... - This method ignores all other fields and dedupe
   * solely on the destination. Normally, if a cursor contains multiple contacts and they have the same
   * destination, we'd still want to show both. - This method creates a MatrixCursor, so all data will be kept
   * in memory. We wouldn't want to do this if the original cursor is large, but it's okay here because the
   * alternate list won't be that big.
   */
  // Visible for testing
  /* package */static Cursor removeDuplicateDestinations(final Cursor original)
    {
    final MatrixCursor result=new MatrixCursor(original.getColumnNames(),original.getCount());
    final HashSet<String> destinationsSeen=new HashSet<String>();
    original.moveToPosition(-1);
    while(original.moveToNext())
      {
      final String destination=original.getString(Query.DESTINATION);
      if(destinationsSeen.contains(destination))
        continue;
      destinationsSeen.add(destination);
      result.addRow(new Object[] {original.getString(Query.NAME),original.getString(Query.DESTINATION),original.getInt(Query.DESTINATION_TYPE),original.getString(Query.DESTINATION_LABEL),original.getLong(Query.CONTACT_ID),original.getLong(Query.DATA_ID),original.getString(Query.PHOTO_THUMBNAIL_URI),original.getInt(Query.DISPLAY_NAME_SOURCE)});
      }
    return result;
    }

  @Override
  public long getItemId(final int position)
    {
    final Cursor c=getCursor();
    if(c.moveToPosition(position))
      c.getLong(Queries.Query.DATA_ID);
    return -1;
    }

  public RecipientEntry getRecipientEntry(final int position)
    {
    final Cursor c=getCursor();
    c.moveToPosition(position);
    return RecipientEntry.constructTopLevelEntry(c.getString(Queries.Query.NAME),c.getInt(Queries.Query.DISPLAY_NAME_SOURCE),c.getString(Queries.Query.DESTINATION),c.getInt(Queries.Query.DESTINATION_TYPE),c.getString(Queries.Query.DESTINATION_LABEL),c.getLong(Queries.Query.CONTACT_ID),c.getLong(Queries.Query.DATA_ID),c.getString(Queries.Query.PHOTO_THUMBNAIL_URI),true,false /*
                                                                                                                                                                                                                                                                                                                                                                                          * isGalContact TODO(skennedy) We should
                                                                                                                                                                                                                                                                                                                                                                                          * look these up eventually
                                                                                                                                                                                                                                                                                                                                                                                          */);
    }

  @Override
  public View getView(final int position,View convertView,final ViewGroup parent)
    {
    final Cursor cursor=getCursor();
    cursor.moveToPosition(position);
    if(convertView==null)
      convertView=newView();
    if(cursor.getLong(Queries.Query.DATA_ID)==mCurrentId)
      {
      mCheckedItemPosition=position;
      if(mCheckedItemChangedListener!=null)
        mCheckedItemChangedListener.onCheckedItemChanged(mCheckedItemPosition);
      }
    bindView(convertView,convertView.getContext(),cursor);
    return convertView;
    }

  // TODO: this is VERY similar to the BaseRecipientAdapter. Can we combine
  // somehow?
  @Override
  public void bindView(final View view,final Context context,final Cursor cursor)
    {
    final int position=cursor.getPosition();
    final TextView display=(TextView)view.findViewById(android.R.id.title);
    final ImageView imageView=(ImageView)view.findViewById(android.R.id.icon);
    final RecipientEntry entry=getRecipientEntry(position);
    if(position==0)
      {
      display.setText(cursor.getString(Queries.Query.NAME));
      display.setVisibility(View.VISIBLE);
      // TODO: see if this needs to be done outside the main thread
      // as it may be too slow to get immediately.
      imageView.setImageURI(entry.getPhotoThumbnailUri());
      imageView.setVisibility(View.VISIBLE);
      }
    else
      {
      display.setVisibility(View.GONE);
      imageView.setVisibility(View.GONE);
      }
    final TextView destination=(TextView)view.findViewById(android.R.id.text1);
    destination.setText(cursor.getString(Queries.Query.DESTINATION));
    final TextView destinationType=(TextView)view.findViewById(android.R.id.text2);
    if(destinationType!=null)
      destinationType.setText(mQuery.getTypeLabel(context.getResources(),cursor.getInt(Queries.Query.DESTINATION_TYPE),cursor.getString(Queries.Query.DESTINATION_LABEL)).toString().toUpperCase(Locale.getDefault()));
    }

  @Override
  public View newView(final Context context,final Cursor cursor,final ViewGroup parent)
    {
    return newView();
    }

  private View newView()
    {
    return mLayoutInflater.inflate(R.layout.chips_recipient_dropdown_item,null);
    }

  /* package */static interface OnCheckedItemChangedListener
    {
    public void onCheckedItemChanged(int position);
    }
  }