/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.openejb.core.interceptor;

import org.apache.openejb.core.Operation;
import org.apache.openejb.util.Classes;

import javax.interceptor.InvocationContext;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

/**
 * @version $Rev$ $Date$
 */
public class ReflectionInvocationContext implements InvocationContext {
    private final Iterator<Interceptor> interceptors;
    private final Object target;
    private final Method method;
    private final Object[] parameters;
    private final Map<String, Object> contextData = new TreeMap<String, Object>();
    private final Class<?>[] parameterTypes;

    private final Operation operation;

    public ReflectionInvocationContext(final Operation operation, final List<Interceptor> interceptors, final Object target, final Method method, final Object... parameters) {
        if (operation == null) {
            throw new NullPointerException("operation is null");
        }
        if (interceptors == null) {
            throw new NullPointerException("interceptors is null");
        }
        if (target == null) {
            throw new NullPointerException("target is null");
        }

        this.operation = operation;
        this.interceptors = interceptors.iterator();
        this.target = target;
        this.method = method;
        this.parameters = parameters;

        if (method == null) {
            parameterTypes = new Class[0];
        } else {
            parameterTypes = method.getParameterTypes();
        }
    }

    public Object getTimer() {
        if (operation.equals(Operation.TIMEOUT)) {
            return parameters[0];
        }
        return null;
    }

    public Object getTarget() {
        return target;
    }

    public Method getMethod() {
        return method;
    }

    public Object[] getParameters() {
        //TODO Need to figure out what is going on with afterCompletion call back here ?
        if (Operation.POST_CONSTRUCT.equals(operation) || Operation.PRE_DESTROY.equals(operation)) {
            //if (operation.isCallback() && !operation.equals(Operation.AFTER_COMPLETION) && !operation.equals(Operation.TIMEOUT)) {
            throw new IllegalStateException(getIllegalParameterAccessMessage());
        }
        return this.parameters;
    }

    private String getIllegalParameterAccessMessage() {
        String m = "Callback methods cannot access parameters.";
        m += "  Callback Type: " + operation;
        if (method != null) {
            m += ", Target Method: " + method.getName();
        }
        if (target != null) {
            m += ", Target Bean: " + target.getClass().getName();
        }
        return m;
    }

    public void setParameters(final Object[] parameters) {
        if (operation.isCallback() && !operation.equals(Operation.TIMEOUT)) {
            throw new IllegalStateException(getIllegalParameterAccessMessage());
        }
        if (parameters == null) {
            throw new IllegalArgumentException("parameters is null");
        }
        if (parameters.length != this.parameters.length) {
            throw new IllegalArgumentException("Expected " + this.parameters.length + " parameters, but only got " + parameters.length + " parameters");
        }
        for (int i = 0; i < parameters.length; i++) {
            final Object parameter = parameters[i];
            final Class<?> parameterType = parameterTypes[i];

            if (parameter == null) {
                if (parameterType.isPrimitive()) {
                    throw new IllegalArgumentException("Expected parameter " + i + " to be primitive type " + parameterType.getName() +
                        ", but got a parameter that is null");
                }
            } else {
                //check that types are applicable
                final Class<?> actual = Classes.deprimitivize(parameterType);
                final Class<?> given = Classes.deprimitivize(parameter.getClass());

                if (!actual.isAssignableFrom(given)) {
                    throw new IllegalArgumentException("Expected parameter " + i + " to be of type " + parameterType.getName() +
                        ", but got a parameter of type " + parameter.getClass().getName());
                }
            }
        }
        System.arraycopy(parameters, 0, this.parameters, 0, parameters.length);
    }

    public Map<String, Object> getContextData() {
        return contextData;
    }

    private Invocation next() {
        if (interceptors.hasNext()) {
            final Interceptor interceptor = interceptors.next();
            final Object nextInstance = interceptor.getInstance();
            final Method nextMethod = interceptor.getMethod();

            if (nextMethod.getParameterTypes().length == 1 && nextMethod.getParameterTypes()[0] == InvocationContext.class) {
                return new InterceptorInvocation(nextInstance, nextMethod, this);
            } else {
                return new LifecycleInvocation(nextInstance, nextMethod, this, parameters);
            }
        } else if (method != null) {
            //EJB 3.1, it is allowed that timeout method does not have parameter Timer.class,
            //However, while invoking the timeout method, the timer value is passed, as it is also required by InnvocationContext.getTimer() method
            final Object[] methodParameters;
            if (operation.equals(Operation.TIMEOUT) && method.getParameterTypes().length == 0) {
                methodParameters = new Object[0];
            } else {
                methodParameters = parameters;
            }
            return new BeanInvocation(target, method, methodParameters);
        } else {
            return new NoOpInvocation();
        }
    }

    public Object proceed() throws Exception {
        // The bulk of the logic of this method has intentionally been moved
        // out so stepping through a large stack in a debugger can be done quickly.
        // Simply put one break point on 'next.invoke()' or one inside that method.
        try {
            final Invocation next = next();
            return next.invoke();
        } catch (final InvocationTargetException e) {
            throw unwrapInvocationTargetException(e);
        }
    }

    private abstract static class Invocation {
        private final Method method;
        private final Object[] args;
        private final Object target;

        public Invocation(final Object target, final Method method, final Object[] args) {
            this.target = target;
            this.method = method;
            this.args = args;
        }

        public Object invoke() throws Exception {

            return method.invoke(target, args);
        }


        public String toString() {
            return method.getDeclaringClass().getName() + "." + method.getName();
        }
    }

    private static class BeanInvocation extends Invocation {
        public BeanInvocation(final Object target, final Method method, final Object[] args) {
            super(target, method, args);
        }
    }

    private static class InterceptorInvocation extends Invocation {
        public InterceptorInvocation(final Object target, final Method method, final InvocationContext invocationContext) {
            super(target, method, new Object[]{invocationContext});
        }
    }

    private static class LifecycleInvocation extends Invocation {
        private final InvocationContext invocationContext;

        public LifecycleInvocation(final Object target, final Method method, final InvocationContext invocationContext, final Object[] args) {
            super(target, method, args);
            this.invocationContext = invocationContext;
        }

        public Object invoke() throws Exception {
            // invoke the callback
            super.invoke();

            // we need to call proceed so callbacks in subclasses get invoked
            return invocationContext.proceed();
        }
    }

    private static class NoOpInvocation extends Invocation {
        public NoOpInvocation() {
            super(null, null, null);
        }

        public Object invoke() throws IllegalAccessException, InvocationTargetException {
            return null;
        }
    }

    // todo verify excpetion types

    /**
     * Business method interceptors can only throw exception allowed by the target business method.
     * Lifecycle interceptors can only throw RuntimeException.
     *
     * @param e the invocation target exception of a reflection method invoke
     * @return the cause of the exception
     * @throws AssertionError if the cause is not an Exception or Error.
     */
    private Exception unwrapInvocationTargetException(final InvocationTargetException e) {
        final Throwable cause = e.getCause();
        if (cause == null) {
            return e;
        } else if (cause instanceof Exception) {
            return (Exception) cause;
        } else if (cause instanceof Error) {
            throw (Error) cause;
        } else {
            throw new AssertionError(cause);
        }
    }

    public String toString() {
        final String methodName = method != null ? method.getName() : null;

        return "InvocationContext(operation=" + operation + ", target=" + target.getClass().getName() + ", method=" + methodName + ")";
    }
}
