/*
 * Copyright 2015 - 2019 Michael Rapp
 *
 * 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 de.mrapp.android.view;

import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.util.AttributeSet;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.annotation.AttrRes;
import androidx.annotation.ColorInt;
import androidx.annotation.ColorRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.annotation.StyleRes;
import androidx.core.content.ContextCompat;
import de.mrapp.android.util.ViewUtil;
import de.mrapp.util.Condition;
import de.mrapp.util.datastructure.ListenerList;

import static de.mrapp.android.util.BitmapUtil.clipCircle;
import static de.mrapp.android.util.BitmapUtil.drawableToBitmap;

/**
 * A chip, which has been designed according to the Material design guidelines.
 *

 * Refer to http://www.google.com/design/spec/components/chips.html for further information on the
 * Material design guidelines.
 *
 * @author Michael Rapp
 * @since 1.0.0
 * @deprecated Since version 3.0.0 this class is deprecated in favor of the Chip, which
 * is now part of Android's official Design support library
 */
@Deprecated
public class Chip extends FrameLayout {

    /**
     * Defines the interface, a class, which should be notified, when a chip has been closed, must
     * implement.
     */
    public interface CloseListener {

        /**
         * The method, which is invoked, when a chip has been closed.
         *
         * @param chip
         *         The chip, which has been closed, as an instance of the class {@link Chip}
         */
        void onChipClosed(@NonNull Chip chip);

    }

    /**
     * The image view, which is used to show the chip's icon.
     */
    private ImageView imageView;

    /**
     * The text view, which is used to show the chip's text.
     */
    private TextView textView;

    /**
     * The image button, which allows to close the chip.
     */
    private ImageButton closeButton;

    /**
     * The chip's color.
     */
    private int color;

    /**
     * True, if the chip is closable, false otherwise.
     */
    private boolean closable;

    /**
     * A set, which contains the listeners, which should be notified, when the chip has been
     * closed.
     */
    private ListenerList<CloseListener> listeners;

    /**
     * Initializes the view.
     *
     * @param attributeSet
     *         The attribute set, the view's attributes should be obtained from, as an instance of
     *         the type {@link AttributeSet} or null, if no attributes should be obtained
     */
    private void initialize(@Nullable final AttributeSet attributeSet) {
        listeners = new ListenerList<>();
        inflateLayout();
        Drawable background = ContextCompat.getDrawable(getContext(), R.drawable.chip_background);
        ViewUtil.setBackground(this, background);
        obtainStyledAttributes(attributeSet);
    }

    /**
     * Inflates the view's layout.
     */
    private void inflateLayout() {
        View view = View.inflate(getContext(), R.layout.chip, null);
        imageView = view.findViewById(android.R.id.icon);
        textView = view.findViewById(android.R.id.text1);
        closeButton = view.findViewById(android.R.id.closeButton);
        closeButton.setOnClickListener(createCloseButtonListener());
        int height = getResources().getDimensionPixelSize(R.dimen.chip_height);
        addView(view, LayoutParams.WRAP_CONTENT, height);
    }

    /**
     * Creates and returns a listener, which allows to observe, when the button, which allows to
     * close the chip, has been clicked.
     *
     * @return The listener, which has been created, as an instance of the type {@link
     * OnClickListener}
     */
    private OnClickListener createCloseButtonListener() {
        return new OnClickListener() {

            @Override
            public void onClick(final View v) {
                notifyOnChipClosed();
            }

        };
    }

    /**
     * Obtains the view's attributes from a specific attribute set.
     *
     * @param attributeSet
     *         The attribute set, the view's attributes should be obtained from, as an instance of
     *         the type {@link AttributeSet} or null, if no attributes should be obtained
     */
    private void obtainStyledAttributes(@Nullable final AttributeSet attributeSet) {
        TypedArray typedArray = getContext().obtainStyledAttributes(attributeSet, R.styleable.Chip);

        try {
            obtainText(typedArray);
            obtainTextColor(typedArray);
            obtainColor(typedArray);
            obtainIcon(typedArray);
            obtainClosable(typedArray);
            obtainCloseIcon(typedArray);
        } finally {
            typedArray.recycle();
        }
    }

    /**
     * Obtains the chip's text from a specific typed array.
     *
     * @param typedArray
     *         The typed array, the text should be obtained from, as an instance of the class {@link
     *         TypedArray}. The typed array may not be null
     */
    private void obtainText(@NonNull final TypedArray typedArray) {
        setText(typedArray.getText(R.styleable.Chip_android_text));
    }

    /**
     * Obtains the chip's text color from a specific typed array.
     *
     * @param typedArray
     *         The typed array, the text color should be obtained from, as an instance of the class
     *         {@link TypedArray}. The typed array may not be null
     */
    private void obtainTextColor(@NonNull final TypedArray typedArray) {
        int defaultColor = ContextCompat.getColor(getContext(), R.color.chip_text_color_light);
        setTextColor(typedArray.getColor(R.styleable.Chip_android_textColor, defaultColor));
    }

    /**
     * Obtains the chip's color from a specific typed array.
     *
     * @param typedArray
     *         The typed array, the color should be obtained from, as an instance of the class
     *         {@link TypedArray}. The typed array may not be null
     */
    private void obtainColor(@NonNull final TypedArray typedArray) {
        int defaultColor = ContextCompat.getColor(getContext(), R.color.chip_color_light);
        setColor(typedArray.getColor(R.styleable.Chip_android_color, defaultColor));
    }

    /**
     * Obtains the chip's icon from a specific typed array.
     *
     * @param typedArray
     *         The typed array, the icon should be obtained from, as an instance of the class {@link
     *         TypedArray}. The typed array may not be null
     */
    private void obtainIcon(@NonNull final TypedArray typedArray) {
        setIcon(typedArray.getDrawable(R.styleable.Chip_android_icon));
    }

    /**
     * Obtains, whether the chip should be closable, or not, from a specific typed array.
     *
     * @param typedArray
     *         The typed array, which should be used to obtain, whether the chip should be closable,
     *         or not, as an instance of the class {@link TypedArray}. The typed array may not be
     *         null
     */
    private void obtainClosable(@NonNull final TypedArray typedArray) {
        setClosable(typedArray.getBoolean(R.styleable.Chip_closable, false));
    }

    /**
     * Obtains the icon of the button, which allows to close the chip, from a specific typed array.
     *
     * @param typedArray
     *         The typed array, which should be used to obtain the icon of the button, which allows
     *         to close the chip, as an instance of the class {@link TypedArray}. The typed array
     *         may not be null
     */
    private void obtainCloseIcon(@NonNull final TypedArray typedArray) {
        Drawable icon = typedArray.getDrawable(R.styleable.Chip_closeButtonIcon);

        if (icon != null) {
            setCloseButtonIcon(icon);
        }
    }

    /**
     * Notifies all listeners, which have been registered to be notified, when the chip has been
     * closed, about the chip being closed.
     */
    private void notifyOnChipClosed() {
        for (CloseListener listener : listeners) {
            listener.onChipClosed(this);
        }
    }

    /**
     * Creates a new chip, which has been designed according to the Material design guidelines.
     *
     * @param context
     *         The context, the view should belong to, as an instance of the class {@link Context}.
     *         The context may not be null
     */
    public Chip(@NonNull final Context context) {
        this(context, null);
    }

    /**
     * Creates a chip, which has been designed according to the Material design guidelines.
     *
     * @param context
     *         The context, the view should belong to, as an instance of the class {@link Context}.
     *         The context may not be null
     * @param attributeSet
     *         The attribute set, the view's attributes should be obtained from, as an instance of
     *         the type {@link AttributeSet} or null, if no attributes should be obtained
     */
    public Chip(@NonNull final Context context, @Nullable final AttributeSet attributeSet) {
        super(context, attributeSet);
        initialize(attributeSet);
    }

    /**
     * Creates a new chip, which has been designed according to the Material design guidelines.
     *
     * @param context
     *         The context, the view should belong to, as an instance of the class {@link Context}.
     *         The context may not be null
     * @param attributeSet
     *         The attribute set, the view's attributes should be obtained from, as an instance of
     *         the type {@link AttributeSet} or null, if no attributes should be obtained
     * @param defaultStyle
     *         The default style to apply to this view. If 0, no style will be applied (beyond what
     *         is included in the theme). This may either be an attribute resource, whose value will
     *         be retrieved from the current theme, or an explicit style resource
     */
    public Chip(final Context context, @Nullable final AttributeSet attributeSet,
                @AttrRes final int defaultStyle) {
        super(context, attributeSet, defaultStyle);
        initialize(attributeSet);
    }

    /**
     * Creates a new chip, which has been designed according to the Material design guidelines.
     *
     * @param context
     *         The context, which should be used by the view, as an instance of the class {@link
     *         Context}. The context may not be null
     * @param attributeSet
     *         The attribute set, the view's attributes should be obtained from, as an instance of
     *         the type {@link AttributeSet} or null, if no attributes should be obtained
     * @param defaultStyle
     *         The default style to apply to this view. If 0, no style will be applied (beyond what
     *         is included in the theme). This may either be an attribute resource, whose value will
     *         be retrieved from the current theme, or an explicit style resource
     * @param defaultStyleResource
     *         A resource identifier of a style resource that supplies default values for the view,
     *         used only if the default style is 0 or can not be found in the theme. Can be 0 to not
     *         look for defaults
     */
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public Chip(@NonNull final Context context, @Nullable final AttributeSet attributeSet,
                @AttrRes final int defaultStyle, @StyleRes final int defaultStyleResource) {
        super(context, attributeSet, defaultStyle, defaultStyleResource);
        initialize(attributeSet);
    }

    /**
     * Adds a new listener, which should be notified, when the chip has been closed.
     *
     * @param listener
     *         The listener, which should be added, as an instance of the type {@link
     *         CloseListener}. The listener may not be null
     */
    public final void addCloseListener(@NonNull final CloseListener listener) {
        Condition.INSTANCE.ensureNotNull(listener, "The listener may not be null");
        listeners.add(listener);
    }

    /**
     * Removes a specific listener, which should not be notified, when the chip has been closed,
     * anymore.
     *
     * @param listener
     *         The listener, which should be removed, as an instance of the type {@link
     *         CloseListener}. The listener may not be null
     */
    public final void removeCloseListener(@NonNull final CloseListener listener) {
        Condition.INSTANCE.ensureNotNull(listener, "The listener may not be null");
        listeners.remove(listener);
    }

    /**
     * Returns the chip's text.
     *
     * @return The chip's text as an instance of the type {@link CharSequence} or null, if no text
     * has been set
     */
    public final CharSequence getText() {
        return textView.getText();
    }

    /**
     * Sets the chip's text.
     *
     * @param resourceId
     *         The resource id of the text, which should be set, as an {@link Integer} value. The
     *         resource id must correspond to a valid string resource
     */
    public final void setText(@StringRes final int resourceId) {
        setText(getContext().getText(resourceId));
    }

    /**
     * Sets the chip's text.
     *
     * @param text
     *         The text, which should be set, as an instance of the type {@link CharSequence} or
     *         null, if no text should be set
     */
    public final void setText(@Nullable final CharSequence text) {
        textView.setText(text);
    }

    /**
     * Sets the chip's text color.
     *
     * @param color
     *         The text color, which should be set, as an {@link Integer} value
     */
    public final void setTextColor(@ColorInt final int color) {
        textView.setTextColor(color);
    }

    /**
     * Sets the chip's text color.
     *
     * @param resourceId
     *         The resource id of the text color, which should be set, as an {@link Integer} value.
     *         The resource id must correspond to a valid color resource
     */
    public final void setTextColorResource(@ColorRes final int resourceId) {
        setTextColor(ContextCompat.getColor(getContext(), resourceId));
    }

    /**
     * Returns the chip's text color.
     *
     * @return The chip's text color as an {@link Integer} value
     */
    public final int getTextColor() {
        return textView.getCurrentTextColor();
    }

    /**
     * Sets the chip's color.
     *
     * @param color
     *         The color, which should be set, as an {@link Integer} value
     */
    public final void setColor(@ColorInt final int color) {
        this.color = color;
        getBackground().setColorFilter(color, PorterDuff.Mode.SRC_IN);
    }

    /**
     * Sets the chip's color.
     *
     * @param resourceId
     *         The resource id of the color, which should be set, as an {@link Integer} value. The
     *         resource id must correspond to a valid color resource
     */
    public final void setColorResource(@ColorRes final int resourceId) {
        setColor(ContextCompat.getColor(getContext(), resourceId));
    }

    /**
     * Returns the chip's color.
     *
     * @return The chip's color as an {@link Integer} value
     */
    public final int getColor() {
        return color;
    }

    /**
     * Sets the chip's icon.
     *
     * @param icon
     *         The icon, which should be set, as an instance of the class {@link Drawable} or null,
     *         if no icon should be set
     */
    public final void setIcon(@Nullable final Drawable icon) {
        setIcon(icon != null ? drawableToBitmap(icon) : null);
    }

    /**
     * Sets the chip's icon.
     *
     * @param icon
     *         The icon, which should be set, as an instance of the class {@link Bitmap} or null, if
     *         no icon should be set
     */
    public final void setIcon(@Nullable final Bitmap icon) {
        if (icon != null) {
            int size = getResources().getDimensionPixelSize(R.dimen.chip_height);
            imageView.setImageBitmap(clipCircle(icon, size));
            imageView.setVisibility(View.VISIBLE);
            textView.setPadding(0, textView.getPaddingTop(), textView.getPaddingRight(),
                    textView.getPaddingBottom());
        } else {
            imageView.setImageBitmap(null);
            imageView.setVisibility(View.GONE);
            textView.setPadding(
                    getResources().getDimensionPixelSize(R.dimen.chip_horizontal_padding),
                    textView.getPaddingTop(), textView.getPaddingRight(),
                    textView.getPaddingBottom());
        }
    }

    /**
     * Returns the chip's icon.
     *
     * @return The chip's icon as an instance of the class {@link Drawable} or null, if no icon has
     * been set
     */
    public final Drawable getIcon() {
        return imageView.getDrawable();
    }

    /**
     * Returns, whether the chip is closable, or not.
     *
     * @return True, if the chip is closable, false otherwise.
     */
    public final boolean isClosable() {
        return closable;
    }

    /**
     * Sets, whether the chip is closable, or not.
     *
     * @param closable
     *         True, if the chip should be closable, false otherwise
     */
    public final void setClosable(final boolean closable) {
        this.closable = closable;

        if (closable) {
            closeButton.setVisibility(View.VISIBLE);
            textView.setPadding(textView.getPaddingLeft(), textView.getPaddingTop(), 0,
                    textView.getPaddingBottom());
        } else {
            closeButton.setVisibility(View.GONE);
            textView.setPadding(textView.getPaddingLeft(), textView.getPaddingTop(),
                    getResources().getDimensionPixelSize(R.dimen.chip_horizontal_padding),
                    textView.getPaddingBottom());
        }
    }

    /**
     * Returns the icon of the button, which allows to close the chip.
     *
     * @return The icon of the button, which allows to close the chip, as an instance of the class
     * {@link Drawable} or null, if the chip is not closable
     */
    public final Drawable getCloseButtonIcon() {
        return closable ? closeButton.getDrawable() : null;
    }

    /**
     * Sets the icon of the button, which allows to close the chip.
     *
     * @param icon
     *         The icon, which should be set, as an instance of the class {@link Drawable}. The icon
     *         may not be null
     */
    public final void setCloseButtonIcon(@NonNull final Drawable icon) {
        Condition.INSTANCE.ensureNotNull(icon, "The icon may not be null");
        closeButton.setImageDrawable(icon);
    }

    /**
     * Sets the icon of the button, which allows to close the chip.
     *
     * @param icon
     *         The icon, which should be set, as an instance of the class {@link Bitmap}. The icon
     *         may not be null
     */
    public final void setCloseButtonIcon(@NonNull final Bitmap icon) {
        Condition.INSTANCE.ensureNotNull(icon, "The icon may not be null");
        closeButton.setImageBitmap(icon);
    }

}


/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2014 <mickael.jeanroy@gmail.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.github.mjeanroy.springmvc.view.mustache.core;

import static org.springframework.util.Assert.notNull;

import java.util.HashMap;
import java.util.Map;

import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.View;

import com.github.mjeanroy.springmvc.view.mustache.MustacheSettings;
import com.github.mjeanroy.springmvc.view.mustache.MustacheView;
import com.github.mjeanroy.springmvc.view.mustache.exceptions.MustachePartialsMappingException;

/**
 * Extension of {@link org.springframework.web.servlet.ModelAndView} class
 * that provide shortcuts to get and add partial mappings that will be used
 * to render view.
 */
public class ModelAndMustacheView extends ModelAndView {

	/**
	 * Default constructor for bean-style usage: populating bean
	 * properties instead of passing in constructor arguments.
	 * @see #setView(org.springframework.web.servlet.View)
	 * @see #setViewName(String)
	 */
	public ModelAndMustacheView() {
		super();
	}

	/**
	 * Convenient constructor when there is no model data to expose.
	 * Can also be used in conjunction with {@code addObject}.
	 * @param viewName name of the View to render, to be resolved
	 * by the DispatcherServlet's ViewResolver
	 * @see #addObject
	 */
	public ModelAndMustacheView(String viewName) {
		super(viewName);
	}

	/**
	 * Convenient constructor when there is no model data to expose.
	 * Can also be used in conjunction with {@code addObject}.
	 * @param view View object to render
	 * @see #addObject
	 */
	public ModelAndMustacheView(MustacheView view) {
		super(view);
	}

	/**
	 * Creates new ModelAndView given a view name and a model.
	 * @param viewName name of the View to render, to be resolved
	 * by the DispatcherServlet's ViewResolver
	 * @param model Map of model names (Strings) to model objects
	 * (Objects). Model entries may not be {@code null}, but the
	 * model Map may be {@code null} if there is no model data.
	 */
	public ModelAndMustacheView(String viewName, Map<String, ?> model) {
		super(viewName, model);
	}

	/**
	 * Creates new ModelAndView given a View object and a model.
	 * Note: the supplied model data is copied into the internal
	 * storage of this class. You should not consider to modify the supplied
	 * Map after supplying it to this class
	 * @param view View object to render
	 * @param model Map of model names (Strings) to model objects
	 * (Objects). Model entries may not be {@code null}, but the
	 * model Map may be {@code null} if there is no model data.
	 */
	public ModelAndMustacheView(MustacheView view, Map<String, ?> model) {
		super(view, model);
	}

	/**
	 * Convenient constructor to take a single model object.
	 * @param viewName name of the View to render, to be resolved
	 * by the DispatcherServlet's ViewResolver
	 * @param modelName name of the single entry in the model
	 * @param modelObject the single model object
	 */
	public ModelAndMustacheView(String viewName, String modelName, Object modelObject) {
		super(viewName, modelName, modelObject);
	}

	/**
	 * Convenient constructor to take a single model object.
	 * @param view View object to render
	 * @param modelName name of the single entry in the model
	 * @param modelObject the single model object
	 */
	public ModelAndMustacheView(MustacheView view, String modelName, Object modelObject) {
		super(view, modelName, modelObject);
	}

	/**
	 * Get current partials stored in view.
	 *
	 * @return Current partials.
	 */
	@SuppressWarnings("unchecked")
	public Map<String, String> getPartials() {
		final View view = this.getView();
		if (view != null && view instanceof MustacheView) {
			MustacheView mustacheView = (MustacheView) view;
			return mustacheView.getAliases();
		} else {
			final Object object = getModelMap().get(MustacheSettings.PARTIALS_KEY);

			if (object != null && !(object instanceof Map)) {
				throw new MustachePartialsMappingException();
			}

			final Map<String, String> map;

			if (object == null) {
				map = new HashMap<String, String>();
			} else {
				map = (Map<String, String>) object;
			}

			return map;
		}
	}

	/**
	 * Add new partials mapping to the view.
	 *
	 * @param key Partial name.
	 * @param name Partial path.
	 */
	public void addPartial(final String key, final String name) {
		notNull(key);
		notNull(name);

		View view = this.getView();
		if (view != null && view instanceof MustacheView) {
			// First try to not pollute view model object
			final MustacheView mustacheView = (MustacheView) view;
			mustacheView.addAlias(key, name);
		} else {
			// Otherwise, add partials to model object
			final Map<String, String> currentPartials = getPartials();
			currentPartials.put(key, name);
			super.addObject(MustacheSettings.PARTIALS_KEY, currentPartials);
		}
	}

	/**
	 * Add new partials mapping to the view.
	 *
	 * @param partials Partials.
	 */
	public void addPartials(final Map<String, String> partials) {
		notNull(partials);

		View view = this.getView();
		if (view != null && view instanceof MustacheView) {
			// First try to not pollute model object
			final MustacheView mustacheView = (MustacheView) view;
			mustacheView.addAliases(partials);
		} else {
			// Otherwise, add partials to model object
			final Map<String, String> currentPartials = getPartials();
			for (Map.Entry<String, String> entry : partials.entrySet()) {
				notNull(entry.getKey());
				notNull(entry.getValue());
				currentPartials.put(entry.getKey(), entry.getValue());
			}
			super.addObject(MustacheSettings.PARTIALS_KEY, currentPartials);
		}
	}
}


/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2014 <mickael.jeanroy@gmail.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.github.mjeanroy.springmvc.view.mustache.commons;

import java.util.Collection;

/**
 * Commons static pre-conditions utilities.
 */
public final class PreConditions {

	private PreConditions() {
	}

	/**
	 * Check that a given object is not null.
	 * If object is null, a {@link java.lang.NullPointerException} is thrown
	 * with given message.
	 * If object is not null, it is automatically returns.
	 *
	 * @param object  Object to check.
	 * @param message Exception message.
	 * @param <T>     Object Generic Type.
	 * @return Non null object.
	 * @throws java.lang.NullPointerException if object is null.
	 */
	public static <T> T notNull(T object, String message) {
		if (object == null) {
			throw new NullPointerException(message);
		}
		return object;
	}

	/**
	 * Check if an array is not null and not empty.
	 * If array is null or empty, an {@link java.lang.IllegalArgumentException} is thrown.
	 * If array is not null and not empty, it will be automatically returned.
	 *
	 * @param array Array to check.
	 * @param message Exception message.
	 * @param <T> Type of array elements.
	 * @return Original array.
	 */
	public static <T> T[] notEmpty(T[] array, String message) {
		if (array == null || array.length == 0) {
			throw new IllegalArgumentException(message);
		}
		return array;
	}

	/**
	 * Check if a collection is not null and not empty.
	 * If collection is null or empty, an {@link java.lang.IllegalArgumentException} is thrown.
	 * If collection is not null and not empty, it will be automatically returned.
	 *
	 * @param array Collection to check.
	 * @param message Exception message.
	 * @param <T> Type of array elements.
	 * @return Original collection.
	 */
	public static <T> Collection<T> notEmpty(Collection<T> array, String message) {
		if (array == null || array.size() == 0) {
			throw new IllegalArgumentException(message);
		}
		return array;
	}

	/**
	 * Check that a given string value is not blank (i.e. not null,
	 * not empty and does not contain only whitespaces).
	 * If value is blank, an {@link java.lang.IllegalArgumentException} is
	 * thrown.
	 * If value is not blank, original string value is automatically returned.
	 *
	 * @param value String value.
	 * @param message Error message given to exception.
	 * @return Original string value.
	 * @throws java.lang.IllegalArgumentException if value is blank.
	 */
	public static String hasText(String value, String message) {
		if (value == null) {
			throw new IllegalArgumentException(message);
		}

		for (char c : value.toCharArray()) {
			if (!Character.isWhitespace(c)) {
				return value;
			}
		}

		// Found only whitespace characters...
		throw new IllegalArgumentException(message);
	}
}


/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2015 <mickael.jeanroy@gmail.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.github.mjeanroy.springmvc.view.mustache.commons;

import com.github.mjeanroy.springmvc.view.mustache.exceptions.NashornException;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Collection;

/**
 * Nashorn Utilities.
 */
public final class NashornUtils {

	private NashornUtils() {
	}

	/**
	 * Get new nashorn engine.
	 *
	 * @return Nashorn Engine.
	 */
	public static ScriptEngine getEngine() {
		return new ScriptEngineManager().getEngineByName("nashorn");
	}

	/**
	 * Get new nashorn engine.
	 *
	 * @param scripts List of scripts to evaluate.
	 * @return Nashorn Engine.
	 */
	public static ScriptEngine getEngine(Collection<InputStream> scripts) {
		ScriptEngine engine = getEngine();

		for (InputStream script : scripts) {
			evaluate(engine, script);
		}

		return engine;
	}

	private static void evaluate(ScriptEngine engine, InputStream script) {
		try {
			engine.eval(new InputStreamReader(script));
		}
		catch (ScriptException ex) {
			throw new NashornException(ex);
		}
	}
}


/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2015 <mickael.jeanroy@gmail.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.github.mjeanroy.springmvc.view.mustache.configuration;

import static java.lang.Boolean.parseBoolean;
import static java.lang.Integer.parseInt;
import static java.lang.String.valueOf;
import static java.util.Collections.emptyMap;
import static java.util.Collections.unmodifiableMap;

import java.util.HashMap;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;

import com.github.mjeanroy.springmvc.view.mustache.MustacheCompiler;
import com.github.mjeanroy.springmvc.view.mustache.MustacheSettings;
import com.github.mjeanroy.springmvc.view.mustache.MustacheViewResolver;

@Configuration
@PropertySource(
		value = "classpath:mustache.properties",
		ignoreResourceNotFound = true
)
public class MustacheWebConfiguration {

	private static final Logger log = LoggerFactory.getLogger(MustacheWebConfiguration.class);

	@Autowired
	private Environment environment;

	@Autowired
	private MustacheCompiler mustacheCompiler;

	/**
	 * Build mustache view resolver.
	 * This view resolver needs an instance of {@link com.github.mjeanroy.springmvc.view.mustache.MustacheCompiler}
	 * to be created.
	 *
	 * @return Mustache view resolver instance.
	 */
	@Bean
	public MustacheViewResolver mustacheViewResolver() {
		String prefix = getPrefix();
		String suffix = getSuffix();
		int order = getOrder();
		boolean cache = getCache();
		String[] viewNames = getViewNames();
		String defaultLayout = getDefaultLayout();
		String layoutKey = getLayoutKey();
		Map<String, String> mappings = getLayoutMappings();

		log.info("Create mustache view resolver");
		log.trace("  => Cache: {}", cache);
		log.trace("  => Prefix: {}", prefix);
		log.trace("  => Suffix: {}", suffix);
		log.trace("  => Order: {}", order);
		log.trace("  => View Names: {}", viewNames);
		log.trace("  => Default layout: {}", defaultLayout);
		log.trace("  => Layout key: {}", layoutKey);
		log.trace("  => Mappings: {}", mappings);

		MustacheViewResolver resolver = new MustacheViewResolver(mustacheCompiler);
		resolver.setCache(cache);
		resolver.setPrefix(prefix);
		resolver.setSuffix(suffix);
		resolver.setOrder(order);
		resolver.setViewNames(viewNames);
		resolver.setLayoutKey(layoutKey);

		if (defaultLayout != null && !defaultLayout.isEmpty()) {
			resolver.setDefaultLayout(defaultLayout);
		}

		if (!mappings.isEmpty()) {
			resolver.setLayoutMappings(mappings);
		}

		return resolver;
	}

	/**
	 * Resolve views prefix value.
	 * Default is to look for "mustache.prefix" property or use {@link com.github.mjeanroy.springmvc.view.mustache.MustacheSettings#PREFIX} if
	 * property cannot be resolved.
	 *
	 * @return Prefix value.
	 */
	public String getPrefix() {
		return environment.getProperty("mustache.prefix", MustacheSettings.PREFIX).trim();
	}

	/**
	 * Resolve views suffix value.
	 * Default is to look for "mustache.suffix" property or use {@link MustacheSettings#SUFFIX} if
	 * property cannot be resolved.
	 *
	 * @return Suffix value.
	 */
	public String getSuffix() {
		return environment.getProperty("mustache.suffix", MustacheSettings.SUFFIX).trim();
	}

	/**
	 * Resolve mustache view resolver order.
	 * Default is to look for "mustache.order" property or use {@link MustacheSettings#ORDER} if
	 * property cannot be resolved.
	 *
	 * @return Order value.
	 */
	public int getOrder() {
		return parseInt(environment.getProperty("mustache.order", valueOf(MustacheSettings.ORDER)).trim());
	}

	/**
	 * Resolve mustache view resolver cache settings.
	 * Default is to look for "mustache.cache" property or use {@link MustacheSettings#CACHE} if
	 * property cannot be resolved.
	 *
	 * @return Cache settings.
	 */
	public boolean getCache() {
		return parseBoolean(environment.getProperty("mustache.cache", valueOf(MustacheSettings.CACHE)).trim());
	}

	/**
	 * Resolve default layout to use.
	 * This layout can be used to define template to be used as main layout and render
	 * view within this layout.
	 *
	 * @return Layout name.
	 */
	public String getDefaultLayout() {
		return environment.getProperty("mustache.defaultLayout", MustacheSettings.DEFAULT_LAYOUT).trim();
	}

	/**
	 * Resolve default key to use as partials alias in default view layout.
	 *
	 * @return Partial key in layout.
	 */
	public String getLayoutKey() {
		return environment.getProperty("mustache.layoutKey", MustacheSettings.LAYOUT_KEY).trim();
	}

	/**
	 * Resolve view names matchers of mustache view resolver.
	 * Default is to look for "mustache.viewNames" property or use {@link MustacheSettings#VIEW_NAMES} if
	 * property cannot be resolved.
	 *
	 * @return View names patterns.
	 */
	public String[] getViewNames() {
		String viewNames = environment.getProperty("mustache.viewNames", valueOf(MustacheSettings.VIEW_NAMES)).trim();
		String[] names = viewNames.split(",");
		for (int i = 0; i < names.length; i++) {
			names[i] = names[i].trim();
		}
		return names;
	}

	/**
	 * Get mappings to use with resolvers.
	 *
	 * @return Layouts mappings
	 */
	public Map<String, String> getLayoutMappings() {
		String mappingsValues = environment.getProperty("mustache.layoutMappings", MustacheSettings.LAYOUT_MAPPINGS).trim();

		if (mappingsValues.isEmpty()) {
			return emptyMap();
		}

		Map<String, String> mappings = new HashMap<String, String>();
		String[] values = mappingsValues.split(";");
		if (values.length > 0) {
			for (String value : values) {
				String val = value == null ? "" : value.trim();
				if (val.isEmpty()) {
					continue;
				}

				String[] mapping = val.split(":");
				if (mapping.length != 2) {
					throw new IllegalArgumentException("Mapping must use [viewName]:[layout] format!");
				}

				mappings.put(mapping[0].trim(), mapping[1].trim());
			}
		}

		return unmodifiableMap(mappings);
	}
}


/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2014 <mickael.jeanroy@gmail.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.github.mjeanroy.springmvc.view.mustache.configuration.jmustache;

import com.github.mjeanroy.springmvc.view.mustache.MustacheCompiler;
import com.github.mjeanroy.springmvc.view.mustache.MustacheTemplateLoader;
import com.github.mjeanroy.springmvc.view.mustache.jmustache.JMustacheCompiler;
import com.samskivert.mustache.Mustache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import static com.samskivert.mustache.Mustache.Compiler;

@Configuration
public class JMustacheConfiguration {

	private static final Logger log = LoggerFactory.getLogger(JMustacheConfiguration.class);

	/**
	 * Build mustache compiler.
	 * This compiler use an instance of {@link com.samskivert.mustache.Mustache.Compiler}
	 * under the hood.
	 *
	 * @param compiler JMustache compiler.
	 * @param templateLoader Template loader.
	 * @return Mustache compiler implementation.
	 */
	@Bean
	public MustacheCompiler mustacheCompiler(Mustache.Compiler compiler, MustacheTemplateLoader templateLoader) {
		log.info("Create JMustache compiler");
		return new JMustacheCompiler(compiler, templateLoader);
	}

	/**
	 * Build original jmustache {@link Compiler} that will be used
	 * internally to compile and render templates.
	 *
	 * @return JMustache compiler.
	 */
	@Bean
	public JMustacheCompilerFactoryBean jMustacheCompiler() {
		JMustacheCompilerFactoryBean factoryBean = new JMustacheCompilerFactoryBean();
		factoryBean.setNullValue("");
		factoryBean.setDefaultValue("");
		factoryBean.setEmptyStringIsFalse(true);
		factoryBean.setZeroIsFalse(true);
		factoryBean.setEscapeHTML(true);
		return factoryBean;
	}
}


/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2014 <mickael.jeanroy@gmail.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.github.mjeanroy.springmvc.view.mustache.commons;

import org.springframework.core.type.AnnotationMetadata;

import java.util.Map;

import static com.github.mjeanroy.springmvc.view.mustache.commons.PreConditions.hasText;

/**
 * Commons static class utilities.
 */
public final class ClassUtils {

	private ClassUtils() {
	}

	/**
	 * Check that a given class is available on
	 * classpath.
	 *
	 * @param klass Class name.
	 * @return True if class is available, false otherwise.
	 */
	public static boolean isPresent(String klass) {
		hasText(klass, "Class name must not be empty");
		try {
			Class.forName(klass);
			return true;
		}
		catch (Exception ex) {
			return false;
		}
	}

	/**
	 * Get annotation method value.
	 *
	 * @param importingClassMetadata Metadata.
	 * @param annotationClass        Annotation class to look for.
	 * @param name                   Name of method.
	 * @param defaultValue           Default value if original value is null.
	 * @param <T>                    Type of returned value.
	 * @return Annotation value, or default value if original value is null.
	 */
	@SuppressWarnings("unchecked")
	public static <T> T getAnnotationValue(AnnotationMetadata importingClassMetadata, Class annotationClass, String name, T defaultValue) {
		Map<String, Object> attributes = importingClassMetadata.getAnnotationAttributes(annotationClass.getName());
		return attributes != null && attributes.containsKey(name) ? (T) attributes.get(name) : defaultValue;
	}
}


/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2014 <mickael.jeanroy@gmail.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.github.mjeanroy.springmvc.view.mustache.commons;

import com.github.mjeanroy.springmvc.view.mustache.exceptions.MustacheIOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;

import java.io.BufferedReader;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.util.Collection;

/**
 * Common static IO Utilities.
 */
public final class IOUtils {

	/**
	 * Class logger.
	 */
	private static final Logger log = LoggerFactory.getLogger(IOUtils.class);

	/**
	 * Buffer size to read files.
	 */
	private static final int BUFFER_SIZE = 1024;

	/**
	 * Line separator, system dependent.
	 */
	private static final String LINE_SEPARATOR = System.getProperty("line.separator");

	private IOUtils() {
	}

	/**
	 * Read input reader: extract content and return it as
	 * a String.
	 *
	 * @param reader Reader input.
	 * @return String content.
	 * @throws MustacheIOException If an IO exception occurs during reading operation.
	 */
	public static String read(final Reader reader) {
		final BufferedReader buffer = new BufferedReader(reader, BUFFER_SIZE);

		try {
			StringBuilder sb = new StringBuilder();
			String line;
			boolean started = false;

			// Read line by line
			while ((line = buffer.readLine()) != null) {
				if (started) {
					sb.append(LINE_SEPARATOR);
				}

				sb.append(line);
				started = true;
			}

			return sb.toString();
		}
		catch (IOException ex) {
			throw new MustacheIOException(ex);
		}
		finally {
			closeQuietly(buffer);
		}
	}

	/**
	 * Return an input stream from given name.
	 * Name is searched into the classpath.
	 * Name can contains ant style pattern.
	 *
	 * @param name Name.
	 * @return Input Stream.
	 */
	public static InputStream getStream(String name) {
		ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(IOUtils.class.getClassLoader());
		InputStream stream = getInputStreamWithResolver(resolver, name);

		// Not Found
		if (stream == null) {
			throw new MustacheIOException("Unable to locate {}" + name);
		}

		return stream;
	}

	/**
	 * Return an input stream from given names.
	 * First result returning a valid stream is returned.
	 * Names are searched into the classpath.
	 * Names can contains ant style pattern.
	 *
	 * @param names Names collection.
	 * @return Input Stream.
	 * @throws MustacheIOException If everything fail.
	 */
	public static InputStream getFirstAvailableStream(Collection<String> names) {
		ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(IOUtils.class.getClassLoader());
		for (String name : names) {
			try {
				InputStream stream = getInputStreamWithResolver(resolver, name);
				if (stream != null) {
					return stream;
				}
			}
			catch (MustacheIOException ex) {
				// Go to the next test
			}
		}

		throw new MustacheIOException("Unable to locate one of: " + names);
	}

	private static InputStream getInputStreamWithResolver(ResourcePatternResolver resolver, String name) {
		try {
			Resource[] resources = resolver.getResources(name);

			// Not Found
			if (resources.length == 0) {
				log.debug("Found zero results with pattern: {}", name);
				return null;
			}

			log.debug("Found {} results with pattern: {}", resources.length, name);

			if (log.isTraceEnabled()) {
				for (Resource resource : resources) {
					log.trace("  -> Found: {}", resource.getFilename());
				}
			}

			return resources[0].getInputStream();
		}
		catch (IOException ex) {
			throw new MustacheIOException("I/O Error with " + name, ex);
		}
	}

	/**
	 * Close a {@link java.io.Closeable} object and do not throws a Checked Exception
	 * if an exception occurs.
	 *
	 * @param stream Closeable stream.
	 */
	private static void closeQuietly(Closeable stream) {
		try {
			stream.close();
		}
		catch (IOException ex) {
			// Just log.
			log.debug(ex.getMessage(), ex);
		}
	}
}


/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2014 <mickael.jeanroy@gmail.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.github.mjeanroy.springmvc.view.mustache.configuration.handlebars;

import com.github.jknack.handlebars.Handlebars;
import com.github.mjeanroy.springmvc.view.mustache.MustacheCompiler;
import com.github.mjeanroy.springmvc.view.mustache.MustacheTemplateLoader;
import com.github.mjeanroy.springmvc.view.mustache.handlebars.HandlebarsCompiler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Configure handlebar template engine.
 */
@Configuration
public class HandlebarsConfiguration {

	private static final Logger log = LoggerFactory.getLogger(HandlebarsConfiguration.class);

	/**
	 * Build mustache compiler.
	 * This compiler use an instance of {@link com.github.jknack.handlebars.Handlebars}
	 * under the hood.
	 *
	 * @param handlebars Handlebars instance.
	 * @param templateLoader Template loader.
	 * @return Mustache compiler implementation.
	 */
	@Bean
	public MustacheCompiler mustacheCompiler(Handlebars handlebars, MustacheTemplateLoader templateLoader) {
		log.info("Create handlebar compiler");
		return new HandlebarsCompiler(handlebars, templateLoader);
	}

	/**
	 * Build original {@link com.github.jknack.handlebars.Handlebars} compiler
	 * that will be used to compile and render templates.
	 *
	 * @return Handlebars compiler.
	 */
	@Bean
	public HandlebarsFactoryBean handlebarsCompiler() {
		return new HandlebarsFactoryBean();
	}
}


/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2014 <mickael.jeanroy@gmail.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.github.mjeanroy.springmvc.view.mustache;

import static com.github.mjeanroy.springmvc.view.mustache.commons.PreConditions.hasText;
import static com.github.mjeanroy.springmvc.view.mustache.commons.PreConditions.notNull;

import java.util.HashMap;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.view.AbstractTemplateViewResolver;

/**
 * Mustache View Resolver.
 */
public class MustacheViewResolver extends AbstractTemplateViewResolver {

	private static final Logger log = LoggerFactory.getLogger(MustacheViewResolver.class);

	/**
	 * Mustache compiler.
	 */
	private final MustacheCompiler compiler;

	/**
	 * Main layout that can be used to define view layouts.
	 * This layout will be used by default for each views if it has been set.
	 */
	// Volatile because it can be accessed from more than one thread
	private volatile String defaultLayout;

	/**
	 * Key to use to define layout content.
	 * For example, with a layout template defined as (using a key equal to 'content'):
	 *
	 * <div>
	 *   Header
	 *   {{> content}}
	 *   Footer
	 * </div>
	 *
	 * Each view will replace 'content' partials.
	 */
	// Volatile because it can be accessed from more than one thread
	private volatile String layoutKey;

	/**
	 * Layout mappings that can be used to map different layout for different views.
	 * If mapping is not found and a default layout is defined, default layout will be used.
	 */
	private final Map<String, String> layoutMappings;

	/**
	 * Build new mustache resolver using compiler
	 *
	 * @param compiler Mustache compiler.
	 */
	public MustacheViewResolver(MustacheCompiler compiler) {
		setViewClass(requiredViewClass());

		this.compiler = notNull(compiler, "Compiler must not be null");
		this.layoutKey = MustacheSettings.DEFAULT_LAYOUT_KEY;
		this.layoutMappings = new HashMap<String, String>();
	}

	@Override
	protected Class<?> requiredViewClass() {
		return MustacheView.class;
	}

	@Override
	public void setPrefix(String prefix) {
		log.trace("Set view resolver prefix: '{}'", prefix);

		super.setPrefix(prefix);
		compiler.setPrefix(prefix);
	}

	@Override
	public void setSuffix(String suffix) {
		log.trace("Set view resolver suffix: '{}'", suffix);

		super.setSuffix(suffix);
		compiler.setSuffix(suffix);
	}

	/**
	 * Update default layout view.
	 *
	 * @param defaultLayout Default layout.
	 */
	public void setDefaultLayout(String defaultLayout) {
		log.trace("Set view resolver default layout: '{}'", defaultLayout);
		this.defaultLayout = hasText(defaultLayout, "Default layout must not be empty");
	}

	/**
	 * Update view layout key.
	 *
	 * @param layoutKey New view layout key.
	 */
	public void setLayoutKey(String layoutKey) {
		log.trace("Set view resolver layout key: '{}'", layoutKey);
		this.layoutKey = hasText(layoutKey, "Layout key must not be empty");
	}

	/**
	 * Replace current layout mappings by new mappings.
	 *
	 * @param layoutMappings New mappings.
	 */
	public void setLayoutMappings(Map<String, String> layoutMappings) {
		notNull(layoutMappings, "Layout mappings must not be null");

		log.debug("Set view resolver layout mappings");

		this.layoutMappings.clear();
		for (Map.Entry<String, String> entry : layoutMappings.entrySet()) {
			addLayoutMapping(entry.getKey(), entry.getValue());
		}
	}

	/**
	 * Replace current layout mappings by new mappings.
	 *
	 * @param viewName View name to map.
	 * @param layoutName Layout name to use for given view.
	 */
	public void addLayoutMapping(String viewName, String layoutName) {
		log.debug("Add new layout mapping");
		log.trace("  => {} -> {}", viewName, layoutName);

		notNull(viewName, "View name must not be null");
		notNull(layoutName, "Layout name must not be null");
		this.layoutMappings.put(viewName, layoutName);
	}

	@Override
	protected MustacheView buildView(String viewName) throws Exception {
		final String mapping = layoutMappings.get(viewName);
		final String layout = mapping != null ? mapping : defaultLayout;
		final boolean useLayout = layout != null && layoutKey != null;
		final String name = useLayout ? layout : viewName;

		log.info("Build view '{}'", viewName);
		log.trace("  => Use layout: {}", useLayout);
		log.trace("  => Layout: {}", layout);
		log.trace("  => Name: {}", name);

		final MustacheView view = (MustacheView) super.buildView(name);
		view.setCompiler(compiler);

		if (useLayout) {
			// Add alias to map main content to real view
			view.addAlias(layoutKey, viewName);
		}

		return view;
	}
}


/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2015 <mickael.jeanroy@gmail.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.github.mjeanroy.springmvc.view.mustache.nashorn;

import com.github.mjeanroy.springmvc.view.mustache.MustacheTemplateLoader;
import com.github.mjeanroy.springmvc.view.mustache.exceptions.NashornException;

import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
import java.io.InputStream;
import java.util.Map;

import static com.github.mjeanroy.springmvc.view.mustache.commons.IOUtils.getFirstAvailableStream;
import static com.github.mjeanroy.springmvc.view.mustache.commons.IOUtils.getStream;
import static com.github.mjeanroy.springmvc.view.mustache.commons.NashornUtils.getEngine;
import static com.github.mjeanroy.springmvc.view.mustache.commons.PreConditions.notNull;
import static java.util.Arrays.asList;

/**
 * Nashorn engine that can be used to render Mustache rendering.
 */
public class MustacheEngine {

	/**
	 * Get default mustache implementation.
	 *
	 * @return Stream associated to default mustache implementation.
	 */
	private static InputStream getDefault() {
		return getFirstAvailableStream(asList(
				// First, try with webjars
				"/META-INF/resources/webjars/mustache/**/mustache.js",
				"/META-INF/resources/npm/mustache/**/mustache.js",

				// Then, try inside a bower_components directory
				"/bower_components/mustache/**/mustache.js",

				// Then, try "generic" directory
				"/vendors/mustache/**/mustache.js",
				"/js/mustache/**/mustache.js"
		));
	}

	/**
	 * Internal Nashorn Engine.
	 */
	private final ScriptEngine engine;

	/**
	 * Partials object.
	 */
	private final NashornPartialsObject partials;

	/**
	 * Build nashorn engine with default mustache js implementation.
	 *
	 * Mustache file will be searched into classpath:
	 * - First, try to locate webjars.
	 * - Then, try to locate into bower_components directory.
	 * - Finally, try to locate into "generic" directory ("vendors" or "js" directories).
	 * If everything fail, an exception will be thrown.
	 *
	 * @param templateLoader Template loader, used to resolve partials.
	 * @throws com.github.mjeanroy.springmvc.view.mustache.exceptions.MustacheIOException If everything fail.
	 */
	public MustacheEngine(MustacheTemplateLoader templateLoader) {
		this(templateLoader, getDefault());
	}

	/**
	 * Build nashorn engine with default mustache js implementation.
	 * Mustache file is given as a second parameter and is resolved against classpath.
	 *
	 * @param templateLoader Template loader, used to resolve partials.
	 * @param mustacheJs Mustache JavaScript file (as {@link InputStream} instance).
	 * @throws com.github.mjeanroy.springmvc.view.mustache.exceptions.MustacheIOException If script does not exist.
	 */
	public MustacheEngine(MustacheTemplateLoader templateLoader, String mustacheJs) {
		this(templateLoader, getStream(mustacheJs));
	}

	/**
	 * Build nashorn engine with default mustache js implementation.
	 * Mustache file is given as a second parameter and is resolved against classpath.
	 *
	 * @param templateLoader Template loader, used to resolve partials.
	 * @param mustacheJs Mustache JavaScript file (as {@link InputStream} instance).
	 * @throws com.github.mjeanroy.springmvc.view.mustache.exceptions.MustacheIOException If script does not exist.
	 */
	public MustacheEngine(MustacheTemplateLoader templateLoader, InputStream mustacheJs) {
		notNull(mustacheJs, "Mustache JS must not be null");
		notNull(templateLoader, "Template Loader must not be null");

		this.engine = getEngine(asList(mustacheJs, getStream("/mustache/nashorn-bindings.js")));
		this.partials = new NashornPartialsObject(templateLoader);
	}

	/**
	 * Render template with given model object.
	 *
	 * @param template Template.
	 * @param model Model object.
	 * @return Rendered template.
	 */
	public String render(String template, Map<String, Object> model) {
		try {
			Invocable invocable = (Invocable) engine;
			return (String) invocable.invokeFunction("render", template, model, partials);
		}
		catch (ScriptException ex) {
			throw new NashornException(ex);
		}
		catch (NoSuchMethodException ex) {
			throw new NashornException(ex);
		}
	}
}


/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2014 <mickael.jeanroy@gmail.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.github.mjeanroy.springmvc.view.mustache;

import static com.github.mjeanroy.springmvc.view.mustache.commons.PreConditions.notNull;
import static java.util.Collections.unmodifiableMap;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.Writer;
import java.util.HashMap;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.view.AbstractTemplateView;

import com.github.mjeanroy.springmvc.view.mustache.exceptions.MustachePartialsMappingException;

/**
 * Implementation of mustache view.
 * Use jmustache internally as template compiler.
 */
public class MustacheView extends AbstractTemplateView {

	private static final Logger log = LoggerFactory.getLogger(MustacheView.class);

	/**
	 * Mustache compiler.
	 */
	private MustacheCompiler compiler;

	/** List of aliases that map alias name to partial path. */
	private final Map<String, String> aliases;

	/**
	 * Build new view.
	 */
	public MustacheView() {
		setContentType("text/html; charset=utf-8");
		this.aliases = new HashMap<String, String>();
	}

	/**
	 * Set new mustache compiler that can be used to compile view.
	 *
	 * @param compiler Mustache compiler.
	 */
	public void setCompiler(MustacheCompiler compiler) {
		notNull(compiler, "Compiler must not be null");
		this.compiler = compiler;
	}

	/**
	 * Get compiler that will be used to compile view.
	 *
	 * @return Mustache compiler.
	 */
	public MustacheCompiler getCompiler() {
		return compiler;
	}

	/**
	 * Add partials mapping.
	 *
	 * @param aliases New aliases.
	 */
	public void addAliases(Map<String, String> aliases) {
		notNull(aliases, "Aliases must not be null");
		for (Map.Entry<String, String> entry : aliases.entrySet()) {
			addAlias(entry.getKey(), entry.getValue());
		}
	}

	/**
	 * Add alias mapping.
	 *
	 * @param key Partial key.
	 * @param value Partial name.
	 */
	public void addAlias(String key, String value) {
		this.aliases.put(
				notNull(key, "Key must not be null"),
				notNull(value, "Value must not be null")
		);
	}

	/**
	 * Get list of aliases that map alias name to partial path.
	 *
	 * @return Aliases.
	 */
	public Map<String, String> getAliases() {
		return unmodifiableMap(aliases);
	}

	@Override
	protected void renderMergedTemplateModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
		notNull(compiler, "Compiler must not be null and must have been set");
		notNull(model, "Model must not be null");

		if (log.isDebugEnabled()) {
			log.debug("Render template {}", viewLayoutName());

			if (log.isTraceEnabled()) {
				log.trace("Using model: ");
				for (Map.Entry<String, Object> entry : model.entrySet()) {
					log.trace("  => {} = {}", entry.getKey(), entry.getValue());
				}
			}
		}

		response.setContentType(getContentType());
		renderTemplate(model, response.getWriter());
	}

	@SuppressWarnings("unchecked")
	private void renderTemplate(Map<String, Object> model, Writer writer) {
		final Map<String, String> viewPartials = new HashMap<String, String>(aliases);
		final Object object = model.get(MustacheSettings.PARTIALS_KEY);

		if (object != null) {
			if (!(object instanceof Map)) {
				throw new MustachePartialsMappingException();
			}

			viewPartials.putAll((Map<String, String>) object);
		}

		final boolean hasAliases = !viewPartials.isEmpty();
		if (hasAliases) {
			compiler.addTemporaryPartialAliases(viewPartials);
		}

		try {
			final MustacheTemplate template = compiler.compile(viewLayoutName());
			template.execute(model, writer);
		}
		finally {
			if (hasAliases) {
				compiler.removeTemporaryPartialAliases();
			}
		}
	}

	private String viewLayoutName() {
		return getUrl();
	}
}


package com.redmadrobot.chronos;

import org.jetbrains.annotations.Contract;

import android.support.annotation.NonNull;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * A controller which handles saving\restoration processes of GUI objects, and help them to connect
 * to Chronos.
 *
 * @author maximefimov
 */
final class ChronosListenerManager {

    private final static ChronosListenerManager INSTANCE = new ChronosListenerManager();

    private final AtomicInteger mNextConnectorId = new AtomicInteger(0);

    private final Map<Integer, ChronosListener> mListeners;

    private ChronosListenerManager() {
        mListeners = new HashMap<>();
    }

    @NonNull
    @Contract(pure = true)
    public static ChronosListenerManager getInstance() {
        return INSTANCE;
    }

    /**
     * Creates a new ServiceConnector instance. Do not call this method twice for a same object,
     * instead use {@link #getListener(int)} method.
     *
     * @return a created ServiceConnector
     */
    @NonNull
    public final synchronized ChronosListener createListener() {
        final int connectorId = mNextConnectorId.getAndIncrement();
        final ChronosListener result = new ChronosListener(connectorId);

        mListeners.put(connectorId, result);

        return result;
    }

    /**
     * Gets previously created ServiceConnector by its id.
     *
     * @param id an id of the saved ServiceConnector
     * @return a restored ServiceConnector, or a new one, if there is no saved instance with a given
     * id
     * @see {@link ChronosListener#getId()}
     * @see #createListener()
     */
    @NonNull
    public final synchronized ChronosListener getListener(final int id) {
        ChronosListener chronosListener = mListeners.get(id);
        if (chronosListener == null) {
            chronosListener = new ChronosListener(id);
            mListeners.put(id, chronosListener);
        }
        return chronosListener;
    }
}


package com.redmadrobot.chronos;

import android.support.annotation.NonNull;

/**
 * Class, which provides a static access to the Chronos library.
 *
 * @author maximefimov
 */
public final class Chronos {

    @SuppressWarnings("WeakerAccess")
    @NonNull
    public final static String OWN_CALLBACK_METHOD_NAME = "onOperationFinished";

    @SuppressWarnings("WeakerAccess")
    @NonNull
    public final static String BROADCAST_CALLBACK_METHOD_NAME = "onBroadcastOperationFinished";

    private Chronos() {
    }

    /**
     * Cancels all running operations for all objects.
     *
     * @param mayInterrupt {@code true} if threads executing operations task should be interrupted;
     *                     otherwise, in-progress tasks are allowed to complete
     */
    public static void cancelAll(final boolean mayInterrupt) {
        RunningOperationStorage.getInstance().cancelAll(mayInterrupt);
    }

    /**
     * Runs operation synchronously.
     *
     * @param operation Operation to be executed.
     * @param <Output>  class of the result, returned by the Operations' {@code run} method
     * @return OperationResult which contains the result of the Operation, or the error, occurred
     * during the execution
     */
    @NonNull
    public static <Output> ChronosOperationResult<Output> run(
            @NonNull final ChronosOperation<Output> operation) {
        return ChronosService.getInstance().runSync(operation, false);
    }

    /**
     * Runs operation synchronously. The result will be broadcasted to all objects, connected to
     * Chronos via {@link ChronosListener}.
     *
     * @param operation Operation to be executed.
     * @param <Output>  class of the result, returned by the Operations' {@code run} method
     * @return OperationResult which contains the result of the Operation, or the error, occurred
     * during the execution
     */
    @NonNull
    public static <Output> ChronosOperationResult<Output> runBroadcast(
            @NonNull final ChronosOperation<Output> operation) {
        return ChronosService.getInstance().runSync(operation, true);
    }
}


package com.redmadrobot.chronos;

import org.jetbrains.annotations.Contract;

import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;

import java.lang.ref.SoftReference;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import de.greenrobot.event.EventBus;

/**
 * The interaction module which passes data and control between GUI elements and Chronos services.
 *
 * @author maximefimov
 */
final class ChronosListener {

    private final static boolean LOG_ENABLED = false;

    @NonNull
    private final static String LOG_TAG = ChronosListener.class.getSimpleName();

    private final int mId;

    @NonNull
    private final Map<String, Integer> mTaggedRequests = new HashMap<>();

    @NonNull
    private final List<Integer> mUntaggedRequests = new LinkedList<>();

    @NonNull
    private final List<SoftReference<OperationDelivery<?>>> mStoredResults = new LinkedList<>();

    private State mState = State.PAUSED;

    private Object mServiceListener;

    private enum State {
        PAUSED,
        RESUMED
    }

    private enum DeliveryMode {
        NORMAL,
        BROADCAST
    }

    /**
     * @param id a unique id that is bound to the instance being created
     * @see ChronosListenerManager#createListener()
     * @see ChronosListenerManager#getListener(int)
     */
    ChronosListener(final int id) {
        mId = id;
        EventBus.getDefault().register(this);
        logd("ServiceConnector with id=" + id + " was created");
    }

    /**
     * Checks if a method can be used as a callback to handle operation result.
     *
     * @param method      a method to be checked
     * @param resultClass an operation result class
     * @param methodName  a required method name
     * @return {@code true} if method can be used as a callback; {@code false} otherwise
     */
    @Contract(pure = true)
    private static boolean isCallback(@NonNull final Method method,
            @NonNull final Class<?> resultClass,
            @NonNull final String methodName) {
        if (method.getName().equals(methodName)) {
            if (method.getReturnType() == Void.TYPE) {
                final Class<?>[] parameters = method.getParameterTypes();
                if (parameters.length == 1 && parameters[0].isAssignableFrom(resultClass)) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * @return the id by which the instance can be restored via {@link ChronosListenerManager#getListener(int)}
     */
    @Contract(pure = true)
    public final int getId() {
        return mId;
    }

    /**
     * This method must be called by a bound GUI element when it passes its own onResume state.
     *
     * @param serviceListener a GUI element that wants to connect to Chronos via the
     *                        ServiceConnector. In fact, it could be any object, no matter it is an
     *                        Activity, Fragment, or something else, but Chronos is designed to work
     *                        with GUI elements.
     * @see #onPause()
     */
    public final void onResume(@NonNull final Object serviceListener) {
        logd("onResume");
        mServiceListener = serviceListener;
        mState = State.RESUMED;

        if (!mStoredResults.isEmpty()) {
            logd("has undelivered results");
            final List<SoftReference<OperationDelivery<?>>> oldResults = new ArrayList<>(
                    mStoredResults);
            mStoredResults.clear();
            for (SoftReference<OperationDelivery<?>> result : oldResults) {
                final OperationDelivery<?> delivery = result.get();
                if (delivery != null) {
                    deliverResult(delivery);
                }
            }
            logd("no more undelivered results");
        } else {
            logd("has no undelivered results");
        }
    }

    /**
     * This method must be called by a bound GUI element when it passes its own onPause state.
     *
     * @see #onResume(Object)
     */
    public final void onPause() {
        logd("onPause");
        mState = State.PAUSED;
        mServiceListener = null;
    }

    /**
     * This method is used to listening to results stream and operate with them. User should never
     * call this method manually.
     *
     * @param operationResult the result to process
     */
    @SuppressWarnings("unused")
    public final void onEventMainThread(@Nullable final ChronosOperationResult<?> operationResult) {
        if (operationResult == null) {
            //somehow the bus delivered us a null object, it should be ignored
            return;
        }
        final int operationId = operationResult.getId();

        DeliveryMode deliveryMode = null;

        if (!RunningOperationStorage.getInstance().isOperationCancelled(operationId)) {
            if (mTaggedRequests.containsValue(operationId)) {
                deliveryMode = DeliveryMode.NORMAL;
            } else if (mUntaggedRequests.contains(operationResult.getId())) {
                mUntaggedRequests.remove(Integer.valueOf(operationId));
                deliveryMode = DeliveryMode.NORMAL;
            } else if (operationResult.isBroadcast()) {
                deliveryMode = DeliveryMode.BROADCAST;
            }
        }

        if (deliveryMode != null) {
            final OperationDelivery<?> operationDelivery = new OperationDelivery<>(operationResult,
                    deliveryMode);
            logd("operation delivery: " + operationDelivery);
            onOperationFinished(operationDelivery);
        }
    }

    /**
     * Launches an operation in background thread.
     *
     * @param operation       an operation to be launched
     * @param broadcastResult {@code true} if the result should be broadcasted, {@code false}
     *                        otherwise
     * @return a unique launch id
     * @see #invoke(ChronosOperation, String, boolean)
     * @see #cancel(int, boolean)
     */
    public final int invoke(@NonNull final ChronosOperation<?> operation,
            final boolean broadcastResult) {
        logd("invoking untagged operation");
        final int id = ChronosService.getInstance().runAsync(operation, broadcastResult);
        mUntaggedRequests.add(id);
        return id;
    }

    /**
     * Launches an operation in background thread. If operation, launched with the same tag from the
     * same ServiceConnector is running, new operation launch will not be triggered.
     *
     * @param operation       an operation to be launched
     * @param tag             a pre-cache key of the launch
     * @param broadcastResult {@code true} if the result should be broadcasted, {@code false}
     *                        otherwise
     * @return a launch id, may be the same with the previous call of the method, if the operation
     * with the same tag is still running
     * @see #invoke(ChronosOperation, boolean)
     * @see #cancel(int, boolean)
     * @see #cancel(String, boolean)
     */
    public final int invoke(@NonNull final ChronosOperation<?> operation, @NonNull final String tag,
            final boolean broadcastResult) {
        logd("invoking tagged operation, tag=" + tag);
        final Integer savedId = mTaggedRequests.get(tag);
        if (savedId != null && isRunning(savedId)) {
            logd("operation with tag=" + tag + " is running, do nothing");
            return savedId;
        }

        logd("operation with tag=" + tag + " is not running, start it");
        final int id = ChronosService.getInstance().runAsync(operation, broadcastResult);
        mTaggedRequests.put(tag, id);
        return id;
    }

    /**
     * Cancels operation launch by its id. May not physically kill the background thread, but it is
     * guaranteed that the result of the operation will not be delivered to any ServiceConnector.
     *
     * @param id           an id of the operation launch that should be cancelled
     * @param mayInterrupt {@code true} if threads executing operations task should be interrupted;
     *                     otherwise, in-progress tasks are allowed to complete
     * @return {@code false} if the task could not be cancelled, typically because it has already
     * completed normally; {@code true} otherwise
     * @see #invoke(ChronosOperation, boolean)
     * @see #invoke(ChronosOperation, String, boolean)
     * @see #cancel(String, boolean)
     * @see Chronos#cancelAll(boolean)
     */
    public final boolean cancel(final int id, final boolean mayInterrupt) {
        //noinspection SimplifiableIfStatement
        if (mUntaggedRequests.contains(id) || mTaggedRequests.containsValue(id)) {
            return RunningOperationStorage.getInstance().cancel(id, mayInterrupt);
        } else {
            return false;
        }
    }

    /**
     * Cancels operation launch by its tag. May not physically kill the background thread, but it is
     * guaranteed that the result of the operation will not be delivered to any ServiceConnector.
     *
     * @param tag          a pre-cache key of the operation launch that should be cancelled
     * @param mayInterrupt {@code true} if threads executing operations task should be interrupted;
     *                     otherwise, in-progress tasks are allowed to complete
     * @return {@code false} if the task could not be cancelled, typically because it has already
     * completed normally or there is no running operation with a given tag; {@code true} otherwise
     * @see #invoke(ChronosOperation, boolean)
     * @see #invoke(ChronosOperation, String, boolean)
     * @see #cancel(int, boolean)
     * @see Chronos#cancelAll(boolean)
     */
    public final boolean cancel(@NonNull final String tag, final boolean mayInterrupt) {
        final Integer id = mTaggedRequests.get(tag);
        //noinspection SimplifiableIfStatement
        if (id != null) {
            return cancel(id, mayInterrupt);
        } else {
            return false;
        }
    }

    /**
     * Checks if an operation with given launch id is running.
     *
     * @param id an id of the operation launch
     * @return {@code true} if the operation is running, {@code false} otherwise
     */
    @Contract(pure = true)
    public final boolean isRunning(final int id) {
        return RunningOperationStorage.getInstance().isOperationRunning(id);
    }

    /**
     * Checks if an operation with given launch tag is running.
     *
     * @param tag a pre-cache key of the operation launch
     * @return {@code true} if the operation is running, {@code false} if it is not running, or
     * there was no operation launch with the tag at all
     */
    @Contract(pure = true)
    public final boolean isRunning(@NonNull final String tag) {
        final Integer id = mTaggedRequests.get(tag);
        //noinspection SimplifiableIfStatement
        if (id != null) {
            return isRunning(id);
        } else {
            return false;
        }
    }

    @Override
    @Contract(pure = true)
    public String toString() {
        return "ServiceConnector[id=" + getId() + "]";
    }

    /**
     * A dispatcher method which decides what to do with an operation result.
     *
     * @param operationResult an operation result which needs to be dispatched
     */
    private void onOperationFinished(@NonNull final OperationDelivery<?> operationResult) {
        logd("onOperationFinished " + operationResult);
        switch (mState) {
            case PAUSED:
                storeResult(operationResult);
                break;
            case RESUMED:
                deliverResult(operationResult);
                break;
            default:
                throw new IllegalStateException("Unknown state: " + mState);
        }
    }

    /**
     * Storing an operation result to use it later.
     *
     * @param operationDelivery an operation result to be stored
     * @see #deliverResult(OperationDelivery)
     */
    private void storeResult(@NonNull final OperationDelivery<?> operationDelivery) {
        logd("store delivery " + operationDelivery);
        mStoredResults.add(new SoftReference<OperationDelivery<?>>(operationDelivery));
    }

    /**
     * Delivers an operation result to the bound client.
     *
     * @param operationDelivery an operation result to be delivered
     * @see #onResume(Object)
     * @see #storeResult(OperationDelivery)
     */
    private void deliverResult(@NonNull final OperationDelivery<?> operationDelivery) {
        logd("deliver delivery " + operationDelivery);
        switch (operationDelivery.getDeliveryMode()) {
            case NORMAL:
                deliverResult(operationDelivery.getResult(), Chronos.OWN_CALLBACK_METHOD_NAME,
                        true);
                break;
            case BROADCAST:
                deliverResult(operationDelivery.getResult(), Chronos.BROADCAST_CALLBACK_METHOD_NAME,
                        false);
                break;
            default:
                break;
        }
    }

    /**
     * Call bound client methods to pass an operation result.
     *
     * @param operationResult  an operation result to be delivered
     * @param methodName       a name of a method that will be called
     * @param warnIfNoCallback {@code true} if a warning message should be posted to LogCat if there
     *                         is no suitable method in the bound client; {@code false} otherwise
     */
    @SuppressWarnings("TryWithIdenticalCatches")
    private void deliverResult(@NonNull final ChronosOperationResult<?> operationResult,
            @NonNull final String methodName, final boolean warnIfNoCallback) {
        final Class listenerClass = mServiceListener.getClass();
        final Method[] listenerMethods = listenerClass.getMethods();

        Method callbackMethod = null;

        final Class resultClass = operationResult.getClass();
        for (Method method : listenerMethods) {
            if (isCallback(method, resultClass, methodName)) {
                callbackMethod = method;
                try {
                    callbackMethod.invoke(mServiceListener, operationResult);
                } catch (IllegalAccessException e) {
                    Log.w(LOG_TAG, Log.getStackTraceString(e));
                } catch (InvocationTargetException e) {
                    Log.w(LOG_TAG, Log.getStackTraceString(e));
                }
            }
        }

        if (warnIfNoCallback && callbackMethod == null) {
            Log.w(LOG_TAG,
                    "Operation result (id=" + operationResult.getId() + "; class=" + operationResult
                            .getClass().getName() + ") was obtained, but there is no method in "
                            + mServiceListener + " to get it"
            );
            Log.w(LOG_TAG, "Method should look like");
            Log.w(LOG_TAG,
                    "public void " + methodName + "(" + resultClass.getName()
                            + " result) {}"
            );
        }
    }

    /**
     * Logs debug message.
     *
     * @param message сообщение для вывода в лог.
     * @see #LOG_ENABLED
     */
    private void logd(@NonNull final String message) {
        if (LOG_ENABLED) {
            Log.d(LOG_TAG, this.toString() + " " + message);
        }
    }

    private final static class OperationDelivery<T> {

        @NonNull
        private final ChronosOperationResult<T> mResult;

        @NonNull
        private final DeliveryMode mDeliveryMode;

        private OperationDelivery(@NonNull final ChronosOperationResult<T> result,
                @NonNull final DeliveryMode deliveryMode) {
            mResult = result;
            mDeliveryMode = deliveryMode;
        }

        @NonNull
        @Contract(pure = true)
        public final ChronosOperationResult<T> getResult() {
            return mResult;
        }

        @NonNull
        @Contract(pure = true)
        public final DeliveryMode getDeliveryMode() {
            return mDeliveryMode;
        }
    }
}


package com.redmadrobot.chronos;

import org.jetbrains.annotations.Contract;

import android.support.annotation.NonNull;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Future;

/**
 * A storage for launched operation.
 *
 * @author maximefimov
 */
final class RunningOperationStorage {

    @NonNull
    private final static RunningOperationStorage INSTANCE = new RunningOperationStorage();

    @NonNull
    private final Map<Integer, RunningOperation> mRunningOperations = new HashMap<>();

    @NonNull
    private final List<Integer> mCancelledOperations = new LinkedList<>();

    private RunningOperationStorage() {
    }

    @NonNull
    @Contract(pure = true)
    static RunningOperationStorage getInstance() {
        return INSTANCE;
    }

    /**
     * Cancels running operation.
     *
     * @param id              the unique id of operations' launch
     * @param mayInterrupt    {@code true} if thread executing operation task should be interrupted;
     *                        otherwise, in-progress tasks are allowed to complete
     * @param removeOperation {@code true} if the operation should be marked as removed
     * @return {@code false} if the task could not be cancelled, typically because it has already
     * completed normally; {@code true} otherwise
     */
    private synchronized boolean cancel(final int id, final boolean mayInterrupt,
            final boolean removeOperation) {
        final RunningOperation runningOperation = mRunningOperations.get(id);
        if (runningOperation != null) {
            if (removeOperation) {
                mRunningOperations.remove(id);
            }
            mCancelledOperations.add(id);
            return runningOperation.cancel(mayInterrupt);
        } else {
            return false;
        }
    }

    /**
     * Stores the future as a running operation with a given runs' id.
     *
     * @param id     the unique id of an operation launch
     * @param future the object that represents a running operation
     */
    synchronized final void operationStarted(final int id, @NonNull final ChronosOperation<?> operation,
            @NonNull final Future<?> future
    ) {
        mRunningOperations.put(id, new RunningOperation(operation, future));
    }

    /**
     * Marks an operation run with id as finished.
     *
     * @param id the unique id of operations' launch
     */
    synchronized final void operationFinished(final int id) {
        mRunningOperations.remove(id);
    }

    /**
     * Cancels running operation.
     *
     * @param id           the unique id of operations' launch
     * @param mayInterrupt {@code true} if thread executing operation task should be interrupted;
     *                     otherwise, in-progress tasks are allowed to complete
     * @return {@code false} if the task could not be cancelled, typically because it has already
     * completed normally; {@code true} otherwise
     */
    synchronized final boolean cancel(final int id, final boolean mayInterrupt) {
        return cancel(id, mayInterrupt, true);
    }

    /**
     * Cancels all running operations.
     *
     * @param mayInterrupt {@code true} if threads executing operations task should be interrupted;
     *                     otherwise, in-progress tasks are allowed to complete
     */
    synchronized final void cancelAll(final boolean mayInterrupt) {
        for (final Integer key : mRunningOperations.keySet()) {
            cancel(key, mayInterrupt, false);
        }

        mRunningOperations.clear();
    }

    /**
     * Checks if operation launch with given id is still running.
     *
     * @param id the unique id of operations' launch
     * @return {@code true} if the operation is still running, {@code false} otherwise
     */
    @Contract(pure = true)
    synchronized final boolean isOperationRunning(final int id) {
        return mRunningOperations.containsKey(id);
    }

    /**
     * Checks if operation launch with given id was cancelled.
     *
     * @param id the unique id of operations' launch
     * @return {@code true} if the operation was cancelled, {@code false} otherwise
     */
    @Contract(pure = true)
    synchronized final boolean isOperationCancelled(final int id) {
        return mCancelledOperations.contains(Integer.valueOf(id));
    }

    private static class RunningOperation {

        private final ChronosOperation<?> mOperation;

        private final Future<?> mFuture;

        public RunningOperation(@NonNull final ChronosOperation<?> operation,
                @NonNull final Future<?> future) {
            mOperation = operation;
            mFuture = future;
        }

        public final boolean cancel(final boolean mayInterrupt) {
            mOperation.cancel();
            return mFuture.cancel(mayInterrupt);
        }
    }
}