/*
 * 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 javax.annotation.Nonnull;

import edu.umd.cs.findbugs.SystemProperties;
import edu.umd.cs.findbugs.ba.Debug;
import edu.umd.cs.findbugs.ba.Location;
import edu.umd.cs.findbugs.ba.XFactory;
import edu.umd.cs.findbugs.ba.XField;
import edu.umd.cs.findbugs.ba.XMethod;
import edu.umd.cs.findbugs.ba.XMethodParameter;
import edu.umd.cs.findbugs.classfile.FieldDescriptor;

/**
 * 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) {
        FieldDescriptor fieldDescriptor = methodInvoked.getAccessMethodForField();
        if (fieldDescriptor != null) {
            XField f = XFactory.getExactXField(fieldDescriptor);
            return markInformationAsComingFromFieldValue(f);
        }

        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 instance representing a parameter marked as Nonnull
     */
    public static IsNullValue parameterMarkedAsNonnull(XMethodParameter mp) {
        return instanceByFlagsList[PARAM >> FLAG_SHIFT][NN];
    }
    /**
     * 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
