/* Copyright 2000 by Basil Vandegriend.  All rights reserved. */

package com.basilv.core.richui;

import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

import javax.swing.JTextField;

import com.basilv.core.Assert;
import com.basilv.core.IntegerValue;

/**
 * A specialization of JTextField that only allows the entry of whole numbers
 * (non-negative integers). An IntegerValue instance is used as the backing
 * model. While IntegerValue itself can accept non-negative numbers, this field
 * can only accept and handle whole numbers.
 * <P>
 * The backing model is only updated when the focus leaves the textfield. The
 * textfield is updated whenever the model's value changes.
 * <P>
 * Therefore, using setText() directly or editing the textfield without leaving
 * the component will not change the value of the backing model.
 */
public class WholeNumberField extends JTextField
{
	private static final long serialVersionUID = 1L;

	/**
	 * Calculate the maximum number of digits for integers up to and including
	 * the specified maximum value.
	 * 
	 * @param maxValue  Must be > 0.
	 */
	private static int calculateMaxDigitsFromMaxValue(int maxValue) {
		final double base10 = 10.0;
		int maxDigits = (int) (Math.log(maxValue) / Math.log(base10)) + 1;
		return maxDigits;
	}

	private IntegerValue integerValue = null;

	private boolean columnsBasedOnMax = true;

	/**
	 * Create an instance with a minimum value of 0 and a default value of 0 and
	 * with a maximum value of Integer.MAX_VALUE
	 */
	public WholeNumberField() {
		this(new IntegerValue());
	}

	/**
	 * Main constructor.
	 * 
	 * @param value  The value model to use for the field. Cannot be null. It's
	 *        minimum value must always be >= 0.
	 */
	public WholeNumberField(IntegerValue value) {
		Assert.notNull("value", value);
		Assert.notNegative("value's minimum", value.getMinimumValue());

		integerValue = value;

		// Use temporary value for maxDigits for document: this will be
		// corrected
		// in the call to handleModelMaxValueChange().
		final int tempMaxDigitsValue = 10;
		setDocument(new WholeNumberDocument(tempMaxDigitsValue));

		PropertyChangeListener listener = new PropertyChangeListener() {
			public void propertyChange(PropertyChangeEvent event) {
				if (IntegerValue.CURRENT_VALUE_PROPERTY.equals(event.getPropertyName())) {
					handleModelValueChange();
				} else if (IntegerValue.MAX_VALUE_PROPERTY.equals(event.getPropertyName())) {
					handleModelMaxValueChange();
				} else if (IntegerValue.MIN_VALUE_PROPERTY.equals(event.getPropertyName())) {
					Assert.notNegative("integerValue's minimum", integerValue.getMinimumValue());
				}
			}
		};
		integerValue.addPropertyChangeListener(IntegerValue.CURRENT_VALUE_PROPERTY, listener);
		integerValue.addPropertyChangeListener(IntegerValue.MAX_VALUE_PROPERTY, listener);
		integerValue.addPropertyChangeListener(IntegerValue.MIN_VALUE_PROPERTY, listener);

		// The order of these two calls is important: the maximum digits
		// allowed in the field's document must be correctly initialized
		// before setting the text to the value of the model.
		handleModelMaxValueChange();
		handleModelValueChange();

		addFocusListener(new FocusListener() {
			public void focusGained(FocusEvent event) {
				// Highlight (select) the text in the field
				selectAll();
			}

			public void focusLost(FocusEvent event) {
				// This fires a property change if appropriate
				integerValue.setValueEnforcingMinAndMax(getIntValueOfField());

				// If the value is changed by the model to the same value as
				// before
				// no event will be fired so we manually update the display.
				handleModelValueChange();
			}
		});
	}

	/**
	 * Return the IntegerValue instance that is the backing model for this
	 * field. Will not be null.
	 */
	public IntegerValue getValueModel() {
		return integerValue;
	}

	/**
	 * Set whether the number of columns of the textfield should be updated
	 * based on the maximum value of the IntegerValue. If false, the number of
	 * columns can be controlled manually.
	 */
	public void setColumnsBasedOnMax(boolean newValue) {
		columnsBasedOnMax = newValue;
	}

	/**
	 * Return an int representation of the contents of the field. If the field
	 * contents are equivalent to null, return the old valid contents.
	 */
	public int getIntValueOfField() {
		String fieldContents = getText();
		if (fieldContents == null) {
			return integerValue.getValue();
		}
		try {
			int intValue = Integer.parseInt(fieldContents);
			return intValue;
		} catch (NumberFormatException e) {
			return integerValue.getValue();
		}
	}

	private void handleModelValueChange() {
		int newValue = integerValue.getValue();
		String newText = Integer.toString(newValue);
		setText(newText);
	}

	private void handleModelMaxValueChange() {
		int maxDigits = calculateMaxDigitsFromMaxValue(integerValue.getMaximumValue());
		if (columnsBasedOnMax) {
			setColumns(maxDigits);
		}
		WholeNumberDocument document = (WholeNumberDocument) getDocument();
		document.setMaxDigits(maxDigits);
	}

}
