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

import java.util.Calendar;
import java.util.List;
import java.util.Set;

import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import javax.jcr.RepositoryException;
import javax.jcr.nodetype.ConstraintViolationException;
import javax.jcr.nodetype.NoSuchNodeTypeException;

import com.google.common.base.Strings;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;

import org.apache.jackrabbit.JcrConstants;
import org.apache.jackrabbit.oak.api.PropertyState;
import org.apache.jackrabbit.oak.api.Tree;
import org.apache.jackrabbit.oak.api.Type;
import org.apache.jackrabbit.oak.commons.PathUtils;
import org.apache.jackrabbit.oak.plugins.identifier.IdentifierManager;
import org.apache.jackrabbit.oak.plugins.memory.PropertyStates;
import org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants;
import org.apache.jackrabbit.oak.plugins.tree.impl.ImmutableTree;
import org.apache.jackrabbit.util.ISO8601;

import static com.google.common.collect.Iterables.contains;
import static com.google.common.collect.Lists.newArrayList;
import static java.util.Collections.emptyList;
import static org.apache.jackrabbit.JcrConstants.JCR_AUTOCREATED;
import static org.apache.jackrabbit.JcrConstants.JCR_CREATED;
import static org.apache.jackrabbit.JcrConstants.JCR_DEFAULTPRIMARYTYPE;
import static org.apache.jackrabbit.JcrConstants.JCR_DEFAULTVALUES;
import static org.apache.jackrabbit.JcrConstants.JCR_HASORDERABLECHILDNODES;
import static org.apache.jackrabbit.JcrConstants.JCR_ISMIXIN;
import static org.apache.jackrabbit.JcrConstants.JCR_LASTMODIFIED;
import static org.apache.jackrabbit.JcrConstants.JCR_MIXINTYPES;
import static org.apache.jackrabbit.JcrConstants.JCR_MULTIPLE;
import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE;
import static org.apache.jackrabbit.JcrConstants.JCR_SAMENAMESIBLINGS;
import static org.apache.jackrabbit.JcrConstants.JCR_UUID;
import static org.apache.jackrabbit.oak.api.Type.BOOLEAN;
import static org.apache.jackrabbit.oak.api.Type.DATE;
import static org.apache.jackrabbit.oak.api.Type.NAME;
import static org.apache.jackrabbit.oak.api.Type.NAMES;
import static org.apache.jackrabbit.oak.api.Type.STRING;
import static org.apache.jackrabbit.oak.api.Type.STRINGS;
import static org.apache.jackrabbit.oak.commons.PathUtils.dropIndexFromName;
import static org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants.JCR_CREATEDBY;
import static org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants.JCR_IS_ABSTRACT;
import static org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants.JCR_LASTMODIFIEDBY;
import static org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants.REP_NAMED_CHILD_NODE_DEFINITIONS;
import static org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants.REP_NAMED_PROPERTY_DEFINITIONS;
import static org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants.REP_RESIDUAL_CHILD_NODE_DEFINITIONS;
import static org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants.REP_SUPERTYPES;

/**
 * Utility providing common operations for the {@code Tree} that are not provided
 * by the API.
 */
public final class TreeUtil {

    private TreeUtil() {
    }

    @CheckForNull
    public static String getPrimaryTypeName(@Nonnull Tree tree) {
        return getStringInternal(tree, JcrConstants.JCR_PRIMARYTYPE, Type.NAME);
    }

    @CheckForNull
    public static Iterable<String> getStrings(@Nonnull Tree tree, @Nonnull String propertyName) {
        PropertyState property = tree.getProperty(propertyName);
        if (property == null) {
            return null;
        } else {
            return property.getValue(STRINGS);
        }
    }

    @CheckForNull
    public static String getString(@Nonnull Tree tree, @Nonnull String propertyName) {
        return getStringInternal(tree, propertyName, Type.STRING);
    }

    @CheckForNull
    private static String getStringInternal(@Nonnull Tree tree,
                                            @Nonnull String propertyName,
                                            @Nonnull Type<String> type) {
        PropertyState property = tree.getProperty(propertyName);
        if (property != null && !property.isArray()) {
            return property.getValue(type);
        } else {
            return null;
        }
    }

    /**
     * Returns the boolean representation of the property with the specified
     * {@code propertyName}. If the property does not exist or
     * {@link org.apache.jackrabbit.oak.api.PropertyState#isArray() is an array}
     * this method returns {@code false}.
     *
     * @param tree         The target tree.
     * @param propertyName The name of the property.
     * @return the boolean representation of the property state with the given
     *         name. This utility returns {@code false} if the property does not exist
     *         or is an multivalued property.
     */
    public static boolean getBoolean(@Nonnull Tree tree, @Nonnull String propertyName) {
        PropertyState property = tree.getProperty(propertyName);
        return property != null && !property.isArray() && property.getValue(BOOLEAN);
    }

    @CheckForNull
    public static String getName(@Nonnull Tree tree, @Nonnull String name) {
        PropertyState property = tree.getProperty(name);
        if (property != null && property.getType() == NAME) {
            return property.getValue(NAME);
        } else {
            return null;
        }
    }

    @Nonnull
    public static Iterable<String> getNames(@Nonnull Tree tree, @Nonnull String name) {
        PropertyState property = tree.getProperty(name);
        if (property != null && property.getType() == NAMES) {
            return property.getValue(NAMES);
        } else {
            return emptyList();
        }
    }

    /**
     * Return the possibly non existing tree located at the passed {@code path} from
     * the location of the start {@code tree} or {@code null} if {@code path} results
     * in a parent of the root.
     *
     * @param tree  start tree
     * @param path  path from the start tree
     * @return  tree located at {@code path} from {@code start} or {@code null}
     */
    @CheckForNull
    public static Tree getTree(@Nonnull Tree tree, @Nonnull String path) {
        for (String element : PathUtils.elements(path)) {
            if (PathUtils.denotesParent(element)) {
                if (tree.isRoot()) {
                    return null;
                } else {
                    tree = tree.getParent();
                }
            } else if (!PathUtils.denotesCurrent(element)) {
                tree = tree.getChild(element);
            }  // else . -> skip to next element
        }
        return tree;
    }

    public static Tree addChild(
            @Nonnull Tree parent, @Nonnull String name,
            @CheckForNull String typeName, @Nonnull Tree typeRoot,
            @CheckForNull String userID) throws RepositoryException {
        if (typeName == null) {
            typeName = getDefaultChildType(typeRoot, parent, name);
            if (typeName == null) {
                String path = PathUtils.concat(parent.getPath(), name);
                throw new ConstraintViolationException(
                        "No default node type available for " + path);
            }
        }

        Tree type = typeRoot.getChild(typeName);
        if (!type.exists()) {
            throw new NoSuchNodeTypeException(
                    "Node type " + typeName + " does not exist");
        } else if (getBoolean(type, JCR_IS_ABSTRACT)
                // OAK-1013: backwards compatibility for abstract default types
                && !typeName.equals(getDefaultChildType(typeRoot, parent, name))) {
            throw new ConstraintViolationException(
                    "Node type " + typeName + " is abstract");
        } else if (getBoolean(type, JCR_ISMIXIN)) {
            throw new ConstraintViolationException(
                    "Node type " + typeName + " is a mixin type");
        }

        Tree child = parent.addChild(name);
        child.setProperty(JCR_PRIMARYTYPE, typeName, NAME);
        if (getBoolean(type, JCR_HASORDERABLECHILDNODES)) {
            child.setOrderableChildren(true);
        }
        autoCreateItems(child, type, typeRoot, userID);
        return child;
    }

    public static void addMixin(@Nonnull Tree tree, @Nonnull String mixinName, @Nonnull Tree typeRoot, @CheckForNull String userID) throws RepositoryException {
        Tree type = typeRoot.getChild(mixinName);
        if (!type.exists()) {
            throw new NoSuchNodeTypeException(
                    "Node type " + mixinName + " does not exist");
        } else if (getBoolean(type, JCR_IS_ABSTRACT)) {
            throw new ConstraintViolationException(
                    "Node type " + mixinName + " is abstract");
        } else if (!getBoolean(type, JCR_ISMIXIN)) {
            throw new ConstraintViolationException(
                    "Node type " + mixinName + " is a not a mixin type");
        }

        List<String> mixins = Lists.newArrayList();
        String primary = getName(tree, JCR_PRIMARYTYPE);
        if (primary != null
                && Iterables.contains(getNames(type, NodeTypeConstants.REP_PRIMARY_SUBTYPES), primary)) {
            return;
        }

        Set<String> subMixins = Sets.newHashSet(getNames(type, NodeTypeConstants.REP_MIXIN_SUBTYPES));
        for (String mixin : getNames(tree, NodeTypeConstants.JCR_MIXINTYPES)) {
            if (mixinName.equals(mixin) || subMixins.contains(mixin)) {
                return;
            }
            mixins.add(mixin);
        }

        mixins.add(mixinName);
        tree.setProperty(JcrConstants.JCR_MIXINTYPES, mixins, NAMES);

        autoCreateItems(tree, type, typeRoot, userID);
    }

    public static void autoCreateItems(@Nonnull Tree tree, @Nonnull Tree type, @Nonnull Tree typeRoot, @CheckForNull String userID)
            throws RepositoryException {
        // TODO: use a separate rep:autoCreatePropertyDefinitions
        Tree properties = type.getChild(REP_NAMED_PROPERTY_DEFINITIONS);
        for (Tree definitions : properties.getChildren()) {
            String name = definitions.getName();
            if (name.equals(NodeTypeConstants.REP_PRIMARY_TYPE)
                    || name.equals(NodeTypeConstants.REP_MIXIN_TYPES)) {
                continue;
            } else if (name.equals(NodeTypeConstants.REP_UUID)) {
                name = JCR_UUID;
            }
            for (Tree definition : definitions.getChildren()) {
                if (getBoolean(definition, JCR_AUTOCREATED)) {
                    if (!tree.hasProperty(name)) {
                        PropertyState property =
                                autoCreateProperty(name, definition, userID);
                        if (property != null) {
                            tree.setProperty(property);
                        } else {
                            throw new RepositoryException(
                                    "Unable to auto-create value for "
                                    + PathUtils.concat(tree.getPath(), name));
                        }
                    }
                    break;
                }
            }
        }

        // TODO: use a separate rep:autoCreateChildNodeDefinitions
        // Note that we use only named, non-SNS child node definitions
        // as there can be no reasonable default values for residual or
        // SNS child nodes
        Tree childNodes = type.getChild(REP_NAMED_CHILD_NODE_DEFINITIONS);
        for (Tree definitions : childNodes.getChildren()) {
            String name = definitions.getName();
            for (Tree definition : definitions.getChildren()) {
                if (getBoolean(definition, JCR_AUTOCREATED)) {
                    if (!tree.hasChild(name)) {
                        String typeName =
                                getName(definition, JCR_DEFAULTPRIMARYTYPE);
                        addChild(tree, name, typeName, typeRoot, userID);
                    }
                    break;
                }
            }
        }
    }

    public static PropertyState autoCreateProperty(@Nonnull String name,
                                                   @Nonnull Tree definition,
                                                   @CheckForNull String userID) {
        if (JCR_UUID.equals(name)) {
            String uuid = IdentifierManager.generateUUID();
            return PropertyStates.createProperty(name, uuid, STRING);
        } else if (JCR_CREATED.equals(name)) {
            String now = ISO8601.format(Calendar.getInstance());
            return PropertyStates.createProperty(name, now, DATE);
        } else if (JCR_CREATEDBY.equals(name)) {
            return PropertyStates.createProperty(name, Strings.nullToEmpty(userID), STRING);
        } else if (JCR_LASTMODIFIED.equals(name)) {
            String now = ISO8601.format(Calendar.getInstance());
            return PropertyStates.createProperty(name, now, DATE);
        } else if (JCR_LASTMODIFIEDBY.equals(name)) {
            return PropertyStates.createProperty(name, Strings.nullToEmpty(userID), STRING);
        }

        // does the definition have a default value?
        PropertyState values = definition.getProperty(JCR_DEFAULTVALUES);
        if (values != null) {
            Type<?> type = values.getType();
            if (getBoolean(definition, JCR_MULTIPLE)) {
                return PropertyStates.createProperty(
                        name, values.getValue(type), type);
            } else if (values.count() > 0) {
                type = type.getBaseType();
                return PropertyStates.createProperty(
                        name, values.getValue(type, 0), type);
            }
        }

        return null;
    }

    /**
     * Finds the default primary type for a new child node with the given name.
     *
     * @param typeRoot root of the {@code /jcr:system/jcr:nodeTypes} tree
     * @param parent parent node
     * @param childName name of the new child node
     * @return name of the default type, or {@code null} if not available
     */
    public static String getDefaultChildType(
            Tree typeRoot, Tree parent, String childName) {
        String name = dropIndexFromName(childName);
        boolean sns = !name.equals(childName);
        List<Tree> types = getEffectiveType(parent, typeRoot);

        // first look for named node definitions
        for (Tree type : types) {
            Tree definitions = type
                    .getChild(REP_NAMED_CHILD_NODE_DEFINITIONS)
                    .getChild(name);
            String defaultName = findDefaultPrimaryType(definitions, sns);
            if (defaultName != null) {
                return defaultName;
            }
        }

        // then check residual definitions
        for (Tree type : types) {
            Tree definitions = type
                    .getChild(REP_RESIDUAL_CHILD_NODE_DEFINITIONS);
            String defaultName = findDefaultPrimaryType(definitions, sns);
            if (defaultName != null) {
                return defaultName;
            }
        }

        // no matching child node definition found
        return null;
    }

    /**
     * Returns the effective node types of the given node.
     */
    public static List<Tree> getEffectiveType(Tree tree, Tree typeRoot) {
        List<Tree> types = newArrayList();

        String primary = getName(tree, JCR_PRIMARYTYPE);
        if (primary != null) {
            Tree type = typeRoot.getChild(primary);
            if (type.exists()) {
                types.add(type);
            }
        }

        for (String mixin : getNames(tree, JCR_MIXINTYPES)) {
            Tree type = typeRoot.getChild(mixin);
            if (type.exists()) {
                types.add(type);
            }
        }

        return types;
    }

    public static String findDefaultPrimaryType(Tree definitions, boolean sns) {
        for (Tree definition : definitions.getChildren()) {
            String defaultName = getName(definition, JCR_DEFAULTPRIMARYTYPE);
            if (defaultName != null
                    && (!sns || getBoolean(definition, JCR_SAMENAMESIBLINGS))) {
                return defaultName;
            }
        }
        return null;
    }

    public static boolean isNodeType(Tree tree, String typeName, Tree typeRoot) {
        String primaryName = TreeUtil.getName(tree, JCR_PRIMARYTYPE);
        if (typeName.equals(primaryName)) {
            return true;
        } else if (primaryName != null) {
            Tree type = typeRoot.getChild(primaryName);
            if (contains(getNames(type, REP_SUPERTYPES), typeName)) {
                return true;
            }
        }

        for (String mixinName : getNames(tree, JCR_MIXINTYPES)) {
            if (typeName.equals(mixinName)) {
                return true;
            } else {
                Tree type = typeRoot.getChild(mixinName);
                if (contains(getNames(type, REP_SUPERTYPES), typeName)) {
                    return true;
                }
            }
        }

        return false;
    }


    /**
     * Returns {@code true} if the specified {@code tree} is a read-only tree
     * such as obtained through {@link org.apache.jackrabbit.oak.plugins.tree.TreeFactory}
     * or a {@link org.apache.jackrabbit.oak.plugins.tree.RootFactory read-only Root}.
     *
     * @param tree The tree object to be tested.
     * @return {@code true} if the specified tree is an immutable read-only tree.
     * @see org.apache.jackrabbit.oak.plugins.tree.TreeFactory#createReadOnlyTree(org.apache.jackrabbit.oak.spi.state.NodeState)
     * @see org.apache.jackrabbit.oak.plugins.tree.RootFactory#createReadOnlyRoot(org.apache.jackrabbit.oak.spi.state.NodeState)
     */
    public static boolean isReadOnlyTree(@Nonnull Tree tree) {
        return tree instanceof ImmutableTree;
    }
}