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

import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;

import javax.annotation.PostConstruct;

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

import org.apache.isis.applib.DomainObjectContainer;
import org.apache.isis.applib.annotation.DomainService;
import org.apache.isis.applib.annotation.NatureOfService;
import org.apache.isis.applib.annotation.Programmatic;
import org.apache.isis.applib.annotation.SemanticsOf;
import org.apache.isis.applib.annotation.When;
import org.apache.isis.applib.annotation.Where;
import org.apache.isis.applib.fixturescripts.FixtureScript;
import org.apache.isis.applib.services.appfeat.ApplicationFeatureRepository;
import org.apache.isis.applib.services.appfeat.ApplicationMemberType;
import org.apache.isis.applib.services.config.ConfigurationService;
import org.apache.isis.applib.services.registry.ServiceRegistry2;
import org.apache.isis.core.metamodel.facetapi.FacetHolder;
import org.apache.isis.core.metamodel.facets.SingleIntValueFacet;
import org.apache.isis.core.metamodel.facets.all.hide.HiddenFacet;
import org.apache.isis.core.metamodel.facets.collections.modify.CollectionAddToFacet;
import org.apache.isis.core.metamodel.facets.collections.modify.CollectionRemoveFromFacet;
import org.apache.isis.core.metamodel.facets.objectvalue.maxlen.MaxLengthFacet;
import org.apache.isis.core.metamodel.facets.objectvalue.typicallen.TypicalLengthFacet;
import org.apache.isis.core.metamodel.facets.properties.update.modify.PropertySetterFacet;
import org.apache.isis.core.metamodel.spec.ObjectSpecification;
import org.apache.isis.core.metamodel.spec.feature.Contributed;
import org.apache.isis.core.metamodel.spec.feature.ObjectAction;
import org.apache.isis.core.metamodel.spec.feature.ObjectAssociation;
import org.apache.isis.core.metamodel.spec.feature.ObjectMember;
import org.apache.isis.core.metamodel.specloader.SpecificationLoader;
import org.apache.isis.core.metamodel.specloader.specimpl.ContributeeMember;

@DomainService(
        nature = NatureOfService.DOMAIN,
        repositoryFor = ApplicationFeature.class,
        menuOrder = "" + Integer.MAX_VALUE
)
public class ApplicationFeatureRepositoryDefault implements ApplicationFeatureRepository {

    //region > caches
    SortedMap<ApplicationFeatureId, ApplicationFeature> packageFeatures = Maps.newTreeMap();
    private final SortedMap<ApplicationFeatureId, ApplicationFeature> classFeatures = Maps.newTreeMap();
    private final SortedMap<ApplicationFeatureId, ApplicationFeature> memberFeatures = Maps.newTreeMap();
    private final SortedMap<ApplicationFeatureId, ApplicationFeature> propertyFeatures = Maps.newTreeMap();
    private final SortedMap<ApplicationFeatureId, ApplicationFeature> collectionFeatures = Maps.newTreeMap();
    private final SortedMap<ApplicationFeatureId, ApplicationFeature> actionFeatures = Maps.newTreeMap();
    //endregion


    //region > init

    private static final String KEY = "isis.services.applicationFeatures.init";

    @Programmatic
    @PostConstruct
    public void init() {
        if(isEagerInitialize()) {
            initializeIfRequired();
        }
    }

    private boolean isEagerInitialize() {
        final String configuredValue = configurationService.getProperty(KEY);
        return "eager".equalsIgnoreCase(configuredValue) ||
                "eagerly".equalsIgnoreCase(configuredValue);
    }

    //endregion


    //region > initializeIfRequired

    enum InitializationState {
        NOT_INITIALIZED,
        INITIALIZED
    }
    private InitializationState initializationState = InitializationState.NOT_INITIALIZED;

    private synchronized void initializeIfRequired() {
        if(initializationState == InitializationState.INITIALIZED) {
            return;
        }
        initializationState = InitializationState.INITIALIZED;
        final Collection<ObjectSpecification> specifications = primeMetaModel();
        createApplicationFeaturesFor(specifications);
    }

    private Collection<ObjectSpecification> primeMetaModel() {
        final List<Object> services = serviceRegistry.getRegisteredServices();
        for (final Object service : services) {
            specificationLoader.loadSpecification(service.getClass());
        }
        return specificationLoader.allSpecifications();
    }

    private void createApplicationFeaturesFor(final Collection<ObjectSpecification> specifications) {
        // take copy to avoid ConcurrentModificationException
        final List<ObjectSpecification> objectSpecifications = Lists.newArrayList(specifications);
        for (final ObjectSpecification spec : objectSpecifications) {
            createApplicationFeaturesFor(spec);
        }
    }

    void createApplicationFeaturesFor(final ObjectSpecification spec) {
        if (exclude(spec)) {
            return;
        }

        final List<ObjectAssociation> properties = spec.getAssociations(Contributed.INCLUDED, ObjectAssociation.Filters.PROPERTIES);
        final List<ObjectAssociation> collections = spec.getAssociations(Contributed.INCLUDED, ObjectAssociation.Filters.COLLECTIONS);
        final List<ObjectAction> actions = spec.getObjectActions(Contributed.INCLUDED);

        if (properties.isEmpty() && collections.isEmpty() && actions.isEmpty()) {
            return;
        }

        final String fullIdentifier = spec.getFullIdentifier();
        final ApplicationFeatureId classFeatureId = ApplicationFeatureId.newClass(fullIdentifier);

        // add class to our map
        // (later on it may get removed if the class turns out to have no features,
        // but we require it in the map for the next bit).
        final ApplicationFeature classFeature = newFeature(classFeatureId);
        classFeatures.put(classFeatureId, classFeature);

        // add members
        boolean addedMembers = false;
        for (final ObjectAssociation property : properties) {
            final Class<?> returnType = correspondingClassFor(property.getSpecification());
            final Integer maxLength = returnType == String.class ? valueOf(property, MaxLengthFacet.class) : null;
            final Integer typicalLength = returnType == String.class ? valueOf(property, TypicalLengthFacet.class) : null;
            final boolean derived = !property.containsDoOpFacet(PropertySetterFacet.class);
            final boolean contributed = property instanceof ContributeeMember;
            addedMembers = newProperty(classFeatureId, property, returnType, contributed, maxLength, typicalLength, derived) || addedMembers;
        }
        for (final ObjectAssociation collection : collections) {
            final boolean derived = !(collection.containsDoOpFacet(CollectionAddToFacet.class) || collection.containsDoOpFacet(CollectionRemoveFromFacet.class));
            final Class<?> elementType = correspondingClassFor(collection.getSpecification());
            final boolean contributed = collection instanceof ContributeeMember;
            addedMembers = newCollection(classFeatureId, collection, elementType, contributed, derived) || addedMembers;
        }
        for (final ObjectAction action : actions) {
            final Class<?> returnType = correspondingClassFor(action.getReturnType());
            final SemanticsOf actionSemantics = SemanticsOf.from(action.getSemantics());
            final boolean contributed = action instanceof ContributeeMember;
            addedMembers = newAction(classFeatureId, action, returnType, contributed, actionSemantics) || addedMembers;
        }

        if (!addedMembers) {
            // remove this class feature, since it turned out to have no members
            classFeatures.remove(classFeatureId);
            return;
        }

        // leave the class as is and (as there were indeed members for this class)
        // and add all of its parent packages
        final ApplicationFeatureId classParentPackageId = addClassParent(classFeatureId);
        addParents(classParentPackageId);
    }

    private static Class<?> correspondingClassFor(final ObjectSpecification objectSpec) {
        return objectSpec != null ? objectSpec.getCorrespondingClass() : null;
    }

    private static Integer valueOf(
            final FacetHolder facetHolder,
            final Class<? extends SingleIntValueFacet> cls) {
        final SingleIntValueFacet facet = facetHolder.getFacet(cls);
        return facet != null ? facet.value() : null;
    }


    ApplicationFeatureId addClassParent(final ApplicationFeatureId classFeatureId) {
        final ApplicationFeatureId parentPackageId = classFeatureId.getParentPackageId();

        final ApplicationFeature parentPackage = findPackageElseCreate(parentPackageId);

        parentPackage.addToContents(classFeatureId);
        return parentPackageId;
    }

    void addParents(final ApplicationFeatureId classOrPackageId) {
        final ApplicationFeatureId parentPackageId = classOrPackageId.getParentPackageId();
        if (parentPackageId == null) {
            return;
        }

        final ApplicationFeature parentPackage = findPackageElseCreate(parentPackageId);

        // add this feature as part of the contents of its parent
        parentPackage.addToContents(classOrPackageId);

        // and recurse up
        addParents(parentPackageId);
    }

    private ApplicationFeature findPackageElseCreate(final ApplicationFeatureId parentPackageId) {
        ApplicationFeature parentPackage = findPackage(parentPackageId);
        if (parentPackage == null) {
            parentPackage = newPackage(parentPackageId);
        }
        return parentPackage;
    }

    private ApplicationFeature newPackage(final ApplicationFeatureId packageId) {
        final ApplicationFeature parentPackage = newFeature(packageId);
        packageFeatures.put(packageId, parentPackage);
        return parentPackage;
    }

    private boolean newProperty(
            final ApplicationFeatureId classFeatureId,
            final ObjectMember objectMember,
            final Class<?> returnType,
            final boolean contributed,
            final Integer maxLength, final Integer typicalLength,
            final boolean derived) {
        return newMember(classFeatureId, objectMember, ApplicationMemberType.PROPERTY, returnType, contributed, derived, maxLength, typicalLength, null);
    }

    private boolean newCollection(
            final ApplicationFeatureId classFeatureId,
            final ObjectMember objectMember,
            final Class<?> returnType,
            final boolean contributed,
            final boolean derived) {
        return newMember(classFeatureId, objectMember, ApplicationMemberType.COLLECTION, returnType, contributed, derived, null, null, null);
    }

    private boolean newAction(
            final ApplicationFeatureId classFeatureId,
            final ObjectMember objectMember,
            final Class<?> returnType,
            final boolean contributed,
            final SemanticsOf actionSemantics) {
        return newMember(classFeatureId, objectMember, ApplicationMemberType.ACTION, returnType, contributed, null, null, null, actionSemantics);
    }

    private boolean newMember(
            final ApplicationFeatureId classFeatureId,
            final ObjectMember objectMember,
            final ApplicationMemberType memberType,
            final Class<?> returnType,
            final boolean contributed,
            final Boolean derived,
            final Integer maxLength, final Integer typicalLength,
            final SemanticsOf actionSemantics) {
        if (objectMember.isAlwaysHidden()) {
            return false;
        }
        newMember(classFeatureId, objectMember.getId(), memberType, returnType, contributed, derived, maxLength, typicalLength, actionSemantics);
        return true;
    }

    private void newMember(
            final ApplicationFeatureId classFeatureId,
            final String memberId,
            final ApplicationMemberType memberType,
            final Class<?> returnType,
            final boolean contributed,
            final Boolean derived,
            final Integer maxLength, final Integer typicalLength,
            final SemanticsOf actionSemantics) {
        final ApplicationFeatureId featureId = ApplicationFeatureId.newMember(classFeatureId.getFullyQualifiedName(), memberId);

        final ApplicationFeature memberFeature = newFeature(featureId);
        memberFeature.setMemberType(memberType);

        memberFeature.setReturnTypeName(returnType != null ? returnType.getSimpleName() : null);
        memberFeature.setContributed(contributed);
        memberFeature.setDerived(derived);
        memberFeature.setPropertyMaxLength(maxLength);
        memberFeature.setPropertyTypicalLength(typicalLength);
        memberFeature.setActionSemantics(actionSemantics);

        memberFeatures.put(featureId, memberFeature);

        // also cache per memberType
        featuresMapFor(memberType).put(featureId, memberFeature);

        final ApplicationFeature classFeature = findClass(classFeatureId);
        classFeature.addToMembers(featureId, memberType);
    }

    private SortedMap<ApplicationFeatureId, ApplicationFeature> featuresMapFor(final ApplicationMemberType memberType) {
        switch (memberType) {
        case PROPERTY:
            return propertyFeatures;
        case COLLECTION:
            return collectionFeatures;
        default: // case ACTION:
            return actionFeatures;
        }
    }

    private ApplicationFeature newFeature(final ApplicationFeatureId featureId) {
        final ApplicationFeature feature = applicationFeatureFactory.newApplicationFeature();
        feature.setFeatureId(featureId);
        return feature;
    }


    protected boolean exclude(final ObjectSpecification spec) {
        return spec.isAbstract() ||
                isBuiltIn(spec) ||
                isHidden(spec) ||
                isFixtureScript(spec) ||
                isSuperClassOfService(spec);
    }

    private boolean isFixtureScript(final ObjectSpecification spec) {
        return FixtureScript.class.isAssignableFrom(spec.getCorrespondingClass());
    }

    /**
     * Ignore the (strict) superclasses of any services.
     * <p/>
     * <p>
     * For example, we want to ignore <code>ExceptionRecognizerComposite</code> because there is no service
     * of that type (only of subtypes of that).
     * </p>
     */
    private boolean isSuperClassOfService(final ObjectSpecification spec) {
        final List<Object> registeredServices = serviceRegistry.getRegisteredServices();

        final Class<?> specClass = spec.getCorrespondingClass();

        // is this class a supertype or the actual type of one of the services?
        boolean serviceCls = false;
        for (final Object registeredService : registeredServices) {
            final Class<?> serviceClass = registeredService.getClass();
            if (specClass.isAssignableFrom(serviceClass)) {
                serviceCls = true;
            }
        }
        if (!serviceCls) {
            return false;
        }

        // yes it is.  In which case, is it the actual concrete class of one of those services?
        for (final Object registeredService : registeredServices) {
            final Class<?> serviceClass = registeredService.getClass();
            if (serviceClass.isAssignableFrom(specClass)) {
                return false;
            }
        }
        // couldn't find a service of exactly this type, so ignore the spec.
        return true;
    }

    protected boolean isHidden(final ObjectSpecification spec) {
        final HiddenFacet facet = spec.getFacet(HiddenFacet.class);
        return facet != null &&
                !facet.isNoop() &&
                (facet.where() == Where.EVERYWHERE || facet.where() == Where.ANYWHERE) &&
                facet.when() == When.ALWAYS;
    }

    protected boolean isBuiltIn(final ObjectSpecification spec) {
        final String className = spec.getFullIdentifier();
        return className.startsWith("java") || className.startsWith("org.joda");
    }
    //endregion

    //region > packageFeatures, classFeatures, memberFeatures
    @Programmatic
    public ApplicationFeature findFeature(final ApplicationFeatureId featureId) {
        initializeIfRequired();
        switch (featureId.getType()) {
        case PACKAGE:
            return findPackage(featureId);
        case CLASS:
            return findClass(featureId);
        case MEMBER:
            return findMember(featureId);
        }
        throw new IllegalArgumentException("Feature has unknown feature type " + featureId.getType());
    }

    @Programmatic
    public ApplicationFeature findPackage(final ApplicationFeatureId featureId) {
        initializeIfRequired();
        return packageFeatures.get(featureId);
    }

    @Programmatic
    public ApplicationFeature findClass(final ApplicationFeatureId featureId) {
        initializeIfRequired();
        return classFeatures.get(featureId);
    }

    @Programmatic
    public ApplicationFeature findMember(final ApplicationFeatureId featureId) {
        initializeIfRequired();
        return memberFeatures.get(featureId);
    }

    //endregion

    //region > allFeatures, allPackages, allClasses, allMembers
    @Programmatic
    public Collection<ApplicationFeature> allFeatures(final ApplicationFeatureType featureType) {
        initializeIfRequired();
        if (featureType == null) {
            return Collections.emptyList();
        }
        switch (featureType) {
        case PACKAGE:
            return allPackages();
        case CLASS:
            return allClasses();
        case MEMBER:
            return allMembers();
        }
        throw new IllegalArgumentException("Unknown feature type " + featureType);
    }

    @Programmatic
    public Collection<ApplicationFeature> allPackages() {
        initializeIfRequired();
        return packageFeatures.values();
    }

    @Programmatic
    public Collection<ApplicationFeature> allClasses() {
        initializeIfRequired();
        return classFeatures.values();
    }

    @Programmatic
    public Collection<ApplicationFeature> allMembers() {
        initializeIfRequired();
        return memberFeatures.values();
    }

    @Programmatic
    public Collection<ApplicationFeature> allProperties() {
        initializeIfRequired();
        return propertyFeatures.values();
    }

    @Programmatic
    public Collection<ApplicationFeature> allCollections() {
        initializeIfRequired();
        return collectionFeatures.values();
    }

    @Programmatic
    public Collection<ApplicationFeature> allActions() {
        initializeIfRequired();
        return actionFeatures.values();
    }
    //endregion

    //region > packageNames, packageNamesContainingClasses, classNamesContainedIn, memberNamesOf
    @Override @Programmatic
    public List<String> packageNames() {
        initializeIfRequired();
        return Lists.newArrayList(
                Iterables.transform(
                        allFeatures(ApplicationFeatureType.PACKAGE), ApplicationFeature.Functions.GET_FQN));
    }

    @Override @Programmatic
    public List<String> packageNamesContainingClasses(final ApplicationMemberType memberType) {
        initializeIfRequired();
        final Collection<ApplicationFeature> packages = allFeatures(ApplicationFeatureType.PACKAGE);
        return Lists.newArrayList(
                Iterables.transform(
                        Iterables.filter(
                                packages,
                                ApplicationFeature.Predicates.packageContainingClasses(memberType, this)
                        ),
                        ApplicationFeature.Functions.GET_FQN));
    }

    @Override @Programmatic
    public List<String> classNamesContainedIn(final String packageFqn, final ApplicationMemberType memberType) {
        initializeIfRequired();
        final ApplicationFeatureId packageId = ApplicationFeatureId.newPackage(packageFqn);
        final ApplicationFeature pkg = findPackage(packageId);
        if (pkg == null) {
            return Collections.emptyList();
        }
        final SortedSet<ApplicationFeatureId> contents = pkg.getContents();
        return Lists.newArrayList(
                Iterables.transform(
                        Iterables.filter(
                                contents,
                                ApplicationFeatureId.Predicates.isClassContaining(memberType, this)),
                        ApplicationFeatureId.Functions.GET_CLASS_NAME));
    }

    @Override @Programmatic
    public List<String> classNamesRecursivelyContainedIn(final String packageFqn) {
        initializeIfRequired();
        final ApplicationFeatureId packageId = ApplicationFeatureId.newPackage(packageFqn);
        final ApplicationFeature pkg = findPackage(packageId);
        if (pkg == null) {
            return Collections.emptyList();
        }
        final Set<ApplicationFeatureId> classIds = this.classFeatures.keySet();
        return Lists.newArrayList(
                Iterables.transform(
                        Iterables.filter(
                                classIds,
                                ApplicationFeatureId.Predicates.isClassRecursivelyWithin(packageId)),
                        ApplicationFeatureId.Functions.GET_CLASS_NAME));
    }

    @Override @Programmatic
    public List<String> memberNamesOf(
            final String packageFqn,
            final String className,
            final ApplicationMemberType memberType) {
        initializeIfRequired();
        final ApplicationFeatureId classId = ApplicationFeatureId.newClass(packageFqn + "." + className);
        final ApplicationFeature cls = findClass(classId);
        if (cls == null) {
            return Collections.emptyList();
        }
        final SortedSet<ApplicationFeatureId> featureIds = cls.membersOf(memberType);
        return Lists.newArrayList(
                Iterables.transform(featureIds, ApplicationFeatureId.Functions.GET_MEMBER_NAME)
        );
    }

    //endregion

    //region  > services (injected)

    @javax.inject.Inject
    ServiceRegistry2 serviceRegistry;

    @javax.inject.Inject
    DomainObjectContainer container;

    @javax.inject.Inject
    ConfigurationService configurationService;

    @javax.inject.Inject
    SpecificationLoader specificationLoader;

    @javax.inject.Inject
    ApplicationFeatureFactory applicationFeatureFactory;

    //endregion


}