content: /*
   * Copyright (C) 2016 Bilibili
   *
   * 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.bilibili.magicasakura.utils;
  
  import android.content.Context;
  import android.content.res.ColorStateList;
  import android.graphics.PorterDuff;
  import android.graphics.PorterDuffColorFilter;
  import android.graphics.drawable.ColorDrawable;
  import android.graphics.drawable.Drawable;
  import android.os.Build;
  import android.support.annotation.ColorRes;
  import android.support.annotation.DrawableRes;
  import android.support.annotation.NonNull;
  import android.support.annotation.Nullable;
  import android.support.v4.util.LruCache;
  import android.support.v7.view.ContextThemeWrapper;
  import android.util.Log;
  import android.util.SparseArray;
  import android.view.View;
  
  import com.bilibili.magicasakura.drawables.FilterableStateListDrawable;
  
  import java.lang.ref.WeakReference;
  import java.util.Map;
  import java.util.WeakHashMap;
  
  /**
   * @author xyczero617@gmail.com
   * @time 15/9/15
   */
  public class TintManager {
  
      private static final String TAG = "TintManager";
      private static final boolean DEBUG = false;
      private static final PorterDuff.Mode DEFAULT_MODE = PorterDuff.Mode.SRC_IN;
      private static final String SKIP_DRAWABLE_TAG = "appcompat_skip_skip";
  
      private static final WeakHashMap<Context, TintManager> INSTANCE_CACHE = new WeakHashMap<>();
      private static final ColorFilterLruCache COLOR_FILTER_CACHE = new ColorFilterLruCache(6);
  
      private final Object mDrawableCacheLock = new Object();
  
      private WeakReference<Context> mContextRef;
      private SparseArray<ColorStateList> mCacheTintList;
      private SparseArray<WeakReference<Drawable.ConstantState>> mCacheDrawables;
      private SparseArray<String> mSkipDrawableIdTags;
  
      public static TintManager get(Context context) {
          if (context == null) return null;
  
          if (context instanceof ContextThemeWrapper) {
              context = ((ContextThemeWrapper) context).getBaseContext();
          }
          if (context instanceof android.view.ContextThemeWrapper) {
              context = ((android.view.ContextThemeWrapper) context).getBaseContext();
          }
          TintManager tm = INSTANCE_CACHE.get(context);
          if (tm == null) {
              tm = new TintManager(context);
              INSTANCE_CACHE.put(context, tm);
              printLog("[get TintManager] create new TintManager.");
          }
          return tm;
      }
  
      private TintManager(Context context) {
          mContextRef = new WeakReference<>(context);
      }
  
      public static void clearTintCache() {
          for (Map.Entry<Context, TintManager> entry : INSTANCE_CACHE.entrySet()) {
              TintManager tm = entry.getValue();
              if (tm != null)
                  tm.clear();
          }
          COLOR_FILTER_CACHE.evictAll();
      }
  
      private void clear() {
          if (mCacheTintList != null) {
              mCacheTintList.clear();
          }
          if (mCacheDrawables != null) {
              mCacheDrawables.clear();
          }
          if (mSkipDrawableIdTags != null) {
              mSkipDrawableIdTags.clear();
          }
      }
  
      @Nullable
      public ColorStateList getColorStateList(@ColorRes int resId) {
          if (resId == 0) return null;
  
          final Context context = mContextRef.get();
          if (context == null) return null;
  
          ColorStateList colorStateList = mCacheTintList != null ? mCacheTintList.get(resId) : null;
          if (colorStateList == null) {
              colorStateList = ColorStateListUtils.createColorStateList(context, resId);
              if (colorStateList != null) {
                  if (mCacheTintList == null) {
                      mCacheTintList = new SparseArray<>();
                  }
                  mCacheTintList.append(resId, colorStateList);
              }
          }
          return colorStateList;
      }
  
      @Nullable
      public Drawable getDrawable(@DrawableRes int resId) {
          final Context context = mContextRef.get();
          if (context == null) return null;
  
          if (resId == 0) return null;
          if (mSkipDrawableIdTags != null) {
              final String cachedTagName = mSkipDrawableIdTags.get(resId);
              if (SKIP_DRAWABLE_TAG.equals(cachedTagName)) {
                  printLog("[Match Skip DrawableTag] Skip the drawable which is matched with the skip tag.");
                  return null;
              }
          } else {
              // Create an id cache as we'll need one later
              mSkipDrawableIdTags = new SparseArray<>();
          }
  
          // Try the cache first (if it exists)
          Drawable drawable = getCacheDrawable(context, resId);
          if (drawable == null) {
              drawable = DrawableUtils.createDrawable(context, resId);
              if (drawable != null && !(drawable instanceof ColorDrawable)) {
                  if (addCachedDrawable(resId, drawable)) {
                      printLog("[loadDrawable] Saved drawable to cache: " +
                              context.getResources().getResourceName(resId));
                  }
              }
          }
  
          if (drawable == null) {
              mSkipDrawableIdTags.append(resId, SKIP_DRAWABLE_TAG);
          }
          return drawable;
      }
  
      private Drawable getCacheDrawable(@NonNull final Context context, final int key) {
          synchronized (mDrawableCacheLock) {
              if (mCacheDrawables == null) return null;
  
              final WeakReference<Drawable.ConstantState> weakReference = mCacheDrawables.get(key);
              if (weakReference != null) {
                  Drawable.ConstantState cs = weakReference.get();
                  if (cs != null) {
                      printLog("[getCacheDrawable] Get drawable from cache: " +
                              context.getResources().getResourceName(key));
                      return cs.newDrawable();
                  } else {
                      mCacheDrawables.delete(key);
                  }
              }
          }
          return null;
      }
  
      private boolean addCachedDrawable(final int key, @NonNull final Drawable drawable) {
          if (drawable instanceof FilterableStateListDrawable) {
              return false;
          }
          final Drawable.ConstantState cs = drawable.getConstantState();
          if (cs != null) {
              synchronized (mDrawableCacheLock) {
                  if (mCacheDrawables == null) {
                      mCacheDrawables = new SparseArray<>();
                  }
                  mCacheDrawables.put(key, new WeakReference<>(cs));
              }
              return true;
          }
          return false;
      }
  
      private static class ColorFilterLruCache extends LruCache<Integer, PorterDuffColorFilter> {
  
          public ColorFilterLruCache(int maxSize) {
              super(maxSize);
          }
  
          PorterDuffColorFilter get(int color, PorterDuff.Mode mode) {
              return get(generateCacheKey(color, mode));
          }
  
          PorterDuffColorFilter put(int color, PorterDuff.Mode mode, PorterDuffColorFilter filter) {
              return put(generateCacheKey(color, mode), filter);
          }
  
          private static int generateCacheKey(int color, PorterDuff.Mode mode) {
              int hashCode = 1;
              hashCode = 31 * hashCode + color;
              hashCode = 31 * hashCode + mode.hashCode();
              return hashCode;
          }
      }
  
      public static void tintViewBackground(View view, TintInfo tint) {
          Drawable background;
          if (view == null || (background = view.getBackground()) == null) return;
  
          if (tint.mHasTintList || tint.mHasTintMode) {
              background.mutate();
              if (background instanceof ColorDrawable) {
                  ((ColorDrawable) background).setColor(ThemeUtils.replaceColor(view.getContext(), tint.mTintList.getColorForState(view.getDrawableState(), tint.mTintList.getDefaultColor())));
              } else {
                  background.setColorFilter(createTintFilter(view.getContext(),
                          tint.mHasTintList ? tint.mTintList : null,
                          tint.mHasTintMode ? tint.mTintMode : DEFAULT_MODE,
                          view.getDrawableState()));
              }
          } else {
              background.clearColorFilter();
          }
  
          if (Build.VERSION.SDK_INT <= 23) {
              // On Gingerbread, GradientDrawable does not invalidate itself when it's ColorFilter
              // has changed, so we need to force an invalidation
              background.invalidateSelf();
          }
      }
  
      public static void tintViewDrawable(View view, Drawable drawable, TintInfo tint) {
          if (view == null || drawable == null) return;
          if (tint.mHasTintList || tint.mHasTintMode) {
              drawable.mutate();
              if (drawable instanceof ColorDrawable) {
                  ((ColorDrawable) drawable).setColor(ThemeUtils.replaceColor(view.getContext(), tint.mTintList.getColorForState(view.getDrawableState(), tint.mTintList.getDefaultColor())));
              } else {
                  drawable.setColorFilter(createTintFilter(view.getContext(),
                          tint.mHasTintList ? tint.mTintList : null,
                          tint.mHasTintMode ? tint.mTintMode : DEFAULT_MODE,
                          view.getDrawableState()));
              }
          } else {
              drawable.clearColorFilter();
          }
  
          if (Build.VERSION.SDK_INT <= 23) {
              // On Gingerbread, GradientDrawable does not invalidate itself when it's ColorFilter
              // has changed, so we need to force an invalidation
              drawable.invalidateSelf();
          }
      }
  
      private static PorterDuffColorFilter createTintFilter(Context context, ColorStateList tint, PorterDuff.Mode tintMode, final int[] state) {
          if (tint == null || tintMode == null) {
              return null;
          }
          final int color = ThemeUtils.replaceColor(context, tint.getColorForState(state, tint.getDefaultColor()));
          return getPorterDuffColorFilter(color, tintMode);
      }
  
      private static PorterDuffColorFilter getPorterDuffColorFilter(int color, PorterDuff.Mode mode) {
          // First, lets see if the cache already contains the color filter
          PorterDuffColorFilter filter = COLOR_FILTER_CACHE.get(color, mode);
  
          if (filter == null) {
              // Cache miss, so create a color filter and add it to the cache
              filter = new PorterDuffColorFilter(color, mode);
              COLOR_FILTER_CACHE.put(color, mode, filter);
          }
  
          return filter;
      }
  
      private static void printLog(String msg) {
          if (DEBUG) {
              Log.i(TAG, msg);
          }
      }
  }
  