// Taken from http://www.java-forums.org/swt/9902-how-use-swt-spinner.html and modified.

package org.atdl4j.ui.swt.util;

import java.math.BigDecimal;
import java.math.RoundingMode;

import org.apache.log4j.Logger;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTError;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.TypedListener;

public class SWTNullableSpinner extends Composite 
{
	private static final Logger logger = Logger.getLogger( SWTNullableSpinner.class );
	
	static final int BUTTON_WIDTH = 20;
	Text text;
	Button up, down;
	BigDecimal minimum, maximum;
	BigDecimal increment = new BigDecimal( 1 );
	int digits = 0;
	public static BigDecimal MIN_INTEGER_VALUE_AS_BIG_DECIMAL = new BigDecimal( -Integer.MAX_VALUE );
	public static BigDecimal MAX_INTEGER_VALUE_AS_BIG_DECIMAL = new BigDecimal( Integer.MAX_VALUE );
	
	public SWTNullableSpinner(Composite parent, int style) {
		super(parent, style);
		
		/*
		GridData gd = new GridData(GridData.FILL_VERTICAL | GridData.HORIZONTAL_ALIGN_BEGINNING);
		gd.grabExcessHorizontalSpace = false;
		this.setLayoutData(gd);*/
		
		text = new Text(this, SWT.SINGLE | SWT.NONE);    		
		up = new Button(this, SWT.ARROW | SWT.UP);
		down = new Button(this, SWT.ARROW | SWT.DOWN);
				
		text.addListener(SWT.Verify, new Listener() {
			public void handleEvent(Event e) {
				verify(e);
			}
		});

		text.addListener(SWT.Modify, new Listener() {
			public void handleEvent(Event e) {
				stripExtraDecimalPoints();
			}
		});


		text.addListener(SWT.Traverse, new Listener() {
			public void handleEvent(Event e) {
				traverse(e);
			}
		});

		up.addListener(SWT.Selection, new Listener() {
			public void handleEvent(Event e) {
				up();
			}
		});

		down.addListener(SWT.Selection, new Listener() {
			public void handleEvent(Event e) {
				down();
			}
		});

		addListener(SWT.Resize, new Listener() {
			public void handleEvent(Event e) {
				resize();
			}
		});

		addListener(SWT.FocusIn, new Listener() {
			public void handleEvent(Event e) {
				focusIn();
			}
		});

		text.setFont(getFont());
	}

	void verify(Event e) 
	{
		try 
		{
			if (e.text==null||e.text.equals("")) return;
			
			if ( ( e.text.equals( "." ) )	|| ( e.text.endsWith( "." ) ) )
			{
				// -- Let it go, assume user is entering the decimal place portion and just entered the "." part thus far --
			}
			else
			{
				new BigDecimal( e.text ); // validate text as a big decimal
			}
		} 
		catch (NumberFormatException ex) 
		{
			e.doit = false;
		}
	}

	void traverse(Event e) {
		switch (e.detail) {
		case SWT.TRAVERSE_ARROW_PREVIOUS:
			if (e.keyCode == SWT.ARROW_UP) {
				e.doit = true;
				e.detail = SWT.NULL;
				up();
			}
			break;

		case SWT.TRAVERSE_ARROW_NEXT:
			if (e.keyCode == SWT.ARROW_DOWN) {
				e.doit = true;
				e.detail = SWT.NULL;
				down();
			}
			break;
		}
	}
	
	void up() 
	{		
		BigDecimal tempValue = getValue();
		if ( tempValue == null )
		{
			if ( ( getMinimum() != null ) && ( ! MIN_INTEGER_VALUE_AS_BIG_DECIMAL.equals( getMinimum() ) ) )
			{
				setValue( getMinimum() );
			}
			else
			{
				setValue( getIncrement() );
			}
		}
		else 
		{
			increment( getIncrement() );
		}
		
		notifyListeners(SWT.Selection, new Event());
	}

	void down() 
	{
		BigDecimal tempValue = getValue();
		if ( tempValue == null )
		{
			if ( ( getMaximum() != null ) && ( ! MAX_INTEGER_VALUE_AS_BIG_DECIMAL.equals( getMaximum() ) ) )
			{
				setValue( getMaximum() );
			}
			else
			{
				setValue( getIncrement() );
			}
		}
		else 
		{
			decrement( getIncrement() );
		}
		
		notifyListeners(SWT.Selection, new Event());
	}

	void focusIn() {
		text.setFocus();
	}

	public void setFont(Font font) {
		super.setFont(font);
		text.setFont(font);
	}

	public void setValue(BigDecimal aValue) 
	{
		BigDecimal tempValue = aValue;
		
// 12/15/2010 Scott Atwell		if ( ( getMinimum() != null ) && ( tempValue.compareTo( getMinimum() ) < 0 ) )
		if ( ( getMinimum() != null ) && ( tempValue != null ) && ( tempValue.compareTo( getMinimum() ) < 0 ) )
		{
			tempValue = getMinimum();
		}
// 12/15/2010 Scott Atwell		else if ( ( getMaximum() != null ) && ( tempValue.compareTo( getMaximum() ) > 0 ) )
		else if ( ( getMaximum() != null ) && ( tempValue != null ) && ( tempValue.compareTo( getMaximum() ) > 0 ) )
		{
			tempValue = getMaximum();
		} 
			
// 12/15/2010 Scott Atwell		text.setText( tempValue.toPlainString() );
		if ( tempValue != null )
		{
			text.setText( tempValue.toPlainString() );
		}
		else
		{
			text.setText( "" );
		}
		text.selectAll();
		text.setFocus();
	}

	public BigDecimal getValue() 
	{
		if ( ( text.getText()==null ) || ( text.getText().equals("") ) )
		{
			return null;
		}
		else
		{
// note this is what we had to use in SWTSpinnerWidget when getSelection() returned equiv of BigDecimal.unscaledValue()
//	return BigDecimal.valueOf( spinner.getSelection(), spinner.getDigits() );
			BigDecimal tempValue = new BigDecimal( text.getText() );
			BigDecimal tempValueScaled = tempValue.setScale( getDigits(), RoundingMode.HALF_UP );			
			if ( ! tempValue.equals( tempValueScaled ) )
			{
				String tempNewValueString = tempValueScaled.toPlainString();
				logger.debug( "tempValue: " + tempValue + " tempValueScaled: " + tempValueScaled + " tempValueScaled.toPlainString(): " + tempNewValueString );				
				int tempCaretPosition = text.getCaretPosition();
				logger.debug( "getCaretPosition(): " + text.getCaretPosition() );
				// -- Handle user entering ".12" and formatter converting to "0.12" -- avoid result of "0.21") --
				if ( ( tempCaretPosition == 2 ) && 
					  ( text.getText().charAt(0) == '.' ) && 
					  ( tempNewValueString.startsWith( "0." ) ) )
				{
					// -- we're notified at ".1" but formatted value is converting to "0.1" --
					logger.debug("Incrementing CaretPosition (was " + tempCaretPosition + ") to account for formatted string having \".\" converted to \"0.\" automatically." );		
					tempCaretPosition++;
				}
				
				text.setText( tempNewValueString );
				text.setSelection( tempCaretPosition, tempCaretPosition );
			}
			
			return tempValueScaled;
		}
	}
	
	
	void resize() {
		Point pt = computeSize(SWT.DEFAULT, SWT.DEFAULT);
		int textWidth = pt.x - BUTTON_WIDTH;
		int buttonHeight = pt.y / 2;
		text.setBounds(0, 0, textWidth, pt.y);
		up.setBounds(textWidth, -3, BUTTON_WIDTH, buttonHeight);
		down.setBounds(textWidth, pt.y - buttonHeight - 3, BUTTON_WIDTH,
				buttonHeight);
	}
	
	public Point computeSize(int wHint, int hHint, boolean changed) {
		int width = 100;
		int height = 20;
		if (wHint != SWT.DEFAULT) width = wHint;
		if (hHint != SWT.DEFAULT) height = hHint;
		return new Point(width, height);
	}

	public void addSelectionListener(SelectionListener listener) {
		if (listener == null)
			throw new SWTError(SWT.ERROR_NULL_ARGUMENT);
		addListener(SWT.Selection, new TypedListener(listener));
	}

	public void setDigits(int aDigits) {
		digits = aDigits;
	}
	
	public int getDigits() {
		return digits;
	}

	public void increment( BigDecimal aIncrement )
	{
		if ( getValue() != null )
		{
			setValue( getValue().add( aIncrement ) );
		}
		else if ( aIncrement != null )
		{
			setValue( aIncrement );
		}
	}

	public void decrement( BigDecimal aDecrement )
	{
		if ( getValue() != null )
		{
			setValue( getValue().subtract( aDecrement ) );
		}
		else if ( aDecrement != null )
		{
			setValue( aDecrement );
		}
	}

	/**
	 * @return the minimum
	 */
	public BigDecimal getMinimum()
	{
		return this.minimum;
	}

	/**
	 * @param aMinimum the minimum to set
	 */
	public void setMinimum(BigDecimal aMinimum)
	{
		this.minimum = aMinimum;
	}

	/**
	 * @return the maximum
	 */
	public BigDecimal getMaximum()
	{
		return this.maximum;
	}

	/**
	 * @param aMaximum the maximum to set
	 */
	public void setMaximum(BigDecimal aMaximum)
	{
		this.maximum = aMaximum;
	}

	/**
	 * @return the increment
	 */
	public BigDecimal getIncrement()
	{
		return this.increment;
	}

	/**
	 * @param aIncrement the increment to set
	 */
	public void setIncrement(BigDecimal aIncrement)
	{
		this.increment = aIncrement;
	}
	
	public void addListener(Listener listener)
	{
		text.addListener( SWT.Modify, listener );
	}

	public void removeListener(Listener listener)
	{
		text.removeListener( SWT.Modify, listener );
	}
	
	/**
    * Spinners with decimal places use DecimalFormatter to automatically render/re-render the string value.  
    * When entering a decimal value via keyboard within an empty Spinner text field, the displayed value
    * will automatically adjust (eg entering "1.23" in a field with 2 decimal places will render "1.23.00")
    * without the use of this adjustment method.
    *
	 * @return true if extra decimal point characters had to be stripped
	 */
	private boolean stripExtraDecimalPoints()
	{
		String tempText = text.getText();
		if ( tempText.length() > 0 )
		{
			int tempFirstDecimalPoint = tempText.indexOf( '.', 0 );
			if ( tempFirstDecimalPoint >= 0 )
			{
				int tempNextDecimalPoint = tempText.indexOf( '.', (tempFirstDecimalPoint + 1) );
				if ( tempNextDecimalPoint >= 0 )
				{
					logger.debug(" Multiple decimal points...  tempText: " + tempText );					
					boolean tempDecimalPointFound = false;
					StringBuffer tempStringBuffer = new StringBuffer();
					for ( int i=0; i < tempText.length(); i++ )
					{
						if ( tempText.charAt( i ) == '.' )
						{
							if ( ! tempDecimalPointFound )
							{
								tempDecimalPointFound = true;
							}
							else
							{
								// skip/omit the character
								continue;
							}
						}
						
						tempStringBuffer.append( tempText.charAt( i ) );
					}
					
					logger.debug(" text.setText( tempStringBuffer.toString() ): " + tempStringBuffer.toString() );		
					int tempCaretPosition = text.getCaretPosition();
					text.setText( tempStringBuffer.toString() );
					text.setSelection( tempCaretPosition, tempCaretPosition );
					return true;
				}
			}
		}
		
		return false;
	}
}