/*
 *  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.isis.core.metamodel.facets;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;

import org.apache.isis.applib.FatalException;
import org.apache.isis.applib.Identifier;
import org.apache.isis.applib.services.command.Command;
import org.apache.isis.applib.services.command.Command2;
import org.apache.isis.applib.services.command.Command3;
import org.apache.isis.applib.services.eventbus.AbstractDomainEvent;
import org.apache.isis.applib.services.eventbus.AbstractInteractionEvent;
import org.apache.isis.applib.services.eventbus.ActionDomainEvent;
import org.apache.isis.applib.services.eventbus.ActionInteractionEvent;
import org.apache.isis.applib.services.eventbus.CollectionDomainEvent;
import org.apache.isis.applib.services.eventbus.EventBusService;
import org.apache.isis.applib.services.eventbus.PropertyDomainEvent;
import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
import org.apache.isis.core.metamodel.facetapi.IdentifiedHolder;
import org.apache.isis.core.metamodel.services.ServicesInjector;
import org.apache.isis.core.metamodel.spec.feature.ObjectAction;
import org.apache.isis.core.metamodel.spec.feature.ObjectActionParameter;

public class DomainEventHelper {

    private final ServicesInjector servicesInjector;


    public DomainEventHelper(final ServicesInjector servicesInjector) {
        this.servicesInjector = servicesInjector;
    }

    //region > postEventForAction, newActionDomainEvent
    @SuppressWarnings({ "rawtypes" })
    public ActionDomainEvent<?> postEventForAction(
            final AbstractDomainEvent.Phase phase,
            final Class eventType,
            final ActionDomainEvent<?> existingEvent,
            final ObjectAction objectAction,
            final IdentifiedHolder identified,
            final ObjectAdapter targetAdapter,
            final ObjectAdapter mixedInAdapter,
            final ObjectAdapter[] argumentAdapters,
            final Command command,
            final ObjectAdapter resultAdapter) {

        try {
            final ActionDomainEvent<?> event;

            if (existingEvent != null && phase.isExecuted()) {
                // reuse existing event from the executing phase
                event = existingEvent;
            } else {
                // all other phases, create a new event
                final Object source = ObjectAdapter.Util.unwrap(targetAdapter);
                final Object[] arguments = ObjectAdapter.Util.unwrap(argumentAdapters);
                final Identifier identifier = identified.getIdentifier();
                event = newActionDomainEvent(eventType, identifier, source, arguments);

                // copy over if have
                if(mixedInAdapter != null ) {
                    event.setMixedIn(mixedInAdapter.getObject());
                }

                if(objectAction != null) {
                    // should always be the case...
                    event.setActionSemantics(objectAction.getSemantics());

                    final List<ObjectActionParameter> parameters = objectAction.getParameters();
                    event.setParameterNames(immutableList(Iterables.transform(parameters, ObjectActionParameter.Functions.GET_NAME)));
                    event.setParameterTypes(immutableList(Iterables.transform(parameters, ObjectActionParameter.Functions.GET_TYPE)));
                }
            }

            event.setEventPhase(phase);
            event.setPhase(AbstractInteractionEvent.Phase.from(phase));

            if(phase.isExecuted()) {
                event.setReturnValue(ObjectAdapter.Util.unwrap(resultAdapter));
            }

            // associate event with command...
            if(command != null) {
                event.setCommand(command);
            }
            // ... and associate command with event
            if(command != null) {
                if(phase.isExecuting()) {
                    if(command instanceof Command3) {
                        final Command3 command3 = (Command3) command;
                        command3.pushActionDomainEvent(event);
                    } else if(command instanceof Command2 && event instanceof ActionInteractionEvent) {
                        final Command2 command2 = (Command3) command;
                        final ActionInteractionEvent<?> aie = (ActionInteractionEvent<?>) event;
                        command2.pushActionInteractionEvent(aie);
                    }
                }
            }

            getEventBusService().post(event);
            return event;
        } catch (Exception e) {
            throw new FatalException(e);
        }
    }

    private static <T> List<T> immutableList(final Iterable<T> iterable) {
        return Collections.unmodifiableList(Lists.newArrayList(iterable));
    }

    @SuppressWarnings("unchecked")
    static <S> ActionDomainEvent<S> newActionDomainEvent(
            final Class<? extends ActionDomainEvent<S>> type,
            final Identifier identifier,
            final S source,
            final Object... arguments) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException {

        final Constructor<?>[] constructors = type.getConstructors();

        // no-arg constructor
        for (final Constructor<?> constructor : constructors) {
            final Class<?>[] parameterTypes = constructor.getParameterTypes();
            if(parameterTypes.length == 0) {
                final Object event = constructor.newInstance();
                final ActionDomainEvent<S> ade = (ActionDomainEvent<S>) event;

                ade.setSource(source);
                ade.setIdentifier(identifier);
                ade.setArguments(asList(arguments));
                return ade;
            }
        }


        for (final Constructor<?> constructor : constructors) {
            final Class<?>[] parameterTypes = constructor.getParameterTypes();
            if(parameterTypes.length != 3) {
                continue;
            }
            if(!parameterTypes[0].isAssignableFrom(source.getClass())) {
                continue;
            }
            if(!parameterTypes[1].isAssignableFrom(Identifier.class)) {
                continue;
            }
            if(!parameterTypes[2].isAssignableFrom(Object[].class)) {
                continue;
            }
            final Object event = constructor.newInstance(source, identifier, arguments);
            return (ActionDomainEvent<S>) event;
        }
        throw new NoSuchMethodException(type.getName()+".<init>(? super " + source.getClass().getName() + ", " + Identifier.class.getName() + ", [Ljava.lang.Object;)");
    }

    // same as in ActionDomainEvent's constructor.
    private static List<Object> asList(final Object[] arguments) {
        return arguments != null
                ? Arrays.asList(arguments)
                : Collections.emptyList();
    }
    //endregion

    //region > postEventForProperty, newPropertyInteraction
    public PropertyDomainEvent<?, ?> postEventForProperty(
            final AbstractDomainEvent.Phase phase,
            final Class eventType,
            final PropertyDomainEvent<?, ?> existingEvent,
            final IdentifiedHolder identified,
            final ObjectAdapter targetAdapter,
            final Object oldValue,
            final Object newValue) {

        try {
            final PropertyDomainEvent<?, ?> event;
            final Object source = ObjectAdapter.Util.unwrap(targetAdapter);
            final Identifier identifier = identified.getIdentifier();

            if(existingEvent != null && phase.isExecuted()) {
                // reuse existing event from the executing phase
                event = existingEvent;
            } else {
                // all other phases, create a new event
                event = newPropertyDomainEvent(eventType, identifier, source, oldValue, newValue);
            }

            event.setEventPhase(phase);
            event.setPhase(AbstractInteractionEvent.Phase.from(phase));

            // just in case the actual new value held by the object is different from that applied
            setEventNewValue(event, newValue);

            this.getEventBusService().post(event);
            return event;
        } catch (Exception e) {
            throw new FatalException(e);
        }
    }

    private static <S,T> void setEventNewValue(PropertyDomainEvent<S, T> event, Object newValue) {
        event.setNewValue((T) newValue);
    }

    @SuppressWarnings("unchecked")
    static <S,T> PropertyDomainEvent<S,T> newPropertyDomainEvent(
            final Class<? extends PropertyDomainEvent<S, T>> type,
            final Identifier identifier,
            final S source,
            final T oldValue,
            final T newValue) throws NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException  {

        final Constructor<?>[] constructors = type.getConstructors();

        // no-arg constructor
        for (final Constructor<?> constructor : constructors) {
            final Class<?>[] parameterTypes = constructor.getParameterTypes();
            if(parameterTypes.length == 0) {
                final Object event = constructor.newInstance();
                final PropertyDomainEvent<S, T> pde = (PropertyDomainEvent<S, T>) event;
                pde.setSource(source);
                pde.setIdentifier(identifier);
                pde.setOldValue(oldValue);
                pde.setNewValue(newValue);
                return pde;
            }
        }

        // else
        for (final Constructor<?> constructor : constructors) {
            final Class<?>[] parameterTypes = constructor.getParameterTypes();
            if(parameterTypes.length != 4) {
                continue;
            }
            if(!parameterTypes[0].isAssignableFrom(source.getClass())) {
                continue;
            }
            if(!parameterTypes[1].isAssignableFrom(Identifier.class)) {
                continue;
            }
            if(oldValue != null && !parameterTypes[2].isAssignableFrom(oldValue.getClass())) {
                continue;
            }
            if(newValue != null && !parameterTypes[3].isAssignableFrom(newValue.getClass())) {
                continue;
            }
            final Object event = constructor.newInstance(source, identifier, oldValue, newValue);
            return (PropertyDomainEvent<S, T>) event;
        }

        throw new NoSuchMethodException(type.getName()+".<init>(? super " + source.getClass().getName() + ", " + Identifier.class.getName() + ", java.lang.Object, java.lang.Object)");
    }
    //endregion

    //region > postEventForCollection, newCollectionDomainEvent

    public CollectionDomainEvent<?, ?> postEventForCollection(
            AbstractDomainEvent.Phase phase,
            final Class eventType,
            final CollectionDomainEvent<?, ?> existingEvent,
            final IdentifiedHolder identified,
            final ObjectAdapter targetAdapter,
            final CollectionDomainEvent.Of of,
            final Object reference) {
        try {
            final CollectionDomainEvent<?, ?> event;
            if (existingEvent != null && phase.isExecuted()) {
                // reuse existing event from the executing phase
                event = existingEvent;
            } else {
                // all other phases, create a new event
                final Object source = ObjectAdapter.Util.unwrap(targetAdapter);
                final Identifier identifier = identified.getIdentifier();
                event = newCollectionDomainEvent(eventType, phase, identifier, source, of, reference);
            }

            event.setEventPhase(phase);
            event.setPhase(AbstractInteractionEvent.Phase.from(phase));

            getEventBusService().post(event);
            return event;
        } catch (Exception e) {
            throw new FatalException(e);
        }
    }

    @SuppressWarnings("unchecked")
    <S, T> CollectionDomainEvent<S, T> newCollectionDomainEvent(
            final Class<? extends CollectionDomainEvent<S, T>> type,
            final AbstractDomainEvent.Phase phase,
            final Identifier identifier,
            final S source,
            final CollectionDomainEvent.Of of,
            final T value)
            throws NoSuchMethodException, SecurityException, InstantiationException,
            IllegalAccessException, IllegalArgumentException, InvocationTargetException {

        final Constructor<?>[] constructors = type.getConstructors();

        // no-arg constructor
        for (final Constructor<?> constructor : constructors) {
            final Class<?>[] parameterTypes = constructor.getParameterTypes();
            if(parameterTypes.length ==0) {
                final Object event = constructor.newInstance();
                final CollectionDomainEvent<S, T> cde = (CollectionDomainEvent<S, T>) event;

                cde.setSource(source);
                cde.setIdentifier(identifier);
                cde.setOf(of);
                cde.setValue(value);
                return cde;
            }
        }

        // search for constructor accepting source, identifier, type, value
        for (final Constructor<?> constructor : constructors) {
            final Class<?>[] parameterTypes = constructor.getParameterTypes();
            if(parameterTypes.length != 4) {
                continue;
            }
            if(!parameterTypes[0].isAssignableFrom(source.getClass())) {
                continue;
            }
            if(!parameterTypes[1].isAssignableFrom(Identifier.class)) {
                continue;
            }
            if(!parameterTypes[2].isAssignableFrom(CollectionDomainEvent.Of.class)) {
                continue;
            }
            if(value != null && !parameterTypes[3].isAssignableFrom(value.getClass())) {
                continue;
            }
            final Object event = constructor.newInstance(source, identifier, of, value);
            return (CollectionDomainEvent<S, T>) event;
        }

        if(phase == AbstractDomainEvent.Phase.EXECUTED) {
            if(of == CollectionDomainEvent.Of.ADD_TO) {
                // support for @PostsCollectionAddedTo annotation:
                // search for constructor accepting source, identifier, value
                for (final Constructor<?> constructor : constructors) {
                    final Class<?>[] parameterTypes = constructor.getParameterTypes();
                    if(parameterTypes.length != 3) {
                        continue;
                    }
                    if(!parameterTypes[0].isAssignableFrom(source.getClass())) {
                        continue;
                    }
                    if(!parameterTypes[1].isAssignableFrom(Identifier.class)) {
                        continue;
                    }
                    if(value != null && !parameterTypes[2].isAssignableFrom(value.getClass())) {
                        continue;
                    }
                    final Object event = constructor.newInstance(source, identifier, value);
                    return (CollectionDomainEvent<S, T>) event;
                }
            } else if(of == CollectionDomainEvent.Of.REMOVE_FROM) {
                // support for @PostsCollectionRemovedFrom annotation:
                // search for constructor accepting source, identifier, value
                for (final Constructor<?> constructor : constructors) {
                    final Class<?>[] parameterTypes = constructor.getParameterTypes();
                    if(parameterTypes.length != 3) {
                        continue;
                    }
                    if(!parameterTypes[0].isAssignableFrom(source.getClass())) {
                        continue;
                    }
                    if(!parameterTypes[1].isAssignableFrom(Identifier.class)) {
                        continue;
                    }
                    if(value != null && !parameterTypes[2].isAssignableFrom(value.getClass())) {
                        continue;
                    }
                    final Object event = constructor.newInstance(
                            source, identifier, value);
                    return (CollectionDomainEvent<S, T>) event;
                }
            }
        }
        throw new NoSuchMethodException(type.getName()+".<init>(? super " + source.getClass().getName() + ", " + Identifier.class.getName() + ", java.lang.Object)");
    }

    //endregion


    //region > eventBusService

    private EventBusService getEventBusService() {
        // previously this method used to cache, however it prevents integration tests
        // from switching out the EventBusService with a mock.
        return this.servicesInjector.lookupService(EventBusService.class);
    }

    //endregion

}
