/*
* JBoss, Home of Professional Open Source
* Copyright 2008-10 Red Hat and individual contributors
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This 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 software 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 software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*
* @authors Andrew Dinn
*/
package org.jboss.byteman.rule.expression;

import org.jboss.byteman.rule.Rule;
import org.jboss.byteman.rule.compiler.CompileContext;
import org.jboss.byteman.rule.exception.CompileException;
import org.jboss.byteman.rule.exception.ExecuteException;
import org.jboss.byteman.rule.exception.TypeException;
import org.jboss.byteman.rule.grammar.ParseNode;
import org.jboss.byteman.rule.helper.HelperAdapter;
import org.jboss.byteman.rule.type.Type;
import org.jboss.byteman.rule.type.TypeGroup;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import java.io.StringWriter;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * Expression which implements a new operation.
 */
public class NewExpression extends Expression
{
    private String typeName;
    private List<Expression> arguments;
    private List<Expression> arrayDims;
    private ArrayInitExpression arrayInits;
    private List<Type> argumentTypes;
    private List<Type> paramTypes;
    private Constructor constructor;
    // if the new value is an array it will have this many dimensions
    private int arrayDimCount;
    // if the new value is an array this many of its dimensions are specified and are to be instantiated
    private int arrayDimDefinedCount;
    // if hte new value has an array initializer list then this tracks how many entries it has
    private int arrayInitsCount;

    public NewExpression(Rule rule, ParseNode token, List<Expression> arguments,
                         List<Expression> arraySizes, ArrayInitExpression arrayInits) {
        super(rule, Type.UNDEFINED, token);
        this.typeName = token.getText();
        this.arguments = arguments;
        this.arrayDims = arraySizes;
        this.arrayInits = arrayInits;
        this.arrayDimCount = arraySizes.size();
        // we check this at bind time and throw a TypeError if it is invalid
        this.arrayDimDefinedCount = 0;
        this.argumentTypes = null;
        this.constructor = null;
    }
    /**
     * verify that variables mentioned in this expression are actually available in the supplied
     * bindings list
     *
     * @throws TypeException if any variable is missing or has the wrong type
     */
    public void bind() throws TypeException
    {
        // check that the recipient and argument expressions have valid bindings

        Iterator<Expression> iterator = arguments.iterator();

        while (iterator.hasNext()) {
            iterator.next().bind();
        }

        // repeat for the array size expressions

        iterator = arrayDims.iterator();

        while (iterator.hasNext()) {
            Expression expr = iterator.next();
            if (expr !=  null)  {
                expr.bind();
                arrayDimDefinedCount++;
            }
        }

        // bind the array init expression if we have one

        if (arrayInits != null) {
            arrayInits.bind();
        }
    }

    /**
     * ensure that all type references in the expression and its component expressions
     * can be resolved, that the type of the expression is well-defined and that it is
     * compatible with the type expected in the context in which it occurs.
     *
     * @param expected  the type expected for the expression in the contxt in which it occurs. this
     *                  may be void but shoudl not be undefined at the point where type checking is performed.
     * @return the expression type
     * @throws org.jboss.byteman.rule.exception.TypeException if type checking fails
     *
     */
    public Type typeCheck(Type expected) throws TypeException {
        // check the new instance type is defined and then look for a relevant constructor

        TypeGroup typeGroup = getTypeGroup();

        type = Type.dereference(typeGroup.create(typeName));

        if (type == null || type.isUndefined()) {
            throw new TypeException("NewExpression.typeCheck : unknown type " + typeName + getPos());
        }

        if (type.isObject() && arrayDimCount == 0) {
            // we need to look for a suitable constructor
            Class clazz = type.getTargetClass();
            // if we can find a unique method then we can use it to type the parameters
            // otherwise we do it the hard way
            int arity = arguments.size();
            Constructor[] constructors = clazz.getConstructors();
            List<Constructor> candidates = new ArrayList<Constructor>();
            boolean duplicates = false;

            for (Constructor constructor : constructors) {
                if (constructor.getParameterTypes().length == arity) {
                    candidates.add(constructor);
                }
            }

            argumentTypes = new ArrayList<Type>();

            // check each argument in turn -- if all candidates have the same argument type then
            // use that as the type to check against
            for (int i = 0; i < arguments.size() ; i++) {
                if (candidates.isEmpty()) {
                    throw new TypeException("NewExpression.typeCheck : invalid constructor for target class " + typeName + getPos());
                }

                // TODO get and prune operations do not allow for coercion but type check does!
                // e.g. the parameter type may be int and the arg type float
                // or the parameter type may be String and the arg type class Foo
                // reimplement this using type inter-assignability to do the pruning

                Class candidateClass = getCandidateArgClass(candidates, i);
                Type candidateType;
                if (candidateClass != null) {
                    candidateType = typeGroup.ensureType(candidateClass);
                } else {
                    candidateType = Type.UNDEFINED;
                }
                Type argType = arguments.get(i).typeCheck(candidateType);
                argumentTypes.add(argType);
                if (candidateType == Type.UNDEFINED) {
                    // we had several constructors to choose from
                    candidates = pruneCandidates(candidates, i, argType.getTargetClass());
                }
            }

            if (candidates.isEmpty()) {
                throw new TypeException("NewExpression.typeCheck : invalid constructor for target class " + typeName + getPos());
            }

            if (candidates.size() > 1) {
                throw new TypeException("NewExpression.typeCheck : ambiguous constructor signature for target class " + typeName + getPos());
            }

            constructor = candidates.get(0);

            // make sure we know the formal parameter types and have included them in the typegroup

            paramTypes = new ArrayList<Type>();
            Class<?>[] paramClasses = constructor.getParameterTypes();

            for (int i = 0; i < arguments.size() ; i++) {
                paramTypes.add(typeGroup.ensureType(paramClasses[i]));
            }
        } else if (arrayDimCount == 0) {
            // if we have a primitive type then have to have some array dimensions
            throw new TypeException("NewExpression.typeCheck : invalid type for new operation " + getPos());
        }
        // if this is a new array operation without an initializer then we must have at least one defined
        // dimension

        if (arrayDimCount > 0 && arrayDimDefinedCount == 0 && arrayInits == null) {
            throw new TypeException("NewExpression.typeCheck : array dimension missing " + getPos());
        }

        // we cannot have more dimensions than we can fit into a byte

        if (arrayDimCount > Byte.MAX_VALUE) {
            throw new TypeException("NewExpression.typeCheck : too many array dimensions " + getPos());
        }

        // if we have an array initializer then we must have some dimensions
        // but cannot have any specified dimensions

        if (arrayInits != null) {
            if (arrayDimCount == 0)
            {
                throw new TypeException("NewExpression.typeCheck : cannot specify initializer for non array" + getPos());
            }

            if (arrayDimDefinedCount != 0)
            {
                throw new TypeException("NewExpression.typeCheck : cannot specify both array dimensions and initializer " + getPos());
            }
        }
        // if we have any array dimension sizings then ensure they all type check as integer expressions

        for (int i = 0; i < arrayDimCount ; i++) {
            if (i < arrayDimDefinedCount) {
                Expression expr = arrayDims.get(i);
                expr.typeCheck(Type.I);
            }
            // replace the current type with the corresponding array type
            type = typeGroup.createArray(type);
        }

        // if we have been passed an ArrayInitExpression then type check it against our result type

        if (arrayInits != null) {
            arrayInits.typeCheck(type);
        }

        // if the expected type is defined then ensure we can assign this type to it

        if (Type.dereference(expected).isDefined() && !expected.isAssignableFrom(type)) {
            throw new TypeException("NewExpression.typeCheck : invalid expected result type " + expected.getName() + getPos());
        }

        return type;
    }

    public Class getCandidateArgClass(List<Constructor> candidates, int argIdx)
    {
        Class argClazz = null;

        for (Constructor c : candidates) {
            Class nextClazz = c.getParameterTypes()[argIdx];
            if (argClazz == null) {
                argClazz = nextClazz;
            } else if (argClazz != nextClazz) {
                return null;
            }
        }

        return argClazz;
    }

    public List<Constructor> pruneCandidates(List<Constructor> candidates, int argIdx, Class argClazz)
    {
        for (int i = 0; i < candidates.size();) {
            Constructor c = candidates.get(i);
            Class nextClazz = c.getParameterTypes()[argIdx];
            if (nextClazz == argClazz || nextClazz.isAssignableFrom(argClazz)) {
                i++;
            } else {
                candidates.remove(i);
            }
        }
        return candidates;
    }

    /**
     * evaluate the expression by interpreting the expression tree
     *
     * @param helper an execution context associated with the rule whcih contains a map of
     *               current bindings for rule variables and another map of their declared types both of which
     *               are indexed by varoable name. This includes entries for the helper (name "-1"), the
     *               recipient if the trigger method is not static (name "0") and the trigger method arguments
     *               (names "1", ...)
     * @return the result of evaluation as an Object
     * @throws org.jboss.byteman.rule.exception.ExecuteException if an error occurs during execution
     *
     */
    public Object interpret(HelperAdapter helper) throws ExecuteException {
        if (arrayDimCount == 0) {
            int l = arguments.size();
            int i;
            Object[] callArgs = new Object[l];
            for (i = 0; i < l; i++) {
                callArgs[i] = arguments.get(i).interpret(helper);
            }
            try {
                Object result = constructor.newInstance(callArgs);
                return result;
            } catch (InstantiationException e) {
                throw new ExecuteException("NewExpression.interpret : unable to instantiate class " + typeName + getPos(), e);
            } catch (IllegalAccessException e) {
                throw new ExecuteException("NewExpression.interpret : unable to access class " + typeName + getPos(), e);
            } catch (InvocationTargetException e) {
                throw new ExecuteException("NewExpression.interpret : unable to invoke constructor for class " + typeName + getPos(), e);
            }
        } else if (arrayInits != null) {
            Object result = arrayInits.interpret(helper);
            return result;
        } else {
            int[] dims = new int[arrayDimDefinedCount];
            Type componentType = type;
            for (int i = 0; i < arrayDimDefinedCount; i++) {
                Expression dim = arrayDims.get(i);
                int dimValue = (Integer)dim.interpret(helper);
                dims[i] = dimValue;
                componentType = componentType.getBaseType();
            }
            try {
                Object result = Array.newInstance(componentType.getTargetClass(), dims);
                return result;
            } catch (IllegalArgumentException e) {
                throw new ExecuteException("NewExpression.interpret : unable to instantiate array " + typeName + getPos(), e);
            } catch (NegativeArraySizeException e) {
                // should never happen
                throw new ExecuteException("NewExpression.interpret : unable to instantiate array " + typeName + getPos(), e);
            }
        }
    }

    public void compile(MethodVisitor mv, CompileContext compileContext) throws CompileException
    {
        // make sure we are at the right source line
        compileContext.notifySourceLine(line);

        int currentStack = compileContext.getStackCount();
        int expected = 1;
        int extraParams = 0;

        if (arrayDimCount ==  0) {
            // ok, we need to create the new instance and then initialise it.

            // create the new instance -- adds 1 to stack
            String instantiatedClassName = type.getInternalName();
            mv.visitTypeInsn(Opcodes.NEW, instantiatedClassName);
            compileContext.addStackCount(1);
            // copy the exception so we can init it
            mv.visitInsn(Opcodes.DUP);
            compileContext.addStackCount(1);

            int argCount = arguments.size();

            // stack each of the arguments to the constructor
            for (int i = 0; i < argCount; i++) {
                Type argType = argumentTypes.get(i);
                Type paramType = paramTypes.get(i);
                int paramCount = (paramType.getNBytes() > 4 ? 2 : 1);

                // track extra storage used after type conversion
                extraParams += (paramCount);
                arguments.get(i).compile(mv, compileContext);
                compileContext.compileTypeConversion(argType, paramType);
            }

            // construct the exception
            mv.visitMethodInsn(Opcodes.INVOKESPECIAL, instantiatedClassName, "<init>", getDescriptor());

            // modify the stack height to account for the removed exception and params
            compileContext.addStackCount(-(extraParams + 1));
        } else if (arrayInits != null) {
            // use the initializer to create the array
            arrayInits.compile(mv, compileContext);
        } else {
            if (arrayDimCount == 1) {
                // we can use a NEWARRAY or ANEWARRAY
                Type baseType = type.getBaseType();
                // compile first array dimension adds 1 to stack
                arrayDims.get(0).compile(mv, compileContext);
                // compile new array op -- pops 1 and adds 1 to stack
                if (baseType.isObject()) {
                    mv.visitTypeInsn(Opcodes.ANEWARRAY, baseType.getInternalName());
                // } else if (baseType.isArray()) {  // cannot happen!!!
                } else {
                    int operand = 0;
                    if (baseType.equals(Type.Z)) {
                        operand = Opcodes.T_BOOLEAN;
                    } else if (baseType.equals(Type.B)) {
                        operand = Opcodes.T_BYTE;
                    } else if (baseType.equals(Type.S)) {
                        operand = Opcodes.T_SHORT;
                    } else if (baseType.equals(Type.C)) {
                        operand = Opcodes.T_CHAR;
                    } else if (baseType.equals(Type.I)) {
                        operand = Opcodes.T_INT;
                    } else if (baseType.equals(Type.J)) {
                        operand = Opcodes.T_LONG;
                    } else if (baseType.equals(Type.F)) {
                        operand = Opcodes.T_FLOAT;
                    } else if (baseType.equals(Type.D)) {
                        operand = Opcodes.T_DOUBLE;
                    }
                    mv.visitIntInsn(Opcodes.NEWARRAY, operand);
                }
            } else {
                // we need to use MULTIANEWARRAY

                for (int i = 0; i < arrayDimDefinedCount; i++) {
                    // compile next array dimension adds 1 to stack
                    arrayDims.get(i).compile(mv, compileContext);
                }
                // execute the MULTIANEWARRAY operation -- pops arrayDims operands and pushes 1
                mv.visitMultiANewArrayInsn(type.getInternalName(), arrayDimDefinedCount);
                compileContext.addStackCount(1 - arrayDimDefinedCount);
            }
        }

        if (compileContext.getStackCount() != currentStack + expected) {
            throw new CompileException("NewExpression.compile : invalid stack height " + compileContext.getStackCount() + " expecting " + (currentStack + expected));
        }
    }

    private String getDescriptor()
    {
        StringBuffer buffer = new StringBuffer();
        buffer.append("(");
        int nParams = paramTypes.size();
        for (int i = 0; i < nParams; i++) {
            buffer.append(paramTypes.get(i).getInternalName(true, true));
        }
        buffer.append(")V");
        return buffer.toString();
    }

    public void writeTo(StringWriter stringWriter) {
        stringWriter.write("new ");
        if (type == null || Type.UNDEFINED == type) {
            stringWriter.write(typeName);
        } else {
            stringWriter.write(type.getName());
        }
        String separator = "";
        if (arrayDimCount == 0) {
            stringWriter.write("(");
            for (Expression argument : arguments) {
                stringWriter.write(separator);
                argument.writeTo(stringWriter);
                separator = ",";
            }
            stringWriter.write(")");
        } else {
            for (int i = 0; i < arrayDimCount ; i++) {
                stringWriter.write("[]");
            }
            if (arrayInits != null) {
                arrayInits.writeTo(stringWriter);
            }
        }
    }
}
