/*
 * FindBugs - Find bugs in Java programs
 * Copyright (C) 2003,2004 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.detect;

import org.apache.bcel.Constants;
import org.apache.bcel.generic.ConstantPoolGen;
import org.apache.bcel.generic.INVOKEINTERFACE;
import org.apache.bcel.generic.INVOKESPECIAL;
import org.apache.bcel.generic.INVOKEVIRTUAL;
import org.apache.bcel.generic.Instruction;
import org.apache.bcel.generic.InstructionHandle;
import org.apache.bcel.generic.InvokeInstruction;

import edu.umd.cs.findbugs.ResourceCreationPoint;
import edu.umd.cs.findbugs.ba.BasicBlock;
import edu.umd.cs.findbugs.ba.Hierarchy;
import edu.umd.cs.findbugs.ba.Location;
import edu.umd.cs.findbugs.ba.RepositoryLookupFailureCallback;
import edu.umd.cs.findbugs.ba.ResourceValue;
import edu.umd.cs.findbugs.ba.ResourceValueFrame;

/**
 * A Stream object marks the location in the code where a stream is created. It
 * also is responsible for determining some aspects of how the stream state is
 * tracked by the ResourceValueAnalysis, such as when the stream is opened or
 * closed, and whether implicit exception edges are significant.
 * <p/>
 * <p>
 * TODO: change streamClass and streamBase to ObjectType
 * <p/>
 * <p>
 * TODO: isStreamOpen() and isStreamClose() should probably be abstract, so we
 * can customize how they work for different kinds of streams
 */
public class Stream extends ResourceCreationPoint implements Comparable<Stream> {
    private final String streamBase;

    private boolean isUninteresting;

    private boolean isOpenOnCreation;

    private Location openLocation;

    private boolean ignoreImplicitExceptions;

    private String bugType;

    private int instanceParam;

    private boolean isClosed;

    @Override
    public String toString() {
        return streamBase + ":" + openLocation;
    }

    /**
     * Constructor. By default, Stream objects are marked as uninteresting.
     * setInteresting("BUG_TYPE") must be called explicitly to mark the Stream
     * as interesting.
     *
     * @param location
     *            where the stream is created
     * @param streamClass
     *            type of Stream
     * @param streamBase
     *            highest class in the class hierarchy through which stream's
     *            close() method could be called
     */
    public Stream(Location location, String streamClass, String streamBase) {
        super(location, streamClass);
        this.streamBase = streamBase;
        isUninteresting = true;
        instanceParam = -1;
    }

    /**
     * Mark this Stream as interesting.
     *
     * @param bugType
     *            the bug type that should be reported if the stream is not
     *            closed on all paths out of the method
     */
    public Stream setInteresting(String bugType) {
        this.isUninteresting = false;
        this.bugType = bugType;
        return this;
    }

    /**
     * Mark whether or not implicit exception edges should be ignored by
     * ResourceValueAnalysis when determining whether or not stream is closed on
     * all paths out of method.
     */
    public Stream setIgnoreImplicitExceptions(boolean enable) {
        ignoreImplicitExceptions = enable;
        return this;
    }

    /**
     * Mark whether or not Stream is open as soon as it is created, or whether a
     * later method or constructor must explicitly open it.
     */
    public Stream setIsOpenOnCreation(boolean enable) {
        isOpenOnCreation = enable;
        return this;
    }

    /**
     * Set the number of the parameter which passes the stream instance.
     *
     * @param instanceParam
     *            number of the parameter passing the stream instance
     */
    public void setInstanceParam(int instanceParam) {
        this.instanceParam = instanceParam;
    }

    /**
     * Set this Stream has having been closed on all paths out of the method.
     */
    public void setClosed() {
        isClosed = true;
    }

    public String getStreamBase() {
        return streamBase;
    }

    public boolean isUninteresting() {
        return isUninteresting;
    }

    public boolean isOpenOnCreation() {
        return isOpenOnCreation;
    }

    public void setOpenLocation(Location openLocation) {
        this.openLocation = openLocation;
    }

    public Location getOpenLocation() {
        return openLocation;
    }

    public boolean ignoreImplicitExceptions() {
        return ignoreImplicitExceptions;
    }

    public int getInstanceParam() {
        return instanceParam;
    }

    public String getBugType() {
        return bugType;
    }

    /**
     * Return whether or not the Stream is closed on all paths out of the
     * method.
     */
    public boolean isClosed() {
        return isClosed;
    }

    public boolean isStreamOpen(BasicBlock basicBlock, InstructionHandle handle, ConstantPoolGen cpg, ResourceValueFrame frame) {
        if (isOpenOnCreation) {
            return false;
        }

        Instruction ins = handle.getInstruction();
        if (!(ins instanceof INVOKESPECIAL)) {
            return false;
        }

        // Does this instruction open the stream?
        INVOKESPECIAL inv = (INVOKESPECIAL) ins;

        return frame.isValid() && getInstanceValue(frame, inv, cpg).isInstance()
                && matchMethod(inv, cpg, this.getResourceClass(), "<init>");
    }

    public static boolean mightCloseStream(BasicBlock basicBlock, InstructionHandle handle, ConstantPoolGen cpg) {

        Instruction ins = handle.getInstruction();

        if ((ins instanceof INVOKEVIRTUAL) || (ins instanceof INVOKEINTERFACE)) {
            // Does this instruction close the stream?
            InvokeInstruction inv = (InvokeInstruction) ins;

            // It's a close if the invoked class is any subtype of the stream
            // base class.
            // (Basically, we may not see the exact original stream class,
            // even though it's the same instance.)

            return "close".equals(inv.getName(cpg)) && "()V".equals(inv.getSignature(cpg));

        }

        return false;
    }

    public boolean isStreamClose(BasicBlock basicBlock, InstructionHandle handle, ConstantPoolGen cpg, ResourceValueFrame frame,
            RepositoryLookupFailureCallback lookupFailureCallback) {
        if (!mightCloseStream(basicBlock, handle, cpg)) {
            return false;
        }

        Instruction ins = handle.getInstruction();

        if ((ins instanceof INVOKEVIRTUAL) || (ins instanceof INVOKEINTERFACE)) {
            // Does this instruction close the stream?
            InvokeInstruction inv = (InvokeInstruction) ins;

            if (!frame.isValid() || !getInstanceValue(frame, inv, cpg).isInstance()) {
                return false;
            }

            // It's a close if the invoked class is any subtype of the stream
            // base class.
            // (Basically, we may not see the exact original stream class,
            // even though it's the same instance.)
            try {
                String classClosed = inv.getClassName(cpg);

                if (relatedType(classClosed) ) {
                    return true;
                }
                if ("java.io.ObjectOutput".equals(classClosed)) {
                    return relatedType("java.io.ObjectOutputStream");
                } else if ("java.io.ObjectInput".equals(classClosed)) {
                    return relatedType("java.io.ObjectInputStream");
                }
                return false;
            } catch (ClassNotFoundException e) {
                lookupFailureCallback.reportMissingClass(e);
                return false;
            }
        }

        return false;
    }

    private boolean relatedType(String classClosed) throws ClassNotFoundException {
        return Hierarchy.isSubtype(classClosed, streamBase) || Hierarchy.isSubtype(streamBase, classClosed);
    }

    private ResourceValue getInstanceValue(ResourceValueFrame frame, InvokeInstruction inv, ConstantPoolGen cpg) {
        int numConsumed = inv.consumeStack(cpg);
        if (numConsumed == Constants.UNPREDICTABLE) {
            throw new IllegalStateException();
        }
        return frame.getValue(frame.getNumSlots() - numConsumed);
    }

    private boolean matchMethod(InvokeInstruction inv, ConstantPoolGen cpg, String className, String methodName) {
        return inv.getClassName(cpg).equals(className) && inv.getName(cpg).equals(methodName);
    }

    @Override
    public int hashCode() {
        return getLocation().hashCode() + 3 * streamBase.hashCode() + 7 * getResourceClass().hashCode() + 11 * instanceParam;
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Stream)) {
            return false;
        }
        Stream other = (Stream) o;
        if (!getLocation().equals(other.getLocation())) {
            return false;
        }
        if (!streamBase.equals(other.streamBase)) {
            return false;
        }
        if (!getResourceClass().equals(other.getResourceClass())) {
            return false;
        }
        if (instanceParam != other.instanceParam) {
            return false;
        }
        return true;
    }

    @Override
    public int compareTo(Stream other) {
        int cmp;

        // The main idea in comparing streams is that
        // if they can't be differentiated by location
        // and base/stream class, then we should try
        // instanceParam. This allows streams passed in
        // different parameters to be distinguished.

        cmp = getLocation().compareTo(other.getLocation());
        if (cmp != 0) {
            return cmp;
        }
        cmp = streamBase.compareTo(other.streamBase);
        if (cmp != 0) {
            return cmp;
        }
        cmp = getResourceClass().compareTo(other.getResourceClass());
        if (cmp != 0) {
            return cmp;
        }
        cmp = instanceParam - other.instanceParam;
        if (cmp != 0) {
            return cmp;
        }

        return 0;
    }
}

