/**
 * Copyright (C) 2014 Pau Picas Sans <[email protected]>
 *
 * 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 cat.ppicas.customtypeface;

import android.app.Activity;
import android.content.Context;
import android.graphics.Typeface;
import android.util.AttributeSet;
import android.view.InflateException;
import android.view.LayoutInflater;
import android.view.View;

import java.lang.reflect.Constructor;
import java.util.HashMap;

/**
 * An implementation of {@link LayoutInflater.Factory} that creates a {@link View} and applies
 * on it a custom {@link Typeface} calling {@link CustomTypeface#applyTypeface(View, AttributeSet)}.
 *
 * <p>
 * This class can also accept another {@code LayoutInflater.Factory} implementation to delegate
 * the view creation first. If the delegated {@code LayoutInflater.Factory} returns
 * {@code null} on the view creation, this class will then use the internal
 * {@code View} factory. Please use {@link #CustomTypefaceFactory(Context, CustomTypeface,
 * LayoutInflater.Factory)} if you want this class to use a delegate {@code LayoutInflater.Factory}.
 * </p>
 *
 * <p>
 * Please, take in account that {@link LayoutInflater} only accepts the
 * {@link LayoutInflater.Factory} to be set once. If you call {@link LayoutInflater#setFactory}
 * on a {@code LayoutInflater} that already has one, it will throw a {@code RuntimeException}.
 * This is why we recommend you to set the {@code Factory} before calling
 * super.{@link Activity#onCreate onCreate} when you are overriding {@code Activity.onCreate}
 * on your own {@code Activity}. Please, check the following example:
 * </p>
 *
 * <pre><code>
 * public class MainActivity extends Activity {
 *
 *     // ...
 *
 *     {@literal @Override}
 *     protected void onCreate(Bundle savedInstanceState) {
 *         getLayoutInflater().setFactory(new CustomTypefaceFactory(
 *                 this, CustomTypeface.getInstance()));
 *
 *         super.onCreate(savedInstanceState);
 *         setContentView(R.layout.activity_main);
 *     }
 *
 *     // ...
 *
 * }
 * </code></pre>
 *
 * <p>
 * For more info on how to use this class in combination with {@link CustomTypeface} please
 * check the {@link CustomTypeface documentation} of that class.
 * </p>
 *
 * @see CustomTypeface
 * @see LayoutInflater.Factory
 */
public class CustomTypefaceFactory implements LayoutInflater.Factory {

    private static final Class<?>[] CONSTRUCTOR_SIGNATURE = new Class[] {
            Context.class, AttributeSet.class};

    private static final HashMap<String, Constructor<? extends View>> CONSTRUCTOR_MAP =
            new HashMap<String, Constructor<? extends View>>();

    private final Object[] mConstructorArgs = new Object[2];

    private final Context mContext;

    private final CustomTypeface mCustomTypeface;

    private LayoutInflater.Factory mFactory;

    public CustomTypefaceFactory(Context context, CustomTypeface customTypeface) {
        this(context, customTypeface, null);
    }

    public CustomTypefaceFactory(Context context, CustomTypeface customTypeface,
            LayoutInflater.Factory factory) {
        mContext = context;
        mCustomTypeface = customTypeface;
        mFactory = factory;
    }

    public LayoutInflater.Factory getFactory() {
        return mFactory;
    }

    public void setFactory(LayoutInflater.Factory factory) {
        mFactory = factory;
    }

    /**
     * Implements {@link LayoutInflater.Factory} interface. Inflate the {@link View} for the
     * specified tag name and apply custom {@link Typeface} if is required. This
     * method first will delegate to the {@code LayoutInflater.Factory} if it was specified on the
     * constructor. When the {@code View} is created, this method will call {@link
     * CustomTypeface#applyTypeface(View, AttributeSet)} on it.
     *
     * <p>
     * This method can be used to delegate the implementation of {@link
     * LayoutInflater.Factory#onCreateView}
     * or {@link LayoutInflater.Factory2#onCreateView}.
     * </p>
     *
     * @param name    Tag name to be inflated.
     * @param context The context the view is being created in.
     * @param attrs   Inflation attributes as specified in XML file.
     * @return Newly created view.
     * @see LayoutInflater.Factory
     * @see CustomTypeface#applyTypeface(View, AttributeSet)
     */
    @Override
    public View onCreateView(String name, Context context, AttributeSet attrs) {
        String prefix = null;
        if (name.indexOf('.') == -1) {
            prefix = "android.widget.";
        }
        try {
            View view = null;
            if (mFactory != null) {
                view = mFactory.onCreateView(name, context, attrs);
            }
            if (view == null) {
                view = createView(name, prefix, context, attrs);
            }
            mCustomTypeface.applyTypeface(view, attrs);
            return view;
        } catch (ClassNotFoundException e) {
            return null;
        }
    }

    /**
     * Low-level function for instantiating a view by name. This attempts to
     * instantiate a view class of the given <var>name</var> found in this
     * LayoutInflater's ClassLoader.
     *
     * <p>
     * There are two things that can happen in an error case: either the
     * exception describing the error will be thrown, or a null will be
     * returned. You must deal with both possibilities -- the former will happen
     * the first time createView() is called for a class of a particular name,
     * the latter every time there-after for that class name.
     *
     * @param name    The full name of the class to be instantiated.
     * @param context The Context in which this LayoutInflater will create its
     *                Views; most importantly, this supplies the theme from which the default
     *                values for their attributes are retrieved.
     * @param attrs   The XML attributes supplied for this instance.
     * @return View The newly instantiated view, or null.
     */
    private View createView(String name, String prefix, Context context, AttributeSet attrs)
            throws ClassNotFoundException, InflateException {

        Constructor<? extends View> constructor = CONSTRUCTOR_MAP.get(name);
        Class<? extends View> clazz = null;

        try {
            if (constructor == null) {
                // Class not found in the cache, see if it's real, and try to add it
                clazz = mContext.getClassLoader().loadClass(
                        prefix != null ? (prefix + name) : name).asSubclass(View.class);
                constructor = clazz.getConstructor(CONSTRUCTOR_SIGNATURE);
                constructor.setAccessible(true);
                CONSTRUCTOR_MAP.put(name, constructor);
            }

            mConstructorArgs[0] = context;
            mConstructorArgs[1] = attrs;

            return constructor.newInstance(mConstructorArgs);
        } catch (NoSuchMethodException e) {
            InflateException ie = new InflateException(attrs.getPositionDescription()
                    + ": Error inflating class "
                    + (prefix != null ? (prefix + name) : name));
            ie.initCause(e);
            throw ie;

        } catch (ClassCastException e) {
            // If loaded class is not a View subclass
            InflateException ie = new InflateException(attrs.getPositionDescription()
                    + ": Class is not a View "
                    + (prefix != null ? (prefix + name) : name));
            ie.initCause(e);
            throw ie;
        } catch (ClassNotFoundException e) {
            // If loadClass fails, we should propagate the exception.
            throw e;
        } catch (Exception e) {
            InflateException ie = new InflateException(attrs.getPositionDescription()
                    + ": Error inflating class "
                    + (clazz == null ? "<unknown>" : clazz.getName()));
            ie.initCause(e);
            throw ie;
        }
    }
}