package com.ftinc.scoop;

import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.PorterDuff;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.support.annotation.ColorInt;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StyleRes;
import android.support.annotation.UiThread;
import android.support.v7.app.AppCompatDelegate;
import android.util.Log;
import android.util.SparseArray;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.animation.Interpolator;

import com.ftinc.scoop.adapters.ColorAdapter;
import com.ftinc.scoop.binding.AnimatedBinding;
import com.ftinc.scoop.binding.IBinding;
import com.ftinc.scoop.binding.StatusBarBinding;
import com.ftinc.scoop.binding.ViewBinding;
import com.ftinc.scoop.internal.ToppingBinder;
import com.ftinc.scoop.util.AttrUtils;
import com.ftinc.scoop.util.BindingUtils;

import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import static com.ftinc.scoop.SugarCone.BINDERS;
import static com.ftinc.scoop.SugarCone.NOP_VIEW_BINDER;

/**
 * Project: ThemeEngineTest
 * Package: com.ftinc.scoop
 * Created by drew.heavner on 6/7/16.
 */

public class Scoop {
    private static final String TAG = "Scoop";

    /***********************************************************************************************
     *
     * Singleton Configuration
     *
     */

    private static Scoop _instance = null;

    // TODO: Find a better name for this
    public static Scoop getInstance(){
        if(_instance == null) _instance = new Scoop();
        return _instance;
    }

    /**
     * Create a builder instance for this class to initialize the library
     *
     * @return      the Builder to initialize the library with
     */
    public static Builder waffleCone(){
        return new Builder();
    }

    /**
     * @deprecated Please just use the {@link #getInstance()} method of Scoop to access
     * the bind methods
     */
    @Deprecated
    public static SugarCone sugarCone(){
        Scoop instance = getInstance();
        instance.checkInit();
        return instance.mSugarCone;
    }

    /***********************************************************************************************
     *
     * Constants
     *
     */

    static final String PREFERENCE_FLAVOR_KEY = "com.ftinc.scoop.preference.FLAVOR_KEY";
    static final String PREFERENCE_DAYNIGHT_KEY = "com.ftinc.scoop.preference.DAY_NIGHT_KEY";

    /***********************************************************************************************
     *
     * Variables
     *
     */

    /**
     * Static mapping of all the available base application themes to use/apply
     * mapped by a developer defined ID
     */
    private List<Flavor> mFlavors = new ArrayList<>();

    /**
     * Mapping of all the toppings that the user has binded for
     */
    private SparseArray<Topping> mToppings = new SparseArray<>();

    /**
     * Mapping of all the bindings per class
     */
    private HashMap<Class, Set<IBinding>> mBindings = new HashMap<>();

    /**
     * The index of the default flavor value
     */
    private int mDefaultFlavorIndex = 0;

    /**
     * Determine if initialized
     */
    private boolean mInitialized = false;

    /**
     * SharedPreference store to handle and save changes to the base theme configuration
     */
    private SharedPreferences mPreferences;

    /**
     * SugarCone instance to track the deeper color customizations
     */
    private SugarCone mSugarCone;

    /**
     * Debug flag for logging
     */
    private static boolean debug = false;

    /**
     * Private constructor to prevent initialization
     */
    private Scoop(){}

    /***********************************************************************************************
     *
     * Private Helper Methods
     *
     */

    /**
     * Initialize this helper class with the provided builder
     * @param builder
     */
    private void initialize(Builder builder){
        // Validate builder
        if(builder.prefs != null && !builder.flavors.isEmpty()){

            // Set Preference Storage
            mPreferences = builder.prefs;

            // Set Flavors
            mFlavors = new ArrayList<>(builder.flavors);

            // Set the default flavor if configured
            if(builder.defaultFlavor != null){
                mDefaultFlavorIndex = mFlavors.indexOf(builder.defaultFlavor);
            }

            // Deprecated
            mSugarCone = new SugarCone();

            // Set init flag
            mInitialized = true;

        }else {
            throw new IllegalStateException("SharedPreferences and at least one flavor must be set");
        }
    }

    /**
     * Get the index of the current configured flavor
     *
     * @return      the index of the current flavor to apply
     */
    private int getCurrentFlavorIndex(){
        checkInit();

        // Get the selected flavor index from the preference storage
        int flavorIndex = mPreferences.getInt(PREFERENCE_FLAVOR_KEY, mDefaultFlavorIndex);

        // Verify that index is valid
        if(flavorIndex > -1 && flavorIndex < mFlavors.size()){
            return flavorIndex;
        }

        return mDefaultFlavorIndex;
    }

    /**
     * Get the current selected scoop of flavor
     *
     * @param excludeDefault        whether or not to return null if the current selected is the default theme
     * @return                      the current scoop of flavor
     */
    private Flavor getCurrentFlavor(boolean excludeDefault){
        int index = getCurrentFlavorIndex();
        if(index != mDefaultFlavorIndex || !excludeDefault) {
            return mFlavors.get(index);
        }
        return null;
    }

    /**
     * Verify the initialization state of the utility
     */
    private void checkInit(){
        if(!mInitialized) throw new IllegalStateException("Scoop needs to be initialized first!");
    }

    @NonNull @UiThread
    private ToppingBinder<Object> getViewBinder(@NonNull Object target) {
        Class<?> targetClass = target.getClass();
        if (debug) Log.d(TAG, "Looking up topping binder for " + targetClass.getName());
        return findViewBinderForClass(targetClass);
    }

    @NonNull @UiThread
    private ToppingBinder<Object> findViewBinderForClass(Class<?> cls) {
        ToppingBinder<Object> viewBinder = BINDERS.get(cls);
        if (viewBinder != null) {
            if (debug) Log.d(TAG, "HIT: Cached in topping binder map.");
            return viewBinder;
        }
        String clsName = cls.getName();
        if (clsName.startsWith("android.") || clsName.startsWith("java.")) {
            if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
            return NOP_VIEW_BINDER;
        }
        //noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
        try {
            Class<?> viewBindingClass = Class.forName(clsName + "_ToppingBinder");
            //noinspection unchecked
            viewBinder = (ToppingBinder<Object>) viewBindingClass.newInstance();
            if (debug) Log.d(TAG, "HIT: Loaded topping binder class.");
        } catch (ClassNotFoundException e) {
            if (debug) Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName());
            viewBinder = findViewBinderForClass(cls.getSuperclass());
        } catch (InstantiationException e) {
            throw new RuntimeException("Unable to create topping binder for " + clsName, e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException("Unable to create topping binder for " + clsName, e);
        }
        BINDERS.put(cls, viewBinder);
        return viewBinder;
    }

    /**
     * Get the set of bindings for a given class
     *
     * @param clazz     the class key for the bindings to look up
     * @return          the set of bindings for the class
     */
    private Set<IBinding> getBindings(Class clazz){
        Set<IBinding> bindings = mBindings.get(clazz);
        if(bindings == null){
            bindings = new HashSet<>();
            mBindings.put(clazz, bindings);
        }
        return bindings;
    }

    /**
     * Find the {@link Topping} object for it's given Id or create one if not found
     *
     * @param toppingId         the id of the topping to get
     * @return                  the topping associated with the id
     */
    private Topping getOrCreateTopping(int toppingId){
        Topping topping = mToppings.get(toppingId);
        if(topping == null){
            topping = new Topping(toppingId);
            mToppings.put(toppingId, topping);
        }

        return topping;
    }

    private void autoUpdateBinding(IBinding binding, Topping topping){
        if(topping.getColor() != 0) {
            if (binding instanceof AnimatedBinding) {
                ((AnimatedBinding) binding).update(topping, false);
            } else {
                binding.update(topping);
            }
        }
    }

    /***********************************************************************************************
     *
     * Public Methods
     *
     */

    /**
     * Enable debug logging
     */
    public static void setDebug(boolean flag){
        debug = flag;
    }

    /**
     * Get the selected day night mode to use with certain themes
     *
     * @return      the day night mode to use
     */
    @AppCompatDelegate.NightMode
    public int getDayNightMode(){
        checkInit();
        return mPreferences.getInt(PREFERENCE_DAYNIGHT_KEY, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
    }

    /**
     * Get the list of available flavors that you can scoop from
     *
     * @return
     */
    public List<Flavor> getFlavors(){
        return Collections.unmodifiableList(mFlavors);
    }

    /**
     * Get the current flavor to apply
     *
     * @return      one scoop of ice cream
     */
    public Flavor getCurrentFlavor(){
        return getCurrentFlavor(false);
    }

    /**
     * Apply the current {@link Flavor} to the given activity based on the user's selected preference.
     *
     * @param activity      the activity to apply the selected theme configuration to
     */
    @SuppressWarnings("WrongConstant")
    public void apply(Activity activity){
        Flavor flavor = getCurrentFlavor(true);
        if(flavor != null){
            // Apply DayNight mode setting if applicable
            if(flavor.isDayNight()){
                AppCompatDelegate.setDefaultNightMode(getDayNightMode());
            }

            // Apply theme
            apply(activity, flavor.getStyleResource());
        }
    }

    /**
     * Apply the current {@link Flavor}s Dialog theme to the activity to give it a Dialog like
     * appearance based on the user selected preference
     *
     * @param activity      the activity to apply the dialog theme to
     */
    @SuppressWarnings("WrongConstant")
    public void applyDialog(Activity activity){
        Flavor flavor = getCurrentFlavor(true);
        if(flavor != null && flavor.getDialogStyleResource() > -1){
            // Apply DayNight mode setting if applicable
            if(flavor.isDayNight()){
                AppCompatDelegate.setDefaultNightMode(getDayNightMode());
            }

            // Apply theme
            apply(activity, flavor.getDialogStyleResource());
        }
    }

    /**
     * Apply the desired theme to an activity and it's window
     *
     * @param activity      the activity to apply to
     * @param theme         the theme to apply
     */
    private void apply(Activity activity, @StyleRes int theme){
        // Apply theme
        activity.setTheme(theme);

        // Ensure window background get's properly set
        int color = AttrUtils.getColorAttr(activity, android.R.attr.colorBackground);
        activity.getWindow().setBackgroundDrawable(new ColorDrawable(color));
    }

    /**
     * Apply the attributed menu item tint to all the icons if the attribute {@link R.attr#toolbarItemTint}
     *
     * @param context      the application context to derive the attr color from
     * @param menu          the menu to apply to
     */
    public void apply(Context context, Menu menu){
        Flavor flavor = getCurrentFlavor();
        if(menu != null && menu.size() > 0 && flavor != null){
            int tint = AttrUtils.getColorAttr(context, flavor.getStyleResource(), R.attr.toolbarItemTint);
            for (int i = 0; i < menu.size(); i++) {
                MenuItem item = menu.getItem(i);
                Drawable icon = item.getIcon();
                if(icon != null){
                    if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
                        icon.setTint(tint);
                    }else{
                        icon.setColorFilter(tint, PorterDuff.Mode.SRC_ATOP);
                    }
                }
            }
        }
    }

    /**
     * Choose a given flavor
     *
     * @param item      the flavor to scoop
     */
    public void choose(Flavor item) {
        checkInit();
        int index = mFlavors.indexOf(item);
        mPreferences.edit().putInt(PREFERENCE_FLAVOR_KEY, index).apply();
    }

    /**
     * Choose the DayNight mode you want to use for selected day/night mode themes
     *
     * @param mode      the daynight mode you wish to use
     */
    public void chooseDayNightMode(@AppCompatDelegate.NightMode int mode){
        checkInit();
        mPreferences.edit().putInt(PREFERENCE_DAYNIGHT_KEY, mode).apply();
    }

    /***********************************************************************************************
     *
     * Topping Binding Methods
     *
     */

    /**
     * Bind all the annotated elements to a given activity
     *
     * @see BindTopping
     * @see BindToppingStatus
     *
     * @param activity      the activity to bind to
     */
    public void bind(Activity activity){
        // Get the pre-genereated bindings
        List<IBinding> bindings = getViewBinder(activity).bind(activity);

        // Iterate and verify topping creation and auto-applying
        for (IBinding binding : bindings) {
            Topping topping = getOrCreateTopping(binding.getToppingId());
            autoUpdateBinding(binding, topping);
        }

        // add to system
        Set<IBinding> _bindings = getBindings(activity.getClass());
        _bindings.addAll(bindings);
    }

    /**
     * Bind a view to a topping on a given object
     *
     * @param obj               the class the view belongs to
     * @param toppingId         the id of the topping to bind to
     * @param view              the view to bind
     * @return                  self for chaining
     */
    public Scoop bind(Object obj, int toppingId, View view){
        return bind(obj, toppingId, view, null);
    }

    /**
     * Bind a view to a topping on a given object with a specified color adapter
     *
     * @param obj               the classs the view belongs to
     * @param toppingId         the id of the topping
     * @param view              the view to bind
     * @param colorAdapter      the color adapter to bind with
     * @return                  self for chaining
     */
    public Scoop bind(Object obj, int toppingId, View view, @Nullable ColorAdapter colorAdapter){
        return bind(obj, toppingId, view, colorAdapter, null);
    }

    /**
     * Bind a view to a topping on a given object with a specified color adapter and change animation
     * interpolator
     *
     * @param obj               the class the view belongs to
     * @param toppingId         the id of the topping
     * @param view              the view to bind
     * @param colorAdapter      the color adapter to bind with
     * @param interpolator      the interpolator to use when switching colors
     * @return                  self for chaining
     */
    public Scoop bind(Object obj, int toppingId, View view, @Nullable ColorAdapter colorAdapter, @Nullable Interpolator interpolator){

        // Get a default color adapter if not supplied
        if(colorAdapter == null){
            colorAdapter = BindingUtils.getColorAdapter(view.getClass());
        }

        // Generate Binding
        IBinding binding = new ViewBinding(toppingId, view, colorAdapter, interpolator);

        // Bind
        return bind(obj, toppingId, binding);
    }

    /**
     * Bind the status bar of an activity to a topping so that it's color is updated when the
     * user/developer updates the color for that topping id
     *
     * @param activity      the activity whoes status bar to bind to
     * @param toppingId     the id of the topping to bind with
     * @return              self for chaining
     */
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public Scoop bindStatusBar(Activity activity, int toppingId){
        return bindStatusBar(activity, toppingId, null);
    }

    /**
     * Bind the status bar of an activity to a topping so that it's color is updated when the
     * user/developer updates the color for that topping id and animation it's color change using
     * the provided interpolator
     *
     * @param activity      the activity whoes status bar to bind to
     * @param toppingId     the id of the topping to bind with
     * @param interpolator  the interpolator that defines how the animation for the color change will run
     * @return              self for chaining
     */
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public Scoop bindStatusBar(Activity activity, int toppingId, @Nullable Interpolator interpolator){
        IBinding binding = new StatusBarBinding(toppingId, activity, interpolator);
        return bind(activity, toppingId, binding);
    }

    /**
     * Provide a custom binding to a certain topping id on a given object. This allows you to
     * customize the changes between color on certain properties, i.e. Toppings, to define it
     * to your use case
     *
     * @param obj               the object to bind on
     * @param toppingId         the topping id to bind to
     * @param binding           the binding that defines how your custom properties are updated
     * @return                  self for chaining
     */
    public Scoop bind(Object obj, int toppingId, IBinding binding){

        // Find or Create Topping
        Topping topping = getOrCreateTopping(toppingId);

        // If topping has a color set, auto-apply to binding
        autoUpdateBinding(binding, topping);

        // Store binding
        Set<IBinding> bindings = getBindings(obj.getClass());
        bindings.add(binding);

        return this;
    }

    /**
     * Unbind all bindings on a certain class
     *
     * @param obj       the class/object that you previously made bindings to (i.e. an Activity, or Fragment)
     */
    public void unbind(Object obj){
        Set<IBinding> bindings = getBindings(obj.getClass());
        for (IBinding binding : bindings) {
            binding.unbind();
        }

        // Clear the bindings out of the map
        mBindings.remove(obj.getClass());
    }

    /**
     * Update a topping, i.e. color property, with a new color and therefore sending it out to
     * all your bindings
     *
     * @param toppingId     the id of the topping you wish to update
     * @param color         the updated color to update to
     * @return              self for chaining.
     */
    public Scoop update(int toppingId, @ColorInt int color){

        // Get the topping
        Topping topping = mToppings.get(toppingId);
        if(topping != null){
            topping.updateColor(color);

            // Update bindings
            Collection<Set<IBinding>> bindings = mBindings.values();
            for (Set<IBinding> bindingSet : bindings) {
                for (IBinding binding : bindingSet) {
                    if(binding.getToppingId() == toppingId) {
                        binding.update(topping);
                    }
                }
            }

        }else{
            throw new InvalidParameterException("Nothing has been bound to the Topping of the given id (" + toppingId + ").");
        }

        return this;
    }

    /***********************************************************************************************
     *
     * Initialization Builder
     *
     */

    public static class Builder{

        private SharedPreferences prefs;
        private Flavor defaultFlavor;
        private final List<Flavor> flavors;

        Builder(){
            flavors = new ArrayList<>();
        }

        public Builder addFlavor(String name,
                                 @StyleRes int styleResourceId){
            return addFlavor(name, styleResourceId, -1, false);
        }

        public Builder addDayNightFlavor(String name,
                                 @StyleRes int styleResourceId){
            return addFlavor(name, styleResourceId, -1, false, true);
        }

        public Builder addFlavor(String name,
                                 @StyleRes int styleResourceId,
                                 boolean isDefault){
            return addFlavor(name, styleResourceId, -1, isDefault);
        }

        public Builder addDayNightFlavor(String name,
                                 @StyleRes int styleResourceId,
                                 boolean isDefault){
            return addFlavor(name, styleResourceId, -1, isDefault, true);
        }

        public Builder addFlavor(String name,
                                 @StyleRes int styleResourceId,
                                 @StyleRes int dialogStyleResourceId){
            return addFlavor(name, styleResourceId, dialogStyleResourceId, false);
        }

        public Builder addFlavor(String name,
                                 @StyleRes int styleResourceId,
                                 @StyleRes int dialogStyleResourceId,
                                 boolean isDefault){
            return addFlavor(name, styleResourceId, dialogStyleResourceId, isDefault, false);
        }

        public Builder addFlavor(String name,
                                 @StyleRes int styleResourceId,
                                 @StyleRes int dialogStyleResourceId,
                                 boolean isDefault,
                                 boolean isDayNight){
            Flavor flavor = new Flavor(name, styleResourceId, dialogStyleResourceId, isDayNight);
            if(isDefault) defaultFlavor = flavor;
            return addFlavor(flavor);
        }

        public Builder addFlavor(Flavor... flavor){
            flavors.addAll(Arrays.asList(flavor));
            return this;
        }

        /**
         * @deprecated      Toppings no longer need to be pre-instantiated
         */
        @Deprecated
        public Builder addToppings(Topping... toppings){
            // This does nothing now
            return this;
        }

        public Builder setSharedPreferences(SharedPreferences prefs){
            this.prefs = prefs;
            return this;
        }

        public void initialize(){
            Scoop.getInstance()
                    .initialize(this);
        }

    }



}