
package jp.riken.brain.ni.samuraigraph.base;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.text.DecimalFormat;
import java.text.ParseException;

import javax.swing.JFormattedTextField;
import javax.swing.JSpinner;
import javax.swing.JTextField;
import javax.swing.SpinnerNumberModel;

/**
 * An original spinner class.
 */
public class SGSpinner extends JSpinner
	implements SGIConstants, PropertyChangeListener
{

	// Suffix used in spinners.
	private static final String SUFFIX_CM = " "+cm;			// cm
	private static final String SUFFIX_MM = " "+mm;			// mm
	private static final String SUFFIX_PT = " "+pt;			// pt
	private static final String SUFFIX_INCH = " "+inch;		// inch
	private static final String SUFFIX_DEGREE = degree;		// degree

	// Modes of the commitment.
	private static final int MODE_DEFAULT = 0;
	private static final int MODE_ON_STEP = 1;


	// The name of this spinner.
	private String mDescription = "";


	/**
	 * The default constructor.
	 */
	public SGSpinner()
	{
		super();
	}


	/**
	 * The constructor with initialization.
	 */
	public SGSpinner(
		final SpinnerNumberModel model,
		final String unit,
		final int min, final int max )
	{
		super();
		this.initProperties( model, unit, min, max );
	}


	/**
	 * Initialize the spinner with fraction digits.
	 * This method must be called immediately on the creation of this spinner instance.
	 * @param model - Number model of this spinner. Note that the range of this model is modified appropriately.
	 * @param min - minimum fraction digits
	 * @param max - maximum fraction digits
	 * @param unit - A string added to the number in the spinner.
	 */
	public void initProperties(
		final SpinnerNumberModel model,
		final String unit,
		final int min, final int max )
	{
		// check input value
		if( model.getStepSize().doubleValue() < Double.MIN_VALUE )
		{
			throw new IllegalArgumentException("too small step value");
		}

		// round off the range
		final int round = - max - 1;
		final double minNew = SGUtilityNumber.roundOffNumber(
			((Number)model.getMinimum()).doubleValue(), round );
		final double maxNew = SGUtilityNumber.roundOffNumber(
			((Number)model.getMaximum()).doubleValue(), round );
		model.setMinimum( new Double(minNew) );
		model.setMaximum( new Double(maxNew) );


		// set the spinner model
		this.setModel(model);

		// set the suffix of unit
		if( unit!=null )
		{
			final String suffix = getSuffix(unit);
			final DecimalFormat df = this.getDecimalFormat();
			df.setPositiveSuffix(suffix);
			df.setNegativeSuffix(suffix);
		}

		// set the property of the formatted-text-field
		final JFormattedTextField ftf = this.getFormattedTextField();
		ftf.setFocusLostBehavior( JFormattedTextField.PERSIST );
		ftf.setHorizontalAlignment( JTextField.LEFT );
		ftf.addPropertyChangeListener(this);


		// set the fraction digits
		this.setMinimumFractionDigits( min );
		this.setMaximumFractionDigits( max );
	}


	/**
	 * 
	 */
	public void setDescription( final String str )
	{
		if( str==null )
		{
			throw new IllegalArgumentException("str==null");
		}
		this.mDescription = str;
	}


	/**
	 * 
	 */
	public String getDescription()
	{
		return this.mDescription;
	}



	//
	// Component
	//


	/**
	 * 
	 * @return
	 */
	public boolean isEditable()
	{
		return this.getFormattedTextField().isEditable();
	}


	/**
	 * 
	 * @param b
	 */
	public void setEditable( final boolean b )
	{
		this.getFormattedTextField().setEditable(b);
	}


	/**
	 * 
	 * @return
	 */
	public JFormattedTextField getFormattedTextField()
	{
		final JSpinner.DefaultEditor editor
			= (JSpinner.DefaultEditor)this.getEditor();
		final JFormattedTextField ftf = editor.getTextField();
		return ftf;
	}


	/**
	 * 
	 * @return
	 */
	public String getText()
	{
		return this.getFormattedTextField().getText();
	}


	/**
	 * 
	 * @param str
	 */
	public void setText( final String str )
	{
		this.getFormattedTextField().setText(str);
	}




	//
	// Format
	//


	/**
	 * 
	 * @return
	 */
	public DecimalFormat getDecimalFormat()
	{
		final JSpinner.NumberEditor editor
			= (JSpinner.NumberEditor)this.getEditor();
		return editor.getFormat();
	}


	// The argument "unit" is one of the constants defined in SGUtilityNumber.
	// Returned value is a string only used in the spinners.
	private static String getSuffix( final String unit )
	{
		String suffix = null;
		if( unit.equals( SGUtilityNumber.cm ) )
		{
			suffix = SUFFIX_CM;
		}
		else if( unit.equals( SGUtilityNumber.mm ) )
		{
			suffix = SUFFIX_MM;
		}
		else if( unit.equals( SGUtilityNumber.pt ) )
		{
			suffix = SUFFIX_PT;
		}
		else if( unit.equals( SGUtilityNumber.inch ) )
		{
			suffix = SUFFIX_INCH;
		}
		else if( unit.equals( SGUtilityNumber.degree ) )
		{
			suffix = SUFFIX_DEGREE;
		}

		return suffix;
	}


	/**
	 * Returns a string of unit.
	 * @return - the string of unit
	 */
	public String getUnit()
	{
		String suffix = this.getSuffix();
		String unit = null;
		if( suffix.equals( SUFFIX_CM ) )
		{
			unit = SGUtilityNumber.cm;
		}
		else if( suffix.equals( SUFFIX_MM ) )
		{
			unit = SGUtilityNumber.mm;
		}
		else if( suffix.equals( SUFFIX_INCH ) )
		{
			unit = SGUtilityNumber.inch;
		}
		else if( suffix.equals( SUFFIX_PT ) )
		{
			unit = SGUtilityNumber.pt;
		}
		else if( suffix.equals( SUFFIX_DEGREE ) )
		{
			unit = SGUtilityNumber.degree;
		}

		return unit;
	}


	// Returns the suffix.
	private String getSuffix()
	{
		return this.getDecimalFormat().getPositiveSuffix();
	}


	/**
	 * 
	 * @return
	 */
	public int getMinimumFractionDigits()
	{
		return this.getDecimalFormat().getMinimumFractionDigits();
	}


	/**
	 * 
	 * @return
	 */
	public int getMaximumFractionDigits()
	{
		return this.getDecimalFormat().getMaximumFractionDigits();
	}


	/**
	 * 
	 * @param newValue
	 */
	private void setMinimumFractionDigits( final int newValue )
	{
		this.getDecimalFormat().setMinimumFractionDigits(newValue);
	}


	/**
	 * 
	 * @param newValue
	 */
	private void setMaximumFractionDigits( final int newValue )
	{
		this.getDecimalFormat().setMaximumFractionDigits(newValue);
	}


	/**
	 * 
	 * @return
	 */
	private int getDigitForRoundingOut()
	{
		return - this.getMaximumFractionDigits() -1;
	}


	// Returns whether this spinner treat only integral numbers.
	private boolean isInteger()
	{
		final int min = this.getMinimumFractionDigits();
		final int max = this.getMaximumFractionDigits();
		return ( min==0 & max==0 );
	}





	//
	// Number model
	//


	// Only casts the SpinnerModel and returns.
	private SpinnerNumberModel getSpinnerNumberModel()
	{
		return (SpinnerNumberModel)this.getModel();
	}


	/**
	 * 
	 * @return
	 */
	public Number getMinimumValue()
	{
		return (Number)this.getSpinnerNumberModel().getMinimum();
	}


	/**
	 * 
	 * @return
	 */
	public Number getMaximumValue()
	{
		return (Number)this.getSpinnerNumberModel().getMaximum();
	}


	/**
	 * 
	 * @return
	 */
	public Number getStepSize()
	{
		return this.getSpinnerNumberModel().getStepSize();
	}



	/**
	 * 
	 * @param v1
	 * @param v2
	 * @return
	 */
	private Number getValueOnStepInside( final double v1, final double v2 )
	{
		double smaller;
		double larger;
		if( v1 < v2 )
		{
			smaller = v1;
			larger = v2;
		}
		else if( v2 < v1 )
		{
			smaller = v2;
			larger = v1;
		}
		else
		{
			return null;
		}

		final double min = this.getMinimumValue().doubleValue();
		final double max = this.getMaximumValue().doubleValue();
		final double step = this.getStepSize().doubleValue();

		Number num = null;
		int cnt = 0;
		final int digit = this.getDigitForRoundingOut();
		while(true)
		{
			final double valueOld = min + cnt*step;
			final double value = SGUtilityNumber.roundOffNumber( valueOld, digit );
			if( smaller<value )
			{
				if( value<larger )
				{
					num = new Double(value);
					break;
				}
				else
				{
					break;
				}
			}
			cnt++;
		}

		return num;
	}




	/**
	 * An overridden method of JSpinner class.
     * Commits the value to the <code>SpinnerModel</code> after rounding up the value.
	 */
	public void commitEdit() throws ParseException
	{
		this.commit(MODE_ON_STEP);
		super.commitEdit();

		// record the temporary string after commited
		this.mTempString = this.getText();
	}



	/**
	 * Returns whether currently set value is valid.
	 * @return
	 */
	public boolean hasValidValue()
	{
		Number num = this.getNumber();
		if( num==null )
		{
			String str = this.getText();
			if( str.equals("") == false )
			{
				return false;
			}
		}
		
		return true;
	}

	

	private String mTempString = null;

	/**
	 * Clear the temporary String object.
	 *
	 */
	void clearTemporaryValues()
	{
		this.mTempString = null;
	}


	/**
	 * 
	 * @param spinner
	 * @return
	 */
	public Number getNumber()
	{
		try
		{
			this.commitEditByDefault();
		}
		catch( Exception ex )
		{
			return null;
		}
		Object obj = this.getValue();
		return (Number)obj;
	}

	
	
	
	/**
	 * 
	 * @return
	 */
	public void commitEditByDefault() throws ParseException
	{
		this.commit(MODE_DEFAULT);
		super.commitEdit();
	}



	/**
	 * 
	 * @return
	 */
	private boolean commit( final int mode ) throws ParseException
	{
		final JFormattedTextField ftf = this.getFormattedTextField();

		// parse the inputted string
		String strNew = this.parseString( ftf.getText(), mode );
		if( strNew != null )
		{
			// set the parsed string to the formatted text field
			ftf.setText( strNew );

			// commit edited text
			try
			{
				ftf.commitEdit();
			}
			catch( ParseException e )
			{
//				e.printStackTrace();
				throw e;
			}
		}
		else
		{
			throw new ParseException("parse failed",0);
		}

		return true;
	}



	/**
	 * Parse the string in the set mode.
	 * @param input - a string to be parsed
	 * @param mode - mode
	 * @return parsed string
	 */
	private String parseString( final String input, final int mode )
	{
//System.out.println(input);
		if( input==null )
		{
			return null;
		}
		if( input.length()==0 )
		{
			return null;
		}

		final JFormattedTextField ftf = this.getFormattedTextField();
		final String unit = this.getUnit();

		// with no unit
		if( unit==null )
		{
			// parse the input string directly
			Number num = SGUtilityText.getDouble(input);
			if( num==null )
			{
				return null;
			}

			if( this.isInteger() )
			{
				final double fValue = num.doubleValue();
				int value = (int)Math.rint(fValue);
				final int min = this.getMinimumValue().intValue();
				final int max = this.getMaximumValue().intValue();
				if( value<min )
				{
					value = min;
				}
				if( value>max )
				{
					value = max;
				}
				String strNew = Integer.toString(value);
				return strNew;
			}

			return num.toString();
		}


		// parse the input string directly, because only the number can be inputted
		String str = null;
		{
			Number num = null;
			if( this.isInteger() )
			{
				num = SGUtilityText.getInteger( input );
			}
			else
			{
				num = SGUtilityText.getDouble( input );
			}

			// just a number is set
			if( num!=null )
			{
				str = num.toString() + getSuffix(unit);
			}
			else
			{
				str = input;
			}
		}


		// in case of the unit of length
		String strValue = null;
		if( SGUtilityText.isLengthUnit( unit ) )
		{
			// convert the string to the number in the default unit
			strValue = SGUtilityText.convertString( str, unit );
		}
		else
		{
			// remove the unit
			strValue = SGUtilityText.removeSuffix( str, unit );
		}
		if( strValue==null )
		{
			return null;
		}

		// check the range
		if( this.isInteger() )
		{
			int value = Integer.valueOf( strValue ).intValue();
			final int min = this.getMinimumValue().intValue();
			final int max = this.getMaximumValue().intValue();
			if( value<min )
			{
				value = min;
			}
			if( value>max )
			{
				value = max;
			}
			String strNew = Integer.toString(value) + getSuffix(unit);
			return strNew;
		}


		// get the value
		double value = SGUtilityText.getDouble(strValue).doubleValue();
		final double min = this.getMinimumValue().doubleValue();
		final double max = this.getMaximumValue().doubleValue();
//System.out.println(value+"  "+min+"  "+max);
		if( value<min )
		{
			value = min;
		}
		if( value>max )
		{
			value = max;
		}


		// round up the number
		double valueNew = 0.0;
		switch( mode )
		{
			case MODE_DEFAULT :
			{
				valueNew = SGUtilityNumber.roundOffNumber( value, this.getDigitForRoundingOut() );
				break;
			}

			case MODE_ON_STEP :
			{
				valueNew = value;
				break;
			}
			
			default :
			{
				throw new IllegalArgumentException();
			}
		}

		String strNew = new Double( valueNew ).toString() + getSuffix(unit);

		return strNew;
	}



	/**
	 * 
	 */
	public void propertyChange( PropertyChangeEvent e )
	{
		Object source = e.getSource();

		// modify the value after committed
		if( source.equals( this.getFormattedTextField() ) )
		{
			String tmp = this.mTempString;
			if( tmp!=null )
			{
				JFormattedTextField ftf = (JFormattedTextField)source;
				String str = ftf.getText();
				if( tmp.equals( str ) == false )
				{
					String unit = this.getUnit();
					if( this.isInteger() == false )
					{
						String strOld = SGUtilityText.removeSuffix( tmp, unit );
						String strNew = SGUtilityText.removeSuffix( str, unit );
						Double valueOld = Double.valueOf( strOld );
						Double valueNew = Double.valueOf( strNew );

						// get the step value between two values
						Number num = this.getValueOnStepInside(
							valueOld.doubleValue(), valueNew.doubleValue() );
						if( num==null )
						{
							num = valueNew;
						}

//System.out.println(valueOld+"  "+valueNew);

						// set new value to the spinner
						this.setValue( num );
					}

					// throw a property change event
					PropertyChangeEvent p
						= new PropertyChangeEvent( this, e.getPropertyName(), tmp, str );
					PropertyChangeListener[] lArray = this.getPropertyChangeListeners();
					for( int ii=0; ii<lArray.length; ii++ )
					{
						lArray[ii].propertyChange(p);
					}

				}

				// clear the temporary string
				this.mTempString = null;

			}
		}

	}
	
	
}


