/*
 * ReflectInvoker.java  $Revision: 1.27 $  $Date: 2007/10/03 04:59:32 $
 *
 * Copyright (C) 2007 Meldcraft Corporation
 *
 * This file is part of Scaffold.
 * 
 * Scaffold is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Scaffold 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.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with Scaffold.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.meldcraft.application;

import java.awt.Component;
import java.io.InterruptedIOException;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
import java.util.logging.Level;

import javax.swing.JComponent;
import javax.swing.SwingUtilities;

import com.meldcraft.application.util.BeanContext;
import com.meldcraft.application.util.BeanUtil;
import com.meldcraft.util.BasicThreadPool;
import com.meldcraft.util.CountDownTimer;
import com.meldcraft.util.PropertiesHelper;
import com.meldcraft.util.ReflectUtil;

/**
 * @version $Revision: 1.27 $ $Date: 2007/10/03 04:59:32 $
 * @author Rob Richards
 */
public class ReflectInvoker
{
    public static final String CONFIG_THROTTLE_KEY = "invokeThrottleMillis";

    private static final String TYPE_CURRENT = "current";
    private static final String TYPE_NEW_THREAD = "newThread";
    private static final String TYPE_NEW_THREAD_WAIT = "newThreadWait";
    private static final String TYPE_EDT_LATER = "edtLater";
    private static final String TYPE_EDT_WAIT = "edtWait";
    private static final String TYPE_EDT_NOW_ELSE_LATER = "edtNowElseLater";
    private static final String TYPE_POOL_WAIT = "poolWait";
    private static final String TYPE_POOL = "pool";

    private static final String EDT_COMPONENT = "EDTComponent";
    private static final String EDT_JCOMPONENT = "EDTJComponent";
    private static final String EDT_AWT = "EDTAWT";
    private static final String EDT_AWTSWING = "EDTAWTSWING";
    private static final String EDT_SWING = "EDTSwing";

    private static final String[] ALL_TYPES = new String[] {TYPE_CURRENT,
                                                            TYPE_NEW_THREAD,
                                                            TYPE_NEW_THREAD_WAIT,
                                                            TYPE_EDT_LATER,
                                                            TYPE_EDT_WAIT,
                                                            TYPE_EDT_NOW_ELSE_LATER,
                                                            TYPE_POOL_WAIT,
                                                            TYPE_POOL,
                                                            EDT_COMPONENT,
                                                            EDT_JCOMPONENT,
                                                            EDT_AWT,
                                                            EDT_AWTSWING,
                                                            EDT_SWING};
    protected ApplicationElementContext      mElementContext;
    private ApplicationContext      mApplicationContext;
    private String                  mElementId;
    protected String                mContextKey;
    protected String                mType;
    protected String                mEDTComponent;
    protected BasicThreadPool       mPool;
    protected Map                   mConfig;
    protected ThreadLocal           mThrottle = new ThreadLocal();

    protected ReflectInvoker(ApplicationElementContext context)
    {
        mElementContext = context;
        mApplicationContext = context.getApplicationContext();
        mElementId = context.getElementId();

        String elementId = context.getElementId();
        mConfig = context.getConfiguration();
        mContextKey = PropertiesHelper.getString(mConfig, "invokeReturnContextKey", elementId);

        String defType = PropertiesHelper.getString(context.getConfiguration(), "com.meldcraft.application.ReflectInvoker.defaultTargetThread", null);
        if (defType != null)
        {
            String type = null;
            for (int i=0 ; i < ALL_TYPES.length ; i++)
            {
                if (defType.equals(ALL_TYPES[i]))
                {
                    type = ALL_TYPES[i];
                    break;
                }
            }
            defType = type;
        }
        if (defType == null)
        {
            defType = EDT_AWTSWING;
        }
        
        mType = PropertiesHelper.getString(mConfig, "invokeTargetThread", defType).trim();
        
        String[] types = new String[]{EDT_COMPONENT, EDT_JCOMPONENT, EDT_AWT, EDT_AWTSWING, EDT_SWING};
        for (int i=0 ; i < types.length ; i++)
        {
            String type = types[i];
            if (mType.endsWith(type))
            {
                mEDTComponent = type;
                if (mType.length() > type.length())
                {
                    mType = mType.substring(0, mType.length() - type.length());
                }
                else
                {
                    mType = TYPE_CURRENT;
                }
                break;
            }
        }

        if (mType.equals(TYPE_POOL) || mType.equals(TYPE_POOL_WAIT))
        {
            int size = PropertiesHelper.getInt(mConfig, "invokeTargetThread.poolMaxThreads", -1);
            if (size <= 0)
            {
                size = 1;
            }

            boolean coalesce = PropertiesHelper.getBoolean(mConfig, "invokeTargetThread.poolCoalesce", false);
            mPool = new BasicThreadPool(size, getThreadName(), coalesce);
        }
    }

    public static ReflectInvoker createInstance(ApplicationElementContext context)
    {
        ReflectInvoker ret = null;

        String elem = PropertiesHelper.getString(context.getConfiguration(), "invoker", null);
        if (elem != null)
        {
            ReflectInvoker invoker = (ReflectInvoker)context.getApplicationContext().getElementManager().getApplicationElement(elem);
            if (invoker != null)
            {
                ret = invoker;
            }
        }

        if (ret == null)
        {
            ret = new ReflectInvoker(context);
        }

        return ret;
    }

    public InvokerStatus invokeMethod(Object target, String method, Object[] args, Object invokeContext)
    {
        InvokerStatus ret = null;

        if ((method != null) && !method.startsWith("<"))
        {
            int idx = method.lastIndexOf('.');
            if ((idx > 0) && (idx < method.length()-1))
            {
                Object obj = BeanUtil.getPropertyValue(target, method.substring(0, idx));
                if (obj != null)
                {
                    target = obj;
                    method = method.substring(idx+1);
                }
            }
        }

        Runner run = new Runner(this, target, method, args, invokeContext);
        String type = getTypeForTarget(target);
        if (type.equals(TYPE_POOL))
        {
            mPool.addTask(run);
        }
        else if (type.equals(TYPE_POOL_WAIT))
        {
            synchronized(run)
            {
                mPool.addTask(run);
                try
                {
                    run.wait();
                }
                catch (InterruptedException e)
                {
                    // expected
                }
                ret = new InvokerStatus(run.getResult(), run.getError());
            }
        }
        else if (type.equals(TYPE_EDT_NOW_ELSE_LATER))
        {
            if (SwingUtilities.isEventDispatchThread())
            {
                run.run();
                ret = new InvokerStatus(run.getResult(), run.getError());
            }
            else
            {
                SwingUtilities.invokeLater(run);
            }
        }
        else if (type.equals(TYPE_EDT_WAIT))
        {
            if (SwingUtilities.isEventDispatchThread())
            {
                run.run();
            }
            else
            {
                synchronized(run)
                {
                    SwingUtilities.invokeLater(run);
                    try
                    {
                        run.wait();
                    }
                    catch (InterruptedException e)
                    {
                        // expected
                    }
                }
            }
            ret = new InvokerStatus(run.getResult(), run.getError());
        }
        else if (type.equals(TYPE_EDT_LATER))
        {
            SwingUtilities.invokeLater(run);
        }
        else if (type.equals(TYPE_NEW_THREAD))
        {
            Thread thread = new Thread(run, getThreadName());
            thread.start();
        }
        else if (type.equals(TYPE_NEW_THREAD_WAIT))
        {
            Thread thread = new Thread(run, getThreadName());
            synchronized(run)
            {
                thread.start();
                try
                {
                    run.wait();
                }
                catch (InterruptedException e)
                {
                    // expected
                }
            }
            ret = new InvokerStatus(run.getResult(), run.getError());
        }
        else
        {
            run.run();
            ret = new InvokerStatus(run.getResult(), run.getError());
        }

        return ret;
    }

    protected String getTypeForTarget(Object target)
    {
        String type = mType;
        if ((mEDTComponent != null) && !type.startsWith("edt"))
        {
            boolean edt = false;
            if (mEDTComponent == EDT_COMPONENT)
            {
                if (target instanceof Component)
                {
                    edt = true;
                }
            }
            else if (mEDTComponent == EDT_JCOMPONENT)
            {
                if (target instanceof JComponent)
                {
                    edt = true;
                }
            }
            else if (mEDTComponent == EDT_AWT)
            {
                edt = classNameHas(target, "java.awt.");
            }
            else if (mEDTComponent == EDT_AWTSWING)
            {
                edt = classNameHas(target, "java.awt.") || classNameHas(target, "javax.swing.");
            }
            else if (mEDTComponent == EDT_SWING)
            {
                edt = classNameHas(target, "java.awt.");
            }

            if (edt)
            {
                boolean current = mType.equals(TYPE_CURRENT);
                if (!current || !SwingUtilities.isEventDispatchThread())
                {
                    mApplicationContext.getLogger().info(BasicApplicationLogger.basicLogString(this, mApplicationContext, mElementId + " using EDT thread override for " + target));
                    type = (mType.endsWith("Wait") || current) ? TYPE_EDT_WAIT : TYPE_EDT_LATER;
                }
            }
        }
        return type;
    }

    protected String getThreadName()
    {
        return "ReflectInvoker-" + mElementId + "-Thread";
    }

    protected void preInvokeMethod(Object target, String method, Object[] args)
    {
        prePostCommon("invokeStart", target, method, args);

        long delay = PropertiesHelper.getLong(mConfig, CONFIG_THROTTLE_KEY, 0);
        if (delay > 0)
        {
            mThrottle.set(new CountDownTimer(delay));
        }
    }

    protected void postInvokeMethod(Object target, String method, Object[] args, Object result, Throwable error, boolean stopped)
    {
        try
        {
            if (stopped)
            {
                handlePostInvokeMethodStop(target, method, args, result, error);
            }
            else if (error != null)
            {
                handlePostInvokeMethodError(target, method, args, error);
            }
            else
            {
                handlePostInvokeMethodSuccess(target, method, args, result);
            }
        }
        finally
        {
            CountDownTimer timer = (CountDownTimer)mThrottle.get();
            if (timer != null)
            {
                timer.waitForCountDown();
            }

            prePostCommon("invokeEnd", target, method, args);
        }
    }

    protected void handlePostInvokeMethodSuccess(Object target, String method, Object[] args, Object result)
    {
        if (mApplicationContext.getLogger().isLoggable(Level.FINE))
        {
            mApplicationContext.getLogger().log(Level.FINE, "Called method: " + method + ",result=" + result);
        }

        mApplicationContext.contextChanged(mContextKey + ".invokeReturn", result);
    }

    protected void handlePostInvokeMethodError(Object target, String method, Object[] args, Throwable error)
    {
        //if (mContext.getLogger().isLoggable(Level.FINE))
        {
            mApplicationContext.getLogger().log(Level.WARNING, "Error calling method: " + method, error);
        }

        mApplicationContext.contextChanged(mContextKey + ".invokeError", error);
    }

    protected void handlePostInvokeMethodStop(Object target, String method, Object[] args, Object result, Throwable error)
    {
        if (mApplicationContext.getLogger().isLoggable(Level.FINE))
        {
            mApplicationContext.getLogger().log(Level.FINE, "Stopped method: " + method + ",result=" + result + ",error=" + error);
        }

        mApplicationContext.contextChanged(mContextKey + ".invokeStop", new Object[]{result, error});
    }

    private void prePostCommon(String prefix, Object target, String method, Object[] args)
    {
        Map config = PropertiesHelper.getSubset(mConfig, prefix + ".");

        long delay = PropertiesHelper.getLong(config, "delayMillis", 0);
        boolean pre = prefix.endsWith("Start");
        if (!pre && (delay > 0))
        {
            try
            {
                Thread.sleep(delay);
            }
            catch (Exception e)
            {
                // ignored
            }
        }

        String msg = PropertiesHelper.getString(config, prefix + ".contextMessage", null);
        if (msg != null)
        {
            String key = PropertiesHelper.getString(config, prefix + ".contextMessageKey", mElementId + "." + prefix);
            //String fmsg = MessageFormat.format(msg, new Object[]{target, method, Arrays.asList(args)});
            Object val = BeanUtil.translateValue(new BeanContext(mApplicationContext, this, target, args));
            mApplicationContext.contextChanged(key, val);
        }

        if (pre && (delay > 0))
        {
            try
            {
                Thread.sleep(delay);
            }
            catch (Exception e)
            {
                // ignored
            }
        }
    }

    private boolean classNameHas(Object target, String prefix)
    {
        boolean ret = false;
        Class cls = (target instanceof Class) ? (Class)target : target.getClass();
        while ((cls != null) && !(ret = cls.getName().startsWith(prefix)))
        {
            cls = cls.getSuperclass();
        }
        return ret;
    }

    public static class InvokerStatus
    {
        private Object      mResult;
        private Throwable   mException;

        public InvokerStatus(Object result, Throwable exception)
        {
            mResult = result;
            mException = exception;
        }

        public Throwable getException()
        {
            return mException;
        }

        public Object getResult()
        {
            return mResult;
        }
    }

    public static class Runner implements Runnable, ApplicationContextListener
    {
        private static long     sCounter;

        private ReflectInvoker  mInvoker;
        private Object          mTarget;
        private String          mMethod;
        private Object[]        mArgs;
        private Object          mInvokeContext;
        private boolean         mJobActionable;

        private Object          mJobLock = new Object();
        private boolean         mStop = false;
        private Thread          mJobThread = null;
        private boolean         mCompleted = false;

        protected Object        mResult;
        protected Throwable     mError;

        public Runner(ReflectInvoker invoker, Object target, String method, Object[] args, Object invokeContext)
        {
            mInvoker = invoker;
            mTarget = target;
            mMethod = method;
            mArgs = args;
            mInvokeContext = invokeContext;
            mJobActionable = PropertiesHelper.getBoolean(mInvoker.mApplicationContext.getConfiguration(), mInvoker.mElementId + ".jobActionable", false);

            if (mJobActionable)
            {
                mInvoker.mApplicationContext.addApplicationContextListener(this, mInvoker.mElementId + ".jobAction");
            }
        }

        public void applicationContextChanged(ApplicationContextEvent e)
        {
            Object value = e.getValue();
            if (value != null)
            {
                String str = String.valueOf(value).toLowerCase();
                if ("stop".equals(str))
                {
                    stop();
                }
            }
        }

        public Object getResult()
        {
            return mResult;
        }

        public Throwable getError()
        {
            return mError;
        }

        public void run()
        {
            try
            {
                long counter;
                synchronized(Runner.class)
                {
                    counter = sCounter++;
                }

                boolean doRun;
                synchronized (mJobLock)
                {
                    doRun = !mStop;
                }

                if (doRun)
                {
                    try
                    {
                        try
                        {
                            String key = mInvoker.mElementId + ".invokeStart";
                            InvokeContext invokeContext = new InvokeContext(counter, mInvokeContext);
                            mInvoker.mApplicationContext.contextChanged(key, invokeContext);
                        }
                        catch (Throwable e)
                        {
                            // ignored
                        }

                        doRun();
                    }
                    finally
                    {
                        try
                        {
                            String key = mInvoker.mElementId + ".invokeEnd";
                            InvokeContext invokeContext = new InvokeContext(counter, mInvokeContext, mCompleted);
                            mInvoker.mApplicationContext.contextChanged(key, invokeContext);
                        }
                        catch (Throwable e)
                        {
                            // ignored
                        }

                    }
                }
            }
            finally
            {
                synchronized(this)
                {
                    notifyAll();
                }
            }
        }

        private void doRun()
        {

            try
            {
                try
                {
                    // jobProgress.start
                    mInvoker.preInvokeMethod(mTarget, mMethod, mArgs);
                    try
                    {
                        synchronized (mJobLock)
                        {
                            mJobThread = Thread.currentThread();
                        }
                        if ((mMethod != null) && mMethod.startsWith("<"))
                        {
                            mResult = BeanUtil.translateValue(new BeanContext(mInvoker.mApplicationContext, mTarget, mArgs, mMethod));
                        }
                        else
                        {
                            mResult = ReflectUtil.invokeMethod(mTarget, mMethod, mArgs);
                        }
                    }
                    finally
                    {
                        Thread.interrupted();
                        synchronized (mJobLock)
                        {
                            mJobThread = null;
                        }
                    }
                }
                catch (Throwable e)
                {
                    boolean stop;
                    synchronized(mJobLock)
                    {
                        stop = mStop;
                    }
                    if (((e instanceof InterruptedException) ||
                         (e instanceof InterruptedIOException)) && stop)
                    {
                        // ignored
                    }
                    else if (e instanceof InvocationTargetException)
                    {
                        mError = ((InvocationTargetException)e).getCause();
                    }
                    else
                    {
                        mError = e;
                    }
                }
                finally
                {
                    Thread.interrupted();
                    boolean stop;
                    synchronized(mJobLock)
                    {
                        stop = mStop;
                    }
                    mCompleted = (mError == null) && !stop;
                    mInvoker.postInvokeMethod(mTarget, mMethod, mArgs, mResult, mError, stop);
                }
            }
            finally
            {
                if (mJobActionable)
                {
                    mInvoker.mApplicationContext.removeApplicationContextListener(this, mInvoker.mElementId + ".jobAction");
                }
            }
        }

        protected void stop()
        {
            synchronized(mJobLock)
            {
                mStop = true;
                Thread th = mJobThread;
                if (th != null)
                {
                    th.interrupt();
                }
            }
        }
    }

    public static class InvokeContext
    {
        private long    mId;
        private Object  mContext;
        private boolean mCompleted;

        public InvokeContext(long id, Object context)
        {
            this(id, context, false);
        }

        public InvokeContext(long id, Object context, boolean completed)
        {
            mId = id;
            mContext = context;
            mCompleted = completed;
        }

        public Object getContext()
        {
            return mContext;
        }

        public long getId()
        {
            return mId;
        }

        public boolean getCompleted()
        {
            return mCompleted;
        }

        public String toString()
        {
            return mId + "," + mCompleted + "," + String.valueOf(mContext);
        }
    }
}



