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

import org.apache.jackrabbit.commons.flat.TreeTraverser.ErrorHandler;
import org.apache.jackrabbit.commons.flat.TreeTraverser.InclusionPolicy;

import javax.jcr.ItemExistsException;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.PathNotFoundException;
import javax.jcr.Property;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.Value;

import java.util.Comparator;
import java.util.Iterator;
import java.util.Set;

/**
 * <p>
 * This class serves as main entry point for obtaining sequences of {@link Node}
 * s and {@link Property}s. It provides factory methods for creating
 * {@link NodeSequence}s and {@link PropertySequence}s.
 * </p>
 *
 * <p>
 * NodeSequence and PropertySequence instances provide a flat representation of
 * a JCR hierarchy rooted at a certain node. They allow iterating over all
 * items, retrieving items by key, checking whether a given key is mapped,
 * adding new items and removing existing items.
 * </p>
 *
 * <p>
 * The specifics of the mapping from the flat representation to the JCR
 * hierarchy are delegated to a {@link TreeManager}. Particularly the
 * TreeManager specifies the order of the items when retrieved as sequence and
 * when and how to add and remove intermediate nodes when new items are inserted
 * or removed.
 * </p>
 *
 * <p>
 * An {@link ErrorHandler} is used to handle exceptions which occur while
 * traversing the hierarchy.
 * </p>
 *
 * @see TreeTraverser
 * @see NodeSequence
 * @see PropertySequence
 * @see TreeManager
 */
public abstract class ItemSequence {

    /**
     * The {@link TreeManager} instance managing the mapping between the
     * sequence view and the JCR hierarchy.
     */
    protected final TreeManager treeManager;

    /**
     * The {@link ErrorHandler} instance used for handling exceptions occurring
     * while traversing the hierarchy.
     */
    protected final ErrorHandler errorHandler;

    /**
     * @see TreeManager#getRoot()
     */
    protected final Node root;

    /**
     * @see TreeManager#getOrder()
     */
    protected final Comparator<String> order;

    /**
     * @see TreeManager#getAutoSave()
     */
    protected final boolean autoSave;

    /**
     * Create a new {@link ItemSequence} instance.
     *
     * @param treeManager The {@link TreeManager} for managing the mapping
     *            between the sequence view and the JCR hierarchy.
     * @param errorHandler The {@link ErrorHandler} for handling exceptions
     *            occurring while traversing the hierarchy.
     * @throws IllegalArgumentException If either <code>treeManager</code> is
     *             <code>null</code> or {@link TreeManager#getRoot()} return
     *             <code>null</code> or {@link TreeManager#getOrder()} return
     *             <code>null</code>.
     */
    protected ItemSequence(TreeManager treeManager, ErrorHandler errorHandler) {
        super();

        if (treeManager == null) {
            throw new IllegalArgumentException("tree manager must not be null");
        }
        if (treeManager.getRoot() == null) {
            throw new IllegalArgumentException("root must not be null");
        }
        if (treeManager.getOrder() == null) {
            throw new IllegalArgumentException("order must not be null");
        }

        this.treeManager = treeManager;
        this.errorHandler = errorHandler;
        this.root = treeManager.getRoot();
        this.order = treeManager.getOrder();
        this.autoSave = treeManager.getAutoSave();
    }

    /**
     * Create a new {@link NodeSequence} instance.
     *
     * @param treeManager The {@link TreeManager} for managing the mapping
     *            between the sequence view and the JCR hierarchy.
     * @param errorHandler The {@link ErrorHandler} for handling exceptions
     *            occurring while
     * @return
     */
    public static NodeSequence createNodeSequence(TreeManager treeManager, ErrorHandler errorHandler) {
        return new NodeSequenceImpl(treeManager, errorHandler);
    }

    /**
     * Create a new {@link NodeSequence} instance.
     *
     * @param treeManager The {@link TreeManager} for managing the mapping
     *            between the sequence view and the JCR hierarchy.
     * @return
     */
    public static NodeSequence createNodeSequence(TreeManager treeManager) {
        return new NodeSequenceImpl(treeManager, ErrorHandler.IGNORE);
    }

    /**
     * Create a new {@link PropertySequence} instance.
     *
     * @param treeManager The {@link TreeManager} for managing the mapping
     *            between the sequence view and the JCR hierarchy.
     * @param errorHandler The {@link ErrorHandler} for handling exceptions
     *            occurring while
     * @return
     */
    public static PropertySequence createPropertySequence(TreeManager treeManager, ErrorHandler errorHandler) {
        return new PropertySequenceImpl(treeManager, errorHandler);
    }

    /**
     * Create a new {@link PropertySequence} instance.
     *
     * @param treeManager The {@link TreeManager} for managing the mapping
     *            between the sequence view and the JCR hierarchy.
     * @return
     */
    public static PropertySequence createPropertySequence(TreeManager treeManager) {
        return new PropertySequenceImpl(treeManager, ErrorHandler.IGNORE);
    }

    /**
     * Create a new {@link NodeSequence} instance with the same parameterization
     * as this instance.
     *
     * @return
     */
    public NodeSequence getNodeSequence() {
        return new NodeSequenceImpl(treeManager, errorHandler);
    }

    /**
     * Create a new {@link PropertySequence} instance with the same
     * parametrization as this instance.
     *
     * @return
     */
    public PropertySequence getPropertySequence() {
        return new PropertySequenceImpl(treeManager, errorHandler);
    }

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

    /**
     * Returns the parent node for the given key. When the key is not present in
     * this sequence already, the returned node is the node that would contain
     * that key if it where present.
     */
    protected abstract Node getParent(String key) throws RepositoryException;

    /**
     * Returns the predecessor node for the given
     * <code>key</key>. That is the node
     * whose key directly precedes the passed <code>key</code> in the order
     * determined by {@link TreeManager#getOrder()}. There are two cases:
     * <ul>
     * <li>A node with the given <code>key</code> is mapped: then that node is
     * returned.</li>
     * <li>A node with the given <code>key</code> is not mapped: the the node
     * where that would contain that key if present is returned.</li>
     * </ul>
     */
    protected final Node getPredecessor(String key) throws RepositoryException {
        Node p = root;
        Node n;
        while ((n = getPredecessor(p, key)) != null) {
            p = n;
        }

        return p;
    }

    /**
     * Returns the direct predecessor of <code>key</code> amongst
     * <code>node</code>'s child nodes wrt. to {@link TreeManager#getOrder()}.
     * Returns <code>null</code> if either <code>node</code> has no child nodes
     * or <code>node</code> is a leaf (see {@link TreeManager#isLeaf(Node)}) or
     * <code>key</code> is smaller than all the keys of all child nodes of
     * <code>node</code>.
     */
    protected final Node getPredecessor(Node node, String key) throws RepositoryException {
        if (!node.hasNodes() || treeManager.isLeaf(node)) {
            return null;
        }

        // Shortcut for exact match
        try {
            return node.getNode(key);
        }
        catch (PathNotFoundException ignore) { }

        // Search for direct predecessor of key in the nodes children
        // todo performance: for ordered nodes use binary search
        NodeIterator childNodes = node.getNodes();
        Node p = null;
        while (childNodes.hasNext()) {
            Node n = childNodes.nextNode();
            String childKey = n.getName();
            if (order.compare(key, childKey) > 0 && (p == null || order.compare(childKey, p.getName()) > 0)) {
                p = n;
            }
        }

        return p;
    }

    /**
     * Returns the successor node for the given
     * <code>key</key>. That is the node
     * whose key directly succeeds the passed <code>key</code> in the order
     * determined by {@link TreeManager#getOrder()}. There are two cases:
     * <ul>
     * <li>A node with the given <code>key</code> is mapped: then that node is
     * returned.</li>
     * <li>A node with the given <code>key</code> is not mapped: the the node
     * where that would contain that key if present is returned.</li>
     * </ul>
     */
    protected final Node getSuccessor(Node node, String key) throws RepositoryException {
        if (!node.hasNodes() || treeManager.isLeaf(node)) {
            return null;
        }

        // Shortcut for exact match
        try {
            return node.getNode(key);
        }
        catch (PathNotFoundException ignore) { }

        // Search for direct successor of key in the nodes children
        // todo performance: for ordered nodes use binary search
        NodeIterator childNodes = node.getNodes();
        Node s = null;
        while (childNodes.hasNext()) {
            Node n = childNodes.nextNode();
            String childKey = n.getName();
            if (order.compare(key, childKey) < 0 && (s == null || order.compare(childKey, s.getName()) < 0)) {
                s = n;
            }
        }

        return s;
    }

    /**
     * Returns the node with the minimal key wrt. {@link TreeManager#getOrder()}.
     * For the empty sequence this is {@link TreeManager#getRoot()}.
     */
    protected final Node getMinimal() throws RepositoryException {
        Node p = null;
        Node n = root;
        while ((n = getMinimal(n)) != null) {
            p = n;
        }

        return p;
    }

    /**
     * Returns the node amongst the child nodes of <code>node</code> whose key
     * is minimal wrt. {@link TreeManager#getOrder()}. Returns <code>null</code>
     * id either <code>node</code> has no child nodes or <code>node</code> is a
     * leaf (see {@link TreeManager#isLeaf(Node)}).
     */
    protected final Node getMinimal(Node node) throws RepositoryException {
        if (!node.hasNodes() || treeManager.isLeaf(node)) {
            return null;
        }

        // Search for minimal key in the nodes children
        // todo performance: for ordered nodes use binary search
        NodeIterator childNodes = node.getNodes();
        Node p = childNodes.nextNode();
        String minKey = p.getName();
        while (childNodes.hasNext()) {
            Node n = childNodes.nextNode();
            if (order.compare(n.getName(), minKey) < 0) {
                p = n;
                minKey = p.getName();
            }
        }

        return p;
    }

    /**
     * Rename the path of the node with the minimal key. That is, assuming
     * <code>node</code> is the node with the minimal key (see
     * {@link #getMinimal()}), this method renames every segment of the path of
     * <code>node</code> up to {@link TreeManager#getRoot()} to <code>key</code>
     * . <em>Note: </em> If <code>node</code> is not the node with the minimal
     * key, the behavior of this method is not specified.
     */
    protected final void renamePath(Node node, String key) throws RepositoryException {
        if (!treeManager.isRoot(node)) {
            Node p = node.getParent();
            renamePath(p, key);
            Session s = node.getSession();
            s.move(node.getPath(), p.getPath() + "/" + key);

            // If orderable, move to first child node
            if (p.getPrimaryNodeType().hasOrderableChildNodes()) {
                p.orderBefore(key, p.getNodes().nextNode().getName());
            }
        }
    }

    protected static class NodeSequenceImpl extends ItemSequence implements NodeSequence {
        private final InclusionPolicy<Node> inclusionPolicy = new InclusionPolicy<Node>() {
            public boolean include(Node node) {
                try {
                    return treeManager.isLeaf(node);
                }
                catch (RepositoryException e) {
                    return false;
                }
            }
        };

        public NodeSequenceImpl(TreeManager treeManager, ErrorHandler errorHandler)  {
            super(treeManager, errorHandler);
        }

        public Iterator<Node> iterator() {
            return TreeTraverser.nodeIterator(root, errorHandler, inclusionPolicy);
        }

        public Node getItem(String key) throws RepositoryException {
            return getParent(key).getNode(key);
        }

        public boolean hasItem(String key) throws RepositoryException {
            return getParent(key).hasNode(key);
        }

        public Node addNode(String key, String primaryNodeTypeName) throws RepositoryException {
            Node parent = getOrCreateParent(key);
            if (parent.hasNode(key)) {
                throw new ItemExistsException(key);
            }

            Node n;
            if (parent.getPrimaryNodeType().hasOrderableChildNodes()) {
                Node dest = getSuccessor(parent, key);
                n = parent.addNode(key, primaryNodeTypeName);
                parent.orderBefore(key, dest == null ? null : dest.getName());
            }
            else {
                n = parent.addNode(key, primaryNodeTypeName);
            }

            treeManager.split(this, parent, n);

            if (autoSave) {
                parent.getSession().save();
            }

            return n;
        }

        public void removeNode(String key) throws RepositoryException {
            Node parent = getParent(key);
            Node n = parent.getNode(key);
            n.remove();
            treeManager.join(this, parent, n);

            if (autoSave) {
                parent.getSession().save();
            }
        }

        @Override
        public Node getParent(String key) throws RepositoryException {
            Node p = getPredecessor(key);
            if (treeManager.isLeaf(p) && !treeManager.isRoot(p)) {
                return p.getParent();
            }
            else {
                return p;
            }
        }

        private Node getOrCreateParent(String key) throws RepositoryException {
            Node p = getParent(key);
            if (treeManager.isRoot(p)) {
                Node min = getMinimal();
                if (min != null) {
                    p = min.getParent();
                    renamePath(p, key);
                }
            }
            return p;
        }

    }

    protected static class PropertySequenceImpl extends ItemSequence implements PropertySequence {
        private final InclusionPolicy<Property> inclusionPolicy = new InclusionPolicy<Property>() {
            private final Set<String> ignoredProperties = treeManager.getIgnoredProperties();
            public boolean include(Property property) {
                try {
                    return !ignoredProperties.contains(property.getName());
                }
                catch (RepositoryException e) {
                    return false;
                }
            }
        };

        public PropertySequenceImpl(TreeManager treeManager, ErrorHandler errorHandler) {
            super(treeManager, errorHandler);
        }

        public Iterator<Property> iterator() {
            return TreeTraverser.propertyIterator(getNodeSequence().iterator(), errorHandler, inclusionPolicy);
        }

        public Property getItem(String key) throws RepositoryException {
            return getParent(key).getProperty(key);
        }

        public boolean hasItem(String key) throws RepositoryException {
            return getParent(key).hasProperty(key);
        }

        public Property addProperty(String key, Value value) throws RepositoryException {
            Node parent = getOrCreateParent(key);
            if (parent.hasProperty(key)) {
                throw new ItemExistsException(key);
            }

            Property p = parent.setProperty(key, value);
            treeManager.split(this, parent, p);

            if (autoSave) {
                p.getSession().save();
            }

            return p;
        }

        public void removeProperty(String key) throws RepositoryException {
            Node parent = getParent(key);
            Property p = parent.getProperty(key);
            p.remove();
            treeManager.join(this, parent, p);

            if (autoSave) {
                parent.getSession().save();
            }
        }

        @Override
        public Node getParent(String key) throws RepositoryException {
            return getPredecessor(key);
        }

        private Node getOrCreateParent(String key) throws RepositoryException {
            Node p = getParent(key);
            if (treeManager.isRoot(p)) {
                Node min = getMinimal();
                if (min != null) {
                    p = min;
                    renamePath(p, key);
                }
            }
            return p;
        }

    }

}
