// (c) 2026-2026 Fair Isaac Corporation
import java.util.Arrays;
import java.util.Collection;

import com.dashoptimization.ColumnType;
import com.dashoptimization.DefaultMessageListener;
import com.dashoptimization.maps.HashMap3;
import com.dashoptimization.objects.Variable;
import com.dashoptimization.objects.XpressProblem;
import static com.dashoptimization.objects.Utils.sum;

/** 9x9 Sudoku puzzle to illustrate solution enumeration.
 * This is a pure feasibility problem, i.e., the objective function is all-zero.
 * We first solve with prefixed values that allow exactly one solution. Then
 * we add clear one of the fixings and enumerate all solutions the new board
 * allows.
 */
public final class Sudoku {
  /** The available row indices (first row has index zero). */
  private static final Collection<Integer> ROWS = Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8);
  /** The available column indices (first column has index zero). */
  private static final Collection<Integer> COLS = Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8);
  /** The available values (1 through 9). */
  private static final Collection<Integer> VALS = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
  /** Values that are fixed in the beginning.
   * These fixings allow exactly one feasible solution.
   * Data set from "The Guardian", 15 July, 2005. http://www.guardian.co.uk/sudoku
   */
  private static final int[][] PRESET = {
    { 0, 0, 0, 7, 0, 5, 6, 9, 0 },
    { 0, 0, 8, 0, 4, 0, 0, 0, 7 },
    { 0, 2, 0, 0, 0, 3, 0, 0, 0 },
    { 0, 0, 2, 0, 0, 0, 9, 0, 0 },
    { 0, 0, 0, 9, 0, 0, 0, 8, 0 },
    { 0, 0, 0, 0, 1, 0, 0, 0, 5 },
    { 0, 0, 0, 0, 0, 4, 0, 0, 0 },
    { 0, 0, 6, 0, 0, 0, 3, 0, 4 },
    { 0, 0, 0, 8, 2, 1, 0, 5, 0 },
  };
  /** Solve the 9x9 Sudoku puzzle with the given fixed values.
   * @param fixed      The values that are fixed on the board.
   * @param enumerate  If <code>true</code> then we enumerate all solutions,
   *                   otherwise we stop on the first solution. Note that
   *                   enumeration can become expensive the more solutions
   *                   there are.
   */
  private static void solve(int[][] fixed, boolean enumerate) {
    try (XpressProblem prob = new XpressProblem()) {
      // Create the variables. One binary variable for each combination of
      // row, column, value.
      // We store the variables in a map rather than a 3D array since rows and
      // columns index from 0, but values index from 1.
      HashMap3<Integer,Integer,Integer,Variable> b = prob.addVariables(ROWS, COLS, VALS)
        .withName("b(%d,%d,%d)")
        .withType(ColumnType.Binary)
        .toMap();

      // Fix the variables that should be fixed on the initial board.
      for (int r : ROWS) {
        for (int c : COLS) {
          if (fixed[r][c] > 0) {
            b.get(r, c, fixed[r][c]).fix(1.0);
          }
        }
      }

      // Each cell must have a number.
      prob.addConstraints(ROWS, COLS, (r, c) -> sum(VALS, v -> b.get(r, c, v)).eq(1.0));

      // Each number must appear exactly once in a row.
      prob.addConstraints(ROWS, VALS, (r,v) -> sum(COLS, c -> b.get(r, c, v)).eq(1.0));
      // Each number must appear exactly once in a column.
      prob.addConstraints(COLS, VALS, (c,v) -> sum(ROWS, r -> b.get(r, c, v)).eq(1.0));
      // In 3x3 squares all values must be different.
      for (int k = 0; k < 3; ++k) {
        Collection<Integer> colidx = Arrays.asList(3 * k, 1 + 3 * k, 2 + 3 * k);
        for (int v : VALS) {
          prob.addConstraint(sum(Arrays.asList(0, 1, 2), colidx,
                                 (x, y) -> b.get(x, y, v)).eq(1.0));
          prob.addConstraint(sum(Arrays.asList(3, 4, 5), colidx,
                                 (x, y) -> b.get(x, y, v)).eq(1.0));
          prob.addConstraint(sum(Arrays.asList(6, 7, 8), colidx,
                                 (x, y) -> b.get(x, y, v)).eq(1.0));
        }
      }
      // Enable log output and prepare for numeration (if requested).
      prob.callbacks.addMessageCallback(DefaultMessageListener::console);
      if (enumerate) {
        prob.controls().setPoolEnumStrategy(com.dashoptimization.XPRSenumerations.PoolEnumStrategy.FULL);
        prob.controls().setPoolLimit(Integer.MAX_VALUE); // Find all solutions.
      }
      // Find all solutions.
      prob.optimize();
      // Print solutions.
      // Note that we only print the first 10 solutions to not flood the screen.
      for (int s = 0; s < Math.min(10, prob.attributes().getPoolSize()); ++s) {
        System.out.println("Solution " + s + ":");
        printSolution(b, prob.getPoolSolution(s));
      }
    }
  }

  /** Print a board to the console.
   * @param board  A 9x9 array that specifies the numbers that are set in
   *               board. A zero means "not set".
   */
  private static void printBoard(int[][] board) {
    for (int r = 0; r < board.length; ++r) {
      String fill = (r == 3 || r == 6) ? "===+" : "---+";
      System.out.print("+");
      for (int c = 0; c < board[r].length; ++c) {
        System.out.print(fill);
        if (c == 2 || c == 5)
          System.out.print('+');
      }
      System.out.println();
      System.out.print("|");
      for (int c = 0; c < board[r].length; ++c) {
        System.out.printf(" %c |", (board[r][c] == 0) ? ' ' : (char)(board[r][c] + '0'));
        if (c == 2 || c == 5)
          System.out.print('|');
      }
      System.out.println();
    }
    System.out.print("+");
    for (int c = 0; c < board[0].length; ++c) {
      System.out.print("---+");
      if (c == 2 || c == 5)
        System.out.print('+');
    }
    System.out.println();
  }

  /** Print a soolution.
   * Constructs a board from a solution and then prints it.
   * @param b    The binary variables.
   * @param sol  The solution values for <code>b</code>.
   */
  private static void printSolution(HashMap3<Integer,Integer,Integer,Variable> b, double[] sol) {
    int[][] result = new int[9][9];
    for (int r : ROWS) {
      for (int c : COLS) {
        for (int v : VALS) {
          if (b.get(r, c, v).getValue(sol) > 0.5) {
            result[r][c] = v;
          }
        }
      }
    }
    printBoard(result);
  }

  public static void main(String[] args) {
    // First solve with the preset board. This should allow exactly one solution.
    solve(PRESET, false);
    // Now remove some fixed values and enumerate all solutions.
    PRESET[0][3] = 0;
    solve(PRESET, true);
  }
}
