/*
 * Copyright (C) 2010 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.contacts.common;

import android.content.ComponentCallbacks2;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.net.Uri.Builder;
import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.view.View;
import android.widget.ImageView;
import android.widget.QuickContactBadge;
import com.android.contacts.common.lettertiles.LetterTileDrawable;
import com.android.contacts.common.util.UriUtils;
import com.android.dialer.common.LogUtil;
import com.android.dialer.util.PermissionsUtil;

/** Asynchronously loads contact photos and maintains a cache of photos. */
public abstract class ContactPhotoManager implements ComponentCallbacks2 {

  /** Scale and offset default constants used for default letter images */
  public static final float SCALE_DEFAULT = 1.0f;

  public static final float OFFSET_DEFAULT = 0.0f;
  public static final boolean IS_CIRCULAR_DEFAULT = false;
  // TODO: Use LogUtil.isVerboseEnabled for DEBUG branches instead of a lint check.
  // LINT.DoNotSubmitIf(true)
  static final boolean DEBUG = false;
  // LINT.DoNotSubmitIf(true)
  static final boolean DEBUG_SIZES = false;
  /** Uri-related constants used for default letter images */
  private static final String DISPLAY_NAME_PARAM_KEY = "display_name";

  private static final String IDENTIFIER_PARAM_KEY = "identifier";
  private static final String CONTACT_TYPE_PARAM_KEY = "contact_type";
  private static final String SCALE_PARAM_KEY = "scale";
  private static final String OFFSET_PARAM_KEY = "offset";
  private static final String IS_CIRCULAR_PARAM_KEY = "is_circular";
  private static final String DEFAULT_IMAGE_URI_SCHEME = "defaultimage";
  private static final Uri DEFAULT_IMAGE_URI = Uri.parse(DEFAULT_IMAGE_URI_SCHEME + "://");
  public static final DefaultImageProvider DEFAULT_AVATAR = new LetterTileDefaultImageProvider();
  private static ContactPhotoManager sInstance;

  /**
   * Given a {@link DefaultImageRequest}, returns an Uri that can be used to request a letter tile
   * avatar when passed to the {@link ContactPhotoManager}. The internal implementation of this uri
   * is not guaranteed to remain the same across application versions, so the actual uri should
   * never be persisted in long-term storage and reused.
   *
   * @param request A {@link DefaultImageRequest} object with the fields configured to return a
   * @return A Uri that when later passed to the {@link ContactPhotoManager} via {@link
   *     #loadPhoto(ImageView, Uri, int, boolean, boolean, DefaultImageRequest)}, can be used to
   *     request a default contact image, drawn as a letter tile using the parameters as configured
   *     in the provided {@link DefaultImageRequest}
   */
  public static Uri getDefaultAvatarUriForContact(DefaultImageRequest request) {
    final Builder builder = DEFAULT_IMAGE_URI.buildUpon();
    if (request != null) {
      if (!TextUtils.isEmpty(request.displayName)) {
        builder.appendQueryParameter(DISPLAY_NAME_PARAM_KEY, request.displayName);
      }
      if (!TextUtils.isEmpty(request.identifier)) {
        builder.appendQueryParameter(IDENTIFIER_PARAM_KEY, request.identifier);
      }
      if (request.contactType != LetterTileDrawable.TYPE_DEFAULT) {
        builder.appendQueryParameter(CONTACT_TYPE_PARAM_KEY, String.valueOf(request.contactType));
      }
      if (request.scale != SCALE_DEFAULT) {
        builder.appendQueryParameter(SCALE_PARAM_KEY, String.valueOf(request.scale));
      }
      if (request.offset != OFFSET_DEFAULT) {
        builder.appendQueryParameter(OFFSET_PARAM_KEY, String.valueOf(request.offset));
      }
      if (request.isCircular != IS_CIRCULAR_DEFAULT) {
        builder.appendQueryParameter(IS_CIRCULAR_PARAM_KEY, String.valueOf(request.isCircular));
      }
    }
    return builder.build();
  }

  /**
   * Adds a business contact type encoded fragment to the URL. Used to ensure photo URLS from Nearby
   * Places can be identified as business photo URLs rather than URLs for personal contact photos.
   *
   * @param photoUrl The photo URL to modify.
   * @return URL with the contact type parameter added and set to TYPE_BUSINESS.
   */
  public static String appendBusinessContactType(String photoUrl) {
    Uri uri = Uri.parse(photoUrl);
    Builder builder = uri.buildUpon();
    builder.encodedFragment(String.valueOf(LetterTileDrawable.TYPE_BUSINESS));
    return builder.build().toString();
  }

  /**
   * Removes the contact type information stored in the photo URI encoded fragment.
   *
   * @param photoUri The photo URI to remove the contact type from.
   * @return The photo URI with contact type removed.
   */
  public static Uri removeContactType(Uri photoUri) {
    String encodedFragment = photoUri.getEncodedFragment();
    if (!TextUtils.isEmpty(encodedFragment)) {
      Builder builder = photoUri.buildUpon();
      builder.encodedFragment(null);
      return builder.build();
    }
    return photoUri;
  }

  /**
   * Inspects a photo URI to determine if the photo URI represents a business.
   *
   * @param photoUri The URI to inspect.
   * @return Whether the URI represents a business photo or not.
   */
  public static boolean isBusinessContactUri(Uri photoUri) {
    if (photoUri == null) {
      return false;
    }

    String encodedFragment = photoUri.getEncodedFragment();
    return !TextUtils.isEmpty(encodedFragment)
        && encodedFragment.equals(String.valueOf(LetterTileDrawable.TYPE_BUSINESS));
  }

  protected static DefaultImageRequest getDefaultImageRequestFromUri(Uri uri) {
    final DefaultImageRequest request =
        new DefaultImageRequest(
            uri.getQueryParameter(DISPLAY_NAME_PARAM_KEY),
            uri.getQueryParameter(IDENTIFIER_PARAM_KEY),
            false);
    try {
      String contactType = uri.getQueryParameter(CONTACT_TYPE_PARAM_KEY);
      if (!TextUtils.isEmpty(contactType)) {
        request.contactType = Integer.valueOf(contactType);
      }

      String scale = uri.getQueryParameter(SCALE_PARAM_KEY);
      if (!TextUtils.isEmpty(scale)) {
        request.scale = Float.valueOf(scale);
      }

      String offset = uri.getQueryParameter(OFFSET_PARAM_KEY);
      if (!TextUtils.isEmpty(offset)) {
        request.offset = Float.valueOf(offset);
      }

      String isCircular = uri.getQueryParameter(IS_CIRCULAR_PARAM_KEY);
      if (!TextUtils.isEmpty(isCircular)) {
        request.isCircular = Boolean.valueOf(isCircular);
      }
    } catch (NumberFormatException e) {
      LogUtil.w(
          "ContactPhotoManager.getDefaultImageRequestFromUri",
          "Invalid DefaultImageRequest image parameters provided, ignoring and using "
              + "defaults.");
    }

    return request;
  }

  public static ContactPhotoManager getInstance(Context context) {
    if (sInstance == null) {
      Context applicationContext = context.getApplicationContext();
      sInstance = createContactPhotoManager(applicationContext);
      applicationContext.registerComponentCallbacks(sInstance);
      if (PermissionsUtil.hasContactsReadPermissions(context)) {
        sInstance.preloadPhotosInBackground();
      }
    }
    return sInstance;
  }

  public static synchronized ContactPhotoManager createContactPhotoManager(Context context) {
    return new ContactPhotoManagerImpl(context);
  }

  @VisibleForTesting
  public static void injectContactPhotoManagerForTesting(ContactPhotoManager photoManager) {
    sInstance = photoManager;
  }

  protected boolean isDefaultImageUri(Uri uri) {
    return DEFAULT_IMAGE_URI_SCHEME.equals(uri.getScheme());
  }

  /**
   * Load thumbnail image into the supplied image view. If the photo is already cached, it is
   * displayed immediately. Otherwise a request is sent to load the photo from the database.
   */
  public abstract void loadThumbnail(
      ImageView view,
      long photoId,
      boolean darkTheme,
      boolean isCircular,
      DefaultImageRequest defaultImageRequest,
      DefaultImageProvider defaultProvider);

  /**
   * Calls {@link #loadThumbnail(ImageView, long, boolean, boolean, DefaultImageRequest,
   * DefaultImageProvider)} using the {@link DefaultImageProvider} {@link #DEFAULT_AVATAR}.
   */
  public final void loadThumbnail(
      ImageView view,
      long photoId,
      boolean darkTheme,
      boolean isCircular,
      DefaultImageRequest defaultImageRequest) {
    loadThumbnail(view, photoId, darkTheme, isCircular, defaultImageRequest, DEFAULT_AVATAR);
  }

  public final void loadDialerThumbnailOrPhoto(
      QuickContactBadge badge,
      Uri contactUri,
      long photoId,
      Uri photoUri,
      String displayName,
      int contactType) {
    badge.assignContactUri(contactUri);
    badge.setOverlay(null);

    badge.setContentDescription(
        badge.getContext().getString(R.string.description_quick_contact_for, displayName));

    String lookupKey = contactUri == null ? null : UriUtils.getLookupKeyFromUri(contactUri);
    ContactPhotoManager.DefaultImageRequest request =
        new ContactPhotoManager.DefaultImageRequest(
            displayName, lookupKey, contactType, true /* isCircular */);
    if (photoId == 0 && photoUri != null) {
      loadDirectoryPhoto(badge, photoUri, false /* darkTheme */, true /* isCircular */, request);
    } else {
      loadThumbnail(badge, photoId, false /* darkTheme */, true /* isCircular */, request);
    }
  }

  /**
   * Load photo into the supplied image view. If the photo is already cached, it is displayed
   * immediately. Otherwise a request is sent to load the photo from the location specified by the
   * URI.
   *
   * @param view The target view
   * @param photoUri The uri of the photo to load
   * @param requestedExtent Specifies an approximate Max(width, height) of the targetView. This is
   *     useful if the source image can be a lot bigger that the target, so that the decoding is
   *     done using efficient sampling. If requestedExtent is specified, no sampling of the image is
   *     performed
   * @param darkTheme Whether the background is dark. This is used for default avatars
   * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default
   *     letter tile avatar should be drawn.
   * @param defaultProvider The provider of default avatars (this is used if photoUri doesn't refer
   *     to an existing image)
   */
  public abstract void loadPhoto(
      ImageView view,
      Uri photoUri,
      int requestedExtent,
      boolean darkTheme,
      boolean isCircular,
      DefaultImageRequest defaultImageRequest,
      DefaultImageProvider defaultProvider);

  /**
   * Calls {@link #loadPhoto(ImageView, Uri, int, boolean, boolean, DefaultImageRequest,
   * DefaultImageProvider)} with {@link #DEFAULT_AVATAR} and {@code null} display names and lookup
   * keys.
   *
   * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default
   *     letter tile avatar should be drawn.
   */
  public final void loadPhoto(
      ImageView view,
      Uri photoUri,
      int requestedExtent,
      boolean darkTheme,
      boolean isCircular,
      DefaultImageRequest defaultImageRequest) {
    loadPhoto(
        view,
        photoUri,
        requestedExtent,
        darkTheme,
        isCircular,
        defaultImageRequest,
        DEFAULT_AVATAR);
  }

  /**
   * Calls {@link #loadPhoto(ImageView, Uri, int, boolean, boolean, DefaultImageRequest,
   * DefaultImageProvider)} with {@link #DEFAULT_AVATAR} and with the assumption, that the image is
   * a thumbnail.
   *
   * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default
   *     letter tile avatar should be drawn.
   */
  public final void loadDirectoryPhoto(
      ImageView view,
      Uri photoUri,
      boolean darkTheme,
      boolean isCircular,
      DefaultImageRequest defaultImageRequest) {
    loadPhoto(view, photoUri, -1, darkTheme, isCircular, defaultImageRequest, DEFAULT_AVATAR);
  }

  /**
   * Remove photo from the supplied image view. This also cancels current pending load request
   * inside this photo manager.
   */
  public abstract void removePhoto(ImageView view);

  /** Cancels all pending requests to load photos asynchronously. */
  public abstract void cancelPendingRequests(View fragmentRootView);

  /** Temporarily stops loading photos from the database. */
  public abstract void pause();

  /** Resumes loading photos from the database. */
  public abstract void resume();

  /**
   * Marks all cached photos for reloading. We can continue using cache but should also make sure
   * the photos haven't changed in the background and notify the views if so.
   */
  public abstract void refreshCache();

  /** Initiates a background process that over time will fill up cache with preload photos. */
  public abstract void preloadPhotosInBackground();

  // ComponentCallbacks2
  @Override
  public void onConfigurationChanged(Configuration newConfig) {}

  // ComponentCallbacks2
  @Override
  public void onLowMemory() {}

  // ComponentCallbacks2
  @Override
  public void onTrimMemory(int level) {}

  /**
   * Contains fields used to contain contact details and other user-defined settings that might be
   * used by the ContactPhotoManager to generate a default contact image. This contact image takes
   * the form of a letter or bitmap drawn on top of a colored tile.
   */
  public static class DefaultImageRequest {

    /**
     * Used to indicate that a drawable that represents a contact without any contact details should
     * be returned.
     */
    public static final DefaultImageRequest EMPTY_DEFAULT_IMAGE_REQUEST = new DefaultImageRequest();
    /**
     * Used to indicate that a drawable that represents a business without a business photo should
     * be returned.
     */
    public static final DefaultImageRequest EMPTY_DEFAULT_BUSINESS_IMAGE_REQUEST =
        new DefaultImageRequest(null, null, LetterTileDrawable.TYPE_BUSINESS, false);
    /**
     * Used to indicate that a circular drawable that represents a contact without any contact
     * details should be returned.
     */
    public static final DefaultImageRequest EMPTY_CIRCULAR_DEFAULT_IMAGE_REQUEST =
        new DefaultImageRequest(null, null, true);
    /**
     * Used to indicate that a circular drawable that represents a business without a business photo
     * should be returned.
     */
    public static final DefaultImageRequest EMPTY_CIRCULAR_BUSINESS_IMAGE_REQUEST =
        new DefaultImageRequest(null, null, LetterTileDrawable.TYPE_BUSINESS, true);
    /** The contact's display name. The display name is used to */
    public String displayName;
    /**
     * A unique and deterministic string that can be used to identify this contact. This is usually
     * the contact's lookup key, but other contact details can be used as well, especially for
     * non-local or temporary contacts that might not have a lookup key. This is used to determine
     * the color of the tile.
     */
    public String identifier;
    /**
     * The type of this contact. This contact type may be used to decide the kind of image to use in
     * the case where a unique letter cannot be generated from the contact's display name and
     * identifier.
     */
    public @LetterTileDrawable.ContactType int contactType = LetterTileDrawable.TYPE_DEFAULT;
    /**
     * The amount to scale the letter or bitmap to, as a ratio of its default size (from a range of
     * 0.0f to 2.0f). The default value is 1.0f.
     */
    public float scale = SCALE_DEFAULT;
    /**
     * The amount to vertically offset the letter or image to within the tile. The provided offset
     * must be within the range of -0.5f to 0.5f. If set to -0.5f, the letter will be shifted
     * upwards by 0.5 times the height of the canvas it is being drawn on, which means it will be
     * drawn with the center of the letter starting at the top edge of the canvas. If set to 0.5f,
     * the letter will be shifted downwards by 0.5 times the height of the canvas it is being drawn
     * on, which means it will be drawn with the center of the letter starting at the bottom edge of
     * the canvas. The default is 0.0f, which means the letter is drawn in the exact vertical center
     * of the tile.
     */
    public float offset = OFFSET_DEFAULT;
    /** Whether or not to draw the default image as a circle, instead of as a square/rectangle. */
    public boolean isCircular = false;

    public DefaultImageRequest() {}

    public DefaultImageRequest(String displayName, String identifier, boolean isCircular) {
      this(
          displayName,
          identifier,
          LetterTileDrawable.TYPE_DEFAULT,
          SCALE_DEFAULT,
          OFFSET_DEFAULT,
          isCircular);
    }

    public DefaultImageRequest(
        String displayName, String identifier, int contactType, boolean isCircular) {
      this(displayName, identifier, contactType, SCALE_DEFAULT, OFFSET_DEFAULT, isCircular);
    }

    public DefaultImageRequest(
        String displayName,
        String identifier,
        int contactType,
        float scale,
        float offset,
        boolean isCircular) {
      this.displayName = displayName;
      this.identifier = identifier;
      this.contactType = contactType;
      this.scale = scale;
      this.offset = offset;
      this.isCircular = isCircular;
    }
  }

  public abstract static class DefaultImageProvider {

    /**
     * Applies the default avatar to the ImageView. Extent is an indicator for the size (width or
     * height). If darkTheme is set, the avatar is one that looks better on dark background
     *
     * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default
     *     letter tile avatar should be drawn.
     */
    public abstract void applyDefaultImage(
        ImageView view, int extent, boolean darkTheme, DefaultImageRequest defaultImageRequest);
  }

  /**
   * A default image provider that applies a letter tile consisting of a colored background and a
   * letter in the foreground as the default image for a contact. The color of the background and
   * the type of letter is decided based on the contact's details.
   */
  private static class LetterTileDefaultImageProvider extends DefaultImageProvider {

    public static Drawable getDefaultImageForContact(
        Resources resources, DefaultImageRequest defaultImageRequest) {
      final LetterTileDrawable drawable = new LetterTileDrawable(resources);
      final int tileShape =
          defaultImageRequest.isCircular
              ? LetterTileDrawable.SHAPE_CIRCLE
              : LetterTileDrawable.SHAPE_RECTANGLE;
      if (defaultImageRequest != null) {
        // If the contact identifier is null or empty, fallback to the
        // displayName. In that case, use {@code null} for the contact's
        // display name so that a default bitmap will be used instead of a
        // letter
        if (TextUtils.isEmpty(defaultImageRequest.identifier)) {
          drawable.setCanonicalDialerLetterTileDetails(
              null, defaultImageRequest.displayName, tileShape, defaultImageRequest.contactType);
        } else {
          drawable.setCanonicalDialerLetterTileDetails(
              defaultImageRequest.displayName,
              defaultImageRequest.identifier,
              tileShape,
              defaultImageRequest.contactType);
        }
        drawable.setScale(defaultImageRequest.scale);
        drawable.setOffset(defaultImageRequest.offset);
      }
      return drawable;
    }

    @Override
    public void applyDefaultImage(
        ImageView view, int extent, boolean darkTheme, DefaultImageRequest defaultImageRequest) {
      final Drawable drawable = getDefaultImageForContact(view.getResources(), defaultImageRequest);
      view.setImageDrawable(drawable);
    }
  }
}
