/*
 *  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.causeway.core.metamodel.facets.actions.action;

import java.util.Optional;

import jakarta.inject.Inject;

import org.apache.causeway.applib.annotation.Action;
import org.apache.causeway.applib.annotation.SemanticsOf;
import org.apache.causeway.applib.mixins.system.HasInteractionId;
import org.apache.causeway.commons.semantics.CollectionSemantics;
import org.apache.causeway.core.metamodel.context.MetaModelContext;
import org.apache.causeway.core.metamodel.facetapi.FeatureType;
import org.apache.causeway.core.metamodel.facets.FacetFactoryAbstract;
import org.apache.causeway.core.metamodel.facets.actcoll.typeof.TypeOfFacet;
import org.apache.causeway.core.metamodel.facets.actions.action.choicesfrom.ChoicesFromFacetForActionAnnotation;
import org.apache.causeway.core.metamodel.facets.actions.action.explicit.ActionExplicitFacetForActionAnnotation;
import org.apache.causeway.core.metamodel.facets.actions.action.invocation.ActionDomainEventFacet;
import org.apache.causeway.core.metamodel.facets.actions.action.invocation.ActionInvocationFacetForAction;
import org.apache.causeway.core.metamodel.facets.actions.action.invocation.ActionInvocationFacetForMixedInPropertyOrCollection;
import org.apache.causeway.core.metamodel.facets.actions.action.prototype.PrototypeFacetForActionAnnotation;
import org.apache.causeway.core.metamodel.facets.actions.action.typeof.TypeOfFacetForActionAnnotation;
import org.apache.causeway.core.metamodel.facets.actions.fileaccept.FileAcceptFacetForActionAnnotation;
import org.apache.causeway.core.metamodel.facets.actions.semantics.ActionSemanticsFacet;
import org.apache.causeway.core.metamodel.facets.members.layout.group.LayoutGroupFacetForActionAnnotation;
import org.apache.causeway.core.metamodel.facets.members.publish.command.CommandPublishingFacetForActionAnnotation;
import org.apache.causeway.core.metamodel.facets.members.publish.execution.ExecutionPublishingFacetForActionAnnotation;
import org.apache.causeway.core.metamodel.specloader.validator.ValidationFailureUtils;

public class ActionAnnotationFacetFactory
extends FacetFactoryAbstract {

    @Inject
    public ActionAnnotationFacetFactory(final MetaModelContext mmc) {
        super(mmc, FeatureType.ACTIONS_ONLY);
    }

    @Override
    public void process(final ProcessMethodContext processMethodContext) {

        var actionIfAny = actionIfAny(processMethodContext);

        processExplicit(processMethodContext, actionIfAny);
        processDomainEvent(processMethodContext, actionIfAny);
        processRestrictTo(processMethodContext, actionIfAny);
        processSemantics(processMethodContext, actionIfAny);

        // must come after processing semantics
        processCommandPublishing(processMethodContext, actionIfAny);

        // must come after processing semantics
        processExecutionPublishing(processMethodContext, actionIfAny);

        processTypeOf(processMethodContext, actionIfAny);
        processChoicesFrom(processMethodContext, actionIfAny);

        processFileAccept(processMethodContext, actionIfAny);
    }

    Optional<Action> actionIfAny(final ProcessMethodContext processMethodContext) {
        return processMethodContext
                .synthesizeOnMethodOrMixinType(
                        Action.class,
                        () -> ValidationFailureUtils
                        .raiseAmbiguousMixinAnnotations(processMethodContext.getFacetHolder(), Action.class));
    }

    void processExplicit(final ProcessMethodContext processMethodContext, final Optional<Action> actionIfAny) {
        var holder = processMethodContext.getFacetHolder();

        // check for @Action at all.
        addFacetIfPresent(
                ActionExplicitFacetForActionAnnotation
                .create(actionIfAny, holder));
    }

    void processDomainEvent(final ProcessMethodContext processMethodContext, final Optional<Action> actionIfAny) {

        var actionMethod = processMethodContext.getMethod();

        final boolean isAction = !processMethodContext.isMixinMain()
                || actionIfAny.isPresent();

        try {

            var typeSpec = getSpecificationLoader().loadSpecification(processMethodContext.getCls());
            if(typeSpec==null) {
                return;
            }

            var returnType = actionMethod.getReturnType();
            var returnSpec = getSpecificationLoader().loadSpecification(returnType);
            if (returnSpec == null) {
                return;
            }

            var holder = processMethodContext.getFacetHolder();

            //
            // Set up ActionDomainEventFacet, which will act as the hiding/disabling/validating advisor
            //

            // search for @Action(domainEvent=...), else use the default event type
            var actionDomainEventFacet = ActionDomainEventFacet.create(actionIfAny, typeSpec, holder);
            addFacet(actionDomainEventFacet);

            // replace the current actionInvocationFacet with one that will
            // emit the appropriate domain event and then delegate onto the underlying
            addFacet(
                /* lazily binds the event-type to the actionDomainEventFacet,
                 * such that any changes to the latter during post processing
                 * are reflected here as well
                 */
                isAction
                    ? new ActionInvocationFacetForAction(
                            actionDomainEventFacet,
                            actionMethod, typeSpec, returnSpec, holder)
                    // when in a mixed-in prop/coll situation, the prop/coll event-type must be used instead
                    : new ActionInvocationFacetForMixedInPropertyOrCollection(
                            actionMethod, typeSpec, returnSpec, holder));
        } finally {
            processMethodContext.removeMethod(actionMethod.asMethodForIntrospection());
        }
    }

    void processRestrictTo(final ProcessMethodContext processMethodContext, final Optional<Action> actionIfAny) {
        var facetedMethod = processMethodContext.getFacetHolder();

        // search for @Action(restrictTo=...)
        addFacetIfPresent(
                PrototypeFacetForActionAnnotation
                .create(
                        actionIfAny, facetedMethod,
                        ()->super.getSystemEnvironment().getDeploymentType()));
    }

    void processSemantics(final ProcessMethodContext processMethodContext, final Optional<Action> actionIfAny) {
        var facetedMethod = processMethodContext.getFacetHolder();

        // check for @Action(semantics=...)
        addFacet(
            actionIfAny
                .map(Action::semantics)
                .filter(semanticsOf -> semanticsOf != SemanticsOf.NOT_SPECIFIED)
                .map(semanticsOf -> new ActionSemanticsFacet("ActionAnnotation", semanticsOf, facetedMethod))
                .orElseGet(()->new ActionSemanticsFacet("FallbackToNonIdempotent", SemanticsOf.NON_IDEMPOTENT, facetedMethod))
        );
    }

    void processCommandPublishing(
            final ProcessMethodContext processMethodContext,
            final Optional<Action> actionIfAny) {

        var facetedMethod = processMethodContext.getFacetHolder();

        //
        // this rule inspired by a similar rule for auditing and publishing, see DomainObjectAnnotationFacetFactory
        //
        if(HasInteractionId.class.isAssignableFrom(processMethodContext.getCls())) {
            // do not install on any implementation of HasInteractionId
            // (ie commands, audit entries, published events).
            return;
        }

        // check for @Action(commandPublishing=...)
        addFacetIfPresent(CommandPublishingFacetForActionAnnotation
                .create(actionIfAny, getConfiguration(), getServiceInjector(), facetedMethod));
    }

    void processExecutionPublishing(
            final ProcessMethodContext processMethodContext,
            final Optional<Action> actionIfAny) {

        var facetedMethod = processMethodContext.getFacetHolder();

        //
        // this rule inspired by a similar rule for auditing and publishing,
        // see DomainObjectAnnotationFacetFactory
        // and for commands, see above
        //
        if(HasInteractionId.class.isAssignableFrom(processMethodContext.getCls())) {
            // do not install on any implementation of HasInteractionId
            // (ie commands, audit entries, published events).
            return;
        }

        // check for @Action(executionPublishing=...)
        addFacetIfPresent(
                ExecutionPublishingFacetForActionAnnotation
                .create(actionIfAny, getConfiguration(), facetedMethod));

    }

    void processTypeOf(final ProcessMethodContext processMethodContext, final Optional<Action> actionIfAny) {

        var method = processMethodContext.getMethod();
        var facetedMethod = processMethodContext.getFacetHolder();

        var methodReturnType = method.getReturnType();

        CollectionSemantics.valueOf(methodReturnType)
        .ifPresent(collectionType->{
            addFacetIfPresent(
                    TypeOfFacetForActionAnnotation.create(actionIfAny, collectionType, facetedMethod)
                    .or(
                        // else infer from generic type arg if any
                        ()->TypeOfFacet.inferFromMethodReturnType(method, facetedMethod)
                        ));

        });
    }

    void processChoicesFrom(final ProcessMethodContext processMethodContext, final Optional<Action> actionIfAny) {

        var holder = processMethodContext.getFacetHolder();

        // check for @Action(choicesFrom=...)
        addFacetIfPresent(
                ChoicesFromFacetForActionAnnotation
                .create(actionIfAny, holder));

        addFacetIfPresent(
                LayoutGroupFacetForActionAnnotation
                .create(actionIfAny, holder));

    }

    void processFileAccept(final ProcessMethodContext processMethodContext, final Optional<Action> actionIfAny) {

        var holder = processMethodContext.getFacetHolder();

        // check for @Action(fileAccept=...)
        addFacetIfPresent(
                FileAcceptFacetForActionAnnotation
                .create(actionIfAny, holder));
    }

}
