/**
 * 
 */
package org.richfaces.component;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.faces.FacesException;
import javax.faces.application.Application;
import javax.faces.application.FacesMessage;
import javax.faces.component.EditableValueHolder;
import javax.faces.component.UIComponent;
import javax.faces.component.UIComponentBase;
import javax.faces.component.UIInput;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;
import javax.faces.convert.ConverterException;
import javax.faces.el.EvaluationException;
import javax.faces.el.MethodBinding;
import javax.faces.el.ValueBinding;
import javax.faces.model.ArrayDataModel;
import javax.faces.model.DataModel;
import javax.faces.model.ListDataModel;
import javax.faces.validator.Validator;
import javax.faces.validator.ValidatorException;

import org.ajax4jsf.component.UIDataAdaptor;
import org.ajax4jsf.model.DataComponentState;
import org.ajax4jsf.model.RepeatState;
import org.apache.commons.collections.Predicate;
import org.apache.commons.collections.iterators.EmptyIterator;
import org.apache.commons.collections.iterators.FilterIterator;

/**
 * @author Nick Belaevski
 *         mailto:nbelaevski@exadel.com
 *         created 16.11.2007
 * @since 3.2
 */
public abstract class UIOrderingBaseComponent extends UIDataAdaptor implements EditableValueHolder {

	private boolean localValueSet;

	private List validators = null;
	private MethodBinding validator;

	public static final Predicate isColumn = new Predicate() {
		public boolean evaluate(Object input) {
			return (input instanceof javax.faces.component.UIColumn || input instanceof Column) &&
					((UIComponent) input).isRendered();
		}
	};

	protected boolean isSuitableValue(Object value, Object restoredObject) {
		if (value instanceof Object[]) {
			Object[] objects = (Object[]) value;
			for (int i = 0; i < objects.length; i++) {
				Object object = objects[i];
			
				if (object != null && object.equals(restoredObject)) {
					return true;
				}
			}

			return false;
		} else {
			if (value != null) {
				return ((Collection) value).contains(restoredObject);
			} else {
				return false;
			}
		}
	}
	
	public abstract boolean isOrderControlsVisible();
	public abstract void setOrderControlsVisible(boolean visible);
	
	public abstract boolean isFastOrderControlsVisible();
	public abstract void setFastOrderControlsVisible(boolean visible);

	public Object saveState(FacesContext faces) {
		Object[] state = new Object[4];

		state[0] = super.saveState(faces);
	
		state[1] = saveAttachedState(faces, validators);
		state[2] = saveAttachedState(faces, validator);

		state[3] = localValueSet ? Boolean.TRUE : Boolean.FALSE;

		return state;
	}

	public void restoreState(FacesContext faces, Object object) {
		Object[] state = (Object[]) object;

		super.restoreState(faces, state[0]);

		validators = (List) restoreAttachedState(faces, state[1]);
		validator = (MethodBinding) restoreAttachedState(faces, state[2]);

		localValueSet = ((Boolean) state[3]).booleanValue();
	}

	protected DataComponentState createComponentState() {
		return new RepeatState();
	}

	public Iterator columns() {
		return new FilterIterator(getChildren().iterator(), isColumn);
	}

	protected Iterator dataChildren() {
		if (getChildCount() != 0) {
			return columns();
		} else {
			return EmptyIterator.INSTANCE;
		}
	}

	protected Iterator fixedChildren() {
		Map facets = getFacets();
		if (facets != null) {
			return facets.values().iterator();
		} else {
			return EmptyIterator.INSTANCE;
		}
	}

	//validators
	public MethodBinding getValidator() {
		return validator;
	}

	public void setValidator(MethodBinding validatorBinding) {
		this.validator = validatorBinding;
	}

	public Validator[] getValidators() {

		if (validators == null) {
			return new Validator[0];
		} else {
			return (Validator[]) validators.toArray(new Validator[validators.size()]);
		}
	}

	public void addValidator(Validator validator) {
		if (validator == null) {
			throw new NullPointerException();
		}

		if (validators == null) {
			validators = new ArrayList();
		}

		validators.add(validator);
	}

	public void removeValidator(Validator validator) {
		if (validators != null) {
			validators.remove(validator);
		}
	}
	//validators end
	
	public boolean isLocalValueSet() {
		return localValueSet;
	}

	public void setLocalValueSet(boolean localValueSet) {
		this.localValueSet = localValueSet;
	}

	protected DataModel createDataModel(Object value) {
		DataModel dataModel;
        
        if (value == null) {
            dataModel = new ListDataModel(Collections.EMPTY_LIST);
        } else if (value instanceof List) {
        	dataModel = new ListDataModel((List) value);
        } else if (Object[].class.isAssignableFrom(value.getClass())) {
        	dataModel = new ArrayDataModel((Object[]) value);
        } else {
        	throw new IllegalArgumentException();
        }
        
        return dataModel;
	}


	/**
	 * @exception NullPointerException {@inheritDoc}     
	 */ 
	public void decode(FacesContext context) {

		if (context == null) {
			throw new NullPointerException();
		}

		// Force validity back to "true"
		setValid(true);
		super.decode(context);
	}

	/**
	 * <p>Specialized decode behavior on top of that provided by the
	 * superclass.  In addition to the standard
	 * <code>processDecodes</code> behavior inherited from {@link
	 * UIComponentBase}, calls <code>validate()</code> if the the
	 * <code>immediate</code> property is true; if the component is
	 * invalid afterwards or a <code>RuntimeException</code> is thrown,
	 * calls {@link FacesContext#renderResponse}.  </p>
	 * @exception NullPointerException {@inheritDoc}     
	 */ 
	public void processDecodes(FacesContext context) {

		if (context == null) {
			throw new NullPointerException();
		}

		// Skip processing if our rendered flag is false
		if (!isRendered()) {
			return;
		}

		super.processDecodes(context);

		if (isImmediate()) {
			executeValidate(context);
		}
	}

	/**
	 * Executes validation logic.
	 */
	protected void executeValidate(FacesContext context) {
		try {
			validate(context);
		} catch (RuntimeException e) {
			context.renderResponse();
			throw e;
		}

		if (!isValid()) {
			context.renderResponse();
		}
	}


	public abstract void validate(FacesContext context);
	
	/**
	 * <p>In addition to the standard <code>processValidators</code> behavior
	 * inherited from {@link UIComponentBase}, calls <code>validate()</code>
	 * if the <code>immediate</code> property is false (which is the 
	 * default);  if the component is invalid afterwards, calls
	 * {@link FacesContext#renderResponse}.
	 * If a <code>RuntimeException</code> is thrown during
	 * validation processing, calls {@link FacesContext#renderResponse}
	 * and re-throw the exception.
	 * </p>
	 * @exception NullPointerException {@inheritDoc}    
	 */ 
	public void processValidators(FacesContext context) {

		if (context == null) {
			throw new NullPointerException();
		}

		// Skip processing if our rendered flag is false
		if (!isRendered()) {
			return;
		}

		super.processValidators(context);
		if (!isImmediate()) {
			executeValidate(context);
		}
	}

	/**
	 *
	 * <p>Set the "valid" property according to the below algorithm.</p>
	 *
	 * <ul>
	 *
	 * <li>If the <code>valid</code> property on this component is still
	 *     <code>true</code>, and the <code>required</code> property is also
	 *     true, ensure that the local value is not empty (where "empty" is
	 *     defined as <code>null</code> or a zero-length String.  If the local
	 *     value is empty:
	 *     <ul>
	 *     <li>Enqueue an appropriate error message by calling the
	 *         <code>addMessage()</code> method on the <code>FacesContext</code>
	 *         instance for the current request.</li>
	 *     <li>Set the <code>valid</code> property on this component to
	 *         <code>false</code>.</li>
	 *     </ul></li>
	 * <li>If the <code>valid</code> property on this component is still
	 *     <code>true</code>, and the local value is not empty, call the
	 *     <code>validate()</code> method of each {@link Validator}
	 *     registered for this {@link UIInput}, followed by the method
	 *     pointed at by the <code>validatorBinding</code> property (if any).
	 *     If any of these validators or the method throws a
	 *     {@link ValidatorException}, catch the exception, add
	 *     its message (if any) to the {@link FacesContext}, and set
	 *     the <code>valid</code> property of this component to false.</li>
	 *
	 * </ul>
	 *
	 */
	protected void validateValue(FacesContext context, Object newValue) {
		// If our value is valid, enforce the required property if present
		if (isValid() && isRequired() && isEmpty(newValue)) {
			FacesMessage message =
				MessageFactory.getMessage(context, UIInput.REQUIRED_MESSAGE_ID);
			message.setSeverity(FacesMessage.SEVERITY_ERROR);
			context.addMessage(getClientId(context), message);
			setValid(false);
		}

		// If our value is valid and not empty, call all validators
		if (isValid() && !isEmpty(newValue)) {
			Validator[] validators = getValidators();
			for (int i = 0; i < validators.length; i++) {
				Validator validator = (Validator) validators[i];
				try { 
					validator.validate(context, this, newValue);
				}
				catch (ValidatorException ve) {
					// If the validator throws an exception, we're
					// invalid, and we need to add a message
					setValid(false);
					FacesMessage message = ve.getFacesMessage();
					if (message != null) {
						message.setSeverity(FacesMessage.SEVERITY_ERROR);
						context.addMessage(getClientId(context), message);
					}
				}
			}

			MethodBinding validator = getValidator();
			if (validator != null) {
				try {
					validator.invoke(context,
							new Object[] { context, this, newValue});
				}
				catch (EvaluationException ee) {
					if (ee.getCause() instanceof ValidatorException) {
						ValidatorException ve =
							(ValidatorException) ee.getCause();

						// If the validator throws an exception, we're
						// invalid, and we need to add a message
						setValid(false);
						FacesMessage message = ve.getFacesMessage();
						if (message != null) {
							message.setSeverity(FacesMessage.SEVERITY_ERROR);
							context.addMessage(getClientId(context), message);
						}
					} else {
						// Otherwise, rethrow the EvaluationException
						throw ee;
					}
				}
			}
		}
	}

	/**
	 * <p>In addition to the standard <code>processUpdates</code> behavior
	 * inherited from {@link UIComponentBase}, calls
	 * <code>updateModel()</code>.
	 * If the component is invalid afterwards, calls
	 * {@link FacesContext#renderResponse}.
	 * If a <code>RuntimeException</code> is thrown during
	 * update processing, calls {@link FacesContext#renderResponse}
	 * and re-throw the exception.
	 * </p>
	 * @exception NullPointerException {@inheritDoc}     
	 */ 
	public void processUpdates(FacesContext context) {

		if (context == null) {
			throw new NullPointerException();
		}

		// Skip processing if our rendered flag is false
		if (!isRendered()) {
			return;
		}

		super.processUpdates(context);

		try {
			updateModel(context);
		} catch (RuntimeException e) {
			context.renderResponse();
			throw e;
		}

		if (!isValid()) {
			context.renderResponse();
		}
	}

	public abstract void updateModel(FacesContext context);

	protected interface UpdateModelCommand {
		public void execute(FacesContext context);
	}

	protected interface DataAdder {
		Object getContainer();
		void add(Object object);
	}

	protected void updateModel(FacesContext context, UpdateModelCommand command) {
		try {
			command.execute(context);
		} catch (EvaluationException e) {
			String messageStr = e.getMessage();
			FacesMessage message = null;
			if (null == messageStr) {
				message =
					MessageFactory.getMessage(context, UIInput.CONVERSION_MESSAGE_ID);
			}
			else {
				message = new FacesMessage(messageStr);
			}
			message.setSeverity(FacesMessage.SEVERITY_ERROR);
			context.addMessage(getClientId(context), message);
			setValid(false);
		}
		catch (FacesException e) {
			FacesMessage message =
				MessageFactory.getMessage(context, UIInput.CONVERSION_MESSAGE_ID);
			message.setSeverity(FacesMessage.SEVERITY_ERROR);
			context.addMessage(getClientId(context), message);
			setValid(false);
		} catch (IllegalArgumentException e) {
			FacesMessage message =
				MessageFactory.getMessage(context, UIInput.CONVERSION_MESSAGE_ID);
			message.setSeverity(FacesMessage.SEVERITY_ERROR);
			context.addMessage(getClientId(context), message);
			setValid(false);
		} catch (Exception e) {
			FacesMessage message =
				MessageFactory.getMessage(context, UIInput.CONVERSION_MESSAGE_ID);
			message.setSeverity(FacesMessage.SEVERITY_ERROR);
			context.addMessage(getClientId(context), message);
			setValid(false);
		}
	}
	
	/**
	 * <p>Return <code>true</code> if the new value is different from the
	 * previous value.</p>
	 *
	 * @param previous old value of this component (if any)
	 * @param value new value of this component (if any)
	 */
	protected boolean compareValues(Object previous, Object value) {

		if (previous == null) {
			return (value != null);
		} else if (value == null) {
			return (true);
		} else {
			if (previous instanceof Object[]) {
				return !Arrays.equals((Object[]) previous, (Object[]) value);
			} else {
				return (!(previous.equals(value)));
			}
		}

	}

	protected void addConversionErrorMessage(FacesContext context, 
			ConverterException ce, Object value) {
		FacesMessage message = ce.getFacesMessage();
		if (message == null) {
			message = MessageFactory.getMessage(context,
					UIInput.CONVERSION_MESSAGE_ID);
			if (message.getDetail() == null) {
				message.setDetail(ce.getMessage());
			}
		}

		message.setSeverity(FacesMessage.SEVERITY_ERROR);
		context.addMessage(getClientId(context), message);
	}
	
	protected boolean isEmpty(Object value) {

		if (value == null) {
			return (true);
		} else if ((value instanceof String) &&
				(((String) value).length() < 1)) {
			return (true);
		} else if (value.getClass().isArray()) {
			if (0 == java.lang.reflect.Array.getLength(value)) {
				return (true);
			}
		}
		else if (value instanceof List) {
			if (0 == ((List) value).size()) {
				return (true);
			}
		}
		return (false);
	}

	protected Object createContainer(ArrayList data, Object object) {
		if (object != null) {
			Class objectClass = object.getClass();
			Class componentType = objectClass.getComponentType();
			if (componentType != null) {
				return data.toArray((Object[]) Array.newInstance(componentType, data.size()));
			}
		}
		
		data.trimToSize();
		return data;
	}

	public abstract ItemState getItemState();
	
	public interface ItemState {
		public boolean isSelected();
		public boolean isActive();
	}

	private Converter getConverterForType(FacesContext context, Class type) {
		if (!Object.class.equals(type)) {
			Application application = context.getApplication();
			return application.createConverter(type);
		}

		return null;
	}
	
	public Converter getConverterForValue(FacesContext context) {
		ValueBinding binding = this.getValueBinding("value");
		if (binding != null) {
			Class type = binding.getType(context);
			if (type != null) {
				Class componentType = type.getComponentType();
				if (componentType != null) {
					return getConverterForType(context, componentType);
				} else {
					//support for generics introspection
				}
			}
		}
		
		return null;
	}
	
}
