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

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.Lists.newArrayListWithCapacity;
import static org.apache.jackrabbit.oak.api.Type.NAMES;
import static org.apache.jackrabbit.oak.plugins.tree.TreeConstants.OAK_CHILD_ORDER;

import java.util.List;

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.plugins.memory.PropertyStates;
import org.apache.jackrabbit.oak.plugins.tree.TreeConstants;
import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
 * {@code AbstractMutableTree} extends {@code AbstractTree} with implementations
 * for most write methods of {@code Tree}. Furthermore it handles the ordering
 * of siblings.
 */
public abstract class AbstractMutableTree extends AbstractTree {

    @Override
    public boolean remove() {
        String name = getName();
        AbstractTree parent = getParentOrNull();
        if (parent != null && parent.hasChild(name)) {
            getNodeBuilder().remove();
            NodeBuilder parentBuilder = parent.getNodeBuilder();
            PropertyState order = parentBuilder.getProperty(OAK_CHILD_ORDER);
            if (order != null) {
                List<String> names = newArrayListWithCapacity(order.count());
                for (String n : order.getValue(NAMES)) {
                    if (!n.equals(name)) {
                        names.add(n);
                    }
                }
                parentBuilder.setProperty(OAK_CHILD_ORDER, names, NAMES);
            }
            return true;
        } else {
            return false;
        }
    }

    @NotNull
    @Override
    public Tree addChild(@NotNull String name) throws IllegalArgumentException {
        checkArgument(!isHidden(name));
        if (!hasChild(name)) {
            NodeBuilder nodeBuilder = getNodeBuilder();
            nodeBuilder.setChildNode(name);
            PropertyState order = nodeBuilder.getProperty(OAK_CHILD_ORDER);
            if (order != null) {
                List<String> names = newArrayListWithCapacity(order.count() + 1);
                for (String n : order.getValue(NAMES)) {
                    if (!n.equals(name)) {
                        names.add(n);
                    }
                }
                names.add(name);
                nodeBuilder.setProperty(OAK_CHILD_ORDER, names, NAMES);
            }
        }
        return createChild(name);
    }

    @Override
    public void setOrderableChildren(boolean enable) {
        if (enable) {
            updateChildOrder(true);
        } else {
            getNodeBuilder().removeProperty(OAK_CHILD_ORDER);
        }
    }

    /**
     * Updates the child order to match any added or removed child nodes that
     * are not yet reflected in the {@link TreeConstants#OAK_CHILD_ORDER}
     * property. If the {@code force} flag is set, the child order is set
     * in any case, otherwise only if the node already is orderable.
     *
     * @param force whether to add child order information if it doesn't exist
     */
    protected void updateChildOrder(boolean force) {
        if (force || hasOrderableChildren()) {
            getNodeBuilder().setProperty(PropertyStates.createProperty(
                    OAK_CHILD_ORDER, getChildNames(), Type.NAMES));
        }
    }

    @Override
    public boolean orderBefore(@Nullable String name) {
        String thisName = getName();
        AbstractTree parent = getParentOrNull();
        if (parent == null) {
            return false; // root does not have siblings
        } else if (thisName.equals(name)) {
            return false; // same node
        }

        // perform the reorder
        List<String> names = newArrayListWithCapacity(10000);
        NodeBuilder builder = parent.getNodeBuilder();
        boolean found = false;

        // first try reordering based on the (potentially out-of-sync)
        // child order property in the parent node
        for (String n : builder.getNames(OAK_CHILD_ORDER)) {
            if (n.equals(name) && parent.hasChild(name)) {
                names.add(thisName);
                found = true;
            }
            if (!n.equals(thisName)) {
                names.add(n);
            }
        }

        // if the target node name was not found in the parent's child order
        // property, we need to fall back to recreating the child order list
        if (!found) {
            names.clear();
            for (String n : parent.getChildNames()) {
                if (n.equals(name)) {
                    names.add(thisName);
                    found = true;
                }
                if (!n.equals(thisName)) {
                    names.add(n);
                }
            }
        }

        if (name == null) {
            names.add(thisName);
            found = true;
        }

        if (found) {
            builder.setProperty(OAK_CHILD_ORDER, names, NAMES);
            return true;
        } else {
            // no such sibling (not existing or not accessible)
            return false;
        }
    }

    @Override
    public void setProperty(@NotNull PropertyState property) {
        checkArgument(!isHidden(checkNotNull(property).getName()));
        getNodeBuilder().setProperty(property);
    }

    @Override
    public <T> void setProperty(@NotNull String name, @NotNull T value) throws IllegalArgumentException {
        checkArgument(!isHidden(checkNotNull(name)));
        getNodeBuilder().setProperty(name, checkNotNull(value));
    }

    @Override
    public <T> void setProperty(@NotNull String name, @NotNull T value, @NotNull Type<T> type)
            throws IllegalArgumentException {
        checkArgument(!isHidden(checkNotNull(name)));
        getNodeBuilder().setProperty(name, checkNotNull(value), checkNotNull(type));
    }

    @Override
    public void removeProperty(@NotNull String name) {
        getNodeBuilder().removeProperty(checkNotNull(name));
    }
}
