/*
 * Copyright (C) 2011 eXo Platform SAS.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.exoplatform.portal.mop.navigation;

import java.util.*;

import org.exoplatform.portal.tree.list.ListTree;

import lombok.Getter;
import lombok.Setter;

/**
 * The context of a node.
 */
public final class NodeContext<N> extends ListTree<NodeContext<N>> {

    /** The owner tree. */
    final TreeContext<N> tree;

    /** The related model node. */
    final N node;

    /** The handle: either the persistent id or a sequence id. */
    String handle;

    /** A data snapshot. */
    @Getter
    @Setter
    NodeData data;

    /** The new name if any. */
    String name;

    /** The new state if any. */
    NodeState state;

    /** Whether or not this node is hidden. */
    private boolean hidden;

    /** The number of hidden children. */
    private int hiddenCount;

    /** The expension value. */
    private boolean expanded;

    NodeContext(NodeModel<N> model, NodeData data) {
        if (data == null) {
            throw new NullPointerException();
        }

        //
        this.handle = data.id;
        this.name = null;
        this.tree = new TreeContext<N>(model, this);
        this.node = tree.model.create(this);
        this.data = data;
        this.state = null;
        this.hidden = false;
        this.hiddenCount = 0;
        this.expanded = false;
    }

    private NodeContext(TreeContext<N> tree, NodeData data) {
        if (data == null) {
            throw new NullPointerException();
        }

        //
        this.handle = data.id;
        this.name = null;
        this.tree = tree;
        this.node = tree.model.create(this);
        this.data = data;
        this.state = null;
        this.hidden = false;
        this.hiddenCount = 0;
        this.expanded = false;
    }

    NodeContext(TreeContext<N> tree, String handle, String name, NodeState state, boolean expanded) {
        if (handle == null) {
            throw new NullPointerException();
        }
        if (name == null) {
            throw new NullPointerException();
        }
        if (state == null) {
            throw new NullPointerException();
        }

        //
        this.handle = handle;
        this.name = name;
        this.tree = tree;
        this.node = tree.model.create(this);
        this.data = null;
        this.state = state;
        this.hidden = false;
        this.hiddenCount = 0;
        this.expanded = expanded;
    }

    /**
     * Returns true if the tree containing this node has pending transient changes.
     *
     * @return true if there are uncommited changes
     */
    public boolean hasChanges() {
        return tree.hasChanges();
    }

    /**
     * Returns an unmodifiable list of uncommitted changes applied to this node context.
     *
     * @return the list of uncommitted changes
     */
    public List<NodeChange<NodeContext<N>>> getChanges() {
        return Collections.unmodifiableList(tree.getChanges());
    }

    /**
     * Returns the associated node with this context
     *
     * @return the node
     */
    public N getNode() {
        return node;
    }

    /**
     * Returns the context id or null if the context is not associated with a persistent navigation node.
     *
     * @return the id
     */
    public String getId() {
        return data != null ? data.getId() : null;
    }

    /**
     * Returns the context index among its parent.
     *
     * @return the index value
     */
    public int getIndex() {
        int count = 0;
        for (NodeContext<N> node = getPrevious(); node != null; node = node.getPrevious()) {
            count++;
        }
        return count;
    }

    public boolean isExpanded() {
        return expanded;
    }

    void expand() {
        if (!expanded) {
            this.expanded = true;
        } else {
            throw new IllegalStateException("Context is already expanded");
        }
    }

    /**
     * Returns true if the context is currently hidden.
     *
     * @return the hidden value
     */
    public boolean isHidden() {
        return hidden;
    }

    /**
     * Updates the hiddent value.
     *
     * @param hidden the hidden value
     */
    public void setHidden(boolean hidden) {
        if (this.hidden != hidden) {
            NodeContext<N> parent = getParent();
            if (parent != null) {
                if (hidden) {
                    parent.hiddenCount++;
                } else {
                    parent.hiddenCount--;
                }
            }
            this.hidden = hidden;
        }
    }

    public NodeState getState() {
        if (state != null) {
            return state;
        } else {
            return data.getState();
        }
    }

    /**
     * Update the context state
     *
     * @param state the new state
     * @throws NullPointerException if the state is null
     */
    public void setState(NodeState state) throws NullPointerException {
        if (state == null) {
            throw new NullPointerException("No null state accepted");
        }

        //
        tree.addChange(new NodeChange.Updated<NodeContext<N>>(this, state));
    }

    public String getName() {
        return name != null ? name : data.name;
    }

    /**
     * Rename this context.
     *
     * @param name the new name
     * @throws NullPointerException if the name is null
     * @throws IllegalStateException if the parent is null
     * @throws IllegalArgumentException if the parent already have a child with the specified name
     */
    public void setName(String name) throws NullPointerException, IllegalStateException, IllegalArgumentException {
        NodeContext<N> parent = getParent();
        if (parent == null) {
            throw new IllegalStateException("Cannot rename a node when its parent is not visible");
        } else {
            NodeContext<N> blah = parent.get(name);
            if (blah != null) {
                if (blah == this) {
                    // We do nothing
                } else {
                    throw new IllegalArgumentException("the node " + name + " already exist");
                }
            } else {
                tree.addChange(new NodeChange.Renamed<>(getParent(), this, name));
            }
        }
    }

    /**
     * Applies a filter recursively, the filter will update the hiddent status of the fragment.
     *
     * @param filter the filter to apply
     */
    public void filter(NodeFilter filter) {
        if (expanded) {
            for (NodeContext<N> node = getFirst(); node != null; node = node.getNext()) {
                node.filter(filter);
            }
        }
        if (accept(filter)) {
          setHidden(false);
        } else if (filter.isHideOnlyPage() && getNodeSize() > 0) {
          if (state != null) {
            state = state.builder().pageRef(null).build();
          }
          if (data != null && data.getState() != null) {
            data = new NodeData(this);
            data.state = data.state.builder().pageRef(null).build();
          }
          setHidden(false);
        } else {
          setHidden(true);
        }
    }

    /**
     * Apply the filter logic on this node only. This method will not modify the state of this node.
     *
     * @param filter the filter to apply
     * @return true if the filter accepts this node
     */
    public boolean accept(NodeFilter filter) {
        return filter.accept(getDepth(), getId(), getName(), getState());
    }

    /**
     * Returns the relative depth of this node with respect to the ancestor argument.
     *
     * @param ancestor the ancestor
     * @return the depth
     * @throws IllegalArgumentException if the ancestor argument is not an ancestor
     * @throws NullPointerException if the ancestor argument is null
     */
    public int getDepth(NodeContext<N> ancestor) throws IllegalArgumentException, NullPointerException {
        if (ancestor == null) {
            throw new NullPointerException();
        }
        int depth = 0;
        for (NodeContext<N> current = this; current != null; current = current.getParent()) {
            if (current == ancestor) {
                return depth;
            } else {
                depth++;
            }
        }
        throw new IllegalArgumentException("Context " + ancestor + " is not an ancestor of " + this);
    }

    public NodeContext<N> getDescendant(String handle) throws NullPointerException {
        if (handle == null) {
            throw new NullPointerException();
        }

        //
        NodeContext<N> found = null;
        if (this.handle.equals(handle)) {
            found = this;
        } else {
            if (expanded) {
                for (NodeContext<N> current = getFirst(); current != null; current = current.getNext()) {
                    found = current.getDescendant(handle);
                    if (found != null) {
                        break;
                    }
                }
            }
        }
        return found;
    }

    public NodeContext<N> get(String name) throws NullPointerException, IllegalStateException {
        if (name == null) {
            throw new NullPointerException();
        }
        if (!expanded) {
            throw new IllegalStateException("No children relationship");
        }

        //
        for (NodeContext<N> node = getFirst(); node != null; node = node.getNext()) {
            if (node.getName().equals(name)) {
                return node;
            }
        }

        //
        return null;
    }

    /**
     * Add a child node at the specified index with the specified name. If the index argument is null then the node is added at
     * the last position among the children otherwise the node is added at the specified index.
     *
     * @param index the index
     * @param name the node name
     * @return the created node
     * @throws NullPointerException if the model or the name is null
     * @throws IndexOutOfBoundsException if the index is negative or greater than the children size
     * @throws IllegalStateException if the children relationship does not exist
     */
    public NodeContext<N> add(Integer index, String name) throws NullPointerException, IndexOutOfBoundsException,
            IllegalStateException {
        if (name == null) {
            throw new NullPointerException("No null name accepted");
        }

        //
        NodeContext<N> nodeContext = new NodeContext<N>(tree, "seq_" + tree.sequence++, name, NodeState.INITIAL, true);
        _add(index, nodeContext);
        return nodeContext;
    }

    /**
     * Move a context as a child context of this context at the specified index. If the index argument is null then the context
     * is added at the last position among the children otherwise the context is added at the specified index.
     *
     * @param index the index
     * @param context the context to move
     * @throws NullPointerException if the model or the context is null
     * @throws IndexOutOfBoundsException if the index is negative or greater than the children size
     * @throws IllegalStateException if the children relationship does not exist
     */
    public void add(Integer index, NodeContext<N> context) throws NullPointerException, IndexOutOfBoundsException,
            IllegalStateException {
        if (context == null) {
            throw new NullPointerException("No null context argument accepted");
        }

        //
        _add(index, context);
    }

    public NodeContext<N> insertLast(NodeData data) {
        if (data == null) {
            throw new NullPointerException("No null data argument accepted");
        }

        //
        NodeContext<N> context = new NodeContext<N>(tree, data);
        insertLast(context);
        return context;
    }

    public NodeContext<N> insertAt(Integer index, NodeData data) {
        if (data == null) {
            throw new NullPointerException("No null data argument accepted");
        }

        //
        NodeContext<N> context = new NodeContext<N>(tree, data);
        insertAt(index, context);
        return context;
    }

    public NodeContext<N> insertAfter(NodeData data) {
        if (data == null) {
            throw new NullPointerException("No null data argument accepted");
        }

        //
        NodeContext<N> context = new NodeContext<N>(tree, data);
        insertAfter(context);
        return context;
    }

    private void _add(final Integer index, NodeContext<N> child) {
        NodeContext<N> previousParent = child.getParent();

        //
        NodeContext<N> previous;
        if (index == null) {
            NodeContext<N> before = getLast();
            while (before != null && before.isHidden()) {
                before = before.getPrevious();
            }
            if (before == null) {
                previous = null;
            } else {
                previous = before;
            }
        } else if (index < 0) {
            throw new IndexOutOfBoundsException("No negative index accepted");
        } else if (index == 0) {
            previous = null;
        } else {
            NodeContext<N> before = getFirst();
            if (before == null) {
                throw new IndexOutOfBoundsException("Index " + index + " is greater than 0");
            }
            for (int count = index; count > 1; count -= before.isHidden() ? 0 : 1) {
                before = before.getNext();
                if (before == null) {
                    throw new IndexOutOfBoundsException("Index " + index + " is greater than the number of children "
                            + (index - count));
                }
            }
            previous = before;
        }

        //
        if (previousParent != null) {
            tree.addChange(new NodeChange.Moved<NodeContext<N>>(previousParent, this, previous, child));
        } else {
            // The name should never be null as it's a newly created node
            tree.addChange(new NodeChange.Created<NodeContext<N>>(this, previous, child, child.getName()));
        }
    }

    // Node related methods

    /**
     * Returns the total number of nodes.
     *
     * @return the total number of nodes
     */
    public int getNodeSize() {
        if (expanded) {
            return getSize();
        } else {
            return data.children.length;
        }
    }

    /**
     * Returns the node count defined by:
     * <ul>
     * <li>when the node has a children relationship, the number of non hidden nodes</li>
     * <li>when the node has not a children relationship, the total number of nodes</li>
     * </ul>
     *
     * @return the node count
     */
    public int getNodeCount() {
        if (expanded) {
            return getSize() - hiddenCount;
        } else {
            return data.children.length;
        }
    }

    public N getParentNode() {
        NodeContext<N> parent = getParent();
        return parent != null ? parent.node : null;
    }

    public N getNode(String name) throws NullPointerException {
        NodeContext<N> child = get(name);
        return child != null && !child.hidden ? child.node : null;
    }

    public N getNode(int index) {
        if (index < 0) {
            throw new IndexOutOfBoundsException("Index " + index + " cannot be negative");
        }
        if (!expanded) {
            throw new IllegalStateException("No children relationship");
        }
        NodeContext<N> context = getFirst();
        while (context != null && (context.hidden || index-- > 0)) {
            context = context.getNext();
        }
        if (context == null) {
            throw new IndexOutOfBoundsException("Index " + index + " is out of bounds");
        } else {
            return context.node;
        }
    }

    public N getDescendantNode(String handle) throws NullPointerException {
        NodeContext<N> descendant = getDescendant(handle);
        return descendant != null && !descendant.hidden ? descendant.node : null;
    }

    public Iterator<N> iterator() {
        return new Iterator<N>() {
            NodeContext<N> next = getFirst();
            {
                while (next != null && next.isHidden()) {
                    next = next.getNext();
                }
            }

            public boolean hasNext() {
                return next != null;
            }

            public N next() {
                if (next != null) {
                    NodeContext<N> tmp = next;
                    do {
                        next = next.getNext();
                    } while (next != null && next.isHidden());
                    return tmp.getNode();
                } else {
                    throw new NoSuchElementException();
                }
            }

            public void remove() {
                throw new UnsupportedOperationException();
            }
        };
    }

    /** . */
    private Collection<N> nodes;

    public Collection<N> getNodes() {
        if (expanded) {
            if (nodes == null) {
                nodes = new AbstractCollection<N>() {
                    public Iterator<N> iterator() {
                        return NodeContext.this.iterator();
                    }

                    public int size() {
                        return getNodeCount();
                    }
                };
            }
            return nodes;
        } else {
            return null;
        }
    }

    /**
     * Remove a specified context when it is not hidden.
     *
     * @param name the name of the context to remove
     * @return true if the context was removed
     * @throws NullPointerException if the name argument is null
     * @throws IllegalArgumentException if the named context does not exist
     * @throws IllegalStateException if the children relationship does not exist
     */
    public boolean removeNode(String name) throws NullPointerException, IllegalArgumentException, IllegalStateException {
        NodeContext<N> node = get(name);
        if (node == null) {
            throw new IllegalArgumentException("Cannot remove non existent " + name + " child");
        }

        //
        return node.removeNode();
    }

    /**
     * Removes this current context when it is not hidden.
     *
     * @return if the context was removed
     * @throws IllegalStateException if the children relationship does not exist
     */
    public boolean removeNode() throws IllegalStateException {
        if (hidden) {
            return false;
        } else {
            tree.addChange(new NodeChange.Destroyed<NodeContext<N>>(getParent(), this));

            //
            return true;
        }
    }

    // Callbacks

    protected void beforeRemove(NodeContext<N> context) {
        if (!expanded) {
            throw new IllegalStateException();
        }
    }

    protected void beforeInsert(NodeContext<N> context) {
        if (!expanded) {
            throw new IllegalStateException("No children relationship");
        }

        //
        if (!tree.editMode) {
            NodeContext<N> existing = get(context.getName());
            if (existing != null && existing != context) {
                throw new IllegalArgumentException("Tree " + context.getName() + " already in the map");
            }
        }
    }

    protected void afterInsert(NodeContext<N> context) {
        super.afterInsert(context);

        //
        if (context.hidden) {
            hiddenCount++;
        }
    }

    protected void afterRemove(NodeContext<N> context) {
        if (context.hidden) {
            hiddenCount--;
        }

        //
        super.afterRemove(context);
    }

    @Override
    public String toString() {
        return toString(1, new StringBuilder()).toString();
    }

    public StringBuilder toString(int depth, StringBuilder sb) {
        if (sb == null) {
            throw new NullPointerException();
        }
        if (depth < 0) {
            throw new IllegalArgumentException("Depth cannot be negative " + depth);
        }
        sb.append("NodeContext[id=").append(getId()).append(",name=").append(getName());
        if (expanded && depth > 0) {
            sb.append(",children={");
            for (NodeContext<N> current = getFirst(); current != null; current = current.getNext()) {
                if (current.getPrevious() != null) {
                    sb.append(',');
                }
                current.toString(depth - 1, sb);
            }
            sb.append("}");
        } else {
            sb.append("]");
        }
        return sb;
    }
}
