package kodkod.engine.satlab;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;

/**
 * Runs an external version of minisat (whichever "minisat" is found in the PATH)
 * 
 * @author aleks
 */
public class MiniSatExternal implements SATSolver {

    private File cnfFile; 
    private OutputStream cnfOut; 
    
    private int vars = 0;
    private int clauses = 0;
    private boolean freed = false; 
    
    private boolean sat = false;
    private boolean[] model; 
    
    public MiniSatExternal() {
        init();
    }

    private void init() {
        try {
            cnfFile = File.createTempFile("kk_minisat", ".cnf");
            cnfOut = new BufferedOutputStream(new FileOutputStream(cnfFile));
        } catch (IOException e) {
            throw new RuntimeException("Could not create cnf file: " + cnfFile.getAbsolutePath(), e);
        }
    }

    @Override
    public boolean addClause(int[] lits) {
        try {
            clauses++;
            StringBuilder sb = new StringBuilder();
            for (int l : lits) sb.append(l + " ");
            sb.append("0\n");
            cnfOut.write(sb.toString().getBytes());
            return false;
        } catch (IOException e) {
            throw new RuntimeException("Could not write to cnf file " + cnfFile.getAbsolutePath(), e);
        }
    }

    @Override
    public void addVariables(int numVars) {
        assert numVars >= 0; 
        vars += numVars;
    }

    @Override
    public void free() {
        try {
            if (!freed) {
                cnfOut.flush();
                cnfOut.close();
                cnfFile.deleteOnExit();
                freed = true;
            }
        } catch (IOException e) {
            throw new RuntimeException("Could not close cnf output stream");
        }
    }

    @Override
    public int numberOfClauses() {
        return clauses;
    }

    @Override
    public int numberOfVariables() {
        return vars;
    }

    @Override
    public boolean solve() throws SATAbortedException {
        finalizeCnf(); 
        File modelFile = runMinisat();
        readModel(modelFile);
        if (sat) 
            assert checkModel();
        return sat;
    }

    private boolean checkModel() {
        BufferedReader br = null; 
        try {
            br = new BufferedReader(new InputStreamReader(new FileInputStream(cnfFile)));
            String line = br.readLine(); //header
            line = br.readLine();
            while (line != null) {
                String[] clause = line.trim().split("\\ ");
                int i; 
                for (i = 0; i < clause.length - 1; i++) {
                    int lit = Integer.parseInt(clause[i]);
                    if ((lit > 0 ? valueOf(lit) : !valueOf(Math.abs(lit))))
                        break;
                }
                if (i == clause.length - 1)
                    return false;
                line = br.readLine();
            }
            return true;
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                if (br != null) br.close();
            } catch (IOException e) {
            }
        }
    }

    @Override
    public boolean valueOf(int variable) {
        if (!sat)
            throw new RuntimeException("UNSAT");
        return model[variable-1];
    }
    
    private void finalizeCnf() {
        try {
            free();
            BufferedInputStream in = new BufferedInputStream(new FileInputStream(cnfFile));
            File tmp = new File(cnfFile + ".tmp"); 
            BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(tmp)); 
            byte[] header = String.format("p cnf %s %s\n", vars, clauses).getBytes();
            out.write(header);
            int ch;
            while ((ch = in.read()) != -1) {
                out.write(ch);
            }
            in.close();
            out.close();
            tmp.renameTo(cnfFile);
        } catch (IOException e) {
            throw new RuntimeException("could not preprend header to cnf file: " + cnfFile.getAbsolutePath(), e);
        }
    }
    
    private File runMinisat() {
        try {
            File modelFile = File.createTempFile("minisat_model", ".txt");
            modelFile.deleteOnExit();
            String cmd = String.format("minisat -verb=0 %s %s", cnfFile.getAbsolutePath(), modelFile.getAbsolutePath());
            Process p = Runtime.getRuntime().exec(cmd);
            int retVal = p.waitFor();
            if (retVal == 0)
                throw new RuntimeException("couldn't run shell command: " + cmd);
            return modelFile;
        } catch (Exception e) {
            throw new RuntimeException("error during solving", e);
        }
    }
    
    private void readModel(File modelFile) {
        try {
            BufferedInputStream in = new BufferedInputStream(new FileInputStream(modelFile));
            String line = readLine(in).trim();
            if (!"SAT".equals(line)) {
                sat = false;
                model = null;
                return;
            }
            sat = true; 
            model = new boolean[vars];
            int chCode;
            int cnt = 0;
            int curr = 0; 
            boolean sign = true;
            while ((chCode = in.read()) != -1) {
                char ch = (char)chCode;
                switch (ch) {
                case '-': 
                    sign = false; 
                    break;
                case ' ': case '\n':
                    if (curr == 0)
                        break;
                    assert curr == cnt + 1; 
                    model[cnt] = sign;
                    cnt++; 
                    curr = 0; 
                    sign = true;
                    break;
                default:    
                    int x = ch - '0';
                    assert x >= 0 && x <= 9; 
                    curr = 10*curr + x;
                    break;
                }
            }
            in.close();
        } catch (IOException e) {
            throw new RuntimeException("couldn't read model file: " + model);
        }
    }

    private String readLine(InputStream in) throws IOException {
        StringBuilder sb = new StringBuilder();
        while (true) {
            int ch = in.read();
            if (ch == -1) break;
            if (ch == '\n') break;
            sb.append((char)ch);
        }
        return sb.toString();
    }

}