/*
 * Copyright (c) 2023, Sirix Contributors
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the <organization> nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package io.sirix.axis.pathsummary;

import io.sirix.axis.IncludeSelf;
import org.checkerframework.checker.index.qual.NonNegative;
import io.sirix.index.path.summary.PathNode;
import io.sirix.index.path.summary.PathSummaryReader;
import io.sirix.settings.Fixed;

import java.util.ArrayDeque;
import java.util.Deque;

import static com.google.common.base.Preconditions.checkArgument;
import static java.util.Objects.requireNonNull;

/**
 * Iterates over a subtree in levelorder / in a breath first traversal.
 *
 * @author Johannes Lichtenberger, University of Konstanz
 */
public final class LevelOrderSettingInMemoryInstancesAxis extends AbstractAxis {

  /**
   * The reader used to navigate.
   */
  private final PathSummaryReader reader;

  /**
   * {@link Deque} for remembering next nodeKey in document order.
   */
  private Deque<PathNode> firstChildren;

  /**
   * Determines if {@code hasNext()} is called for the first time.
   */
  private boolean isFirst;

  /**
   * Filter by level.
   */
  private int filterLevel = Integer.MAX_VALUE;

  /**
   * Current level.
   */
  private int level;

  /**
   * Get a new builder instance.
   *
   * @param rtx the {@link PathSummaryReader} to iterate with
   * @return {@link Builder} instance
   */
  public static Builder newBuilder(final PathSummaryReader rtx) {
    return new Builder(rtx);
  }

  /**
   * Builder.
   */
  public static class Builder {
    /**
     * Filter by level.
     */
    private int filterLevel = Integer.MAX_VALUE;

    /**
     * Sirix {@link PathSummaryReader}.
     */
    private final PathSummaryReader reader;

    /**
     * Determines if current start node to traversal should be included or not.
     */
    private IncludeSelf includeSelf = IncludeSelf.NO;

    /**
     * Constructor.
     *
     * @param pathSummaryReader Sirix {@link PathSummaryReader}
     */
    public Builder(final PathSummaryReader pathSummaryReader) {
      this.reader = requireNonNull(pathSummaryReader);
    }

    /**
     * Determines that the current node should also be considered.
     *
     * @return this builder instance
     */
    public Builder includeSelf() {
      includeSelf = IncludeSelf.YES;
      return this;
    }

    /**
     * Determines the maximum level to filter.
     *
     * @param filterLevel maximum level to filter nodes
     * @return this builder instance
     */
    public Builder filterLevel(final @NonNegative int filterLevel) {
      checkArgument(filterLevel >= 0, "filterLevel must be >= 0!");
      this.filterLevel = filterLevel;
      return this;
    }

    /**
     * Build a new instance.
     *
     * @return new instance
     */
    public LevelOrderSettingInMemoryInstancesAxis build() {
      return new LevelOrderSettingInMemoryInstancesAxis(this);
    }
  }

  /**
   * Constructor initializing internal state.
   *
   * @param builder the builder reference
   */
  private LevelOrderSettingInMemoryInstancesAxis(final Builder builder) {
    super(builder.reader.getPathNode(), builder.includeSelf);
    filterLevel = builder.filterLevel;
    reader = builder.reader;
  }

  @Override
  public void reset(final PathNode pathNode) {
    super.reset(pathNode);
    level = 0;
    isFirst = true;
    firstChildren = new ArrayDeque<>();
    if (reader != null) {
      reader.moveTo(pathNode.getNodeKey());
    }
  }

  @Override
  protected PathNode nextNode() {
    // Determines if it's the first call to hasNext().
    final long nodeKey = nextNode.getNodeKey();
    reader.moveTo(nodeKey);

    if (isFirst) {
      isFirst = false;

      if (includeSelf() == IncludeSelf.YES) {
        if (nextNode.getParent() == null && reader.hasParent()
            && reader.getParentKey() != Fixed.DOCUMENT_NODE_KEY.getStandardProperty()) {
          reader.moveToParent();
          final PathNode parentNode = reader.getPathNode();
          nextNode.setParent(parentNode);
          reader.moveTo(nodeKey);
        }
        return nextNode;
      } else {
        if (nextNode.hasRightSibling()) {
          return getRightSibling(nodeKey);
        } else if (nextNode.hasFirstChild()) {
          return getFirstChild(nodeKey);
        } else {
          return done();
        }
      }
    }
    // Follow right sibling if there is one.
    if (nextNode.hasRightSibling()) {
      // Add first child to queue.
      if (nextNode.hasFirstChild()) {
        firstChildren.add(getFirstChild(nodeKey));
      }

      return getRightSibling(nodeKey);
    }

    // Add first child to queue.
    if (nextNode.hasFirstChild()) {
      firstChildren.add(getFirstChild(nodeKey));
    }

    // Then follow first child on stack.
    if (!firstChildren.isEmpty()) {
      level++;

      // End traversal if level is reached.
      if (level > filterLevel) {
        return done();
      }

      final var pathNode = firstChildren.pop();
      reader.moveTo(pathNode.getNodeKey());
      return pathNode;
    }

    // Then follow first child if there is one.
    if (nextNode.hasFirstChild()) {
      level++;

      // End traversal if level is reached.
      if (level > filterLevel) {
        return done();
      }

      return getFirstChild(nodeKey);
    }

    return done();
  }

  private PathNode getFirstChild(long nodeKey) {
    PathNode firstChild = nextNode.getFirstChild();
    if (firstChild == null) {
      reader.moveToFirstChild();
      firstChild = reader.getPathNode();
      nextNode.setFirstChild(firstChild);
      reader.moveTo(nodeKey);
    }
    if (firstChild.getParent() == null) {
      firstChild.setParent(nextNode);
    }
    return firstChild;
  }

  private PathNode getRightSibling(long nodeKey) {
    PathNode rightSibling = nextNode.getRightSibling();
    if (rightSibling == null) {
      reader.moveToRightSibling();
      rightSibling = reader.getPathNode();
      nextNode.setRightSibling(rightSibling);
      reader.moveTo(nodeKey);
    }
    if (rightSibling.getLeftSibling() == null) {
      var parentNode = nextNode.getParent();
      rightSibling.setParent(parentNode);
      rightSibling.setLeftSibling(nextNode);
    }
    return rightSibling;
  }

  /**
   * Get the current level.
   *
   * @return the current level
   */
  public int getCurrentLevel() {
    return level;
  }
}
