package org.gwtbootstrap3.extras.animate.client.ui;

/*
 * #%L
 * GwtBootstrap3
 * %%
 * Copyright (C) 2013 - 2014 GwtBootstrap3
 * %%
 * 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.
 * #L%
 */

import com.google.gwt.core.client.Scheduler;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.StyleInjector;
import com.google.gwt.user.client.ui.UIObject;
import org.gwtbootstrap3.extras.animate.client.ui.constants.Animation;

import java.util.ArrayList;

/**
 * Utility class to dynamically animate objects using CSS animations.
 *
 * @author Pavel Zlámal
 */
public class Animate {

    // store used styles, so they are not injected to the DOM everytime.
    private static final ArrayList<String> usedStyles = new ArrayList<String>();

    /**
     * Animate any element with specific animation. Animation is done by CSS and runs only once.
     *
     * Animation is started when element is appended to the DOM or new (not same) animation is added
     * to already displayed element. Animation runs on hidden elements too and is not paused/stopped
     * when element is set as hidden.
     *
     * @param widget Widget to apply animation to.
     * @param animation Type of animation to apply.
     * @param <T> Any object extending UIObject class (typically Widget).
     * @return Animation's CSS class name, which can be removed to stop animation.
     */
    public static <T extends UIObject> String animate(final T widget, final Animation animation) {
        return animate(widget, animation, 1, -1, -1);
    }

    /**
     * Animate any element with specific animation. Animation is done by CSS and runs multiple times.
     *
     * Animation is started when element is appended to the DOM or new (not same) animation is added
     * to already displayed element. Animation runs on hidden elements too and is not paused/stopped
     * when element is set as hidden.
     *
     * @param widget Widget to apply animation to.
     * @param animation Type of animation to apply.
     * @param count Number of animation repeats. 0 disables animation, any negative value set repeats to infinite.
     * @param <T> Any object extending UIObject class (typically Widget).
     * @return Animation's CSS class name, which can be removed to stop animation.
     */
    public static <T extends UIObject> String animate(final T widget, final Animation animation, final int count) {
        return animate(widget, animation, count, -1, -1);
    }

    /**
     * Animate any element with specific animation. Animation is done by CSS and runs multiple times.
     *
     * Animation is started when element is appended to the DOM or new (not same) animation is added
     * to already displayed element. Animation runs on hidden elements too and is not paused/stopped
     * when element is set as hidden.
     *
     * @param widget Widget to apply animation to.
     * @param animation Type of animation to apply.
     * @param count Number of animation repeats. 0 disables animation, any negative value set repeats to infinite.
     * @param duration Animation duration in ms. 0 disables animation, any negative value keeps default of original animation.
     * @param <T> Any object extending UIObject class (typically Widget).
     * @return Animation's CSS class name, which can be removed to stop animation.
     */
    public static <T extends UIObject> String animate(final T widget, final Animation animation, final int count, final int duration) {
        return animate(widget, animation, count, duration, -1);
    }

    /**
     * Animate any element with specific animation. Animation is done by CSS and runs multiple times.
     *
     * Animation is started when element is appended to the DOM or new (not same) animation is added
     * to already displayed element. Animation runs on hidden elements too and is not paused/stopped
     * when element is set as hidden.
     *
     * @param widget Widget to apply animation to.
     * @param animation Type of animation to apply.
     * @param count Number of animation repeats. 0 disables animation, any negative value set repeats to infinite.
     * @param duration Animation duration in ms. 0 disables animation, any negative value keeps default of original animation.
     * @param delay Delay before starting the animation loop in ms. Value <= 0 means no delay.
     * @param <T> Any object extending UIObject class (typically Widget).
     * @return Animation's CSS class name, which can be removed to stop animation.
     */
    public static <T extends UIObject> String animate(final T widget, final Animation animation, final int count, final int duration, final int delay) {

        if (widget != null && animation != null) {
            // on valid input
            if (widget.getStyleName().contains(animation.getCssName())) {
                // animation is present, remove it and run again.
                stopAnimation(widget, animation.getCssName() + " " + getStyleNameFromAnimation(animation.getCssName(),count,duration,delay));
                Scheduler.get().scheduleFixedDelay(new Scheduler.RepeatingCommand() {
                    @Override
                    public boolean execute() {
                        styleElement(widget.getElement(), animation.getCssName(), count, duration, delay);
                        return false;
                    }
                }, 200);
                return animation.getCssName() + " " + getStyleNameFromAnimation(animation.getCssName(),count,duration,delay);
            } else {
                // animation was not present, run immediately
                return styleElement(widget.getElement(), animation.getCssName(), count, duration, delay);
            }
        } else {
            return null;
        }

    }

    /**
     * Animate any element with specific animation. Animation is done by CSS and runs only once.
     *
     * Animation is started when element is appended to the DOM or new (not same) animation is added
     * to already displayed element. Animation runs on hidden elements too and is not paused/stopped
     * when element is set as hidden.
     *
     * @param widget Widget to apply animation to.
     * @param animation Custom CSS class name used as animation.
     * @param <T> Any object extending UIObject class (typically Widget).
     * @return Animation's CSS class name, which can be removed to stop animation.
     */
    public static <T extends UIObject> String animate(final T widget, final String animation) {
        return animate(widget, animation, 1, -1, -1);
    }

    /**
     * Animate any element with specific animation. Animation is done by CSS and runs multiple times.
     *
     * Animation is started when element is appended to the DOM or new (not same) animation is added
     * to already displayed element. Animation runs on hidden elements too and is not paused/stopped
     * when element is set as hidden.
     *
     * @param widget Widget to apply animation to.
     * @param animation Custom CSS class name used as animation.
     * @param count Number of animation repeats. 0 disables animation, any negative value set repeats to infinite.
     * @param <T> Any object extending UIObject class (typically Widget).
     * @return Animation's CSS class name, which can be removed to stop animation.
     */
    public static <T extends UIObject> String animate(final T widget, final String animation, final int count) {
        return animate(widget, animation, count, -1, -1);
    }

    /**
     * Animate any element with specific animation. Animation is done by CSS and runs multiple times.
     *
     * Animation is started when element is appended to the DOM or new (not same) animation is added
     * to already displayed element. Animation runs on hidden elements too and is not paused/stopped
     * when element is set as hidden.
     *
     * @param widget Widget to apply animation to.
     * @param animation Custom CSS class name used as animation.
     * @param count Number of animation repeats. 0 disables animation, any negative value set repeats to infinite.
     * @param duration Animation duration in ms. 0 disables animation, any negative value keeps default of original animation.
     * @param <T> Any object extending UIObject class (typically Widget).
     * @return Animation's CSS class name, which can be removed to stop animation.
     */
    public static <T extends UIObject> String animate(final T widget, final String animation, final int count, final int duration) {
        return animate(widget, animation, count, duration, -1);
    }

    /**
     * Animate any element with specific animation. Animation is done by CSS and runs multiple times.
     *
     * Animation is started when element is appended to the DOM or new (not same) animation is added
     * to already displayed element. Animation runs on hidden elements too and is not paused/stopped
     * when element is set as hidden.
     *
     * @param widget Widget to apply animation to.
     * @param animation Custom CSS class name used as animation.
     * @param count Number of animation repeats. 0 disables animation, any negative value set repeats to infinite.
     * @param duration Animation duration in ms. 0 disables animation, any negative value keeps default of original animation.
     * @param delay Delay before starting the animation loop in ms. Value <= 0 means no delay.
     * @param <T> Any object extending UIObject class (typically Widget).
     * @return Animation's CSS class name, which can be removed to stop animation.
     */
    public static <T extends UIObject> String animate(final T widget, final String animation, final int count, final int duration, final int delay) {

        if (widget != null && animation != null) {
            // on valid input
            if (widget.getStyleName().contains(animation)) {
                // animation is present, remove it and run again.
                stopAnimation(widget, animation);
                Scheduler.get().scheduleFixedDelay(new Scheduler.RepeatingCommand() {
                    @Override
                    public boolean execute() {
                        styleElement(widget.getElement(), animation, count, duration, delay);
                        return false;
                    }
                }, 200);
                return animation + " " + getStyleNameFromAnimation(animation,count,duration,delay);
            } else {
                // animation was not present, run immediately
                return styleElement(widget.getElement(), animation, count, duration, delay);
            }
        } else {
            return null;
        }

    }

    /**
     * Styles element with animation class. New class name is generated to customize count, duration and delay.
     * Style is removed on animation end (if not set to infinite).
     *
     * @param element Element to apply animation to.
     * @param animation Type of animation to apply.
     * @param count Number of animation repeats. 0 disables animation, any negative value set repeats to infinite.
     * @param duration Animation duration in ms. 0 disables animation, any negative value keeps default of original animation.
     * @param delay Delay before starting the animation loop in ms. Value <= 0 means no delay.
     * @param <T> Any object extending UIObject class (typically Widget).
     * @return Animation's CSS class name, which can be removed to stop animation.
     */
    private static <T extends UIObject> String styleElement(Element element, String animation, int count, int duration, int delay) {

        if (!usedStyles.contains(animation + " " + getStyleNameFromAnimation(animation,count,duration,delay))) {

            String styleSheet = "." + getStyleNameFromAnimation(animation, count, duration, delay) + " {";

            // 1 is default, 0 disable animation, any negative -> infinite loop
            if (count >= 0) {

                styleSheet += "-webkit-animation-iteration-count: " + count + ";" +
                        "-moz-animation-iteration-count:" + count + ";" +
                        "-ms-animation-iteration-count:" + count + ";" +
                        "-o-animation-iteration-count:" + count + ";" +
                        "animation-iteration-count:" + count + ";";

            } else {

                styleSheet += "-webkit-animation-iteration-count: infinite;" +
                        "-moz-animation-iteration-count: infinite;" +
                        "-ms-animation-iteration-count: infinite;" +
                        "-o-animation-iteration-count: infinite;" +
                        "animation-iteration-count: infinite;";

            }

            // if not default (any negative -> use default)
            if (duration >= 0) {

                styleSheet += "-webkit-animation-duration: " + duration + "ms;" +
                        "-moz-animation-duration:" + duration + "ms;" +
                        "-ms-animation-duration:" + duration + "ms;" +
                        "-o-animation-duration:" + duration + "ms;" +
                        "animation-duration:" + duration + "ms;";

            }

            // if not default (any negative -> use default)
            if (delay >= 0) {

                styleSheet += "-webkit-animation-delay: " + delay + "ms;" +
                        "-moz-animation-delay:" + delay + "ms;" +
                        "-ms-animation-delay:" + delay + "ms;" +
                        "-o-animation-delay:" + delay + "ms;" +
                        "animation-delay:" + delay + "ms;";

            }

            styleSheet += "}";

            // inject new style
            StyleInjector.injectAtEnd(styleSheet, true);

            usedStyles.add(animation + " " + getStyleNameFromAnimation(animation, count, duration, delay));

        }

        // start animation
        element.addClassName(animation + " " + getStyleNameFromAnimation(animation,count,duration,delay));

        // remove animation on end so we could start it again
        // removeAnimationOnEnd(element, animation + " anim-"+count+"-"+duration+"-"+delay);

        return animation + " " + getStyleNameFromAnimation(animation,count,duration,delay);

    }

    /**
     * Removes custom animation class on animation end.
     *
     * @param widget Element to remove style from.
     * @param animation Animation CSS class to remove.
     */
    public static final <T extends UIObject> void removeAnimationOnEnd(final T widget, final String animation) {
        if (widget != null && animation != null) {
            removeAnimationOnEnd(widget.getElement(), animation);
        }
    }

    /**
     * Removes custom animation class on animation end.
     *
     * @param element Element to remove style from.
     * @param animation Animation CSS class to remove.
     */
    private static final native void removeAnimationOnEnd(Element element, String animation) /*-{

        var elem = $wnd.jQuery(element);
        elem.one('webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', { elem: elem }, function(event) {
            event.data.elem.removeClass(animation);
        });

    }-*/;

    /**
     * Removes custom animation class and stops animation.
     *
     * @param widget Element to remove style from.
     * @param animation Animation CSS class to remove.
     */
    public static final <T extends UIObject> void stopAnimation(final T widget, final String animation){
        if (widget != null && animation != null) {
            stopAnimation(widget.getElement(), animation);
        }
    }

    /**
     * Removes custom animation class and stops animation.
     *
     * @param element Element to remove style from.
     * @param animation Animation CSS class to remove.
     */
    private static final native void stopAnimation(Element element, String animation) /*-{
        $wnd.jQuery(element).removeClass(animation);
    }-*/;

    /**
     * Helper method, which returns unique class name for combination of animation and it's settings.
     *
     * @param animation Animation CSS class name.
     * @param count Number of animation repeats.
     * @param duration Animation duration in ms.
     * @param delay Delay before starting the animation loop in ms.
     * @return String representation of class name like "animation-count-duration-delay".
     */
    private static String getStyleNameFromAnimation(final String animation, int count, int duration, int delay) {

        // fix input
        if (count < 0) count = -1;
        if (duration < 0) duration = -1;
        if (delay < 0) delay = -1;

        String styleName = "";

        // for all valid animations
        if (animation != null && !animation.isEmpty() && animation.split(" ").length > 1) {

            styleName += animation.split(" ")[1]+"-"+count+"-"+duration+"-"+delay;

        // for all custom animations
        } else if (animation != null && !animation.isEmpty() && animation.split(" ").length == 1) {

            styleName += animation+"-"+count+"-"+duration+"-"+delay;

        }

        return styleName;

    }

}