/*
 * Bytecode Analysis Framework
 * Copyright (C) 2003-2005 University of Maryland
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package edu.umd.cs.findbugs.ba.npe;

import edu.umd.cs.findbugs.SystemProperties;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.ba.Debug;
import edu.umd.cs.findbugs.ba.Location;
import edu.umd.cs.findbugs.ba.XField;
import edu.umd.cs.findbugs.ba.XMethod;
import edu.umd.cs.findbugs.ba.XMethodParameter;

/**
 * A class to abstractly represent values in stack slots,
 * indicating whether thoses values can be null, non-null,
 * null on some incoming path, or unknown.
 *
 * @author David Hovemeyer
 * @see IsNullValueFrame
 * @see IsNullValueAnalysis
 */
public class IsNullValue implements IsNullValueAnalysisFeatures, Debug {
	private static final boolean DEBUG_EXCEPTION = SystemProperties.getBoolean("inv.debugException");
	private static final boolean DEBUG_KABOOM = SystemProperties.getBoolean("inv.debugKaboom");

	/** Definitely null. */
	private static final int NULL = 0;
	/** Definitely null because of a comparison to a known null value. */
	private static final int CHECKED_NULL = 1;

	/** Definitely not null. */
	private static final int NN = 2;
	/** Definitely not null because of a comparison to a known null value. */
	private static final int CHECKED_NN = 3;
	/** Definitely not null an NPE would have occurred and we would not be here if it were null. */
	private static final int NO_KABOOM_NN = 4;


	/** Null on some simple path (at most one branch) to current location. */
	private static final int NSP = 5;
	/** Unknown value (method param, value read from heap, etc.), assumed not null. */
	private static final int NN_UNKNOWN = 6;
	/** Null on some complex path (at least two branches) to current location. */
	private static final int NCP2 = 7;
	/** Null on some complex path (at least three branches) to current location. */
	private static final int NCP3 = 8;

	private static final int FLAG_SHIFT = 8;

	/** Value was propagated along an exception path. */
	private static final int EXCEPTION = 1 << FLAG_SHIFT;
	/** Value is (potentially) null because of a parameter passed to the method. */
	private static final int PARAM = 2 << FLAG_SHIFT;
	/** Value is (potentially) null because of a value returned from a called method. */
	private static final int RETURN_VAL = 4 << FLAG_SHIFT;
	private static final int FIELD_VAL = 8 << FLAG_SHIFT;
	/** Value is (potentially) null because of a value returned from readline. */
	private static final int READLINE_VAL = (16 << FLAG_SHIFT) | RETURN_VAL;

	private static final int FLAG_MASK = EXCEPTION | PARAM | RETURN_VAL | FIELD_VAL | READLINE_VAL; 

	private static final int[][] mergeMatrix = {
		// NULL, CHECKED_NULL, NN,         CHECKED_NN, NO_KABOOM_NN, NSP,  NN_UNKNOWN, NCP2, NCP3
		{NULL},                                                                      // NULL
		{NULL,   CHECKED_NULL, },                                                    // CHECKED_NULL
		{NSP,    NSP,          NN},                                                  // NN
		{NSP,    NSP,          NN,         CHECKED_NN, },                            // CHECKED_NN
		{NSP,    NSP,          NN,         NN,			 NO_KABOOM_NN},                // NO_KABOOM_NN
		{NSP,    NSP,          NSP,        NSP,		NSP,     NSP},                         // NSP
		{NSP,    NSP,          NN_UNKNOWN, NN_UNKNOWN, NN_UNKNOWN,	NSP,  NN_UNKNOWN, },          // NN_UNKNOWN
		{NSP,   NSP,         NCP2,       NCP2,       NCP2, NSP, NCP2,        NCP2,},    // NCP2
		{NSP,   NSP,         NCP3,       NCP3,       NCP3, NSP, NCP3,        NCP3, NCP3}// NCP3
	};

	private static final IsNullValue[][] instanceByFlagsList = createInstanceByFlagList();

	private static IsNullValue[][] createInstanceByFlagList() {
		final int max = FLAG_MASK >>> FLAG_SHIFT;
		IsNullValue[][] result = new IsNullValue[max + 1][];
		for (int i = 0; i <= max; ++i) {
			final int flags = i << FLAG_SHIFT;
			result[i] = new IsNullValue[]{
					new IsNullValue(NULL | flags),
					new IsNullValue(CHECKED_NULL | flags),
					new IsNullValue(NN | flags),
					new IsNullValue(CHECKED_NN | flags),
					null, // NO_KABOOM_NN values must be allocated dynamically
					new IsNullValue(NSP | flags),
					new IsNullValue(NN_UNKNOWN | flags),
					new IsNullValue(NCP2 | flags),
					new IsNullValue(NCP3 | flags),
			};
		}

		return result;
	}

	// Fields
	private final int kind;
	private final Location locationOfKaBoom;

	private IsNullValue(int kind) {
		this.kind = kind;
		locationOfKaBoom = null;
		if (VERIFY_INTEGRITY) checkNoKaboomNNLocation();
	}

	private IsNullValue(int kind, Location ins) {
		this.kind = kind;
		locationOfKaBoom = ins;
		if (VERIFY_INTEGRITY) checkNoKaboomNNLocation();
	}

	private void checkNoKaboomNNLocation() {
		if (getBaseKind() == NO_KABOOM_NN && locationOfKaBoom == null) {
			throw new IllegalStateException("construction of no-KaBoom NN without Location");
		}
	}

	@Override
	public boolean equals(Object o) {
		if (o == null || this.getClass() != o.getClass())
			return false;
		IsNullValue other = (IsNullValue) o;
		if ( kind != other.kind) return false;
		if (locationOfKaBoom == other.locationOfKaBoom) return true;
		if (locationOfKaBoom == null || other.locationOfKaBoom == null) return false;
		return locationOfKaBoom.equals(other.locationOfKaBoom);
	}

	@Override
	public int hashCode() {
		int hashCode =  kind;
		if (locationOfKaBoom != null)
			hashCode += locationOfKaBoom.hashCode();
		return hashCode;
	}

	private int getBaseKind() {
		return kind & ~FLAG_MASK;
	}

	private int getFlags() {
		return kind & FLAG_MASK;
	}
	
	private boolean hasFlag(int flag) {
		return (kind & flag) == flag;
	}

	/**
	 * Was this value propagated on an exception path?
	 */
	public boolean isException() {
		return hasFlag(EXCEPTION);
	}
	/**
	 * Was this value marked as a possibly null return value?
	 */
	public boolean isReturnValue() {
		return hasFlag(RETURN_VAL);
	}
	public boolean isReadlineValue() {
		return hasFlag(READLINE_VAL);
	}

	public boolean isFieldValue() {
		return hasFlag(FIELD_VAL);
	}
	/**
	 * Was this value marked as a possibly null parameter?
	 */
	public boolean isParamValue() {
		return (kind & PARAM) != 0;
	}

	/**
	 * Is this value known because of an explicit null check?
	 */
	public boolean isChecked() {
		return getBaseKind() == CHECKED_NULL || getBaseKind() == CHECKED_NN;
	}

	/**
	 * Is this value known to be non null because a NPE would have occurred otherwise?
	 */
	public boolean wouldHaveBeenAKaboom() {
		return getBaseKind() == NO_KABOOM_NN;
	}

	private IsNullValue toBaseValue() {
		return instanceByFlagsList[0][getBaseKind()];
	}

	/**
	 * Convert to an exception path value.
	 */
	public IsNullValue toExceptionValue() {
		if (getBaseKind() == NO_KABOOM_NN) return new IsNullValue(kind | EXCEPTION, locationOfKaBoom);
		return instanceByFlagsList[(getFlags() | EXCEPTION) >> FLAG_SHIFT][getBaseKind()];
	}

	/**
	 * Convert to a value known because it was returned from a method
	 * in a method property database.
	 * @param methodInvoked TODO
	 */
	public IsNullValue markInformationAsComingFromReturnValueOfMethod(XMethod methodInvoked) {
		int flag = RETURN_VAL;
		if (methodInvoked.getName().equals("readLine") && methodInvoked.getSignature().equals("()Ljava/lang/String;"))
			flag = READLINE_VAL;
		if (getBaseKind() == NO_KABOOM_NN) return new IsNullValue(kind | flag, locationOfKaBoom);
		return instanceByFlagsList[(getFlags() | flag) >> FLAG_SHIFT][getBaseKind()];
	}
	/**
	 * Convert to a value known because it was returned from a method
	 * in a method property database.
	 * @param field TODO
	 */
	public IsNullValue markInformationAsComingFromFieldValue(XField field) {
		if (getBaseKind() == NO_KABOOM_NN) return new IsNullValue(kind | FIELD_VAL, locationOfKaBoom);
		return instanceByFlagsList[(getFlags() | FIELD_VAL) >> FLAG_SHIFT][getBaseKind()];
	}

	/**
	 * Get the instance representing values that are definitely null.
	 */
	public static IsNullValue nullValue() {
		return instanceByFlagsList[0][NULL];
	}

	/**
	 * Get the instance representing a value known to be null
	 * because it was compared against null value, or because
	 * we saw that it was assigned the null constant.
	 */
	public static IsNullValue checkedNullValue() {
		return instanceByFlagsList[0][CHECKED_NULL];
	}

	/**
	 * Get the instance representing values that are definitely not null.
	 */
	public static IsNullValue nonNullValue() {
		return instanceByFlagsList[0][NN];
	}

	/**
	 * Get the instance representing a value known to be non-null
	 * because it was compared against null value, or because
	 * we saw the object creation.
	 */
	public static IsNullValue checkedNonNullValue() {
		return instanceByFlagsList[0][CHECKED_NN];
	}

	/**
	 * Get the instance representing a value known to be non-null
	 * because a NPE would have occurred if it were null.
	 */
	public static IsNullValue noKaboomNonNullValue(@NonNull Location ins) {
		if (ins == null) 
			throw new NullPointerException("ins cannot be null");
		return new IsNullValue(NO_KABOOM_NN, ins);
	}

	/**
	 * Get the instance representing values that are definitely null
	 * on some simple (no branches) incoming path.
	 */
	public static IsNullValue nullOnSimplePathValue() {
		return instanceByFlagsList[0][NSP];
	}

	/**
	 * Get instance representing a parameter marked as MightBeNull
	 */
	public static IsNullValue parameterMarkedAsMightBeNull(XMethodParameter mp) {
		return instanceByFlagsList[PARAM >> FLAG_SHIFT][NSP];
	}

	/**
	 * Get non-reporting non-null value.
	 * This is what we use for unknown values.
	 */
	public static IsNullValue nonReportingNotNullValue() {
		return instanceByFlagsList[0][NN_UNKNOWN];
	}

	/**
	 * Get null on complex path value.
	 * This is like null on simple path value, but there
	 * are at least two branches between the explicit null value
	 * and the current location.  If the conditions are correlated,
	 * then the path on which the value is null may be infeasible.
	 */
	public static IsNullValue nullOnComplexPathValue() {
		return instanceByFlagsList[0][NCP2];
	}

	/**
	 * Like "null on complex path" except that there are at least
	 * <em>three</em> branches between the explicit null value
	 * and the current location.
	 */
	public static IsNullValue nullOnComplexPathValue3() {
		return instanceByFlagsList[0][NCP3];
	}

	/**
	 * Get null value resulting from comparison to explicit null.
	 */
	public static IsNullValue pathSensitiveNullValue() {
		return instanceByFlagsList[0][CHECKED_NULL];
	}

	/**
	 * Get non-null value resulting from comparison to explicit null.
	 */
	public static IsNullValue pathSensitiveNonNullValue() {
		return instanceByFlagsList[0][CHECKED_NN];
	}

	/**
	 * Merge two values.
	 */
	public static IsNullValue merge(IsNullValue a, IsNullValue b) {
		if (a == b) return a;
		if (a.equals(b)) return a;
		int aKind = a.kind & 0xff;
		int bKind = b.kind & 0xff;
		int aFlags = a.getFlags();
		int bFlags = b.getFlags();


		int combinedFlags =  aFlags & bFlags;


		if (!(a.isNullOnSomePath() || a.isDefinitelyNull()) && b.isException())
				combinedFlags |= EXCEPTION;
		else
			if (!(b.isNullOnSomePath() || b.isDefinitelyNull()) && a.isException())
				combinedFlags |= EXCEPTION;

		// Left hand value should be >=, since it is used
		// as the first dimension of the matrix to index.
		if (aKind < bKind) {
			int tmp = aKind;
			aKind = bKind;
			bKind = tmp;
		}
		assert aKind >= bKind;
		int result = mergeMatrix[aKind][bKind];

		IsNullValue resultValue = (result == NO_KABOOM_NN)
				? noKaboomNonNullValue(a.locationOfKaBoom)
				: instanceByFlagsList[combinedFlags >> FLAG_SHIFT][result];

		return resultValue;
	}

	/**
	 * Is this value definitely null?
	 */
	public boolean isDefinitelyNull() {
		int baseKind = getBaseKind();
		return baseKind == NULL || baseKind == CHECKED_NULL;
	}

	/**
	 * Is this value null on some path?
	 */
	public boolean isNullOnSomePath() {
		int baseKind = getBaseKind();
		if (NCP_EXTRA_BRANCH) {
			// Note: NCP_EXTRA_BRANCH is an experimental feature
			// to see how many false warnings we get when we allow
			// two branches between an explicit null and a
			// a dereference.
			return baseKind == NSP || baseKind == NCP2;
		} else {
			return baseKind == NSP;
		}
	}
	/**
	 * Is this value null on a complicated path?
	 */
	public boolean isNullOnComplicatedPath() {
		int baseKind = getBaseKind();
		 return baseKind == NN_UNKNOWN || baseKind == NCP2 || baseKind == NCP3;
	}
	/**
	 * Is this value null on a complicated path?
	 */
	public boolean isNullOnComplicatedPath23() {
		int baseKind = getBaseKind();
		 return  baseKind == NCP2 || baseKind == NCP3;
	}
	/**
	 * Is this value null on a complicated path?
	 */
	public boolean isNullOnComplicatedPath2() {
		int baseKind = getBaseKind();
		 return  baseKind == NCP2;
	}
	/**
	 * Return true if this value is either definitely null,
	 * or might be null on a simple path. 
	 * 
	 * @return true if this value is either definitely null,
	 *         or might be null on a simple path, false otherwise
	 */
	public boolean mightBeNull() {
		return isDefinitelyNull() || isNullOnSomePath();
	}

	/**
	 * Is this value definitely not null?
	 */
	public boolean isDefinitelyNotNull() {
		int baseKind = getBaseKind();
		return baseKind == NN || baseKind == CHECKED_NN || baseKind == NO_KABOOM_NN;
	}

	@Override
	public String toString() {
		String pfx = "";
		if (DEBUG_EXCEPTION) {
			int flags = getFlags();
			if (flags == 0)
				pfx = "_";
			else {
				if ((flags & EXCEPTION) != 0) pfx += "e";
				if ((flags & PARAM) != 0) pfx += "p";
				if ((flags & RETURN_VAL) != 0) pfx += "r";
				if ((flags & FIELD_VAL) != 0) pfx += "f";
			}
		}
		if (DEBUG_KABOOM && locationOfKaBoom == null) {
			pfx += "[?]";
		}
		switch (getBaseKind()) {
		case NULL:
			return pfx + "n" + ",";
		case CHECKED_NULL:
			return pfx + "w" + ",";
		case NN:
			return pfx + "N" + ",";
		case CHECKED_NN:
			return pfx + "W" + ",";
		case NO_KABOOM_NN:
			return pfx + "K" + ",";
		case NSP:
			return pfx + "s" + ",";
		case NN_UNKNOWN:
			return pfx + "-" + ",";
		case NCP2:
			return pfx + "/" + ",";
		default:
			throw new IllegalStateException("unknown kind of IsNullValue: " + kind);
		}
	}
	public Location getLocationOfKaBoom() {
		return locationOfKaBoom;
	}

	/**
	 * Control split: move given value down in the lattice
	 * if it is a conditionally-null value.
	 * 
	 * @return another value (equal or further down in the lattice)
	 */
	public IsNullValue downgradeOnControlSplit() {
		IsNullValue value = this;

		if (NCP_EXTRA_BRANCH) {
			// Experimental: track two distinct kinds of "null on complex path" values.
			if (value.isNullOnSomePath())
				value = nullOnComplexPathValue();
			else if (value.equals(nullOnComplexPathValue()))
				value = nullOnComplexPathValue3();

		} else {
			// Downgrade "null on simple path" values to
			// "null on complex path".
			if (value.isNullOnSomePath())
				value = nullOnComplexPathValue();
		}
		return value;
	}
}

// vim:ts=4
