/*
 *  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.applib.services.appfeatui;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;

import jakarta.inject.Inject;
import jakarta.inject.Named;

import org.apache.causeway.applib.CausewayModuleApplib;
import org.apache.causeway.applib.ViewModel;
import org.apache.causeway.applib.annotation.DomainObject;
import org.apache.causeway.applib.annotation.MemberSupport;
import org.apache.causeway.applib.annotation.Navigable;
import org.apache.causeway.applib.annotation.ObjectSupport;
import org.apache.causeway.applib.annotation.Parameter;
import org.apache.causeway.applib.annotation.ParameterLayout;
import org.apache.causeway.applib.annotation.Programmatic;
import org.apache.causeway.applib.annotation.Property;
import org.apache.causeway.applib.annotation.PropertyLayout;
import org.apache.causeway.applib.annotation.Where;
import org.apache.causeway.applib.services.appfeat.ApplicationFeature;
import org.apache.causeway.applib.services.appfeat.ApplicationFeatureId;
import org.apache.causeway.applib.services.appfeat.ApplicationFeatureRepository;
import org.apache.causeway.applib.services.appfeat.ApplicationFeatureSort;
import org.apache.causeway.applib.services.bookmark.Bookmark;
import org.apache.causeway.applib.services.factory.FactoryService;
import org.apache.causeway.applib.util.Equality;
import org.apache.causeway.applib.util.Hashing;
import org.apache.causeway.applib.util.ObjectContracts;
import org.apache.causeway.applib.util.ToString;
import org.apache.causeway.commons.internal.assertions._Assert;
import org.apache.causeway.commons.internal.base._Casts;
import org.apache.causeway.commons.internal.collections._Lists;

import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;

/**
 * View model identified by {@link ApplicationFeatureId} and backed by an {@link ApplicationFeature}.
 *
 * @since 2.x  {@index}
 */
@Named(ApplicationFeatureViewModel.LOGICAL_TYPE_NAME)
@DomainObject
public abstract class ApplicationFeatureViewModel implements ViewModel {

    static final String LOGICAL_TYPE_NAME = CausewayModuleApplib.NAMESPACE_FEAT + ".ApplicationFeatureViewModel";

    public static abstract class PropertyDomainEvent<S extends ApplicationFeatureViewModel,T> extends CausewayModuleApplib.PropertyDomainEvent<S, T> {}
    public static abstract class CollectionDomainEvent<S extends ApplicationFeatureViewModel,T> extends CausewayModuleApplib.CollectionDomainEvent<S, T> {}
    public static abstract class ActionDomainEvent<S extends ApplicationFeatureViewModel> extends CausewayModuleApplib.ActionDomainEvent<S> {}

    @Inject private FactoryService factory;
    @Inject private ApplicationFeatureRepository featureRepository;

    // -- constructors

    public static ApplicationFeatureViewModel newViewModel(
            final ApplicationFeatureId featureId,
            final ApplicationFeatureRepository applicationFeatureRepository,
            final FactoryService factoryService) {
        final Class<? extends ApplicationFeatureViewModel> vmClass =
                viewModelClassFor(featureId, applicationFeatureRepository);

        if(featureId.getSort().isNamespace()) {
            _Assert.assertEquals(vmClass, ApplicationNamespace.class);
            return factoryService.viewModel(new ApplicationNamespace(featureId));
        }

        return factoryService.viewModel(vmClass,
                Bookmark.forLogicalTypeNameAndIdentifier(
                        featureId.getLogicalTypeName(),
                        featureId.asEncodedString()));
    }

    private static Class<? extends ApplicationFeatureViewModel> viewModelClassFor(
            final ApplicationFeatureId featureId,
            final ApplicationFeatureRepository applicationFeatureRepository) {
        switch (featureId.getSort()) {
        case NAMESPACE:
            return ApplicationNamespace.class;
        case TYPE:
            return ApplicationType.class;
        case MEMBER:

            var memberSort =
            Optional.ofNullable(applicationFeatureRepository.findFeature(featureId))
                .flatMap(ApplicationFeature::getMemberSort)
                .orElse(null);

            if(memberSort != null) {
                switch(memberSort) {
                case PROPERTY:
                    return ApplicationTypeProperty.class;
                case COLLECTION:
                    return ApplicationTypeCollection.class;
                case ACTION:
                    return ApplicationTypeAction.class;
                }
            }

        }
        throw new IllegalArgumentException("could not determine feature type; featureId = " + featureId);
    }

    public ApplicationFeatureViewModel() {
        this(ApplicationFeatureId.NAMESPACE_DEFAULT);
    }

    ApplicationFeatureViewModel(final ApplicationFeatureId featureId) {
        setFeatureId(featureId);
    }

    // -- identification
    /**
     * having a title() method (rather than using @Title annotation) is necessary as a workaround to be able to use
     * wrapperFactory#unwrap(...) method, which is otherwise broken in Causeway 1.6.0
     */
    @ObjectSupport public String title() {
        return getFullyQualifiedName();
    }
    @ObjectSupport public String iconName() {
        return "applicationFeature";
    }

    // -- VIEWMODEL CONTRACT

    public ApplicationFeatureViewModel(final String encodedMemento) {
        this(ApplicationFeatureId.parseEncoded(encodedMemento));
    }

    @Override
    public String viewModelMemento() {
        return getFeatureId().asEncodedString();
    }

    // -- featureId (property, programmatic)

    @Getter(onMethod_ = {@Programmatic})
    @Setter
    private ApplicationFeatureId featureId;

    // -- feature (property, programmatic)

    @Programmatic
    ApplicationFeature getFeature() {
        return featureRepository.findFeature(getFeatureId());
    }

    // -- fullyQualifiedName (property, programmatic)
    @Programmatic // in the title
    public String getFullyQualifiedName() {
        return getFeatureId().getFullyQualifiedName();
    }

    // -- type (programmatic)
    @Programmatic
    public ApplicationFeatureSort getSort() {
        return getFeatureId().getSort();
    }

    @Property(
            domainEvent = NamespaceName.DomainEvent.class,
            maxLength = NamespaceName.MAX_LENGTH
    )
    @PropertyLayout(
            fieldSetId = "identity",
            sequence = "2.2",
            typicalLength = NamespaceName.TYPICAL_LENGTH
    )
    @Parameter(
            maxLength = NamespaceName.MAX_LENGTH
    )
    @ParameterLayout(
            named = "Namespace Name",
            typicalLength = NamespaceName.TYPICAL_LENGTH
    )
    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface NamespaceName {
        int MAX_LENGTH = 50;
        int TYPICAL_LENGTH = 50;

        class DomainEvent extends PropertyDomainEvent<ApplicationFeatureViewModel, String> {}
    }

    @NamespaceName
    public String getNamespaceName() {
        return getFeatureId().getNamespace();
    }

    // -- className

    /**
     * For packages, will be null. Is in this class (rather than subclasses) so is shown in
     * {@link ApplicationNamespace#getContents() package contents}.
     */

    @Property(
            domainEvent = TypeSimpleName.DomainEvent.class,
            maxLength = TypeSimpleName.MAX_LENGTH
    )
    @PropertyLayout(
            typicalLength = TypeSimpleName.TYPICAL_LENGTH,
            fieldSetId = "identity",
            sequence = "2.3"
    )
    @Parameter(
            maxLength = TypeSimpleName.MAX_LENGTH
    )
    @ParameterLayout(
            named = "Type Simple Name",
            typicalLength = TypeSimpleName.TYPICAL_LENGTH
    )
    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface TypeSimpleName {

        int MAX_LENGTH = 50;
        int TYPICAL_LENGTH = 50;

        class DomainEvent extends PropertyDomainEvent<ApplicationFeatureViewModel, String> {}
    }

    @TypeSimpleName
    public String getTypeSimpleName() {
        return getFeatureId().getTypeSimpleName();
    }
    @MemberSupport public boolean hideTypeSimpleName() {
        return getSort().isNamespace();
    }

    // -- memberName

    @Property(
            domainEvent = MemberName.DomainEvent.class,
            maxLength = MemberName.MAX_LENGTH
    )
    @PropertyLayout(
            fieldSetId = "identity",
            sequence = "2.4",
            typicalLength = MemberName.TYPICAL_LENGTH
    )
    @Parameter(
            maxLength = MemberName.MAX_LENGTH
    )
    @ParameterLayout(
            named = "Member Name",
            typicalLength = MemberName.TYPICAL_LENGTH
    )
    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface MemberName {

        int MAX_LENGTH = 50;
        int TYPICAL_LENGTH = 50;

        class DomainEvent extends PropertyDomainEvent<ApplicationFeatureViewModel, String> {}
    }

    /**
     * For packages and class names, will be null.
     */
    @MemberName
    public String getMemberName() {
        return getFeatureId().getLogicalMemberName();
    }
    @MemberSupport public boolean hideMemberName() {
        return !getSort().isMember();
    }

    // -- parent (property)

    @Property(
            domainEvent = Parent.DomainEvent.class
    )
    @PropertyLayout(
            fieldSetId = "parent",
            hidden = Where.ALL_TABLES,
            navigable = Navigable.PARENT,
            sequence = "2.6"
    )
    @ParameterLayout(
            named = "Parent"
    )
    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Parent {
        class DomainEvent extends PropertyDomainEvent<ApplicationFeatureViewModel, ApplicationFeatureViewModel> {}
    }

    @Parent
    public ApplicationFeatureViewModel getParent() {
        final ApplicationFeatureId parentId;
        parentId = getSort() == ApplicationFeatureSort.MEMBER
                ? getFeatureId().getParentTypeFeatureId()
                : getFeatureId().getParentNamespaceFeatureId();
        if(parentId == null) {
            return null;
        }
        final ApplicationFeature feature = featureRepository.findFeature(parentId);
        if (feature == null) {
            return null;
        }
        final Class<? extends ApplicationFeatureViewModel> cls =
                viewModelClassFor(parentId, featureRepository);
        return factory.viewModel(cls,
                Bookmark.forLogicalTypeNameAndIdentifier(
                        ApplicationFeatureViewModel.LOGICAL_TYPE_NAME,
                        parentId.asEncodedString()));
    }

    // -- parentPackage (property, programmatic, for packages & classes only)

    /**
     * The parent package feature of this class or package.
     */
    @Programmatic
    public ApplicationFeatureViewModel getParentNamespace() {
        return ApplicationFeatureViewModel
        .newViewModel(getFeatureId().getParentNamespaceFeatureId(), featureRepository, factory);
    }

    // -- equals, hashCode, toString

    private static final Equality<ApplicationFeatureViewModel> equality =
            ObjectContracts.checkEquals(ApplicationFeatureViewModel::getFeatureId);

    private static final Hashing<ApplicationFeatureViewModel> hashing =
            ObjectContracts.hashing(ApplicationFeatureViewModel::getFeatureId);

    private static final ToString<ApplicationFeatureViewModel> toString =
            ObjectContracts.toString("featureId", ApplicationFeatureViewModel::getFeatureId);

    @Override
    public boolean equals(final Object obj) {
        return equality.equals(this, obj);
    }

    @Override
    public int hashCode() {
        return hashing.hashCode(this);
    }

    @Override
    public String toString() {
        return toString.toString(this);
    }

    // -- FACTORY

    public static <T extends ApplicationFeatureViewModel> Function<ApplicationFeatureId, T> factory(
            final @NonNull ApplicationFeatureRepository featureRepository,
            final @NonNull FactoryService factory,
            final @NonNull Class<T> viewModelType) {

        return featureId -> _Casts.<T>uncheckedCast(ApplicationFeatureViewModel
                .newViewModel(featureId, featureRepository, factory));
    }

    // -- HELPER

    protected <T extends ApplicationFeatureViewModel> List<T> asViewModels(
            final java.util.Collection<ApplicationFeatureId> featureIds,
            final Class<T> viewModelType) {
        return featureIds.stream()
                .map(factory(featureRepository, factory, viewModelType))
                .collect(_Lists.toUnmodifiable());
    }

    protected <T extends ApplicationFeatureViewModel> T asViewModel(
            final ApplicationFeatureId featureId,
            final Class<T> viewModelType) {
        return factory(featureRepository, factory, viewModelType).apply(featureId);
    }

}
