/*
 * Copyright 2016-2024 MicroEJ Corp. All rights reserved.
 * This library is provided in source code for use, modification and test, subject to license terms.
 * Any modification of the source code will break MicroEJ Corp. warranties on the whole library.
 */
package ej.mwt.animation;

import ej.annotation.Nullable;
import ej.basictool.ArrayTools;
import ej.basictool.ThreadUtils;
import ej.bon.Constants;
import ej.bon.Util;
import ej.microui.MicroUI;
import ej.microui.display.Display;

/**
 * An animator executes animations as fast as possible.
 * <p>
 * The animator loops on:
 * <ul>
 * <li>retrieve the current time with {@link ej.bon.Util#platformTimeMillis()},</li>
 * <li>call the {@link Animation#tick(long)} of all the registered animations,</li>
 * <li>wait for the end of the flush,</li>
 * <li>start again while at least one animation is running.</li>
 * </ul>
 * <p>
 * Only one animator can be running at any time.
 * <p>
 * In the same way, {@link Display#callOnFlushCompleted(Runnable)} must not be called when an animator is running
 */
public class Animator {

    /**
     * {@link Constants#getBoolean(String) BON boolean constant} to enable/disable the animator debug.
     * <p>
     * If enabled, {@value #DEBUG_ANIMATOR_MONITOR_CONSTANT} must also be set.
     */
    public static final String DEBUG_ANIMATOR_ENABLED_CONSTANT = "ej.mwt.debug.animator.enabled";

    /**
     * {@link Constants#getClass(String) BON Class constant} to set the animator monitor.
     * <p>
     * Set it to the FQN of an implementation of {@link AnimatorListener} to monitor the animator.
     */
    public static final String DEBUG_ANIMATOR_MONITOR_CONSTANT = "ej.mwt.debug.animator.monitor";

    static {
        if (Constants.getBoolean(DEBUG_ANIMATOR_ENABLED_CONSTANT)) {
            Class<?> animatorListenerClass = Constants.getClass(DEBUG_ANIMATOR_MONITOR_CONSTANT);
            if (!AnimatorListener.class.isAssignableFrom(animatorListenerClass)) {
                throw new IllegalArgumentException("Class set to " + DEBUG_ANIMATOR_MONITOR_CONSTANT + " ( " + animatorListenerClass.getName() + " ) does not implement " + AnimatorListener.class.getSimpleName());
            }
            Object animatorListener;
            try {
                animatorListener = animatorListenerClass.newInstance();
            } catch (InstantiationException | IllegalAccessException e) {
                throw new IllegalArgumentException("Class set to " + DEBUG_ANIMATOR_MONITOR_CONSTANT + " cannot be instantiated", e);
            }
            assert (animatorListener instanceof AnimatorListener);
            ANIMATOR_LISTENER = (AnimatorListener) animatorListener;
        } else {
            ANIMATOR_LISTENER = null;
        }
    }

    /**
     * Creates an animator.
     * <p>
     * Multiple animators may be created but only one can be running at any time.
     */
    public Animator() {
        this.monitor = new Object();
        this.animations = new Animation[0];
        this.started = false;
        this.ticking = false;
    }

    /**
     * Starts an animation.
     * <p>
     * The animation tick method is then called every period.
     * <p>
     * If the animation is already started, nothing is done.
     *
     * @param animation
     *            the animation to start.
     * @see Animation#tick(long)
     */
    public void startAnimation(Animation animation) {
        synchronized (this.monitor) {
            Animation[] animations = this.animations;
            if (!ArrayTools.contains(animations, animation)) {
                this.animations = ArrayTools.add(animations, animation);
                if (!this.started) {
                    this.started = true;
                    this.tickingRunnable = createTickRunnable();
                    callOnFlushCompleted(this.tickingRunnable);
                }
            }
        }
    }

    /**
     * Stops an animation.
     * <p>
     * The tick method of the given animation will not be called anymore by this animator.
     * <p>
     * This method must be called in the MicroUI thread.
     *
     * @param animation
     *            the animation to stop.
     * @throws IllegalStateException
     *             if this method is called in an other thread than the MicroUI thread or during an animation tick.
     */
    public void stopAnimation(Animation animation) {
        if (!MicroUI.isUIThread() || this.ticking) {
            throw new IllegalStateException();
        }
        stopAnimationNoCheck(animation);
    }

    /**
     * Stops all the animations.
     * <p>
     * The tick method of every animation will not be called anymore by this animator.
     * <p>
     * This method must be called in the MicroUI thread.
     *
     * @throws IllegalStateException
     *             if this method is called in an other thread than the MicroUI thread or during an animation tick.
     */
    public void stopAllAnimations() {
        if (!MicroUI.isUIThread() || this.ticking) {
            throw new IllegalStateException();
        }
        synchronized (this.monitor) {
            if (this.animations.length > 0) {
                this.animations = new Animation[0];
                this.started = false;
                this.tickingRunnable = null;
                Display.getDisplay().cancelCallOnFlushCompleted();
            }
        }
    }

    /**
     * Indicates to this animator that a render has been requested.
     * <p>
     * Calling this method is only useful if the {@link #DEBUG_ANIMATOR_ENABLED_CONSTANT} constant is enabled.
     * <p>
     * This method is called automatically when a render of a widget or of a desktop is requested. This method can be
     * called manually if the animator is used to render something else.
     */
    public void indicateRenderRequested() {
        this.renderRequested = true;
    }

    /**
     * Listener for animator events.
     */
    public interface AnimatorListener {

        /**
         * Handles the case when none of the animations has requested a render during an animator tick.
         *
         * @param animations
         *            the animations of the animator.
         */
        void onVoidTick(Animation[] animations);
    }
}
