/*
 * 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.myfaces.tobago.model;

import javax.swing.tree.TreeNode;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

/**
 * Manages the expanded state of an tree.
 *
 * @since 2.0.0
 */
public class ExpandedState implements Serializable {

  private int defaultExpandedLevels;
  private Set<TreePath> expandedSet;
  private Set<TreePath> collapsedSet;

  /**
   * Creates a new state object to store which nodes of a tree are expanded and collapsed in a view.
   *
   * @param defaultExpandedLevels All nodes up to this level are expanded by default.
   */
  public ExpandedState(final int defaultExpandedLevels) {
    this.defaultExpandedLevels = defaultExpandedLevels;
    this.expandedSet = new HashSet<>();
    this.collapsedSet = new HashSet<>();
  }

  /**
   * Checks if a node is expanded.
   *
   * @param node The node to check.
   * @return Is the node expanded?
   */
  public boolean isExpanded(final TreeNode node) {
    final TreePath path = new TreePath(node);
    return isExpanded(path);
  }

  /**
   * Checks if a node is expanded.
   *
   * @param path The path of the node to check.
   * @return Is the node behind this path is expanded?
   */
  public boolean isExpanded(final TreePath path) {
    if (expandedSet.contains(path)) {
      return true;
    }
    if (collapsedSet.contains(path)) {
      return false;
    }
    return path.getLength() < defaultExpandedLevels;
  }

  /**
   * Expands a single node.
   *
   * @param node The node to expand. Also the parents will be expanded since Tobago 3.0.0.
   */
  public void expand(final TreeNode node) {
    expand(node, true);
  }

  /**
   * Expands a single node.
   *
   * @param node    The node to expand.
   * @param parents Should the parents also be expanded?
   */
  public void expand(final TreeNode node, final boolean parents) {
    final TreePath path = new TreePath(node);
    expand(path, parents);
  }

  /**
   * Expands a single node.
   *
   * @param path The path of the node to expand. Also the parents will be expanded since Tobago 3.0.0.
   */
  public void expand(final TreePath path) {
    expand(path, true);
  }

  /**
   * Expands a single node.
   *
   * @param path    The path of the node to expand.
   * @param parents Should the parents also be expanded?
   */
  public void expand(final TreePath path, final boolean parents) {
    if (path.getLength() >= defaultExpandedLevels) {
      expandedSet.add(path);
    } else {
      collapsedSet.remove(path);
    }
    if (parents && !path.isRoot()) {
      expand(path.getParent(), true);
    }
  }

  /**
   * Expands all nodes that level are lower or equals the parameter level.
   *
   * @param level The level to expand.
   */
  public void expand(final int level) {
    final ArrayList<TreePath> toRemove = new ArrayList<>();
    if (level > defaultExpandedLevels) {
      defaultExpandedLevels = level;
      for (final TreePath treePath : expandedSet) {
        if (treePath.getLength() < defaultExpandedLevels) {
          toRemove.add(treePath);
        }
      }
      expandedSet.removeAll(toRemove);
      collapsedSet.clear();
    } else {
      for (final TreePath treePath : collapsedSet) {
        if (treePath.getLength() < level) {
          toRemove.add(treePath);
        }
      }
      collapsedSet.removeAll(toRemove);
    }
  }

  /**
   * Expands a nodes of the tree.
   */
  public void expandAll() {
    defaultExpandedLevels = Integer.MAX_VALUE;
    expandedSet.clear();
    collapsedSet.clear();
  }

  /**
   * Collapses a single node.
   *
   * @param node The node to collapse.
   */
  public void collapse(final TreeNode node) {
    final TreePath path = new TreePath(node);
    collapse(path);
  }

  /**
   * Collapses a single node.
   *
   * @param path The path of the node to collapse.
   */
  public void collapse(final TreePath path) {
    if (path.getLength() < defaultExpandedLevels) {
      collapsedSet.add(path);
    } else {
      expandedSet.remove(path);
    }
  }

  /**
   * Collapses all nodes that level are higher or equals the parameter level.
   *
   * @param level The level to expand.
   */
  public void collapse(final int level) {
    int count = level;
    // to use a symmetric algorithm like in expand
    count--;

    final ArrayList<TreePath> toRemove = new ArrayList<>();
    if (count < defaultExpandedLevels) {
      defaultExpandedLevels = count;
      for (final TreePath treePath : collapsedSet) {
        if (treePath.getLength() >= defaultExpandedLevels) {
          toRemove.add(treePath);
        }
      }
      collapsedSet.removeAll(toRemove);
      expandedSet.clear();
    } else {
      for (final TreePath treePath : expandedSet) {
        if (treePath.getLength() >= count) {
          toRemove.add(treePath);
        }
      }
      expandedSet.removeAll(toRemove);
    }
  }

  /**
   * Collapses a nodes of the tree.
   */
  public void collapseAll() {
    defaultExpandedLevels = 0;
    expandedSet.clear();
    collapsedSet.clear();
  }

  /**
   * Collapses a nodes of the tree. The root node will be expanded.
   */
  public void collapseAllButRoot() {
    defaultExpandedLevels = 1;
    expandedSet.clear();
    collapsedSet.clear();
  }

  /**
   * Resets the state to the defaults. After this call, the nodes with level smaller than defaultExpandedLevels
   * are expanded, the other ones are collapsed.
   */
  public void reset() {
    expandedSet.clear();
    collapsedSet.clear();
  }

  /**
   * @return A unmodifiable set of paths of the expanded nodes.
   */
  public Set<TreePath> getExpandedSet() {
    return Collections.unmodifiableSet(expandedSet);
  }

  /**
   * @return A unmodifiable set of paths of the collapsed nodes.
   */
  public Set<TreePath> getCollapsedSet() {
    return Collections.unmodifiableSet(collapsedSet);
  }
}
