/*
 * This file is part of Nuts Framework.
 * Copyright (C) 2009 Nuts Develop Team.
 *
 * Nuts Framework is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License any later version.
 *
 * Nuts Framework is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	 See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Nuts Framework. If not, see <http://www.gnu.org/licenses/>.
 */
package nuts.exts.xwork2.converter.converters;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.text.ParsePosition;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import nuts.core.collections.MessageFormatKey;
import nuts.core.lang.StringUtils;

import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;


/**
 * NumberTypeConverter
 */
public class NumberTypeConverter extends AbstractTypeConverter  {

	private static Log log = LogFactory.getLog(NumberTypeConverter.class);

	/**
	 * NumberFormat cache
	 */
	private static Map<MessageFormatKey, NumberFormat> formatCache = new ConcurrentHashMap<MessageFormatKey, NumberFormat>();

	/**
	 * NUMBER_FORMAT_DEFAULT = "number-format";
	 */
	public static final String NUMBER_FORMAT_DEFAULT = "number-format";

	/**
	 * NUMBER_FORMAT_PREFIX = "number-format-";
	 */
	public static final String NUMBER_FORMAT_PREFIX = "number-format-";

	/**
	 * Converts one or more String values to the specified class.
	 *
	 * @param context the action context
	 * @param values  the String values to be converted, such as those submitted from an HTML form
	 * @param toClass the class to convert to
	 * @return the converted object
	 */
	public Object convertFromString(Map context, String[] values, Class toClass) {
		if (ArrayUtils.isEmpty(values) || StringUtils.isEmpty(values[0])) {
			return null;
		}

		return doConvertToNumber(context, values[0], toClass);
	}

	private Object doConvertToNumber(Map context, String value, Class toType) {
		if (toType == BigDecimal.class) {
			return new BigDecimal(parseNumber(context, value, toType).toString());
		}
		else if (toType == BigInteger.class) {
			return new BigInteger(value);
		}
		else {
			if (!toType.isPrimitive() && (value == null || value.length() == 0)) {
				return null;
			}

			Number number = parseNumber(context, value, toType);
			if (toType == Number.class) {
				return number;
			}
			else {
				return performFallbackConversion(context, number, toType);
			}
		}
	}

	private Number parseNumber(Map context, String value, Class toType) {
		NumberFormat numFormat = NumberFormat.getInstance(getLocale());
		ParsePosition parsePos = new ParsePosition(0);
		numFormat.setGroupingUsed(true);
		Number number = numFormat.parse(value, parsePos);

		if (parsePos.getIndex() != value.length()) {
			String msg = "Unparseable number: \"" + value + "\" at position " + parsePos.getIndex();
			log.warn(msg);
			throw new IllegalArgumentException(msg);
		}
		return number;
	}
	
	/**
	 * Converts the specified object to a String.
	 *
	 * @param context the action context
	 * @param o       the object to be converted
	 * @return the converted String
	 */
	public String convertToString(Map context, Object o) {
		if (o != null && o instanceof Number) {
			NumberFormat numFormat = getDefaultNumberFormat(o);
			if (numFormat != null) {
				String number = numFormat.format(o);
				if (number != null) {
					return number;
				}
			}
		}

		return o == null ? null : o.toString();
	}

	private boolean isIntegerType(Class type) {
		return type.equals(Byte.class)
			|| type.equals(Short.class) 
			|| type.equals(Integer.class) 
			|| type.equals(Long.class)
			|| type.equals(BigInteger.class);
	}

	/**
	 * get the cached NumberFormat
	 * @param pattern pattern string
	 * @param locale locale
	 * @return cached NumberFormat
	 */
	private static NumberFormat getCachedNumberFormat(String pattern, Locale locale) {
		MessageFormatKey key = new MessageFormatKey(pattern, locale);
		return formatCache.get(key);
	}
	
	/**
	 * set the NumberFormat to cache
	 * @param pattern pattern string
	 * @param locale locale
	 * @param dateForamt NumberFormat object
	 */
	private static void setCachedNumberFormat(String pattern, Locale locale, NumberFormat dateForamt) {
		MessageFormatKey key = new MessageFormatKey(pattern, locale);
		formatCache.put(key, dateForamt);
	}
	
	/**
	 * getNumberFormat
	 *
	 * @param format format
	 * @return NumberFormat object
	 */
	private NumberFormat getNumberFormat(String format) {
		String pattern = getText(format == null ? NUMBER_FORMAT_DEFAULT : NUMBER_FORMAT_PREFIX + format, (String)null);
		if (StringUtils.isEmpty(pattern)) {
			return null;
		}
		return getNumberFormat(pattern, getLocale());
	}

	/**
	 * getDefaultNumberFormat
	 *
	 * @param o object
	 * @param format format
	 * @return NumberFormat object
	 */
	private NumberFormat getDefaultNumberFormat(Object o) {
		NumberFormat numFormat = getNumberFormat(null);
//		if (numFormat == null) {
//			numFormat = NumberFormat.getInstance(getLocale());
//			if (isIntegerType(o.getClass())) {
//				numFormat.setParseIntegerOnly(true);
//			}
//			numFormat.setGroupingUsed(true);
//			// to be sure we include all digits after decimal seperator, 
//			// otherwise some of the fractions can be chopped
//			numFormat.setMaximumFractionDigits(99);
//		}
		return numFormat;
	}

	/**
	 * getNumberFormat
	 *
	 * @param pattern pattern
	 * @param locale locale
	 * @return NumberFormat object
	 */
	public static NumberFormat getNumberFormat(String pattern, Locale locale) {
		NumberFormat df = getCachedNumberFormat(pattern, locale);
		if (df == null) {
			try {
				df = new DecimalFormat(pattern, new DecimalFormatSymbols(locale));
			}
			catch (Exception e) {
				throw new IllegalArgumentException("The NumberFormat pattern [" + pattern + "] is invalid.", e);
			}
			setCachedNumberFormat(pattern, locale, df);
		}
		return df;
	}
}
