package serp.bytecode;

import java.io.*;
import java.lang.reflect.*;

import serp.bytecode.lowlevel.*;
import serp.bytecode.visitor.*;
import serp.util.*;

/**
 * An instruction that invokes a method.
 *
 * @author Abe White
 */
public class MethodInstruction extends Instruction {
    private int _index = 0;

    MethodInstruction(Code owner, int opcode) {
        super(owner, opcode);
    }

    int getLength() {
        if (getOpcode() == Constants.INVOKEINTERFACE)
            return super.getLength() + 4;
        if (getOpcode() == Constants.INVOKEDYNAMIC)
            return super.getLength() + 4;
        return super.getLength() + 2;
    }

    public int getLogicalStackChange() {
        String ret = getMethodReturnName();
        if (ret == null)
            return 0;

        // subtract a stack pos for the this ptr
        int stack = 0;
        if (getOpcode() != Constants.INVOKESTATIC)
            stack--;

        // and for each arg
        String[] params = getMethodParamNames();
        for (int i = 0; i < params.length; i++)
            stack--;

        // add for the return value, if any
        if (!void.class.getName().equals(ret))
            stack++;
        return stack;
    }

    public int getStackChange() {
        String ret = getMethodReturnName();
        if (ret == null)
            return 0;

        // subtract a stack pos for the this ptr
        int stack = 0;
        if (getOpcode() != Constants.INVOKESTATIC)
            stack--;

        // and for each arg (2 for longs, doubles)
        String[] params = getMethodParamNames();
        for (int i = 0; i < params.length; i++, stack--)
            if (long.class.getName().equals(params[i]) 
                || double.class.getName().equals(params[i]))
                stack--;

        // add for the return value, if any
        if (!void.class.getName().equals(ret))
            stack++;
        if (long.class.getName().equals(ret) 
            || double.class.getName().equals(ret))
            stack++;
        return stack;
    }

    /////////////////////
    // Method operations
    /////////////////////

    /**
     * Return the index in the class {@link ConstantPool} of the
     * {@link ComplexEntry} describing the method to operate on.
     */
    public int getMethodIndex() {
        return _index;
    }

    /**
     * Set the index in the class {@link ConstantPool} of the
     * {@link ComplexEntry} describing the method to operate on.
     *
     * @return this instruction, for method chaining
     */
    public MethodInstruction setMethodIndex(int index) {
        _index = index;
        return this;
    }

    /**
     * Return the method this instruction operates on, or null if not set.
     */
    public BCMethod getMethod() {
        String dec = getMethodDeclarerName();
        if (dec == null)
            return null;

        BCClass bc = getProject().loadClass(dec, getClassLoader());
        BCMethod[] meths = bc.getMethods(getMethodName(),getMethodParamNames());
        if (meths.length == 0)
            return null;
        return meths[0];
    }

    /**
     * Set the method this instruction operates on.
     *
     * @return this instruction, for method chaining
     */
    public MethodInstruction setMethod(BCMethod method) {
        if (method == null)
            return setMethodIndex(0);
        return setMethod(method.getDeclarer().getName(), method.getName(),
            method.getReturnName(), method.getParamNames(), false);
    }

    /**
     * Set the method this instruction operates on.
     *
     * @return this instruction, for method chaining
     */
    public MethodInstruction setMethod(Method method) {
        if (method == null)
            return setMethodIndex(0);
        return setMethod(method.getDeclaringClass(), method.getName(),
            method.getReturnType(), method.getParameterTypes());
    }

    /**
     * Set the method this instruction operates on.
     *
     * @return this instruction, for method chaining
     */
    public MethodInstruction setMethod(Constructor method) {
        if (method == null)
            return setMethodIndex(0);
        setOpcode(Constants.INVOKESPECIAL);
        return setMethod(method.getDeclaringClass(), "<init>", void.class,
            method.getParameterTypes());
    }

    /**
     * Set the method this instruction operates on.
     *
     * @param dec the full class name of the method's declaring class
     * @param name the method name
     * @param returnType the full class name of the method return type
     * @param param the full class names of the method param types
     * @return this instruction, for method chaining
     */
    public MethodInstruction setMethod(String dec, String name,
        String returnType, String[] params) {
        return setMethod(dec, name, returnType, params, true);
    }

    /**
     * Set the method this instruction operates on.
     *
     * @param dec the full class name of the method's declaring class, or the bootstrap index for InvokeDynamic
     * @param name the method name
     * @param returnType the full class name of the method return type
     * @param param the full class names of the method param types
     * @param copy whether to copy the the parameter array
     * @return this instruction, for method chaining
     */
    private MethodInstruction setMethod(String dec, String name,
        String returnType, String[] params, boolean copy) {
        if (name == null && returnType == null && dec == null 
            && (params == null || params.length == 0))
            return setMethodIndex(0);

        if (dec == null)
            dec = "";
        if (name == null)
            name = "";
        if (returnType == null)
            returnType = "";
        if (params == null)
            params = new String[0];
        else if (copy) {
            String[] pcopy = new String[params.length];
            System.arraycopy(params, 0, pcopy, 0, params.length);
            params = pcopy;
        }

        NameCache cache = getProject().getNameCache();
        returnType = cache.getInternalForm(returnType, true);
        dec = cache.getInternalForm(dec, false);
        for (int i = 0; i < params.length; i++)
            params[i] = cache.getInternalForm(params[i], true);

        String desc = cache.getDescriptor(returnType, params);
        if (getOpcode() == Constants.INVOKEINTERFACE)
            return setMethodIndex(getPool().findInterfaceMethodEntry(dec, name,
                desc, true));
        if (getOpcode() == Constants.INVOKEDYNAMIC) { 
            int bootstrapindex = Integer.parseInt(dec); // Dec represents the bootstrap index
            return setMethodIndex(getPool().findInvokeDynamicEntry(bootstrapindex, name, desc, true));
        }
        return setMethodIndex(getPool().findMethodEntry(dec, name, desc, true));
    }

    /**
     * Set the method this instruction operates on, for methods that are
     * declared by the current class.
     *
     * @param name the method name
     * @param returnType the full class name of the method return type
     * @param param the full class names of the method param types
     * @return this instruction, for method chaining
     */
    public MethodInstruction setMethod(String name, String returnType,
        String[] params) {
        BCClass owner = getCode().getMethod().getDeclarer();
        return setMethod(owner.getName(), name, returnType, params);
    }

    /**
     * Set the method this instruction operates on.
     *
     * @param dec the method's declaring class
     * @param name the method name
     * @param returnType the class of the method return type
     * @param param the class of the method param types
     * @return this instruction, for method chaining
     */
    public MethodInstruction setMethod(Class dec, String name,
        Class returnType, Class[] params) {
        String decName = (dec == null) ? null : dec.getName();
        String returnName = (returnType == null) ? null : returnType.getName();
        String[] paramNames = null;
        if (params != null) {
            paramNames = new String[params.length];
            for (int i = 0; i < params.length; i++)
                paramNames[i] = params[i].getName();
        }
        return setMethod(decName, name, returnName, paramNames, false);
    }

    /**
     * Set the method this instruction operates on, for methods that are
     * declared by the current class.
     *
     * @param name the method name
     * @param returnType the class of the method return type
     * @param param the class of the method param types
     * @return this instruction, for method chaining
     */
    public MethodInstruction setMethod(String name, Class returnType,
        Class[] params) {
        BCClass owner = getCode().getMethod().getDeclarer();
        String returnName = (returnType == null) ? null : returnType.getName();
        String[] paramNames = null;
        if (params != null) {
            paramNames = new String[params.length];
            for (int i = 0; i < params.length; i++)
                paramNames[i] = params[i].getName();
        }
        return setMethod(owner.getName(), name, returnName, paramNames, false);
    }

    /**
     * Set the method this instruction operates on.
     *
     * @param dec the method's declaring class
     * @param name the method name
     * @param returnType the class of the method return type
     * @param param the class of the method param types
     * @return this instruction, for method chaining
     */
    public MethodInstruction setMethod(BCClass dec, String name,
        BCClass returnType, BCClass[] params) {
        String decName = (dec == null) ? null : dec.getName();
        String returnName = (returnType == null) ? null : returnType.getName();
        String[] paramNames = null;
        if (params != null) {
            paramNames = new String[params.length];
            for (int i = 0; i < params.length; i++)
                paramNames[i] = params[i].getName();
        }
        return setMethod(decName, name, returnName, paramNames, false);
    }

    /**
     * Set the method this instruction operates on, for methods that are
     * declared by the current class.
     *
     * @param name the method name
     * @param returnType the class of the method return type
     * @param param the class of the method param types
     * @return this instruction, for method chaining
     */
    public MethodInstruction setMethod(String name, BCClass returnType,
        BCClass[] params) {
        BCClass owner = getCode().getMethod().getDeclarer();
        String returnName = (returnType == null) ? null : returnType.getName();
        String[] paramNames = null;
        if (params != null) {
            paramNames = new String[params.length];
            for (int i = 0; i < params.length; i++)
                paramNames[i] = params[i].getName();
        }
        return setMethod(owner.getName(), name, returnName, paramNames, false);
    }

    /////////////////////////////////////////
    // Name, Return, Param, Owner operations
    /////////////////////////////////////////

    /**
     * Return the name of the method this instruction operates on, or null
     * if not set.
     */
    public String getMethodName() {
        if (_index == 0)
            return null;

        String name = null;
        
        if (getOpcode() == Constants.INVOKEDYNAMIC) {
            InvokeDynamicEntry ide = (InvokeDynamicEntry) getPool().getEntry(_index);
            name = ide.getNameAndTypeEntry().getNameEntry().getValue();
        } else {
            ComplexEntry entry = (ComplexEntry) getPool().getEntry(_index);
            name = entry.getNameAndTypeEntry().getNameEntry().getValue();
        }    
        
        if (name.length() == 0) {
            name = null;
        }
        
        return name;
    }

    /**
     * Set the name of the method this instruction operates on.
     *
     * @return this instruction, for method chaining
     */
    public MethodInstruction setMethodName(String name) {
        return setMethod(getMethodDeclarerName(), name, getMethodReturnName(),
            getMethodParamNames());
    }

    /**
     * Return the return type of the method this instruction operates on,
     * or null if not set.
     */
    public String getMethodReturnName() {
        if (_index == 0)
            return null;
        
        String desc = null;
        if (getOpcode() == Constants.INVOKEDYNAMIC) {
            InvokeDynamicEntry ide = (InvokeDynamicEntry) getPool().getEntry(_index);
            desc = ide.getNameAndTypeEntry().getDescriptorEntry().getValue();        
        } else {
            ComplexEntry entry = (ComplexEntry) getPool().getEntry(_index);
            desc = entry.getNameAndTypeEntry().getDescriptorEntry().getValue();
        }

        NameCache cache = getProject().getNameCache();
        String name = cache.getExternalForm(cache.getDescriptorReturnName(desc), false);        
        if (name.length() == 0)
            return null;
        return name;
    }

    /**
     * Return the return type of the method this instruction operates on,
     * or null if not set.
     */
    public Class getMethodReturnType() {
        String type = getMethodReturnName();
        if (type == null)
            return null;
        return Strings.toClass(type, getClassLoader());
    }

    /**
     * Return the return type of the method this instruction operates on,
     * or null if not set.
     */
    public BCClass getMethodReturnBC() {
        String type = getMethodReturnName();
        if (type == null)
            return null;
        return getProject().loadClass(type, getClassLoader());
    }

    /**
     * Set the return type of the method this instruction operates on.
     *
     * @return this instruction, for method chaining
     */
    public MethodInstruction setMethodReturn(String type) {
        return setMethod(getMethodDeclarerName(), getMethodName(), type,
            getMethodParamNames());
    }

    /**
     * Set the return type of the method this instruction operates on.
     *
     * @return this instruction, for method chaining
     */
    public MethodInstruction setMethodReturn(Class type) {
        String name = null;
        if (type != null)
            name = type.getName();
        return setMethodReturn(name);
    }

    /**
     * Set the return type of the method this instruction operates on.
     *
     * @return this instruction, for method chaining
     */
    public MethodInstruction setMethodReturn(BCClass type) {
        String name = null;
        if (type != null)
            name = type.getName();
        return setMethodReturn(name);
    }

    /**
     * Return the param types of the method this instruction operates on,
     * or empty array if none.
     */
    public String[] getMethodParamNames() {
        if (_index == 0)
            return new String[0];

        String desc = null;
        if (getOpcode() == Constants.INVOKEDYNAMIC) {
            InvokeDynamicEntry ide = (InvokeDynamicEntry) getPool().getEntry(_index);
            desc = ide.getNameAndTypeEntry().getDescriptorEntry().getValue();
        } else {
            ComplexEntry entry = (ComplexEntry) getPool().getEntry(_index);
            desc = entry.getNameAndTypeEntry().getDescriptorEntry().
                getValue();
        }
        
        NameCache cache = getProject().getNameCache();
        String[] names = cache.getDescriptorParamNames(desc);
        for (int i = 0; i < names.length; i++)
            names[i] = cache.getExternalForm(names[i], false);
        return names;
    }

    /**
     * Return the param types of the method this instruction operates on,
     * or empty array if none.
     */
    public Class[] getMethodParamTypes() {
        String[] paramNames = getMethodParamNames();
        Class[] params = new Class[paramNames.length];
        for (int i = 0; i < paramNames.length; i++)
            params[i] = Strings.toClass(paramNames[i], getClassLoader());
        return params;
    }

    /**
     * Return the param types of the method this instruction operates on,
     * or empty array if none.
     */
    public BCClass[] getMethodParamBCs() {
        String[] paramNames = getMethodParamNames();
        BCClass[] params = new BCClass[paramNames.length];
        for (int i = 0; i < paramNames.length; i++)
            params[i] = getProject().loadClass(paramNames[i], getClassLoader());
        return params;
    }

    /**
     * Set the param types of the method this instruction operates on.
     *
     * @return this instruction, for method chaining
     */
    public MethodInstruction setMethodParams(String[] types) {
        return setMethod(getMethodDeclarerName(), getMethodName(),
            getMethodReturnName(), types);
    }

    /**
     * Set the param types of the method this instruction operates on.
     *
     * @return this instruction, for method chaining
     */
    public void setMethodParams(Class[] types) {
        if (types == null)
            setMethodParams((String[]) null);
        else {
            String[] names = new String[types.length];
            for (int i = 0; i < types.length; i++)
                names[i] = types[i].getName();
            setMethodParams(names);
        }
    }

    /**
     * Set the param types of the method this instruction operates on.
     *
     * @return this instruction, for method chaining
     */
    public void setMethodParams(BCClass[] types) {
        if (types == null)
            setMethodParams((String[]) null);
        else {
            String[] names = new String[types.length];
            for (int i = 0; i < types.length; i++)
                names[i] = types[i].getName();
            setMethodParams(names);
        }
    }

    /**
     * Return the declaring type of the method this instruction operates on,
     * or null if not set.
     */
    public String getMethodDeclarerName() {
        if (_index == 0)
            return null;

        String name = null;
        if (getOpcode() == Constants.INVOKEDYNAMIC) {
            // InvokeDynamic doesn't hvae a method declarer, but it does have a bootstrap index.
            InvokeDynamicEntry ide = (InvokeDynamicEntry) getPool().getEntry(_index);
            name = String.valueOf(ide.getBootstrapMethodAttrIndex());
        } else {
            ComplexEntry entry = (ComplexEntry) getPool().getEntry(_index);
            name = getProject().getNameCache().getExternalForm
                (entry.getClassEntry().getNameEntry().getValue(), false);
        }
        
        if (name.length() == 0)
            return null;
        return name;
    }

    /**
     * Return the declaring type of the method this instruction operates on,
     * or null if not set.
     */
    public Class getMethodDeclarerType() {
        String type = getMethodDeclarerName();
        if (type == null)
            return null;
        return Strings.toClass(type, getClassLoader());
    }

    /**
     * Return the declaring type of the method this instruction operates on,
     * or null if not set.
     */
    public BCClass getMethodDeclarerBC() {
        String type = getMethodDeclarerName();
        if (type == null)
            return null;
        return getProject().loadClass(type, getClassLoader());
    }

    /**
     * Set the declaring type of the method this instruction operates on.
     *
     * @return this instruction, for method chaining
     */
    public MethodInstruction setMethodDeclarer(String type) {
        return setMethod(type, getMethodName(), getMethodReturnName(),
            getMethodParamNames());
    }

    /**
     * Set the declaring type of the method this instruction operates on.
     *
     * @return this instruction, for method chaining
     */
    public MethodInstruction setMethodDeclarer(Class type) {
        String name = null;
        if (type != null)
            name = type.getName();
        return setMethodDeclarer(name);
    }

    /**
     * Set the declaring type of the method this instruction operates on.
     *
     * @return this instruction, for method chaining
     */
    public MethodInstruction setMethodDeclarer(BCClass type) {
        String name = null;
        if (type != null)
            name = type.getName();
        return setMethodDeclarer(name);
    }

    /**
     * MethodInstructions are equal if the method they reference is the same,
     * or if the method of either is unset.
     */
    public boolean equalsInstruction(Instruction other) {
        if (other == this)
            return true;
        if (!(other instanceof MethodInstruction))
            return false;
        if (!super.equalsInstruction(other))
            return false;

        MethodInstruction ins = (MethodInstruction) other;
        String s1 = getMethodName();
        String s2 = ins.getMethodName();
        if (!(s1 == null || s2 == null || s1.equals(s2)))
            return false;

        s1 = getMethodReturnName();
        s2 = ins.getMethodReturnName();
        if (!(s1 == null || s2 == null || s1.equals(s2)))
            return false;

        s1 = getMethodDeclarerName();
        s2 = ins.getMethodDeclarerName();
        if (!(s1 == null || s2 == null || s1.equals(s2)))
            return false;

        String[] p1 = getMethodParamNames();
        String[] p2 = ins.getMethodParamNames();
        if (!(p1.length == 0 || p2.length == 0 || p1.length == p2.length))
            return false;

        for (int i = 0; i < p1.length; i++)
            if (!(p1[i] == null || p2[i] == null || p1[i].equals(p2[i])))
                return false;
        return true;
    }

    public void acceptVisit(BCVisitor visit) {
        visit.enterMethodInstruction(this);
        visit.exitMethodInstruction(this);
    }

    void read(Instruction orig) {
        super.read(orig);
        MethodInstruction ins = (MethodInstruction) orig;
        setMethod(ins.getMethodDeclarerName(), ins.getMethodName(),
            ins.getMethodReturnName(), ins.getMethodParamNames());
    }

    void read(DataInput in) throws IOException {
        super.read(in);
        _index = in.readUnsignedShort();
        if (getOpcode() == Constants.INVOKEINTERFACE || getOpcode() == Constants.INVOKEDYNAMIC) {
            in.readByte();
            in.readByte();
        }
    }

    void write(DataOutput out) throws IOException {
        super.write(out);
        out.writeShort(_index);
        if (getOpcode() == Constants.INVOKEINTERFACE) {
            String[] args = getMethodParamNames();
            int count = 1;
            for (int i = 0; i < args.length; i++, count++)
                if (long.class.getName().equals(args[i]) 
                    || double.class.getName().equals(args[i]))
                    count++;

            out.writeByte(count);
            out.writeByte(0);
        } else if (getOpcode() == Constants.INVOKEDYNAMIC) {
        	out.writeByte(0);
        	out.writeByte(0);
        }
    }
}
