/*
 * 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 org.apache.jackrabbit.guava.common.base.Objects;
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.tree.ReadOnly;
import org.apache.jackrabbit.oak.plugins.tree.TreeType;
import org.apache.jackrabbit.oak.plugins.tree.TreeTypeAware;
import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
import org.apache.jackrabbit.oak.spi.state.NodeState;
import org.apache.jackrabbit.oak.spi.state.ReadOnlyBuilder;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
 * Immutable implementation of the {@code Tree} interface in order to provide
 * the much feature rich API functionality for a given {@code NodeState}.
 *
 * <h3>Tree hierarchy</h3>
 * Due to the nature of this {@code Tree} implementation creating a proper
 * hierarchical view of the tree structure is the responsibility of the caller.
 * It is recommended to start with the state of the
 * {@link #ImmutableTree(org.apache.jackrabbit.oak.spi.state.NodeState) root node}
 * and build up the hierarchy by calling
 * {@link #ImmutableTree(ImmutableTree, String, org.apache.jackrabbit.oak.spi.state.NodeState)}
 * for every subsequent child state. Note, that this implementation will not
 * perform any kind of validation of the passed state and methods like {@link #isRoot()},
 * {@link #getName()} or {@link #getPath()} will just make use of the hierarchy that has been
 * create by that sequence. In order to create a disconnected individual tree in cases where
 * the hierarchy information is not (yet) need or known it is suggested to use
 * {@link #ImmutableTree(ImmutableTree.ParentProvider, String, org.apache.jackrabbit.oak.spi.state.NodeState)}
 * an specify an appropriate {@code ParentProvider} implementation.
 *
 * <h3>ParentProvider</h3>
 * Apart from create the tree hierarchy in traversal mode this tree implementation
 * allows to instantiate disconnected trees that depending on the use may
 * never or on demand retrieve hierarchy information. The following default
 * implementations of this internal interface are present:
 *
 * <ul>
 *     <li>{@link DefaultParentProvider}: used with the default usage where the
 *     parent tree is passed to the constructor</li>
 *     <li>{@link ParentProvider#ROOT_PROVIDER}: the default parent provider for
 *     the root tree. All children will get {@link DefaultParentProvider}</li>
 *     <li>{@link ParentProvider#UNSUPPORTED}: throws {@code UnsupportedOperationException}
 *     upon hierarchy related methods like {@link #getParent()}, {@link #getPath()}</li>
 * </ul>
 *
 * <h3>Filtering 'hidden' items</h3>
 * This {@code Tree} implementation reflects the item hierarchy as exposed by the
 * underlying {@code NodeState}. In contrast to the mutable implementations it
 * does not filter out 'hidden' items as identified by
 * {@code org.apache.jackrabbit.oak.spi.state.NodeStateUtils#isHidden(String)}.
 *
 * <h3>Equality and hash code</h3>
 * In contrast to {@link org.apache.jackrabbit.oak.plugins.tree.impl.AbstractMutableTree}
 * the {@code ImmutableTree} implements
 * {@link Object#equals(Object)} and {@link Object#hashCode()}: Two {@code ImmutableTree}s
 * are consider equal if their name and the underlying {@code NodeState}s are equal. Note
 * however, that according to the contract defined in {@code NodeState} these
 * objects are not expected to be used as hash keys.
 */
public final class ImmutableTree extends AbstractTree implements TreeTypeAware, ReadOnly {

    /**
     * Underlying node state
     */
    private final NodeBuilder nodeBuilder;

    /**
     * Name of this tree
     */
    private final String name;

    private final ParentProvider parentProvider;

    private String path;

    private TreeType type;

    public ImmutableTree(@NotNull NodeState rootState) {
        this(ParentProvider.ROOT_PROVIDER, PathUtils.ROOT_NAME, rootState);
    }

    public ImmutableTree(@NotNull ImmutableTree parent, @NotNull String name, @NotNull NodeState state) {
        this(new DefaultParentProvider(parent), name, state);
    }

    public ImmutableTree(@NotNull ParentProvider parentProvider, @NotNull String name, @NotNull NodeState state) {
        this.nodeBuilder = new ReadOnlyBuilder(state);
        this.name = name;
        this.parentProvider = parentProvider;
    }

    //----------------------------------------------------------< TypeAware >---
    @Nullable
    public TreeType getType() {
        return type;
    }

    public void setType(@NotNull TreeType type) {
        this.type = type;
    }

    //-------------------------------------------------------< AbstractTree >---
    @Override
    @NotNull
    protected ImmutableTree createChild(@NotNull String name) {
        return new ImmutableTree(this, name, nodeBuilder.getNodeState().getChildNode(name));
    }

    @Override
    public boolean isRoot() {
        return PathUtils.ROOT_NAME.equals(name);
    }

    @Override
    @Nullable
    protected AbstractTree getParentOrNull() {
        return parentProvider.getParent();
    }

    @NotNull
    @Override
    protected NodeBuilder getNodeBuilder() {
        return nodeBuilder;
    }

    @Override
    protected boolean isHidden(@NotNull String name) {
        return false;
    }

    @NotNull
    @Override
    protected String[] getInternalNodeNames() {
        return new String[0];
    }

    //---------------------------------------------------------------< Tree >---

    @NotNull
    @Override
    public String getName() {
        return name;
    }

    @Override
    @NotNull
    public String getPath() {
        if (path == null) {
            path = super.getPath();
        }
        return path;
    }

    @NotNull
    @Override
    public ImmutableTree getChild(@NotNull String name) throws IllegalArgumentException {
        return createChild(name);

    }

    @Override
    public boolean remove() {
        throw new UnsupportedOperationException();
    }

    @Override
    @NotNull
    public Tree addChild(@NotNull String name) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void setOrderableChildren(boolean enable) {
        throw new UnsupportedOperationException();
    }

    @Override
    public boolean orderBefore(@Nullable String name) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void setProperty(@NotNull PropertyState property) {
        throw new UnsupportedOperationException();
    }

    @Override
    public <T> void setProperty(@NotNull String name, @NotNull T value) {
        throw new UnsupportedOperationException();
    }

    @Override
    public <T> void setProperty(@NotNull String name, @NotNull T value, @NotNull Type<T> type) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void removeProperty(@NotNull String name) {
        throw new UnsupportedOperationException();
    }

    //-------------------------------------------------------------< Object >---

    @Override
    public int hashCode() {
        return Objects.hashCode(getName(), nodeBuilder.getNodeState());
    }

    @Override
    public boolean equals(Object o) {
        if (o == this) {
            return true;
        }
        if (o instanceof ImmutableTree) {
            ImmutableTree other = (ImmutableTree) o;
            return getName().equals(other.getName()) && nodeBuilder.getNodeState().equals(other.nodeBuilder.getNodeState());
        }
        return false;
    }

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

    public interface ParentProvider {

        ParentProvider UNSUPPORTED = new ParentProvider() {
            @Override
            public ImmutableTree getParent() {
                throw new UnsupportedOperationException("not supported.");
            }
        };

        ParentProvider ROOT_PROVIDER = new ParentProvider() {
            @Override
            public ImmutableTree getParent() {
                return null;
            }
        };

        @Nullable
        ImmutableTree getParent();
    }

    public static final class DefaultParentProvider implements ParentProvider {
        private final ImmutableTree parent;

        DefaultParentProvider(@NotNull ImmutableTree parent) {
            this.parent = parent;
        }

        @Override
        public ImmutableTree getParent() {
            return parent;
        }
    }

}
