package org.itscool.commons.aop;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.List;

import org.itscool.commons.collection.KeyLinkedList;

/**
 * プロクシを生成するクラスです
 * @author kanou
 *
 */
public class ProxyMethodInvocator implements InvocationHandler{
    /**
     * 生成された代理オブジェクト
     */
    private Object proxy;
    /**
     * 代理オブジェクトにイベント通知を依頼する実オブジェクト
     */
    private Object original;
    /**
     * 代理オブジェクトのメソッド引数
     */
    private Object[] args;
    /**
     * 代理オブジェクトのメソッド
     */
    private Method method;
    /**
     * メソッド実行結果（void型の場合はnullを返す）
     */
    private Object result = null;
    /**
     * 登録済みのAdviceの一覧
     */
    private KeyLinkedList adviceMap = new KeyLinkedList();
    /**
     * Adviceをリンクするカウンター値
     */
    private int chainCounter=0;
    
    /**
     * プロクシを生成します<BR>
     * proxy（プロクシ）とは、「代理人」や「代用品」、あるいは「……の代理」
     * という意味の言葉です。
     *
     * コンピュータの世界だと、HTTPサーバーへのリクエスト／レスポンスの中継
     * を行うプロクシサーバーが一番有名かも知れません。
     *
     * java.lang.reflect.Proxyは名前の通り、指定したインターフェイスを持つ
     * オブジェクトを偽装することで、ユーザーが指定したハンドラへクライアント
     * からの呼び出しを中継します。
     *
     * Proxyクラスは、特定のインターフェイスを偽装して（あるいは実行時に動的
     * に実装して）クライアントからの呼び出しをユーザー定義のハンドラオブジ
     * ェクトへデリゲートします。
     *
     * Proxyクラスに特定のインターフェイスを実装したオブジェクトを生成させる
     * には、次のリストのようにProxy.newProxyInstance.staticメソッドを利用します。
     *
     * Proxy.newProxyInstanceメソッドの引数は
     *
     * <li>Proxyオブジェクトを定義するのに利用するクラスローダ </li>
     * <p>
     *  通常はこの例のように偽装するインターフェイスのクラスローダを指定すれ
     *  ば良いでしょう。
     * </p>
     * <li>偽装するインターフェイスの配列 </li>
     * <p>
     *  Proxyは同時に複数のインターフェイスを実装することができます。
     * </p>
     * <li>InvocationHandlerのインスタンス </li>
     * <p>
     *  クライアントからの呼び出しを受け取るユーザー供与のオブジェクトです。
     * の3つです。<br>
     * このうち3番目の引数として与えるInvocationHandlerインターフェイス
     * （invocationはメソッドの呼び出しの意味）は、3引数のinvokeメソッド
     * を定義したイベント受信用インターフェイスです。<br>
     * Proxyオブジェクトはクライアントからの呼び出しを受けると
     * InvocationHandler#invokeメソッドを呼び出します<br>
     * </p>
     * @param implementsObject Proxyを適用する実オブジェクトを指定します
     * @param interfaceName Proxyにイベントを通知するためのインターフェースを指定します
     */
    public ProxyMethodInvocator(Object implementsObject, Class interfaceName){
        proxy = Proxy.newProxyInstance(interfaceName.getClassLoader(),
            new Class[] { interfaceName }, this);
        original = implementsObject;
    }
    
    /**
     * プロクシオブジェクトを取得します
     * @return プロクシオブジェクト
     */
    public Object getProxy(){
        return proxy;
    }
    
    /**
     * Proxyオブジェクトはクライアントからの呼び出しを受けると
     * InvocationHandler#invokeメソッドを呼び出します
     *
     * <li>Proxyオブジェクト
     * 同一ハンドラで複数のProxyオブジェクトの通知を
     * 受ける時の識別に利用できます
     *
     * <li>Methodオブジェクト
     * クライアントが呼び出したメソッド
     *
     * <li>Object配列
     * クライアントがメソッドに与えた引数の配列
     *
     * の3つです。また戻り値は、クライアントが呼び出したメソッドの戻り値と、
     * 型を一致させる必要があります。
     * もしvoid型メソッドの場合にはnullを返します。
     * この時、戻り値の型が一致しないとClassCastExceptionがスローされます。
     * また、invokeメソッドからスローした例外が呼び出されたメソッドで宣言
     * していないチェック例外の場合にはUndeclaredThrowableExceptionが
     * スローされます。
     *
     */
    //TODO これでいいのか??(synchronized Object invoke())
    public synchronized Object invoke(Object proxy, Method method, Object[] args)
    //public Object invoke(Object proxy, Method method, Object[] args)
    throws Throwable {
        try {
            this.args = args;
            this.method = method;
            List handlers = adviceMap.getList(method.getName());
            int size = handlers.size();
            if(size==0){
                return method.invoke(original, args);
            }
            if(chainCounter < size){
                ProxyMethodHandler handler = (ProxyMethodHandler)handlers.get(chainCounter);
                result = handler.invoke(this);
            }
            return result;
        } catch (InvocationTargetException e) {
            chainCounter = 0;
            if (e.getCause() != null) {
                throw e.getCause();
            }
            throw e;
        }
    }
    
    /**
     * オブジェクトの呼出もとのメソッドを実行します
     * @return オブジェクトの実行結果を返します
     * @throws Throwable
     */
    public Object proceed() throws Throwable {
        try {
            chainCounter ++;
            List handlers = adviceMap.getList(method.getName());
            int size = handlers.size();
            if(chainCounter<size){
                ProxyMethodHandler handler = (ProxyMethodHandler)handlers.get(chainCounter);
                return handler.invoke(this);
            }else{
                chainCounter = 0;
                return method.invoke(original, args);
            }
        } catch (InvocationTargetException e) {
            chainCounter = 0;
            if (e.getCause() != null) {
                throw e.getCause();
            }
            throw e;
        }
    }
    
    /**
     * Adviceを追加します<br>
     * 引数"methodName"に指定されたメソッドが呼ばれた場合、ProxyMethodHandler
     * を実装したオブジェクト(のinvoke()メソッド)が実行されます
     * @param methodName 追加するAdviceに紐づくメソッド名を指定します
     * @param handler Adviceを実装したオブジェクトを指定します
     */
    public void addAdvice(String methodName, ProxyMethodHandler handler){
        adviceMap.put(methodName, handler);
    }
    
    /**
     * Adviceを追加します<br>
     * プロクシオブジェクトのメソッドが呼ばれた場合、ProxyMethodHandler
     * を実装したオブジェクト(のinvoke()メソッド)が実行されます
     * @param handler Adviceを実装したオブジェクトを指定します
     */
    public void addAdvice(ProxyMethodHandler handler){
        adviceMap.put(handler.getClass().getName(), handler);
    }
    
    /**
     * プロクシオブジェクトによって呼び出されたメソッドの引数を取得します
     * @return プロクシオブジェクトによって呼び出されたメソッドの引数
     */
    public Object[] getArgs() {
        return args;
    }
    
    /**
     * プロクシオブジェクトが呼び出すメソッドの引数を設定します
     * @param args プロクシオブジェクトが呼び出すメソッドの引数
     */
    public void setArgs(Object[] args) {
        this.args = args;
    }
    
    /**
     * 実オブジェクトを取得します
     * @return 実オブジェクト
     */
    public Object getOriginal(){
        return original;
    }
}

