package com.nextjet.web.util;

import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import org.apache.commons.beanutils.PropertyUtils;
import com.nextjet.util.date.TZDate;
import com.nextjet.web.struts.NJActionErrors;

public class BeanMapper {

	private HashMap propertyMappingsMap = null;
	private PropertyMapping[] propertyMappings = null;
	private HashMap invalidFieldValues = new HashMap();
	private Object bean = null;
	private NJActionErrors errors = new NJActionErrors();

	public BeanMapper() {
		this(null);
	}

	public BeanMapper(PropertyMapping[] aPropertyMappings) {
		setPropertyMappings(aPropertyMappings);
	}

	public BeanMapper(PropertyMapping[] aPropertyMappings, Object aBean) {
		this(aPropertyMappings);
		setBean(aBean);
	}

	public String getValue(String name) {
		if (getEditor(name) != null)
			return getEditorPropertyValue(name);
		else {
			insureValidName(name);
			String errorValue = (String) invalidFieldValues.get(name);
			if (errorValue != null) return errorValue;

			PropertyMapping mapping = (PropertyMapping) propertyMappingsMap.get(name);
			try {
				Object value = PropertyUtils.getProperty(bean, mapping.getPath());
				return value == null?"":ConvertUtils.convert(value);
			} catch (IllegalArgumentException e) {
				//null in path -- return ""
			} catch (NullPointerException e) {
				//property is null -- return ""
			} catch (Throwable e) {
				handleException(mapping, e);
			}
			return "";
		}
	}

	public void setValue(String name, String value) {
		if (getEditor(name) != null)
			setEditorPropertyValue(name, value);
		else {
			insureValidName(name);
			PropertyMapping mapping = (PropertyMapping) propertyMappingsMap.get(name);
			try {
				Class clazz = getPropertyType(mapping.getPath());

				Object oValue = ConvertUtils.convert(value, clazz);

				if (isValueNull(oValue, value) || isTypeSupported(oValue, clazz)) {
					PropertyUtils.setProperty(bean, mapping.getPath(), oValue);
				} else {
					errors.addApplicationErrorMessage("'" + value + "' is not valid");
					invalidFieldValues.put(name, value);
				}
			} catch (Throwable e) {
				handleException(mapping, e);
			}
		}
	}

	public FormPropertyEditor getEditor(String aPropertyName) {
		PropertyMapping mapping = (PropertyMapping) propertyMappingsMap.get(getRootPropertyName(aPropertyName));
		if (mapping == null) throwBadNameException(getRootPropertyName(aPropertyName));
		return mapping.getEditor();
	}

	private void insureValidName(String name) {
		if (propertyMappingsMap.get(name) == null) throwBadNameException(name);
	}

	private void throwBadNameException(String name) {
		throw new IllegalArgumentException(name + " is not a recognized mapped property name for " + getClass().getName());
	}

	private String getRootPropertyName(String aName) {
		int firstDot = aName.indexOf('.');
		if (firstDot != -1)
			return aName.substring(0, firstDot);
		else
			return aName;
	}

	private String getPropertyPathMinusRoot(String aName) {
		int firstDot = aName.indexOf('.');
		if (firstDot != -1)
			return aName.substring(firstDot + 1, aName.length());
		else
			return null;
	}

	private void setEditorPropertyValue(String aName, String aValue) {
		FormPropertyEditor editor = getEditor(aName);
		String subPath = getPropertyPathMinusRoot(aName);
		if (subPath != null)
			try {
				PropertyUtils.setProperty(editor, subPath, aValue);
			} catch (Throwable e) {
				handleException(editor, aName, subPath, e, true);
			}
		else
			com.nextjet.j2eeunit.NotImplementedException.warn();
	}

	private String getEditorPropertyValue(String aName) {
		FormPropertyEditor editor = getEditor(aName);
		String subPath = getPropertyPathMinusRoot(aName);
		if (subPath != null)
			try {
				return (String) PropertyUtils.getProperty(editor, subPath);
			} catch (Throwable e) {
				handleException(editor, aName, subPath, e, true);
			}
		else
			com.nextjet.j2eeunit.NotImplementedException.warn();
		return null;

	}

	private boolean isTypeSupported(Object aOValue, Class aClazz) {
		return (aOValue != null && aOValue.getClass() != String.class) || aClazz == String.class;
	}

	private boolean isValueNull(Object aOValue, String value) {
		return NJWebUtils.isEmpty(value) && aOValue == null;
	}

	private Class getPropertyType(String aPath) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
		PropertyDescriptor pd = PropertyUtils.getPropertyDescriptor(bean, aPath);
		if (pd == null) throw new NoSuchMethodException("cannot find property with path" + aPath);
		Method setter = pd.getWriteMethod();
		if (setter != null)
			return setter.getParameterTypes()[0];
		return pd.getReadMethod().getReturnType();
	}

	public void reset() {
		invalidFieldValues.clear();
		errors = new NJActionErrors();
		clearEditorsErrors();
	}

	private void clearEditorsErrors() {
		for (int i = 0; i < propertyMappings.length; i++) {
			FormPropertyEditor editor = propertyMappings[i].getEditor();
			if (editor != null) editor.reset();
		}
	}

	public Object getBean() {
		setEditorsPropertyToBean();
		return bean;
	}

	public void setBean(Object aBean) {
		bean = aBean;
		setEditorsPropertyFromBean();
	}

	public void setPropertyMappings(PropertyMapping[] aPropertyMappings) {
		propertyMappings = aPropertyMappings != null?aPropertyMappings:new PropertyMapping[0];
		propertyMappingsMap = createPropertyMappingMap(propertyMappings);
	}

	public PropertyMapping[] getPropertyMappings() {
		return propertyMappings;
	}

	private void handleException(PropertyMapping mapping, Throwable e) {
		handleException(bean, mapping.getName(), mapping.getPath(), e, true);
	}

	private void handleException(PropertyMapping mapping, Throwable e, boolean rethrow) {
		handleException(bean, mapping.getName(), mapping.getPath(), e, rethrow);
	}

	private void handleException(Object object, String name, String path, Throwable e, boolean rethrow) {
		String error =
				"BeanMapper.value failed: Bean="
				+ (object != null?object.getClass().getName():"null")
				+ " Name="
				+ name
				+ " Path="
				+ path
				+ " Exception="
				+ "[" + e.getClass().getName() + "]" + e.getMessage();
		Log.getInstance().debug(e, error);
		if (rethrow) throw new IllegalArgumentException(error);
	}

	private HashMap createPropertyMappingMap(PropertyMapping[] mappings) {
		HashMap map = new HashMap();
		for (int i = 0; i < mappings.length; i++)
			map.put(mappings[i].getName(), mappings[i]);
		return map;
	}

	private FormPropertyEditor getEditorForPropertyPath(String aPath) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
		Class clazz = getPropertyType(aPath);
		if (clazz == TZDate.class)
			return new TZDateEditor();
		else
			return null;
	}

	public NJActionErrors getErrors() {
		// Hack to be changed JM 11/28/01
		// Make sure errors from editors are not added multiple times.
		NJActionErrors allErrors = new NJActionErrors();
		allErrors.add(errors);
		addEditorsErrors(allErrors);
		return allErrors;
	}

	public void setErrors(NJActionErrors aErrors) {
		errors = aErrors;
	}

	private void addEditorsErrors(NJActionErrors errors) {
		for (int i = 0; i < propertyMappings.length; i++) {
			FormPropertyEditor editor = propertyMappings[i].getEditor();
			if (editor == null) continue;
			errors.add(editor.getErrors());
		}
	}

	private void setEditorsPropertyToBean() {
		for (int i = 0; i < propertyMappings.length; i++) {
			PropertyMapping mapping = propertyMappings[i];
			if (mapping.getEditor() == null) continue;
			try {
				PropertyUtils.setProperty(bean, mapping.getPath(), mapping.getEditor().getObject());
			} catch (Throwable ex) {
				handleException(mapping, ex);
			}
		}
	}

	private void setEditorsPropertyFromBean() {
		initDefaultEditors();
		for (int i = 0; i < propertyMappings.length; i++) {
			PropertyMapping mapping = propertyMappings[i];
			if (mapping.getEditor() == null) continue;
			try {
				mapping.getEditor().setObject(PropertyUtils.getProperty(bean, mapping.getPath()));
			} catch (Throwable ex) {
				handleException(mapping, ex);
			}
		}
	}

	private void initDefaultEditors() {
		for (int i = 0; i < propertyMappings.length; i++) {
			PropertyMapping mapping = propertyMappings[i];
			if (mapping.getEditor() != null) continue;
			try {
				mapping.setEditor(getEditorForPropertyPath(mapping.getPath()));
			} catch (Throwable e) {
				handleException(mapping, e, false);
			}
		}
	}

}