/*
* JBoss, Home of Professional Open Source
* Copyright 2007, JBoss Inc., and individual contributors as indicated
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software 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 software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.kernel.plugins.dependency;

import java.lang.annotation.Annotation;
import java.util.List;
import java.util.Set;

import org.jboss.beans.info.spi.BeanInfo;
import org.jboss.beans.info.spi.PropertyInfo;
import org.jboss.beans.metadata.spi.AnnotationMetaData;
import org.jboss.beans.metadata.spi.BeanMetaData;
import org.jboss.beans.metadata.spi.CachingAnnotationMetaData;
import org.jboss.beans.metadata.spi.PropertyMetaData;
import org.jboss.dependency.plugins.AbstractScopeInfo;
import org.jboss.dependency.spi.Controller;
import org.jboss.dependency.spi.ControllerContext;
import org.jboss.kernel.plugins.config.Configurator;
import org.jboss.kernel.spi.dependency.KernelController;
import org.jboss.kernel.spi.dependency.KernelControllerContext;
import org.jboss.logging.Logger;
import org.jboss.metadata.spi.ComponentMutableMetaData;
import org.jboss.metadata.spi.loader.MutableMetaDataLoader;
import org.jboss.metadata.spi.repository.MutableMetaDataRepository;
import org.jboss.metadata.spi.retrieval.MetaDataRetrieval;
import org.jboss.metadata.spi.scope.CommonLevels;
import org.jboss.metadata.spi.scope.Scope;
import org.jboss.metadata.spi.scope.ScopeKey;
import org.jboss.metadata.spi.signature.FieldSignature;
import org.jboss.metadata.spi.signature.MethodSignature;
import org.jboss.metadata.spi.signature.Signature;
import org.jboss.reflect.spi.FieldInfo;
import org.jboss.reflect.spi.MethodInfo;

/**
 * KernelScopeInfo.
 * 
 * @author <a href="adrian@jboss.com">Adrian Brock</a>
 * @version $Revision: 1.1 $
 */
public class KernelScopeInfo extends AbstractScopeInfo
{
   /** The log */
   private static final Logger log = Logger.getLogger(KernelScopeInfo.class);
   
   /** The bean metadata */
   private BeanMetaData beanMetaData;
   
   /**
    * Create a new KernelScopeInfo.
    * 
    * @param name the name
    * @param className the class name
    * @param beanMetaData the bean metadata
    */
   public KernelScopeInfo(Object name, String className, BeanMetaData beanMetaData)
   {
      super(name, className);
      this.beanMetaData = beanMetaData;
   }

   @Override
   @SuppressWarnings("unchecked")
   public ScopeKey getScope()
   {
      // THIS IS A HACK - the scope originally gets initialise with a class name, we fix it to have the class
      ScopeKey key = super.getScope();
      Scope scope = key.getScope(CommonLevels.CLASS);
      if (scope == null)
         return key;
      Object qualifier = scope.getQualifier();
      if (qualifier instanceof Class)
         return key;

      String className = (String) qualifier;
      ClassLoader cl;
      try
      {
         cl = Configurator.getClassLoader(beanMetaData);
      }
      catch (Throwable t)
      {
         throw new RuntimeException("Error getting classloader for " + key, t);
      }
      Class<?> clazz;
      try
      {
         clazz = Class.forName(className, false, cl);
      }
      catch (ClassNotFoundException e)
      {
         throw new RuntimeException("Unable to load class: " + className + " for " + key, e);
      }
      key.addScope(new Scope(CommonLevels.CLASS, clazz));
      return key;
   }

   @Override
   public void updateMetaData(MutableMetaDataRepository repository, ControllerContext context, MutableMetaDataLoader mutable, boolean add)
   {
      if (context instanceof KernelControllerContext == false)
         return;

      KernelControllerContext kernelContext = (KernelControllerContext) context;
      updateClassAnnotations(mutable, kernelContext, add);
      if (mutable instanceof ComponentMutableMetaData)
          updatePropertyAnnotations(repository, (ComponentMutableMetaData) mutable, kernelContext, add);
      else if (add == true)
         log.warn("Unable to add properties to mutable metadata that does not support components: " + mutable + " for " + context.toShortString());
   }
   
   /**
    * Update class annotations
    * 
    * @param mutable the mutable metadata
    * @param context the context
    * @param add true for add, false for remove
    */
   private void updateClassAnnotations(MutableMetaDataLoader mutable, KernelControllerContext context, boolean add)
   {
      BeanMetaData beanMetaData = context.getBeanMetaData();
      if (beanMetaData != null)
      {
         ClassLoader cl;
         try
         {
            cl = Configurator.getClassLoader(beanMetaData);
         }
         catch(Throwable t)
         {
            throw new RuntimeException("Error getting classloader for " + beanMetaData.getName(), t);
         }
         updateAnnotations(cl, mutable, beanMetaData.getAnnotations(), add);
      }
   }

   /**
    * Update property annotations
    * 
    * @param repository the repository
    * @param mutable the mutable
    * @param context the kernel controller contex
    * @param add true for add, false for remove
    */
   private void updatePropertyAnnotations(MutableMetaDataRepository repository, ComponentMutableMetaData mutable, KernelControllerContext context, boolean add)
   {
      BeanMetaData beanMetaData = context.getBeanMetaData();
      if (beanMetaData == null)
         return;

      Set<PropertyMetaData> properties = beanMetaData.getProperties();

      if (properties == null || properties.size() == 0)
         return;

      BeanInfo beanInfo = context.getBeanInfo();
      if (beanInfo == null)
         return;

      ClassLoader cl;
      try
      {
         cl = Configurator.getClassLoader(beanMetaData);
      }
      catch(Throwable t)
      {
         throw new RuntimeException("Error getting classloader for metadata");
      }
      for (PropertyMetaData property : properties)
         updatePropertyAnnotations(repository, cl, mutable, context, property, beanInfo, add);
   }

   /**
    * Update property annotations
    * 
    * @param repository the repository
    * @param classloader the classloader
    * @param mutable the mutable
    * @param context the context
    * @param propertyMetaData the property
    * @param beanInfo the bean info
    * @param add true for add, false for remove
    */
   private void updatePropertyAnnotations(MutableMetaDataRepository repository, ClassLoader classloader, ComponentMutableMetaData mutable, KernelControllerContext context, PropertyMetaData propertyMetaData, BeanInfo beanInfo, boolean add)
   {
      Set<AnnotationMetaData> propertyAnnotations = propertyMetaData.getAnnotations();
      if (propertyAnnotations == null || propertyAnnotations.size() == 0)
         return;

      PropertyInfo propertyInfo = beanInfo.getProperty(propertyMetaData.getName());
      // method annotations
      MethodInfo methodInfo = propertyInfo.getGetter();
      if (methodInfo != null)
         updateAnnotations(repository, classloader, mutable, context, methodInfo, propertyAnnotations, add);
      methodInfo = propertyInfo.getSetter();
      if (methodInfo != null)
         updateAnnotations(repository, classloader, mutable, context, methodInfo, propertyAnnotations, add);
      // field annotations
      FieldInfo fieldInfo = propertyInfo.getFieldInfo();
      if (fieldInfo != null)
         updateAnnotations(repository, classloader, mutable, context, fieldInfo, propertyAnnotations, add);
   }
   
   /**
    * Update component annotations
    *
    * @param repository the repository
    * @param classloader the classloader
    * @param component the component metadata
    * @param context the context
    * @param signature the signature
    * @param scope the scope
    * @param annotations the annotations
    * @param add true for add, false for remove
    */
   private void updateAnnotations(MutableMetaDataRepository repository, ClassLoader classloader, ComponentMutableMetaData component, KernelControllerContext context, Signature signature, ScopeKey scope, Set<AnnotationMetaData> annotations, boolean add)
   {
      MetaDataRetrieval retrieval = ((MetaDataRetrieval) component).getComponentMetaDataRetrieval(signature);
      MutableMetaDataLoader mutable;
      if (retrieval != null)
      {
         mutable = getMutableMetaDataLoader(retrieval);
         if (mutable == null)
         {
            if (add)
               log.warn("MetaData is not mutable with signature: " + signature + " for " + context.toShortString());
            return;
         }
      }
      else if (add)
      {
         mutable = initMutableMetaDataRetrieval(repository, context, scope);
         component.addComponentMetaDataRetrieval(signature, mutable);
      }
      else
      {
         return;
      }
      updateAnnotations(classloader, mutable, annotations, add);
   }
   
   /**
    * Update annotations for a method
    *
    * @param repository the repository
    * @param classloader the classloader
    * @param component the mutable metadata
    * @param context the context
    * @param methodInfo the method info
    * @param annotations the annotations
    * @param add true for add, false for remove
    */
   private void updateAnnotations(MutableMetaDataRepository repository, ClassLoader classloader, ComponentMutableMetaData component, KernelControllerContext context, MethodInfo methodInfo, Set<AnnotationMetaData> annotations, boolean add)
   {
      if (annotations == null || annotations.isEmpty())
         return;

      Signature signature = new MethodSignature(methodInfo);
      ScopeKey scope = new ScopeKey(CommonLevels.JOINPOINT_OVERRIDE, methodInfo.getName());
      updateAnnotations(repository, classloader, component, context, signature, scope, annotations, add);
   }
   
   /**
    * Add annotations for a field
    *
    * @param repository the repository
    * @param classloader the classloader
    * @param component the mutable metadata
    * @param context the context
    * @param fieldInfo the field info
    * @param annotations the annotations
    * @param add true for add, false for remove
    */
   private void updateAnnotations(MutableMetaDataRepository repository, ClassLoader classloader, ComponentMutableMetaData component, KernelControllerContext context, FieldInfo fieldInfo, Set<AnnotationMetaData> annotations, boolean add)
   {
      if (annotations == null || annotations.isEmpty())
         return;

      Signature signature = new FieldSignature(fieldInfo);
      ScopeKey scope = new ScopeKey(CommonLevels.JOINPOINT_OVERRIDE, fieldInfo.getName());
      updateAnnotations(repository, classloader, component, context, signature, scope, annotations, add);
   }

   /**
    * Add annotations to a mutable metadata
    *
    * @param classloader the classloader
    * @param mutable the mutable metadata
    * @param annotations the annotations
    * @param add true for add, false for remove
    */
   private void updateAnnotations(ClassLoader classloader, MutableMetaDataLoader mutable, Set<AnnotationMetaData> annotations, boolean add)
   {
      if (annotations == null || annotations.size() == 0)
         return;

      for (AnnotationMetaData annotation : annotations)
      {
         if (add)
         {
            Annotation annotationInstance = annotation.getAnnotationInstance(classloader); 
            mutable.addAnnotation(annotationInstance);
         }
         else
         {
            Annotation annotationInstance = null;
            if (annotation instanceof CachingAnnotationMetaData)
               annotationInstance = ((CachingAnnotationMetaData) annotation).removeAnnotation();
            if (annotationInstance == null)
               annotationInstance = annotation.getAnnotationInstance(classloader);
            // Null means we never constructed it in the first place 
            if (annotationInstance != null)
               mutable.removeAnnotation(annotationInstance.annotationType());
         }
      }
   }

   @Override
   protected MetaDataRetrieval createMetaDataRetrieval(ControllerContext context, List<MetaDataRetrieval> retrievals)
   {
      Controller controller = context.getController();
      if (controller instanceof KernelController)
      {
         KernelController kernelController = (KernelController) controller;
         MetaDataRetrieval result = kernelController.getKernel().getMetaDataRepository().createMetaDataRetrieval(context, retrievals);
         if (result != null)
            return result;
      }
      return super.createMetaDataRetrieval(context, retrievals);
   }
}
