/*
 * The contents of this file are subject to the terms of the Common Development
 * and Distribution License (the License). You may not use this file except in
 * compliance with the License.
 *
 * You can obtain a copy of the License at http://www.netbeans.org/cddl.html
 * or http://www.netbeans.org/cddl.txt.
 * 
 * When distributing Covered Code, include this CDDL Header Notice in each file
 * and include the License file at http://www.netbeans.org/cddl.txt.
 * If applicable, add the following below the CDDL Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * The Original Software is NetBeans. The Initial Developer of the Original
 * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
 * Microsystems, Inc. All Rights Reserved.
 */
package org.openide.awt;

import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;

import java.util.Vector;


/**
* A class that produces a Spin Button.
*
* <P>
* <TABLE BORDER COLS=3 WIDTH=100%>
* <TR><TH WIDTH=15%>Property<TH WIDTH=15%>Property Type<TH>Description
* <TR><TD> Orientation <TD> boolean <TD> Orientation of SpinButton (Left-right or Up-down)
* <TR><TD> Minimum     <TD> int     <TD> Minimum value.
* <TR><TD> Maximum     <TD> int     <TD> Maximum value.
* <TR><TD> Step        <TD> int     <TD> Step.
* <TR><TD> Value       <TD> int     <TD> Current value.
* <TR><TD> RepeatDelay <TD> int     <TD> Delay time after press SpinButton [ms]
* <TR><TD> RepeatRate  <TD> int     <TD> Repeat rate while holding PressButton [ms]
* </TABLE>
*
* @deprecated Obsoleted by <code>javax.swing.JSpinner</code> in JDK 1.4
* @version 3.06, November 17, 1997
* @author  Petr Hamernik, Jan Jancura
*/
public class SpinButton extends Canvas {
    /** generated Serialized Version UID */
    static final long serialVersionUID = -3525959415481788776L;

    /** Default orientation of SpinButton. Currently false (UP-DOWN).
    * @see #DEFAULT_ORIENTATION
    * @see #setOrientation
    * @see #getOrientation
    */
    public static final boolean DEFAULT_ORIENTATION = false;

    /** Default minimum. Currently 0.
    * @see #minimum
    * @see #setMinimum
    * @see #getMinimum
    */
    public static final int DEFAULT_MINIMUM = 0;

    /** Default maximum. Currently 100.
    * @see #maximum
    * @see #setMaximum
    * @see #getMaximum
    */
    public static final int DEFAULT_MAXIMUM = 100;

    /** Default step. Currently 1.
    * @see #step
    * @see #setStep
    * @see #getStep
    */
    public static final int DEFAULT_STEP = 1;

    /** Default value of repeatDelay. Currently 300 ms.
    * @see #setDelay
    * @see #getDelay
    * @see #repeatDelay
    */
    public static final int DEFAULT_REPEAT_DELAY = 300;

    /** Default value of repeatRate. Currently 70 ms.
    * @see #setRate
    * @see #getRate
    * @see #repeatRate
    */
    public static final int DEFAULT_REPEAT_RATE = 70;

    /** Helper constant */
    private static final boolean SPIN_UP = true;

    /** Helper constant */
    private static final boolean SPIN_DOWN = false;

    /** Current orientation of SpinButton.
    *  True = LEFT-RIGHT, False = UP-DOWN
    * @see #DEFAULT_ORIENTATION
    * @see #setOrientation
    * @see #getOrientation
    */
    protected boolean orientation = DEFAULT_ORIENTATION;

    /** Current orientation of arrows of SpinButton.
    *  True = LEFT-RIGHT, False = UP-DOWN
    * @see #DEFAULT_ORIENTATION
    * @see #setOrientation
    * @see #getOrientation
    */
    protected boolean arrowsOrientation = DEFAULT_ORIENTATION;

    /** Minimum of the range of the SpinButton.
    * @see #DEFAULT_MINIMUM
    * @see #setMinimum
    * @see #getMinimum
    */
    protected int minimum = DEFAULT_MINIMUM;

    /** Maximum of the range of the SpinButton.
    * @see #DEFAULT_MAXIMUM
    * @see #setMaximum
    * @see #getMaximum
    */
    protected int maximum = DEFAULT_MAXIMUM;

    /** Step of the SpinButton.
    * @see #DEFAULT_STEP
    * @see #setStep
    * @see #getStep
    */
    protected int step = DEFAULT_STEP;

    /** Value of the SpinButton. Default 0.
    * @see #setValue
    * @see #getValue
    */
    protected int value = 0;

    /** Adjusts the amount of time that elapses before a increment
    * (or decrement) begins repeating when you hold down a mouse
    * button. [ms]
    * @see #setDelay
    * @see #getDelay
    * @see #DEFAULT_REPEAT_DELAY
    */
    protected int repeatDelay = DEFAULT_REPEAT_DELAY;

    /** Adjusts the speed at which a increment (or decrement)
    *  repeats when you hold down a mouse button. [ms]
    * @see #setRate
    * @see #getRate
    * @see #DEFAULT_REPEAT_RATE
    */
    protected int repeatRate = DEFAULT_REPEAT_RATE;

    /** Spin repeat thread. When the SpinButton is holded this thread
     *  runs and regulary sends the events to SpinButton.
     */
    protected RepeatThread rt = null;

    /** Flag if the SpinRepeatThread is currently running. */
    protected boolean running = false;

    /** Flag if the SpinRepeatThread is currently running. */
    protected boolean repeating = true;

    /** Current direction of the run of the SpinRepeatThread. */
    protected boolean runningDir = SPIN_DOWN;
    protected boolean boundsIgnored = false;

    /** Property change listeners storage */
    private PropertyChangeSupport valueSupport = new PropertyChangeSupport(this);

    /** SpinButton change listeners storage */
    private Vector spinButtonListeners = new Vector(3, 3);

    /** Create new SpinButton. */
    public SpinButton() {
        setBackground(SystemColor.control);
        setForeground(SystemColor.controlText);

        addMouseListener(
            new MouseAdapter() {
                public void mousePressed(MouseEvent evt) {
                    Dimension d = getSize();
                    boolean newDir = SPIN_UP;

                    if (orientation) {
                        if (evt.getX() <= ((d.width - 1) / 2)) {
                            newDir = SPIN_DOWN;
                        } else {
                            newDir = SPIN_UP;
                        }
                    } else {
                        if (evt.getY() <= ((d.height - 1) / 2)) {
                            newDir = SPIN_UP;
                        } else {
                            newDir = SPIN_DOWN;
                        }
                    }

                    if (
                        (((newDir == SPIN_UP) && (value >= maximum)) || ((newDir == SPIN_DOWN) && (value <= minimum))) &&
                            !boundsIgnored
                    ) {
                        return;
                    }

                    switchRun(newDir);
                    repaint();
                }

                public void mouseReleased(MouseEvent evt) {
                    boolean r = running;
                    switchStop();

                    if (r) {
                        repaint();
                    }
                }
            }
        );
    }

    /**
    * Setter method for foreground color.
    *
    * @param color New foreground color.
    */
    public void setForeground(Color color) {
        super.setForeground(color);
        repaint();
    }

    /** Sets the new orientation.
    * @param aDir new value of orientation.
    * @see #orientation
    * @see #DEFAULT_ORIENTATION
    * @see #getOrientation
    */
    public void setOrientation(boolean aDir) {
        orientation = aDir;
        switchStop();
        repaint();
    }

    /** Sets the new orientation of arows.
    * @param aDir new value of orientation of arows.
    * @see #orientation
    * @see #DEFAULT_ORIENTATION
    * @see #getOrientation
    */
    public void setArrowsOrientation(boolean aDir) {
        arrowsOrientation = aDir;
        switchStop();
        repaint();
    }

    /** Gets the current orientation of SpinButton.
    * @return value of orientation.
    * @see #orientation
    * @see #DEFAULT_ORIENTATION
    * @see #setOrientation
    */
    public boolean getOrientation() {
        return orientation;
    }

    /** Gets the current orientation of Arrows of SpinButton.
    * @return value of orientation of Arrows.
    * @see #orientation
    * @see #DEFAULT_ORIENTATION
    * @see #setOrientation
    */
    public boolean getArrowsOrientation() {
        return arrowsOrientation;
    }

    /** Sets a minimum of the range of the SpinButton. If value
    * or maximum fall out of acceptable values they are adjusted.
    * @param aMin New minimum.
    * @see #getMinimum
    */
    public void setMinimum(int aMin) {
        minimum = aMin;

        if (maximum < minimum) {
            maximum = minimum;
        }

        if (value < minimum) {
            setValue(value);
        }

        switchStop();
        repaint();
    }

    /** Gets the current minimum of the range of SpinButton.
    * @return Minimum.
    * @see #setMinimum
    */
    public int getMinimum() {
        return minimum;
    }

    /** Sets a maximum of the range of the SpinButton. If value
    *  or minimum fall out of acceptable values they are adjusted.
    * @param aMax New maximum.
    * @see #getMinimum
    */
    public void setMaximum(int aMax) {
        maximum = aMax;

        if (maximum < minimum) {
            minimum = maximum;
        }

        if (value > maximum) {
            setValue(value);
        }

        switchStop();
        repaint();
    }

    /** Gets the current maximum of the range of SpinButton.
    * @return Maximum.
    * @see #setMaximum
    */
    public int getMaximum() {
        return maximum;
    }

    /** Sets a new value of the SpinButton. If value is outside
    *  the ranges it is set to nearest acceptable value.
    * @param aValue New value.
    * @see #getValue
    */
    public void setValue(int aValue) {
        int oldValue = value;

        value = aValue;

        if (!boundsIgnored) {
            if (value < minimum) {
                value = minimum;
            }

            if (value > maximum) {
                value = maximum;
            }
        }

        if (value != oldValue) {
            valueSupport.firePropertyChange("value", new Integer(oldValue), new Integer(value)); // NOI18N
        }

        if ((getValue() == minimum) || (getValue() == maximum) || (oldValue == minimum) || (oldValue == maximum)) {
            repaint();
        }
    }

    /** Gets the current value of the SpinButton.
    * @return Value.
    * @see #setValue
    */
    public int getValue() {
        return value;
    }

    /** Sets a new step of the SpinButton.
    * @param aStep New step.
    * @see #getStep
    */
    public void setStep(int aStep) {
        step = aStep;
        switchStop();
        repaint();
    }

    /** Gets the current step of the SpinButton.
    * @return Step.
    * @see #setStep
    */
    public int getStep() {
        return step;
    }

    /** Sets new value of repeatDelay variable.
    * @param aDelay New delay.
    * @see #repeatDelay
    * @see #getDelay
    */
    public void setDelay(int aDelay) {
        repeatDelay = aDelay;
        switchStop();
        repaint();
    }

    /** Gets the current value of repeatDelay variable.
    * @return Delay.
    * @see #repeatDelay
    * @see #setDelay
    */
    public int getDelay() {
        return repeatDelay;
    }

    /** Sets new value of repeatRate variable.
    * @param aRate New rate.
    * @see #repeatRate
    * @see #getRate
    */
    public void setRate(int aRate) {
        repeatRate = aRate;
        switchStop();
        repaint();
    }

    /** Gets the current value of rate variable.
    * @return Rate.
    * @see #repeatRate
    * @see #setRate
    */
    public int getRate() {
        return repeatRate;
    }

    public boolean isBoundsIgnored() {
        return boundsIgnored;
    }

    public void setBoundsIgnored(boolean ignored) {
        boundsIgnored = ignored;
    }

    public boolean isRepeating() {
        return repeating;
    }

    public void setRepeating(boolean aRepeating) {
        repeating = aRepeating;
    }

    public void paint(Graphics g) {
        Dimension d = getSize();

        int left = 0;
        int top = 0;
        int w = d.width - 1;
        int h = d.height - 1;

        g.setColor(getBackground());
        g.fillRect(left, top, w, h);

        if (orientation) {
            w = w / 2;
            paintBorder(g, left, top, w, h, running && (runningDir == SPIN_DOWN), SPIN_DOWN);
            left += (w + 1);
            w = d.width - 1 - left;
            paintBorder(g, left, top, w, h, running && (runningDir == SPIN_UP), SPIN_UP);
        } else {
            h = h / 2;
            paintBorder(g, left, top, w, h, running && (runningDir == SPIN_UP), SPIN_UP);
            top += (h + 1);
            h = d.height - 1 - top;
            paintBorder(g, left, top, w, h, running && (runningDir == SPIN_DOWN), SPIN_DOWN);
        }
    }

    private void paintBorder(Graphics g, int x, int y, int w, int h, boolean isDown, boolean aDir) {
        g.setColor(Color.black);

        if (!isDown) {
            g.drawLine(x, y + h, x + w, y + h);
            g.drawLine(x + w, y, x + w, y + h);
        } else {
            g.drawLine(x, y, x + w, y);
            g.drawLine(x, y, x, y + h);
            x++;
            y++;
        }

        w--;
        h--;
        g.setColor(SystemColor.controlHighlight);
        g.draw3DRect(x, y, w, h, !isDown);
        paintArrow(g, x, y, w, h, aDir);
    }

    private void paintArrow(Graphics g, int x, int y, int w, int h, boolean aDir) {
        if ((w <= 0) || (h <= 0)) {
            return;
        }

        int wd = w / 4;
        int hd = h / 4;
        int[] xP = new int[3];
        int[] yP = new int[3];

        if (arrowsOrientation) {
            if (aDir == SPIN_UP) {
                xP[0] = x + wd;
                xP[2] = (x + w) - wd;
            } else {
                xP[0] = (x + w) - wd;
                xP[2] = x + wd;
            }

            xP[1] = xP[0];
            yP[0] = y + hd;
            yP[1] = (y + h) - hd;
            yP[2] = y + (h / 2);
        } else {
            if (aDir == SPIN_UP) {
                yP[0] = (y + h) - hd;
                yP[2] = y + hd;
            } else {
                yP[0] = y + hd;
                yP[2] = (y + h) - hd;
            }

            yP[1] = yP[0];
            xP[0] = x + wd;
            xP[1] = (x + w) - wd;
            xP[2] = x + (w / 2);
        }

        if (
            (((aDir == SPIN_UP) && (value >= maximum)) || ((aDir == SPIN_DOWN) && (value <= minimum))) &&
                !boundsIgnored
        ) {
            Color fg = getForeground();
            Color bg = getBackground();
            g.setColor(
                new Color(
                    (fg.getRed() + (2 * bg.getRed())) / 3, (fg.getGreen() + (2 * bg.getGreen())) / 3,
                    (fg.getBlue() + (2 * bg.getBlue())) / 3
                )
            );
        } else {
            g.setColor(getForeground());
        }

        g.fillPolygon(xP, yP, 3);
    }

    protected synchronized void switchRun(boolean aDirect) {
        if (running) {
            rt.finish = true;
        }

        rt = new RepeatThread();
        rt.start();
        runningDir = aDirect;
        running = true;
    }

    public synchronized void switchStop() {
        if (rt == null) {
            return;
        }

        rt.finish = true;
        running = false;
    }

    public Dimension getMinimumSize() {
        return countSize();
    }

    public Dimension getPreferredSize() {
        return countSize();
    }

    private Dimension countSize() {
        int x = 11;
        int y = x;

        if (orientation) {
            x = x + x;
        } else {
            y = y + y;
        }

        return new Dimension(x, y);
    }

    public void addPropertyChangeListener(PropertyChangeListener l) {
        valueSupport.addPropertyChangeListener(l);
    }

    public void removePropertyChangeListener(PropertyChangeListener l) {
        valueSupport.removePropertyChangeListener(l);
    }

    public void addSpinButtonListener(SpinButtonListener spinButtonListener) {
        spinButtonListeners.addElement(spinButtonListener);
    }

    public void removeSpinButtonListener(SpinButtonListener spinButtonListener) {
        spinButtonListeners.removeElement(spinButtonListener);
    }

    public void notifySpinButtonListenersAboutUpMove() {
        int i;
        int k = spinButtonListeners.size();

        for (i = 0; i < k; i++)
            ((SpinButtonListener) spinButtonListeners.elementAt(i)).moveUp();
    }

    public void notifySpinButtonListenersAboutDownMove() {
        int i;
        int k = spinButtonListeners.size();

        for (i = 0; i < k; i++)
            ((SpinButtonListener) spinButtonListeners.elementAt(i)).moveDown();
    }

    protected void repeatThreadNotify() {
        int old_val = getValue();

        if (runningDir) {
            setValue(getValue() + step);

            if (value != old_val) {
                notifySpinButtonListenersAboutUpMove();
            }
        } else {
            setValue(getValue() - step);

            if (value != old_val) {
                notifySpinButtonListenersAboutDownMove();
            }
        }

        if ((getValue() == old_val) && !boundsIgnored) {
            switchStop();
            repaint();
        }
    }

    class RepeatThread extends Thread {
        boolean finish = false;

        public RepeatThread() {
            finish = false;
        }

        public void run() {
            repeatThreadNotify();

            try {
                sleep(repeatDelay);
            } catch (InterruptedException e) {
            }

            if (!repeating) {
                return;
            }

            while (true) {
                if (finish) {
                    break;
                }

                repeatThreadNotify();

                if (finish) {
                    break;
                }

                try {
                    sleep(repeatRate);
                } catch (InterruptedException e) {
                }
            }
        }
    }
}
