/*
 *
 * Copyright 2018 iQIYI.com
 *
 * 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 org.qiyi.pluginlibrary.utils;

import android.content.Context;
import android.content.res.TypedArray;
import android.os.Build;
import android.support.design.widget.CoordinatorLayout;
import android.support.v4.util.SimpleArrayMap;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;

import android.support.coreui.R;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Vector;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * 通过给LayoutInflater设置privateFactory,用于解决多个插件使用了
 * 相同类名的View或者Fragment导致的类冲突的问题,比如同时依赖了android design库,
 * 原因是{@link android.view.LayoutInflater}缓存了View的构造函数map,Android N以下没有verifyClassLoader,
 * {@link android.support.v4.app.Fragment}同样缓存了Fragment的构造函数map
 */
public class LayoutInflaterCompat {
    private static final String TAG = "LayoutInflaterCompat";
    private static final ConcurrentMap<String, Vector<Method>> sMethods = new ConcurrentHashMap<String, Vector<Method>>(1);

    /**
     * 给LayoutInflater设置privateFactory
     * 解决同名View或者Fragment冲突的问题
     */
    public static void setPrivateFactory(LayoutInflater inflater) {
        LayoutInflater.Factory2 factory2 = null;
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
            // 5.0以下重复设置privateFactory没有FactoryMerger,而Activity会把自己设置成privateFactory
            factory2 = ReflectionUtils.on(inflater).get("mPrivateFactory");
        }
        LayoutInflater.Factory2 privateFactory = new CompatPrivateFactory(factory2);
        Class<?>[] paramTypes = new Class[]{LayoutInflater.Factory2.class};
        ReflectionUtils.on(inflater).call("setPrivateFactory", sMethods, paramTypes, privateFactory);
    }

    static class FragmentTag {
        public static final int[] Fragment = {
                0x01010003, 0x010100d0, 0x010100d1
        };
        public static final int Fragment_id = 1;
        public static final int Fragment_name = 0;
        public static final int Fragment_tag = 2;
    }

    /**
     * 自定义PrivateFactory,用于移除ClassLoader发生变化时系统缓存的构造函数或类
     */
    private static class CompatPrivateFactory implements LayoutInflater.Factory2 {
        private static String WIDGET_PACKAGE_NAME;
        // android.view.LayoutInflater中的View构造函数缓存map
        private static Map<String, Constructor<? extends View>> sViewConstructorMap;
        // android.support.v4.app.Fragment中的Fragment类缓存map
        private static SimpleArrayMap<String, Class<?>> sSupportFragmentClassMap;
        // android.app.Fragment中的Fragment类缓存map
        private static ArrayMap<String, Class<?>> sFragmentClassMap;
        // android.support.design.widget.CoordinatorLayout中的behavior缓存
        private static ThreadLocal<Map<String, Constructor<CoordinatorLayout.Behavior>>> sBehaviorConstructors;

        private final LayoutInflater.Factory2 mOrigFactory;

        static {
            final Package pkg = CoordinatorLayout.class.getPackage();
            WIDGET_PACKAGE_NAME = pkg != null ? pkg.getName() : null;
        }

        private static Map<String, Constructor<? extends View>> getViewConstructorMap() {
            if (sViewConstructorMap == null) {
                sViewConstructorMap = ReflectionUtils.on(LayoutInflater.class).get("sConstructorMap");
            }
            return sViewConstructorMap;
        }

        private static SimpleArrayMap<String, Class<?>> getSupportFragmentClassMap() {
            if (sSupportFragmentClassMap == null) {
                sSupportFragmentClassMap = ReflectionUtils.on(android.support.v4.app.Fragment.class).get("sClassMap");
            }
            return sSupportFragmentClassMap;
        }

        private static ArrayMap<String, Class<?>> getFragmentClassMap() {
            if (sFragmentClassMap == null) {
                sFragmentClassMap = ReflectionUtils.on(android.app.Fragment.class).get("sClassMap");
            }
            return sFragmentClassMap;
        }

        private static Map<String, Constructor<CoordinatorLayout.Behavior>> getBehaviorConstructors() {
            Map<String, Constructor<CoordinatorLayout.Behavior>> constructorMap = null;
            if (sBehaviorConstructors == null) {
                sBehaviorConstructors = ReflectionUtils.on(CoordinatorLayout.class).get("sConstructors");
            }
            if (sBehaviorConstructors != null) {
                constructorMap = sBehaviorConstructors.get();
            }
            return constructorMap;
        }

        CompatPrivateFactory(LayoutInflater.Factory2 factory) {
            mOrigFactory = factory;
        }

        @Override
        public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
            if ("fragment".equals(name)) {
                String fname = attrs.getAttributeValue(null, "class");
                TypedArray a =  context.obtainStyledAttributes(attrs, FragmentTag.Fragment);
                if (fname == null) {
                    fname = a.getString(FragmentTag.Fragment_name);
                }
                a.recycle();
                // 处理Fragment
                resetFragmentClassMap(context, fname);
            } else {
                // 处理View
                resetViewConstructorMap(context, attrs, name);
            }
            // 处理CoordinatorLayout的Behavior
            if (parent instanceof CoordinatorLayout) {
                final TypedArray ta = context.obtainStyledAttributes(attrs,
                        R.styleable.CoordinatorLayout_Layout);
                if (ta.hasValue(R.styleable.CoordinatorLayout_Layout_layout_behavior)) {
                    String behavior = ta.getString(
                            R.styleable.CoordinatorLayout_Layout_layout_behavior);
                    resetBehaviorConstructorMap(context, behavior);
                }
                ta.recycle();
            }

            return mOrigFactory != null ? mOrigFactory.onCreateView(parent, name, context, attrs) : null;
        }

        @Override
        public View onCreateView(String name, Context context, AttributeSet attrs) {
            // 这个API用于3.0之前的系统,暂不实现
            return null;
        }

        /**
         * 处理Fragment的缓存
         */
        private void resetFragmentClassMap(Context context, String fname) {
            if (isSupportFragmentClass(context, fname)) {
                resetSupportFragmentClassMap(context, fname);
                return;
            }

            ArrayMap<String, Class<?>> classMap = getFragmentClassMap();
            if (classMap != null) {
                Class<?> clazz = classMap.get(fname);
                if (clazz != null && !verifyClassLoader(context, clazz)) {
                    PluginDebugLog.runtimeFormatLog(TAG, "find same app fragment class name in LayoutInflater cache and remove it %s", fname);
                    clazz = null;
                    classMap.remove(fname);
                }
            }
        }

        /**
         * 处理android support库Fragment的缓存
         */
        private void resetSupportFragmentClassMap(Context context, String fname) {
            SimpleArrayMap<String, Class<?>> classMap = getSupportFragmentClassMap();
            if (classMap != null) {
                Class<?> clazz = classMap.get(fname);
                if (clazz != null && !verifyClassLoader(context, clazz)) {
                    PluginDebugLog.runtimeFormatLog(TAG, "find same support fragment class name in LayoutInflater cache and remove it %s", fname);
                    clazz = null;
                    classMap.remove(fname);
                }
            }
        }

        /**
         * 是否是android support库里的Fragment
         */
        private boolean isSupportFragmentClass(Context context, String fname) {
            try {
                Class<?> clazz = context.getClassLoader().loadClass(fname);
                return android.support.v4.app.Fragment.class.isAssignableFrom(clazz);
            } catch (Exception e) {
                ErrorUtil.throwErrorIfNeed(e);
            }
            return false;
        }

        /**
         * 处理View的缓存
         */
        private void resetViewConstructorMap(Context context, AttributeSet attrs, String viewName) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                PluginDebugLog.runtimeLog(TAG, "No need to handle LayoutInflater above N");
                return;
            }

            if (viewName.indexOf(".") > 0) {
                // 处理自定义View,忽略系统View类
                Map<String, Constructor<? extends View>> constructorMap = getViewConstructorMap();
                if (constructorMap != null) {
                    Constructor<? extends View> constructor = constructorMap.get(viewName);
                    if (constructor != null && !verifyClassLoader(context, constructor)) {
                        PluginDebugLog.runtimeFormatLog(TAG, "find same view class name in LayoutInflater cache and remove it %s", viewName);
                        constructor = null;
                        constructorMap.remove(viewName);
                    }
                }
            }
        }

        /**
         * 处理behavior的缓存
         */
        private void resetBehaviorConstructorMap(Context context, String name) {
            if (TextUtils.isEmpty(name)) {
                return;
            }

            final String behaviorName;
            if (name.startsWith(".")) {
                // Relative to the app package. Prepend the app package name.
                behaviorName = context.getPackageName() + name;
            } else if (name.indexOf('.') >= 0) {
                // Fully qualified package name.
                behaviorName = name;
            } else {
                // Assume stock behavior in this package (if we have one)
                behaviorName = !TextUtils.isEmpty(WIDGET_PACKAGE_NAME)
                        ? (WIDGET_PACKAGE_NAME + '.' + name)
                        : name;
            }

            Map<String, Constructor<CoordinatorLayout.Behavior>> constructors = getBehaviorConstructors();
            if (constructors != null) {
                Constructor<?> constructor = constructors.get(behaviorName);
                if (constructor != null && !verifyClassLoader(context, constructor)) {
                    PluginDebugLog.runtimeFormatLog(TAG, "find same behavior class name in CoordinatorLayout cache and remove it %s", behaviorName);
                    constructor = null;
                    constructors.remove(behaviorName);
                }
            }
        }

        private static final ClassLoader BOOT_CLASS_LOADER = LayoutInflater.class.getClassLoader();

        private static boolean verifyClassLoader(Context context, Constructor<?> constructor) {
            return verifyClassLoader(context, constructor.getDeclaringClass());
        }

        private static boolean verifyClassLoader(Context context, Class<?> clazz) {
            final ClassLoader constructorLoader = clazz.getClassLoader();
            if (constructorLoader == BOOT_CLASS_LOADER) {
                // fast path for boot class loader (most common case?) - always ok
                return true;
            }
            // in all normal cases (no dynamic code loading), we will exit the following loop on the
            // first iteration (i.e. when the declaring classloader is the contexts class loader).
            ClassLoader cl = context.getClassLoader();
            do {
                if (constructorLoader == cl) {
                    return true;
                }
                cl = cl.getParent();
            } while (cl != null);
            return false;
        }
    }
}