/*
 * Copyright 2011 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed 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.drools.examples.sudoku;

import org.drools.examples.sudoku.swing.AbstractSudokuGridModel;
import org.drools.examples.sudoku.swing.SudokuGridEvent;
import org.drools.examples.sudoku.swing.SudokuGridModel;
import org.kie.api.event.rule.ObjectDeletedEvent;
import org.kie.api.event.rule.ObjectInsertedEvent;
import org.kie.api.event.rule.ObjectUpdatedEvent;
import org.kie.api.event.rule.RuleRuntimeEventListener;
import org.kie.api.runtime.KieContainer;
import org.kie.api.runtime.KieSession;
import org.kie.api.runtime.rule.FactHandle;

import java.util.Formatter;
import java.util.HashSet;
import java.util.Set;

/**
 * An object of this class solves Sudoku problems.
 */
public class Sudoku extends AbstractSudokuGridModel implements SudokuGridModel {

    public static Sudoku sudoku;
    
    public  Cell[][]    cells;
    private CellSqr[][] sqrs = new CellSqr[][]{ new CellSqr[3], new CellSqr[3], new CellSqr[3] };
    private CellRow[]   rows = new CellRow[9];
    private CellCol[]   cols = new CellCol[9];
    
    private KieContainer kc;
    private KieSession session;
    private SudokuWorkingMemoryListener workingMemoryListener = new SudokuWorkingMemoryListener();
    private Counter counter;
    private Boolean explain = false;
    private FactHandle steppingFactHandle;
    private Stepping stepping;
    private boolean unsolvable = false;
    
    /**
     * Constructor.
     */
    public Sudoku(KieContainer kc) {
        this.kc = kc;
        sudoku = this;
    }
    
    /*
     * (non-Javadoc)
     * @see org.kie.examples.sudoku.swing.SudokuGridModel#getCellValue(int, int)
     */
    public String getCellValue(int iRow, int iCol) {
        if (cells == null) return " ";
        return cells[iRow][iCol].valueAsString();
    }
    
    /**
     * Nice printout of the grid.
     */
    public void dumpGrid() {
        StringBuilder dumpGridSb = new StringBuilder();
        Formatter fmt = new Formatter(dumpGridSb);
        fmt.format("       ");
        for (int iCol = 0; iCol < 9; iCol++) {
            fmt.format("Col: %d     ", iCol);
        }
        fmt.format("\n");
        for (int iRow = 0; iRow < 9; iRow++) {
            fmt.format("Row " + iRow + ": ");
            for (int iCol = 0; iCol < 9; iCol++) {
                if (cells[iRow][iCol].getValue() != null) {
                    fmt.format(" --- %d --- ", cells[iRow][iCol].getValue());
                } else {
                    StringBuilder sb = new StringBuilder();
                    Set<Integer> perms = cells[iRow][iCol].getFree();
                    for (int i = 1; i <= 9; i++) {
                        if (perms.contains(i)) {
                            sb.append(i);
                        } else {
                            sb.append(' ');
                        }
                    }
                    fmt.format(" %-10s", sb.toString());
                }
            }
            fmt.format("\n");
        }
        fmt.close();
        System.out.println(dumpGridSb);
    }
    
    /**
     * Checks that everything is still according to the sudoku rules.
     */
    public void consistencyCheck() {
        for (int iRow = 0; iRow < 9; iRow++) {
            for (int iCol = 0; iCol < 9; iCol++) {
                Cell cell = cells[iRow][iCol];
                Integer value = cell.getValue();
                if (value != null) {
                    if (! cell.getFree().isEmpty()) {
                        throw new IllegalStateException("free not empty");
                    }
                    // any containing group
                    for (Cell other: cell.getExCells()) {
                        // must not occur in any of the other cells
                        if (value.equals(other.getValue())) {
                            throw new IllegalStateException("duplicate");
                        }
                        // must not occur in the permissibles of any of the other cells
                        if (other.getFree().contains(value)) {
                            throw new IllegalStateException("not eliminated");
                        }
                    }
                }
            }
        }
        
        for (int i = 0; i < rows.length; i++) {
            Set<Integer> aSet = new HashSet<>();
            for (int j = 0; j < rows[i].getCells().size(); j++) {
                Cell cell = rows[i].getCells().get(j);
                Integer value = cell.getValue();
                if (value != null) {
                    aSet.add(value);
                } else {
                    aSet.addAll(cell.getFree());
                }
            }
            if (! aSet.equals(CellGroup.ALL_NINE)) {
                throw new IllegalStateException("deficit in row");
            }
        }
        
        for (int i = 0; i < cols.length; i++) {
            Set<Integer> aSet = new HashSet<>();
            for (int j = 0; j < cols[i].getCells().size(); j++) {
                Cell cell = cols[i].getCells().get(j);
                Integer value = cell.getValue();
                if (value != null) {
                    aSet.add(value);
                } else {
                    aSet.addAll(cell.getFree());
                }
            }
            if (! aSet.equals(CellGroup.ALL_NINE)) {
                throw new IllegalStateException("deficit in column");
            }
        }

        for (int ir = 0; ir < sqrs.length; ir++) {
            for (int ic = 0; ic < sqrs[ir] .length; ic++) {
                Set<Integer> aSet = new HashSet<>();
                for (int j = 0; j < sqrs[ir][ic].getCells().size(); j++) {
                    Cell cell = sqrs[ir][ic].getCells().get(j);
                    Integer value = cell.getValue();
                    if (value != null) {
                        aSet.add(value);
                    } else {
                        aSet.addAll(cell.getFree());
                    }
                }
                if (! aSet.equals(CellGroup.ALL_NINE)) {
                    throw new IllegalStateException("deficit in square");
                }
            }
        }
        System.out.println("+++ check OK +++");
    }
    
    /*
     * (non-Javadoc)
     * @see org.kie.examples.sudoku.swing.SudokuGridModel#solve()
     */
    public void solve() {
        if (this.isSolved()) return;
        explain = false;
        session.setGlobal("explain", explain);
        if( steppingFactHandle != null ){
            session.delete( steppingFactHandle );
            steppingFactHandle = null;
            stepping = null;
        }
        this.session.fireAllRules();
//        dumpGrid();
    }
    
    /*
     * (non-Javadoc)
     * @see org.kie.examples.sudoku.swing.SudokuGridModel#step()
     */
    public void step() {
        if (this.isSolved()) return;
        explain = true;
        session.setGlobal("explain", explain);
        this.counter.setCount(1);
        session.update(session.getFactHandle(this.counter), this.counter);
        if( steppingFactHandle == null ){
            steppingFactHandle = session.insert( stepping = new Stepping() );
        }
        this.session.fireUntilHalt();
        if( stepping.isEmergency() ){
            this.unsolvable = true;
        }
//        dumpGrid();
    }

    public boolean isSolved() {
        for (int iRow = 0; iRow < 9; iRow++) {
            for (int iCol = 0; iCol < 9; iCol++) {
                if (cells[iRow][iCol].getValue() == null) return false;
            }
        }
        return true;
    }
    
    public boolean isUnsolvable(){
        return unsolvable;
    }
    
    private void create() {
        for (int i = 0; i < 9; i++) {
            session.insert(Integer.valueOf(i+1));
            rows[i] = new CellRow(i);
            cols[i] = new CellCol(i);
        }
        
        cells = new Cell[9][];
        for (int iRow = 0; iRow < 9; iRow++) {
            cells[iRow] = new Cell[9];
            for (int iCol = 0; iCol < 9; iCol++) {
                Cell cell = cells[iRow][iCol] = new Cell();
                rows[iRow].addCell(cell);
                cols[iCol].addCell(cell);
            }
        }

        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                sqrs[i][j] = new CellSqr(rows[i*3], rows[i*3+1], rows[i*3+2],
                                         cols[j*3], cols[j*3+1], cols[j*3+2]);
            }
        }

        for (int iRow = 0; iRow < 9; iRow++) {
            for (int iCol = 0; iCol < 9; iCol++) {
                cells[iRow][iCol].makeReferences(rows[iRow], cols[iCol], sqrs[iRow/3][iCol/3]);
                session.insert(cells[iRow][iCol]);
            }
            session.insert(rows[iRow]);
            session.insert(cols[iRow]);
            session.insert(sqrs[iRow/3][iRow%3]);
        }
    }

    /*
     * (non-Javadoc)
     * @see org.kie.examples.sudoku.swing.SudokuGridModel#setCellValues(java.lang.Integer[][])
     */
    public void setCellValues(Integer[][] cellValues) {
        if (session != null) {
            session.removeEventListener(workingMemoryListener);
            session.dispose();
            steppingFactHandle = null;
        }
        
        this.session = kc.newKieSession("SudokuKS");
        session.setGlobal("explain", explain);
        session.addEventListener(workingMemoryListener);

        Setting s000 = new Setting(0, 0, 0);
        FactHandle fh000 = this.session.insert(s000);
        this.create();

        int initial = 0;
        for (int iRow = 0; iRow < 9; iRow++) {
            for (int iCol = 0; iCol < 9; iCol++) {
                Integer value = cellValues[iRow][iCol];
                if (value != null) {
                    session.insert(new Setting(iRow, iCol, value));
                    initial++;
                }
            }
        }
        this.counter = new Counter(initial);
        this.session.insert(counter);
        this.session.delete(fh000);
        this.session.fireAllRules();
    }
    
    /*
     * (non-Javadoc)
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();

        sb.append("Sudoku:").append('\n');
        for (int iRow = 0; iRow < 9;  iRow++) {
            sb.append("  ").append(rows[iRow].toString()).append('\n');
        }
        
        return sb.toString();
    }
    
    class SudokuWorkingMemoryListener implements RuleRuntimeEventListener {

        public void objectInserted(ObjectInsertedEvent ev) {
            if (ev.getObject() instanceof Counter) {
                fireRestartEvent(null);
            }
        }

        public void objectDeleted(ObjectDeletedEvent ev) {
        }

        public void objectUpdated(ObjectUpdatedEvent ev) {
            if (ev.getObject() instanceof Cell) {
                Cell cell = (Cell) ev.getObject();
                if (cell.getValue() != null) {
                    fireCellUpdatedEvent(new SudokuGridEvent(this,
                            cell.getRowNo(),
                            cell.getColNo(),
                            cell.getValue()));
                }
            }
        }
    }
    
    public void validate(){
        session.getAgenda().getAgendaGroup( "validate" ).setFocus();
        session.fireUntilHalt();
    }
}
