/* CommonFns.java

	Purpose:

	Description:

	History:
		Wed Apr 20 18:35:21     2005, Created by tomyeh

Copyright (C) 2005 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.xel.fn;

import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Iterator;
import java.util.ListIterator;
import java.util.List;
import java.util.Collection;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import java.lang.reflect.Field;
import java.lang.reflect.Array;
import java.math.BigDecimal;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zkoss.lang.Classes;
import org.zkoss.lang.Objects;
import org.zkoss.mesg.Messages;
import org.zkoss.util.Locales;
import org.zkoss.util.TimeZones;
import org.zkoss.util.resource.Labels;

import org.zkoss.text.DateFormats;
import org.zkoss.text.MessageFormats;

/**
 * Common functions used with EL.
 *
 * @author tomyeh
 */
public class CommonFns {
	private static final Logger log = LoggerFactory.getLogger(CommonFns.class);

	protected CommonFns() {}

	/** Converts the specified object to a boolean.
	 */
	public static boolean toBoolean(Object val) {
		return ((Boolean)Classes.coerce(boolean.class, val)).booleanValue();
	}
	/** Converts the specified object to a string.
	 */
	public static String toString(Object val) {
		return (String)Classes.coerce(String.class, val);
	}
	/** Converts the specified object to a number.
	 */
	public static Number toNumber(Object val) {
		return (Number)Classes.coerce(Number.class, val);
	}
	/** Converts the specified object to an integer.
	 */
	public static int toInt(Object val) {
		return ((Integer)Classes.coerce(int.class, val)).intValue();
	}
	/** Converts the specified object to a (big) decimal.
	 */
	public static BigDecimal toDecimal(Object val) {
		return (BigDecimal)Classes.coerce(BigDecimal.class, val);
	}
	/** Converts the specified object to an character.
	 */
	public static char toChar(Object val) {
		return ((Character)Classes.coerce(char.class, val)).charValue();
	}
	/** Tests whether an object, o, is an instance of a class, c.
	 */
	public static boolean isInstance(Object c, Object o) {
		if (c instanceof Class) {
			return ((Class)c).isInstance(o);
		} else if (c instanceof String) {
			try {
				return Classes.forNameByThread((String)c).isInstance(o);
			} catch (ClassNotFoundException ex) {
				throw new IllegalArgumentException("Class not found: "+c);
			}
		} else {
			throw new IllegalArgumentException("Unknown class: "+c);
		}
	}

	/** Returns the label or message of the specified key.
	 * <ul>
	 * <li>If key is "mesg:class:MMM", Messages.get(class.MMM) is called</li>
	 * <li>Otherwise, {@link Labels#getLabel(String)} is called.
	 * </ul>
	 * @see #getLabel(String, Object[])
	 */
	public static final String getLabel(String key) {
		if (key == null)
			return "";

		if (key.startsWith("mesg:")) {
			final int j = key.lastIndexOf(':');
			if (j > 5) {
				final String clsnm = key.substring(5, j);
				final String fldnm = key.substring(j + 1);
				try {
					final Class cls = Classes.forNameByThread(clsnm);
					final Field fld = cls.getField(fldnm);
					return Messages.get(((Integer) fld.get(null)).intValue());
				} catch (ClassNotFoundException ex) {
					log.warn("Class not found: {}", clsnm, ex);
				} catch (NoSuchFieldException ex) {
					log.warn("Field not found: {}", fldnm, ex);
				} catch (IllegalAccessException ex) {
					log.warn("Field not accessible: {}", fldnm, ex);
				}
			} else if (log.isDebugEnabled()) {
				log.debug("Not a valid format: {}", key);
			}
		}
		return Labels.getLabel(key);
	}
	/** Returns the label of the specified key and formats it
	 * with the specified argument, or null if not found.
	 *
	 * <p>It first uses {@link #getLabel(String)} to load the label.
	 * Then, it, if not null, invokes {@link MessageFormats#format} to format it.
	 *
	 * <p>The current locale is given by {@link org.zkoss.util.Locales#getCurrent}.
	 * @since 3.0.6
	 */
	public static final String getLabel(String key, Object[] args) {
		final String s = getLabel(key);
		return s != null ? MessageFormats.format(s, args, null): null;
	}
	/** Returns the length of an array, string, collection or map.
	 */
	public static final int length(Object o) {
		if (o instanceof String) {
			return ((String)o).length();
		} else if (o == null) {
			return 0;
		} else if (o instanceof Collection) {
			return ((Collection)o).size();
		} else if (o instanceof Map) {
			return ((Map)o).size();
		} else if (o.getClass().isArray()) {
			return Array.getLength(o);
		} else {
			throw new IllegalArgumentException("Unknown object for length: "+o.getClass());
		}
	}

	/** Returns the index of the given element.
	 * @param o the array/collection of objects to examine, or a string.
	 * If o is a map, then {@link Map#keySet} is assumed.
	 * @since 5.0.7
	 */
	public static final int indexOf(Object o, Object element) {
		if (o instanceof String) {
			return element instanceof String ?
				((String)o).indexOf((String)element): -1;
		} else if (o instanceof Collection) {
			int j = 0;
			for (Iterator it = ((Collection)o).iterator(); it.hasNext(); ++j)
				if (Objects.equals(it.next(), element))
					return j;
		} else if (o instanceof Map) {
			return indexOf(((Map)o).keySet(), element);
		} else if (o instanceof Object[]) {
			final Object[] ary = (Object[])o;
			for (int j = 0; j < ary.length; j++)
				if (Objects.equals(ary[j], element))
					return j;
		} else if (o instanceof int[]) {
			if (element instanceof Number) {
				int v = ((Number)element).intValue();
				final int[] ary = (int[])o;
				for (int j = 0; j < ary.length; j++)
					if (ary[j] == v)
						return j;
			}
		} else if (o instanceof long[]) {
			if (element instanceof Number) {
				long v = ((Number)element).longValue();
				final long[] ary = (long[])o;
				for (int j = 0; j < ary.length; j++)
					if (ary[j] == v)
						return j;
			}
		} else if (o instanceof short[]) {
			if (element instanceof Number) {
				short v = ((Number)element).shortValue();
				final short[] ary = (short[])o;
				for (int j = 0; j < ary.length; j++)
					if (ary[j] == v)
						return j;
			}
		} else if (o instanceof byte[]) {
			if (element instanceof Number) {
				byte v = ((Number)element).byteValue();
				final byte[] ary = (byte[])o;
				for (int j = 0; j < ary.length; j++)
					if (ary[j] == v)
						return j;
			}
		} else if (o instanceof double[]) {
			if (element instanceof Number) {
				double v = ((Number)element).doubleValue();
				final double[] ary = (double[])o;
				for (int j = 0; j < ary.length; j++)
					if (Double.compare(ary[j], v) == 0)
						return j;
			}
		} else if (o instanceof float[]) {
			if (element instanceof Number) {
				float v = ((Number)element).floatValue();
				final float[] ary = (float[])o;
				for (int j = 0; j < ary.length; j++)
					if (Float.compare(ary[j], v) == 0)
						return j;
			}
		} else if (o instanceof char[]) {
			char v;
			if (element instanceof Character)
				v = ((Character)element).charValue();
			else if (element instanceof String && ((String)element).length() > 0)
				v = ((String)element).charAt(0);
			else
				return -1;

			final char[] ary = (char[])o;
			for (int j = 0; j < ary.length; j++)
				if (ary[j] == v)
					return j;
		} else if (o != null) {
			throw new IllegalArgumentException("Unknown object for indexOf: "+o.getClass());
		}
		return -1;
	}
	/** Returns the last index of the given element.
	 * @param o the array/list of objects to examine, or a string.
	 * @since 5.0.7
	 */
	public static final int lastIndexOf(Object o, Object element) {
		if (o instanceof String) {
			return element instanceof String ?
				((String)o).lastIndexOf((String)element): -1;
		} else if (o instanceof List) {
			int j = ((List)o).size();
			for (ListIterator it = ((List)o).listIterator(j); it.hasPrevious(); j--)
				if (Objects.equals(it.previous(), element))
					return j - 1;
		} else if (o instanceof Object[]) {
			final Object[] ary = (Object[])o;
			for (int j = ary.length; --j >= 0;)
				if (Objects.equals(ary[j], element))
					return j;
		} else if (o instanceof int[]) {
			if (element instanceof Number) {
				int v = ((Number)element).intValue();
				final int[] ary = (int[])o;
				for (int j = ary.length; --j >= 0;)
					if (ary[j] == v)
						return j;
			}
		} else if (o instanceof long[]) {
			if (element instanceof Number) {
				long v = ((Number)element).longValue();
				final long[] ary = (long[])o;
				for (int j = ary.length; --j >= 0;)
					if (ary[j] == v)
						return j;
			}
		} else if (o instanceof short[]) {
			if (element instanceof Number) {
				short v = ((Number)element).shortValue();
				final short[] ary = (short[])o;
				for (int j = ary.length; --j >= 0;)
					if (ary[j] == v)
						return j;
			}
		} else if (o instanceof byte[]) {
			if (element instanceof Number) {
				byte v = ((Number)element).byteValue();
				final byte[] ary = (byte[])o;
				for (int j = ary.length; --j >= 0;)
					if (ary[j] == v)
						return j;
			}
		} else if (o instanceof double[]) {
			if (element instanceof Number) {
				double v = ((Number)element).doubleValue();
				final double[] ary = (double[])o;
				for (int j = ary.length; --j >= 0;)
					if (Double.compare(ary[j], v) == 0)
						return j;
			}
		} else if (o instanceof float[]) {
			if (element instanceof Number) {
				float v = ((Number)element).floatValue();
				final float[] ary = (float[])o;
				for (int j = ary.length; --j >= 0;)
					if (Float.compare(ary[j], v) == 0)
						return j;
			}
		} else if (o instanceof char[]) {
			char v;
			if (element instanceof Character)
				v = ((Character)element).charValue();
			else if (element instanceof String && ((String)element).length() > 0)
				v = ((String)element).charAt(0);
			else
				return -1;

			final char[] ary = (char[])o;
			for (int j = ary.length; --j >= 0;)
				if (ary[j] == v)
					return j;
		} else if (o != null) {
			throw new IllegalArgumentException("Unknown object for indexOf: "+o.getClass());
		}
		return -1;
	}

	/** Instantiates the specified class.
	 */
	public static final Object new_(Object o) throws Exception {
		if (o instanceof String) {
			return Classes.newInstanceByThread((String)o);
		} else if (o instanceof Class) {
			return ((Class)o).newInstance();
		} else {
			throw new IllegalArgumentException("Unknow object for new: "+o);
		}
	}
	/** Instantiates the specified class, and argument.
	 * @param o the class name or class
	 * @param arg the argument
	 * @since 5.0.5
	 */
	public static final Object new_(Object o, Object arg) throws Exception {
		if (o instanceof String) {
			return Classes.newInstance(Classes.forNameByThread((String)o), new Object[] {arg});
		} else if (o instanceof Class) {
			return Classes.newInstance((Class)o, new Object[] {arg});
		} else {
			throw new IllegalArgumentException("Unknow object for new: "+o);
		}
	}
	/** Instantiates the specified class, and two arguments.
	 * @param o the class name or class
	 * @param arg1 the first argument
	 * @param arg2 the second argument
	 * @since 5.0.5
	 */
	public static final Object new_(Object o, Object arg1, Object arg2) throws Exception {
		if (o instanceof String) {
			return Classes.newInstance(Classes.forNameByThread((String)o), new Object[] {arg1, arg2});
		} else if (o instanceof Class) {
			return Classes.newInstance((Class)o, new Object[] {arg1, arg2});
		} else {
			throw new IllegalArgumentException("Unknow object for new: "+o);
		}
	}
	/** Instantiates the specified class, and two arguments.
	 * @param o the class name or class
	 * @param arg1 the first argument
	 * @param arg2 the second argument
	 * @since 5.0.5
	 */
	public static final Object new_(Object o, Object arg1, Object arg2, Object arg3) throws Exception {
		if (o instanceof String) {
			return Classes.newInstance(Classes.forNameByThread((String)o), new Object[] {arg1, arg2, arg3});
		} else if (o instanceof Class) {
			return Classes.newInstance((Class)o, new Object[] {arg1, arg2, arg3});
		} else {
			throw new IllegalArgumentException("Unknow object for new: "+o);
		}
	}

	/**
	 * Formats a Date into a date/time string.
	 * @param date the time value to be formatted into a time string.
	 * @param pattern the pattern describing the date and time format
	 * @return the formatted time string.
	 * @since 6.0.0
	 */
	public static final String formatDate(Date date, String pattern) {
		return formatDate(date, pattern, null, null, null, null);
	}
	/**
	 * Parses text from the beginning of the given string to produce a date.
	 * The method may not use the entire text of the given string.
	 * @param source A <code>String</code> whose beginning should be parsed.
	 * @param pattern the pattern describing the date and time format
	 * @return A <code>Date</code> parsed from the string.
	 * @throws Exception
	 * @since 6.0.0
	 */
	public static final Date parseDate(String source, String pattern) throws Exception {
		return parseDate(source, pattern, null, null, null, null);
	}

	/**
	 * 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.
	 * @param value The number to format.
	 * @param format The pattern to apply, if it is null,
	 * the system's default format is used.
	 * @return the formatted number string.
	 * @since 6.0.1
	 */
	public static final String formatNumber(Object value, String format) {
		return formatNumber(value, format, null);
	}
	/**
	 * Parses text from the beginning of the given string to produce a number.
	 * The method may not use the entire text of the given string.
	 * @param source A <code>String</code> whose beginning should be parsed.
	 * @param pattern the pattern describing the date and time format
	 * @return A <code>Number</code> parsed from the string.
	 * @throws Exception
	 * @since 6.0.1
	 */
	public static final Number parseNumber (String source, String pattern) throws Exception {
		return parseNumber(source, pattern, null);
	}

	/**
	 * Formats a Date into a date/time string.
	 * @param date the time value to be formatted into a time string.
	 * @param pattern the pattern describing the date and time format
	 * @param locale The Locale to apply, if it is null,
	 * The current locale given by {@link org.zkoss.util.Locales#getCurrent} is used.
	 * @param timezone the time zone to apply, if it is null,
	 * The current timezone given by {@link org.zkoss.util.TimeZones#getCurrent} is used.
	 * @param dateStyle styling index of date.
	 * @param timeStyle styling index of time.
	 * @return the formatted time string.
	 * @since 6.0.0
	 */
	public static final String formatDate(Date date, String pattern, Locale locale, TimeZone timezone, String dateStyle, String timeStyle) {
		return getDateFormat(pattern, locale, timezone, dateStyle, timeStyle).format(date);
	}
	/**
	 * Parses text from the beginning of the given string to produce a date.
	 * The method may not use the entire text of the given string.
	 * @param source A <code>String</code> whose beginning should be parsed.
	 * @param pattern the pattern describing the date and time format
	 * @param locale The Locale to apply, if it is null,
	 * The current locale given by {@link org.zkoss.util.Locales#getCurrent} is used.
	 * @param timezone the time zone to apply, if it is null,
	 * The current timezone given by {@link org.zkoss.util.TimeZones#getCurrent} is used.
	 * @param dateStyle styling index of date.
	 * @param timeStyle styling index of time.
	 * @return A <code>Date</code> parsed from the string.
	 * @throws Exception
	 * @since 6.0.0
	 */
	public static final Date parseDate(String source, String pattern, Locale locale, TimeZone timezone, String dateStyle, String timeStyle) throws Exception {
		return getDateFormat(pattern, locale, timezone, dateStyle, timeStyle).parse(source);
	}
	/**
	 * 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.
	 * @param number The number to format.
	 * @param pattern The pattern to apply, if it is null,
	 * the system's default format is used.
	 * @param locale The Locale to apply, if it is null,
	 * The current locale given by {@link org.zkoss.util.Locales#getCurrent} is used.
	 * @return String The formatted number string.
	 * @since 6.0.1
	 */
	public static final String formatNumber (Object number, String pattern, Locale locale) {
		if (number == null)
			return "";
		return getDecimalFormat(pattern, locale).format(number);
	}
	/**
	 * Parses text from the beginning of the given string to produce a number.
	 * The method may not use the entire text of the given string.
	 * @param source A <code>String</code> whose beginning should be parsed.
	 * @param pattern the pattern describing the date and time format
	 * @param locale The Locale to apply, if it is null,
	 * The current locale given by {@link org.zkoss.util.Locales#getCurrent} is used.
	 * @return A <code>Number</code> parsed from the string.
	 * @throws Exception
	 * @since 6.0.1
	 */
	public static final Number parseNumber (String source, String pattern, Locale locale) throws Exception {
		return getDecimalFormat(pattern, locale).parse(source);
	}
	private static final DecimalFormat getDecimalFormat (String pattern, Locale locale) {
		final DecimalFormat df = (DecimalFormat)
			NumberFormat.getInstance(locale != null ? locale : Locales.getCurrent());
		if (pattern != null)
			df.applyPattern(pattern);
		return df;
	}
	private static final DateFormat getDateFormat (String pattern, Locale locale, TimeZone timezone, String dateStyle, String timeStyle) {
		if (locale == null)
			locale = Locales.getCurrent();
		if (timezone == null)
			timezone = TimeZones.getCurrent();
		pattern = getRealFormat(pattern, locale, dateStyle, timeStyle);
		final DateFormat df = new SimpleDateFormat(pattern,
				locale);
		df.setTimeZone(timezone);
		return df;
	}
	private static final String getRealFormat (String pattern, Locale locale, String dateStyle, String timeStyle) {
		int ts, ds;
		if (pattern.isEmpty())
			pattern = null;
		ds = toStyle(dateStyle);
		if (ds != -111) {
			ts = toStyle(timeStyle);
			if (ts != -111) {
				return DateFormats.getDateTimeFormat(ds, ts, locale, pattern);
			}
			return DateFormats.getDateFormat(ds, locale, pattern);
		}
		return pattern != null ? pattern : "M/d/yy";
	}
	private static final int toStyle (String style) {
		if (style != null) {
			style = style.trim().toLowerCase(java.util.Locale.ENGLISH);
			return "short".equals(style) ? DateFormat.SHORT
					: "medium".equals(style) ? DateFormat.MEDIUM
					: "long".equals(style) ? DateFormat.LONG
					: "full".equals(style) ? DateFormat.FULL
					: -111; //not found
		}
		return -111;
	}
}