/**
 * 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.res.AssetManager;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Typeface;
import android.os.Build;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.AutoCompleteTextView;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CheckedTextView;
import android.widget.EditText;
import android.widget.RadioButton;
import android.widget.TextView;
import android.widget.ToggleButton;

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

import static android.view.LayoutInflater.Factory;

/**
 * This class can be used to automatically apply custom {@link Typeface} to views inflated from
 * any XML.
 *
 * <p>
 * This class is intended to be used as a parameter for {@link CustomTypefaceFactory}.
 * </p>
 *
 * <p>
 * Alternatively this class can also be used calling {@link #applyTypeface(View, AttributeSet)}
 * directly, and passing the {@link View} the {@link AttributeSet} result of an inflation.
 * </p>
 *
 * <p>
 * Here is an example of the use of this class. First you should register the {@code Typeface}
 * that you will use from the XML layouts. A good place to do this is in the {@code Application}
 * {@code onCreate} method.
 * </p>
 *
 * <pre><code>
 * public class App extends Application {
 *     {@literal @Override}
 *     public void onCreate() {
 *         super.onCreate();
 *
 *         // Register a Typeface creating first the object, and then registering the object
 *         // with a name.
 *         Typeface typeface = Typeface.createFromAsset(getAssets(), "permanent-marker.ttf");
 *         CustomTypeface.getInstance().registerTypeface("permanent-marker", typeface);
 *
 *         // Also you can directly use this shortcut to let CustomTypeface to create the
 *         // Typeface object for you.
 *         CustomTypeface.getInstance().registerTypeface("audiowide", getAssets(), "audiowide.ttf");
 *     }
 * }
 * </code></pre>
 *
 * <p>
 * The next step is set {@link CustomTypefaceFactory} as the {@link Factory} for the
 * {@link LayoutInflater} of each {@link Activity}. It's important to call
 * {@link LayoutInflater#setFactory} <strong>before</strong> calling
 * super.{@link Activity#onCreate onCreate}, otherwise the parent {@code Activity} could
 * call {@code LayoutInflater#setFactory} before you. If this happens, you will not be
 * able to set your {@code Factory} because {@code LayoutInflater} only accepts the
 * {@code Factory} to be set once.
 * </p>
 *
 * <p>
 * For you convenience you can create a base {@code Activity} class that overrides
 * {@link Activity#onCreate} and calls {@code setFactory}. This will enable you to extend this
 * class and avoid copy-paste the same line on each {@code Activity}.
 * </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>
 * Now all the templates inflated in the context of this {@code Activity} will have applied a
 * custom {@code Typeface} if it's defined in the XML. Check the following layout file.
 * </p>
 * <pre>{@code
 * <LinearLayout
 *     xmlns:android="http://schemas.android.com/apk/res/android"
 *     xmlns:tools="http://schemas.android.com/tools"
 *     xmlns:app="http://schemas.android.com/apk/res-auto"
 *     ...
 *     />
 *
 *     ...
 *
 *     <Button
 *         android:text="Permanent maker"
 *         android:layout_width="match_parent"
 *         android:layout_height="wrap_content"
 *         android:textSize="22sp"
 *         app:customTypeface="permanent-marker"
 *         tools:ignore="MissingPrefix"/>
 *
 *     ...
 *
 * </LinearLayout>
 * }</pre>
 *
 * <p>
 * In the previous sample you see the use of attribute {@code tools:ignore="MissingPrefix"}.
 * This is because sometimes you will get a warning from lint that you are applying an
 * attribute with an invalid namespace. In this cases the ignore MissingPrefix will hide this
 * warnings.
 * </p>
 *
 * <p>
 * Also you can use the {@code customTypeface} attribute in your styles, themes and
 * textAppearances as well. You can find some examples of this in the <strong>sample</strong>
 * project.
 * </p>
 *
 * <p>
 * <strong>Custom views extending {@code TextView}</strong>
 * </p>
 *
 * <p>
 * If you have a custom view with a default style defined in theme, then you must register
 * this theme attribute in {@code CustomTypeface}. To do that you can use the method
 * {@link #registerAttributeForDefaultStyle}. This is because {@code CustomTypeface} doesn't have
 * a way to know what is a default style of a view, and this is why must be registered before.
 * Here is a sample code to register a custom view default style attribute.
 * </p>
 * <pre><code>
 *     CustomTypeface.getInstance().registerAttributeForDefaultStyle(
 *         CustomTextView.class, R.attr.customTextViewStyle);
 * </code></pre>
 */
public class CustomTypeface {

    private final Map<Class<?>, Integer> mDefStyleAttrs = new HashMap<Class<?>, Integer>();
    private final Map<String, Typeface> mTypefaces = new HashMap<String, Typeface>();

    public static CustomTypeface getInstance() {
        return SingletonHolder.instance;
    }

    /**
     * Register the theme attributes with the default style for all the views extending
     * {@link TextView} defined in Android SDK. You can use this method if you create an
     * instance of {@code CustomTypeface}, and you want to configure with the default
     * styles for the default android widgets.
     *
     * @param instance an instance of {@code CustomTypeface} to register the attributes
     * @see #registerAttributeForDefaultStyle(Class, int)
     */
    public static void registerAttributesForDefaultStyles(CustomTypeface instance) {
        instance.registerAttributeForDefaultStyle(TextView.class, android.R.attr.textViewStyle);
        instance.registerAttributeForDefaultStyle(EditText.class, android.R.attr.editTextStyle);
        instance.registerAttributeForDefaultStyle(Button.class, android.R.attr.buttonStyle);
        instance.registerAttributeForDefaultStyle(AutoCompleteTextView.class,
                android.R.attr.autoCompleteTextViewStyle);
        instance.registerAttributeForDefaultStyle(CheckBox.class, android.R.attr.checkboxStyle);
        instance.registerAttributeForDefaultStyle(RadioButton.class,
                android.R.attr.radioButtonStyle);
        instance.registerAttributeForDefaultStyle(ToggleButton.class,
                android.R.attr.buttonStyleToggle);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            instance.registerAttributeForDefaultStyle(CheckedTextView.class,
                    android.R.attr.checkedTextViewStyle);
        }
    }

    /**
     * Register the theme attribute that reference the default style to be used by a view.
     * This attribute will be used in the cases where you define a view in a layout XML,
     * and you don't define a {@code customTypeface} or {@code style} specifying a
     * {@code customTypeface}. In this cases {@code CustomTypeface} will try to apply
     * the {@code customTypeface} found in the default style of that view.
     *
     * @param clazz          a {@code Class} of a view extending {@code TextView}
     * @param themeAttribute an integer with the number of the theme attribute
     */
    public void registerAttributeForDefaultStyle(Class<? extends TextView> clazz,
            int themeAttribute) {
        mDefStyleAttrs.put(clazz, themeAttribute);
    }

    /**
     * Returns the {@link Typeface} that was registered with the specified name.
     *
     * @param typefaceName a {@code String} with the name of the registered {@code TypeFace}
     * @return a {@link Typeface} or null if not found
     */
    public Typeface getTypeface(String typefaceName) {
        return mTypefaces.get(typefaceName);
    }

    /**
     * Register a {@link Typeface} with the specified name. This name will be able to be referenced
     * using a {@code customTypeface} attribute in the layout files, in order to apply the
     * registered {@code Typeface} to a a view.
     *
     * @param typefaceName a name that will identify this {@code Typeface}
     * @param typeface     a {@link Typeface} instance to register
     */
    public void registerTypeface(String typefaceName, Typeface typeface) {
        mTypefaces.put(typefaceName, typeface);
    }

    /**
     * This is a shortcut to let {@code CustomTypeface} create directly a {@link Typeface}
     * for you. This will create the Typeface from a file located in the assets directory.
     * For more information see the {@link #registerTypeface(String, Typeface)} method.
     *
     * @param typefaceName a name that will identify this {@code Typeface}
     * @param assets       a instance of {@link AssetManager}
     * @param filePath     a path to a TTF file located inside the assets folder
     *
     * @see #registerTypeface(String, Typeface)
     */
    public void registerTypeface(String typefaceName, AssetManager assets, String filePath) {
        Typeface typeface = Typeface.createFromAsset(assets, filePath);
        mTypefaces.put(typefaceName, typeface);
    }

    /**
     * Apply a custom {@literal Typeface} if it has a {@code customTypeface} attribute.
     * This method will search for a {@code customTypeface} attribute looking in the following
     * places:
     *
     * <ul>
     * <li>Attributes of the tag defined in the layout</li>
     * <li>Attributes of the style applied to the tag</li>
     * <li>Attributes of the default style defined in the theme</li>
     * <li>Attributes of textAppearance found in any of previous places</li>
     * </ul>
     *
     * <p>
     * If after search in the previous places it has not found a {@code customTypeface}, will
     * over iterate all the parent classes searching in the default styles until a
     * {@code customTypeface} is found.
     * </p>
     *
     * <p>
     * This method will also look for the {@code customTypefaceIgnoreParents} attribute
     * in the same places as the {@code customTypeface} attribute. If this boolean attribute is
     * found and it's set to true, it will ignore any {@code customTypeface} defined in the parent
     * classes.
     * </p>
     *
     * @param view  the {@code View} to apply the typefaces
     * @param attrs attributes object extracted in the layout inflation
     */
    public void applyTypeface(View view, AttributeSet attrs) {
        if (!(view instanceof TextView) || view.getContext() == null) {
            return;
        }
        TextView textView = (TextView) view;
        Resources.Theme theme = view.getContext().getTheme();

        List<Integer> defStyleAttrs = getHierarchyDefStyleAttrs(textView.getClass());
        for (Integer defStyleAttr : defStyleAttrs) {
            boolean applied = applyTypeface(textView, defStyleAttr, attrs, theme);
            if (applied) {
                break;
            }
        }
    }

    private List<Integer> getHierarchyDefStyleAttrs(Class<?> clazz) {
        List<Integer> attrs = new ArrayList<Integer>();
        while (clazz != null) {
            Integer attr = mDefStyleAttrs.get(clazz);
            if (attr != null) {
                attrs.add(attr);
            }
            clazz = clazz.getSuperclass();
        }
        attrs.add(0);
        return attrs;
    }

    private boolean applyTypeface(TextView textView, int defStyleAttr,
            AttributeSet attrs, Resources.Theme theme) {

        boolean applied = false;

        TypedArray typedArray = theme.obtainStyledAttributes(attrs,
                new int[]{android.R.attr.textAppearance}, defStyleAttr, 0);
        int taResId = typedArray.getResourceId(0, -1);
        typedArray.recycle();
        if (taResId != -1) {
            typedArray = theme.obtainStyledAttributes(taResId, new int[]{
                    R.attr.customTypeface, R.attr.customTypefaceIgnoreParents});
            applied = applyTypedArray(textView, typedArray);
            typedArray.recycle();
        }

        typedArray = theme.obtainStyledAttributes(attrs,
                R.styleable.CustomTypeface, defStyleAttr, 0);
        try {
            return applyTypedArray(textView, typedArray) || applied;
        } finally {
            typedArray.recycle();
        }
    }

    private boolean applyTypedArray(TextView textView, TypedArray typedArray) {
        boolean ignoreParents = typedArray.getBoolean(
                R.styleable.CustomTypeface_customTypefaceIgnoreParents, false);
        String typefaceName = typedArray.getString(
                R.styleable.CustomTypeface_customTypeface);

        if (ignoreParents && typefaceName == null) {
            return true;
        } else  if (typefaceName != null) {
            Typeface typeface = getTypeface(typefaceName);
            if (typeface != null) {
                textView.setTypeface(typeface);
            }
            return true;
        } else {
            return false;
        }
    }

    private static class SingletonHolder {
        public static final CustomTypeface instance = new CustomTypeface();

        static {
            registerAttributesForDefaultStyles(instance);
        }
    }
}