/*
 * Copyright (c) 1998-2012 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source 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, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.config.event;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Set;

import javax.enterprise.context.Dependent;
import javax.enterprise.context.spi.Context;
import javax.enterprise.context.spi.CreationalContext;
import javax.enterprise.event.ObserverException;
import javax.enterprise.event.Observes;
import javax.enterprise.event.Reception;
import javax.enterprise.event.TransactionPhase;
import javax.enterprise.inject.Disposes;
import javax.enterprise.inject.spi.AnnotatedMethod;
import javax.enterprise.inject.spi.AnnotatedParameter;
import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.InjectionPoint;

import com.caucho.config.ConfigException;
import com.caucho.config.bytecode.ScopeProxy;
import com.caucho.config.inject.CreationalContextImpl;
import com.caucho.config.inject.InjectManager;
import com.caucho.config.inject.InjectionPointImpl;
import com.caucho.config.inject.OwnerCreationalContext;
import com.caucho.config.program.BeanArg;
import com.caucho.util.L10N;

/**
 * Internal implementation for a producer Bean
 */
public class ObserverMethodImpl<X, T> extends AbstractObserverMethod<T> {
  private static final L10N L = new L10N(ObserverMethodImpl.class);
  
  private InjectManager _cdiManager;

  private Bean<X> _bean;
  private AnnotatedMethod<X> _method;

  private Type _type;
  private Set<Annotation> _qualifiers;

  private BeanArg<X> []_args;
  private boolean _isIfExists;
  private boolean _isStatic;
  
  private TransactionPhase _transactionPhase = TransactionPhase.IN_PROGRESS;

  public ObserverMethodImpl(InjectManager beanManager,
                            Bean<X> bean,
                            AnnotatedMethod<X> method,
                            Type type,
                            Set<Annotation> qualifiers)
  {
    _cdiManager = beanManager;
    _bean = bean;
    _method = method;
    _method.getJavaMember().setAccessible(true);
    _isStatic = _method.isStatic();
    _type = type;
    _qualifiers = qualifiers;

    introspect();
  }

  /**
   * Returns the annotated method
   */
  public AnnotatedMethod<X> getAnnotatedMethod()
  {
    return _method;
  }

  /**
   * Returns the declaring bean
   */
  public Bean<X> getParentBean()
  {
    return _bean;
  }
  
  @Override
  public Class<X> getBeanClass()
  {
    return (Class<X>) _bean.getBeanClass();
  }

  /**
   * Returns the observed event type
   */
  @Override
  public Type getObservedType()
  {
    return _type;
  }

  /**
   * Returns the observed event bindings
   */
  @Override
  public Set<Annotation> getObservedQualifiers()
  {
    return _qualifiers;
  }

  private void introspect()
  {
    List<AnnotatedParameter<X>> parameters = _method.getParameters();

    /*
    if (parameters.size() == 1) {
      if (parameters.get(0).isAnnotationPresent(IfExists.class)) {
        _isIfExists = true;
      }

      return;
    }
    */
    
    Method javaMethod = _method.getJavaMember();

    _args = new BeanArg[parameters.size()];

    for (int i = 0; i < _args.length; i++) {
      AnnotatedParameter<?> param = parameters.get(i);

      Observes observes = param.getAnnotation(Observes.class);
      
      if (observes != null) {
        _isIfExists = observes.notifyObserver() == Reception.IF_EXISTS;
        _transactionPhase = observes.during();
      }
      else {
        InjectionPoint ip = new InjectionPointImpl(_cdiManager,
                                                   _bean,
                                                   param);

        _args[i] = new BeanArg<X>(_cdiManager,
                                  param.getBaseType(),
                                  _cdiManager.getQualifiers(param.getAnnotations()),
                                  ip);
      }
      
      if (param.isAnnotationPresent(Disposes.class)) {
        throw new ConfigException(L.l("{0}.{1} may not have @Observes and @Disposes on the same method",
                                      javaMethod.getDeclaringClass().getName(),
                                      javaMethod.getName()));
      }
    }
    
    if (_isIfExists && _bean.getScope() == Dependent.class) {
      throw new ConfigException(L.l("{0}.{1} @Observer cannot use IF_EXISTS because the bean has Dependent scope",
                                    javaMethod.getDeclaringClass().getName(),
                                    javaMethod.getName()));
    }
  }

  /**
   * Send the event notification.
   */
  @Override
  public void notify(T event)
  {
    notifyImpl(event);
  }
  
  protected void notifyImpl(T event)
  {
    X instance;
    
    CreationalContextImpl<X> env = null;
    OwnerCreationalContext<X> argEnv = null;

    if (_isIfExists) {
      instance = getExistsInstance();
      
      if (instance == null)
        return;
    }
    else if (_isStatic) {
      instance = null;
    }
    else {
      if (_bean.getScope() == Dependent.class) {
        env = new OwnerCreationalContext<X>(getParentBean());
      
        instance = getParentBean().create(env);
      }
      else {
        instance = _cdiManager.getReference(getParentBean());
      }
    }
    
    if (_args != null && _args.length > 1)
      argEnv = new OwnerCreationalContext<X>(null);
    
    Method method = _method.getJavaMember();

    try {
      Object object;
      
      if (instance instanceof ScopeProxy) {
        object = ((ScopeProxy) instance).__caucho_getDelegate();
      }
      else
        object = instance;

      method.invoke(object, getEventArguments(event, argEnv));
    } catch (IllegalArgumentException e) {
      String loc = (method.getDeclaringClass().getSimpleName() + "."
                    + method.getName() + ": ");

      throw new ObserverException(loc + e.toString(), e.getCause());
    } catch (RuntimeException e) {
      throw e;
    } catch (InvocationTargetException e) {
      Throwable exn = e.getCause();
      
      if (exn instanceof RuntimeException)
        throw (RuntimeException) exn;
      
      String loc = (method.getDeclaringClass().getSimpleName() + "."
                    + method.getName() + ": ");

      throw new ObserverException(loc + exn.toString(), exn.getCause());
    } catch (Exception e) {
      String loc = (method.getDeclaringClass().getSimpleName() + "."
                    + method.getName() + ": ");

      throw new ObserverException(loc + e.toString(), e.getCause());
    } finally {
      if (argEnv != null)
        argEnv.release();
      
      if (env != null)
        getParentBean().destroy(instance, env);
    }
  }

  protected X getExistsInstance()
  {
    Bean<X> bean = getParentBean();
    
    Class<? extends Annotation>scopeType = bean.getScope();
    Context context = _cdiManager.getContext(scopeType);

    if (context != null)
      return (X) context.get(bean);
    else
      return null;
  }

  protected Object[] getEventArguments(Object event, 
                                       CreationalContextImpl<?> parentEnv)
  {
    if (_args == null)
      return new Object[] { event };

    Object []args = new Object[_args.length];

    for (int i = 0; i < _args.length; i++) {
      BeanArg<X> arg = _args[i];

      if (arg != null)
        args[i] = arg.eval((CreationalContextImpl) parentEnv);
      else
        args[i] = event;
    }

    return args;
  }

  @Override
  public Reception getReception()
  {
    if (_isIfExists)
      return Reception.IF_EXISTS;
    else
      return Reception.ALWAYS;
  }

  @Override
  public TransactionPhase getTransactionPhase()
  {
    return _transactionPhase;
  }

  /*
  public Set<InjectionPoint> getInjectionPoints()
  {
    throw new UnsupportedOperationException(getClass().getName());
  }

  public AnnotatedParameter<X> getEventParameter()
  {
    throw new UnsupportedOperationException(getClass().getName());
  }
  */

  @Override
  public String toString()
  {
    return getClass().getSimpleName() + "[" + _method + "]";
  }
}
