/**
 * 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.hadoop.examples.dancing;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;


/**
 * This class uses the dancing links algorithm from Knuth to solve sudoku
 * puzzles. It has solved 42x42 puzzles in 1.02 seconds.
 */
public class Sudoku {

  /**
   * The preset values in the board
   * board[y][x] is the value at x,y with -1 = any
   */ 
  private int[][] board;
  
  /**
   * The size of the board
   */
  private int size;
  
  /**
   * The size of the sub-squares in cells across
   */
  private int squareXSize;
  
  /**
   * The size of the sub-squares in celss up and down
   */
  private int squareYSize;
  
  /**
   * This interface is a marker class for the columns created for the
   * Sudoku solver.
   */
  protected static interface ColumnName {
    // NOTHING
  }

  /**
   * A string containing a representation of the solution.
   * @param size the size of the board
   * @param solution a list of list of column names
   * @return a string of the solution matrix
   */
  static String stringifySolution(int size, List<List<ColumnName>> solution) {
    int[][] picture = new int[size][size];
    StringBuilder result = new StringBuilder();
    // go through the rows selected in the model and build a picture of the
    // solution.
    for(List<ColumnName> row: solution) {
      int x = -1;
      int y = -1;
      int num = -1;
      for(ColumnName item: row) {
        if (item instanceof ColumnConstraint) {
          x = ((ColumnConstraint) item).column;
          num = ((ColumnConstraint) item).num;
        } else if (item instanceof RowConstraint) {
          y = ((RowConstraint) item).row;
        }
      }
      picture[y][x] = num;
    }
    // build the string
    for(int y=0; y < size; ++y) {
      for (int x=0; x < size; ++x) {
        result.append(picture[y][x]);
        result.append(" ");
      }
      result.append("\n");
    }
    return result.toString();
  }

  /**
   * An acceptor to get the solutions to the puzzle as they are generated and
   * print them to the console.
   */
  private static class SolutionPrinter 
  implements DancingLinks.SolutionAcceptor<ColumnName> {
    int size;

    public SolutionPrinter(int size) {
      this.size = size;
    }

    /**
     * A debugging aid that just prints the raw information about the 
     * dancing link columns that were selected for each row.
     * @param solution a list of list of column names
     */
    void rawWrite(List solution) {
      for (Iterator itr=solution.iterator(); itr.hasNext(); ) {
        Iterator subitr = ((List) itr.next()).iterator();
        while (subitr.hasNext()) {
          System.out.print(subitr.next().toString() + " ");
        }
        System.out.println();
      }      
    }
    
    public void solution(List<List<ColumnName>> names) {
      System.out.println(stringifySolution(size, names));
    }
  }

  /**
   * Set up a puzzle board to the given size.
   * Boards may be asymmetric, but the squares will always be divided to be
   * more cells wide than they are tall. For example, a 6x6 puzzle will make 
   * sub-squares that are 3x2 (3 cells wide, 2 cells tall). Clearly that means
   * the board is made up of 2x3 sub-squares.
   * @param stream The input stream to read the data from
   */
  public Sudoku(InputStream stream) throws IOException {
    BufferedReader file = new BufferedReader(
        new InputStreamReader(stream, StandardCharsets.UTF_8));
    String line = file.readLine();
    List<int[]> result = new ArrayList<int[]>();
    while (line != null) {
      StringTokenizer tokenizer = new StringTokenizer(line);
      int size = tokenizer.countTokens();
      int[] col = new int[size];
      int y = 0;
      while(tokenizer.hasMoreElements()) {
        String word = tokenizer.nextToken();
        if ("?".equals(word)) {
          col[y] = - 1;
        } else {
          col[y] = Integer.parseInt(word);
        }
        y += 1;
      }
      result.add(col);
      line = file.readLine();
    }
    size = result.size();
    board = result.toArray(new int [size][]);
    squareYSize = (int) Math.sqrt(size);
    squareXSize = size / squareYSize;
    file.close();
  }
  
  /**
   * A constraint that each number can appear just once in a column.
   */
  static private class ColumnConstraint implements ColumnName {
    ColumnConstraint(int num, int column) {
      this.num = num;
      this.column = column;
    }
    int num;
    int column;
    public String toString() {
      return num + " in column " + column;
    }
  }
  
  /**
   * A constraint that each number can appear just once in a row.
   */
  static private class RowConstraint implements ColumnName {
    RowConstraint(int num, int row) {
      this.num = num;
      this.row = row;
    }
    int num;
    int row;
    public String toString() {
      return num + " in row " + row;
    }
  }

  /**
   * A constraint that each number can appear just once in a square.
   */
  static private class SquareConstraint implements ColumnName {
    SquareConstraint(int num, int x, int y) {
      this.num = num;
      this.x = x;
      this.y = y;
    }
    int num;
    int x;
    int y;
    public String toString() {
      return num + " in square " + x + "," + y;
    }
  }

  /**
   * A constraint that each cell can only be used once.
   */
  static private class CellConstraint implements ColumnName {
    CellConstraint(int x, int y) {
      this.x = x;
      this.y = y;
    }
    int x;
    int y;
    public String toString() {
      return "cell " + x + "," + y;
    }
  }
  
  /**
   * Create a row that places num in cell x, y.
   * @param rowValues a scratch pad to mark the bits needed
   * @param x the horizontal offset of the cell
   * @param y the vertical offset of the cell
   * @param num the number to place
   * @return a bitvector of the columns selected
   */
  private boolean[] generateRow(boolean[] rowValues, int x, int y, int num) {
    // clear the scratch array
    for(int i=0; i < rowValues.length; ++i) {
      rowValues[i] = false;
    }
    // find the square coordinates
    int xBox = x / squareXSize;
    int yBox = y / squareYSize;
    // mark the column
    rowValues[x*size + num - 1] = true;
    // mark the row
    rowValues[size*size + y*size + num - 1] = true;
    // mark the square
    rowValues[2*size*size + (xBox*squareXSize + yBox)*size + num - 1] = true;
    // mark the cell
    rowValues[3*size*size + size*x + y] = true;
    return rowValues;
  }
  
  private DancingLinks<ColumnName> makeModel() {
    DancingLinks<ColumnName> model = new DancingLinks<ColumnName>();
    // create all of the columns constraints
    for(int x=0; x < size; ++x) {
      for(int num=1; num <= size; ++num) {
        model.addColumn(new ColumnConstraint(num, x));
      }
    }
    // create all of the row constraints
    for(int y=0; y < size; ++y) {
      for(int num=1; num <= size; ++num) {
        model.addColumn(new RowConstraint(num, y));
      }
    }
    // create the square constraints
    for(int x=0; x < squareYSize; ++x) {
      for(int y=0; y < squareXSize; ++y) {
        for(int num=1; num <= size; ++num) {
          model.addColumn(new SquareConstraint(num, x, y));
        }
      }
    }
    // create the cell constraints
    for(int x=0; x < size; ++x) {
      for(int y=0; y < size; ++y) {
        model.addColumn(new CellConstraint(x, y));
      }
    }
    boolean[] rowValues = new boolean[size*size*4]; 
    for(int x=0; x < size; ++x) {
      for(int y=0; y < size; ++y) {
        if (board[y][x] == -1) {
          // try each possible value in the cell
          for(int num=1; num <= size; ++num) {
            model.addRow(generateRow(rowValues, x, y, num));
          }
        } else {
          // put the given cell in place
          model.addRow(generateRow(rowValues, x, y, board[y][x]));
        }
      }
    }
    return model;
  }
  
  public void solve() {
    DancingLinks<ColumnName> model = makeModel();
    int results = model.solve(new SolutionPrinter(size));
    System.out.println("Found " + results + " solutions");
  }
  
  /**
   * Solves a set of sudoku puzzles.
   * @param args a list of puzzle filenames to solve
   */
  public static void main(String[] args) throws IOException {
    if (args.length == 0) {
      System.out.println("Include a puzzle on the command line.");
    }
    for(int i=0; i < args.length; ++i) {
      Sudoku problem = new Sudoku(new FileInputStream(args[i]));
      System.out.println("Solving " + args[i]);
      problem.solve();
    }
  }

}
