/*
 * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package javafx.animation;

import java.util.Collection;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;

import javafx.beans.NamedArg;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.util.Duration;

/**
 * Defines target values at a specified point in time for a set of variables
 * that are interpolated along a {@link Timeline}.
 * <p>
 * The developer controls the interpolation of a set of variables for the
 * interval between successive key frames by providing a target value and an
 * {@link Interpolator} associated with each variable. The variables are
 * interpolated such that they will reach their target value at the specified
 * time. An {@link #onFinished} function is invoked on each {@code KeyFrame} if one
 * is provided. A {@code KeyFrame} can optionally have a {@link #name}, which
 * will result in a cuepoint that is automatically added to the {@code Timeline}.
 *
 * @see Timeline
 * @see KeyValue
 * @see Interpolator
 *
 * @since JavaFX 2.0
 */
public final class KeyFrame {

    private static final EventHandler<ActionEvent> DEFAULT_ON_FINISHED = null;
    private static final String DEFAULT_NAME = null;

    /**
     * Returns the time offset of this {@code KeyFrame}.
     *
     * The returned {@link javafx.util.Duration} defines the time offset within
     * a single cycle of a {@link Timeline} at which the {@link KeyValue
     * KeyValues} will be set and at which the {@link #onFinished} function
     * variable will be called.
     * <p>
     * The {@code time} of a {@code KeyFrame} has to be greater than or equal to
     * {@link javafx.util.Duration#ZERO} and it cannot be
     * {@link javafx.util.Duration#UNKNOWN}.
     *
     * Note: While the unit of {@code time} is a millisecond, the granularity
     * depends on the underlying operating system and will in general be larger.
     * For example animations on desktop systems usually run with a maximum of
     * 60fps which gives a granularity of ~17 ms.
     */
    public Duration getTime() {
        return time;
    }
    private final Duration time;

    /**
     * Returns an immutable {@code Set} of {@link KeyValue} instances.
     *
     * A {@code KeyValue} defines a target and the desired value that should be
     * interpolated at the specified time of this {@code KeyFrame}.
     */
    public Set<KeyValue> getValues() {
        return values;
    }
    private final Set<KeyValue> values;

    /**
     * Returns the {@code onFinished} event handler of this {@code KeyFrame}.
     *
     * The {@code onFinished} event handler is a function that is called when
     * the elapsed time on a cycle passes the specified time of this
     * {@code KeyFrame}. The {@code onFinished} function variable will be called
     * if the elapsed time passes the indicated value, even if it never equaled
     * the time value exactly.
     */
    public EventHandler<ActionEvent> getOnFinished() {
        return onFinished;
    }
    private final EventHandler<ActionEvent> onFinished;

    /**
     * Returns the {@code name} of this {@code KeyFrame}.
     *
     * If a named {@code KeyFrame} is added to a {@link Timeline}, a cuepoint
     * with the {@code name} and the {@link #time} of the {@code KeyFrame} will
     * be added automatically. If the {@code KeyFrame} is removed, the cuepoint
     * will also be removed.
     */
    public String getName() {
        return name;
    }
    private final String name;

    /**
     * Constructor of {@code KeyFrame}
     * <p>
     * If a passed in {@code KeyValue} is {@code null} or a duplicate, it will
     * be ignored.
     *
     * @param time
     *            the {@link #time}
     * @param name
     *            the {@link #name}
     * @param onFinished
     *            the {@link #onFinished onFinished-handler}
     * @param values
     *            a {@link javafx.collections.ObservableList} of
     *            {@link KeyValue} instances
     * @throws NullPointerException
     *             if {@code time} is null
     * @throws IllegalArgumentException
     *             if {@code time} is invalid (see {@link #time})
     */
    public KeyFrame(@NamedArg("time") Duration time, @NamedArg("name") String name,
            @NamedArg("onFinished") EventHandler<ActionEvent> onFinished, @NamedArg("values") Collection<KeyValue> values) {
        if (time == null) {
            throw new NullPointerException("The time has to be specified");
        }
        if (time.lessThan(Duration.ZERO) || time.equals(Duration.UNKNOWN)) {
            throw new IllegalArgumentException("The time is invalid.");
        }
        this.time = time;
        this.name = name;
        if (values != null) {
            final Set<KeyValue> set = new CopyOnWriteArraySet<KeyValue>(values);
            set.remove(null);
            this.values = (set.size() == 0) ? Collections.<KeyValue> emptySet()
                    : (set.size() == 1) ? Collections.<KeyValue> singleton(set
                            .iterator().next()) : Collections
                            .unmodifiableSet(set);
        } else {
            this.values = Collections.<KeyValue> emptySet();
        }
        this.onFinished = onFinished;
    }

    /**
     * Constructor of {@code KeyFrame}
     * <p>
     * If a passed in {@code KeyValue} is {@code null} or a duplicate, it will
     * be ignored.
     *
     * @param time
     *            the {@link #time}
     * @param name
     *            the {@link #name}
     * @param onFinished
     *            the {@link #onFinished onFinished-handler}
     * @param values
     *            the {@link KeyValue} instances
     * @throws NullPointerException
     *             if {@code time} is null
     * @throws IllegalArgumentException
     *             if {@code time} is invalid (see {@link #time})
     */
    public KeyFrame(@NamedArg("time") Duration time, @NamedArg("name") String name,
            @NamedArg("onFinished") EventHandler<ActionEvent> onFinished, @NamedArg("values") KeyValue... values) {
        if (time == null) {
            throw new NullPointerException("The time has to be specified");
        }
        if (time.lessThan(Duration.ZERO) || time.equals(Duration.UNKNOWN)) {
            throw new IllegalArgumentException("The time is invalid.");
        }
        this.time = time;
        this.name = name;
        if (values != null) {
            final Set<KeyValue> set = new CopyOnWriteArraySet<KeyValue>();
            for (final KeyValue keyValue : values) {
                if (keyValue != null) {
                    set.add(keyValue);
                }
            }
            this.values = (set.size() == 0) ? Collections.<KeyValue> emptySet()
                    : (set.size() == 1) ? Collections.<KeyValue> singleton(set
                            .iterator().next()) : Collections
                            .unmodifiableSet(set);
        } else {
            this.values = Collections.emptySet();
        }
        this.onFinished = onFinished;
    }

    /**
     * Constructor of {@code KeyFrame}
     *
     * @param time
     *            the {@link #time}
     * @param onFinished
     *            the {@link #onFinished onFinished-handler}
     * @param values
     *            the {@link KeyValue} instances
     * @throws NullPointerException
     *             if {@code time} is null
     * @throws IllegalArgumentException
     *             if {@code time} is invalid (see {@link #time})
     */
    public KeyFrame(@NamedArg("time") Duration time, @NamedArg("onFinished") EventHandler<ActionEvent> onFinished,
            @NamedArg("values") KeyValue... values) {
        this(time, DEFAULT_NAME, onFinished, values);
    }

    /**
     * Constructor of {@code KeyFrame}
     *
     * @param time
     *            the {@link #time}
     * @param name
     *            the {@link #name}
     * @param values
     *            the {@link KeyValue} instances
     * @throws NullPointerException
     *             if {@code time} is null
     * @throws IllegalArgumentException
     *             if {@code time} is invalid (see {@link #time})
     */
    public KeyFrame(@NamedArg("time") Duration time, @NamedArg("name") String name, @NamedArg("values") KeyValue... values) {
        this(time, name, DEFAULT_ON_FINISHED, values);
    }

    /**
     * Constructor of {@code KeyFrame}
     *
     * @param time
     *            the {@link #time}
     * @param values
     *            the {@link KeyValue} instances
     * @throws NullPointerException
     *             if {@code time} is null
     * @throws IllegalArgumentException
     *             if {@code time} is invalid (see {@link #time})
     */
    public KeyFrame(@NamedArg("time") Duration time, @NamedArg("values") KeyValue... values) {
        this(time, DEFAULT_NAME, DEFAULT_ON_FINISHED, values);
    }

    /**
     * Returns a string representation of this {@code KeyFrame} object.
     * @return a string representation of this {@code KeyFrame} object.
     */
    @Override
    public String toString() {
        return "KeyFrame [time=" + time + ", values=" + values
                + ", onFinished=" + onFinished + ", name=" + name + "]";
    }

    /**
     * Returns a hash code for this {@code KeyFrame} object.
     * @return a hash code for this {@code KeyFrame} object.
     */
    @Override
    public int hashCode() {
        assert (time != null) && (values != null);
        final int prime = 31;
        int result = 1;
        result = prime * result + time.hashCode();
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        result = prime * result
                + ((onFinished == null) ? 0 : onFinished.hashCode());
        result = prime * result + values.hashCode();
        return result;
    }

    /**
     * Indicates whether some other object is "equal to" this one.
     * Two {@code KeyFrames} are considered equal, if their {@link #getTime()
     * time}, {@link #onFinished onFinished}, and {@link #getValues() values}
     * are equal.
     */
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj instanceof KeyFrame) {
            final KeyFrame kf = (KeyFrame) obj;
            assert (time != null) && (values != null) && (kf.time != null)
                    && (kf.values != null);
            return time.equals(kf.time)
                    && ((name == null) ? kf.name == null : name.equals(kf.name))
                    && ((onFinished == null) ? kf.onFinished == null
                            : onFinished.equals(kf.onFinished))
                    && values.equals(kf.values);
        }
        return false;
    }

}
