/*
 *  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.object.navparent.annotation;

import java.lang.reflect.AnnotatedElement;
import java.util.Objects;
import java.util.Optional;

import jakarta.inject.Inject;

import org.apache.causeway.applib.annotation.PropertyLayout;
import org.apache.causeway.commons.collections.Can;
import org.apache.causeway.commons.internal.reflection._Annotations;
import org.apache.causeway.commons.internal.reflection._GenericResolver.ResolvedMethod;
import org.apache.causeway.commons.internal.reflection._Reflect;
import org.apache.causeway.commons.internal.reflection._Reflect.InterfacePolicy;
import org.apache.causeway.commons.internal.reflection._Reflect.TypeHierarchyPolicy;
import org.apache.causeway.core.metamodel.context.MetaModelContext;
import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
import org.apache.causeway.core.metamodel.facetapi.FeatureType;
import org.apache.causeway.core.metamodel.facetapi.MetaModelRefiner;
import org.apache.causeway.core.metamodel.facets.Evaluators;
import org.apache.causeway.core.metamodel.facets.Evaluators.MethodEvaluator;
import org.apache.causeway.core.metamodel.facets.FacetFactoryAbstract;
import org.apache.causeway.core.metamodel.facets.object.navparent.NavigableParentFacet;
import org.apache.causeway.core.metamodel.facets.object.navparent.method.NavigableParentFacetViaMethod;
import org.apache.causeway.core.metamodel.progmodel.ProgrammingModel;
import org.apache.causeway.core.metamodel.specloader.validator.ValidationFailure;

/**
 * For detailed behavioral specification see
 * <a href="https://issues.apache.org/jira/browse/CAUSEWAY-1816">CAUSEWAY-1816</a>.
 *
 * @since 2.0
 *
 */
//@Log4j2
public class NavigableParentAnnotationFacetFactory
extends FacetFactoryAbstract
implements MetaModelRefiner {

    @Inject
    public NavigableParentAnnotationFacetFactory(final MetaModelContext mmc) {
        super(mmc, FeatureType.OBJECTS_ONLY);
    }

    @Override
    public void process(final ProcessClassContext processClassContext) {
        final Class<?> cls = processClassContext.getCls();
        final FacetHolder facetHolder = processClassContext.getFacetHolder();

        // Starting from the current domain-object class, we search down the object
        // inheritance hierarchy (super class, super super class, ...), until we find
        // the first class that has a @PropertyLayout(navigable=Navigable.PARENT) annotation.
        // That's the one we use to
        // resolve the current domain-object's navigable parent.

        final Optional<Evaluators.Evaluator> evaluators =
                Evaluators.streamEvaluators(cls,
                        NavigableParentAnnotationFacetFactory::isNavigableParentFlagSet,
                        TypeHierarchyPolicy.INCLUDE,
                        InterfacePolicy.EXCLUDE)
                .findFirst();

        if (evaluators.isEmpty()) {
            return; // no parent resolvable
        }

        final Evaluators.Evaluator parentEvaluator = evaluators.get();

        final ResolvedMethod method;

        // find method that provides the parent ...
        if(parentEvaluator instanceof Evaluators.MethodEvaluator methodEvaluator) {
            // we have a 'parent' annotated method
            method = methodEvaluator.getMethod();
        } else if(parentEvaluator instanceof Evaluators.FieldEvaluator fieldEvaluator) {
            // we have a 'parent' annotated field (useful if one uses lombok's @Getter on a field)
            method = fieldEvaluator.getCorrespondingGetter().orElse(null);
            if(method==null)
                return; // code should not be reached, since case should be handled by meta-data validation

        } else {
            return; // no parent resolvable
        }

        addFacetIfPresent(
                NavigableParentFacetViaMethod.create(cls, method, facetHolder));
    }

    private static boolean isNavigableParentFlagSet(final AnnotatedElement annotatedElement){
        return _Annotations
                .synthesize(annotatedElement, PropertyLayout.class)
                .map(propertyLayout->propertyLayout.navigable().isParent())
                .orElse(false);
    }

    /**
     * For detailed behavior see
     * <a href="https://issues.apache.org/jira/browse/CAUSEWAY-1816">CAUSEWAY-1816</a>.
     */
    @Override
    public void refineProgrammingModel(final ProgrammingModel programmingModel) {

        programmingModel.addValidatorSkipManagedBeans(spec->{

            var cls = spec.getCorrespondingClass();

            if(!spec.lookupFacet(NavigableParentFacet.class).isPresent()) {
                return; // skip check
            }

            var evaluators =
                    Evaluators.streamEvaluators(cls,
                            NavigableParentAnnotationFacetFactory::isNavigableParentFlagSet,
                            TypeHierarchyPolicy.EXCLUDE,
                            InterfacePolicy.INCLUDE)
                    .collect(Can.toCan())
                    // guard against inherited method having identical synthesized annotations as bas method,
                    // while not actually overriding
                    .distinct((a, b)->{
                        if(!Objects.equals(a.getClass(), b.getClass())) {
                            return false; // different
                        }
                        if(a instanceof MethodEvaluator) {
                            var ma = (MethodEvaluator) a;
                            var mb = (MethodEvaluator) b;
                            return _Reflect.methodsSame(ma.getMethod().method(), mb.getMethod().method());
                        }
                        return true; // equal
                    });;

            if (evaluators.isEmpty()) {
                return; // no conflict, continue validation processing
            }

            if (evaluators.isCardinalityMultiple()) {

                var conflictingEvaluatorNames = evaluators.map(Evaluators.Evaluator::name).toSet();

                ValidationFailure.raiseFormatted(
                        spec,
                        "%s: conflict for determining a strategy for retrieval of (navigable) parent for class, "
                                + "contains multiple annotations '@%s' having navigable=PARENT, "
                                + "while at most one is allowed.\n\tConflicting members: %s",
                                spec.getFeatureIdentifier().className(),
                                PropertyLayout.class.getName(),
                                conflictingEvaluatorNames.toString()
                                );

                return; // continue validation processing
            }

            final Evaluators.Evaluator parentEvaluator = evaluators.getSingletonOrFail();

            if(parentEvaluator instanceof Evaluators.FieldEvaluator) {
                // we have a @Parent annotated field (useful if one uses lombok's @Getter on a field)

                final Evaluators.FieldEvaluator fieldEvaluator =
                        (Evaluators.FieldEvaluator) parentEvaluator;

                if(!fieldEvaluator.getCorrespondingGetter().isPresent()) {

                    ValidationFailure.raiseFormatted(
                            spec,
                            "%s: unable to determine a strategy for retrieval of (navigable) parent for class, "
                                    + "field '%s' annotated with '@%s' having navigable=PARENT does not provide a getter.",
                                    spec.getFeatureIdentifier().className(),
                                    fieldEvaluator.getField().getName(),
                                    PropertyLayout.class.getName());
                }

            }

        });

    }

}
