/*
 * 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.openjpa.event;

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.lang.reflect.Method;
import java.security.AccessController;
import java.util.Arrays;

import org.apache.openjpa.lib.util.J2DoPrivHelper;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.util.UserException;

/**
 * Callback adapter that invokes a callback method via reflection.
 *
 * @author Steve Kim
 */
public class MethodLifecycleCallbacks
    implements LifecycleCallbacks, Externalizable {

    private static final Localizer _loc = Localizer.forPackage
        (MethodLifecycleCallbacks.class);

    private transient Method _callback;
    private boolean _arg;

    /**
     * Constructor. Supply callback class and its callback method name.
     *
     * @param arg Whether we expect a further argument such as in AfterDetach
     */
    public MethodLifecycleCallbacks(Class cls, String method, boolean arg) {
        Class[] args = arg ? new Class[]{ Object.class } : null;
        _callback = getMethod(cls, method, args);
        _arg = arg;
    }

    /**
     * Constructor. Supply callback method.
     */
    public MethodLifecycleCallbacks(Method method, boolean arg) {
        _callback = method;
        _arg = arg;
    }

    /**
     * The callback method.
     */
    public Method getCallbackMethod() {
        return _callback;
    }

    /**
     * Returns if this callback expects another argument
     */
    public boolean requiresArgument() {
        return _arg;
    }

    @Override
    public boolean hasCallback(Object obj, int eventType) {
        return true;
    }

    @Override
    public void makeCallback(Object obj, Object arg, int eventType)
        throws Exception {
        if (!_callback.isAccessible())
            AccessController.doPrivileged(J2DoPrivHelper.setAccessibleAction(
                _callback, true));

        if (_arg)
            _callback.invoke(obj, new Object[]{ arg });
        else
            _callback.invoke(obj, (Object[]) null);
    }

    @Override
    public String toString() {
        return getClass().getName() + ":" + _callback;
    }

    /**
     * Helper method to return the named method of the given class, throwing
     * the proper exception on error.
     */
    protected static Method getMethod(Class cls, String method, Class[] args) {
        Class currentClass = cls;
        do {
            Method[] methods = (Method[]) AccessController.doPrivileged(
                J2DoPrivHelper.getDeclaredMethodsAction(currentClass));
            for (Method value : methods) {
                if (!method.equals(value.getName()))
                    continue;

                if (isAssignable(value.getParameterTypes(), args))
                    return value;
            }
        } while ((currentClass = currentClass.getSuperclass()) != null);

        // if we get here, no suitable method was found
        throw new UserException(_loc.get("method-notfound", cls.getName(),
                method, args == null ? null : Arrays.asList(args)));
	}

    /**
     * Returns true if all parameters in the from array are assignable
     * from the corresponding parameters of the to array.
     */
    private static boolean isAssignable(Class[] from, Class[] to) {
        if (from == null)
            return to == null || to.length == 0;
        if (to == null)
            return from == null || from.length == 0;

        if (from.length != to.length)
            return false;

        for (int i = 0; i < from.length; i++) {
            if (from[i] != null && !from[i].isAssignableFrom(to[i]))
                return false;
        }

        return true;
    }

    @Override
    public void readExternal(ObjectInput in)
        throws IOException, ClassNotFoundException {
        Class cls = (Class) in.readObject();
        String methName = (String) in.readObject();
        _arg = in.readBoolean();

        Class[] args = _arg ? new Class[]{ Object.class } : null;
        _callback = getMethod(cls, methName, args);
    }

    @Override
    public void writeExternal(ObjectOutput out)
        throws IOException {
        out.writeObject(_callback.getClass());
        out.writeObject(_callback.getName());
        out.writeBoolean(_arg);
    }
}
