/*
 * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package javafx.scene.paint;


import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javafx.beans.NamedArg;

/**
 * Defines one element of the ramp of colors to use on a gradient.
 * For more information see {@code javafx.scene.paint.LinearGradient} and
 * {@code javafx.scene.paint.RadialGradient}.
 *
 * <p>Example:</p>
 * <pre><code>
 * // object bounding box relative (proportional:true, default)
 * Stop[] stops = { new Stop(0, Color.WHITE), new Stop(1, Color.BLACK)};
 * LinearGradient lg = new LinearGradient(0, 0, 1, 0, true, CycleMethod.No_CYCLE, stops);
 * Rectangle r = new Rectangle();
 * r.setFill(lg);
 *</code></pre>
 * @since JavaFX 2.0
 */
public final class Stop {

    static final List<Stop> NO_STOPS =
        Collections.unmodifiableList(Arrays.asList(
            new Stop(0.0, Color.TRANSPARENT),
            new Stop(1.0, Color.TRANSPARENT)
        ));

    static List<Stop> normalize(Stop stops[]) {
        List<Stop> stoplist = (stops == null ? null : Arrays.asList(stops));
        return normalize(stoplist);
    }

    static List<Stop> normalize(List<Stop> stops) {
        if (stops == null) {
            return NO_STOPS;
        }
        Stop zerostop = null;
        Stop onestop = null;
        List<Stop> newlist = new ArrayList<Stop>(stops.size());
        for (Stop s : stops) {
            if (s == null || s.getColor() == null) continue;
            double off = s.getOffset();
            if (off <= 0.0) {
                if (zerostop == null || off >= zerostop.getOffset()) {
                    zerostop = s;
                }
            } else if (off >= 1.0) {
                if (onestop == null || off < onestop.getOffset()) {
                    onestop = s;
                }
            } else if (off == off) { // non-NaN
                for (int i = newlist.size() - 1; i >= 0; i--) {
                    Stop s2 = newlist.get(i);
                    if (s2.getOffset() <= off) {
                        if (s2.getOffset() == off) {
                            if (i > 0 && newlist.get(i-1).getOffset() == off) {
                                newlist.set(i, s);
                            } else {
                                newlist.add(i+1, s);
                            }
                        } else {
                            newlist.add(i+1, s);
                        }
                        s = null;
                        break;
                    }
                }
                if (s != null) {
                    newlist.add(0, s);
                }
            }
        }

        if (zerostop == null) {
            Color zerocolor;
            if (newlist.isEmpty()) {
                if (onestop == null) {
                    return NO_STOPS;
                }
                zerocolor = onestop.getColor();
            } else {
                zerocolor = newlist.get(0).getColor();
                if (onestop == null && newlist.size() == 1) {
                    // Special case for a single color with a non-0,1 offset.
                    // If we leave the color in there we end up with a 3-color
                    // gradient with all the colors being identical and we
                    // will not catch the optimization to a solid color.
                    newlist.clear();
                }
            }
            zerostop = new Stop(0.0, zerocolor);
        } else if (zerostop.getOffset() < 0.0) {
            zerostop = new Stop(0.0, zerostop.getColor());
        }
        newlist.add(0, zerostop);

        if (onestop == null) {
            onestop = new Stop(1.0, newlist.get(newlist.size()-1).getColor());
        } else if (onestop.getOffset() > 1.0) {
            onestop = new Stop(1.0, onestop.getColor());
        }
        newlist.add(onestop);

        return Collections.unmodifiableList(newlist);
    }

    /**
     * The {@code offset} variable is a number ranging from {@code 0} to {@code 1}
     * that indicates where this gradient stop is placed. For linear gradients,
     * the {@code offset} variable represents a location along the gradient vector.
     * For radial gradients, it represents a percentage distance from
     * the focus point to the edge of the outermost/largest circle.
     *
     * @profile common
     * @defaultValue 0.0
     */
    private double offset;

    /**
     * Gets a number ranging from {@code 0} to {@code 1}
     * that indicates where this gradient stop is placed. For linear gradients,
     * the {@code offset} variable represents a location along the gradient vector.
     * For radial gradients, it represents a percentage distance from
     * the focus point to the edge of the outermost/largest circle.
     *
     * @return position of the Stop within the gradient
     *         (ranging from {@code 0} to {@code 1})
     */
    public final double getOffset() {
        return offset;
    }

    /**
     * The color of the gradient at this offset.
     *
     * @profile common
     * @defaultValue Color.BLACK
     */
    private Color color;

    /**
     * Gets the color of the gradient at this offset.
     * @return the color of the gradient at this offset
     */
    public final Color getColor() {
        return color;
    }

    /**
     * The cached hash code, used to improve performance in situations where
     * we cache gradients, such as in the CSS routines.
     */
    private int hash = 0;

    /**
     * Creates a new instance of Stop.
     * @param offset Stop's position (ranging from {@code 0} to {@code 1}
     * @param color Stop's color
     */
    public Stop(@NamedArg("offset") double offset, @NamedArg(value="color", defaultValue="BLACK") Color color) {
        this.offset = offset;
        this.color = color;
    }

    /**
     * Indicates whether some other object is "equal to" this one.
     * @param obj the reference object with which to compare.
     * @return {@code true} if this object is equal to the {@code obj} argument; {@code false} otherwise.
     */
    @Override public boolean equals(Object obj) {
        if (obj == null) return false;
        if (obj == this) return true;
        if (obj instanceof Stop) {
            Stop other = (Stop) obj;
            return offset == other.offset &&
              (color == null ? other.color == null : color.equals(other.color));
        } else return false;
    }

    /**
     * Returns a hash code for this {@code Stop} object.
     * @return a hash code for this {@code Stop} object.
     */
    @Override public int hashCode() {
        if (hash == 0) {
            long bits = 17L;
            bits = 37L * bits + Double.doubleToLongBits(offset);
            bits = 37L * bits + color.hashCode();
            hash = (int) (bits ^ (bits >> 32));
        }
        return hash;
    }

    /**
     * Returns a string representation of this {@code Stop} object.
     * @return a string representation of this {@code Stop} object.
     */
    @Override public String toString() {
        return color + " " + offset*100 + "%";
    }
}
