/* NumberInputElement.java

	Purpose:
		
	Description:
		
	History:
		Fri May  4 11:39:46     2007, Created by tomyeh

Copyright (C) 2007 Potix Corporation. All Rights Reserved.

{{IS_RIGHT
	This program is distributed under LGPL Version 2.1 in the hope that
	it will be useful, but WITHOUT ANY WARRANTY.
}}IS_RIGHT
*/
package org.zkoss.zul.impl;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

import org.zkoss.json.JSONValue;
import org.zkoss.lang.JVMs;
import org.zkoss.lang.Library;
import org.zkoss.lang.Objects;
import org.zkoss.math.RoundingModes;
import org.zkoss.util.Locales;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.sys.ObjectPropertyAccess;
import org.zkoss.zk.ui.sys.PropertyAccess;

/**
 * A skeletal implementation for number-type input box.
 *
 * @author tomyeh
 */
public abstract class NumberInputElement extends FormatInputElement {
	/** The rounding mode. */
	private int _rounding = BigDecimal.ROUND_HALF_EVEN;
	private Locale _locale;

	/** Sets the rounding mode.
	 * Note: You cannot change the rounding mode unless you are
	 * using Java 6 or later.
	 *
	 * @param mode the rounding mode. Allowed value:
	 * {@link BigDecimal#ROUND_CEILING}, {@link BigDecimal#ROUND_DOWN},
	 * {@link BigDecimal#ROUND_FLOOR}, {@link BigDecimal#ROUND_HALF_DOWN},
	 * {@link BigDecimal#ROUND_HALF_UP}, {@link BigDecimal#ROUND_HALF_EVEN},
	 * {@link BigDecimal#ROUND_UNNECESSARY} and {@link BigDecimal#ROUND_UP}
	 *
	 * @exception UnsupportedOperationException if Java 5 or below
	 */
	public void setRoundingMode(int mode) {
		if (_rounding != mode) {
			if (!JVMs.isJava6())
				throw new UnsupportedOperationException("Java 6 or above is required");
			_rounding = mode;
			smartUpdate("rounding", mode);
		}
	}

	/** Sets the rounding mode by the name.
	 * Note: You cannot change the rounding mode unless you are
	 * using Java 6 or later.
	 *
	 * @param name the rounding mode's name. Allowed value:
	<dl>
	<dt>CEILING</dt>
	<dd>Rounding mode to round towards positive infinity.</dd>
	<dt>DOWN</dt>
	<dd>Rounding mode to round towards zero.</dd>
	<dt>FLOOR</dt>
	<dd>Rounding mode to round towards negative infinity.</dd>
	<dt>HALF_DOWN</dt>
	<dd>Rounding mode to round towards "nearest neighbor" unless both neighbors are equidistant, in which case round down.</dd>
	<dt>HALF_EVEN</dt>
	<dd>Rounding mode to round towards the "nearest neighbor" unless both neighbors are equidistant, in which case, round towards the even neighbor.</dd>
	<dt>HALF_UP</dt>
	<dd>Rounding mode to round towards "nearest neighbor" unless both neighbors are equidistant, in which case round up.</dd>
	<dt>UNNECESSARY</dt>
	<dd>Rounding mode to assert that the requested operation has an exact result, hence no rounding is necessary.</dd>
	<dt>UP</dt>
	<dd>Rounding mode to round away from zero.</dd>
	</dl>
	 * @exception UnsupportedOperationException if Java 5 or below
	 * @see RoundingModes
	 */
	public void setRoundingMode(String name) {
		setRoundingMode(RoundingModes.valueOf(name));
	}

	/** Returns the rounding mode.
	 * <p>Default: {@link BigDecimal#ROUND_HALF_EVEN}.
	 */
	public int getRoundingMode() {
		return _rounding;
	}

	/** Returns the locale associated with this number input element,
	 * or null if {@link Locales#getCurrent} is preferred.
	 * @since 5.0.8
	 */
	public Locale getLocale() {
		return _locale;
	}

	/** Sets the locale used to identify the symbols of this number input element.
	 * <p>Default: null (i.e., {@link Locales#getCurrent}, the current locale
	 * is assumed)
	 * <p> If the format of {@link #getFormat()} is null, the format is assumed from
	 * {@link #getDefaultFormat()}. (since 5.0.9)
	 * @since 5.0.8
	 */
	public void setLocale(Locale locale) {
		if (!Objects.equals(_locale, locale)) {
			_locale = locale;

			if (getFormat() == null)
				setFormat(getDefaultFormat());

			invalidate();
		}
	}

	/** Sets the locale used to identify the symbols of this number input element.
	 * <p>Default: null (i.e., {@link Locales#getCurrent}, the current locale
	 * is assumed)
	 * @since 5.0.8
	 */
	public void setLocale(String locale) {
		setLocale(locale != null && locale.length() > 0 ? Locales.getLocale(locale) : null);
	}

	/** Returns the real symbols according to the current locale.
	 * @since 5.0.8
	 */
	private String getRealSymbols() {
		if (_locale != null || isLocaleFormat()) {
			Locale usedLocale = getDefaultLocale();
			String localeName = usedLocale.toString();
			if (org.zkoss.zk.ui.impl.Utils.markClientInfoPerDesktop(getDesktop(),
					"org.zkoss.zul.impl.NumberInputElement" + localeName)) {
				final DecimalFormatSymbols symbols = new DecimalFormatSymbols(usedLocale);
				Map<String, String> map = new HashMap<String, String>();
				map.put("GROUPING", String.valueOf(symbols.getGroupingSeparator()));
				map.put("DECIMAL", String.valueOf(symbols.getDecimalSeparator()));
				map.put("PERCENT", String.valueOf(symbols.getPercent()));
				map.put("PER_MILL", String.valueOf(symbols.getPerMill()));
				map.put("MINUS", String.valueOf(symbols.getMinusSign()));
				return JSONValue.toJSONString(new Object[] { localeName, map });
			} else
				return JSONValue.toJSONString(new Object[] { localeName, null });
		}
		return null;
	}

	/**
	 * Returns if the "locale:" in {@link #getFormat()} is presented.
	 * @since 9.5.1
	 */
	protected boolean isLocaleFormat() {
		String format = getFormat();
		return format != null && format.startsWith("locale:");
	}

	/** Returns the default locale, either {@link #getLocale} or
	 * {@link Locales#getCurrent} (never null).
	 * It is useful when you wan to get a locale for this input.
	 * @since 5.0.10
	 */
	protected Locale getDefaultLocale() {
		if (isLocaleFormat())
			return Locales.getLocale(getFormat().substring(7), '-');
		return _locale != null ? _locale : Locales.getCurrent();
	}

	//super//
	protected void renderProperties(org.zkoss.zk.ui.sys.ContentRenderer renderer) throws java.io.IOException {
		super.renderProperties(renderer);

		if (_rounding != BigDecimal.ROUND_HALF_EVEN)
			renderer.render("_rounding", _rounding);
		if (_locale != null)
			renderer.render("localizedSymbols", getRealSymbols());
	}

	/**
	 * Return a default format for the number input element when the locale is specified.
	 * <p> Default: <code>##,##0.##</code>, you can overwrite this by specifying
	 * the following setting in zk.xml
	 * <pre>{@code
	 *	<library-property>
	 *		<name>org.zkoss.zul.numberFormat</name>
	 *		<value>##,##0.##</value>
	 *	</library-property>
	 * }</pre>
	 * @since 5.0.9
	 * @see #setLocale(Locale)
	 */
	protected String getDefaultFormat() {
		return Library.getProperty("org.zkoss.zul.numberFormat", "##,##0.##");
	}

	//utilities//
	/** Formats a number (Integer, BigDecimal...) into a string.
	 * If null, an empty string is returned.
	 *
	 * <p>A utility to assist the handling of numeric data.
	 *
	 * @see #toNumberOnly
	 * @param defaultFormat used if {@link #getFormat} returns null.
	 * If defaultFormat and {@link #getFormat} are both null,
	 * the system's default format is used.
	 */
	protected String formatNumber(Object value, String defaultFormat) {
		if (value == null)
			return "";

		final DecimalFormat df = (DecimalFormat) NumberFormat.getInstance(getDefaultLocale());
		if (_rounding != BigDecimal.ROUND_HALF_EVEN)
			df.setRoundingMode(RoundingMode.valueOf(_rounding));

		String fmt = getFormat();
		if (fmt == null)
			fmt = defaultFormat;
		if (fmt != null && !isLocaleFormat()) {
			try {
				df.applyPattern(fmt); //Bug ZK-1518: should try apply pattern first
			} catch (IllegalArgumentException e) {
				if (_locale != null)
					df.applyLocalizedPattern(fmt); //Bug ZK-1227: apply localized pattern for decimalbox
			}
		}
		return df.format(value);
	}

	/** Filters out non digit characters, such comma and whitespace,
	 * from the specified value.
	 * It is designed to let user enter data in more free style.
	 * They may or may not enter data in the specified format.
	 *
	 * @return a two element array. The first element is the string to
	 * parse with, say, Double.parseDouble. The second element is
	 * an integer to indicate how many digits the result shall be scaled.
	 * For example, if the second element is 2. Then, the result shall be
	 * divided with 10 ^ 2.
	 *
	 * @see #formatNumber
	 */
	protected Object[] toNumberOnly(String val) {
		if (val == null)
			return new Object[] { null, null };

		final DecimalFormatSymbols symbols = new DecimalFormatSymbols(getDefaultLocale());
		final char GROUPING = symbols.getGroupingSeparator(), DECIMAL = symbols.getDecimalSeparator(),
				PERCENT = symbols.getPercent(), PER_MILL = symbols.getPerMill(), //1/1000
				//not support yet: INFINITY = symbols.getInfinity(), NAN = symbols.getNaN(),
				MINUS = symbols.getMinusSign();
		final String fmt = getFormat();
		StringBuffer sb = null;
		int divscale = 0; //the second element
		boolean minus = false;
		for (int j = 0, len = val.length(); j < len; ++j) {
			final char cc = val.charAt(j);

			boolean ignore = false;
			//We handle percent and (nnn) specially
			if (cc == PERCENT) {
				divscale += 2;
				ignore = true;
			} else if (cc == PER_MILL) {
				divscale += 3;
				ignore = true;
			} else if (cc == '(') {
				minus = true;
				ignore = true;
			} else if (cc == '+') {
				ignore = true;
			}

			//We don't add if cc shall be ignored (not alphanum but in fmt)
			if (!ignore)
				ignore = (cc < '0' || cc > '9') && cc != DECIMAL && cc != MINUS && cc != '+'
						&& (Character.isWhitespace(cc) || cc == GROUPING || cc == ')'
								|| (fmt != null && fmt.indexOf(cc) >= 0));
			if (ignore) {
				if (sb == null)
					sb = new StringBuffer(len).append(val.substring(0, j));
			} else {
				final char c2 = cc == MINUS ? '-' : cc == DECIMAL ? '.' : cc;
				if (cc != c2) {
					if (sb == null)
						sb = new StringBuffer(len).append(val.substring(0, j));
					sb.append(c2);
				} else if (sb != null) {
					sb.append(c2);
				}
			}
		}
		if (minus) {
			if (sb == null)
				sb = new StringBuffer(val.length() + 1).append(val);
			if (sb.length() > 0) {
				if (sb.charAt(0) == '-') {
					sb.deleteCharAt(0);
				} else {
					sb.insert(0, '-');
				}
			}
		}

		//handle '%'
		if (fmt != null && divscale > 0) {
			l_out: for (int j = 0, k, len = fmt.length(); (k = fmt.indexOf('\'', j)) >= 0;) {
				while (++k < len) {
					final char cc = fmt.charAt(k);
					if (cc == '%')
						divscale -= 2;
					else if (cc == '‰')
						divscale -= 3;
					else if (cc == '\'') {
						++k;
						break;
					}
					if (divscale <= 0) {
						divscale = 0;
						break l_out;
					}
				}
				j = k;
			}
		}

		return new Object[] { (sb != null ? sb.toString() : val), new Integer(divscale) };
	}

	//--ComponentCtrl--//
	private static HashMap<String, PropertyAccess> _properties = new HashMap<String, PropertyAccess>(2);

	static {
		_properties.put("locale", new ObjectPropertyAccess() {
			public void setValue(Component cmp, Object locale) {
				if (locale instanceof String)
					((NumberInputElement) cmp).setLocale((String) locale);
				else if (locale instanceof Locale)
					((NumberInputElement) cmp).setLocale((Locale) locale);
			}

			public String getValue(Component cmp) {
				return ((NumberInputElement) cmp).getLocale().toString();
			}
		});
	}

	public PropertyAccess getPropertyAccess(String prop) {
		PropertyAccess pa = _properties.get(prop);
		if (pa != null)
			return pa;
		return super.getPropertyAccess(prop);
	}
}
