/* FieldComparator.java

	Purpose:
		
	Description:
		
	History:
		Jan 8, 2009 5:49:21 PM, Created by henrichen

Copyright (C) 2009 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;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.zkoss.lang.Strings;
import org.zkoss.lang.reflect.Fields;
import org.zkoss.util.CollectionsX;
import org.zkoss.zk.ui.UiException;

/**
 * <p>Based on the the given field names to compare the field value of the object 
 * that is passed in {@link #compare} method.</p> 
 * <p>The field names accept compound "a.b.c" expression. It also
 * accept multiple field names that you can give expression in the form 
 * of e.g. "name, age, salary" and this comparator will compare in that sequence.</p>
 * 
 * @author henrichen
 * @since 3.6.0
 */
public class FieldComparator implements Comparator, Serializable {
	private static final long serialVersionUID = 20090120111922L;
	/** The field names collection. */
	private Collection<FieldInfo> _fieldnames;
	/** The cached field name string. */
	private transient String _orderBy;
	/** The original orderBy passed to the constructor. */
	private String _rawOrderBy;
	/** Whether to treat null as the maximum value. */
	private boolean _maxnull;
	private boolean _ascending;

	/** Compares with the fields per the given "ORDER BY" clause.
	 * <p>Note: It assumes null as minimum value.
	 *  If not, use {@link #FieldComparator(String, boolean, boolean)}
	 * instead.</p>
	 * 
	 * @param orderBy the "ORDER BY" clause to be compared upon for the given object in {@link #compare}.
	 * @param ascending whether to sort as ascending (or descending).
	 */
	public FieldComparator(String orderBy, boolean ascending) {
		this(orderBy, ascending, false);
	}

	/** Compares with the fields per the given "ORDER BY" clause.
	 *
	 * @param orderBy the "ORDER BY" clause to be compared upon for the given object in {@link #compare}.
	 * @param ascending whether to sort as ascending (or descending).
	 * @param nullAsMax whether to consider null as the maximum value.
	 * If false, null is considered as the minimum value.
	 */
	public FieldComparator(String orderBy, boolean ascending, boolean nullAsMax) {
		if (Strings.isBlank(orderBy)) {
			throw new UiException("Empty fieldnames: " + orderBy);
		}
		_fieldnames = parseFieldNames(orderBy, ascending);
		_maxnull = nullAsMax;
		_rawOrderBy = orderBy;
		_ascending = ascending;
	}

	public int compare(Object o1, Object o2) {
		try {
			for (FieldInfo fi : _fieldnames) {
				final int res = compare0(o1, o2, fi.fieldname, fi.asc, fi.func);
				if (res != 0) {
					return res;
				}
			}
			return 0;
		} catch (NoSuchMethodException ex) {
			throw UiException.Aide.wrap(ex);
		}
	}

	/** Returns the order-by clause.
	 * Notice that is the parsed result, such as <code>name=category ASC</code>.
	 * For the original format, please use {@link #getRawOrderBy}.
	 */
	public String getOrderBy() {
		if (_orderBy == null) {
			final StringBuffer sb = new StringBuffer(_fieldnames.size() * 16);
			final Iterator<FieldInfo> it = _fieldnames.iterator();
			if (it.hasNext()) {
				appendField(sb, it.next());
			}
			while (it.hasNext()) {
				sb.append(',');
				appendField(sb, it.next());
			}
			_orderBy = sb.toString();
		}
		return _orderBy;
	}

	/** Returns the original order-by clause passed to the constructor.
	 * It is usually the field's name, such as <code>category</code>,
	 * or a concatenation of field names, such as <code>category.name</code>.
	 * <p>Notice that, with the field's name, you could retrieve the value
	 * by use of {@link Fields#getByCompound}.
	 * @since 5.0.6
	 */
	public String getRawOrderBy() {
		return _rawOrderBy;
	}

	/** Returns whether the sorting is ascending.
	 * @since 5.0.6
	 */
	public boolean isAscending() {
		return _ascending;
	}

	private void appendField(StringBuffer sb, FieldInfo fi) {
		if (fi.func != null) {
			sb.append(fi.func).append('(').append(fi.fieldname).append(')');
		} else {
			sb.append(fi.fieldname);
		}
		sb.append(fi.asc ? " ASC" : " DESC");
	}

	@SuppressWarnings("unchecked")
	private int compare0(Object o1, Object o2, String fieldname, boolean asc, String func)
			throws NoSuchMethodException {
		// Bug B50-3183438: Access to bean shall be consistent
		final Object f1 = o1 instanceof Map ? ((Map) o1).get(fieldname)
				: Fields.getByCompound(getCompareObject(o1), fieldname);
		final Object f2 = o2 instanceof Map ? ((Map) o2).get(fieldname)
				: Fields.getByCompound(getCompareObject(o2), fieldname);
		final Object v1 = handleFunction(f1, func);
		final Object v2 = handleFunction(f2, func);

		if (v1 == null)
			return v2 == null ? 0 : (asc == _maxnull) ? 1 : -1;
		if (v2 == null)
			return (asc == _maxnull) ? -1 : 1;
		final int v = ((Comparable) v1).compareTo(v2);
		return asc ? v : -v;
	}

	private Object getCompareObject(Object o) {
		if (o instanceof TreeNode)
			return ((TreeNode) o).getData();
		return o;
	}

	private Object handleFunction(Object c, String func) {
		if ("UPPER".equals(func)) {
			if (c instanceof String)
				return ((String) c).toUpperCase();
			if (c instanceof Character)
				return new Character(Character.toUpperCase(((Character) c).charValue()));
		} else if ("LOWER".equals(func)) {
			if (c instanceof String)
				return ((String) c).toLowerCase(java.util.Locale.ENGLISH);
			if (c instanceof Character)
				return new Character(Character.toLowerCase(((Character) c).charValue()));
		}
		return c;
	}

	private Collection<FieldInfo> parseFieldNames(String fieldnames, boolean ascending) {
		final Collection<String> fields = CollectionsX.parse(new ArrayList<String>(), fieldnames, ',');
		final List<FieldInfo> results = new ArrayList<FieldInfo>(fields.size());
		for (final Iterator<String> it = fields.iterator(); it.hasNext();) {
			final String field = it.next().trim();
			String fieldname;
			String ascstr = "asc";
			//whether a String
			String func = null;
			final String ufn = field.toUpperCase();
			if (ufn.startsWith("UPPER(") || ufn.startsWith("LOWER(")) { //with function
				final int k = field.lastIndexOf(')');
				if (k == 0) {
					throw new UiException("No closing function ')' mark: " + field);
				} else if (k == 6) {
					throw new UiException("No data inside function: " + field);
				} else {
					fieldname = field.substring(6, k);
					if ((k + 1) < field.length()) { //with asc
						ascstr = field.substring(k + 1);
						if (Strings.isBlank(ascstr)) {
							ascstr = "asc";
						}
					}
					func = ufn.substring(0, 5);
				}
			} else {
				final int j = field.indexOf(' ');
				if (j < 0) {
					fieldname = field;
				} else {
					fieldname = field.substring(0, j);
					ascstr = field.substring(j + 1);
				}
			}

			boolean asc;
			if ("asc".equalsIgnoreCase(ascstr)) {
				asc = ascending;
			} else if ("desc".equalsIgnoreCase(ascstr)) {
				asc = !ascending;
			} else {
				throw new UiException("field must be in the form of \"field ASC\" or \"field DESC\":" + ascstr);
			}

			results.add(new FieldInfo(fieldname, asc, func));
		}
		return results;
	}

	private static class FieldInfo implements Serializable {
		private String fieldname;
		private boolean asc;
		private String func;

		public FieldInfo(String fieldname, boolean asc, String func) {
			this.fieldname = fieldname;
			this.asc = asc;
			this.func = func;
		}
	}
}
