/*
 * 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.jackrabbit.oak.plugins.nodetype;

import static java.util.Objects.requireNonNull;
import static org.apache.jackrabbit.JcrConstants.JCR_MIXINTYPES;
import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE;
import static org.apache.jackrabbit.oak.api.Type.STRING;
import static org.apache.jackrabbit.oak.commons.PathUtils.dropIndexFromName;
import static org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants.NODE_TYPES_PATH;
import static org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants.REP_SUPERTYPES;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;

import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.UnsupportedRepositoryOperationException;
import javax.jcr.ValueFactory;
import javax.jcr.nodetype.NoSuchNodeTypeException;
import javax.jcr.nodetype.NodeDefinition;
import javax.jcr.nodetype.NodeDefinitionTemplate;
import javax.jcr.nodetype.NodeType;
import javax.jcr.nodetype.NodeTypeDefinition;
import javax.jcr.nodetype.NodeTypeIterator;
import javax.jcr.nodetype.NodeTypeManager;
import javax.jcr.nodetype.NodeTypeTemplate;
import javax.jcr.nodetype.PropertyDefinition;
import javax.jcr.nodetype.PropertyDefinitionTemplate;

import org.apache.jackrabbit.JcrConstants;
import org.apache.jackrabbit.commons.iterator.NodeTypeIteratorAdapter;
import org.apache.jackrabbit.oak.api.PropertyState;
import org.apache.jackrabbit.oak.api.Root;
import org.apache.jackrabbit.oak.api.Tree;
import org.apache.jackrabbit.oak.api.Type;
import org.apache.jackrabbit.oak.commons.collections.IterableUtils;
import org.apache.jackrabbit.oak.namepath.NameMapper;
import org.apache.jackrabbit.oak.namepath.NamePathMapper;
import org.apache.jackrabbit.oak.namepath.impl.NamePathMapperImpl;
import org.apache.jackrabbit.oak.spi.nodetype.DefinitionProvider;
import org.apache.jackrabbit.oak.spi.nodetype.EffectiveNodeType;
import org.apache.jackrabbit.oak.spi.nodetype.EffectiveNodeTypeProvider;
import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
 * Base implementation of a {@link NodeTypeManager} with support for reading
 * node types from the {@link Tree} returned by {@link #getTypes()}. Methods
 * related to node type modifications throw
 * {@link UnsupportedRepositoryOperationException}.
 */
public abstract class ReadOnlyNodeTypeManager implements NodeTypeManager, EffectiveNodeTypeProvider, DefinitionProvider {

    /**
     * Returns the internal name for the specified JCR name.
     *
     * @param jcrName JCR node type name.
     * @return the internal representation of the given JCR name.
     * @throws javax.jcr.RepositoryException If there is no valid internal representation
     * of the specified JCR name.
     */
    @NotNull
    protected final String getOakName(String jcrName) throws RepositoryException {
        return getNamePathMapper().getOakName(jcrName);
    }

    /**
     * Returns the {@link Tree} instance where the node types are stored. This
     * method never returns {@code null} and may return a {@code Tree} that
     * does not exist (see {@link Tree#exists()} when there are no types stored.
     *
     * @return {@link Tree} instance where the node types are stored.
     */
    @NotNull
    protected abstract Tree getTypes();

    /**
     * The value factory to be used by {@link org.apache.jackrabbit.oak.plugins.nodetype.PropertyDefinitionImpl#getDefaultValues()}.
     * If {@code null} the former returns {@code null}.
     * @return  {@code ValueFactory} instance or {@code null}.
     */
    @Nullable
    protected ValueFactory getValueFactory() {
        return null;
    }

    /**
     * Returns a {@link NameMapper} to be used by this node type manager. This
     * implementation returns the {@link NamePathMapperImpl#DEFAULT} instance. A
     * subclass may override this method and provide a different
     * implementation.
     *
     * @return {@link NameMapper} instance.
     */
    @NotNull
    protected NamePathMapper getNamePathMapper() {
        return NamePathMapper.DEFAULT;
    }

    //--------------------------------------------------------------------------

    /**
     * Return a new instance of {@code ReadOnlyNodeTypeManager} that reads node
     * type information from the tree at {@link NodeTypeConstants#NODE_TYPES_PATH}.
     *
     * @param root The root to read node types from.
     * @param namePathMapper The {@code NamePathMapper} to use.
     * @return a new instance of {@code ReadOnlyNodeTypeManager}.
     */
    @NotNull
    public static ReadOnlyNodeTypeManager getInstance(final Root root,
                                                      final NamePathMapper namePathMapper) {
        return new ReadOnlyNodeTypeManager() {
            @NotNull
            @Override
            protected Tree getTypes() {
                return root.getTree(NODE_TYPES_PATH);
            }

            @NotNull
            @Override
            protected NamePathMapper getNamePathMapper() {
                return namePathMapper;
            }
        };
    }

    //----------------------------------------------------< NodeTypeManager >---

    @Override
    public boolean hasNodeType(String name) throws RepositoryException {
        return getTypes().hasChild(getOakName(name));
    }

    @Override
    public NodeType getNodeType(String name) throws RepositoryException {
        return internalGetNodeType(getOakName(name));
    }

    @Override
    public NodeTypeIterator getAllNodeTypes() throws RepositoryException {
        List<NodeType> list = new ArrayList<>();
        Tree types = getTypes();
        NamePathMapper mapper = getNamePathMapper();
        for (Tree type : types.getChildren()) {
            list.add(new NodeTypeImpl(type, mapper));
        }
        return new NodeTypeIteratorAdapter(list);
    }

    @Override
    public NodeTypeIterator getPrimaryNodeTypes() throws RepositoryException {
        List<NodeType> list = new ArrayList<>();
        NodeTypeIterator iterator = getAllNodeTypes();
        while (iterator.hasNext()) {
            NodeType type = iterator.nextNodeType();
            if (!type.isMixin()) {
                list.add(type);
            }
        }
        return new NodeTypeIteratorAdapter(list);
    }

    @Override
    public NodeTypeIterator getMixinNodeTypes() throws RepositoryException {
        List<NodeType> list = new ArrayList<>();
        NodeTypeIterator iterator = getAllNodeTypes();
        while (iterator.hasNext()) {
            NodeType type = iterator.nextNodeType();
            if (type.isMixin()) {
                list.add(type);
            }
        }
        return new NodeTypeIteratorAdapter(list);
    }

    @Override
    public NodeTypeTemplate createNodeTypeTemplate()
            throws RepositoryException {
        throw new UnsupportedRepositoryOperationException();
    }

    @Override
    public NodeTypeTemplate createNodeTypeTemplate(NodeTypeDefinition ntd)
            throws RepositoryException {
        throw new UnsupportedRepositoryOperationException();
    }

    @Override
    public NodeDefinitionTemplate createNodeDefinitionTemplate()
            throws RepositoryException {
        throw new UnsupportedRepositoryOperationException();
    }

    @Override
    public PropertyDefinitionTemplate createPropertyDefinitionTemplate()
            throws RepositoryException {
        throw new UnsupportedRepositoryOperationException();
    }

    /**
     * This implementation always throws a {@link UnsupportedRepositoryOperationException}.
     */
    @Override
    public NodeType registerNodeType(NodeTypeDefinition ntd, boolean allowUpdate) throws RepositoryException {
        throw new UnsupportedRepositoryOperationException();
    }

    /**
     * This implementation always throws a {@link UnsupportedRepositoryOperationException}.
     */
    @Override
    public NodeTypeIterator registerNodeTypes(NodeTypeDefinition[] ntds, boolean allowUpdate) throws RepositoryException {
        throw new UnsupportedRepositoryOperationException();
    }

    /**
     * This implementation always throws a {@link UnsupportedRepositoryOperationException}.
     */
    @Override
    public void unregisterNodeType(String name) throws RepositoryException {
        throw new UnsupportedRepositoryOperationException();
    }

    /**
     * This implementation always throws a {@link UnsupportedRepositoryOperationException}.
     */
    @Override
    public void unregisterNodeTypes(String[] names) throws RepositoryException {
        throw new UnsupportedRepositoryOperationException();
    }

    //------------------------------------------< EffectiveNodeTypeProvider >---

    @Override
    public boolean isNodeType(@NotNull Tree tree, @NotNull String oakNtName) {
        // shortcuts for common cases
        if (JcrConstants.NT_BASE.equals(oakNtName)) {
            return true;
        } else if (JcrConstants.MIX_REFERENCEABLE.equals(oakNtName)
                && !tree.hasProperty(JcrConstants.JCR_UUID)) {
            return false;
        } else if (JcrConstants.MIX_VERSIONABLE.equals(oakNtName)
                && !tree.hasProperty(JcrConstants.JCR_ISCHECKEDOUT)) {
            return false;
        }

        Tree types = getTypes();

        PropertyState primary = tree.getProperty(JcrConstants.JCR_PRIMARYTYPE);
        if (primary != null && primary.getType() == Type.NAME) {
            String name = primary.getValue(Type.NAME);
            if (isa(types, name, oakNtName)) {
                return true;
            }
        }

        PropertyState mixins = tree.getProperty(JcrConstants.JCR_MIXINTYPES);
        if (mixins != null && mixins.getType() == Type.NAMES) {
            for (String name : mixins.getValue(Type.NAMES)) {
                if (isa(types, name, oakNtName)) {
                    return true;
                }
            }
        }

        return false;
    }

    @Override
    public boolean isNodeType(@Nullable String primaryTypeName, @NotNull Supplier<Iterable<String>> mixinTypes, @NotNull String nodeTypeName) {
        // shortcut
        if (JcrConstants.NT_BASE.equals(nodeTypeName)) {
            return true;
        }
        Tree types = getTypes();
        if (primaryTypeName != null && isa(types, primaryTypeName, nodeTypeName)) {
            return true;
        }
        for (String mixin : mixinTypes.get()) {
            if (isa(types, mixin, nodeTypeName)) {
                return true;
            }
        }
        return false;
    }

    private static boolean isa(Tree types, String typeName, String superName) {
        if (typeName.equals(superName)) {
            return true;
        }

        Tree type = types.getChild(typeName);
        if (!type.exists()) {
            return false;
        }

        PropertyState supertypes = type.getProperty(REP_SUPERTYPES);
        return supertypes != null
                && IterableUtils.contains(supertypes.getValue(Type.NAMES), superName);
    }

    @Override
    public boolean isNodeType(@NotNull String typeName, @NotNull String superName) {
        return isa(getTypes(), typeName, superName);
    }

    /**
     * Returns all the node types of the given node, in a breadth-first
     * traversal order of the type hierarchy.
     *
     * @param node node instance
     * @return all types of the given node
     * @throws RepositoryException if the type information can not be accessed
     */
    @NotNull
    @Override
    public EffectiveNodeType getEffectiveNodeType(@NotNull Node node)
            throws RepositoryException {
        NodeTypeImpl primary = (NodeTypeImpl) node.getPrimaryNodeType(); // FIXME
        NodeType[] mixins = node.getMixinNodeTypes();
        NodeTypeImpl[] mixinImpls = new NodeTypeImpl[mixins.length];
        for (int i = 0; i < mixins.length; i++) {
            mixinImpls[i] = (NodeTypeImpl) mixins[i]; // FIXME
        }
        return new EffectiveNodeTypeImpl(primary, mixinImpls, this);
    }

    @NotNull
    @Override
    public EffectiveNodeType getEffectiveNodeType(@NotNull Tree tree) throws RepositoryException {
        NodeTypeImpl primaryType;
        PropertyState jcrPrimaryType = tree.getProperty(JCR_PRIMARYTYPE);
        if (jcrPrimaryType != null) {
            String ntName = jcrPrimaryType.getValue(STRING);
            primaryType = internalGetNodeType(ntName);
        } else {
            throw new RepositoryException("Node at "+tree.getPath()+" has no primary type.");
        }

        PropertyState jcrMixinType = tree.getProperty(JCR_MIXINTYPES);
        if (jcrMixinType == null) {
            return new EffectiveNodeTypeImpl(primaryType, this);
        } else {
            NodeTypeImpl[] mixinTypes = new NodeTypeImpl[jcrMixinType.count()];
            for (int i = 0; i < mixinTypes.length; i++) {
                mixinTypes[i] = internalGetNodeType(jcrMixinType.getValue(Type.NAME, i));
            }
            return new EffectiveNodeTypeImpl(primaryType, mixinTypes, this);
        }
    }

    //-------------------------------------------------< DefinitionProvider >---

    @NotNull
    @Override
    public NodeDefinition getRootDefinition() throws RepositoryException {
        return new RootNodeDefinition(this);
    }

    @NotNull
    @Override
    public NodeDefinition getDefinition(@NotNull Tree parent, @NotNull String nodeName)
            throws RepositoryException {
        requireNonNull(parent);
        requireNonNull(nodeName);
        EffectiveNodeType effective = getEffectiveNodeType(parent);
        return effective.getNodeDefinition(nodeName, null);
    }

    @NotNull
    @Override
    public NodeDefinition getDefinition(@NotNull Tree parent, @NotNull Tree targetNode)
            throws RepositoryException {
        requireNonNull(parent);
        requireNonNull(targetNode);

        String name = dropIndexFromName(targetNode.getName());
        EffectiveNodeType eff = getEffectiveNodeType(parent);
        return eff.getNodeDefinition(name, getEffectiveNodeType(targetNode));
    }

    @NotNull
    @Override
    public PropertyDefinition getDefinition(@NotNull Tree parent, @NotNull PropertyState property, boolean exactTypeMatch)
            throws RepositoryException {
        Type<?> type = property.getType();
        EffectiveNodeType effective = getEffectiveNodeType(parent);
        return effective.getPropertyDefinition(
                property.getName(), type.isArray(), type.tag(), exactTypeMatch);
    }

    //-----------------------------------------------------------< internal >---

    @NotNull
    NodeTypeImpl internalGetNodeType(@NotNull String oakName) throws NoSuchNodeTypeException {
        Tree types = getTypes();
        Tree type = types.getChild(oakName);
        if (type.exists()) {
            return new NodeTypeImpl(type, getNamePathMapper());
        }
        throw new NoSuchNodeTypeException(getNamePathMapper().getJcrName(oakName));
    }

}
