/*
 * This software is distributed under following license based on modified BSD
 * style license.
 * ----------------------------------------------------------------------
 * 
 * Copyright 2009 The Nimbus2 Project. All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer. 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE NIMBUS PROJECT ``AS IS'' AND ANY EXPRESS
 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
 * NO EVENT SHALL THE NIMBUS PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 * The views and conclusions contained in the software and documentation are
 * those of the authors and should not be interpreted as representing official
 * policies, either expressed or implied, of the Nimbus2 Project.
 */
package jp.ossc.nimbus.service.aop.interceptor;

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

import org.apache.commons.jexl2.Expression;
import org.apache.commons.jexl2.JexlEngine;
import org.apache.commons.jexl2.JexlContext;
import org.apache.commons.jexl2.MapContext;

import jp.ossc.nimbus.beans.*;
import jp.ossc.nimbus.core.*;
import jp.ossc.nimbus.service.aop.*;
import jp.ossc.nimbus.service.aop.invoker.MethodReflectionCallInvokerService;

/**
 * リターンインターセプタ。<p>
 *
 * @author M.Takata
 */
public class ReturnInterceptorService extends ServiceBase
 implements Interceptor, Serializable, ReturnInterceptorServiceMBean{
    
    private static final long serialVersionUID = 6314705352031037858L;
    
    private Map<String, Object> returnValues;
    private Map<String, ServiceName> returnServiceNames;
    private List<Condition> returnConditions;
    private Object returnValue;
    private ServiceName returnServiceName;
    private Class<?> returnInterfaceClass;
    private ServiceName interceptorChainListServiceName;
    private ProxyInvocationHandler invocationHandler;
    private Object proxy;
    private boolean isEnabled = true;
    
    public void setEnabled(boolean enabled){
        isEnabled = enabled;
    }
    
    public boolean isEnabled(){
        return isEnabled;
    }
    
    public void setReturnValue(String condition, Object value){
        if(returnValues == null){
            returnValues = new LinkedHashMap<String, Object>();
        }
        returnValues.put(condition, value);
    }
    public Object getReturnValue(String condition){
        return returnValues == null
             ? null : returnValues.get(condition);
    }
    
    public void setReturnValue(Object value){
        returnValue = value;
    }
    public Object getReturnValue(){
        return returnValue;
    }
    
    public void setReturnServiceName(String condition, ServiceName name){
        if(returnServiceNames == null){
            returnServiceNames = new LinkedHashMap<String, ServiceName>();
        }
        returnValues.put(condition, name);
    }
    public ServiceName getReturnServiceName(String condition){
        return returnServiceNames == null
             ? null : (ServiceName)returnServiceNames.get(condition);
    }
    
    public void setReturnServiceName(ServiceName name){
        returnServiceName = name;
    }
    public ServiceName getReturnServiceName(){
        return returnServiceName;
    }
    
    public void setReturnInterfaceClass(Class<?> clazz){
        returnInterfaceClass = clazz;
    }
    public Class<?> getReturnInterfaceClass(){
        return returnInterfaceClass;
    }
    
    public void setInterceptorChainListServiceName(ServiceName name){
        interceptorChainListServiceName = name;
    }
    public ServiceName getInterceptorChainListServiceName(){
        return interceptorChainListServiceName;
    }
    
    public void startService() throws Exception{
        if(returnConditions != null){
            returnConditions.clear();
        }
        if(returnValues != null
             && returnValues.size() != 0){
            if(returnConditions == null){
                returnConditions = new ArrayList<Condition>();
            }
            for(Map.Entry<String, Object> entry : returnValues.entrySet()){
                final String condition = entry.getKey();
                final Object value = entry.getValue();
                returnConditions.add(new Condition(condition, value));
            }
        }
        if(returnServiceNames != null
             && returnServiceNames.size() != 0){
            if(returnConditions == null){
                returnConditions = new ArrayList<Condition>();
            }
            for(Map.Entry<String, ServiceName> entry : returnServiceNames.entrySet()){
                final String condition = entry.getKey();
                final ServiceName name = entry.getValue();
                returnConditions.add(new Condition(condition, name));
            }
        }
        if(returnInterfaceClass != null
             && interceptorChainListServiceName != null){
            final MethodReflectionCallInvokerService invoker
                 = new MethodReflectionCallInvokerService();
            invoker.create();
            invoker.start();
            invocationHandler = new ProxyInvocationHandler(
                interceptorChainListServiceName,
                invoker
            );
            proxy = Proxy.newProxyInstance(
                NimbusClassLoader.getInstance(),
                new Class[]{returnInterfaceClass},
                invocationHandler
            );
        }
    }
    
    /**
     * 該当するオブジェクトを返す。<p>
     * サービスが開始されていない場合は、次のインターセプタを呼び出す。<br>
     *
     * @param context 呼び出しのコンテキスト情報
     * @param chain 次のインターセプタを呼び出すためのチェーン
     * @return 呼び出し結果の戻り値
     * @exception Throwable 呼び出し先で例外が発生した場合、またはこのインターセプタで任意の例外が発生した場合。但し、本来呼び出される処理がthrowしないRuntimeException以外の例外をthrowしても、呼び出し元には伝播されない。
     */
    public Object invoke(
        InvocationContext context,
        InterceptorChain chain
    ) throws Throwable{
        if(getState() == State.STARTED && isEnabled){
            if(returnConditions != null && returnConditions.size() != 0){
                for(int i = 0, imax = returnConditions.size(); i < imax; i++){
                    final Condition condition
                         = (Condition)returnConditions.get(i);
                    if(condition.evaluate(context)){
                        return condition.getReturnValue();
                    }
                }
            }
            if(returnValue != null){
                return returnValue;
            }
            if(returnServiceName != null){
                return ServiceManagerFactory.getServiceObject(returnServiceName);
            }
            if(proxy != null && invocationHandler != null){
                Object ret = chain.invokeNext(context);
                if(ret == null){
                    return null;
                }
                invocationHandler.setTarget(ret);
                return proxy;
            }
        }
        return chain.invokeNext(context);
    }
    
    private class Condition implements Serializable{
        
        private static final long serialVersionUID = 8865216875252550610L;
        
        private transient Expression expression;
        private String condition;
        private Object returnValue;
        private ServiceName returnServiceName;
        
        private static final String PROP_FUNCTION_NAME = "prop";
        private static final String CONTEXT = "context";
        
        Condition(String cond) throws Exception{
            initCondition(cond);
        }
        
        Condition(String cond, Object value) throws Exception{
            this(cond);
            returnValue = value;
        }
        
        Condition(String cond, ServiceName name) throws Exception{
            this(cond);
            returnServiceName = name;
        }
        
        private void initCondition(String cond) throws Exception{
            JexlEngine jexl = new JexlEngine();
            jexl.setSilent(true);
            Map<String, Object> funcs = new HashMap<String, Object>();
            PropertyAccess propAccess = new PropertyAccess();
            propAccess.setIgnoreNullProperty(true);
            funcs.put(PROP_FUNCTION_NAME, propAccess);
            jexl.setFunctions(funcs);
            expression = jexl.createExpression(cond);
            evaluate("", true);
            condition = cond;
        }
        
        public Object getReturnValue(){
            if(returnValue != null){
                return returnValue;
            }
            if(returnServiceName != null){
                return ServiceManagerFactory.getServiceObject(returnServiceName);
            }
            return null;
        }
        
        public boolean evaluate(Object object){
            return evaluate(object, false);
        }
        
        protected boolean evaluate(Object object, boolean isTest){
            JexlContext jexlContext = new MapContext();
            jexlContext.set(CONTEXT, object);
            
            try{
                Object exp = expression.evaluate(jexlContext);
                if(exp instanceof Boolean){
                    return ((Boolean)exp).booleanValue();
                }else{
                    if(exp == null && isTest){
                        return true;
                    }
                    throw new IllegalArgumentException(expression.getExpression());
                }
            }catch(Exception e){
                throw new RuntimeException(e);
            }
        }
        
        private void readObject(ObjectInputStream in)
         throws IOException, ClassNotFoundException{
            in.defaultReadObject();
            try{
                initCondition(condition);
            }catch(Exception e){
                // 起こらないはず
            }
        }
    }
    
    private static class ProxyInvocationHandler
     implements InvocationHandler, Serializable{
        
        private static final long serialVersionUID = 538686435253235644L;
        
        private final InterceptorChain chain;
        private transient ThreadLocal<Object> target = new ThreadLocal<Object>();
        
        public ProxyInvocationHandler(
            ServiceName interceptorChainListServiceName,
            Invoker invoker
        ){
            DefaultThreadLocalInterceptorChain chain = new DefaultThreadLocalInterceptorChain();
            chain.setInterceptorChainListServiceName(
                interceptorChainListServiceName
            );
            chain.setInvoker(invoker);
            this.chain = chain;
        }
        
        public void setTarget(Object target){
            this.target.set(target);
        }
        
        public Object invoke(
            Object proxy,
            Method method,
            Object[] args
        ) throws Throwable{
            final InvocationContext ctx = new DefaultMethodInvocationContext(
                target.get(),
                method,
                args
            );
            return chain.invokeNext(ctx);
        }
        
        private void writeObject(ObjectOutputStream out) throws IOException{
            out.defaultWriteObject();
            out.writeObject(target.get());
        }
        
        private void readObject(ObjectInputStream in)
         throws IOException, ClassNotFoundException{
            in.defaultReadObject();
            target = new ThreadLocal<Object>();
            target.set(in.readObject());
        }
    }
}
