/*
 * Copyright (C) 2019 Cricin
 *
 * 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 cn.cricin.folivora;

import android.content.Context;
import android.content.ContextWrapper;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.RippleDrawable;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Log;
import android.util.LruCache;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageView;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Folivora support sets drawable directly in your layout.xml files, no need
 * to create XXX.xml in drawable directory. just write down attributes we
 * provided in your layout.xml, folivora will take care of with this attrs
 * and create suitable drawable's for view.
 * <p>
 * Folivora is light weight, you would use {@link #wrap(Context)} wrap()
 * or {@link #installViewFactory(Context)} installViewFactory() to enable
 * folivora functions
 *
 * @see #wrap(Context)
 * @see #installViewFactory(Context)
 * @see #setRippleFallback(RippleFallback)
 * @see #getDrawable(Context, TypedArray, AttributeSet, int)
 * @see #addOnViewCreatedListener(OnViewCreatedListener)
 */
public final class Folivora {
  static final String TAG = "Folivora";

  // Drawable type enums, keep sync with app:drawableType
  private static final int DRAWABLE_TYPE_SHAPE = 0;
  private static final int DRAWABLE_TYPE_SELECTOR = 1;
  private static final int DRAWABLE_TYPE_LAYER = 2;
  private static final int DRAWABLE_TYPE_RIPPLE = 3;
  private static final int DRAWABLE_TYPE_LEVEL = 4;
  private static final int DRAWABLE_TYPE_CLIP = 5;
  private static final int DRAWABLE_TYPE_INSET = 6;
  private static final int DRAWABLE_TYPE_SCALE = 7;
  private static final int DRAWABLE_TYPE_ANIMATION = 8;

  // Set as enums, keep sync with app:setAs
  private static final int SET_AS_BACKGROUND = 0;
  private static final int SET_AS_SRC = 1;
  private static final int SET_AS_FOREGROUND = 2;

  // Exposed apis
  private static RippleFallback sRippleFallback;
  private static List<OnViewCreatedListener> sOnViewCreatedListeners;
  private static Map<String, DrawableParser> sDrawableParsers = new HashMap<>();
  private static DrawableParser sReflectiveDrawableParser = new DrawableParser.ReflectiveDrawableParser();
  private static SparseArray<String> sSystemDrawableNames = new SparseArray<>();

  static {
    sDrawableParsers.put("android.graphics.drawable.GradientDrawable", new DrawableParser.GradientDrawableParser());
    sDrawableParsers.put("android.graphics.drawable.StateListDrawable", new DrawableParser.StateListDrawableParser());
    sDrawableParsers.put("android.graphics.drawable.LayerDrawable", new DrawableParser.LayerDrawableParser());
    sDrawableParsers.put("android.graphics.drawable.RippleDrawable", new DrawableParser.RippleDrawableParser());
    sDrawableParsers.put("android.graphics.drawable.LevelListDrawable", new DrawableParser.LevelListDrawableParser());
    sDrawableParsers.put("android.graphics.drawable.ClipDrawable", new DrawableParser.ClipDrawableParser());
    sDrawableParsers.put("android.graphics.drawable.InsetDrawable", new DrawableParser.InsetDrawableParser());
    sDrawableParsers.put("android.graphics.drawable.ScaleDrawable", new DrawableParser.ScaleDrawableParser());
    sDrawableParsers.put("android.graphics.drawable.AnimationDrawable", new DrawableParser.AnimationDrawableParser());

    sSystemDrawableNames.put(DRAWABLE_TYPE_SHAPE, "android.graphics.drawable.GradientDrawable");
    sSystemDrawableNames.put(DRAWABLE_TYPE_SELECTOR, "android.graphics.drawable.StateListDrawable");
    sSystemDrawableNames.put(DRAWABLE_TYPE_LAYER, "android.graphics.drawable.LayerDrawable");
    sSystemDrawableNames.put(DRAWABLE_TYPE_RIPPLE, "android.graphics.drawable.RippleDrawable");
    sSystemDrawableNames.put(DRAWABLE_TYPE_LEVEL, "android.graphics.drawable.LevelListDrawable");
    sSystemDrawableNames.put(DRAWABLE_TYPE_CLIP, "android.graphics.drawable.ClipDrawable");
    sSystemDrawableNames.put(DRAWABLE_TYPE_INSET, "android.graphics.drawable.InsetDrawable");
    sSystemDrawableNames.put(DRAWABLE_TYPE_SCALE, "android.graphics.drawable.ScaleDrawable");
    sSystemDrawableNames.put(DRAWABLE_TYPE_ANIMATION, "android.graphics.drawable.AnimationDrawable");

  }

  // Cached drawables with it's ids
  private static LruCache<String, Drawable> sDrawableCache = new LruCache<>(128);
  // Cache is enabled at runtime, but at design time, this should be disabled for work properly
  @SuppressWarnings("FieldCanBeLocal") // This is accessed by layout editor
  private static boolean sDrawableCacheEnabled = true;

  /**
   * Try to get a child drawable, if the attrIndex pointing to a specific drawable,
   * then load it in normal way, if the attrIndex pointing a enum(shape index), try
   * to create it from the given attrs
   *
   * @param ctx       current context
   * @param a         caller's typed array
   * @param attrs     attributes from view tag
   * @param attrIndex attribute index in the typed array
   * @return a drawable, or a newly created GradientDrawable from attrs, or null
   */
  @SuppressWarnings("WeakerAccess")
  public static Drawable getDrawable(Context ctx, TypedArray a, AttributeSet attrs, int attrIndex) {
    if (!a.hasValue(attrIndex)) return null;
    Drawable result = null;
    ShapeAttrs shapeAttrs = ShapeAttrs.forIndex(a.getInt(attrIndex, -1));
    if (shapeAttrs != null) {
      final String className = "android.graphics.drawable.GradientDrawable";
      DrawableParser parser = sDrawableParsers.get(className);
      ParseRequest request = new ParseRequest(ctx, attrs, sRippleFallback, shapeAttrs, className);
      if (parser != null) {
        result = parser.parse(request);
      }
    }

    if (result == null) {
      result = a.getDrawable(attrIndex);
    }
    return result;
  }

  /**
   * Create a drawable to the specific view with attrs, this method is
   * used by folivora internally, but in order to support preview for
   * the views folivora not stubbed, this method becomes publicly
   *
   * @param view  view of drawable attached
   * @param attrs attributes from view tag
   */
  static void applyDrawableToView(View view, AttributeSet attrs) {
    final Context ctx = view.getContext();
    // Step1 extract attrs
    TypedArray a = ctx.obtainStyledAttributes(attrs, R.styleable.Folivora);
    int drawableType = a.getInt(R.styleable.Folivora_drawableType, -1);
    String drawableId = a.getString(R.styleable.Folivora_drawableId);
    String drawableName = a.getString(R.styleable.Folivora_drawableName);
    int setAs = a.getInt(R.styleable.Folivora_setAs, SET_AS_BACKGROUND);
    a.recycle();
    if (drawableType < 0 && drawableId == null && drawableName == null) return;
    // Step2 lookup cached if available
    Drawable cached = null;
    Drawable d = null;
    if (drawableId != null) {
      cached = sDrawableCache.get(drawableId);
      if (cached != null && cached.getConstantState() != null) {
        cached = cached.getConstantState().newDrawable();
      }
    }
    // Step3 try to create a new drawable and cached it
    if (!sDrawableCacheEnabled || cached == null) {
      d = createDrawable(ctx, attrs, drawableType, drawableName);
      if (d != null && d.getConstantState() != null && drawableId != null) {
        sDrawableCache.put(drawableId, d);
      }
    }
    d = d == null ? cached : d;
    if (d == null) return;
    // Step4 set drawable to view
    if (setAs == SET_AS_BACKGROUND) {
      view.setBackground(d);
    } else if (setAs == SET_AS_SRC && view instanceof ImageView) {
      ((ImageView) view).setImageDrawable(d);
    } else if (setAs == SET_AS_FOREGROUND) {
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        view.setForeground(d);
      } else if (view instanceof FrameLayout) {
        //noinspection RedundantCast
        ((FrameLayout) view).setForeground(d);
      } else {
        Log.w(TAG, "Folivora can not set foreground to [" + view.getClass()
          + "], Current device platform is lower than MarshMallow");
      }
    }
  }

  private static Drawable createDrawable(Context ctx, AttributeSet attrs, int drawableType, String drawableName) {
    String realDrawableName = null;
    if (drawableType >= 0) {
      realDrawableName = sSystemDrawableNames.get(drawableType);
    }
    if (realDrawableName == null) {
      realDrawableName = drawableName;
    }
    if (realDrawableName == null) return null;
    DrawableParser parser = sDrawableParsers.get(realDrawableName);
    if (parser == null && !realDrawableName.startsWith("android.graphics.drawable")) {
      parser = sReflectiveDrawableParser;
    }
    if (parser == null) return null;
    ParseRequest request = new ParseRequest(ctx, attrs, sRippleFallback, ShapeAttrs.forIndex(0), realDrawableName);
    return parser.parse(request);
  }

  /**
   * Install Folivora's ViewFactory to current context. note that if
   * you are using AppCompatActivity, this method should called after
   * your activity's super.onCreate() method, since AppCompatDelegate
   * will install a {@link LayoutInflater.Factory2} factory2 to this
   * context.
   *
   * @param ctx context to enable folivora support
   */
  public static void installViewFactory(Context ctx) {
    LayoutInflater inflater = LayoutInflater.from(ctx);
    LayoutInflater.Factory2 factory2 = inflater.getFactory2();
    if (factory2 instanceof FolivoraViewFactory) return;
    FolivoraViewFactory viewFactory = new FolivoraViewFactory();
    viewFactory.mFactory2 = factory2;
    if (factory2 != null) {
      FolivoraViewFactory.forceSetFactory2(inflater, viewFactory);
    } else {
      inflater.setFactory2(viewFactory);
    }
  }

  /**
   * Wraps the given context, replace the {@link LayoutInflater} inflater
   * to folivora's implementation, this method does nothing if the given
   * context is already been wrapped.
   *
   * @param newBase new base context
   * @return a wrapped context
   */
  public static Context wrap(final Context newBase) {
    final LayoutInflater inflater = LayoutInflater.from(newBase);
    if (inflater instanceof FolivoraInflater) return newBase;
    return new ContextWrapper(newBase) {
      private FolivoraInflater mInflater;

      @Override
      public Object getSystemService(String name) {
        if (LAYOUT_INFLATER_SERVICE.equals(name)) {
          if (mInflater == null) {
            mInflater = new FolivoraInflater(newBase, inflater);
          }
          return mInflater;
        }
        return super.getSystemService(name);
      }
    };
  }

  /**
   * Set a fallback to create substitute drawable when the {@link RippleDrawable}
   * RippleDrawable is not available in current device.
   *
   * @param fallback a fallback to create drawable
   */
  public static void setRippleFallback(RippleFallback fallback) {
    sRippleFallback = fallback;
  }

  public static void registerDrawableParser(Class<? extends Drawable> drawableClass, DrawableParser parser) {
    final String className = drawableClass.getCanonicalName();
    sDrawableParsers.put(className, parser);
  }

  /**
   * Add a {@link OnViewCreatedListener} listener to folivora, folivora
   * will notify these listeners when a view is created. this listener
   * allows you do some extra customization about the inflated views.
   *
   * @param l listener to register
   */
  public static void addOnViewCreatedListener(OnViewCreatedListener l) {
    if (sOnViewCreatedListeners == null) {
      sOnViewCreatedListeners = new ArrayList<>();
    }
    sOnViewCreatedListeners.add(l);
  }

  static void dispatchViewCreated(View view, AttributeSet attrs) {
    if (sOnViewCreatedListeners != null) {
      for (OnViewCreatedListener l : sOnViewCreatedListeners) {
        l.onViewCreated(view, attrs);
      }
    }
  }

  private Folivora() {}
}