/* Useful imports */

import edu.neu.ccs.*;
import edu.neu.ccs.gui.*;
import edu.neu.ccs.codec.*;
import edu.neu.ccs.console.*;
import edu.neu.ccs.filter.*;
import edu.neu.ccs.jpf.*;
import edu.neu.ccs.parser.*;
import edu.neu.ccs.pedagogy.*;
import edu.neu.ccs.quick.*;
import edu.neu.ccs.util.*;

import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.awt.font.*;
import java.awt.image.*;
import javax.swing.*;
import javax.swing.border.*;
import java.io.*;
import java.util.*;
import java.math.*;
import java.beans.*;
import java.lang.reflect.*;
import java.net.URL;
import java.util.regex.*;
import java.text.ParseException;


public class SudokuModel
    implements Stringable
{

    // Static definitions //
    
    /**
     * 9 puzzle rows in rows 1 to 9.
     * 
     * 9 puzzle cols in rows 10 to 18.
     * 
     * 9 puzzle blocks in rows 19 to 27.
     * 
     * By convention, row 0 and col 0 of this array
     * will be null.
     */
    private static CellPosition[][] celllist =
        new CellPosition[28][10];
    
    
    /** Initialize celllist. */
    static {
        int r;
        int c;
        int m;
        int k;
        CellPosition cell;
        
        for (int row = 1; row <= 9; row++) {
            for (int col = 1; col <= 9; col++) {
                cell = new CellPosition(row, col);
                
                k = row;
                
                celllist[k][col] = cell;
                
                k = col + 9;
                
                celllist[k][row] = cell;
                
                r = (row - 1) / 3;
                c = (col - 1) / 3;
                k = 3 * r + c + 19;
                
                r = (row - 1) % 3;
                c = (col - 1) % 3;
                m = 3 * r + c + 1;
                
                celllist[k][m] = cell;
            }
        }
    }
    
    
    // Member definitions //
    
    /**
     * The Sudoku GUI object.
     * 
     * Note: The purpose for maintaining a reference to the GUI
     * is to provide that reference to the table object and to
     * all blocks within the table.  The model does not use the
     * GUI in an active fashion.
     */
    private SudokuBase GUI = null;
    
    
    /** The view table associated with the model. */
    private SudokuTable table = null;
    
    
    /**
     * The cell data associated with the model.
     * 
     * By convention, row 0 and col 0 of this array
     * will be zero.
     */
    private int[][] celldata = new int[10][10];
    
    
    /**
     * The hint data associated with the model.
     * 
     * By convention, row 0 and col 0 of this array
     * will be null.
     */
    private SudokuHint[][] hints = new SudokuHint[10][10];
    
    
    /** Whether or not to prune triplets. */
    private boolean doPruneTriplets = true;
    
    /** Whether or not to prune hint sets. */
    private boolean doPruneHintSets = true;
    
    /** Whether or not to prune unique hints. */
    private boolean doPruneUniqueHints = true;
    
    
    // Constructors //
    
    /** The default constructor. */
    public SudokuModel() {
        initializeModel();
    }
    
    
    /** The constructor called by the Sudoku GUI object. */
    public SudokuModel(SudokuBase GUI) {
        this.GUI = GUI;
        
        initializeModel();
    }
    
    
    // Methods //
    
    /** Get the Sudoku puzzle GUI object. */
    public SudokuBase getSoduku() {
        return GUI;
    }
    
    
    /** Get the Sudoku puzzle table panel. */
    public SudokuTable getSudokuTable() {
        return table;
    }
    
    
    /** Initialize the model. */
    private void initializeModel() {
        table = new SudokuTable(this);
        
        for (int row = 1; row <= 9; row++)
            for (int col = 1; col <= 9; col++) {
                hints[row][col] = new SudokuHint();
                hints[row][col].setAll(true);
            }
    }
    
    
    /** Remove all cell data. */
    public void removeEverything() {
        for (int row = 1; row <= 9; row++)
            for (int col = 1; col <= 9; col++) {
                celldata[row][col] = 0;
                hints[row][col].setAll(true);
            }
    }
    
    
    /**
     * Get the cell data in the model.
     * 
     * A value from 1 to 9 should appear in the view.
     * A value of 0 signifies empty.
     * 
     * @param row the row between 1 and 9
     * @param col the col between 1 and 9
     */
    public int getCellData(int row, int col) {
        if ((row >= 1) && (row <= 9))
            if ((col >= 1) && (col <= 9))
                return celldata[row][col];
        
        return 0;
    }
    
    
    /**
     * Get the cell data in the model.
     * 
     * A value from 1 to 9 should appear in the view.
     * A value of 0 signifies empty.
     * 
     * @param position the cell position
     */
    public int getCellData(CellPosition position) {
        if (position == null)
            return 0;
        
        int row = position.row;
        int col = position.col;
        
        return getCellData(row, col);
    }
    
    
    /**
     * Set the cell data in the model but do
     * not update the view.
     * 
     * @param row the row between 1 and 9
     * @param col the col between 1 and 9
     * @param val the cell value between 0 and 9 with 0 = empty
     */
    public void setCellData(int row, int col, int val) {
        if ((val < 1) || (val > 9))
            val = 0;
        
        if ((row >= 1) && (row <= 9))
            if ((col >= 1) && (col <= 9))
                celldata[row][col] = val;
    }
    
    
    /**
     * Set the cell data in the model but do
     * not update the view.
     * 
     * @param position the cell position
     * @param val the cell value between 0 and 9 with 0 = empty
     */
    public void setCellData(CellPosition position, int val) {
        if (position == null)
            return;
        
        int row = position.row;
        int col = position.col;
        
        setCellData(row, col, val);
    }
    
    
    /**
     * Get the cell hint in the model.
     * 
     * @param row the row between 1 and 9
     * @param col the col between 1 and 9
     */
    public SudokuHint getCellHint(int row, int col) {
        if ((row >= 1) && (row <= 9))
            if ((col >= 1) && (col <= 9))
                return hints[row][col];
        
        return null;
    }
    
    
    /**
     * Get the cell hint in the model.
     * 
     * @param position the cell position
     */
    public SudokuHint getCellHint(CellPosition position) {
        if (position == null)
            return null;
        
        int row = position.row;
        int col = position.col;
        
        return getCellHint(row, col);
    }
    
    
    /** Initialize the hints prior to pruning. */
    public void initializeHints() {
        for (int row = 1; row <= 9; row++)
            for (int col = 1; col <= 9; col++) {
                hints[row][col].setAll(true);
            }
        
        for (int row = 1; row <= 9; row++)
            for (int col = 1; col <= 9; col++) {
                int data = celldata[row][col];
                
                if (data > 0) {
                    hints[row][col].setOneHint(data);
                    pruneBasic(row, col, data);
                }
            }
    }
    
    
    /** Set whether or not to prune triplets when pruning. */
    public void setPruneTriplets(boolean setting) {
        doPruneTriplets = setting;
    }
    
    
    /** Set whether or not to prune hint sets when pruning. */
    public void setPruneHintSets(boolean setting) {
        doPruneHintSets = setting;
    }
    
    
    /** Set whether or not to prune unique hints when pruning. */
    public void setPruneUniqueHints(boolean setting) {
        doPruneUniqueHints = setting;
    }
    
    
    /**
     * Prune hints by eliminating hints made impossible
     * by data or hints in other rows or cols.
     * 
     * This method repeats the pruning process until no
     * more hints can be pruned.
     * 
     * The pruning algorithms used depend on the settings
     * made via the methods:
     *     setPruneTriplets
     *     setPruneHintSets
     *     setPruneUniqueHints
     * 
     * Returns true if at least one hint was pruned.
     */
    public boolean pruneComplete() {
        boolean haspruned = false;
        
        while (pruneOneCycle())
            haspruned = true;
        
        return haspruned;
    }
    
    
    /** 
     * Performs one cycle of pruning hints.
     * 
     * The pruning algorithms used depend on the settings
     * made via the methods:
     *     setPruneTriplets
     *     setPruneHintSets
     *     setPruneUniqueHints
     * 
     * Returns true if at least one hint was pruned.
     */
    public boolean pruneOneCycle() {
        boolean haspruned = false;
        
        if (doPruneTriplets)
            haspruned |= pruneTriplets();
        
        if (doPruneHintSets)
            haspruned |= pruneHintSets();
        
        if (doPruneUniqueHints)
            haspruned |= pruneUniqueHints();
        
        return haspruned;
    }
    
    
    /**
     *  Prune the data hint from the row, col, and block
     *  containing (row, col) except for the cell itself.
     */
    private boolean pruneBasic(int row, int col, int data) {
        boolean haspruned = false;
        
        for (int r = 1; r <= 9; r++)
            if (r != row)
                haspruned |= hints[r][col].pruneHint(data);
        
        for (int c = 1; c <= 9; c++)
            if (c != col)
                haspruned |= hints[row][c].pruneHint(data);
        
        int row1 = 3 * ((row - 1) / 3) + 1;
        int row3 = row1 + 2;
        
        int col1 = 3 * ((col - 1) / 3) + 1;
        int col3 = col1 + 2;
        
        for (int r = row1; r <= row3; r++)
            for (int c = col1; c <= col3; c++)
                if (! ((r == row) && (c == col)))
                    haspruned |= hints[r][c].pruneHint(data);
        
        return haspruned;
    }
    
    
    /** Prune the row and col triplets. */
    private boolean pruneTriplets() {
        boolean haspruned = false;
        
        for (int data = 1; data <= 9; data++) {
            haspruned |= pruneRowTriplet(data);
            haspruned |= pruneColTriplet(data);
        }
        
        return haspruned;
    }
    
    
    /** Prune hints with the given data value via row comparison. */
    private boolean pruneRowTriplet(int data) {
        boolean haspruned = false;
        
        for (int rowgroup = 1; rowgroup <= 3; rowgroup++)
            haspruned |= pruneRowTriplet(data, rowgroup);
        
        return haspruned;
    }
    
    
    /**
     * Prune hints with the given data value via row comparison.
     * 
     * The rowgroup is between 1 and 3.
     * 
     * The startrow is 3 * (rowgroup - 1) + 1.  The other rows
     * are the next two rows.
     */
    private boolean pruneRowTriplet
        (int data, int rowgroup)
    {
        boolean haspruned = false;
        
        boolean[][] datainhint = new boolean[10][4];
        
        int row1 = 3 * (rowgroup - 1) + 1;
        int row3 = row1 + 2;
        
        for (int row = row1; row <= row3; row++) {
            for (int col = 1; col <= 9; col++) {
                int colgroup = ((col - 1) / 3) + 1;
                
                datainhint[row][colgroup] |=
                    hints[row][col].getHint(data);
            }
        }
        
        for (int colgroup = 1; colgroup <= 3; colgroup++) {
            int sum = 0;
            int row = 0;
            
            for (int r = row1; r <= row3; r++)
                if (datainhint[r][colgroup]) {
                    sum++;
                    row = r;
                }
            
            if (sum == 1) {
                for (int col = 1; col <= 9; col++)
                    if (colgroup != (((col - 1) / 3) + 1))
                        haspruned |= hints[row][col].pruneHint(data);
            }
        }
        
        return haspruned;
    }
    
    
    /** Prune hints with the given data value via col comparison. */
    private boolean pruneColTriplet(int data) {
        boolean haspruned = false;
        
        for (int colgroup = 1; colgroup <= 3; colgroup++)
            haspruned |= pruneColTriplet(data, colgroup);
        
        return haspruned;
    }
    
    
    /**
     * Prune hints with the given data value via col comparison.
     * 
     * The colgroup is between 1 and 3.
     * 
     * The startcol is 3 * (colgroup - 1) + 1.  The other cols
     * are the next two cols.
     */
    private boolean pruneColTriplet
        (int data, int colgroup)
    {
        boolean haspruned = false;
        
        boolean[][] datainhint = new boolean[4][10];
        
        int col1 = 3 * (colgroup - 1) + 1;
        int col3 = col1 + 2;
        
        for (int col = col1; col <= col3; col++) {
            for (int row = 1; row <= 9; row++) {
                int rowgroup = ((row - 1) / 3) + 1;
                
                datainhint[rowgroup][col] |=
                    hints[row][col].getHint(data);
            }
        }
        
        for (int rowgroup = 1; rowgroup <= 3; rowgroup++) {
            int sum = 0;
            int col = 0;
            
            for (int c = col1; c <= col3; c++)
                if (datainhint[rowgroup][c]) {
                    sum++;
                    col = c;
                }
            
            if (sum == 1) {
                for (int row = 1; row <= 9; row++)
                    if (rowgroup != (((row - 1) / 3) + 1))
                        haspruned |= hints[row][col].pruneHint(data);
            }
        }
        
        return haspruned;
    }
    
    
    /**
     * Returns an array of the hints corresponding to the
     * cell positions in the given array that are not yet
     * filled with data in the model.
     * 
     * Assumes cells has data in slots 1 to 9.
     * 
     * Returns an array of length 0 if all cell positions
     * already have data in the model.
     */
    private SudokuHint[] getHints(CellPosition[] cells) {
        int N = 0;
        
        for (int m = 1; m <= 9; m++) {
            int row = cells[m].row;
            int col = cells[m].col;
            
            if (celldata[row][col] == 0)
                N++;
        }
        
        SudokuHint[] array = new SudokuHint[N];
        
        if (N == 0)
            return array;
        
        int K = 0;
        
        for (int m = 1; m <= 9; m++) {
            int row = cells[m].row;
            int col = cells[m].col;
            
            if (celldata[row][col] == 0) {
                array[K] = hints[row][col];
                K++;
            }
        }
        
        return array;
    }
    
    
    /**
     * Prune the hints corresponding to each row of the
     * celllist array by using getHints to obtain the
     * hints for cells without data and then applying
     * the one argument form of pruneHintSets.
     */
    private boolean pruneHintSets() {
        boolean haspruned = false;
        
        SudokuHint[] hintlist;
        
        for (int i = 1; i <= 27; i++) {
            hintlist = getHints(celllist[i]);
            haspruned |= pruneHintListLoop(hintlist);
        }
        
        return haspruned;
    }
    
    
    /**
     * Prune the hints in the array of hints
     * provided that certain set-like conditions hold.
     * 
     * If there is a hint with 1 data value { a },
     * then prune { a } from all other hints.
     * 
     * If there is a hint with 2 data values { a, b }
     * and there is another hint that is a subset of
     * this hint, then prune { a, b } from all other
     * hints.
     * 
     * If there is a hint with 3 data values { a, b, c }
     * and there are another 2 hints that are subsets of
     * this hint, then prune { a, b, c } from all other
     * hints.
     * 
     * Etc, etc.
     * 
     * This method manages the loop to prune repeatedly
     * until pruning sets cannot be done further.
     */
    private boolean pruneHintListLoop(SudokuHint[] hintlist) {
        boolean haspruned = false;
        
        if (hintlist.length == 0)
            return haspruned;
        
        while (pruneHintListOnce(hintlist))
            haspruned = true;
        
        return haspruned;
    }
    
    
    /**
     * Prune the hints in the array of hints
     * provided that certain set-like conditions hold.
     * 
     * If there is a hint with 1 data value { a },
     * then prune { a } from all other hints.
     * 
     * If there is a hint with 2 data values { a, b }
     * and there is another hint that is a subset of
     * this hint, then prune { a, b } from all other
     * hints.
     * 
     * If there is a hint with 3 data values { a, b, c }
     * and there are another 2 hints that are subsets of
     * this hint, then prune { a, b, c } from all other
     * hints.
     * 
     * Etc, etc.
     * 
     * This method manages one cycle in the prune loop.
     */
    private boolean pruneHintListOnce(SudokuHint[] hintlist) {
        boolean haspruned = false;
        
        Arrays.sort(hintlist);
        
        int N = hintlist.length;
        
        int n = 0;
        
        while (n < N) {
            int k = hintlist[n].count();
            
            boolean ok = (0 < k) && ((k-1) <= n);
            
            if (! ok) {
                n++;
                continue;
            }
            
            boolean[] issubset = new boolean[N];
            
            issubset[n] = true;
            int m = 1;
            
            for (int i = 0; i < n; i++) {
                if (hintlist[i].count() > 0)
                    if (hintlist[n].contains(hintlist[i])) {
                        issubset[i] = true;
                        m++;
                    }
            }
            
            if (m == k) {
                for (int i = 0; i < N; i++)
                    if (! issubset[i])
                        haspruned |= hintlist[i].pruneHint(hintlist[n]);
                
                if (haspruned)
                    return haspruned;
            }
            
            n++;
        }
        
        return haspruned;
    }

    
    /**
     * Prune hints that are unique in a row, col, or block.
     */
    private boolean pruneUniqueHints() {
        boolean haspruned = false;
        
        for (int i = 1; i <= 27; i++)
            haspruned |= pruneUniqueHints(i);
        
        return haspruned;
    }
    
    
    /**
     * Prune hints that are unique in a row, col, or block
     * corresponding to celldata[i].
     */
    private boolean pruneUniqueHints(int i) {
        boolean haspruned = false;
        
        for (int data = 1; data <= 9; data++)
            haspruned |= pruneUniqueHints(i, data);
        
        return haspruned;
    }
    
    
    /**
     * Prune hints of value data
     * that are unique in a row, col, or block
     * corresponding to celldata[i].
     */
    private boolean pruneUniqueHints(int i, int data) {
        boolean haspruned = false;
        
        int sum = 0;
        int row = 0;
        int col = 0;
        int r = 0;
        int c = 0;
        
        for (int k = 1; k <= 9; k++) {
            row = celllist[i][k].row;
            col = celllist[i][k].col;
            
            if (celldata[row][col] == 0)
                if (hints[row][col].getHint(data)) {
                    sum++;
                    r = row;
                    c = col;
                }
        }
        
        if (sum == 1) {
            // prune all others hints from (r,c) itself
            for (int d = 1; d <= 9; d++)
                if (d != data)
                    haspruned |= hints[r][c].pruneHint(d);
            
            // prune data from direct neighbors of (r,c)
            haspruned |= pruneBasic(r, c, data);
        }
        
        return haspruned;
    }
    
    
    /**
     * Converts the Sudoku data from 9 rows with 9 cols
     * to 9 lines of text each with 9 digits.  Empty
     * cells are coded with digit 0 and non-empty cells
     * with their numeric value.
     */
    public String toString() {
        StringBuffer buffer = new StringBuffer(128);
        
        String string = "";
        
        for (int row = 1; row <= 9; row++) {
            for (int col = 1; col <= 9; col++)
                buffer.append(Integer.toString(celldata[row][col]));
            
            buffer.append('\n');
        }
        
        return buffer.toString();
    }
    
    
    /**
     * Identical to toString().
     * 
     * Used to implement Stringable.
     */
    public String toStringData() {
        return toString();
    }
    
    
    /**
     * Fills the cell data of this model using digit data
     * from the given string.
     * 
     * First clears all cell data in the model to 0.
     * 
     * Then uses the first 9 lines in the string to get 9
     * digits each to determine the corresponding cell
     * data in each col of each row.
     * 
     * Ignores non-digits other than the newlines used to
     * separate lines.  Gracefully handles the situation
     * of fewer than 9 lines or fewer than 9 digits per
     * line by simply leaving the corresponding cells at
     * the default of 0.
     * 
     * Permits the data to have no separators at all or
     * to have separators such as blanks or commas.
     * 
     * The call fromStringData(null) will clear the model.
     * 
     * Never throws ParseException.
     * 
     * Used to implement Stringable.
     */
    public void fromStringData(String data) {
        for (int row = 1; row <= 9; row++)
            for (int col = 1; col <= 9; col++)
                celldata[row][col] = 0;
        
        if (data == null)
            return;
        
        String[] lines = TextTools.extractNonEmptyLines(data);
        
        int rows = lines.length;
        
        for (int row = 1; row <= 9; row++) {
            int r = row - 1;
            
            if (r >= rows)
                break;
            
            int cols = lines[r].length();
            
            int c = 0;
            int col = 1;
            
            while (c < cols) {
                char x = lines[r].charAt(c);
                
                if (('0' <= x) && (x <= '9')) {
                    int digit = ((int) x) - ((int) '0');
                    setCellData(row, col, digit);
                    
                    col++;
                    if (col > 9)
                        break;
                }
                
                c++;
            }
        }
    }
    
    
    /** The main method that launches a SudokuBase. */
    public static void main(String[] args) {
        SudokuBase sudoku = new SudokuBase();
        
        sudoku.frame("Sudoku Puzzle");
    }
}
