package org.jboss.seam.util;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

import org.jboss.seam.log.Log;
import org.jboss.seam.log.Logging;

/**
 * An InvocationHandler implementation that delegates method invocations to a specified object,
 * optionally allowing the method to be overridden locally. 
 * 
 * @author Shane Bryzak
 */
public class DelegatingInvocationHandler<T> implements InvocationHandler
{   
   private static Log log = Logging.getLog(DelegatingInvocationHandler.class);     
   
   private class MethodTarget
   {
      public Method method;
      public Object target;
      
      public MethodTarget(Object target, Method method)
      {
         this.target = target;
         this.method = method;
      }
   }   
   
   private Map<Method,MethodTarget> methodCache = new HashMap<Method,MethodTarget>();
   private T delegate;
   
   public DelegatingInvocationHandler(T delegate)
   {
      this.delegate = delegate;
   }
   
   public T getDelegate()
   {
      return delegate;
   }
   
   public Object invoke(Object proxy, Method method, Object[] args)
         throws Throwable
   {  
      MethodTarget target = methodCache.get(method);      
      
      if (target == null && !methodCache.containsKey(method))
      {
         synchronized(methodCache)
         {
            if (!methodCache.containsKey(method))
            {
               try
               {
                  target = new MethodTarget(this, getClass().getMethod(method.getName(), method.getParameterTypes()));                  
               }
               catch (NoSuchMethodException ex)
               {
                  // Swallow this, we'll try to find a matching method on the delegate
               }
               
               if (target == null)
               {
                  try
                  {
                     target = new MethodTarget(delegate, delegate.getClass().getMethod(method.getName(), method.getParameterTypes()));
                  }
                  catch (NoSuchMethodException ex)
                  {
                     // Swallow this, put a null entry in methodCache
                  }
               }
               
               methodCache.put(method, target);
            }
            else
            {
               target = methodCache.get(method);               
            }
         }                  
      }
      
      if (target == null)
      {
         throw new IllegalStateException("Proxied session does not implement method " + method.getName() +
               " with args [" + (args == null ? null : Arrays.asList(args)) + "]");
      }
      
      if (log.isTraceEnabled())
      {
         log.trace("Delegating method " + method.getName() + " with args " + 
                 (args == null ? null : Arrays.asList(args)));
      }
      
      return target.method.invoke(target.target, args);      
   }  
}
