/**
 *  Licensed under GPL. For more information, see
 *    http://jaxodraw.sourceforge.net/license.html
 *  or the LICENSE file in the jaxodraw distribution.
 */
package net.sf.jaxodraw.gui.swing.spinner;

import javax.swing.SpinnerNumberModel;


/** Specialization of SpinnerNumberModel that a) only allows integers (to avoid
 * rounding issues) and has some convenience methods for them;
 * b) optionally allows snapping the 'value' to only a subset of the integers.
 * @since 2.0
 */
public class JaxoSpinnerIntModel extends SpinnerNumberModel {
    private static final long serialVersionUID = 7526471155622776147L;

    private boolean snap;
    private int origin;

    /** With the given properties. 'value' and 'origin' are set to the 'minimum',
     * 'snap' to false.
     * @param minimum minimum
     * @param maximum  maximum
     * @param stepSize stepSize
     */
    public JaxoSpinnerIntModel(final int minimum, final int maximum, final int stepSize) {
        super(minimum, minimum, maximum, stepSize);

        origin = minimum;
    }

    /** With the given properties. 'snap' is set to true, 'value' is set to the
     *  snapped 'minimum'.
     * @param minimum minimum
     * @param maximum  maximum
     * @param stepSize stepSize
     * @param orig origin
     */
    public JaxoSpinnerIntModel(final int minimum, final int maximum, final int stepSize,
        final int orig) {
        super(minimum, minimum, maximum, stepSize);

        this.snap = true;
        this.origin = orig;

        snapValue();
    }

    /** {@link #getValue} as an int.
     * @return int
     */
    public final int getIntValue() {
        return getNumber().intValue();
    }

    /** {@inheritDoc} */
    @Override
    public void setValue(final Object value) {
        if (!(value instanceof Integer)) {
            throw new IllegalArgumentException();
        }

        setIntValue(((Integer) value).intValue());
    }

    /** {@link #setValue} with int argument.
     * @param newValue the new int value.
     */
    public final void setIntValue(final int newValue) {
        int value = newValue;

        if (snap) {
            value = snappedValue(value);
        }

        if (getNumber().intValue() != value) {
            super.setValue(Integer.valueOf(value));
        }
    }

    /** {@link #setStepSize} with int argument.
     * @param newValue the new step size.
     */
    public void setIntStepSize(final int newValue) {
        setStepSize(Integer.valueOf(newValue));
    }

    /** {@inheritDoc} */
    @Override
    public void setStepSize(final Number stepSize) {
        if (!(stepSize instanceof Integer)) {
            throw new IllegalArgumentException();
        }

        super.setStepSize(stepSize);

        snapValue();
    }

    /** Origin to be used for snapping. If 'snap' is true, the only
     *  allowed values are <code>origin + n * stepSize</code>, with
     *  'n' an integer.
     * @return int
     */
    public final int getOrigin() {
        return origin;
    }

    /** Set origin property (potentially changing also 'value').
     * @param newValue the new origin.
     */
    public void setOrigin(final int newValue) {
        if (origin != newValue) {
            origin = newValue;

            snapValue();
        }
    }

    /** Is  'value' always snapped to 'origin' + multiples of stepSize?
     * @return boolean
     */
    public final boolean isSnap() {
        return snap;
    }

    /** Set snap property (potentially changing also 'value').
     * @param newValue the new snap.
     */
    public void setSnap(final boolean newValue) {
        if (snap != newValue) {
            snap = newValue;

            snapValue();
        }
    }

    private void snapValue() {
        if (snap) {
            setIntValue(getIntValue());
        }
    }

    private int snappedValue(final int candidate) {
        final int min = ((Number) getMinimum()).intValue();
        final int max = ((Number) getMaximum()).intValue();
        final int stepSize = getStepSize().intValue();

        // a) round towards origin
        int candidateR = roundedTowardsOrigin(candidate);

        // b) but if 'candidate' is within [minimum,maximum] and there is an
        // allowed value within, stay there
        if ((min <= candidate) && (candidate <= max)) {
            final int minR = roundedTowardsOrigin(min);

            if (minR < min) {
                if (candidateR == minR) {
                    candidateR += stepSize;

                    // candidate may now be larger than 'max', but only
                    // if there is not a single allowed value between
                    // minimum and maximum
                }
            } else {
                final int maxR = roundedTowardsOrigin(max);

                // Note: both minR < min and maxR > max cannot happen
                if (maxR > max && candidateR == maxR) {
                        candidateR -= stepSize;
                }
            }
        }

        return candidateR;
    }

    private int roundedTowardsOrigin(final int candidate) {
        final int stepSize = getStepSize().intValue();

        return origin
        + (
            ((candidate < origin) ? (-1) : 1) * stepSize * Math.round(Math.abs(candidate
                    - origin) / (float) stepSize)
        );
    }
}
