package org.mockejb.interceptor;

import java.util.*;
import java.lang.reflect.*;


/**
 * Performs the invocation of interceptors in their order in the
 * interceptor list. 
 * Each interceptor is called in turn until we get to the target object. 
 * At this point, the target object's method is called using reflection. 
 * Also keeps the invocation's  custom context (properties).   
 * To be thread safe, clients should create a new object of this 
 * class for each method call. 
 * 
 * @author Alexander Ananiev
 */
public class InvocationContext {
    
    private transient ListIterator iter;

    // TODO: context should be Threadlocal??
    private Map contextProperties = new HashMap();    
    private List interceptorList;

    private Object proxyObj;
    private Object targetObj;
    private Method targetMethod;
    private Method proxyMethod;
    private Object[] paramVals;
    
    private Object returnObject;
    private Throwable thrownThrowable;

                     
    /**
     * Creates a new instance of the InvocationContext.
     * 
     * @param interceptorList interceptors that will be invoked before the target method
     * @param proxyObj object that was intercepted, 
     * most likely it is the dynamic proxy object. Can be null.
     * @param proxyMethod method invoked on the proxy. The declaring class of the method is the 
     * interface's class. 
     * @param targetObj target object being called. 
     * @param targetMethod method being called. 
     * @param paramVals parameter values
     */    
    public InvocationContext( List interceptorList, Object proxyObj, Method proxyMethod, 
            Object targetObj, Method targetMethod, Object[] paramVals ){
        
        this( interceptorList, proxyObj, proxyMethod, targetObj, targetMethod, paramVals, null  );

    }

    /**
     * Creates a new instance of the InvocationContext.
     * 
     * @param interceptorList interceptors that will be invoked before the target method
     * @param proxyObj object that was intercepted, 
     * most likely it is the dynamic proxy object. Can be null if the object is not known.
     * @param proxyMethod method invoked on the proxy. The declaring class of the method is the 
     * interface's class. 
     * @param targetObj target object being called. 
     * @param targetMethod method being called. 
     * @param paramVals parameter values
     * @param contextProperties any additional context info for the interceptors   
     * 
     */    
    public InvocationContext( List interceptorList, Object proxyObj, Method proxyMethod, 
            Object targetObj, Method targetMethod, Object[] paramVals, Map contextProperties ){
        
        this.proxyObj=proxyObj;
        this.proxyMethod = proxyMethod;
        this.targetObj = targetObj;
        this.targetMethod = targetMethod;
        this.paramVals = paramVals;
        
        setInterceptorList( interceptorList );
        if ( contextProperties != null )
            this.contextProperties = new HashMap( contextProperties );
    
    }
    
    
    
    /**
     * Sets the list of interceptors
     * @param interceptorList list to set
     */
    public void setInterceptorList( final List interceptorList ){

        verifyInterceptors( interceptorList );
        this.interceptorList = interceptorList;
        reset();    
        
    }
    
    
    public List getInterceptorList(){
        
        return this.interceptorList;
        
    }

    /**
     * Returns the iterator currently in use to traverse the 
     * interceptor list. Clients can use the returned iterator
     * to find out their place in the call chain.
     * 
     * @return list iterator
     */
    public ListIterator getInterceptorIterator(){
        return iter;
    }
    
    /**
     * Makes sure that interceptors implement the Interceptor interface. 
     * Otherwise, throws IllegalArgumentException.
     *
     */
    private void verifyInterceptors( List interceptorList ){

        if ( interceptorList == null ){
            throw new IllegalArgumentException( "Interceptor list can't be null" );
        }
        
        Iterator i = interceptorList.iterator();
        while( i.hasNext() ){
            Object interceptor = i.next();
              if ( ! ( interceptor instanceof Interceptor ) ){
                throw new IllegalArgumentException( "Object "+interceptor+
                    " in the interceptor list does not implement interceptor interface");
            }
        }
        
    }
    
    /**
     * Resets the interceptor iterator.
     * @deprecated
     *
     */
    public void reset(){
        iter = interceptorList.listIterator();
    }
    
    /**
     * Clears the context properties and resets the interceptor iterator.
     *
     */
    public void clear(){
        reset();
        contextProperties.clear();
    }
    
    
    /**
     * Calls the next interceptor in the list. If this is the end of the list, 
     * calls the given method of the target object using reflection if the target 
     * object is not null.
     * "proceed" name is consistent with the "proceed" keyword used by AspectJ for "around" 
     * advices. 
     * Use "getReturnObject" to get the return value for this invocation. 
     */
    public void proceed() throws Exception {
        
        // Check if we're at the head of the chain
        if ( ! iter.hasPrevious() ) {
            // recreate iterator in case if the list changed
            reset();                    
        }
        

        if ( iter.hasNext() ) {
            Interceptor nextInterceptor = (Interceptor) iter.next();  
            try {
                nextInterceptor.intercept( this );
            }
            // Record throwable
            catch( Throwable throwable) {
                // store it for the record
                thrownThrowable = throwable;

                // Convert into exception
                if ( throwable instanceof Error ) {
                    throw (Error)throwable;
                }
                else if ( throwable instanceof Exception ){
                    throw (Exception)throwable;
                }
            }
            //in any event we need to restore the iterator to return where we were
            finally {
                iter.previous();
            }
        }

        
    }

    /**
     * Returns the proxy object. This is a dynamic proxy
     * object implementing an interface or a CGLIB-enhanced class
     * @return intercepted object
     */
    public Object getProxyObject( ) {
        return proxyObj;
    }
    
    
    /**
     * Returns the target object of the invocation.
     * This is the target object being called in response to the 
     * call of the proxy (interface). 
     * @return target object
     */
    public Object getTargetObject( ) {
        return targetObj;
    }
    
    /**
     * Returns the target method of the invocation.
     * This is the target method being called in response to the 
     * call to the proxy's method. For example, "find" method
     * of the Entity business interface is the intercepted method, 
     * whereas "ejbFind" method of the entity implementation class 
     * is the target method. 
     * 
     * @return method
     */
    public Method getTargetMethod( ) {
        return targetMethod;
    }
    
    /**
     * @deprecated Use getProxyObject instead 
     * @return proxy object
     */
    public Object getInterceptedObject( ) {
        return getProxyObject();
    } 
    
    /**
     * @deprecated Use getProxyMethod instead
     */
    public Method getInterceptedMethod( ) {
        return getProxyMethod();
    } 
    
    /**
     * Returns the proxy method, the method that was called on the proxy.
     * For example, "find" method
     * of the Entity business interface is the proxy method, 
     * and "ejbFind" method of the entity implementation class 
     * is the target method.      
     * @return proxy method
     */
    public Method getProxyMethod( ) {
        return proxyMethod;
    }
    
    public Object[] getParamVals(){
        return paramVals;
    }
    
    
    /**
     * Returns the return value of the invocation. Normally, 
     * this is a return value of the target method, however interceptors
     * can change it. 
     * @return Object or null if the method has void type or 
     * if the method threw exception
     */
    public Object getReturnObject( ) {
        return returnObject;
    }
    
    /**
     * Sets the return value of the invocation. This allows interceptors
     * to change the current return value.
     * @param returnObject return object to set
     * 
     */
    public void setReturnObject( final Object returnObject ) {
        this.returnObject = returnObject;
    }



    /**
     * Returns the throwable thrown by the target method or by one of the
     * interceptors. 
     * 
     * @return throwable or null if no exceptions were thrown during the invocation
     * 
     */
    public Object getThrownThrowable( ) {
        return thrownThrowable;
    }
    
    /**
     * Sets the throwable thrown by the invoked method 
     * @param throwable
     */
    public void setThrownThrowable( Throwable throwable ) {
        this.thrownThrowable = throwable;
    }
    

    
    /**
     * Adds the invocation context property. 
     * Context property is a piece of data made available 
     * to all interceptors. Interceptors can add/modify the context properties during the call. 
     * @param key key for this contextProperties's data
     * @param data contextProperties data
     */
    public void setContext( String key, Object data ){
        contextProperties.put( key, data );                
    }

    /**
     * Returns the custome context's property value associated with the provided key
     * or throws IllegalStateException if the key is not found
     * @param key contextProperties key
     * @return contextProperties data
     */
    public Object getPropertyValue( String key ) {
        if ( ! contextProperties.containsKey( key ) )
            throw new IllegalStateException("Key "+key+" is not found in the invocation context");
        
        return contextProperties.get( key );
    }
    
    /**
     * Returns the context property value associated with the provided key
     * or null if the key is not found
     * @param key contextProperties key
     * @return contextProperties data
     */
    public Object getOptionalPropertyValue( String key ) {
        
        return contextProperties.get( key );
    }
    

    /**
     * Calls the object's method using reflection.
     * This method takes <code>InvocationTargetException</code> out of the 
     * stack in case of exception. This allows exception handlers not to deal with 
     * reflection-specific exceptions.
     * @param targetObj target object being called
     * @param method method being called
     * @param paramVals parameter values  
     * @return value returned by the given method
     */
    protected Object invokeMethod( Object targetObj, Method method, Object[] paramVals ) 
        throws Throwable {

        Object returnObj;
        
        if ( targetObj == null ){
            throw new IllegalStateException("TargetObject is null during an attempt to call "+
                method+"\nOne of the interceptors should have handled target object invocation.");
        }
        
        try {        
            returnObj = method.invoke( targetObj, paramVals);
        }
        // We need to re-throw the cause of the exception, 
        // we don't want to give up the fact that the reflection is used.
        catch( InvocationTargetException ite ){
            throw ite.getTargetException();
        }
        
        return returnObj;        
    }
    
    public String toString(){
        // TODO: add concat values
        return targetMethod.toString();         
    }
    
}