/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common
 * Development and Distribution License("CDDL") (collectively, 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-gplv2.html
 * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
 * specific language governing permissions and limitations under the
 * License.  When distributing the software, include this License Header
 * Notice in each file and include the License file at
 * nbbuild/licenses/CDDL-GPL-2-CP.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the GPL Version 2 section of the License file that
 * accompanied this code. If applicable, add the following below the
 * License Header, with the fields enclosed by brackets [] replaced by
 * your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * Contributor(s): Alexandre Iline.
 *
 * The Original Software is the Jemmy library.
 * The Initial Developer of the Original Software is Alexandre Iline.
 * All Rights Reserved.
 *
 * If you wish your version of this file to be governed by only the CDDL
 * or only the GPL Version 2, indicate your decision by adding
 * "[Contributor] elects to include this software in this distribution
 * under the [CDDL or GPL Version 2] license." If you do not indicate a
 * single choice of license, a recipient has the option to distribute
 * your version of this file under either the CDDL, the GPL Version 2 or
 * to extend the choice of license to its licensees as provided above.
 * However, if you add GPL Version 2 code and therefore, elected the GPL
 * Version 2 license, then the option applies only if the new code is
 * made subject to such option by the copyright holder.
 *
 *
 *
 * $Id$ $Revision$ $Date$
 *
 */

package org.netbeans.jemmy.operators;

import java.awt.Component;
import java.awt.Container;
import java.text.ParseException;
import java.util.Date;
import java.util.Hashtable;
import java.util.List;

import javax.swing.JComponent;
import javax.swing.JSpinner;
import javax.swing.SpinnerDateModel;
import javax.swing.SpinnerListModel;
import javax.swing.SpinnerModel;
import javax.swing.SpinnerNumberModel;
import javax.swing.SwingConstants;
import javax.swing.event.ChangeListener;
import javax.swing.plaf.SpinnerUI;

import org.netbeans.jemmy.Action;
import org.netbeans.jemmy.ComponentChooser;
import org.netbeans.jemmy.ComponentSearcher;
import org.netbeans.jemmy.JemmyException;
import org.netbeans.jemmy.Outputable;
import org.netbeans.jemmy.TestOut;
import org.netbeans.jemmy.TimeoutExpiredException;
import org.netbeans.jemmy.Timeoutable;
import org.netbeans.jemmy.Timeouts;
import org.netbeans.jemmy.drivers.DriverManager;
import org.netbeans.jemmy.drivers.ScrollDriver;
import org.netbeans.jemmy.drivers.scrolling.ScrollAdjuster;

/**
 * Provides methods to work with <code>javax.swing.JSpinner</code> component <br>
 *
 * @see NumberSpinnerOperator
 * @see ListSpinnerOperator
 * @see DateSpinnerOperator
 *
 * @author Alexandre Iline (alexandre.iline@sun.com)
 */

public class JSpinnerOperator extends JComponentOperator 
    implements Timeoutable, Outputable {

    /**
     * Identifier for a "value" property.
     * @see #getDump
     */
    public static final String VALUE_DPROP = "Value";

    private final static long WHOLE_SCROLL_TIMEOUT = 60000;

    private Timeouts timeouts;
    private TestOut output;

    private ScrollDriver driver;

    private JButtonOperator increaseOperator = null;
    private JButtonOperator decreaseOperator = null;

    /**
     * Constructor.
     * @param b JSpinner component.
     */
    public JSpinnerOperator(JSpinner b) {
	super(b);
	driver = DriverManager.getScrollDriver(getClass());
    }

    /**
     * Constructs a JSpinnerOperator object.
     * @param cont a container
     * @param chooser a component chooser specifying searching criteria.
     * @param index an index between appropriate ones.
     * @throws TimeoutExpiredException
     */
    public JSpinnerOperator(ContainerOperator cont, ComponentChooser chooser, int index) {
	this((JSpinner)cont.
             waitSubComponent(new JSpinnerFinder(chooser),
                              index));
	copyEnvironment(cont);
    }

    /**
     * Constructs a JSpinnerOperator object.
     * @param cont a container
     * @param chooser a component chooser specifying searching criteria.
     * @throws TimeoutExpiredException
     */
    public JSpinnerOperator(ContainerOperator cont, ComponentChooser chooser) {
	this(cont, chooser, 0);
    }

    /**
     * Constructs a JSpinnerOperator object.
     * @param cont The operator for a container containing the sought for button.
     * @param text toString() representation of the current spinner value. 
     * @param index Ordinal component index. The first component has <code>index</code> 0.
     * @throws TimeoutExpiredException
     */
    public JSpinnerOperator(ContainerOperator cont, String text, int index) {
	this((JSpinner)waitComponent(cont, 
                                     new JSpinnerByTextFinder(text, 
                                                              cont.getComparator()),
                                     index));
	copyEnvironment(cont);
    }

    /**
     * Constructs a JSpinnerOperator object.
     * @param cont The operator for a container containing the sought for button.
     * @param text toString() representation of the current spinner value. 
     * @throws TimeoutExpiredException
     */
    public JSpinnerOperator(ContainerOperator cont, String text) {
	this(cont, text, 0);
    }

    /**
     * Constructor.
     * Waits component in container first.
     * Uses cont's timeout and output for waiting and to init operator.
     * @param cont Operator pointing a container to search component in.
     * @param index Ordinal component index.
     * @throws TimeoutExpiredException
     */
    public JSpinnerOperator(ContainerOperator cont, int index) {
	this((JSpinner)waitComponent(cont, 
				    new JSpinnerFinder(), 
				    index));
	copyEnvironment(cont);
    }

    /**
     * Constructor.
     * Waits component in container first.
     * Uses cont's timeout and output for waiting and to init operator.
     * @param cont Operator pointing a container to search component in.
     * @throws TimeoutExpiredException
     */
    public JSpinnerOperator(ContainerOperator cont) {
	this(cont, 0);
    }

    /**
     * Searches JSpinner in container.
     * @param cont Container to search component in.
     * @param chooser org.netbeans.jemmy.ComponentChooser implementation.
     * @param index Ordinal component index.
     * @return JSpinner instance or null if component was not found.
     */
    public static JSpinner findJSpinner(Container cont, ComponentChooser chooser, int index) {
	return((JSpinner)findComponent(cont, new JSpinnerFinder(chooser), index));
    }

    /**
     * Searches 0'th JSpinner in container.
     * @param cont Container to search component in.
     * @param chooser org.netbeans.jemmy.ComponentChooser implementation.
     * @return JSpinner instance or null if component was not found.
     */
    public static JSpinner findJSpinner(Container cont, ComponentChooser chooser) {
	return(findJSpinner(cont, chooser, 0));
    }

    /**
     * Searches JSpinner in container.
     * @param cont Container to search component in.
     * @param index Ordinal component index.
     * @return JSpinner instance or null if component was not found.
     */
    public static JSpinner findJSpinner(Container cont, int index) {
	return(findJSpinner(cont, ComponentSearcher.getTrueChooser(Integer.toString(index) + "'th JSpinner instance"), index));
    }

    /**
     * Searches 0'th JSpinner in container.
     * @param cont Container to search component in.
     * @return JSpinner instance or null if component was not found.
     */
    public static JSpinner findJSpinner(Container cont) {
	return(findJSpinner(cont, 0));
    }

    /**
     * Waits JSpinner in container.
     * @param cont Container to search component in.
     * @param chooser org.netbeans.jemmy.ComponentChooser implementation.
     * @param index Ordinal component index.
     * @return JSpinner instance or null if component was not displayed.
     * @throws TimeoutExpiredException
     */
    public static JSpinner waitJSpinner(Container cont, ComponentChooser chooser, int index)  {
	return((JSpinner)waitComponent(cont, new JSpinnerFinder(chooser), index));
    }

    /**
     * Waits 0'th JSpinner in container.
     * @param cont Container to search component in.
     * @param chooser org.netbeans.jemmy.ComponentChooser implementation.
     * @return JSpinner instance or null if component was not displayed.
     * @throws TimeoutExpiredException
     */
    public static JSpinner waitJSpinner(Container cont, ComponentChooser chooser) {
	return(waitJSpinner(cont, chooser, 0));
    }

    /**
     * Waits JSpinner in container.
     * @param cont Container to search component in.
     * @param index Ordinal component index.
     * @return JSpinner instance or null if component was not displayed.
     * @throws TimeoutExpiredException
     */
    public static JSpinner waitJSpinner(Container cont, int index)  {
	return(waitJSpinner(cont, ComponentSearcher.getTrueChooser(Integer.toString(index) + "'th JSpinner instance"), index));
    }

    /**
     * Waits 0'th JSpinner in container.
     * @param cont Container to search component in.
     * @return JSpinner instance or null if component was not displayed.
     * @throws TimeoutExpiredException
     */
    public static JSpinner waitJSpinner(Container cont) {
	return(waitJSpinner(cont, 0));
    }

    /**
     * Checks operator's model type.
     * @param oper an operator to check model
     * @param modelClass a model class.
     * @throws SpinnerModelException if an operator's model is not an instance of
     * specified class.
     */
    public static void checkModel(JSpinnerOperator oper, Class modelClass) {
        if(!modelClass.isInstance(oper.getModel())) {
            throw(new SpinnerModelException("JSpinner model is not a " + modelClass.getName(),
                                            oper.getSource()));
        }
    }

    static {
	Timeouts.initDefault("JSpinnerOperator.WholeScrollTimeout", WHOLE_SCROLL_TIMEOUT);
    }

    public void setOutput(TestOut out) {
	output = out;
	super.setOutput(output.createErrorOutput());
    }

    public TestOut getOutput() {
	return(output);
    }

    public void setTimeouts(Timeouts timeouts) {
	this.timeouts = timeouts;
	super.setTimeouts(timeouts);
    }

    public Timeouts getTimeouts() {
	return(timeouts);
    }

    /**
     * Returns an instance of <code>NumberSpinnerOperator</code> operator,
     * the operator used for <code>JSpinner</code> having <code>SpinnerNumberModel</code> model.
     * @return a <code>NumberSpinnerOperator</code> created for the same <code>JSpinner</code> as this operator.
     * @throws SpinnerModelException if an operator's model is not an instance of <code>SpinnerNumberModel</code>
     */
    public NumberSpinnerOperator getNumberSpinner() {
        return(new NumberSpinnerOperator(this));
    }

    /**
     * Returns an instance of <code>ListSpinnerOperator</code> operator,
     * the operator used for <code>JSpinner</code> having <code>SpinnerListModel</code> model.
     * @return a <code>ListSpinnerOperator</code> created for the same <code>JSpinner</code> as this operator.
     * @throws SpinnerModelException if an operator's model is not an instance of <code>SpinnerListModel</code>
     */
    public ListSpinnerOperator getListSpinner() {
        return(new ListSpinnerOperator(this));
    }

    /**
     * Returns an instance of <code>DateSpinnerOperator</code> operator,
     * the operator used for <code>JSpinner</code> having <code>SpinnerDateModel</code> model.
     * @return a <code>DateSpinnerOperator</code> created for the same <code>JSpinner</code> as this operator.
     * @throws SpinnerModelException if an operator's model is not an instance of <code>SpinnerDateModel</code>
     */
    public DateSpinnerOperator getDateSpinner() {
        return(new DateSpinnerOperator(this));
    }

    /**
     * Scrolls to reach a condition specified by <code>ScrollAdjuster</code>
     * @param adj scrolling criteria.
     */
    public void scrollTo(final ScrollAdjuster adj) {
	produceTimeRestricted(new Action() {
		public Object launch(Object obj) {
		    driver.scroll(JSpinnerOperator.this, adj);
		    return(null);
		}
		public String getDescription() {
		    return("Scrolling");
		}
	    }, getTimeouts().getTimeout("JSpinnerOperator.WholeScrollTimeout"));
    }

    /**
     * Scrolls to maximum value.
     * @throws SpinnerModelException if an operator's model does not have a maximum value.
     */
    public void scrollToMaximum() {
	produceTimeRestricted(new Action() {
		public Object launch(Object obj) {
		    driver.scrollToMaximum(JSpinnerOperator.this, SwingConstants.VERTICAL);
		    return(null);
		}
		public String getDescription() {
		    return("Scrolling");
		}
	    }, getTimeouts().getTimeout("JSpinnerOperator.WholeScrollTimeout"));
    }

    /**
     * Scrolls to minimum value.
     * @throws SpinnerModelException if an operator's model does not have a minimum value.
     */
    public void scrollToMinimum() {
	produceTimeRestricted(new Action() {
		public Object launch(Object obj) {
		    driver.scrollToMinimum(JSpinnerOperator.this, SwingConstants.VERTICAL);
		    return(null);
		}
		public String getDescription() {
		    return("Scrolling");
		}
	    }, getTimeouts().getTimeout("JSpinnerOperator.WholeScrollTimeout"));
    }

    /**
     * Scrolls to exact match of a spinner value to the specified value.
     * @param value an value to scroll to.
     * @param direction a scrolling direction - one of <code>ScrollAdjuster.*_SCROLL_DIRECTION</code> fields.
     */
    public void scrollToObject(Object value, int direction) {
        scrollTo(new ExactScrollAdjuster(this, value, direction));
    }

    /**
     * Scrolls to matching of <code>getValue().toString() with the pattern.
     * @param pattern a pattern to compare with
     * @param comparator a string comparision criteria
     * @param direction a scrolling direction - one of <code>ScrollAdjuster.*_SCROLL_DIRECTION</code> fields.
     */
    public void scrollToString(String pattern, StringComparator comparator, int direction) {
        scrollTo(new ToStringScrollAdjuster(this, pattern, comparator, direction));
    }

    /**
     * Scrolls to matching of <code>getValue().toString()</code> with the pattern.
     * Uses <code>StringComparator</code> assigned to the operator.
     * @param pattern a pattern to compare with
     * @param direction a scrolling direction - one of <code>ScrollAdjuster.*_SCROLL_DIRECTION</code> fields.
     */
    public void scrollToString(String pattern, int direction) {
        scrollToString(pattern, getComparator(), direction);
    }

    /**
     * Returns an operator for a button used for value increasing.
     * @return an operator for a first <code>JButton<code> inside this spinner.
     */
    public JButtonOperator getIncreaseOperator() {
        if(increaseOperator == null) {
            increaseOperator = (JButtonOperator)createSubOperator(new JButtonOperator.JButtonFinder(), 0);
            increaseOperator.copyEnvironment(this);
            increaseOperator.setOutput(getOutput().createErrorOutput());
        }
        return(increaseOperator);
    }

    /**
     * Returns an operator for a button used for value decreasing.
     * @return an operator for a second <code>JButton<code> inside this spinner.
     */
    public JButtonOperator getDecreaseOperator() {
        if(decreaseOperator == null) {
            decreaseOperator = (JButtonOperator)createSubOperator(new JButtonOperator.JButtonFinder(), 1);
            decreaseOperator.copyEnvironment(this);
            decreaseOperator.setOutput(getOutput().createErrorOutput());
        }
        return(decreaseOperator);
    }

    /**
     * Returns a minimal value. Returns null if model is not
     * one of the following: 
     * <code>javax.swing.SpinnerDateModel</code>, 
     * <code>javax.swing.SpinnerListModel</code>, 
     * <code>javax.swing.SpinnerNumberModel</code>.
     * Also, returns null if the model does not have a minimal value.
     * @return a minimal value.
     */
    public Object getMinimum() {
        SpinnerModel model = getModel();
        if       (model instanceof SpinnerNumberModel) {
            return(((SpinnerNumberModel)model).getMinimum());
        } else if(model instanceof SpinnerDateModel) {
            return(((SpinnerDateModel)model).getEnd());
        } else if(model instanceof SpinnerListModel) {
            List list = ((SpinnerListModel)model).getList();
            return(list.get(list.size() - 1));
        } else {
            return(null);
        }
    }

    /**
     * Returns a maximal value. Returns null if model is not
     * one of the following: 
     * <code>javax.swing.SpinnerDateModel</code>, 
     * <code>javax.swing.SpinnerListModel</code>, 
     * <code>javax.swing.SpinnerNumberModel</code>.
     * Also, returns null if the model does not have a maximal value.
     * @return a maximal value.
     */
    public Object getMaximum() {
        SpinnerModel model = getModel();
        if       (model instanceof SpinnerNumberModel) {
            return(((SpinnerNumberModel)model).getMaximum());
        } else if(model instanceof SpinnerDateModel) {
            return(((SpinnerDateModel)model).getEnd());
        } else if(model instanceof SpinnerListModel) {
            List list = ((SpinnerListModel)model).getList();
            return(list.get(list.size() - 1));
        } else {
            return(null);
        }
    }

    public Hashtable getDump() {
	Hashtable result = super.getDump();
	result.put(VALUE_DPROP, ((JSpinner)getSource()).getValue().toString());
	return(result);
    }

    ////////////////////////////////////////////////////////
    //Mapping                                             //

    /**Maps <code>JSpinner.getValue()</code> through queue*/
    public Object getValue() {
	return((Object)runMapping(new MapAction("getValue") {
		public Object map() {
		    return(((JSpinner)getSource()).getValue());
		}}));}

    /**Maps <code>JSpinner.setValue(Object)</code> through queue*/
    public void setValue(final Object object) {
	runMapping(new MapVoidAction("setValue") {
		public void map() {
		    ((JSpinner)getSource()).setValue(object);
		}});}

    /**Maps <code>JSpinner.getUI()</code> through queue*/
    public SpinnerUI getUI() {
	return((SpinnerUI)runMapping(new MapAction("getUI") {
		public Object map() {
		    return(((JSpinner)getSource()).getUI());
		}}));}

    /**Maps <code>JSpinner.setUI(SpinnerUI)</code> through queue*/
    public void setUI(final SpinnerUI spinnerUI) {
	runMapping(new MapVoidAction("setUI") {
		public void map() {
		    ((JSpinner)getSource()).setUI(spinnerUI);
		}});}

    /**Maps <code>JSpinner.setModel(SpinnerModel)</code> through queue*/
    public void setModel(final SpinnerModel spinnerModel) {
	runMapping(new MapVoidAction("setModel") {
		public void map() {
		    ((JSpinner)getSource()).setModel(spinnerModel);
		}});}

    /**Maps <code>JSpinner.getModel()</code> through queue*/
    public SpinnerModel getModel() {
	return((SpinnerModel)runMapping(new MapAction("getModel") {
		public Object map() {
		    return(((JSpinner)getSource()).getModel());
		}}));}

    /**Maps <code>JSpinner.getNextValue()</code> through queue*/
    public Object getNextValue() {
	return((Object)runMapping(new MapAction("getNextValue") {
		public Object map() {
		    return(((JSpinner)getSource()).getNextValue());
		}}));}

    /**Maps <code>JSpinner.addChangeListener(ChangeListener)</code> through queue*/
    public void addChangeListener(final ChangeListener changeListener) {
	runMapping(new MapVoidAction("addChangeListener") {
		public void map() {
		    ((JSpinner)getSource()).addChangeListener(changeListener);
		}});}

    /**Maps <code>JSpinner.removeChangeListener(ChangeListener)</code> through queue*/
    public void removeChangeListener(final ChangeListener changeListener) {
	runMapping(new MapVoidAction("removeChangeListener") {
		public void map() {
		    ((JSpinner)getSource()).removeChangeListener(changeListener);
		}});}

    /**Maps <code>JSpinner.getChangeListeners()</code> through queue*/
    public ChangeListener[] getChangeListeners() {
	return((ChangeListener[])runMapping(new MapAction("getChangeListeners") {
		public Object map() {
		    return(((JSpinner)getSource()).getChangeListeners());
		}}));}

    /**Maps <code>JSpinner.getPreviousValue()</code> through queue*/
    public Object getPreviousValue() {
	return((Object)runMapping(new MapAction("getPreviousValue") {
		public Object map() {
		    return(((JSpinner)getSource()).getPreviousValue());
		}}));}

    /**Maps <code>JSpinner.setEditor(JComponent)</code> through queue*/
    public void setEditor(final JComponent jComponent) {
	runMapping(new MapVoidAction("setEditor") {
		public void map() {
		    ((JSpinner)getSource()).setEditor(jComponent);
		}});}

    /**Maps <code>JSpinner.getEditor()</code> through queue*/
    public JComponent getEditor() {
	return((JComponent)runMapping(new MapAction("getEditor") {
		public Object map() {
		    return(((JSpinner)getSource()).getEditor());
		}}));}

    /**Maps <code>JSpinner.commitEdit()</code> through queue*/
    public void commitEdit() {
	runMapping(new MapVoidAction("commitEdit") {
		public void map() throws ParseException {
		    ((JSpinner)getSource()).commitEdit();
		}});}

    //End of mapping                                      //
    ////////////////////////////////////////////////////////

    /**
     * Allows to find component by text.
     */
    public static class JSpinnerByTextFinder implements ComponentChooser {
	String label;
	StringComparator comparator;

        /**
         * Constructs JSpinnerByTextFinder.
         * @param lb a text pattern
         * @param comparator specifies string comparision algorithm.
         */
	public JSpinnerByTextFinder(String lb, StringComparator comparator) {
	    label = lb;
	    this.comparator = comparator;
	}

        /**
         * Constructs JSpinnerByTextFinder.
         * @param lb a text pattern
         */
	public JSpinnerByTextFinder(String lb) {
            this(lb, Operator.getDefaultStringComparator());
	}

	public boolean checkComponent(Component comp) {
	    if(comp instanceof JSpinner) {
		if(((JSpinner)comp).getValue() != null) {
		    return(comparator.equals(((JSpinner)comp).getValue().toString(),
					     label));
		}
	    }
	    return(false);
	}

	public String getDescription() {
	    return("JSpinner with text \"" + label + "\"");
	}
    }

    /**
     * Checks component type.
     */
    public static class JSpinnerFinder extends Finder {
        /**
         * Constructs JSpinnerFinder.
         * @param sf other searching criteria.
         */
	public JSpinnerFinder(ComponentChooser sf) {
            super(JSpinner.class, sf);
	}
        /**
         * Constructs JSpinnerFinder.
         */
	public JSpinnerFinder() {
            super(JSpinner.class);
	}
    }

    /**
     * A <code>ScrollAdjuster</code> to be used for <code>JSpinner</code>
     * component having <code>SpinnerNumberModel</code> model.
     * @see NumberSpinnerOperator
     */
    public static class NumberScrollAdjuster implements ScrollAdjuster {
        SpinnerNumberModel model;
        double value;
        /**
         * Constructs a <code>NumberScrollAdjuster</code> object.
         * @param oper an operator to work with.
         * @param value a value to scroll to.
         */
	public NumberScrollAdjuster(JSpinnerOperator oper, double value) {
	    this.value = value;
            checkModel(oper, SpinnerNumberModel.class);
            model = (SpinnerNumberModel)oper.getModel();
	}
        /**
         * Constructs a <code>NumberScrollAdjuster</code> object.
         * @param oper an operator to work with.
         * @param value a value to scroll to.
         */
	public NumberScrollAdjuster(JSpinnerOperator oper, Number value) {
	    this(oper, value.doubleValue());
	}
	public int getScrollDirection() {
            if       (value > model.getNumber().doubleValue()) {
                return(ScrollAdjuster.INCREASE_SCROLL_DIRECTION);
            } else if(value < model.getNumber().doubleValue()) {
                return(ScrollAdjuster.DECREASE_SCROLL_DIRECTION);
            } else {
                return(ScrollAdjuster.DO_NOT_TOUCH_SCROLL_DIRECTION);
            }
	}
	public int getScrollOrientation() {
	    return(SwingConstants.VERTICAL);
	}
	public String getDescription() {
	    return("Spin to " + value + " value");
	}
    }

    /**
     * A <code>ScrollAdjuster</code> to be used for <code>JSpinner</code>
     * component having <code>SpinnerListModel</code> model.
     * @see ListSpinnerOperator
     */
    public static class ListScrollAdjuster implements ScrollAdjuster {
        SpinnerListModel model;
        int itemIndex;
        List elements;
	private ListScrollAdjuster(JSpinnerOperator oper) {
            checkModel(oper, SpinnerListModel.class);
            model = (SpinnerListModel)oper.getModel();
            elements = model.getList();
        }
        /**
         * Constructs a <code>ListScrollAdjuster</code> object.
         * @param oper an operator to work with.
         * @param value a value to scroll to.
         */
	public ListScrollAdjuster(JSpinnerOperator oper, Object value) {
            this(oper);
            this.itemIndex = elements.indexOf(value);
	}
        /**
         * Constructs a <code>ListScrollAdjuster</code> object.
         * @param oper an operator to work with.
         * @param itemIndex an item index to scroll to.
         */
	public ListScrollAdjuster(JSpinnerOperator oper, int itemIndex) {
            this(oper);
            this.itemIndex = itemIndex;
        }
	public int getScrollDirection() {
            int curIndex = elements.indexOf(model.getValue());
            if       (itemIndex > curIndex) {
                return(ScrollAdjuster.INCREASE_SCROLL_DIRECTION);
            } else if(itemIndex < curIndex) {
                return(ScrollAdjuster.DECREASE_SCROLL_DIRECTION);
            } else {
                return(ScrollAdjuster.DO_NOT_TOUCH_SCROLL_DIRECTION);
            }
	}
	public int getScrollOrientation() {
	    return(SwingConstants.VERTICAL);
	}
	public String getDescription() {
	    return("Spin to " + Integer.toString(itemIndex) + "'th item");
	}
    }

    /**
     * A <code>ScrollAdjuster</code> to be used for <code>JSpinner</code>
     * component having <code>SpinnerDateModel</code> model.
     * @see DateSpinnerOperator
     */
    public static class DateScrollAdjuster implements ScrollAdjuster {
        Date date;
        SpinnerDateModel model;
        /**
         * Constructs a <code>DateScrollAdjuster</code> object.
         * @param oper an operator to work with.
         * @param date a date to scroll to.
         */
	public DateScrollAdjuster(JSpinnerOperator oper, Date date) {
            this.date = date;
            checkModel(oper, SpinnerDateModel.class);
            model = (SpinnerDateModel)oper.getModel();
        }
	public int getScrollDirection() {
            if       (date.after (model.getDate())) {
                return(ScrollAdjuster.INCREASE_SCROLL_DIRECTION);
            } else if(date.before(model.getDate())) {
                return(ScrollAdjuster.DECREASE_SCROLL_DIRECTION);
            } else {
                return(ScrollAdjuster.DO_NOT_TOUCH_SCROLL_DIRECTION);
            }
	}
	public int getScrollOrientation() {
	    return(SwingConstants.VERTICAL);
	}
	public String getDescription() {
	    return("Spin to " + date.toString() + " date");
	}
    }

    /**
     * Abstract class for a scrolling of a spinner having unknown model type.
     * A subclass needs to override <code>equals(Object)</code> value
     * to specify a criteria of successful scrolling.
     */
    public abstract static class ObjectScrollAdjuster implements ScrollAdjuster {
        SpinnerModel model;
        int direction;
        /**
         * Constructs a <code>ObjectScrollAdjuster</code> object.
         * @param oper an operator to work with.
         * @param direction a scrolling direction - one of <code>ScrollAdjuster.*_SCROLL_DIRECTION</code> fields.
         */
	public ObjectScrollAdjuster(JSpinnerOperator oper, int direction) {
            this.direction = direction;
            model = oper.getModel();
        }
	public int getScrollDirection() {
            if(equals(model.getValue())) {
                return(ScrollAdjuster.DO_NOT_TOUCH_SCROLL_DIRECTION);
            } else if(direction == ScrollAdjuster.INCREASE_SCROLL_DIRECTION &&
                      model.getNextValue() != null ||
                      direction == ScrollAdjuster.DECREASE_SCROLL_DIRECTION &&
                      model.getPreviousValue() != null) {
                return(direction);
            } else {
                return(ScrollAdjuster.DO_NOT_TOUCH_SCROLL_DIRECTION);
            }
	}
        public abstract boolean equals(Object curvalue);
	public int getScrollOrientation() {
	    return(SwingConstants.VERTICAL);
	}
    }

    /**
     * Class for a scrolling of a spinner having unknown model type.
     * Checks spinner value for exact equality with a specified value.
     */
    public static class ExactScrollAdjuster extends ObjectScrollAdjuster {
        Object obj;
        /**
         * Constructs a <code>ExactScrollAdjuster</code> object.
         * @param oper an operator to work with.
         * @param direction a scrolling direction - one of <code>ScrollAdjuster.*_SCROLL_DIRECTION</code> fields.
         */
	public ExactScrollAdjuster(JSpinnerOperator oper, Object obj, int direction) {
            super(oper, direction);
            this.obj = obj;
        }
        public boolean equals(Object curvalue) {
            return(curvalue.equals(obj));
        }
	public String getDescription() {
	    return("Spin to " + obj.toString() + " value");
	}
	public int getScrollOrientation() {
	    return(SwingConstants.VERTICAL);
	}
    }

    /**
     * Class for a scrolling of a spinner having unknown model type.
     * Checks spinner value's toString() reprsentation to match a string pattern.
     */
    public static class ToStringScrollAdjuster extends ObjectScrollAdjuster {
        String pattern;
        StringComparator comparator;
        /**
         * Constructs a <code>ToStringScrollAdjuster</code> object.
         * @param oper an operator to work with.
         * @param pattern a pattern to compare with
         * @param comparator specifies string comparision algorithm.
         * @param direction a scrolling direction - one of <code>ScrollAdjuster.*_SCROLL_DIRECTION</code> fields.
         */
	public ToStringScrollAdjuster(JSpinnerOperator oper, String pattern, StringComparator comparator, int direction) {
            super(oper, direction);
            this.pattern = pattern;
            this.comparator = comparator;
        }
        /**
         * Constructs a <code>ToStringScrollAdjuster</code> object.
         * Uses <code>StringComparator</code> assigned to the operator.
         * @param oper an operator to work with.
         * @param pattern a pattern to compare with
         * @param direction a scrolling direction - one of <code>ScrollAdjuster.*_SCROLL_DIRECTION</code> fields.
         */
	public ToStringScrollAdjuster(JSpinnerOperator oper, String pattern, int direction) {
            this(oper, pattern, oper.getComparator(), direction);
        }
        public boolean equals(Object curvalue) {
            return(comparator.equals(curvalue.toString(), pattern));
        }
	public String getDescription() {
	    return("Spin to \"" + pattern + "\" value");
	}
	public int getScrollOrientation() {
	    return(SwingConstants.VERTICAL);
	}
    }

    /**
     * Provides some specific functionality for <code>JSpinner</code>
     * components having <code>SpinnerNumberModel</code> model.
     * Constructor of this object is private - it cannot be received only from
     * another JSpinnerOperator instance.
     * @see #getNumberSpinner
     */
    public class NumberSpinnerOperator extends JSpinnerOperator {
        private NumberSpinnerOperator(JSpinnerOperator spinner) {
            super((JSpinner)spinner.getSource());
            copyEnvironment(spinner);
            checkModel(this, SpinnerNumberModel.class);
        }
        /**
         * Costs spinner's model to <code>SpinnerNumberModel<code>.
         * @return a spinner model.
         */
        public SpinnerNumberModel getNumberModel() {
            return((SpinnerNumberModel)getModel());
        }
        /**
         * Scrolls to a double value.
         * @param value a value to scroll to.
         */
        public void scrollToValue(double value) {
            scrollTo(new NumberScrollAdjuster(this, value));
        }
        /**
         * Scrolls to a number value.
         * @param value a value to scroll to.
         */
        public void scrollToValue(Number value) {
            scrollTo(new NumberScrollAdjuster(this, value));
        }
    }

    /**
     * Provides some specific functionality for <code>JSpinner</code>
     * components having <code>SpinnerListModel</code> model.
     * Constructor of this object is private - it cannot be received only from
     * another JSpinnerOperator instance.
     * @see #getListSpinner
     */
    public class ListSpinnerOperator extends JSpinnerOperator {
        private ListSpinnerOperator(JSpinnerOperator spinner) {
            super((JSpinner)spinner.getSource());
            copyEnvironment(spinner);
            checkModel(this, SpinnerListModel.class);
        }
        /**
         * Costs spinner's model to <code>SpinnerListModel<code>.
         * @return a spinner model.
         */
        public SpinnerListModel getListModel() {
            return((SpinnerListModel)getModel());
        }
        /**
         * Looks for an index of an item having <code>toString()</code> matching a specified pattern.
         * @param pattern a string pattern
         * @param comparator a string comparision criteria.
         */
        public int findItem(String pattern, StringComparator comparator) {
            List list = getListModel().getList();
            for(int i = 0; i < list.size(); i++) {
                if(comparator.equals(list.get(i).toString(), pattern)) {
                    return(i);
                }
            }
            return(-1);
        }
        /**
         * Looks for an index of an item having <code>toString()</code> matching a specified pattern.
         * Uses a <code>StringComparator</code> assigned to the operator.
         * @param pattern a string pattern
         */
        public int findItem(String pattern) {
            return(findItem(pattern, getComparator()));
        }
        /**
         * Scrolls to an item having specified instance.
         * @param index an index to scroll to.
         */
        public void scrollToIndex(int index) {
            scrollTo(new ListScrollAdjuster(this, index));
        }
        /**
         * Scrolls to <code>getValue().toString()</code> match a specified pattern.
         * @param pattern a string pattern
         * @param comparator a string comparision criteria.
         */
        public void scrollToString(String pattern, StringComparator comparator) {
            int index = findItem(pattern, comparator);
            if(index != -1) {
                scrollToIndex(index);
            } else {
                throw(new JemmyException("No \"" + pattern + "\" item in JSpinner", getSource()));
            }
        }
        /**
         * Scrolls to <code>getValue().toString()</code> match a specified pattern.
         * Uses a <code>StringComparator</code> assigned to the operator.
         * @param pattern a string pattern
         */
        public void scrollToString(String pattern) {
            scrollToString(pattern, getComparator());
        }
    }

    /**
     * Provides some specific functionality for <code>JSpinner</code>
     * components having <code>SpinnerDateModel</code> model.
     * Constructor of this object is private - it cannot be received only from
     * another JSpinnerOperator instance.
     * @see #getDateSpinner
     */
    public class DateSpinnerOperator extends JSpinnerOperator {
        private DateSpinnerOperator(JSpinnerOperator spinner) {
            super((JSpinner)spinner.getSource());
            copyEnvironment(spinner);
            checkModel(this, SpinnerDateModel.class);
        }
        /**
         * Costs spinner's model to <code>SpinnerDateModel<code>.
         * @return a spinner model.
         */
        public SpinnerDateModel getDateModel() {
            return((SpinnerDateModel)getModel());
        }
        /**
         * Scrolls to a date.
         * @param date a date to scroll to.
         */
        public void scrollToDate(Date date) {
            scrollTo(new DateScrollAdjuster(this, date));
        }
    }

    /**
     * Exception is thown whenever spinner model is threated wrong.
     */
    public static class SpinnerModelException extends JemmyException {
        /**
         * Constructs a <code>SpinnerModelException</code> object.
         * @param message error message.
         * @param comp a spinner which model cased the exception.
         */
        public SpinnerModelException(String message, Component comp) {
            super(message, comp);
        }
    }
}
