package uk.co.chrisjenx.calligraphy;

import android.content.Context;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.text.Editable;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.widget.TextView;

/**
 * Created by chris on 20/12/2013
 * Project: Calligraphy
 */
public final class CalligraphyUtils {

    public static final int[] ANDROID_ATTR_TEXT_APPEARANCE = new int[]{android.R.attr.textAppearance};

    /**
     * Applies a custom typeface span to the text.
     *
     * @param s        text to apply it too.
     * @param typeface typeface to apply.
     * @return Either the passed in Object or new Spannable with the typeface span applied.
     */
    public static CharSequence applyTypefaceSpan(CharSequence s, Typeface typeface) {
        if (s != null && s.length() > 0) {
            if (!(s instanceof Spannable)) {
                s = new SpannableString(s);
            }
            ((Spannable) s).setSpan(TypefaceUtils.getSpan(typeface), 0, s.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        }
        return s;
    }

    /**
     * Applies a Typeface to a TextView.
     * Defaults to false for deferring, if you are having issues with the textview keeping
     * the custom Typeface, use
     * {@link #applyFontToTextView(android.widget.TextView, android.graphics.Typeface, boolean)}
     *
     * @param textView Not null, TextView or child of.
     * @param typeface Not null, Typeface to apply to the TextView.
     * @return true if applied otherwise false.
     * @see #applyFontToTextView(android.widget.TextView, android.graphics.Typeface, boolean)
     */
    public static boolean applyFontToTextView(final TextView textView, final Typeface typeface) {
        return applyFontToTextView(textView, typeface, false);
    }

    /**
     * Applies a Typeface to a TextView, if deferred,its recommend you don't call this multiple
     * times, as this adds a TextWatcher.
     *
     * Deferring should really only be used on tricky views which get Typeface set by the system at
     * weird times.
     *
     * @param textView Not null, TextView or child of.
     * @param typeface Not null, Typeface to apply to the TextView.
     * @param deferred If true we use Typefaces and TextChange listener to make sure font is always
     *                 applied, but this sometimes conflicts with other
     *                 {@link android.text.Spannable}'s.
     * @return true if applied otherwise false.
     * @see #applyFontToTextView(android.widget.TextView, android.graphics.Typeface)
     */
    public static boolean applyFontToTextView(final TextView textView, final Typeface typeface, boolean deferred) {
        if (textView == null || typeface == null) return false;
        textView.setPaintFlags(textView.getPaintFlags() | Paint.SUBPIXEL_TEXT_FLAG | Paint.ANTI_ALIAS_FLAG);
        textView.setTypeface(typeface);
        if (deferred) {
            textView.setText(applyTypefaceSpan(textView.getText(), typeface), TextView.BufferType.SPANNABLE);
            textView.addTextChangedListener(new TextWatcher() {
                @Override
                public void beforeTextChanged(CharSequence s, int start, int count, int after) {
                }

                @Override
                public void onTextChanged(CharSequence s, int start, int before, int count) {
                }

                @Override
                public void afterTextChanged(Editable s) {
                    applyTypefaceSpan(s, typeface);
                }
            });
        }
        return true;
    }

    /**
     * Useful for manually fonts to a TextView. Will not default back to font
     * set in {@link uk.co.chrisjenx.calligraphy.CalligraphyConfig}
     *
     * @param context  Context
     * @param textView Not null, TextView to apply to.
     * @param filePath if null/empty will do nothing.
     * @return true if fonts been applied
     */
    public static boolean applyFontToTextView(final Context context, final TextView textView, final String filePath) {
        return applyFontToTextView(context, textView, filePath, false);
    }

    static boolean applyFontToTextView(final Context context, final TextView textView, final String filePath, boolean deferred) {
        if (textView == null || context == null) return false;
        final AssetManager assetManager = context.getAssets();
        final Typeface typeface = TypefaceUtils.load(assetManager, filePath);
        return applyFontToTextView(textView, typeface, deferred);
    }

    static void applyFontToTextView(final Context context, final TextView textView, final CalligraphyConfig config) {
        applyFontToTextView(context, textView, config, false);
    }

    static void applyFontToTextView(final Context context, final TextView textView, final CalligraphyConfig config, boolean deferred) {
        if (context == null || textView == null || config == null) return;
        if (!config.isFontSet()) return;
        applyFontToTextView(context, textView, config.getFontPath(), deferred);
    }

    /**
     * Applies font to TextView. Will fall back to the default one if not set.
     *
     * @param context      context
     * @param textView     textView to apply to.
     * @param config       Default Config
     * @param textViewFont nullable, will use Default Config if null or fails to find the
     *                     defined font.
     */
    public static void applyFontToTextView(final Context context, final TextView textView, final CalligraphyConfig config, final String textViewFont) {
        applyFontToTextView(context, textView, config, textViewFont, false);
    }

    static void applyFontToTextView(final Context context, final TextView textView, final CalligraphyConfig config, final String textViewFont, boolean deferred) {
        if (context == null || textView == null || config == null) return;
        if (!TextUtils.isEmpty(textViewFont) && applyFontToTextView(context, textView, textViewFont, deferred)) {
            return;
        }
        applyFontToTextView(context, textView, config, deferred);
    }

    /**
     * Tries to pull the Custom Attribute directly from the TextView.
     *
     * @param context     Activity Context
     * @param attrs       View Attributes
     * @param attributeId if -1 returns null.
     * @return null if attribute is not defined or added to View
     */
    static String pullFontPathFromView(Context context, AttributeSet attrs, int[] attributeId) {
        if (attributeId == null || attrs == null)
            return null;

        final String attributeName;
        try {
            attributeName = context.getResources().getResourceEntryName(attributeId[0]);
        } catch (Resources.NotFoundException e) {
            // invalid attribute ID
            return null;
        }

        final int stringResourceId = attrs.getAttributeResourceValue(null, attributeName, -1);
        return stringResourceId > 0
                ? context.getString(stringResourceId)
                : attrs.getAttributeValue(null, attributeName);
    }

    /**
     * Tries to pull the Font Path from the View Style as this is the next decendent after being
     * defined in the View's xml.
     *
     * @param context     Activity Activity Context
     * @param attrs       View Attributes
     * @param attributeId if -1 returns null.
     * @return null if attribute is not defined or found in the Style
     */
    static String pullFontPathFromStyle(Context context, AttributeSet attrs, int[] attributeId) {
        if (attributeId == null || attrs == null)
            return null;
        final TypedArray typedArray = context.obtainStyledAttributes(attrs, attributeId);
        if (typedArray != null) {
            try {
                // First defined attribute
                String fontFromAttribute = typedArray.getString(0);
                if (!TextUtils.isEmpty(fontFromAttribute)) {
                    return fontFromAttribute;
                }
            } catch (Exception ignore) {
                // Failed for some reason.
            } finally {
                typedArray.recycle();
            }
        }
        return null;
    }

    /**
     * Tries to pull the Font Path from the Text Appearance.
     *
     * @param context     Activity Context
     * @param attrs       View Attributes
     * @param attributeId if -1 returns null.
     * @return returns null if attribute is not defined or if no TextAppearance is found.
     */
    static String pullFontPathFromTextAppearance(final Context context, AttributeSet attrs, int[] attributeId) {
        if (attributeId == null || attrs == null) {
            return null;
        }

        int textAppearanceId = -1;
        final TypedArray typedArrayAttr = context.obtainStyledAttributes(attrs, ANDROID_ATTR_TEXT_APPEARANCE);
        if (typedArrayAttr != null) {
            try {
                textAppearanceId = typedArrayAttr.getResourceId(0, -1);
            } catch (Exception ignored) {
                // Failed for some reason
                return null;
            } finally {
                typedArrayAttr.recycle();
            }
        }

        final TypedArray textAppearanceAttrs = context.obtainStyledAttributes(textAppearanceId, attributeId);
        if (textAppearanceAttrs != null) {
            try {
                return textAppearanceAttrs.getString(0);
            } catch (Exception ignore) {
                // Failed for some reason.
                return null;
            } finally {
                textAppearanceAttrs.recycle();
            }
        }
        return null;
    }

    /**
     * Last but not least, try to pull the Font Path from the Theme, which is defined.
     *
     * @param context     Activity Context
     * @param styleAttrId Theme style id
     * @param attributeId if -1 returns null.
     * @return null if no theme or attribute defined.
     */
    static String pullFontPathFromTheme(Context context, int styleAttrId, int[] attributeId) {
        if (styleAttrId == -1 || attributeId == null)
            return null;

        final Resources.Theme theme = context.getTheme();
        final TypedValue value = new TypedValue();

        theme.resolveAttribute(styleAttrId, value, true);
        final TypedArray typedArray = theme.obtainStyledAttributes(value.resourceId, attributeId);
        try {
            String font = typedArray.getString(0);
            return font;
        } catch (Exception ignore) {
            // Failed for some reason.
            return null;
        } finally {
            typedArray.recycle();
        }
    }

    /**
     * Last but not least, try to pull the Font Path from the Theme, which is defined.
     *
     * @param context        Activity Context
     * @param styleAttrId    Theme style id
     * @param subStyleAttrId the sub style from the theme to look up after the first style
     * @param attributeId    if -1 returns null.
     * @return null if no theme or attribute defined.
     */
    static String pullFontPathFromTheme(Context context, int styleAttrId, int subStyleAttrId, int[] attributeId) {
        if (styleAttrId == -1 || attributeId == null)
            return null;

        final Resources.Theme theme = context.getTheme();
        final TypedValue value = new TypedValue();

        theme.resolveAttribute(styleAttrId, value, true);
        int subStyleResId = -1;
        final TypedArray parentTypedArray = theme.obtainStyledAttributes(value.resourceId, new int[]{subStyleAttrId});
        try {
            subStyleResId = parentTypedArray.getResourceId(0, -1);
        } catch (Exception ignore) {
            // Failed for some reason.
            return null;
        } finally {
            parentTypedArray.recycle();
        }

        if (subStyleResId == -1) return null;
        final TypedArray subTypedArray = context.obtainStyledAttributes(subStyleResId, attributeId);
        if (subTypedArray != null) {
            try {
                return subTypedArray.getString(0);
            } catch (Exception ignore) {
                // Failed for some reason.
                return null;
            } finally {
                subTypedArray.recycle();
            }
        }
        return null;
    }

    private static Boolean sToolbarCheck = null;
    private static Boolean sAppCompatViewCheck = null;

    /**
     * See if the user has added appcompat-v7, this is done at runtime, so we only check once.
     *
     * @return true if the v7.Toolbar is on the classpath
     */
    static boolean canCheckForV7Toolbar() {
        if (sToolbarCheck == null) {
            try {
                Class.forName("android.support.v7.widget.Toolbar");
                sToolbarCheck = Boolean.TRUE;
            } catch (ClassNotFoundException e) {
                sToolbarCheck = Boolean.FALSE;
            }
        }
        return sToolbarCheck;
    }

    /**
     * See if the user has added appcompat-v7 with AppCompatViews
     *
     * @return true if AppcompatTextView is on the classpath
     */
    static boolean canAddV7AppCompatViews() {
        if (sAppCompatViewCheck == null) {
            try {
                Class.forName("android.support.v7.widget.AppCompatTextView");
                sAppCompatViewCheck = Boolean.TRUE;
            } catch (ClassNotFoundException e) {
                sAppCompatViewCheck = Boolean.FALSE;
            }
        }
        return sAppCompatViewCheck;
    }

    private CalligraphyUtils() {
    }

}