/*******************************************************************************
 * Copyright (c) 2012, 2020 Original authors and others.
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Original authors and others - initial API and implementation
 *     Dirk Fauth <dirk.fauth@googlemail.com> - Bug 453707
 ******************************************************************************/
package org.eclipse.nebula.widgets.nattable.extension.glazedlists.tree;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.eclipse.nebula.widgets.nattable.tree.AbstractTreeRowModel;

import ca.odell.glazedlists.TreeList;

public class GlazedListTreeRowModel<T> extends AbstractTreeRowModel<T> {

    public GlazedListTreeRowModel(GlazedListTreeData<T> treeData) {
        super(treeData);
    }

    /**
     * Note: The check only works for visible elements, therefore a check for an
     * element that is below an collapsed element does not work.
     */
    @Override
    public boolean isCollapsed(int index) {
        if (!this.getTreeData().isValidIndex(index)) {
            return false;
        }

        return !this.getTreeData().getTreeList().isExpanded(index);
    }

    /**
     * Collapse the tree node at the given visible index.
     *
     * @param index
     *            The index of the node in the collection that should be
     *            collapsed.
     *
     * @return Returns an empty list because the list transformation for
     *         expand/collapse is handled by GlazedLists {@link TreeList}.
     */
    @Override
    public List<Integer> collapse(int index) {
        if (this.getTreeData().isValidIndex(index)) {
            this.getTreeData().getTreeList().setExpanded(index, false);
            notifyListeners();
        }

        return new ArrayList<>();
    }

    /**
     * Will check the whole TreeList for its nodes and collapses them if they
     * are expanded. After executing this method, all nodes in the TreeList will
     * be collapsed.
     *
     * @return Returns an empty list because the list transformation for
     *         expand/collapse is handled by GlazedLists {@link TreeList}.
     */
    @Override
    public List<Integer> collapseAll() {
        TreeList<T> treeList = this.getTreeData().getTreeList();
        treeList.getReadWriteLock().writeLock().lock();
        try {
            // iterating directly over the TreeList is a lot faster than
            // checking the nodes
            // which is related that on collapsing we only need to iterate once
            // from bottom to top
            for (int i = (treeList.size() - 1); i >= 0; i--) {
                /*
                 * Checks if the node at the given visible index has children
                 * and is collapsible. If it is it will be collapsed otherwise
                 * skipped. This backwards searching and collapsing mechanism is
                 * necessary to ensure to really get every collapsible node in
                 * the whole tree structure.
                 */
                if (hasChildren(i) && !isCollapsed(i)) {
                    treeList.setExpanded(i, false);
                }
            }
        } finally {
            treeList.getReadWriteLock().writeLock().unlock();
        }

        notifyListeners();
        return new ArrayList<>();
    }

    /**
     * Expand the tree node at the given visible index.
     *
     * @param index
     *            The index of the node in the collection that should be
     *            expanded.
     *
     * @return Returns an empty list because the list transformation for
     *         expand/collapse is handled by GlazedLists {@link TreeList}.
     */
    @Override
    public List<Integer> expand(int index) {
        if (this.getTreeData().isValidIndex(index)) {
            this.getTreeData().getTreeList().setExpanded(index, true);
            notifyListeners();
        }

        return new ArrayList<>();
    }

    @Override
    public List<Integer> expandToLevel(int parentIndex, int level) {
        if (this.getTreeData().isValidIndex(parentIndex)) {
            internalExpandToLevel(parentIndex, level);
            notifyListeners();
        }

        return new ArrayList<>();
    }

    /**
     * Performs the expand operations iteratively without notifying the
     * listeners while processing.
     *
     * @param parentIndex
     *            The index of the node in the collection that should be
     *            expanded.
     * @param level
     *            The level to which the tree nodes should be expanded.
     */
    protected void internalExpandToLevel(int parentIndex, int level) {
        if (this.getTreeData().isValidIndex(parentIndex)
                && depth(parentIndex) <= (level - 1)) {

            this.getTreeData().getTreeList().setExpanded(parentIndex, true);

            List<Integer> directChildren = getDirectChildIndexes(parentIndex);
            // iterate backwards because the indexes are changing on expand
            Collections.sort(directChildren, Collections.reverseOrder());
            for (Integer child : directChildren) {
                if (hasChildren(child) && depth(child) <= (level - 1)) {
                    internalExpandToLevel(child, level);
                }
            }
        }
    }

    /**
     * {@inheritDoc}
     *
     * @return Returns an empty list because the list transformation for
     *         expand/collapse is handled by GlazedLists {@link TreeList}.
     */
    @Override
    public List<Integer> expandAll() {
        internalExpandAll();
        notifyListeners();
        return new ArrayList<>();
    }

    /**
     * Performs the expand operations iteratively without notifying the
     * listeners while processing.
     */
    protected void internalExpandAll() {
        TreeList<T> treeList = this.getTreeData().getTreeList();

        boolean expandPerformed = false;
        treeList.getReadWriteLock().writeLock().lock();
        try {
            // iterating directly over the TreeList is a lot faster than
            // checking the nodes
            for (int i = (treeList.size() - 1); i >= 0; i--) {
                /*
                 * Checks if the node at the given visible index has children
                 * and is expandable. If it is it will be expanded otherwise
                 * skipped. This backwards searching and expanding mechanism is
                 * necessary to ensure to really get every expandable node in
                 * the whole tree structure.
                 */
                if (hasChildren(i) && isCollapsed(i)) {
                    treeList.setExpanded(i, true);
                    expandPerformed = true;
                }
            }
        } finally {
            treeList.getReadWriteLock().writeLock().unlock();
        }

        // if at least one element was expanded we need to perform the step
        // again as we are only able to retrieve the visible nodes
        if (expandPerformed) {
            expandAll();
        }
    }

    /**
     * {@inheritDoc}
     *
     * @return Returns an empty list because the list transformation for
     *         expand/collapse is handled by GlazedLists {@link TreeList}.
     */
    @Override
    public List<Integer> expandToLevel(int level) {
        internalExpandToLevel(level);
        notifyListeners();
        return new ArrayList<>();
    }

    /**
     * Performs the expand operations iteratively without notifying the
     * listeners while processing.
     *
     * @param level
     *            The level to which the tree nodes should be expanded.
     */
    protected void internalExpandToLevel(int level) {
        TreeList<T> treeList = this.getTreeData().getTreeList();

        boolean expandPerformed = false;
        treeList.getReadWriteLock().writeLock().lock();
        try {
            // iterating directly over the TreeList is a lot faster than
            // checking the nodes
            for (int i = (treeList.size() - 1); i >= 0; i--) {
                /*
                 * Checks if the node at the given visible index has children,
                 * is expandable and is on a level below the given level. If it
                 * is it will be expanded otherwise skipped. This backwards
                 * searching and expanding mechanism is necessary to ensure to
                 * really get every expandable node in the whole tree structure.
                 */
                if (hasChildren(i) && isCollapsed(i) && treeList.getTreeNode(i).path().size() <= level) {
                    treeList.setExpanded(i, true);
                    expandPerformed = true;
                }
            }
        } finally {
            treeList.getReadWriteLock().writeLock().unlock();
        }

        // if at least one element was expanded we need to perform the step
        // again as we are only able to retrieve the visible nodes
        if (expandPerformed) {
            expandToLevel(level);
        }
    }

    /**
     * {@inheritDoc}
     *
     * @since 1.6
     */
    @Override
    public GlazedListTreeData<T> getTreeData() {
        return (GlazedListTreeData<T>) super.getTreeData();
    }
}
