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

import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Stream;

import org.apache.causeway.applib.Identifier;
import org.apache.causeway.applib.id.LogicalType;
import org.apache.causeway.applib.services.i18n.HasTranslationContext;
import org.apache.causeway.applib.services.i18n.TranslationContext;
import org.apache.causeway.core.metamodel.context.HasMetaModelContext;
import org.apache.causeway.core.metamodel.context.MetaModelContext;

import org.jspecify.annotations.NonNull;

/**
 * Anything in the metamodel (which also includes peers in the reflector) that
 * can be extended.
 */
public interface FacetHolder
extends HasMetaModelContext, HasTranslationContext {

    // -- FACTORIES

    public static FacetHolder simple(
            final MetaModelContext mmc,
            final Identifier featureIdentifier) {
        return new FacetHolderSimple(mmc, featureIdentifier);
    }

    public static FacetHolder layered(
            final Identifier featureIdentifier,
            final FacetHolder parentLayer) {
        return new FacetHolderLayered(featureIdentifier, parentLayer);
    }

    // -- JUNIT SUPPORT

    /**
     *  Meant for simple JUnit tests, that don't use the FacetHolder's identifier.
     */
    public static FacetHolder forTesting(final MetaModelContext mmc) {
        return simple(mmc, Identifier.classIdentifier(LogicalType.fqcn(Object.class)));
    }

    // --

    /**
     * Identifier of the feature this holder represents or is associated with.
     */
    Identifier getFeatureIdentifier();

    int getFacetCount();

    /**
     * Get the facet of the specified type (as per the type it reports from
     * {@link Facet#facetType()}).
     */
    <T extends Facet> T getFacet(Class<T> facetType);

    // -- FACET LOOKUP

    default <T extends Facet> Optional<T> lookupFacet(
            final @NonNull Class<T> facetType) {
        return Optional.ofNullable(getFacet(facetType));
    }

    default <T extends Facet> Optional<T> lookupFacet(
            final @NonNull Class<T> facetType,
            final @NonNull Predicate<T> filter) {
        return lookupFacet(facetType).map(facet->filter.test(facet) ? facet : null);
    }

    default <T extends Facet> Optional<T> lookupNonFallbackFacet(
            final @NonNull Class<T> facetType) {
        return lookupFacet(facetType, facet->!facet.getPrecedence().isFallback());
    }

    // -- CONTAINS

    /**
     * Whether there is a facet registered of the specified type.
     */
    boolean containsFacet(Class<? extends Facet> facetType);

    /**
     * Whether there is a facet registered of the specified type that is not a
     * {@link Facet.Precedence#isFallback() fallback} .
     * <p>
     * Convenience; saves having to {@link #getFacet(Class)} and then check if
     * <tt>null</tt> and not a fallback.
     */
    default boolean containsNonFallbackFacet(final Class<? extends Facet> facetType) {
        var facet = getFacet(facetType);
        return facet != null
                && !facet.getPrecedence().isFallback();
    }

    /**
     * As {@link #containsNonFallbackFacet(Class)}, with additional requirement, that the
     * facet is <i>explicit</i>, not {@link Facet.Precedence#isInferred() inferred}.
     */
    default boolean containsExplicitNonFallbackFacet(final Class<? extends Facet> facetType) {
        var facet = getFacet(facetType);
        return facet != null
                && !facet.getPrecedence().isFallback()
                && !facet.getPrecedence().isInferred();
    }

    Stream<Facet> streamFacets();

    default <F extends Facet> Stream<F> streamFacets(final Class<F> requiredType) {
        return streamFacets()
                .filter(facet->requiredType.isAssignableFrom(facet.getClass()))
                .map(requiredType::cast);
    }

    /**
     * Adds the facet, extracting its {@link Facet#facetType() type} as the key.
     *
     * <p>
     * Any previously added facet of the same type will be overwritten,
     * when given {@link Facet} has equal or higher precedence.
     * Otherwise is ignored.
     */
    void addFacet(@NonNull Facet facet);

    // -- VALIDATION SUPPORT

    Stream<FacetRanking> streamFacetRankings();
    Optional<FacetRanking> getFacetRanking(Class<? extends Facet> facetType);

    // -- TRANSLATION CONTEXT

    @Override
    default TranslationContext getTranslationContext() {
        return getFeatureIdentifier().getTranslationContext();
    }

}
