import java.util.*;
import gov.nasa.jpf.*;
import gov.nasa.jpf.util.*;
import gov.nasa.jpf.jvm.*;
import gov.nasa.jpf.jvm.bytecode.*;
import gov.nasa.jpf.search.*;

public class NonNullChecker extends PropertyListenerAdapter {

    /**
     * Visitor for instructions.  This way, we do not have to do any
     * instanceof checks.  The compiler does all that for use with a little
     * runtime overhead.
     */
    private class NonNullPostVisitor extends InstructionVisitorAdapter {
	public void visit(PUTFIELD insn) {
	    checkFieldInsn(insn);
	}

	public void visit(PUTSTATIC insn) {
	    checkFieldInsn(insn);
	}

	private void checkFieldInsn(FieldInstruction insn) {
	    if (isRelevantField(insn)) {
		if (isNullFieldStore(insn)) {
		    storeError(vm, insn);
		    vm.breakTransition();
		}
	    }
	}

	public void visit(ASTORE insn) {
	    checkLocalVarInsn(insn);
	}

	private void checkLocalVarInsn(ASTORE insn) {
	    if (isRelevantVar(insn)) {
		if (isNullVarStore(insn)) {
		    storeError(vm, insn);
		    vm.breakTransition();
		}
	    }
	}
    }

    /// The non-nullable fields
    FieldSpec[] nonNullableFields;
    /// The non-nullable vars
    VarSpec[] nonNullableVars;
    /// Error string
    String error;
    
    /// --- For easy visiting ---
    /// The VM of the last executed instruction
    JVM vm;
    /// The visitor to use
    InstructionVisitor visitor;

    /**
     * Parse configuration and get what we are interested in.
     * @param conf JPF-Configuration.
     */
    public NonNullChecker(Config conf) {
	Set<String> spec = conf.getStringSet("nnc.fields");
	if (spec == null)
	    spec = Collections.emptySet();
	nonNullableFields = new FieldSpec[spec.size()];
	int i = -1;
	for (String field : spec)
	    nonNullableFields[++i] = FieldSpec.createFieldSpec(field);
	spec = conf.getStringSet("nnc.vars");
	if (spec == null)
	    spec = Collections.emptySet();
	nonNullableVars = new VarSpec[spec.size()];
	i = -1;
	for (String var : spec)
	    nonNullableVars[++i] = VarSpec.createVarSpec(var);
	visitor = new NonNullPostVisitor();
    }

    /**
     * The JVM just executed an instruction and notifies us about it.  If this
     * instruction was a write into a field we are interested in, do a null
     * check.
     * @param vm JPF-JVM
     */
    public void instructionExecuted(JVM vm) {
	this.vm = vm;
	vm.getLastInstruction().accept(visitor);
    }

    private boolean isRelevantVar(ASTORE insn) {
        int slotIdx = insn.getLocalVariableIndex();
	MethodInfo mi = insn.getMethodInfo();
	int pc = insn.getPosition() + 1;

	for (VarSpec varSpec : nonNullableVars) {
	    if (varSpec.getMatchingLocalVarInfo(mi, pc, slotIdx) != null)
		return true;
	}
	return false;
    }

    /**
     * Was the write to the field we are monitoring.
     * @param insn Instruction that just modified a field.
     * @return <code>true</code> if the write was to
     *         {@link #fieldSpec fieldSpec}
     */
    private boolean isRelevantField(FieldInstruction insn) {
	if (!insn.isReferenceField())
	    return false;
	FieldInfo fi = insn.getFieldInfo();
	for (FieldSpec fieldSpec : nonNullableFields) {
	    if (fieldSpec.matches(fi)) {
		return true;
	    }
	}
	return false;
    }

    // POST EXECUTION METHODS
    /**
     * Did this field instruction store <code>null</code> into a relevant field?
     * @param insn Field instruction (should be PUTFIELD or PUTSTATIC).
     * @return <code>true</code> iff the store wrote <code>null</code>
     */
    private boolean isNullFieldStore(FieldInstruction insn) {
	FieldInfo fi = insn.getFieldInfo();
	ElementInfo ei = insn.getLastElementInfo();
	return ei.getFieldValueObject(fi.getName()) == null;
    }

    /**
     * Did this instruction store <code>null</code> in a relevant local var?
     * @param insn The instruction.
     * @return <code>true</code> iff the store wrote <code>null</code>
     */
    private boolean isNullVarStore(ASTORE insn) {
	// Get the thread info
	ThreadInfo ti = vm.getLastThreadInfo();
	// Get the index of the local variable on the stack frame
	int slotIdx = insn.getLocalVariableIndex();
	// We either get null for null, or a valid ElementInfo
	return ti.getObjectLocal(slotIdx) == null;
    }

    /**
     * Store an error message.
     * @param vm JPF-JVM
     * @param insn Field instruction that wrote <code>null</code> to a
     *             non_nullable field
     */
    private void storeError(JVM vm, FieldInstruction insn) {
	ThreadInfo ti = vm.getLastThreadInfo();
	FieldInfo fi = insn.getFieldInfo();
	error = String.format("field %s assigned to null in thread %s at %s", fi.getFullName(), ti.getName(), insn.getSourceLocation());
    }

    /**
     * Store an error message.
     * @param vm JPF-JVM
     * @param insn Field instruction that wrote <code>null</code> to a
     *             non_nullable field
     */
    private void storeError(JVM vm, LocalVariableInstruction insn) {
	ThreadInfo ti = vm.getLastThreadInfo();
	LocalVarInfo vi = insn.getLocalVarInfo();
	error = String.format("variable %s assigned to null in thread %s at %s", vi.getName(), ti.getName(), insn.getSourceLocation());
    }

    public boolean check(Search search, JVM vm) {
	return error == null;
    }

    public String getErrorMessage() {
	return error;
    }

    public void reset() {
	error = null;
    }

}
