/*
 * ModeShape (http://www.modeshape.org)
 * See the COPYRIGHT.txt file distributed with this work for information
 * regarding copyright ownership.  Some portions may be licensed
 * to Red Hat, Inc. under one or more contributor license agreements.
 * See the AUTHORS.txt file in the distribution for a full listing of 
 * individual contributors.
 *
 * ModeShape is free software. Unless otherwise indicated, all code in ModeShape
 * is licensed to you under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 * 
 * ModeShape is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.modeshape.jcr;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.jcr.PropertyType;
import javax.jcr.Value;
import javax.jcr.nodetype.NodeType;
import javax.jcr.nodetype.NodeTypeIterator;
import javax.jcr.nodetype.PropertyDefinition;
import org.modeshape.common.annotation.ThreadSafe;
import org.modeshape.common.util.CheckArg;
import org.modeshape.jcr.NodeTypes;
import org.modeshape.jcr.NodeTypes.NodeDefinitionSet;
import org.modeshape.jcr.api.Namespaced;
import org.modeshape.jcr.cache.NodeKey;
import org.modeshape.jcr.cache.SiblingCounter;
import org.modeshape.jcr.value.Name;
import org.modeshape.jcr.value.basic.BasicName;

/**
 * ModeShape implementation of JCR {@link NodeType}s.
 */
@ThreadSafe
class JcrNodeType implements NodeType, Namespaced {

    public static final String RESIDUAL_ITEM_NAME = "*";
    public static final Name RESIDUAL_NAME = new BasicName("", RESIDUAL_ITEM_NAME);

    private final NodeKey key;
    /** The name of the node type (e.g., <code>{http://www.jcp.org/jcr/nt/1.0}base</code>) */
    private final Name name;
    /** The name of the node's primary item */
    private final Name primaryItemName;

    /** The supertypes for this node. */
    private final List<JcrNodeType> declaredSupertypes;

    /**
     * The list of all supertypes for this node, beginning with the immediate supertypes, followed by the supertypes of those
     * supertypes, etc.
     */
    private final List<JcrNodeType> allSupertypes;

    /**
     * The list of this type and all supertypes for this node, beginning with this type, continuing with the immediate supertypes,
     * followed by the supertypes of those supertypes, etc.
     */
    private final List<JcrNodeType> thisAndAllSupertypes;
    private final Set<Name> thisAndAllSupertypesNames;

    /** Indicates whether this node type is a mixin type (as opposed to a primary type). */
    private final boolean mixin;
    /** Indicates whether the child nodes of nodes of this type can be ordered. */
    private final boolean orderableChildNodes;

    /** Indicates whether this node type is abstract */
    private final boolean isAbstract;

    /** Indicates whether this node is queryable (i.e., should be included in query results) */
    private final boolean queryable;

    /**
     * The child node definitions that are defined on this node type.
     */
    private final List<JcrNodeDefinition> childNodeDefinitions;

    /**
     * The property definitions that are defined on this node type.
     */
    private final List<JcrPropertyDefinition> propertyDefinitions;

    /**
     * A local cache of all defined and inherited child node definitions and property definitions. Residual definitions are
     * included. This class's methods to find a property definition and find child node definitions, and since they're frequently
     * used by SessionCache this cache provides very quick access.
     */
    private final DefinitionCache allDefinitions;

    /**
     * A reference to the execution context in which this node type exists, used to remap the internal names to their appropriate
     * prefixed version (e.g., <code>{http://www.jcp.org/jcr/nt/1.0}base</code> to <code>&quot;nt:base&quot;</code>.).
     */
    private ExecutionContext context;

    /**
     * A reference to a user session, in the context of which the node type is created. May be null during system initialization.
     */
    private JcrSession session;

    /** Link to the repository node type manager for the repository to which this node type belongs. */
    private RepositoryNodeTypeManager nodeTypeManager;

    JcrNodeType( NodeKey prototypeKey,
                 ExecutionContext context,
                 JcrSession session,
                 RepositoryNodeTypeManager nodeTypeManager,
                 Name name,
                 List<JcrNodeType> declaredSupertypes,
                 Name primaryItemName,
                 Collection<JcrNodeDefinition> childNodeDefinitions,
                 Collection<JcrPropertyDefinition> propertyDefinitions,
                 boolean mixin,
                 boolean isAbstract,
                 boolean queryable,
                 boolean orderableChildNodes ) {
        assert context != null;

        this.context = context;
        this.session = session;
        this.nodeTypeManager = nodeTypeManager;
        this.name = name;
        this.primaryItemName = primaryItemName;
        this.declaredSupertypes = declaredSupertypes != null ? declaredSupertypes : Collections.<JcrNodeType>emptyList();
        this.mixin = mixin;
        this.queryable = queryable;
        this.isAbstract = isAbstract;
        this.orderableChildNodes = orderableChildNodes;
        this.propertyDefinitions = new ArrayList<JcrPropertyDefinition>(propertyDefinitions.size());
        for (JcrPropertyDefinition property : propertyDefinitions) {
            this.propertyDefinitions.add(property.with(this));
        }

        this.childNodeDefinitions = new ArrayList<JcrNodeDefinition>(childNodeDefinitions.size());
        for (JcrNodeDefinition childNode : childNodeDefinitions) {
            this.childNodeDefinitions.add(childNode.with(this));
        }

        // Build the list of all types, including supertypes ...
        List<JcrNodeType> thisAndAllSupertypes = new LinkedList<JcrNodeType>();
        Set<Name> typeNames = new HashSet<Name>();
        thisAndAllSupertypes.add(this);
        typeNames.add(this.name);
        for (int i = 0; i != thisAndAllSupertypes.size(); ++i) {
            JcrNodeType superType = thisAndAllSupertypes.get(i);
            for (NodeType superSuperType : superType.getDeclaredSupertypes()) {
                JcrNodeType jcrSuperSuperType = (JcrNodeType)superSuperType;

                if (jcrSuperSuperType == null) {
                    assert JcrNtLexicon.BASE.equals(name);
                    continue;
                }

                if (typeNames.add(jcrSuperSuperType.getInternalName())) {
                    thisAndAllSupertypes.add(jcrSuperSuperType);
                }
            }
        }
        this.thisAndAllSupertypes = Collections.unmodifiableList(thisAndAllSupertypes);
        // Make the list of all supertypes to be a sublist of the first ...
        this.allSupertypes = thisAndAllSupertypes.size() > 1 ? thisAndAllSupertypes.subList(1, thisAndAllSupertypes.size()) : Collections.<JcrNodeType>emptyList();

        // Set up the set of all supertype names ...
        this.thisAndAllSupertypesNames = Collections.unmodifiableSet(typeNames);

        this.allDefinitions = new DefinitionCache(this);
        this.key = prototypeKey.withId("/jcr:system/jcr:nodeTypes/" + name.getString());
    }

    final NodeKey key() {
        return key;
    }

    List<JcrNodeType> getTypeAndSupertypes() {
        return thisAndAllSupertypes;
    }

    List<JcrNodeType> supertypes() {
        return allSupertypes;
    }

    /**
     * Get the child definitions defined on this node type (excluding inherited definitions).
     *
     * @return this node's child node definitions; never null
     */
    List<JcrNodeDefinition> childNodeDefinitions() {
        return childNodeDefinitions;
    }

    /**
     * Determine if there is at least one child node definition.
     * 
     * @return true if there is at least one child node definitions, or false if there are none
     */
    boolean hasChildNodeDefinitions() {
        return allDefinitions.anyChildNodeDefinitions();
    }

    /**
     * Get the property definitions defined on this node type (excluding inherited definitions).
     *
     * @return this node's property definitions; never null
     */
    List<JcrPropertyDefinition> propertyDefinitions() {
        return propertyDefinitions;
    }

    /**
     * Determine if there is at least one property definition.
     * 
     * @return true if there is at least one child node definitions, or false if there are none
     */
    boolean hasPropertyDefinitions() {
        return allDefinitions.anyPropertyDefinitions();
    }

    /**
     * Get all of the property definitions defined on this node type and its supertypes.
     *
     * @return this node's explicit and inherited property definitions; never null
     */
    Collection<JcrPropertyDefinition> allPropertyDefinitions() {
        return allDefinitions.allPropertyDefinitions();
    }

    Collection<JcrPropertyDefinition> allSingleValuePropertyDefinitions( Name propertyName ) {
        return allDefinitions.allSingleValuePropertyDefinitions(propertyName);
    }

    Collection<JcrPropertyDefinition> allMultiValuePropertyDefinitions( Name propertyName ) {
        return allDefinitions.allMultiValuePropertyDefinitions(propertyName);
    }

    Collection<JcrPropertyDefinition> allPropertyDefinitions( Name propertyName ) {
        return allDefinitions.allPropertyDefinitions(propertyName);
    }

    /**
     * Get all of the child node definitions defined on this node type and its supertypes.
     *
     * @return this node's explicit and inherited child node definitions; never null
     */
    Collection<JcrNodeDefinition> allChildNodeDefinitions() {
        return allDefinitions.allChildNodeDefinitions();
    }

    Collection<JcrNodeDefinition> allChildNodeDefinitions( Name childName,
                                                           boolean requireSns ) {
        return allDefinitions.allChildNodeDefinitions(childName, requireSns);
    }

    Collection<JcrNodeDefinition> allChildNodeDefinitions( Name childName ) {
        return allDefinitions.allChildNodeDefinitions(childName);
    }

    JcrNodeDefinition childNodeDefinition( NodeDefinitionId nodeDefnId ) {
        List<Name> requiredPrimaryTypeNames = Arrays.asList(nodeDefnId.getRequiredPrimaryTypes());
        for (JcrNodeDefinition nodeDefn : allChildNodeDefinitions(nodeDefnId.getChildDefinitionName())) {
            if (nodeDefn.requiredPrimaryTypeNameSet().size() == requiredPrimaryTypeNames.size()
                && nodeDefn.requiredPrimaryTypeNameSet().containsAll(requiredPrimaryTypeNames)) {
                return nodeDefn;
            }
        }
        return null;
    }

    @Override
    public boolean canAddChildNode( String childNodeName ) {
        CheckArg.isNotNull(childNodeName, "childNodeName");
        Name childName = context.getValueFactories().getNameFactory().create(childNodeName);
        boolean skipProtected = true;
        NodeDefinitionSet childDefns = nodeTypes().findChildNodeDefinitions(this.name, null);
        JcrNodeDefinition childNodeDefinition = childDefns.findBestDefinitionForChild(childName, null, skipProtected,
                                                                                      SiblingCounter.constant(0));
        // No primary node type was given, which means the child node definition (if found) must have a default type ...
        return childNodeDefinition != null && childNodeDefinition.getDefaultPrimaryType() != null;
    }

    @Override
    public boolean canAddChildNode( String childNodeName,
                                    String primaryNodeTypeName ) {

        CheckArg.isNotNull(childNodeName, "childNodeName");
        CheckArg.isNotNull(primaryNodeTypeName, "primaryNodeTypeName");
        Name childName = context.getValueFactories().getNameFactory().create(childNodeName);
        Name childPrimaryTypeName = context.getValueFactories().getNameFactory().create(primaryNodeTypeName);

        NodeTypes nodeTypes = nodeTypes();
        boolean skipProtected = true;
        NodeDefinitionSet childDefns = nodeTypes.findChildNodeDefinitions(this.name, null);
        JcrNodeDefinition childNodeDefinition = childDefns.findBestDefinitionForChild(childName, childPrimaryTypeName,
                                                                                      skipProtected, SiblingCounter.constant(0));
        if (childNodeDefinition == null) {
            // We found no valid child node definition ...
            return false;
        }
        if (RESIDUAL_ITEM_NAME.equals(childNodeDefinition.getName())) {
            // the TCK expects that for residual children definitions, this returns true
            return true;
        }
        if (childPrimaryTypeName != null) {
            // The child's primary node type name was given, so make sure that it is not abstract or a mixin.
            // If it were not for the residual item check (previous `if`), then we could do this at the top of the method.
            // But it has to go here.
            JcrNodeType childType = nodeTypes.getNodeType(childPrimaryTypeName);
            if (childType.isAbstract() || childType.isMixin()) return false;
        }

        return true;
    }

    @Override
    public boolean canRemoveNode( String itemName ) {
        CheckArg.isNotNull(itemName, "itemName");
        Name childName = context.getValueFactories().getNameFactory().create(itemName);
        return nodeTypes().canRemoveAllChildren(this.name, null, childName, true);
    }

    /**
     * {@inheritDoc}
     * <p>
     * In JCR 1.0, this method applied to all children. However, this was changed in the JSR-283 specification to apply only to
     * nodes, and it is also deprecated.
     * </p>
     *
     * @see javax.jcr.nodetype.NodeType#canRemoveItem(java.lang.String)
     */
    @Override
    public boolean canRemoveItem( String itemName ) {
        CheckArg.isNotNull(itemName, "itemName");
        Name childName = context.getValueFactories().getNameFactory().create(itemName);
        return nodeTypes().canRemoveItem(this.name, null, childName, true);
    }

    /**
     * Returns <code>true</code> if <code>value</code> can be cast to <code>property.getRequiredType()</code> per the type
     * conversion rules in section 3.6.4 of the JCR 2.0 specification AND <code>value</code> satisfies the constraints (if any)
     * for the property definition. If the property definition has a required type of {@link PropertyType#UNDEFINED}, the cast
     * will be considered to have succeeded and the value constraints (if any) will be interpreted using the semantics for the
     * type specified in <code>value.getType()</code>.
     *
     * @param session the session in which the constraints are to be checked; may not be null
     * @param propertyDefinition the property definition to validate against
     * @param value the value to be validated
     * @return <code>true</code> if the value can be cast to the required type for the property definition (if it exists) and
     *         satisfies the constraints for the property (if any exist).
     * @see PropertyDefinition#getValueConstraints()
     * @see JcrPropertyDefinition#satisfiesConstraints(Value,JcrSession)
     */
    boolean canCastToTypeAndMatchesConstraints( JcrSession session,
                                                JcrPropertyDefinition propertyDefinition,
                                                Value value ) {
        try {
            assert value instanceof JcrValue : "Illegal implementation of Value interface";
            ((JcrValue)value).asType(propertyDefinition.getRequiredType()); // throws ValueFormatException if there's a problem
            return propertyDefinition.satisfiesConstraints(value, session);
        } catch (javax.jcr.ValueFormatException vfe) {
            // Cast failed
            return false;
        }
    }

    /**
     * Returns <code>true</code> if <code>value</code> can be cast to <code>property.getRequiredType()</code> per the type
     * conversion rules in section 3.6.4 of the JCR 2.0 specification AND <code>value</code> satisfies the constraints (if any)
     * for the property definition. If the property definition has a required type of {@link PropertyType#UNDEFINED}, the cast
     * will be considered to have succeeded and the value constraints (if any) will be interpreted using the semantics for the
     * type specified in <code>value.getType()</code>.
     *
     * @param session the session in which the constraints are to be checked; may not be null
     * @param propertyDefinition the property definition to validate against
     * @param values the values to be validated
     * @return <code>true</code> if the value can be cast to the required type for the property definition (if it exists) and
     *         satisfies the constraints for the property (if any exist).
     * @see PropertyDefinition#getValueConstraints()
     * @see JcrPropertyDefinition#satisfiesConstraints(Value,JcrSession)
     */
    boolean canCastToTypeAndMatchesConstraints( JcrSession session,
                                                JcrPropertyDefinition propertyDefinition,
                                                Value[] values ) {
        for (Value value : values) {
            if (!canCastToTypeAndMatchesConstraints(session, propertyDefinition, value)) return false;
        }
        return true;
    }

    @Override
    public boolean canSetProperty( String propertyName,
                                   Value value ) {
        return canSetProperty(session, propertyName, value);
    }

    @Override
    public boolean canSetProperty( String propertyName,
                                   Value[] values ) {
        return canSetProperty(session, propertyName, values);
    }

    public boolean canSetProperty( JcrSession session,
                                   String propertyName,
                                   Value value ) {
        CheckArg.isNotNull(propertyName, "propertyName");
        Name name = context.getValueFactories().getNameFactory().create(propertyName);

        // Reuse the logic in RepositoryNodeTypeManager ...
        return nodeTypes().findPropertyDefinition(session, this.name, null, name, value, false, true) != null;
    }

    public boolean canSetProperty( JcrSession session,
                                   String propertyName,
                                   Value[] values ) {
        CheckArg.isNotNull(propertyName, "propertyName");
        if (values == null || values.length == 0) {
            return canRemoveProperty(propertyName);
        }

        Name name = context.getValueFactories().getNameFactory().create(propertyName);
        // Reuse the logic in RepositoryNodeTypeManager ...
        return nodeTypes().findPropertyDefinition(session, this.name, null, name, values, true) != null;
    }

    @Override
    public boolean canRemoveProperty( String propertyName ) {
        CheckArg.isNotNull(propertyName, "propertyName");
        Name name = context.getValueFactories().getNameFactory().create(propertyName);

        // Reuse the logic in RepositoryNodeTypeManager ...
        return nodeTypes().canRemoveProperty(this.name, null, name, true);
    }

    @Override
    public JcrNodeDefinition[] getDeclaredChildNodeDefinitions() {
        // Always have to make a copy to prevent changes ...
        return childNodeDefinitions.toArray(new JcrNodeDefinition[childNodeDefinitions.size()]);
    }

    @Override
    public JcrNodeDefinition[] getChildNodeDefinitions() {
        // Always have to make a copy to prevent changes ...
        Collection<JcrNodeDefinition> definitions = this.allDefinitions.allChildNodeDefinitions();
        return definitions.toArray(new JcrNodeDefinition[definitions.size()]);
    }

    @Override
    public JcrPropertyDefinition[] getPropertyDefinitions() {
        // Always have to make a copy to prevent changes ...
        Collection<JcrPropertyDefinition> definitions = this.allDefinitions.allPropertyDefinitions();
        return definitions.toArray(new JcrPropertyDefinition[definitions.size()]);
    }

    @Override
    public JcrNodeType[] getDeclaredSupertypes() {
        // Always have to make a copy to prevent changes ...
        return declaredSupertypes.toArray(new JcrNodeType[declaredSupertypes.size()]);
    }

    /**
     * {@inheritDoc}
     *
     * @return the array of names of supertypes declared for this node; possibly empty, never null
     */
    @Override
    public String[] getDeclaredSupertypeNames() {
        List<String> supertypeNames = new ArrayList<String>(declaredSupertypes.size());

        for (JcrNodeType declaredSupertype : declaredSupertypes) {
            supertypeNames.add(declaredSupertype.getName());
        }

        // Always have to make a copy to prevent changes ...
        return supertypeNames.toArray(new String[supertypeNames.size()]);
    }

    @Override
    public NodeTypeIterator getSubtypes() {
        return new JcrNodeTypeIterator(nodeTypes().subtypesFor(this));
    }

    @Override
    public NodeTypeIterator getDeclaredSubtypes() {
        return new JcrNodeTypeIterator(nodeTypes().declaredSubtypesFor(this));
    }

    @Override
    public String getName() {
        // Translate the name to the correct prefix. Need to check the session to support url-remapping.
        return name.getString(context.getNamespaceRegistry());
    }

    /**
     * Returns the internal {@link Name} object for the node type. This method exists outside the JCR API and should not be
     * exposed outside of the package.
     *
     * @return the internal {@link Name} object for the node type.
     */
    final Name getInternalName() {
        return name;
    }

    /**
     * Returns the internal {@link Name} object for the primary item of this node type. This method exists outside the JCR API and
     * should not be exposed outside of the package.
     *
     * @return the internal {@link Name} object for the primary item of this node type.
     */
    Name getInternalPrimaryItemName() {
        return primaryItemName;
    }

    @Override
    public String getPrimaryItemName() {
        if (primaryItemName == null) {
            return null;
        }

        // Translate the name to the correct prefix. Need to check the session to support url-remapping.
        return primaryItemName.getString(context.getNamespaceRegistry());
    }

    @Override
    public JcrPropertyDefinition[] getDeclaredPropertyDefinitions() {
        return propertyDefinitions.toArray(new JcrPropertyDefinition[propertyDefinitions.size()]);
    }

    @Override
    public NodeType[] getSupertypes() {
        return allSupertypes.toArray(new NodeType[allSupertypes.size()]);
    }

    @Override
    public boolean hasOrderableChildNodes() {
        return orderableChildNodes;
    }

    @Override
    public boolean isMixin() {
        return mixin;
    }

    @Override
    public boolean isAbstract() {
        return isAbstract;
    }

    @Override
    public boolean isQueryable() {
        return queryable;
    }

    @Override
    public boolean isNodeType( String nodeTypeName ) {
        if (nodeTypeName == null) return false;
        Name name = context.getValueFactories().getNameFactory().create(nodeTypeName);
        return this.thisAndAllSupertypesNames.contains(name);
    }

    boolean isNodeType( Name nodeTypeName ) {
        if (nodeTypeName == null) return false;
        return this.thisAndAllSupertypesNames.contains(nodeTypeName);
    }

    boolean isNodeTypeOneOf( Name... nodeTypeNames ) {
        if (nodeTypeNames == null || nodeTypeNames.length == 0) return false;
        for (Name nodeTypeName : nodeTypeNames) {
            if (this.thisAndAllSupertypesNames.contains(nodeTypeName)) return true;
        }
        return false;
    }

    @Override
    public String getLocalName() {
        return name.getLocalName();
    }

    @Override
    public String getNamespaceURI() {
        return name.getNamespaceUri();
    }

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

    @Override
    public boolean equals( Object obj ) {
        if (obj == this) return true;
        if (obj instanceof JcrNodeType) {
            JcrNodeType that = (JcrNodeType)obj;
            return this.name.equals(that.name);
        }
        if (obj instanceof NodeType) {
            NodeType that = (NodeType)obj;
            return this.getName().equals(that.getName());
        }
        return false;
    }

    @Override
    public String toString() {
        return getName();
    }

    /**
     * Returns a {@link JcrNodeType} that is equivalent to this {@link JcrNodeType}, except with a different repository node type
     * manager. This method should only be called during the initialization of the repository node type manager, unless some kind
     * of cross-repository type shipping is implemented.
     *
     * @param nodeTypeManager the new repository node type manager
     * @return a new {@link JcrNodeType} that has the same state as this node type, but with the given node type manager.
     */
    final JcrNodeType with( RepositoryNodeTypeManager nodeTypeManager ) {
        return new JcrNodeType(this.key, this.context, this.session, nodeTypeManager, this.name, this.declaredSupertypes,
                               this.primaryItemName, this.childNodeDefinitions, this.propertyDefinitions, this.mixin,
                               this.isAbstract, this.queryable, this.orderableChildNodes);
    }

    /**
     * Returns a {@link JcrNodeType} that is equivalent to this {@link JcrNodeType}, except with a different execution context.
     *
     * @param context the new execution context
     * @param session an active user session, in the context of which the node type is created. May be null, during system
     *        initialization.
     * @return a new {@link JcrNodeType} that has the same state as this node type, but with the given node type manager.
     */
    final JcrNodeType with( ExecutionContext context,
                            JcrSession session ) {
        return new JcrNodeType(this.key, context, session, this.nodeTypeManager, this.name, this.declaredSupertypes,
                               this.primaryItemName, this.childNodeDefinitions, this.propertyDefinitions, this.mixin,
                               this.isAbstract, this.queryable, this.orderableChildNodes);
    }

    final NodeTypes nodeTypes() {
        return nodeTypeManager.getNodeTypes();
    }

    final RepositoryNodeTypeManager nodeTypeManager() {
        return nodeTypeManager;
    }

    /**
     * Returns whether this node type is in conflict with the provided primary node type or mixin types.
     * <p>
     * A node type is in conflict with another set of node types if either of the following is true:
     * <ol>
     * <li>This node type has the same name as any of the other node types</li>
     * <li>This node type defines a property (or inherits the definition of a property) with the same name as a property defined
     * in any of the other node types <i>unless</i> this node type and the other node type both inherited the property definition
     * from the same ancestor node type</li>
     * <li>This node type defines a child node (or inherits the definition of a child node) with the same name as a child node
     * defined in any of the other node types <i>unless</i> this node type and the other node type both inherited the child node
     * definition from the same ancestor node type</li>
     * </ol>
     * </p>
     *
     * @param primaryNodeType the primary node type to check
     * @param mixinNodeTypes the mixin node types to check
     * @return true if this node type conflicts with the provided primary or mixin node types as defined below
     */
    final boolean conflictsWith( NodeType primaryNodeType,
                                 NodeType[] mixinNodeTypes ) {
        Map<PropertyDefinitionId, JcrPropertyDefinition> props = new HashMap<PropertyDefinitionId, JcrPropertyDefinition>();
        /*
         * Need to have the same parent name for all PropertyDefinitionIds and NodeDefinitionIds as we're checking for conflicts on the definition name
         */
        final Name DEFAULT_NAME = this.name;

        for (JcrPropertyDefinition property : propertyDefinitions()) {
            /*
             * I'm trying really hard to reuse existing code, but it's a stretch in this case.  I don't care about the property
             * types or where they were declared... if more than one definition with the given name exists (not counting definitions
             * inherited from the same root definition), then there is a conflict.
             */
            PropertyDefinitionId pid = new PropertyDefinitionId(DEFAULT_NAME, property.name, PropertyType.UNDEFINED,
                                                                property.isMultiple());
            props.put(pid, property);
        }

        /*
         * The specification does not mandate whether this should or should not be consider a conflict.  However, the Apache
         * TCK canRemoveMixin test cases assume that this will generate a conflict.
         */
        if (primaryNodeType.getName().equals(getName())) {
            // This node type has already been applied to the node
            return true;
        }

        for (JcrPropertyDefinition property : ((JcrNodeType)primaryNodeType).propertyDefinitions()) {
            PropertyDefinitionId pid = new PropertyDefinitionId(DEFAULT_NAME, property.name, PropertyType.UNDEFINED,
                                                                property.isMultiple());
            JcrPropertyDefinition oldProp = props.put(pid, property);
            if (oldProp != null) {
                String oldPropTypeName = oldProp.getDeclaringNodeType().getName();
                String propTypeName = property.getDeclaringNodeType().getName();
                if (!oldPropTypeName.equals(propTypeName)) {
                    // The two types conflict as both separately declare a property with the same name
                    return true;
                }
            }
        }

        for (NodeType mixinNodeType : mixinNodeTypes) {
            /*
             * The specification does not mandate whether this should or should not be consider a conflict.  However, the Apache
             * TCK canRemoveMixin test cases assume that this will generate a conflict.
             */
            if (mixinNodeType.getName().equals(getName())) {
                // This node type has already been applied to the node
                return true;
            }

            for (JcrPropertyDefinition property : ((JcrNodeType)mixinNodeType).propertyDefinitions()) {
                PropertyDefinitionId pid = new PropertyDefinitionId(DEFAULT_NAME, property.name, PropertyType.UNDEFINED,
                                                                    property.isMultiple());
                JcrPropertyDefinition oldProp = props.put(pid, property);
                if (oldProp != null) {
                    String oldPropTypeName = oldProp.getDeclaringNodeType().getName();
                    String propTypeName = property.getDeclaringNodeType().getName();
                    if (!oldPropTypeName.equals(propTypeName)) {
                        // The two types conflict as both separately declare a property with the same name
                        return true;
                    }
                }
            }
        }

        Map<NodeDefinitionId, JcrNodeDefinition> childNodes = new HashMap<NodeDefinitionId, JcrNodeDefinition>();

        for (JcrNodeDefinition childNode : childNodeDefinitions()) {
            NodeDefinitionId nid = new NodeDefinitionId(DEFAULT_NAME, childNode.name, new Name[0]);
            childNodes.put(nid, childNode);
        }

        for (JcrNodeDefinition childNode : ((JcrNodeType)primaryNodeType).childNodeDefinitions()) {
            NodeDefinitionId nid = new NodeDefinitionId(DEFAULT_NAME, childNode.name, new Name[0]);
            JcrNodeDefinition oldNode = childNodes.put(nid, childNode);
            if (oldNode != null) {
                String oldNodeTypeName = oldNode.getDeclaringNodeType().getName();
                String childNodeTypeName = childNode.getDeclaringNodeType().getName();
                if (!oldNodeTypeName.equals(childNodeTypeName)) {
                    // The two types conflict as both separately declare a child node with the same name
                    return true;
                }
            }
        }

        for (NodeType mixinNodeType : mixinNodeTypes) {
            for (JcrNodeDefinition childNode : ((JcrNodeType)mixinNodeType).childNodeDefinitions()) {
                NodeDefinitionId nid = new NodeDefinitionId(DEFAULT_NAME, childNode.name, new Name[0]);
                JcrNodeDefinition oldNode = childNodes.put(nid, childNode);
                if (oldNode != null) {
                    String oldNodeTypeName = oldNode.getDeclaringNodeType().getName();
                    String childNodeTypeName = childNode.getDeclaringNodeType().getName();
                    if (!oldNodeTypeName.equals(childNodeTypeName)) {
                        // The two types conflict as both separately declare a child node with the same name
                        return true;
                    }
                }
            }
        }

        return false;
    }
}
