/**
   * Somewhat multi-threaded depth-first search. Performs a DFS of the subtrees from the current
   * node in parallel.
   *
   * @param s The tile sequence
   * @param b The board to search
   * @param pool The thread pool in which to submit jobs to.
   * @return The board with the highest evaluation or null if no board can continue.
   */
  private Board solve_pdfs(Board b, ExecutorService pool) {
    List<Future<Board>> rets = new ArrayList<>(BOARD_WIDTH);
    Board best = null;
    int best_score = -1;

    for (Direction d : directions) {
      Board n = new Board(b);
      if (n.move(tileSequence, d)) {
        rets.add(pool.submit(new ParallelDFS(n)));
      }
    }

    for (Future<Board> ret : rets) {
      try {
        Board c = ret.get();
        if (c != null) {
          int score = evaluate(c);
          if (score > best_score) {
            best = c;
            best_score = score;
          }
        }
      } catch (InterruptedException | ExecutionException e) {
        System.err.println("Error: " + e.getMessage());
      }
    }

    return best;
  }
  private Board solve_dfs(Board b, int depthLimit, int depth) {
    if (depth >= depthLimit) { // Cutoff test
      return b;
    }

    Board best = null;
    int best_score = -1;

    for (int i = 0; i < BOARD_WIDTH; i++) {
      Board next = new Board(b);
      if (next.move(tileSequence, directions[i])) {
        Board candidate = solve_dfs(next, depthLimit, depth + 1);
        if (candidate == null && next.finished()) {
          candidate = next;
        }
        if (candidate != null) {
          if (candidate.finished()) {
            updateBest(candidate);
          } else {
            int score = evaluate(candidate);
            if (score > best_score) {
              best_score = score;
              best = candidate;
            }
          }
        }
      }
    }
    return best;
  }
 private synchronized void updateBest(Board b) {
   int score = b.score();
   if (score > fbest_score) {
     fbest_score = score;
     fbest = b;
   }
 }
  /**
   * Single threaded depth-limited depth first search, with no backtracking.
   *
   * @param s The tile sequence
   * @param b The board to search
   * @return The final best board state that it can find.
   */
  private Board solve_ldfs(Board b) {
    Board input = b;
    fbest_score = -1;
    fbest = null;
    currentfactors = factors;
    while (b != null && !b.finished()) {
      b = solve_dfs(b, MAX_DEPTH, 0);
      if (b != null) {
        log_info(b);
      }
    }

    // Super edge-case: The input board can't be moved...
    return fbest == null ? input : fbest;
  }
  /**
   * Multi-threaded depth-limited depth first search, with no backtracking.
   *
   * @param s The tile sequence
   * @param b The board to search
   * @param pool The thread pool
   * @return The final best board state that it can find.
   */
  private Board solve_pndfs(Board b, ExecutorService pool) {
    Board input = b;

    fbest_score = -1;
    fbest = null;
    currentfactors = factors;

    while (b != null && !b.finished()) {
      b = solve_pdfs(b, pool);
      if (b != null) {
        // log_info(b);
      }
    }

    // Super edge-case: The input board can't be moved...
    return fbest == null ? input : fbest;
  }
  // Edge case: As we're approaching the end of a sequence, try to maximise score...
  // Possible change to ncombinable: Weight combinables that increase the score significantly
  private int evaluate(Board b) {
    int[] thefactors = currentfactors;

    // We are close to the end of the sequence! Use different weights!
    if (b.nMoves() + MAX_DEPTH * 2 >= tileSequence.length) {
      // System.err.println(b.nMoves());
      thefactors = closefactors;
    }
    return ((int) Math.pow(4, b.dof()))
        + thefactors[0] * b.zeros()
        + thefactors[1] * b.checkerboarding3()
        + thefactors[2] * b.smoothness()
        + thefactors[3] * b.nCombinable();
  }
  /**
   * 'Learns' the factors that are good. More of, just iterate over all possible heuristic weight
   * combinations and you have to observe which ones are good.
   *
   * <p>This learning does not utilise backtracking - it sticks to using one weight only.
   *
   * @param b The board to learn against
   * @param learnClose Whether we are learning for the 'close' weight factors or not.
   */
  public void learn_factors(Board b, boolean learnClose) {
    int[] fl = learnClose ? closefactors : factors;
    int[] best = new int[fl.length];
    int best_score = -1;
    Board best_board = null;
    ExecutorService pool = Executors.newFixedThreadPool(nThreads);

    for (int i = learning_starts[0]; i < 19; i++) {
      for (int j = learning_starts[1]; j < 19; j++) {
        for (int k = learning_starts[2]; k < 8; k++) {
          for (int l = learning_starts[3]; l < 17; l++) {
            fl[0] = i;
            fl[1] = j;
            fl[2] = k;
            fl[3] = l;

            Board n = solve_pndfs(b, pool);
            int score = n.score();
            if (score > best_score) {
              System.arraycopy(factors, 0, best, 0, best.length);
              best_score = score;
              best_board = n;
            }
            System.out.printf("Current score: %d (%d moves)\n", score, n.nMoves());
            for (int v : fl) {
              System.out.printf("%d ", v);
            }
            System.out.println();
          }
          if (best_board != null) {
            System.out.println("Current best:");
            System.out.println(best_board);
            System.out.printf("%d (%d moves)\n", best_board.score(), best_board.nMoves());
          }
          for (int v : best) {
            System.out.printf("%d ", v);
          }
          System.out.println();
        }
      }
    }
    for (int v : best) System.out.printf("%d ", v);
    System.out.println();

    pool.shutdown();
  }
  /**
   * Solves a board game using a depth-limited depth first search. Also makes use of some
   * backtracking to further optimise the result. Will also attempt to run multithreadedly.
   *
   * @param s The tile sequence
   * @param b The board to search
   * @return The final best board state that it can find.
   */
  private Board solve_mdfs(Board b) {
    Ringbuffer<Board> rb = new Ringbuffer<>(6);
    Board current = b, choke_best = null;
    char fc = 0, foff = 0;
    // VTEC just kicked in yo
    ExecutorService pool = Executors.newFixedThreadPool(nThreads);

    fbest_score = -1;
    fbest = null;
    while (current != null && !current.finished()) {
      rb.push(current);
      current = solve_pdfs(current, pool);

      if (choke_best != null && choke_best != fbest) {
        log_info(
            "Recovery!: [%d:%s] %d(%d) --> %d(%d)",
            (int) fc,
            Arrays.toString(currentfactors),
            choke_best.score(),
            choke_best.nMoves(),
            fbest.score(),
            fbest.nMoves());
        choke_best = null;
        // Test: Is it always best to stick to 18,2,2,9 where possible?
        // Maybe not, but some factors shouldn't be used for extended periods of time.
        if (fc > 3) {
          log_info(
              "Volatile weights were used; switching back to %s",
              Arrays.toString(choicefactors[0]));
          fc = 0;
          currentfactors = choicefactors[0];
        }
      }

      if (current != null) {
        log_info(current);
      } else if (fbest != null && fbest.nMoves() < tileSequence.length) {
        current = rb.pop();

        if (choke_best != fbest) {
          choke_best = fbest;
          foff = 1;
          fc = (char) ((fc + 1) % choicefactors.length);
          currentfactors = choicefactors[fc];
          log_info("Dead-end, back-tracking two steps and trying with different weights!");
          log_info(
              "Starting index: %d (current score %d/%d)",
              (int) fc, current.score(), current.nMoves());
        } else if (foff++ < choicefactors.length - 1) {
          fc = (char) ((fc + 1) % choicefactors.length);
          currentfactors = choicefactors[fc];
          log_info("No improvement, trying factor index %d...", (int) fc);
        } else {
          current = null;
          log_info("Factor exhaustion");
        }
      }
    }

    pool.shutdown();
    return fbest == null ? b : fbest;
  }