/*
 * Copyright 2003-2005 The Apache Software Foundation
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.commons.attributes;

import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Field;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.StringTokenizer;

/**
 * An index providing a list of elements with given attributes. This
 * requires that the attribute is {@link Indexed} and that the
 * attribute indexer tool has been run on the jar file containing the
 * classes.
 *
 * @since 2.1
 */
public class AttributeIndex {
    
    /**
     * Reference to a method parameter. A method parameter
     * is defined by the Method object it is defined in, and the index
     * of the parameter in the method's parameter list.
     *
     * @since 2.1
     */
    public static class MethodParameter {
        
        private final Method method;
        private final int index;
        
        /**
         * Constructs a new MethodParameter.
         *
         * @since 2.1
         */
        public MethodParameter (Method method, int index) {
            this.method = method;
            this.index = index;
        }
        
        /**
         * Get the method this parameter is defined in.
         *
         * @since 2.1
         */
        public Method getMethod () {
            return method;
        }
        
        /**
         * Get the index of this parameter in the parameter list of the method.
         *
         * @since 2.1
         */
        public int getIndex () {
            return index;
        }
        
        /**
         * Compares two <code>MethodParameter</code>s for equality.
         * They must point to the same method and have the same index.
         *
         * @since 2.1
         */
        public boolean equals (Object o) {
            return o != null && o instanceof MethodParameter &&
                method.equals (((MethodParameter) o).method) &&
                index == ((MethodParameter) o).index;
        }
        
        /**
         * Computes the hashCode.
         *
         * @since 2.1
         */        
        public int hashCode () {
            return method.hashCode () + index;
        }
        
        /**
         * Converts this method parameter into a human-readable string.
         *
         * @since 2.1
         */
        public String toString () {
            return method.toString () + ":" + index;
        }
    }
    
    /**
     * A constructor parameter. A method parameter
     * is defined by the Method object it is defined in, and the index
     * of the parameter in the method's parameter list.
     *
     * @since 2.1
     */
    public static class ConstructorParameter {
        
        private final Constructor ctor;
        private final int index;
        
        
        /**
         * Constructs a new ConstructorParameter.
         *
         * @since 2.1
         */
        public ConstructorParameter (Constructor ctor, int index) {
            this.ctor = ctor;
            this.index = index;
        }
        
        /**
         * Get the constructor this parameter is defined in.
         *
         * @since 2.1
         */
        public Constructor getConstructor () {
            return ctor;
        }
        
        /**
         * Get the index of this parameter in the parameter list of the constructor.
         *
         * @since 2.1
         */
        public int getIndex () {
            return index;
        }
        
        /**
         * Compares two <code>ConstructorParameter</code>s for equality.
         * They must point to the same constructor and have the same index.
         *
         * @since 2.1
         */
        public boolean equals (Object o) {
            return o != null && o instanceof ConstructorParameter &&
                ctor.equals (((ConstructorParameter) o).ctor) &&
                index == ((ConstructorParameter) o).index;
        }
        
        /**
         * Computes the hashCode.
         *
         * @since 2.1
         */    
        public int hashCode () {
            return ctor.hashCode () + index;
        }
        
        /**
         * Converts this constructor parameter into a human-readable string.
         *
         * @since 2.1
         */
        public String toString () {
            return ctor.toString () + ":" + index;
        }
    }
    
    private static class IndexNode {
        public Collection classes = new HashSet ();
        public Collection fields = new HashSet ();
        public Collection methods = new HashSet ();
        public Collection constructors = new HashSet ();
        public Collection returnValues = new HashSet ();
        public Collection constructorParameters = new HashSet ();
        public Collection methodParameters = new HashSet ();
        
        public void seal () {
            classes = seal (classes);
            fields = seal (fields);
            methods = seal (methods);
            constructors = seal (constructors);
            returnValues = seal (returnValues);
            constructorParameters = seal (constructorParameters);
            methodParameters = seal (methodParameters);
        }
        
        private Collection seal (Collection coll) {
            return Collections.unmodifiableCollection (coll);
        }
    }
    
    private final HashMap index = new HashMap ();
    private final ClassLoader classLoader;
    
    /**
     * Creates a new AttributeIndex for the given ClassLoader.
     *
     * @since 2.1
     */
    public AttributeIndex (ClassLoader cl) throws Exception {
        this.classLoader = cl;
        Enumeration e = cl.getResources ("META-INF/attrs.index");
        while (e.hasMoreElements ()) {
            URL url = (URL) e.nextElement ();
            loadFromURL (url);
        }
        
        Iterator iter = index.values ().iterator ();
        while (iter.hasNext ()) {
            ((IndexNode) iter.next ()).seal ();
        }
    }
    
    private IndexNode getNode (Class attributeClass) {
        IndexNode node = (IndexNode) index.get (attributeClass.getName ());
        if (node == null) {
            node = new IndexNode ();
            index.put (attributeClass.getName (), node);
        }
        
        return node;
    }
    
    private void addIndex (Collection attributes, Class clazz) {
        Iterator iter = attributes.iterator ();
        while (iter.hasNext ()) {
            getNode (iter.next ().getClass ()).classes.add (clazz);
        }
    }
    
    private void addIndex (Collection attributes, Field field) {
        Iterator iter = attributes.iterator ();
        while (iter.hasNext ()) {
            getNode (iter.next ().getClass ()).fields.add (field);
        }
    }
    
    private void addIndex (Collection attributes, Method method) {
        Iterator iter = attributes.iterator ();
        while (iter.hasNext ()) {
            getNode (iter.next ().getClass ()).methods.add (method);
        }
    }
    
    private void addIndex (Collection attributes, Constructor constructor) {
        Iterator iter = attributes.iterator ();
        while (iter.hasNext ()) {
            getNode (iter.next ().getClass ()).constructors.add (constructor);
        }
    }
    
    private void addReturnIndex (Collection attributes, Method method) {
        Iterator iter = attributes.iterator ();
        while (iter.hasNext ()) {
            getNode (iter.next ().getClass ()).returnValues.add (method);
        }
    }
    
    private void addIndex (Collection attributes, Method method, int parameter) {
        Iterator iter = attributes.iterator ();
        while (iter.hasNext ()) {
            getNode (iter.next ().getClass ()).methodParameters.add (new MethodParameter (method, parameter));
        }
    }
    
    private void addIndex (Collection attributes, Constructor ctor, int parameter) {
        Iterator iter = attributes.iterator ();
        while (iter.hasNext ()) {
            getNode (iter.next ().getClass ()).constructorParameters.add (new ConstructorParameter (ctor, parameter));
        }
    }
    
    /**
     * Add a class to the index.
     */
    private void addClass (String clazzName) throws Exception {
        Class clazz = classLoader.loadClass (clazzName);
        
        // Get the attributes attached to the class itself...
        Collection coll = Attributes.getAttributes (clazz);
        
        coll = AttributeUtil.getObjectsWithAttributeType (coll, Indexed.class);
        addIndex (coll, clazz);
        
        Field[] fields = clazz.getDeclaredFields ();
        for (int i = 0; i < fields.length; i++) {
            coll = Attributes.getAttributes (fields[i]);
            
            coll = AttributeUtil.getObjectsWithAttributeType (coll, Indexed.class);
            addIndex (coll, fields[i]);
        }
        
        Method[] methods = clazz.getDeclaredMethods ();
        for (int i = 0; i < methods.length; i++) {
            coll = Attributes.getAttributes (methods[i]);
            
            coll = AttributeUtil.getObjectsWithAttributeType (coll, Indexed.class);
            addIndex (coll, methods[i]);
            
            // Return values
            coll = Attributes.getReturnAttributes (methods[i]);
            
            coll = AttributeUtil.getObjectsWithAttributeType (coll, Indexed.class);
            addReturnIndex (coll, methods[i]);
            
            // Parameters
            int numParameters = methods[i].getParameterTypes().length; 
            for (int j = 0; j < numParameters; j++) {
                coll = Attributes.getParameterAttributes (methods[i], j);
                
                coll = AttributeUtil.getObjectsWithAttributeType (coll, Indexed.class);
                addIndex (coll, methods[i], j);
            }
        }
        
        Constructor[] ctors = clazz.getDeclaredConstructors ();
        for (int i = 0; i < ctors.length; i++) {
            coll = Attributes.getAttributes (ctors[i]);
            
            coll = AttributeUtil.getObjectsWithAttributeType (coll, Indexed.class);
            addIndex (coll, ctors[i]);
            
            // Parameters
            int numParameters = ctors[i].getParameterTypes().length; 
            for (int j = 0; j < numParameters; j++) {
                coll = Attributes.getParameterAttributes (ctors[i], j);
                
                coll = AttributeUtil.getObjectsWithAttributeType (coll, Indexed.class);
                addIndex (coll, ctors[i], j);
            }
        }
    }
    
    /**
     * Load the attrs.index from a given URL.
     *
     * @since 2.1
     */
    private void loadFromURL (URL url) throws Exception {
        URLConnection connection = url.openConnection ();
        BufferedReader br = new BufferedReader (new InputStreamReader (connection.getInputStream ()));
        try {
            String currentAttributeClass = null;
            String line = null;
            while ((line = br.readLine ()) != null) {
                if (line.startsWith ("Class: ")) {
                    String className = line.substring ("Class: ".length ()).trim ();
                    addClass (className);
                }
            }
        } finally {
            br.close ();
        }
    }
    
    /**
     * Gets a Collection of the classes that have an attribute of the specified class.
     * The Collection contains the class names (String).
     * 
     * @deprecated Use the getClasses(Class) method instead.
     *
     * @since 2.1
     */    
    public Collection getClassesWithAttribute (String attributeClass) {
        if (index.containsKey (attributeClass)) {
            Collection classes = ((IndexNode) index.get (attributeClass)).classes;
            Iterator iter = classes.iterator ();
            Collection converted = new ArrayList (classes.size ());
            while (iter.hasNext ()) {
                converted.add (((Class) iter.next ()).getName ().replace ('$', '.'));
            }
            return converted;
        } else {
            return Collections.EMPTY_SET;
        }
    }
    
    /**
     * Gets a Collection of the classes that have an attribute of the specified class.
     * The Collection contains the class names (String).
     *
     * @deprecated Use the getClasses(Class) method instead.
     */
    public Collection getClassesWithAttribute (Class attributeClass) {
        return getClassesWithAttribute (attributeClass.getName ());
    }
    
    /**
     * Gets a Collection of the <code>Class</code>es that have an attribute of the specified class.
     * The Collection contains the classes (Class).
     *
     * @since 2.1
     */    
    public Collection getClasses (Class attributeClass) {
        if (index.containsKey (attributeClass.getName ())) {
            return ((IndexNode) index.get (attributeClass.getName ())).classes;
        } else {
            return Collections.EMPTY_SET;
        }
    }
    
    /**
     * Gets a Collection of the <code>Method</code>s that have an attribute of the specified class.
     * The Collection contains the methods (java.lang.reflect.Method).
     *
     * @since 2.1
     */    
    public Collection getMethods (Class attributeClass) {
        if (index.containsKey (attributeClass.getName ())) {
            return ((IndexNode) index.get (attributeClass.getName ())).methods;
        } else {
            return Collections.EMPTY_SET;
        }
    }
    
    /**
     * Gets a Collection of the <code>Method</code>s whose return value has an attribute of the specified class.
     * The Collection contains the methods (java.lang.reflect.Method).
     *
     * @since 2.1
     */    
    public Collection getMethodsReturning (Class attributeClass) {
        if (index.containsKey (attributeClass.getName ())) {
            return ((IndexNode) index.get (attributeClass.getName ())).returnValues;
        } else {
            return Collections.EMPTY_SET;
        }
    }
    
    /**
     * Gets a Collection of the <code>Field</code>s that have an attribute of the specified class.
     * The Collection contains the methods (java.lang.reflect.Field).
     *
     * @since 2.1
     */    
    public Collection getFields (Class attributeClass) {
        if (index.containsKey (attributeClass.getName ())) {
            return ((IndexNode) index.get (attributeClass.getName ())).fields;
        } else {
            return Collections.EMPTY_SET;
        }
    }
    
    /**
     * Gets a Collection of the <code>Constructor</code>s that have an attribute of the specified class.
     * The Collection contains the methods (java.lang.reflect.Constructor).
     *
     * @since 2.1
     */    
    public Collection getConstructors (Class attributeClass) {
        if (index.containsKey (attributeClass.getName ())) {
            return ((IndexNode) index.get (attributeClass.getName ())).constructors;
        } else {
            return Collections.EMPTY_SET;
        }
    }
    
    /**
     * Gets a Collection of the <code>ConstructorParameter</code>s that have an attribute of the specified class.
     * The Collection contains the methods ({@link AttributeIndex.ConstructorParameter}).
     *
     * @since 2.1
     */    
    public Collection getConstructorParameters (Class attributeClass) {
        if (index.containsKey (attributeClass.getName ())) {
            return ((IndexNode) index.get (attributeClass.getName ())).constructorParameters;
        } else {
            return Collections.EMPTY_SET;
        }
    }
    
    /**
     * Gets a Collection of the <code>MethodParameter</code>s that have an attribute of the specified class.
     * The Collection contains the methods ({@link AttributeIndex.MethodParameter}).
     *
     * @since 2.1
     */    
    public Collection getMethodParameters (Class attributeClass) {
        if (index.containsKey (attributeClass.getName ())) {
            return ((IndexNode) index.get (attributeClass.getName ())).methodParameters;
        } else {
            return Collections.EMPTY_SET;
        }
    }
}