/*
 * The Apache Software License, Version 1.1
 *
 * Copyright (c) 2000-2001 The Apache Software Foundation.  All rights
 * reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution, if
 *    any, must include the following acknowlegement:
 *       "This product includes software developed by the
 *        Apache Software Foundation (http://www.apache.org/)."
 *    Alternately, this acknowlegement may appear in the software itself,
 *    if and wherever such third-party acknowlegements normally appear.
 *
 * 4. The names "The Jakarta Project", "Ant", and "Apache Software
 *    Foundation" must not be used to endorse or promote products derived
 *    from this software without prior written permission. For written
 *    permission, please contact apache@apache.org.
 *
 * 5. Products derived from this software may not be called "Apache"
 *    nor may "Apache" appear in their names without prior written
 *    permission of the Apache Group.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 */

package org.apache.tools.ant;

import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.types.EnumeratedAttribute;

import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Constructor;
import java.io.File;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Locale;

/**
 * Helper class that collects the methods a task or nested element
 * holds to set attributes, create nested elements or hold PCDATA
 * elements.
 *
 * @author <a href="mailto:stefan.bodewig@epost.de">Stefan Bodewig</a>
 */
public class IntrospectionHelper implements BuildListener {

    /**
     * holds the types of the attributes that could be set.
     */
    private Hashtable attributeTypes;

    /**
     * holds the attribute setter methods.
     */
    private Hashtable attributeSetters;

    /**
     * Holds the types of nested elements that could be created.
     */
    private Hashtable nestedTypes;

    /**
     * Holds methods to create nested elements.
     */
    private Hashtable nestedCreators;

    /**
     * Holds methods to store configured nested elements.
     */
    private Hashtable nestedStorers;

    /**
     * The method to add PCDATA stuff.
     */
    private Method addText = null;

    /**
     * The Class that's been introspected.
     */
    private Class bean;

    /**
     * instances we've already created
     */
    private static Hashtable helpers = new Hashtable();

    private IntrospectionHelper(final Class bean) {
        attributeTypes = new Hashtable();
        attributeSetters = new Hashtable();
        nestedTypes = new Hashtable();
        nestedCreators = new Hashtable();
        nestedStorers = new Hashtable();

        this.bean = bean;

        Method[] methods = bean.getMethods();
        for (int i=0; i<methods.length; i++) {
            final Method m = methods[i];
            final String name = m.getName();
            Class returnType = m.getReturnType();
            Class[] args = m.getParameterTypes();

            // not really user settable properties on tasks
            if (org.apache.tools.ant.Task.class.isAssignableFrom(bean)
                && args.length == 1 &&
                (
                 (
                  "setLocation".equals(name) && org.apache.tools.ant.Location.class.equals(args[0])
                  ) || (
                   "setTaskType".equals(name) && java.lang.String.class.equals(args[0])
                  )
                 )) {
                continue;
            }

            // hide addTask for TaskContainers
//              if (org.apache.tools.ant.TaskContainer.class.isAssignableFrom(bean)
//                  && args.length == 1 && "addTask".equals(name)
//                  && org.apache.tools.ant.Task.class.equals(args[0])) {
//                  continue;
//              }


            if ("addText".equals(name)
                && java.lang.Void.TYPE.equals(returnType)
                && args.length == 1
                && java.lang.String.class.equals(args[0])) {

                addText = methods[i];

            } else if (name.startsWith("set")
                       && java.lang.Void.TYPE.equals(returnType)
                       && args.length == 1
                       && !args[0].isArray()) {

                String propName = getPropertyName(name, "set");
                if (attributeSetters.get(propName) != null) {
                    if (java.lang.String.class.equals(args[0])) {
                        /*
                            Ignore method m, as there is an overloaded
                            form of this method that takes in a
                            non-string argument, which gains higher
                            priority.
                        */
                        continue;
                    }
                    /*
                        If the argument is not a String, and if there
                        is an overloaded form of this method already defined,
                        we just override that with the new one.
                        This mechanism does not guarantee any specific order
                        in which the methods will be selected: so any code
                        that depends on the order in which "set" methods have
                        been defined, is not guaranteed to be selected in any
                        particular order.
                    */
                }
                AttributeSetter as = createAttributeSetter(m, args[0]);
                if (as != null) {
                    attributeTypes.put(propName, args[0]);
                    attributeSetters.put(propName, as);
                }

            } else if (name.startsWith("create")
                       && !returnType.isArray()
                       && !returnType.isPrimitive()
                       && args.length == 0) {

                String propName = getPropertyName(name, "create");
                nestedTypes.put(propName, returnType);
                nestedCreators.put(propName, new NestedCreator() {

                        public Object create(Object parent)
                            throws InvocationTargetException,
                            IllegalAccessException {

                            return m.invoke(parent, new Object[] {});
                        }

                    });
                nestedStorers.remove(propName);

            } else if (name.startsWith("addConfigured")
                       && java.lang.Void.TYPE.equals(returnType)
                       && args.length == 1
                       && !java.lang.String.class.equals(args[0])
                       && !args[0].isArray()
                       && !args[0].isPrimitive()) {

                try {
                    final Constructor c =
                        args[0].getConstructor(new Class[] {});
                    String propName = getPropertyName(name, "addConfigured");
                    nestedTypes.put(propName, args[0]);
                    nestedCreators.put(propName, new NestedCreator() {

                            public Object create(Object parent)
                                throws InvocationTargetException, 
				       IllegalAccessException, 
				       InstantiationException {

                                Object o = c.newInstance(new Object[] {});
                                return o;
                            }

                        });
                    nestedStorers.put(propName, new NestedStorer() {

                            public void store(Object parent, Object child)
                                throws InvocationTargetException, 
				       IllegalAccessException, 
				       InstantiationException {

                                m.invoke(parent, new Object[] {child});
                            }

                        });
                } catch (NoSuchMethodException nse) {
                }
            } else if (name.startsWith("add")
                       && java.lang.Void.TYPE.equals(returnType)
                       && args.length == 1
                       && !java.lang.String.class.equals(args[0])
                       && !args[0].isArray()
                       && !args[0].isPrimitive()) {

                try {
                    final Constructor c =
                        args[0].getConstructor(new Class[] {});
                    String propName = getPropertyName(name, "add");
                    nestedTypes.put(propName, args[0]);
                    nestedCreators.put(propName, new NestedCreator() {

                            public Object create(Object parent)
                                throws InvocationTargetException, 
				       IllegalAccessException, 
				       InstantiationException {

                                Object o = c.newInstance(new Object[] {});
                                m.invoke(parent, new Object[] {o});
                                return o;
                            }

                        });
                    nestedStorers.remove(name);
                } catch (NoSuchMethodException nse) {
                }
            }
        }
    }

    /**
     * Factory method for helper objects.
     */
    public static synchronized IntrospectionHelper getHelper(Class c) {
        IntrospectionHelper ih = (IntrospectionHelper) helpers.get(c);
        if (ih == null) {
            ih = new IntrospectionHelper(c);
            helpers.put(c, ih);
        }
        return ih;
    }

    /**
     * Sets the named attribute.
     */
    public void setAttribute(Project p, Object element, String attributeName,
                             String value)
        throws BuildException {
        AttributeSetter as = (AttributeSetter) attributeSetters.get(attributeName);
        if (as == null) {
            String msg = getElementName(p, element) +
            //String msg = "Class " + element.getClass().getName() +
                " doesn't support the \"" + attributeName + "\" attribute.";
            throw new BuildException(msg);
        }
        try {
            as.set(p, element, value);
        } catch (IllegalAccessException ie) {
            // impossible as getMethods should only return public methods
            throw new BuildException(ie);
        } catch (InvocationTargetException ite) {
            Throwable t = ite.getTargetException();
            if (t instanceof BuildException) {
                throw (BuildException) t;
            }
            throw new BuildException(t);
        }
    }

    /**
     * Adds PCDATA areas.
     */
    public void addText(Project project, Object element, String text) {
        if (addText == null) {
            // Element doesn't handle text content
            if ( text.trim().length() == 0 ) {
                // Only whitespace - ignore
                return;
            }
            else {
                // Not whitespace - fail
                String msg = getElementName(project, element) +
                    " doesn't support nested text data.";
                throw new BuildException(msg);
            }
        }
        try {
            addText.invoke(element, new String[] {text});
        } catch (IllegalAccessException ie) {
            // impossible as getMethods should only return public methods
            throw new BuildException(ie);
        } catch (InvocationTargetException ite) {
            Throwable t = ite.getTargetException();
            if (t instanceof BuildException) {
                throw (BuildException) t;
            }
            throw new BuildException(t);
        }
    }

    /**
     * Creates a named nested element.
     */
    public Object createElement(Project project, Object element, String elementName)
        throws BuildException {
	
        try {
	    // First check if there are any roles supported by this class
	    Object nestedElement = project.createInRole(element, elementName);
	    if (nestedElement == null) {
		NestedCreator nc = 
		    (NestedCreator) nestedCreators.get(elementName);
		if (nc == null) {
		    String msg = getElementName(project, element) +
			" doesn't support the nested \"" + elementName + 
			"\" element.";
		    throw new BuildException(msg);
		}
		nestedElement = nc.create(element);
	    }
            if (nestedElement instanceof ProjectComponent) {
                ((ProjectComponent) nestedElement).setProject(project);
            }
            return nestedElement;
        } catch (IllegalAccessException ie) {
            // impossible as getMethods should only return public methods
            throw new BuildException(ie);
        } catch (InstantiationException ine) {
            // impossible as getMethods should only return public methods
            throw new BuildException(ine);
        } catch (InvocationTargetException ite) {
            Throwable t = ite.getTargetException();
            if (t instanceof BuildException) {
                throw (BuildException) t;
            }
            throw new BuildException(t);
        }
    }

    /**
     * Creates a named nested element.
     */
    public void storeElement(Project project, Object element, Object child, String elementName)
        throws BuildException {
        if (elementName == null) {
            return;
        }
        NestedStorer ns = (NestedStorer)nestedStorers.get(elementName);
        if (ns == null) {
            return;
        }
        try {
            ns.store(element, child);
        } catch (IllegalAccessException ie) {
            // impossible as getMethods should only return public methods
            throw new BuildException(ie);
        } catch (InstantiationException ine) {
            // impossible as getMethods should only return public methods
            throw new BuildException(ine);
        } catch (InvocationTargetException ite) {
            Throwable t = ite.getTargetException();
            if (t instanceof BuildException) {
                throw (BuildException) t;
            }
            throw new BuildException(t);
        }
    }

    /**
     * returns the type of a named nested element.
     */
    public Class getElementType(String elementName)
        throws BuildException {
        Class nt = (Class) nestedTypes.get(elementName);
        if (nt == null) {
            String msg = "Class " + bean.getName() +
                " doesn't support the nested \"" + elementName + "\" element.";
            throw new BuildException(msg);
        }
        return nt;
    }

    /**
     * returns the type of a named attribute.
     */
    public Class getAttributeType(String attributeName)
        throws BuildException {
        Class at = (Class) attributeTypes.get(attributeName);
        if (at == null) {
            String msg = "Class " + bean.getName() +
                " doesn't support the \"" + attributeName + "\" attribute.";
            throw new BuildException(msg);
        }
        return at;
    }

    /**
     * Does the introspected class support PCDATA?
     */
    public boolean supportsCharacters() {
        return addText != null;
    }

    /**
     * Return all attribues supported by the introspected class.
     */
    public Enumeration getAttributes() {
        return attributeSetters.keys();
    }

    /**
     * Return all nested elements supported by the introspected class.
     */
    public Enumeration getNestedElements() {
        return nestedTypes.keys();
    }

    /**
     * Create a proper implementation of AttributeSetter for the given
     * attribute type.
     */
    private AttributeSetter createAttributeSetter(final Method m,
                                                  final Class arg) {

        // simplest case - setAttribute expects String
        if (java.lang.String.class.equals(arg)) {
            return new AttributeSetter() {
                    public void set(Project p, Object parent, String value)
                        throws InvocationTargetException, IllegalAccessException {
                        m.invoke(parent, new String[] {value});
                    }
                };

        // now for the primitive types, use their wrappers
        } else if (java.lang.Character.class.equals(arg)
                   || java.lang.Character.TYPE.equals(arg)) {
            return new AttributeSetter() {
                    public void set(Project p, Object parent, String value)
                        throws InvocationTargetException, IllegalAccessException {
                        m.invoke(parent, new Character[] {new Character(value.charAt(0))});
                    }

                };
        } else if (java.lang.Byte.TYPE.equals(arg)) {
            return new AttributeSetter() {
                    public void set(Project p, Object parent, String value)
                        throws InvocationTargetException, IllegalAccessException {
                        m.invoke(parent, new Byte[] {new Byte(value)});
                    }

                };
        } else if (java.lang.Short.TYPE.equals(arg)) {
            return new AttributeSetter() {
                    public void set(Project p, Object parent, String value)
                        throws InvocationTargetException, IllegalAccessException {
                        m.invoke(parent, new Short[] {new Short(value)});
                    }

                };
        } else if (java.lang.Integer.TYPE.equals(arg)) {
            return new AttributeSetter() {
                    public void set(Project p, Object parent, String value)
                        throws InvocationTargetException, IllegalAccessException {
                        m.invoke(parent, new Integer[] {new Integer(value)});
                    }

                };
        } else if (java.lang.Long.TYPE.equals(arg)) {
            return new AttributeSetter() {
                    public void set(Project p, Object parent, String value)
                        throws InvocationTargetException, IllegalAccessException {
                        m.invoke(parent, new Long[] {new Long(value)});
                    }

                };
        } else if (java.lang.Float.TYPE.equals(arg)) {
            return new AttributeSetter() {
                    public void set(Project p, Object parent, String value)
                        throws InvocationTargetException, IllegalAccessException {
                        m.invoke(parent, new Float[] {new Float(value)});
                    }

                };
        } else if (java.lang.Double.TYPE.equals(arg)) {
            return new AttributeSetter() {
                    public void set(Project p, Object parent, String value)
                        throws InvocationTargetException, IllegalAccessException {
                        m.invoke(parent, new Double[] {new Double(value)});
                    }

                };

        // boolean gets an extra treatment, because we have a nice method
        // in Project
        } else if (java.lang.Boolean.class.equals(arg)
                   || java.lang.Boolean.TYPE.equals(arg)) {
            return new AttributeSetter() {
                    public void set(Project p, Object parent, String value)
                        throws InvocationTargetException, IllegalAccessException {
                        m.invoke(parent,
                                 new Boolean[] {new Boolean(Project.toBoolean(value))});
                    }

                };

        // Class doesn't have a String constructor but a decent factory method
        } else if (java.lang.Class.class.equals(arg)) {
            return new AttributeSetter() {
                    public void set(Project p, Object parent, String value)
                        throws InvocationTargetException, IllegalAccessException, BuildException {
                        try {
                            m.invoke(parent, new Class[] {Class.forName(value)});
                        } catch (ClassNotFoundException ce) {
                            throw new BuildException(ce);
                        }
                    }
                };

        // resolve relative paths through Project
        } else if (java.io.File.class.equals(arg)) {
            return new AttributeSetter() {
                    public void set(Project p, Object parent, String value)
                        throws InvocationTargetException, IllegalAccessException {
                        m.invoke(parent, new File[] {p.resolveFile(value)});
                    }

                };

        // resolve relative paths through Project
        } else if (org.apache.tools.ant.types.Path.class.equals(arg)) {
            return new AttributeSetter() {
                    public void set(Project p, Object parent, String value)
                        throws InvocationTargetException, IllegalAccessException {
                        m.invoke(parent, new Path[] {new Path(p, value)});
                    }

                };

        // EnumeratedAttributes have their own helper class
        } else if (org.apache.tools.ant.types.EnumeratedAttribute.class.isAssignableFrom(arg)) {
            return new AttributeSetter() {
                    public void set(Project p, Object parent, String value)
                        throws InvocationTargetException, IllegalAccessException, BuildException {
                        try {
                            org.apache.tools.ant.types.EnumeratedAttribute ea = (org.apache.tools.ant.types.EnumeratedAttribute)arg.newInstance();
                            ea.setValue(value);
                            m.invoke(parent, new EnumeratedAttribute[] {ea});
                        } catch (InstantiationException ie) {
                            throw new BuildException(ie);
                        }
                    }
                };

        // worst case. look for a public String constructor and use it
        } else {

            try {
                final Constructor c =
                    arg.getConstructor(new Class[] {java.lang.String.class});

                return new AttributeSetter() {
                        public void set(Project p, Object parent,
                                        String value)
                            throws InvocationTargetException, IllegalAccessException, BuildException {
                            try {
                                Object attribute = c.newInstance(new String[] {value});
                                if (attribute instanceof ProjectComponent) {
                                    ((ProjectComponent) attribute).setProject(p);
                                }
                                m.invoke(parent, new Object[] {attribute});
                            } catch (InstantiationException ie) {
                                throw new BuildException(ie);
                            }
                        }
                    };

            } catch (NoSuchMethodException nme) {
            }
        }

        return null;
    }

    protected String getElementName(Project project, Object element)
    {
        Hashtable elements = project.getTaskDefinitions();
        String typeName = "task";
        if (!elements.contains( element.getClass() ))
        {
            elements = project.getDataTypeDefinitions();
            typeName = "data type";
            if (!elements.contains( element.getClass() ))
            {
                elements = null;
            }
        }

        if (elements != null)
        {
            Enumeration e = elements.keys();
            while (e.hasMoreElements())
            {
                String elementName = (String) e.nextElement();
                Class elementClass = (Class) elements.get( elementName );
                if ( element.getClass().equals( elementClass ) )
                {
                    return "The <" + elementName + "> " + typeName;
                }
            }
        }

        return "Class " + element.getClass().getName();
    }

    /**
     * extract the name of a property from a method name - subtracting
     * a given prefix.
     */
    private String getPropertyName(String methodName, String prefix) {
        int start = prefix.length();
        return methodName.substring(start).toLowerCase(Locale.US);
    }

    private interface NestedCreator {
        Object create(Object parent)
            throws InvocationTargetException, IllegalAccessException, InstantiationException;
    }

    private interface NestedStorer {
        void store(Object parent, Object child)
            throws InvocationTargetException, IllegalAccessException, InstantiationException;
    }

    private interface AttributeSetter {
        void set(Project p, Object parent, String value)
            throws InvocationTargetException, IllegalAccessException,
                   BuildException;
    }

    public void buildStarted(BuildEvent event) {}
    public void buildFinished(BuildEvent event) {
        attributeTypes.clear();
        attributeSetters.clear();
        nestedTypes.clear();
        nestedCreators.clear();
        addText = null;
        helpers.clear();
    }

    public void targetStarted(BuildEvent event) {}
    public void targetFinished(BuildEvent event) {}
    public void taskStarted(BuildEvent event) {}
    public void taskFinished(BuildEvent event) {}
    public void messageLogged(BuildEvent event) {}
}
