package org.ajax4jsf.builder.config;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.TreeMap;

import javax.faces.component.StateHolder;
import javax.faces.context.FacesContext;
import javax.naming.ConfigurationException;

import org.apache.commons.beanutils.PropertyUtils;

public class ComponentBaseBean extends JsfBean {

	private static final String[] ignorableComponentProperties = {
								"class",
								"attributes",
								"childCount",
								"children",
								"facets",
								"facetsAndChildren",
								"family",
								"parent",
								"rendererType",
								"rendersChildren",
								"submittedValue",
								"transient"};
	private static final String[] enabledTagProperties = {
			"binding"
			};
	private static final String[] attachedStateProperties = {
			"javax.faces.component.StateHolder",
			"java.util.List",
			"javax.faces.el.MethodBinding",
			"javax.faces.el.ValueBinding",
			"javax.el.MethodExpression",
			"javax.el.ValueExpression",
			"javax.faces.convert.Converter"
		};
	
	
	/**
	 * JSP ( facelets ? ) tag description for component
	 */
	private TagBean _tag;
	private TagHandlerBean _taghandler;
	/**
	 * Descriptions of component JavaBean properties
	 */
	private Map<String,PropertyBean> properties = new TreeMap<String,PropertyBean>();
	/**
	 * Flag for generate component instance class
	 */
	private boolean generate = true;

	/**
	 * @return Returns the tag.
	 */
	public TagBean getTag() {
		return _tag;
	}

	/**
	 * @param tag The tag to set.
	 */
	public void setTag(TagBean tag) {
		_tag = tag;
		tag.setParent(this);
	}

	/**
	 * @return Returns the generate.
	 */
	public boolean isGenerate() {
		return this.generate;
	}

	/**
	 * @param generate The generate to set.
	 */
	public void setGenerate(boolean generate) {
		this.generate = generate;
	}

	/**
	 * @return Returns the tagSupport.
	 */
	public TagHandlerBean getTaghandler() {
		return this._taghandler;
	}

	/**
	 * @param tagSupport The tagSupport to set.
	 */
	public void setTaghandler(TagHandlerBean tagSupport) {
		this._taghandler = tagSupport;
	}

	/**
	 * @return Returns the properties.
	 */
	public Collection<PropertyBean> getProperties() {
		return properties.values();
	}

	/**
	 * @param name
	 * @return true if component have property with given name
	 */
	public boolean containProperty(String name) {
		return properties.containsKey(name);
	}
	
	public PropertyBean getProperty(String name){
	    return properties.get(name);
	}

	/**
	 * Append property to component. If property with name exist, replace it
	 * @param property
	 */
	public void addProperty(PropertyBean property) {
		this.properties.put(property.getName(),property);
		property.setParent(this);
	}

	/**
	 * Subclasses should extend this method to provide specifc checks
	 * 
	 * Check existing and default properties
	 * For properties filled from configuration, attempt to set additional parameters.
	 * If base class have any bean properties, append it to configured
	 * @throws ConfigurationException 
	 */
	public void checkProperties() throws ParsingException {
		try {
			getLog().debug("Parse properties for Component "+getName()+" with superclass "+getSuperclass());
            if (getSuperclass() != null)
            {
    			Class <?> superClass = getLoader().loadClass(getSuperclass());
    
    			new ClassWalkingLogic(superClass)
    				.walk(new ClassVisitor() {
    					public void visit(Class<?> clazz) {
    						checkPropertiesForClass(clazz);
    					}
    				});
            }
		} catch (ClassNotFoundException e) {
			getLog().error("superclass not found for component "+getName(), e);
		}
		if (null != getTag()) {
			try {
				Class superClass = getLoader().loadClass(getTag().getSuperclass());
				PropertyDescriptor[] properties = PropertyUtils.getPropertyDescriptors(superClass);
				// for all properties, add it to component. If property have not abstract getter/setter ,
				// add it with exist = true . If property in list of hidden names, set hidden = true.
				for (int i = 0; i < properties.length; i++) {
					PropertyDescriptor descriptor = properties[i];
					Method writeMethod = descriptor.getWriteMethod();
					if(containProperty(descriptor.getName())){
						if (null != writeMethod && !Modifier.isAbstract(writeMethod.getModifiers()) && Modifier.isPublic(writeMethod.getModifiers()) ) {
							((PropertyBean) this.properties.get(descriptor
									.getName())).setExistintag(true);
						}
					} else if (null != writeMethod && Modifier.isPublic(writeMethod.getModifiers())) {
						if (Arrays.asList(enabledTagProperties).contains(
								descriptor.getName())) {
							Class type = descriptor.getPropertyType();
							getLog().debug("Register tag property  "
									+ descriptor.getName() + " with type name "
									+ type.getCanonicalName() );
							PropertyBean property = new PropertyBean();
							property.setName(descriptor.getName());
							property.setDescription(descriptor
									.getShortDescription());
							property
									.setDisplayname(descriptor.getDisplayName());
							property.setClassname(descriptor.getPropertyType()
									.getCanonicalName());
							property.setExist(true);
							if (!Modifier
									.isAbstract(writeMethod.getModifiers())) {
								property.setExistintag(true);
							}
							addProperty(property);
						}
					}
				}
				} catch (ClassNotFoundException e) {
				// TODO Auto-generated catch block
				getLog().error("superclass not found for tag "+getTag().getName(), e);
			}
			
		}
	}

	
	
	
	/**
	 * @param superClass
	 */
	private void checkPropertiesForClass(Class<?> superClass) {
		getLog().debug("Check properties for class "+superClass.getName());
		// get all property descriptors
		PropertyDescriptor[] properties = PropertyUtils.getPropertyDescriptors(superClass);
		// for all properties, add it to component. If property have not abstract getter/setter ,
		// add it with exist = true . If property in list of hidden names, set hidden = true.
		PropertyBean property;
		for (int i = 0; i < properties.length; i++) {
			PropertyDescriptor descriptor = properties[i];
			if(!containProperty(descriptor.getName())){
				if(isIgnorableProperty(superClass, descriptor.getName())) {
					continue;
				}
				Class<?> type = descriptor.getPropertyType();
				getLog().debug("Register property  "+descriptor.getName()+" with type name "+type.getCanonicalName());
				property = new PropertyBean();
				property.setName(descriptor.getName());
				property.setDescription(descriptor.getShortDescription());
				property.setDisplayname(descriptor.getDisplayName());
				property.setClassname(descriptor.getPropertyType().getCanonicalName());
				property.setExist(true);
				addProperty(property);
			} else {
				// Load and check property.
				getLog().debug("Check  property  "+descriptor.getName());
				property = (PropertyBean) this.properties.get(descriptor.getName());
				if(property.getClassname() == null) {
					property.setClassname(descriptor.getPropertyType().getCanonicalName());						
				} else {
					if(!property.getClassname().equals(descriptor.getPropertyType().getCanonicalName())){
						String message = "Class "+property.getClassname()+" for property "+property.getName()+" not equals with real bean property type: "+descriptor.getPropertyType().getCanonicalName();
						getLog().error(message);
						throw new IllegalArgumentException(message);
					}
				}
				if (property.getDescription() == null) {
					property.setDescription(descriptor.getShortDescription());					
				}
				if(property.getDisplayname() == null){
					property.setDisplayname(descriptor.getDisplayName());
				}
				property.setExist(true);
			}
				Method getter = descriptor.getReadMethod();
				Method setter = descriptor.getWriteMethod();
				// Abstract methods
				if(null != setter && null != getter ){
					if( (Modifier.isAbstract(getter.getModifiers()) && Modifier.isAbstract(setter.getModifiers())) || superClass.isInterface()){
						getLog().debug("Detect as abstract property  "+descriptor.getName());
						property.setExist(false);
					} 
				}
				
				if(null == setter || (! Modifier.isPublic(setter.getModifiers())) ){
					getLog().debug("Detect as hidden property  "+descriptor.getName());
					property.setHidden(true);
				}
				if (isAttachedProperty(property)) {
					property.setAttachedstate(true);
				}
				if (property.isInstanceof("javax.faces.el.MethodBinding") || 
						property.isInstanceof("javax.faces.el.ValueBinding")) {
					property.setElonly(true);
				}
	
		}
	}

	private boolean isIgnorableProperty(Class<?> base, String name) {
		return Arrays.asList(ignorableComponentProperties).contains(name);
	}

	private boolean isAttachedProperty(PropertyBean prop) {
		for (int i = 0; i < attachedStateProperties.length; i++) {
			String clazz = attachedStateProperties[i];
			if(prop.isInstanceof(clazz)) {
				return true;
			}
		}
		return false;
	}

    public boolean isStateHolderRequired() {
        getLog().info("isStateHolderRequired");
        for (PropertyBean propertyBean : getProperties())
        {
            getLog().info("Property " + propertyBean.getName() + "/" + propertyBean.isTransient());
            if (!propertyBean.isTransient())
            {
                return true;
            }
        }
        return false;
    }
    
    public boolean isSuperclassImplementsStateHolder()
    {
        try {
            Class superClass = getLoader().loadClass(getSuperclass());
            return (StateHolder.class.isAssignableFrom(superClass));
        } catch (ClassNotFoundException e) {
            getLog().error("superclass not found for tag "+getTag().getName(), e);
            return false;
        }
    }
    
    public boolean isSuperSaveStateMethodExists()
    {
        try {
            Class superClass = getLoader().loadClass(getSuperclass());
            Class[] signature = {FacesContext.class};
            try {
                Method m = superClass.getMethod("saveState", signature);
                return !Modifier.isAbstract(m.getModifiers());
            } catch (NoSuchMethodException e) {
                return false;
            }
        } catch (ClassNotFoundException e) {
            getLog().error("superclass not found for tag "+getTag().getName(), e);
            return false;
        }
    }
    
    public boolean isSuperRestoreStateMethodExists()
    {
        try {
            Class superClass = getLoader().loadClass(getSuperclass());
            Class[] signature = {FacesContext.class, Object.class};
            try {
                Method m = superClass.getMethod("restoreState", signature);
                return !Modifier.isAbstract(m.getModifiers());
            } catch (NoSuchMethodException e) {
                return false;
            }
        } catch (ClassNotFoundException e) {
            getLog().error("superclass not found for tag "+getTag().getName(), e);
            return false;
        }
    }
    
    public boolean isSuperIsTransientMethodExists()
    {
        try {
            Class superClass = getLoader().loadClass(getSuperclass());
            Class[] signature = new Class[0];
            try {
                Method m = superClass.getMethod("isTransient", signature);
                return !Modifier.isAbstract(m.getModifiers());
            } catch (NoSuchMethodException e) {
                return false;
            }
        } catch (ClassNotFoundException e) {
            getLog().error("superclass not found for tag "+getTag().getName(), e);
            return false;
        }
    }
    
    public boolean isSuperSetTransientMethodExists()
    {
        try {
            Class superClass = getLoader().loadClass(getSuperclass());
            Class[] signature = {boolean.class};
            try {
                Method m = superClass.getMethod("setTransient", signature);
                return !Modifier.isAbstract(m.getModifiers());
            } catch (NoSuchMethodException e) {
                return false;
            }
        } catch (ClassNotFoundException e) {
            getLog().error("superclass not found for tag "+getTag().getName(), e);
            return false;
        }
    }
}
