/*
 * 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.lucene.util.automaton;

import java.util.BitSet;
import org.apache.lucene.util.ArrayUtil;
import org.apache.lucene.util.IntsRef;
import org.apache.lucene.util.IntsRefBuilder;
import org.apache.lucene.util.RamUsageEstimator;

/**
 * Iterates all accepted strings.
 *
 * <p>If the {@link Automaton} has cycles then this iterator may throw an {@code
 * IllegalArgumentException}, but this is not guaranteed!
 *
 * <p>Be aware that the iteration order is implementation dependent and may change across releases.
 *
 * <p>If the automaton is not determinized then it's possible this iterator will return duplicates.
 *
 * @lucene.experimental
 */
public class FiniteStringsIterator {
  /** Empty string. */
  private static final IntsRef EMPTY = new IntsRef();

  /** Automaton to create finite string from. */
  private final Automaton a;

  /** The state where each path should stop or -1 if only accepted states should be final. */
  private final int endState;

  /** Tracks which states are in the current path, for cycle detection. */
  private final BitSet pathStates;

  /** Builder for current finite string. */
  private final IntsRefBuilder string;

  /** Stack to hold our current state in the recursion/iteration. */
  private PathNode[] nodes;

  /** Emit empty string?. */
  private boolean emitEmptyString;

  /**
   * Constructor.
   *
   * @param a Automaton to create finite string from.
   */
  public FiniteStringsIterator(Automaton a) {
    this(a, 0, -1);
  }

  /**
   * Constructor.
   *
   * @param a Automaton to create finite string from.
   * @param startState The starting state for each path.
   * @param endState The state where each path should stop or -1 if only accepted states should be
   *     final.
   */
  public FiniteStringsIterator(Automaton a, int startState, int endState) {
    this.a = a;
    this.endState = endState;
    this.nodes = new PathNode[16];
    for (int i = 0, end = nodes.length; i < end; i++) {
      nodes[i] = new PathNode();
    }
    this.string = new IntsRefBuilder();
    this.pathStates = new BitSet(a.getNumStates());
    this.string.setLength(0);
    this.emitEmptyString = a.isAccept(0);

    // Start iteration with node startState.
    if (a.getNumTransitions(startState) > 0) {
      pathStates.set(startState);
      nodes[0].resetState(a, startState);
      string.append(startState);
    }
  }

  /**
   * Generate next finite string. The return value is just valid until the next call of this method!
   *
   * @return Finite string or null, if no more finite strings are available.
   */
  public IntsRef next() {
    // Special case the empty string, as usual:
    if (emitEmptyString) {
      emitEmptyString = false;
      return EMPTY;
    }

    for (int depth = string.length(); depth > 0; ) {
      PathNode node = nodes[depth - 1];

      // Get next label leaving the current node:
      int label = node.nextLabel(a);
      if (label != -1) {
        string.setIntAt(depth - 1, label);

        int to = node.to;
        if (a.getNumTransitions(to) != 0 && to != endState) {
          // Now recurse: the destination of this transition has outgoing transitions:
          if (pathStates.get(to)) {
            throw new IllegalArgumentException("automaton has cycles");
          }
          pathStates.set(to);

          // Push node onto stack:
          growStack(depth);
          nodes[depth].resetState(a, to);
          depth++;
          string.setLength(depth);
          string.grow(depth);
        } else if (endState == to || a.isAccept(to)) {
          // This transition leads to an accept state, so we save the current string:
          return string.get();
        }
      } else {
        // No more transitions leaving this state, pop/return back to previous state:
        int state = node.state;
        assert pathStates.get(state);
        pathStates.clear(state);
        depth--;
        string.setLength(depth);

        if (a.isAccept(state)) {
          // This transition leads to an accept state, so we save the current string:
          return string.get();
        }
      }
    }

    // Finished iteration.
    return null;
  }

  /** Grow path stack, if required. */
  private void growStack(int depth) {
    if (nodes.length == depth) {
      PathNode[] newNodes =
          new PathNode
              [ArrayUtil.oversize(nodes.length + 1, RamUsageEstimator.NUM_BYTES_OBJECT_REF)];
      System.arraycopy(nodes, 0, newNodes, 0, nodes.length);
      for (int i = depth, end = newNodes.length; i < end; i++) {
        newNodes[i] = new PathNode();
      }
      nodes = newNodes;
    }
  }

  /** Nodes for path stack. */
  private static class PathNode {

    /** Which state the path node ends on, whose transitions we are enumerating. */
    public int state;

    /** Which state the current transition leads to. */
    public int to;

    /** Which transition we are on. */
    public int transition;

    /** Which label we are on, in the min-max range of the current Transition */
    public int label;

    private final Transition t = new Transition();

    public void resetState(Automaton a, int state) {
      assert a.getNumTransitions(state) != 0;
      this.state = state;
      transition = 0;
      a.getTransition(state, 0, t);
      label = t.min;
      to = t.dest;
    }

    /**
     * Returns next label of current transition, or advances to next transition and returns its
     * first label, if current one is exhausted. If there are no more transitions, returns -1.
     */
    public int nextLabel(Automaton a) {
      if (label > t.max) {
        // We've exhaused the current transition's labels;
        // move to next transitions:
        transition++;
        if (transition >= a.getNumTransitions(state)) {
          // We're done iterating transitions leaving this state
          label = -1;
          return -1;
        }
        a.getTransition(state, transition, t);
        label = t.min;
        to = t.dest;
      }
      return label++;
    }
  }
}
