/*
 * Copyright 2003-2004 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.lang.reflect.Field;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Set;
import java.util.Map;
import java.util.HashMap;
import java.util.HashSet;
import java.util.ArrayList;
import java.util.List;

/**
 * Class used to define attributes programmatically for a class.
 * It is recommended that this class is used in the static initializer for
 * a class:
 * <pre><code>
 * public class RuntimeSample extends SuperSample implements SampleIFJoin {
 *
 *     static {
 *         try {
 *             RuntimeAttributeRepository rar = 
 *                 new RuntimeAttributeRepository (RuntimeSample.class);
 *
 *             rar.addClassAttribute (new ThreadSafe ());
 *             
 *             rar.addFieldAttribute ("field", new ThreadSafe ());
 *             
 *             rar.addMethodAttribute ("someMethod", new Class[]{}, 
 *                 new Dependency ( SampleService.class, "sample-some-method1" ));
 *             
 *             rar.addParameterAttribute ("methodWithAttributes", 
 *                 new Class[]{ Integer.TYPE, Integer.TYPE }, 1, 
 *                 new ThreadSafe ());
 * 
 *             rar.addReturnAttribute ("methodWithAttributes", 
 *                 new Class[]{ Integer.TYPE, Integer.TYPE }, 
 *                 new Dependency ( SampleService.class, "sample-return" ));
 *             
 *             rar.addMethodAttribute ("someMethod", 
 *                 new Class[]{ Integer.TYPE },  
 *                 new Dependency ( SampleService.class, "sample-some-method2" ));
 *             
 *             Attributes.setAttributes (rar);
 *         } catch (Exception e) {
 *             throw new Error ("Unable to set attribute information: " + e.toString ());
 *         }
 *     }
 * </code></pre>
 *
 * @since 2.1
 */
public class RuntimeAttributeRepository implements AttributeRepositoryClass {
    
    /**
     * Flag indicating whether this repository is modifiable.
     * Once sealed, a repository can't be un-sealed.
     */
    private volatile boolean sealed = false;
    
    /**
     * Set of class attributes. See javadoc for {@link AttributeRepositoryClass} for structure.
     */
    private final Set classAttributes = new HashSet ();
    
    /**
     * Set of field attributes. See javadoc for {@link AttributeRepositoryClass} for structure.
     */
    private final Map fieldAttributes = new HashMap ();
    
    /**
     * Set of ctor attributes. See javadoc for {@link AttributeRepositoryClass} for structure.
     */
    private final Map constructorAttributes = new HashMap ();
    
    /**
     * Set of method attributes. See javadoc for {@link AttributeRepositoryClass} for structure.
     */
    private final Map methodAttributes = new HashMap ();
    
    /**
     * Class we are defining attributes for.
     */
    private final Class clazz;
    
    /**
     * Synchronization lock for sealing the repository. We use this instead of
     * synchronizing on the instance (this), since the sync:ing was added
     * in 2.2 and we don't want to break any code.
     * 
     * @since 2.2
     */
    private final Object syncLock = new Object ();
    
    /**
     * Create a new runtime repository.
     *
     * @since 2.1
     */
    public RuntimeAttributeRepository (Class clazz) {
        this.clazz = clazz;
    }
    
    /**
     * Adds a new attribute to the class itself.
     *
     * @since 2.1
     */
    public void addClassAttribute (Object attribute) {
        checkSealed ();
        classAttributes.add (attribute);
    }
    
    /**
     * Convenience function to check if the repository is sealed.
     * 
     * @throws IllegalStateException if sealed
     *
     * @since 2.1
     */
    private void checkSealed () throws IllegalStateException {
        synchronized (syncLock) {
            if (sealed) {
                throw new IllegalStateException ("RuntimeAttributeRepository has been sealed.");
            }
        }
    }
    
    /**
     * Convenience method to get and initialize an enry in the method or
     * constructor attribute map.
     *
     * @since 2.1
     */
    private List getMethodOrConstructorAttributeBundle (Map map, String signature, int numSlots) {
        List bundle = (List) map.get (signature);
        if (bundle == null) {
            bundle = new ArrayList ();
            map.put (signature, bundle);
            
            for (int i = 0; i < numSlots; i++) {
                bundle.add (new HashSet ());
            }
        }
        
        return bundle;
    }
    
    /**
     * Convenience method to get and initialize an entry in the method map.
     *
     * @return a fully initialized List (as defined for the {@link AttributeRepositoryClass} interface)
     *         for the given method.
     *
     * @since 2.1
     */
    private List getMethodAttributeBundle (Method m) {
        String signature = Util.getSignature (m);
        if (m.getDeclaringClass () != clazz) {
            throw new IllegalArgumentException ("There is no " + signature + " in " + clazz.getName () + ". It is defined in " + 
                m.getDeclaringClass ().getName ());
        }
        
        return getMethodOrConstructorAttributeBundle (methodAttributes, signature, m.getParameterTypes ().length + 2);
    }
    
    /**
     * Convenience method to get and initialize an entry in the constructor map.
     *
     * @return a fully initialized List (as defined for the {@link AttributeRepositoryClass} interface)
     *         for the given constructor.
     *
     * @since 2.1
     */
    private List getConstructorAttributeBundle (Constructor c) {
        String signature = Util.getSignature (c);
        if (c.getDeclaringClass () != clazz) {
            throw new IllegalArgumentException ("There is no " + signature + " in " + clazz.getName () + ". It is defined in " + 
                c.getDeclaringClass ().getName ());
        }
        
        return getMethodOrConstructorAttributeBundle (constructorAttributes, signature, c.getParameterTypes ().length + 1);
    }
    
    /**
     * Adds an attribute to a field.
     *
     * @since 2.1
     */
    public void addFieldAttribute (String name, Object attribute) throws NoSuchFieldException, SecurityException {
        addFieldAttribute (clazz.getDeclaredField (name), attribute);
    }
    
    /**
     * Adds an attribute to a field.
     *
     * @since 2.1
     */
    public void addFieldAttribute (Field f, Object attribute) {
        checkSealed ();
        String signature = f.getName ();
        if (f.getDeclaringClass () != clazz) {
            throw new IllegalArgumentException ("There is no " + signature + " in " + clazz.getName () + ". It is defined in " + 
                f.getDeclaringClass ().getName ());
        }
        
        Set attributeSet = (Set) fieldAttributes.get (signature);
        if (attributeSet == null) {
            attributeSet = new HashSet ();
            fieldAttributes.put (signature, attributeSet);
        }
        
        attributeSet.add (attribute);
    }
    
    /**
     * Adds an attribute to a constructor. The constructor is obtained via the getDeclaredConstrutor method 
     * of the class this repository defines.
     *
     * @since 2.1
     */
    public void addConstructorAttribute (Class[] parameters, Object attribute) throws NoSuchMethodException, SecurityException {
        addConstructorAttribute (clazz.getDeclaredConstructor (parameters), attribute);
    }
    
    /**
     * Adds an attribute to a constructor.
     *
     * @since 2.1
     */
    public void addConstructorAttribute (Constructor c, Object attribute) {
        checkSealed ();
        List bundle = getConstructorAttributeBundle (c);
        Set ctorAttrs = (Set) bundle.get (0);
        ctorAttrs.add (attribute);
    }
    
    /**
     * Adds an attribute to a method. The method is obtained via the getDeclaredMethod method 
     * of the class this repository defines.
     *
     * @since 2.1
     */
    public void addMethodAttribute (String name, Class[] parameters, Object attribute) throws NoSuchMethodException, SecurityException {
        addMethodAttribute (clazz.getDeclaredMethod (name, parameters), attribute);
    }
    
    /**
     * Adds an attribute to a method.
     *
     * @since 2.1
     */
    public void addMethodAttribute (Method m, Object attribute) {
        checkSealed ();
        List bundle = getMethodAttributeBundle (m);
        Set methodAttrs = (Set) bundle.get (0);
        methodAttrs.add (attribute);
    }
    
    /**
     * Adds an attribute to a parameter of a constructor. The constructor is obtained via the getDeclaredConstrutor method 
     * of the class this repository defines.
     *
     * @since 2.1
     */
    public void addParameterAttribute (Class[] parameters, int parameterIndex, Object attribute) throws NoSuchMethodException, SecurityException {
        addParameterAttribute (clazz.getDeclaredConstructor (parameters), parameterIndex, attribute);
    }
    
    /**
     * Adds an attribute to a parameter of a constructor. 
     *
     * @since 2.1
     */
    public void addParameterAttribute (Constructor c, int parameterIndex, Object attribute) {
        checkSealed ();
        List bundle = getConstructorAttributeBundle (c);
        
        Set parameterAttrs = (Set) bundle.get (parameterIndex + 1);
        parameterAttrs.add (attribute);
    }
    
    
    /**
     * Adds an attribute to a parameter of a method. The method is obtained via the getDeclaredMethod method 
     * of the class this repository defines.
     *
     * @since 2.1
     */
    public void addParameterAttribute (String name, Class[] parameters, int parameterIndex, Object attribute) throws NoSuchMethodException, SecurityException {
        addParameterAttribute (clazz.getDeclaredMethod (name, parameters), parameterIndex, attribute);
    }
    
    /**
     * Adds an attribute to a parameter of a method. The method is obtained via the getDeclaredMethod method 
     * of the class this repository defines.
     *
     * @since 2.1
     */
    public void addParameterAttribute (Method m, int parameterIndex, Object attribute) {
        checkSealed ();
        List bundle = getMethodAttributeBundle (m);
        
        Set parameterAttrs = (Set) bundle.get (parameterIndex + 2);
        parameterAttrs.add (attribute);
    }
    
    /**
     * Adds an attribute to the return value of a method. The method is obtained via the getDeclaredMethod method 
     * of the class this repository defines.
     *
     * @since 2.1
     */
    public void addReturnAttribute (String name, Class[] parameters, Object attribute) throws NoSuchMethodException, SecurityException {
        addReturnAttribute (clazz.getDeclaredMethod (name, parameters), attribute);
    }
    
    /**
     * Adds an attribute to the return value of a method. The method is obtained via the getDeclaredMethod method 
     * of the class this repository defines.
     *
     * @since 2.1
     */
    public void addReturnAttribute (Method m, Object attribute) {
        checkSealed ();
        
        List bundle = getMethodAttributeBundle (m);
        
        Set returnAttrs = (Set) bundle.get (1);
        returnAttrs.add (attribute);
    }
    
    /**
     * Gets the class this repository defines attributes for.
     *
     * @since 2.1
     */
    public Class getDefinedClass () {
        return clazz;
    }
    
    public Set getClassAttributes () {
        return classAttributes;
    }
    
    public Map getFieldAttributes () {
        return fieldAttributes;
    }
    
    public Map getMethodAttributes () {
        return methodAttributes;
    }
    
    public Map getConstructorAttributes () {
        return constructorAttributes;
    }
    
    /**
     * Seals this repository. A sealed repository can't be modified.
     *
     * @since 2.1
     */
    public void seal () {
        synchronized (syncLock) {
            sealed = true;
        }
    }
}