/*
 * Copyright 2010 Werner Guttmann
 *
 * Licensed 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.castor.cpa.jpa.info;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.exolab.castor.jdo.Database;
import org.exolab.castor.mapping.AccessMode;
import org.exolab.castor.persist.spi.CallbackInterceptor;

import javax.persistence.*;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Handles JPA annotation-driven callback hooks.
 */
public class JPACallbackHandler implements CallbackInterceptor {

    private static final Log LOG = LogFactory.getLog(JPACallbackHandler.class);

    /**
     * Objects for which callbacks need to be invoked.
     */
    private final List<Object> objectsToInvokeCallbacksOn = new ArrayList<Object>();

    /**
     * Memorises overridden callbacks.
     */
    private final Map<String, Object> overriddenCallbacks = new HashMap<String, Object>();

    /**
     * Memorises if superclass listeners are to be excluded.
     */
    private boolean excludeSuperclassListeners = false;

    public Class<?> loaded(final Object object, final AccessMode accessMode)
            throws Exception {
        if (LOG.isDebugEnabled()) {
            LOG.debug("Calling `loaded`.");
        }
        handleCallbacksFor(PostLoad.class, object);
        return object.getClass();
    }

    public void modifying(final Object object) throws Exception {
        if (LOG.isDebugEnabled()) {
            LOG.debug("Calling `modifying`.");
        }
        handleCallbacksFor(PreUpdate.class, object);
    }

    public void storing(final Object object, final boolean modified)
            throws Exception {
        if (LOG.isDebugEnabled()) {
            LOG.debug("Calling `storing`.");
        }
        if (modified) {
            handleCallbacksFor(PostUpdate.class, object);
        }
    }

    public void creating(final Object object, final Database db)
            throws Exception {
        if (LOG.isDebugEnabled()) {
            LOG.debug("Calling `creating`.");
        }
        handleCallbacksFor(PrePersist.class, object);
    }

    public void created(final Object object) throws Exception {
        if (LOG.isDebugEnabled()) {
            LOG.debug("Calling `created`.");
        }
        handleCallbacksFor(PostPersist.class, object);
    }

    public void removing(final Object object) throws Exception {
        if (LOG.isDebugEnabled()) {
            LOG.debug("Calling `removing`.");
        }
        handleCallbacksFor(PreRemove.class, object);
    }

    public void removed(final Object object) throws Exception {
        if (LOG.isDebugEnabled()) {
            LOG.debug("Calling `removed`.");
        }
        handleCallbacksFor(PostRemove.class, object);
    }

    public void releasing(final Object object, final boolean committed) {
        if (LOG.isDebugEnabled()) {
            LOG.debug("Calling `releasing`.");
        }
        // TODO: impl.?
    }

    public void using(final Object object, final Database db) {
        if (LOG.isDebugEnabled()) {
            LOG.debug("Calling `using`.");
        }
        // TODO: impl.?
    }

    public void updated(final Object object) throws Exception {
        if (LOG.isDebugEnabled()) {
            LOG.debug("Calling `updated`.");
        }
        // TODO: impl.?
    }

    /**
     * Handles callbacks accordingly.
     * 
     * @param annotationClass
     *            the annotation to look for
     * @param object
     *            the object to handle callbacks for
     * @param <A>
     *            helper annotation generics
     * @throws InvocationTargetException
     *             on callback invocation error
     * @throws IllegalAccessException
     *             on illegal method access error
     * @throws InstantiationException
     *             on instantiation error
     * @throws NoSuchMethodException
     *             on method not present error
     */
    private <A extends Annotation> void handleCallbacksFor(
            final Class<A> annotationClass, final Object object)
            throws InvocationTargetException, IllegalAccessException,
            InstantiationException, NoSuchMethodException {
        objectsToInvokeCallbacksOn.clear();
        overriddenCallbacks.clear();
        excludeSuperclassListeners = false;
        walkCallbacksHierarchyFor(annotationClass, object);
        if (objectsToInvokeCallbacksOn.size() > 1) {
            handleOverriddenCallbacksFor(annotationClass,
                    objectsToInvokeCallbacksOn.get(objectsToInvokeCallbacksOn
                            .size() - 1));
        }
        for (Object obj : objectsToInvokeCallbacksOn) {
            invokeCallbacksFor(annotationClass, obj);
        }
        for (Map.Entry<String, Object> entry : overriddenCallbacks.entrySet()) {
            invokeCallback(entry.getValue().getClass().getDeclaredMethod(
                    entry.getKey()), entry.getValue());
        }
    }

    /**
     * Walks callbacks hierarchy accordingly.
     * 
     * @param annotationClass
     *            the annotation to look for
     * @param object
     *            the object to walk hierarchy for
     * @param <A>
     *            helper annotation generics
     * @throws InvocationTargetException
     *             on callback invocation error
     * @throws IllegalAccessException
     *             on illegal method access error
     * @throws InstantiationException
     *             on instantiation error
     */
    private <A extends Annotation> void walkCallbacksHierarchyFor(
            final Class<A> annotationClass, final Object object)
            throws InvocationTargetException, IllegalAccessException,
            InstantiationException {
        final Class<?> klass = object.getClass();
        if (klass.isAnnotationPresent(Entity.class)) {
            final Class<?> superclass = klass.getSuperclass();
            if (superclass != Object.class) {
                walkCallbacksHierarchyFor(annotationClass, superclass
                        .newInstance());
            }
            if (!excludeSuperclassListeners) {
                invokeListenerCallbacksFor(annotationClass, klass);
            }
            if (klass.isAnnotationPresent(ExcludeSuperclassListeners.class)) {
                excludeSuperclassListeners = true;
            }
            objectsToInvokeCallbacksOn.add(object);
        }
    }

    /**
     * Invokes listener callbacks accordingly.
     * 
     * @param annotationClass
     *            the annotation to look for
     * @param klass
     *            the class to handle listeners
     * @param <A>
     *            helper annotation generics
     * @throws InvocationTargetException
     *             on callback invocation error
     * @throws IllegalAccessException
     *             on illegal method access error
     * @throws InstantiationException
     *             on instantiation error
     */
    private <A extends Annotation> void invokeListenerCallbacksFor(
            final Class<A> annotationClass, final Class<?> klass)
            throws InvocationTargetException, IllegalAccessException,
            InstantiationException {
        if (!klass.isAnnotationPresent(ExcludeDefaultListeners.class)) {
            final EntityListeners entityListeners = klass
                    .getAnnotation(EntityListeners.class);
            if (entityListeners != null) {
                final Class<?>[] listeners = entityListeners.value();
                for (Class<?> listener : listeners) {
                    invokeCallbacksFor(annotationClass, listener.newInstance());
                }
            }
        }
    }

    /**
     * Handles overridden CB methods accordingly.
     * 
     * @param annotationClass
     *            the annotation to look for
     * @param object
     *            the object to handle
     * @throws InvocationTargetException
     *             on callback invocation error
     * @throws IllegalAccessException
     *             on illegal method access error
     */
    private <A extends Annotation> void handleOverriddenCallbacksFor(
            final Class<A> annotationClass, final Object object)
            throws InvocationTargetException, IllegalAccessException {
        Class<?> klass = object.getClass();
        Class<?> superclass = klass.getSuperclass();
        while (superclass != Object.class) {
            for (Method method : klass.getDeclaredMethods()) {
                if (method.isAnnotationPresent(annotationClass)) {
                    final String methodName = method.getName();
                    try {
                        final Method overridden = superclass
                                .getDeclaredMethod(methodName);
                        if (overridden.isAnnotationPresent(annotationClass)) {
                            overriddenCallbacks.put(methodName, object);
                        }
                    } catch (NoSuchMethodException e) {
                        if (LOG.isDebugEnabled()) {
                            LOG
                                    .debug(String
                                            .format(
                                                    "CB method `%s` is not overridden in `%s`.",
                                                    method.getName(),
                                                    superclass.getSimpleName()));
                        }
                    }
                }
            }
            superclass = superclass.getSuperclass();
        }
    }

    /**
     * Invokes callback methods accordingly.
     * 
     * @param annotationClass
     *            the annotation to look for
     * @param object
     *            the object to invoke callbacks on
     * @param <A>
     *            helper annotation generics
     * @throws InvocationTargetException
     *             on callback invocation error
     * @throws IllegalAccessException
     *             on illegal method access error
     */
    private <A extends Annotation> void invokeCallbacksFor(
            final Class<A> annotationClass, final Object object)
            throws InvocationTargetException, IllegalAccessException {
        final Class<?> klass = object.getClass();
        final Method[] declaredMethods = klass.getDeclaredMethods();
        for (Method method : declaredMethods) {
            if (method.isAnnotationPresent(annotationClass)) {
                if (!overriddenCallbacks.containsKey(method.getName())) {
                    invokeCallback(method, object);
                }
            }
        }
    }

    /**
     * Invokes a callback method accordingly.
     * 
     * @param method
     *            the CB method to invoke
     * @param object
     *            the object to invoke on
     * @throws InvocationTargetException
     *             on callback invocation error
     * @throws IllegalAccessException
     *             on illegal method access error
     */
    private void invokeCallback(final Method method, final Object object)
            throws InvocationTargetException, IllegalAccessException {
        if (LOG.isDebugEnabled()) {
            LOG.debug(String.format("Invoking CB method `%s` on `%s`.", method
                    .getName(), object.getClass().getSimpleName()));
        }
        method.setAccessible(true);
        method.invoke(object);
    }

}
