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

import org.apache.jackrabbit.oak.api.PropertyState;
import org.apache.jackrabbit.oak.api.Type;
import org.apache.jackrabbit.oak.commons.collections.IterableUtils;
import org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState;
import org.apache.jackrabbit.oak.plugins.memory.MemoryChildNodeEntry;
import org.apache.jackrabbit.oak.plugins.memory.PropertyStates;
import org.apache.jackrabbit.oak.spi.state.AbstractNodeState;
import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry;
import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
import org.apache.jackrabbit.oak.spi.state.NodeState;
import org.apache.jackrabbit.oak.spi.state.NodeStateDiff;
import org.apache.jackrabbit.oak.spi.state.ReadOnlyBuilder;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

import static org.apache.jackrabbit.oak.plugins.tree.TreeConstants.OAK_CHILD_ORDER;

public abstract class AbstractDecoratedNodeState extends AbstractNodeState {

    protected final NodeState delegate;

    private final boolean useNativeEquals;

    protected AbstractDecoratedNodeState(@NotNull final NodeState delegate, boolean useNativeEquals) {
        this.delegate = delegate;
        this.useNativeEquals = useNativeEquals;
    }

    public NodeState getDelegate() {
        return delegate;
    }

    protected boolean hideChild(@NotNull final String name, @NotNull final NodeState delegateChild) {
        return false;
    }

    @NotNull
    protected abstract NodeState decorateChild(@NotNull final String name, @NotNull final NodeState delegateChild);

    @NotNull
    private NodeState decorate(@NotNull final String name, @NotNull final NodeState child) {
        return hideChild(name, child) ? EmptyNodeState.MISSING_NODE : decorateChild(name, child);
    }

    protected boolean hideProperty(@NotNull final String name) {
        return false;
    }

    @NotNull
    protected Iterable<PropertyState> getNewPropertyStates() {
        return Collections.emptyList();
    }

    @Nullable
    protected abstract PropertyState decorateProperty(@NotNull final PropertyState delegatePropertyState);

    @Nullable
    private PropertyState decorate(@Nullable final PropertyState property) {
        return property == null || hideProperty(property.getName()) ? null : decorateProperty(property);
    }

    /**
     * Convenience method to help implementations that hide nodes set the
     * :childOrder (OAK_CHILD_ORDER) property to its correct value.
     * <br>
     * Intended to be used to implement {@link #decorateProperty(PropertyState)}.
     *
     * @param nodeState The current node state.
     * @param propertyState The property that chould be checked.
     * @return The original propertyState, unless the property is called {@code :childOrder}.
     */
    protected static PropertyState fixChildOrderPropertyState(NodeState nodeState, PropertyState propertyState) {
        if (propertyState != null && OAK_CHILD_ORDER.equals(propertyState.getName())) {
            final Collection<String> childNodeNames = new ArrayList<String>();
            nodeState.getChildNodeNames().forEach(childNodeNames::add);
            final Iterable<String> values = IterableUtils.filter(
                    propertyState.getValue(Type.NAMES), x -> childNodeNames.contains(x));
            return PropertyStates.createProperty(OAK_CHILD_ORDER, values, Type.NAMES);
        }
        return propertyState;
    }

    /**
     * The AbstractDecoratedNodeState implementation returns a ReadOnlyBuilder, which
     * will fail for any mutable operation.
     *
     * This method can be overridden to return a different NodeBuilder implementation.
     *
     * @return a NodeBuilder instance corresponding to this NodeState.
     */
    @Override
    @NotNull
    public NodeBuilder builder() {
        return new ReadOnlyBuilder(this);
    }

    @Override
    public boolean exists() {
        return delegate.exists();
    }

    @Override
    public boolean hasChildNode(@NotNull final String name) {
        return getChildNode(name).exists();
    }

    @Override
    @NotNull
    public NodeState getChildNode(@NotNull final String name) throws IllegalArgumentException {
        return decorate(name, delegate.getChildNode(name));
    }

    @Override
    @NotNull
    public Iterable<? extends ChildNodeEntry> getChildNodeEntries() {
        final Iterable<ChildNodeEntry> transformed = IterableUtils.transform(delegate.getChildNodeEntries(), childNodeEntry -> {
            if (childNodeEntry != null) {
                final String name = childNodeEntry.getName();
                final NodeState nodeState = decorate(name, childNodeEntry.getNodeState());
                if (nodeState.exists()) {
                    return new MemoryChildNodeEntry(name, nodeState);
                }
            }
            return null;
        });
        return IterableUtils.filter(transformed, x -> x != null);
    }

    @Override
    @Nullable
    public PropertyState getProperty(@NotNull String name) {
        PropertyState ps = decorate(delegate.getProperty(name));
        if (ps == null) {
            for (PropertyState p : getNewPropertyStates()) {
                if (name.equals(p.getName())) {
                    ps = p;
                    break;
                }
            }
        }
        return ps;
    }

    @Override
    @NotNull
    public Iterable<? extends PropertyState> getProperties() {
        final Iterable<PropertyState> propertyStates = IterableUtils.transform(
                delegate.getProperties(),
                propertyState -> decorate(propertyState));
        return IterableUtils.filter(IterableUtils.chainedIterable(propertyStates, getNewPropertyStates()), Objects::nonNull);
    }

    /**
     * Note that any implementation-specific optimizations of wrapped NodeStates
     * will not work if a AbstractDecoratedNodeState is passed into their {@code #equals()}
     * method. This implementation will compare the wrapped NodeState, however. So
     * optimizations work when calling {@code #equals()} on a ReportingNodeState.
     *
     * @param other Object to compare with this NodeState.
     * @return true if the given object is equal to this NodeState, false otherwise.
     */
    @Override
    public boolean equals(final Object other) {
        if (!(other instanceof NodeState)) {
            return false;
        }

        if (useNativeEquals) {
            if (this.getClass() == other.getClass()) {
                final AbstractDecoratedNodeState o = (AbstractDecoratedNodeState) other;
                return delegate.equals(o.delegate);
            }
        }

        return AbstractDecoratedNodeState.equals(this, (NodeState) other);
    }

    @Override
    public boolean compareAgainstBaseState(final NodeState base, final NodeStateDiff diff) {
        NodeStateDiff decoratingDiff = new DecoratingDiff(diff, this);

        if (!comparePropertiesAgainstBaseState(this, base, decoratingDiff)) {
            return false;
        }

        Set<String> baseChildNodes = new HashSet<String>();
        for (ChildNodeEntry beforeCNE : base.getChildNodeEntries()) {
            String name = beforeCNE.getName();
            NodeState beforeChild = beforeCNE.getNodeState();
            NodeState afterChild = this.getChildNode(name);
            if (!afterChild.exists()) {
                if (!decoratingDiff.childNodeDeleted(name, beforeChild)) {
                    return false;
                }
            } else {
                baseChildNodes.add(name);
                if (!afterChild.equals(beforeChild)) { // TODO: fastEquals?
                    if (!decoratingDiff.childNodeChanged(name, beforeChild, afterChild)) {
                        return false;
                    }
                }
            }
        }

        for (ChildNodeEntry afterChild : this.getChildNodeEntries()) {
            String name = afterChild.getName();
            if (!baseChildNodes.contains(name)) {
                if (!decoratingDiff.childNodeAdded(name, afterChild.getNodeState())) {
                    return false;
                }
            }
        }

        return true;
    }

    private static class DecoratingDiff implements NodeStateDiff {

        private final NodeStateDiff diff;

        private AbstractDecoratedNodeState nodeState;

        private DecoratingDiff(final NodeStateDiff diff, final AbstractDecoratedNodeState nodeState) {
            this.diff = diff;
            this.nodeState = nodeState;
        }

        @Override
        public boolean childNodeAdded(final String name, final NodeState after) {
            return diff.childNodeAdded(name, nodeState.decorate(name, after));
        }

        @Override
        public boolean childNodeChanged(final String name, final NodeState before, final NodeState after) {
            return diff.childNodeChanged(name, before, nodeState.decorate(name, after));
        }

        @Override
        public boolean childNodeDeleted(final String name, final NodeState before) {
            return diff.childNodeDeleted(name, before);
        }

        @Override
        public boolean propertyAdded(final PropertyState after) {
            return diff.propertyAdded(nodeState.decorate(after));
        }

        @Override
        public boolean propertyChanged(final PropertyState before, final PropertyState after) {
            return diff.propertyChanged(nodeState.decorate(before), nodeState.decorate(after));
        }

        @Override
        public boolean propertyDeleted(final PropertyState before) {
            return diff.propertyDeleted(nodeState.decorate(before));
        }
    }
}
