/**
 * License Agreement.
 *
 * Rich Faces - Natural Ajax for Java Server Faces (JSF)
 *
 * Copyright (C) 2007 Exadel, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License version 2.1 as published by the Free Software Foundation.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA
 */

package org.ajax4jsf.builder.config;

import java.beans.PropertyDescriptor;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import javax.naming.ConfigurationException;

import org.ajax4jsf.builder.generator.Logger;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.digester.Digester;
import org.apache.commons.digester.ExtendedBaseRules;
import org.apache.tools.ant.BuildException;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.ext.EntityResolver2;

/**
 * Parse builder config file for use with component creation.
 * 
 * @author asmirnov@exadel.com (latest modification by $Author: ishabalov $)
 * @version $Revision: 1.1.2.10 $ $Date: 2007/02/20 20:57:58 $
 * 
 */
public class BuilderConfig implements LoaderHolder {

    /**
         * Resource path to config file schemas and entities
         */
    public static final String COMPONENT_SCHEMA_BASE = "/META-INF/schema/";

    /**
         * Resource path to config file schemas and entities
         */
    public static final String ENTITIES_FOLDER = "entities";

    /**
         * default URI to dtd in classpath
         */
    private static final String COMPONENT_CONFIG_DTD_URI = COMPONENT_SCHEMA_BASE
	    + "component-config.dtd";

    /**
         * root element of configuration file
         */
    private static final String GENERATOR_CONFIG_ROOT_ELEMENT = "components";

    /**
         * PUBLIC Id of configuration schema
         */
    private static final String GENERATOR_CONFIG_PUBLIC_ID = "-//AJAX4JSF//CDK Generator config/EN";

    /**
         * components described in this configuration
         */
    private List<ComponentBean> components = new ArrayList<ComponentBean>();

    /**
         * validators described in this configuration
         */
    private List<ValidatorBean> validators = new ArrayList<ValidatorBean>();

    /**
         * components described in this configuration
         */
    private List<ConverterBean> converters = new ArrayList<ConverterBean>();

    /**
         * renderers described in this configuration
         */
    private List<RendererBean> renderers = new ArrayList<RendererBean>();

    private List<ListenerBean> listeners = new ArrayList<ListenerBean>();

    private List<FunctionBean> functions = new ArrayList<FunctionBean>();
    
    private ClassLoader _loader;

    private Logger _log;

    /**
         * @param project -
         *                current ant project
         */
    public BuilderConfig(ClassLoader loader, Logger log) {
	_loader = loader;
	_log = log;
    }

    /**
         * Parsing builder configuration file
         * 
         * @param configFile
         * @throws ParsingException
         */
    public void parseConfig(final File configFile) throws ParsingException {
	getLog().info(" Parse config file " + configFile.toString());
	Digester digester = new Digester();
	digester.setRules(new ExtendedBaseRules());
	digester.setValidating(false);
	digester.setNamespaceAware(false);
	// try {
	// URL dtd =
        // this.getClass().getClassLoader().getResource(COMPONENT_CONFIG_DTD_URI);
	// new
        // URL("resource:/com/exadel/vcp/builder/config/component-config.dtd");
	getLog()
		.debug("Register config DTD as URI " + COMPONENT_CONFIG_DTD_URI);
	digester.register(GENERATOR_CONFIG_PUBLIC_ID, "file:"+COMPONENT_CONFIG_DTD_URI);
	// } catch (MalformedURLException e) {
	// throw new ParsingException("Malformed URL for internal DTD
        // reference",e);
	// }

	// setup custom entity resolver for handle file-resource path's
	// resolve DTD even it not pointed in file, and, for entities - resolve
        // it in
	// classpath if entity registered in DTD witn path /META-INF/schema
	digester.setEntityResolver(new EntityResolver2() {

	    /*
                 * (non-Javadoc)
                 * 
                 * @see org.xml.sax.EntityResolver#resolveEntity(java.lang.String,
                 *      java.lang.String)
                 */
	    public InputSource resolveEntity(String publicId, String systemId)
		    throws SAXException, IOException {
		getLog().debug(
			"Request for entity with systemId " + systemId
				+ " and publicId " + publicId);
		if (GENERATOR_CONFIG_PUBLIC_ID.equals(publicId)) {
		    return getDTDSource();
		} else if (null != publicId && null != systemId
			&& systemId.contains(ENTITIES_FOLDER)) {
		    int base = systemId.indexOf(ENTITIES_FOLDER);
		    String entity = COMPONENT_SCHEMA_BASE
			    + systemId.substring(base);
		    getLog().debug(
			    "attempt to load entity from classpath " + entity);
		    InputStream entityStream = BuilderConfig.class
			    .getResourceAsStream(entity);
		    if (null != entityStream) {
			return new InputSource(entityStream);
		    }
		}
		return null;
	    }

	    public InputSource getExternalSubset(String name, String baseURI)
		    throws SAXException, IOException {
		getLog().debug(
			"Request for ExternalSubset with name " + name
				+ " and baseURI " + baseURI);
		if (GENERATOR_CONFIG_ROOT_ELEMENT.equals(name)) {
		    return getDTDSource();
		}
		return null;
	    }

	    public InputSource resolveEntity(String name, String publicId,
		    String baseURI, String systemId) throws SAXException,
		    IOException {
		getLog().debug(
			"Request for extended entity with systemId " + systemId
				+ " and publicId " + publicId);
		getLog().debug(
			"additional parameters with name " + name
				+ " and baseURI " + baseURI);
		if ("[dtd]".equals(name)
			&& GENERATOR_CONFIG_PUBLIC_ID.equals(publicId)) {
		    return getDTDSource();
		} else if (null == name
			|| (!name.startsWith("[") && !name.startsWith("&"))) {
		    return resolveEntity(publicId, systemId);
		}
		return null;
	    }

	    /**
                 * Resolve config DTD from classpath
                 * 
                 * @return source of config file DTD
                 */
	    private InputSource getDTDSource() {
		return new InputSource(BuilderConfig.class
			.getResourceAsStream(COMPONENT_CONFIG_DTD_URI));
	    }

	});

	// Parsing rules.

	// Components
	String path = "components/component";
	digester.addObjectCreate(path, ComponentBean.class);
	digester.addBeanPropertySetter(path + "/name");
	digester.addBeanPropertySetter(path + "/family");
	digester.addBeanPropertySetter(path + "/classname");
	digester.addBeanPropertySetter(path + "/superclass");
	digester.addBeanPropertySetter(path + "/description");
	digester.addBeanPropertySetter(path + "/displayname");
	digester.addBeanPropertySetter(path + "/icon");
	// TODO - for superclass, populate from description in config file, if
        // exist
	digester.addSetProperties(path);
	digester.addSetNext(path, "addComponent");
	path = "components/renderer";
	digester.addObjectCreate(path, RendererBean.class);
	digester.addBeanPropertySetter(path + "/?");
	digester.addSetProperties(path);
	digester.addSetNext(path, "addRenderer");
	path = "components/component/renderer";
	digester.addObjectCreate(path, RendererBean.class);
	digester.addBeanPropertySetter(path + "/?");
	digester.addSetProperties(path);
	digester.addSetNext(path, "setRenderer");

	path = "components/component/facet";
	digester.addObjectCreate(path, JsfBean.class);
	digester.addBeanPropertySetter(path + "/?");
	digester.addSetProperties(path);
	digester.addSetNext(path, "addFacet");
	path = "components/component/event";
	digester.addObjectCreate(path, EventBean.class);
	digester.addBeanPropertySetter(path + "/?");
	digester.addSetProperties(path);
	digester.addSetNext(path, "addEvent");

	// Validators

	path = "components/validator";
	digester.addObjectCreate(path, ValidatorBean.class);
	digester.addBeanPropertySetter(path + "/id");
	digester.addBeanPropertySetter(path + "/classname");
	digester.addBeanPropertySetter(path + "/superclass");
	digester.addBeanPropertySetter(path + "/description");
	digester.addBeanPropertySetter(path + "/displayname");
	digester.addBeanPropertySetter(path + "/icon");
	// TODO - for superclass, populate from description in config file, if
        // exist
	digester.addSetProperties(path);
	digester.addSetNext(path, "addValidator");

	path = "components/validator/property";
	digester.addObjectCreate(path, PropertyBean.class);
	digester.addBeanPropertySetter(path + "/?");
	digester.addSetProperties(path);
	digester.addSetNext(path, "addProperty");

	// Converters

	path = "components/converter";
	digester.addObjectCreate(path, ConverterBean.class);
	digester.addBeanPropertySetter(path + "/id");
	digester.addBeanPropertySetter(path + "/classname");
	digester.addBeanPropertySetter(path + "/superclass");
	digester.addBeanPropertySetter(path + "/description");
	digester.addBeanPropertySetter(path + "/displayname");
	digester.addBeanPropertySetter(path + "/icon");
	digester.addBeanPropertySetter(path + "/forclass");
	// TODO - for superclass, populate from description in config file, if
        // exist
	digester.addSetProperties(path);
	digester.addSetNext(path, "addConverter");

	// Functions

	path = "components/function";
	digester.addObjectCreate(path, FunctionBean.class);
	digester.addBeanPropertySetter(path + "/name");
	digester.addBeanPropertySetter(path + "/description");
	digester.addBeanPropertySetter(path + "/method");
	digester.addSetNext(path, "addFunction");
	
	// - Tags & Tag handlers

	path = "components/component/tag";
	digester.addObjectCreate(path, TagBean.class);
	digester.addBeanPropertySetter(path + "/?");
	digester.addSetProperties(path);
	digester.addSetNext(path, "setTag");

	path = "components/validator/tag";
	digester.addObjectCreate(path, TagBean.class);
	digester.addBeanPropertySetter(path + "/?");
	digester.addSetProperties(path);
	digester.addSetNext(path, "setTag");

	path = "components/converter/tag";
	digester.addObjectCreate(path, TagBean.class);
	digester.addBeanPropertySetter(path + "/?");
	digester.addSetProperties(path);
	digester.addSetNext(path, "setTag");

	path = "components/component/tag/test";
	digester.addObjectCreate(path, TagTestClassHolder.class);
	digester.addBeanPropertySetter(path + "/?");
	digester.addSetProperties(path);
	digester.addSetNext(path, "setTest");

	path = "components/component/test";
	digester.addObjectCreate(path, TestClassHolder.class);
	digester.addBeanPropertySetter(path + "/?");
	digester.addSetProperties(path);
	digester.addSetNext(path, "setTest");

	path = "components/component/taghandler";
	digester.addObjectCreate(path, TagHandlerBean.class);
	digester.addBeanPropertySetter(path + "/?");
	digester.addSetProperties(path);
	digester.addSetNext(path, "setTaghandler");
	/*
         * path = "components/component/taghandler/test";
         * digester.addObjectCreate(path, TestClassHolder.class);
         * digester.addBeanPropertySetter(path+"/?");
         * digester.addSetProperties(path); digester.addSetNext(path,
         * "setTest");
         */

	// Properties
	path = "components/component/property";
	digester.addObjectCreate(path, PropertyBean.class);
	digester.addBeanPropertySetter(path + "/?");
	digester.addSetProperties(path);
	digester.addSetNext(path, "addProperty");

	// path = "components/validator/property";
	// digester.addObjectCreate(path, PropertyBean.class);
	// digester.addBeanPropertySetter(path+"/?");
	// digester.addSetProperties(path);
	// digester.addSetNext(path, "addProperty");

	path = "components/converter/property";
	digester.addObjectCreate(path, PropertyBean.class);
	digester.addBeanPropertySetter(path + "/?");
	digester.addSetProperties(path);
	digester.addSetNext(path, "addProperty");

	path = "*/properties/property";
	digester.addObjectCreate(path, PropertyBean.class);
	digester.addBeanPropertySetter(path + "/?");
	digester.addSetProperties(path);
	digester.addSetNext(path, "addProperty");

	// Listeners

	path = "components/listener";
	digester.addObjectCreate(path, ListenerBean.class);
	digester.addBeanPropertySetter(path + "/?");
	digester.addSetProperties(path);
	digester.addSetNext(path, "addListener");
	path = "components/listener/tag";
	digester.addObjectCreate(path, TagBean.class);
	digester.addBeanPropertySetter(path + "/?");
	digester.addSetProperties(path);
	digester.addSetNext(path, "setTag");
	path = "components/listener/taghandler";
	digester.addObjectCreate(path, TagHandlerBean.class);
	digester.addBeanPropertySetter(path + "/?");
	digester.addSetProperties(path);
	digester.addSetNext(path, "setTaghandler");
	path = "components/listener/property";
	digester.addObjectCreate(path, PropertyBean.class);
	digester.addBeanPropertySetter(path + "/?");
	digester.addSetProperties(path);
	digester.addSetNext(path, "addProperty");

	// Set this config as root.
	digester.push(this);
	try {
	    digester.parse("file:"+configFile.toString());
	} catch (IOException e) {
	    // TODO Auto-generated catch block
	    throw new ParsingException("I/O error on parsing config file ", e);
	} catch (SAXException e) {
	    // TODO Auto-generated catch block
	    throw new ParsingException("SAX Parsing error in config file ", e);
	}
	// checkComopnentProperties();
	// return this;
    }

    /**
         * Check all components for existing and default properties.
         * 
         * @param classpath -
         *                classpath to find user components, renderers, tags
         * @throws ConfigurationException
         */
    public void checkComponentProperties() throws ParsingException {
	// ClassLoader loader = getProject().createClassLoader(classpath);
	// if(null == loader) {
	// loader = this.getClass().getClassLoader();
	// }
	// setLoader(loader);

	for (ListenerBean listener : getListeners()) {
	    try {
		Class<?> listenerClass = Class.forName(listener
			.getComponentclass(), false, getLoader());

		for (ComponentBean bean : getComponents()) {
		    if (bean.getSuperclass() != null) {
			Class<?> componentSClass = Class.forName(bean
				.getSuperclass(), false, getLoader());
			
			if (listenerClass.isAssignableFrom(componentSClass)) {

			    PropertyBean listenerProperty = bean
				    .getProperty(listener.getName());
			    
			    if (null == listenerProperty) {
					listenerProperty = new PropertyBean();
					listenerProperty.setName(listener.getName());
					bean.addProperty(listenerProperty);
				    listenerProperty.setClassname("javax.el.MethodExpression");
			    }
			    
			    Map<String, PropertyDescriptor> map = 
			    	getPropertyDescriptors(componentSClass);
				
			    PropertyDescriptor propertyDescriptor = 
			    	map.get(listener.getName());
			    
			    if (propertyDescriptor != null) {
			    	String componentPropertyName = propertyDescriptor.getPropertyType().getName();
			    	
			    	if (!componentPropertyName.equals(listenerProperty.getClassname())) {
			    		_log.error(
			    				String.format("Overriding property type %s with %s for %s.%s",
			    				listenerProperty.getClassname(),
			    				componentPropertyName,
			    				bean.getClassname(),
			    				listener.getName()
			    				));
			    	}
			    	
					listenerProperty.setClassname(componentPropertyName);
			    }
			    
			    // TODO - check existing property for compability with this listener. 
			    listenerProperty.setEl(true);
			    listenerProperty.setElonly(true);
			    listenerProperty.setAttachedstate(true);
			    listenerProperty.setMethodargs(listener
				    .getEventclass());
			    listener.addSuitableComponent(bean);
			}
		    }
		}
	    } catch (ClassNotFoundException e) {
		throw new BuildException(e);
	    }
	    
	    listener.checkProperties();
	}

	for (Iterator iter = this.getComponents().iterator(); iter.hasNext();) {
	    ComponentBaseBean component = (ComponentBaseBean) iter.next();
	    component.checkProperties();
	}

	for (Iterator iter = this.getValidators().iterator(); iter.hasNext();) {
	    ComponentBaseBean component = (ComponentBaseBean) iter.next();
	    component.checkProperties();
	}

	for (Iterator iter = this.getConverters().iterator(); iter.hasNext();) {
	    ComponentBaseBean component = (ComponentBaseBean) iter.next();
	    component.checkProperties();
	}

    }

    
    private Map<String, PropertyDescriptor> getPropertyDescriptors(Class<?> clazz) {
    	
    	if (clazz.equals(Object.class)) {
    		return Collections.emptyMap();
    	}
    	
    	Map<String, PropertyDescriptor> m = 
    		new TreeMap<String, PropertyDescriptor>();

    	Class<?> superclass = clazz.getSuperclass();
    	if (superclass != null) {
    		m.putAll(getPropertyDescriptors(superclass));
    	}
    	
    	Class<?>[] interfaces = clazz.getInterfaces();
    	if (interfaces != null) {
        	for (Class<?> intrfc : interfaces) {
    			m.putAll(getPropertyDescriptors(intrfc));
    		}
    	}
    	
    	PropertyDescriptor[] descriptors = 
    		PropertyUtils.getPropertyDescriptors(clazz);
    	
    	for (PropertyDescriptor propertyDescriptor : descriptors) {
			m.put(propertyDescriptor.getName(), propertyDescriptor);
		}
    	
    	return m;
    }
    
    /*
         * (non-Javadoc)
         * 
         * @see com.exadel.vcp.builder.config.LoaderHolder#getLoader()
         */
    public ClassLoader getLoader() {
	return _loader;
    }

    /*
         * (non-Javadoc)
         * 
         * @see com.exadel.vcp.builder.config.LoaderHolder#setLoader(java.lang.ClassLoader)
         */
    public void setLoader(ClassLoader loader) {
	_loader = loader;
    }

    public Logger getLog() {
	return _log;
    }

    public void addComponent(ComponentBean component) {
	this.components.add(component);
	component.setParent(this);
    }

    public void addValidator(ValidatorBean validator) {
	this.validators.add(validator);
	validator.setParent(this);
    }

    public void addConverter(ConverterBean converter) {
	this.converters.add(converter);
	converter.setParent(this);
    }

    public void addRenderer(RendererBean renderer) {
	this.renderers.add(renderer);
	renderer.setParent(this);
    }

    public void addListener(ListenerBean listener) {
	this.listeners.add(listener);
	listener.setParent(this);
    }

    public void addFunction(FunctionBean function) {
	this.functions.add(function);
	function.setParent(this);
    }

    /*
         * (non-Javadoc)
         * 
         * @see java.util.List#clear()
         */
    public void clear() {
	components.clear();
	renderers.clear();
    }

    /*
         * (non-Javadoc)
         * 
         * @see java.util.List#contains(java.lang.Object)
         */
    public boolean contains(Object o) {
	return components.contains(o);
    }

    /*
         * (non-Javadoc)
         * 
         * @see java.util.List#get(int)
         */
    public ComponentBaseBean get(int index) {
	return components.get(index);
    }

    /*
         * (non-Javadoc)
         * 
         * @see java.util.List#indexOf(java.lang.Object)
         */
    public int indexOf(ComponentBaseBean o) {
	return components.indexOf(o);
    }

    /*
         * (non-Javadoc)
         * 
         * @see java.util.List#isEmpty()
         */
    public boolean isEmpty() {
	return components.isEmpty();
    }

    /*
         * (non-Javadoc)
         * 
         * @see java.util.List#listIterator()
         */
    public List<ComponentBean> getComponents() {
	return components;
    }

    public List<ConverterBean> getConverters() {
	return converters;
    }

    public List<ValidatorBean> getValidators() {
	return validators;
    }

    public List<ListenerBean> getListeners() {
	return listeners;
    }

    /**
         * @return the renderers
         */
    public List<RendererBean> getRenderers() {
	return this.renderers;
    }

    public List<FunctionBean> getFunctions() {
	return functions;
    }
    
    /*
         * (non-Javadoc)
         * 
         * @see java.util.List#remove(int)
         */
    public ComponentBaseBean remove(int index) {
	return components.remove(index);
    }

    /*
         * (non-Javadoc)
         * 
         * @see java.util.List#remove(java.lang.Object)
         */
    public boolean remove(ComponentBaseBean o) {
	return components.remove(o);
    }

    /*
         * (non-Javadoc)
         * 
         * @see java.util.List#size()
         */
    public int size() {
	return components.size();
    }
}
