/**
   * Calculates and returns a path that satisfies the required tables list or null if one cannot be
   * found
   *
   * @param requiredTables Tables that are required to be in path
   * @return Path with smallest number of relationships to ensure all required tables are included
   */
  public Path getPath(PathType searchTechnique, List<BusinessTable> requiredTables) {
    // if reset works and validity check passes, build path
    if (reset(requiredTables) && isValid(searchTechnique)) {
      logger.debug("Path determined sucessfully");

      Path path = new Path();
      for (Arc arc : arcs) {
        if (arc.isRequired()) {
          if (logger.isDebugEnabled()) {
            logger.debug("Arc selected for path: " + arc);
          }
          path.addRelationship(arc.getRelationship());
        } else if (logger.isDebugEnabled()) {
          logger.debug(
              "Arc not used for path: Requirement Known["
                  + arc.isRequirementKnown()
                  + "], Required["
                  + arc.isRequired()
                  + "]");
        }
      }

      if (logger.isDebugEnabled()) {
        for (Node n : nodes) {
          logger.debug(
              "Node selection state: Requirement Known["
                  + n.isRequirementKnown()
                  + "], Required["
                  + n.isRequired()
                  + "]");
        }
      }
      if (path.size() > 0) {
        return path;
      }
    }

    return null;
  }
  /**
   * Attempts to find next valid solution to the graph depending on what type of <code>PathType
   * </code> is desired.
   *
   * @param searchTechnique Indicates type of search that should be performed
   * @param prevSolution Previous solution to allow this search to continue from that point
   * @return The resulting solution
   * @throws ConsistencyException When determine that graph is impossible to satisfy
   */
  private Solution searchForNextSolution(PathType searchTechnique, Solution prevSolution)
      throws ConsistencyException {
    // A left move equates to setting a requirement to false and a right move is equivalent to true.
    // Try setting to "false" first to reduce the number of tables for most searches.
    // For the "any relevant" search use "true" first which is quicker
    SearchDirection firstDirection;
    SearchDirection secondDirection;
    if (searchTechnique == PathType.ANY_RELEVANT) {
      firstDirection = SearchDirection.RIGHT;
      secondDirection = SearchDirection.LEFT;
    } else {
      firstDirection = SearchDirection.LEFT;
      secondDirection = SearchDirection.RIGHT;
    }

    // if this is a subsequent search after a solution was already found, we need
    // to return to the location where the last move in the first direction was made
    List<SearchDirection> searchPath = new LinkedList<SearchDirection>();
    List<Arc> searchArcs = new LinkedList<Arc>();
    if (prevSolution != null) {
      // check for situation where we have already traversed all possible paths
      boolean prevContainsFirstDirection = false;
      for (SearchDirection direction : prevSolution.searchPath) {
        if (direction == firstDirection) {
          prevContainsFirstDirection = true;
          break;
        }
      }
      if (!prevContainsFirstDirection) {
        return null;
      }

      ListIterator<SearchDirection> pathIter =
          prevSolution.searchPath.listIterator(prevSolution.searchPath.size());

      // continue to move back in search path until we find an arc that can
      // be assigned the second direction
      boolean foundSecondDir = false;
      while (pathIter.hasPrevious() && !foundSecondDir) {

        // reset the search function for next search operation
        reset(requiredTables);
        propagate();
        searchPath.clear();
        searchArcs.clear();

        // locate the last move that has an alternative
        while (pathIter.hasPrevious()) {
          SearchDirection direction = pathIter.previous();

          if (direction == firstDirection) {
            break;
          }
        }

        // recreate path up to point where we can try a different direction
        Iterator<Arc> arcIter = prevSolution.getSearchArcs().iterator();
        if (pathIter.hasPrevious()) {
          Iterator<SearchDirection> redoIter = prevSolution.getSearchPath().iterator();
          int lastIdx = pathIter.previousIndex();
          for (int idx = 0; idx <= lastIdx; idx++) {

            // all of these operations should work without issue
            SearchDirection direction = redoIter.next();
            Arc arc = arcIter.next();
            if (!attemptArcAssignment(arc, direction)) {
              throw new ConsistencyException(arc);
            }

            // add movement to newly constructed search path
            searchPath.add(direction);
            searchArcs.add(arc);
          }
        }

        // before any searching will begin, make sure the path we are going down shouldn't
        // just be skipped
        int rating = getRatingForCurrentState(searchTechnique);

        // current state isn't any better, return it as next solution
        if (rating >= prevSolution.getRating()) {
          return new Solution(arcs, rating, searchPath, searchArcs, true);
        }

        // retrieve arc which we are going to move second direction
        Arc arc = arcIter.next();

        // if we can't move the second direction here, continue
        // to move back in search path until we find an arc that can
        // be assigned the second direction
        if (attemptArcAssignment(arc, secondDirection)) {
          // update new search path
          searchPath.add(secondDirection);
          searchArcs.add(arc);

          // before any searching will begin, make sure the path we are going down shouldn't
          // just be skipped
          rating = getRatingForCurrentState(searchTechnique);

          // current state isn't any better, return it as next solution
          if (rating >= prevSolution.getRating()) {
            return new Solution(arcs, rating, searchPath, searchArcs, true);
          }

          // set second direction flag so search will continue
          foundSecondDir = true;
        }
      }

      // if we weren't able to make another movement, there are not more solutions
      if (searchPath.size() == 0) {
        return null;
      }
    }

    // dump current state of graph
    if (logger.isDebugEnabled()) {
      logger.debug("-- Graph State Before Search --");
      dumpStateToLog();
    }

    // look for arcs that are not bound
    int rating = -1;
    for (Arc a : arcs) {
      if (!a.isRequirementKnown()) {
        // try the first direction
        if (attemptArcAssignment(a, firstDirection)) {
          searchPath.add(firstDirection);
        } else if (attemptArcAssignment(
            a, secondDirection)) { // if first direction fails, try the second
          searchPath.add(secondDirection);
        } else { // If arc cannot be assigned a requirement value, throw an exception
          throw new ConsistencyException(a);
        }

        // record arc that was altered in search path
        searchArcs.add(a);

        // make sure solution is getting better
        if (prevSolution != null) {
          rating = getRatingForCurrentState(searchTechnique);

          // current state isn't any better, return it as next solution
          if (rating >= prevSolution.getRating()) {
            return new Solution(arcs, rating, searchPath, searchArcs, true);
          }
        }
      }
    }

    // compute rating if never computed
    if (rating < 0) {
      rating = getRatingForCurrentState(searchTechnique);
    }

    // return solution to graph problem
    return new Solution(arcs, rating, searchPath, searchArcs, false);
  }