/*
* 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.compiler.CompileContext;
import org.jboss.byteman.rule.type.Type;
import org.jboss.byteman.rule.exception.TypeException;
import org.jboss.byteman.rule.exception.ExecuteException;
import org.jboss.byteman.rule.exception.EarlyReturnException;
import org.jboss.byteman.rule.exception.CompileException;
import org.jboss.byteman.rule.Rule;
import org.jboss.byteman.rule.helper.HelperAdapter;
import org.jboss.byteman.rule.grammar.ParseNode;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import java.io.StringWriter;

/**
 *  A return expression which is used in a rule action to cause a return from the rule trigger
 * method, supplying a return value where appropriate.
 */

public class ReturnExpression extends Expression
{
    private Expression returnValue;

    public ReturnExpression(Rule rule, ParseNode token, Expression returnValue)
    {
        // the trigger method may return any old type but the return expression can only occur
        // at the top level in a rule action seuqence so it is actually a VOID expression

        super(rule, Type.VOID, token);

        this.returnValue = returnValue;
    }
    /**
     * verify that variables mentioned in this expression are actually available in the supplied
     * bindings list and infer/validate the type of this expression or its subexpressions
     * where possible
     *
     * @throws TypeException if any variable is missing or has the wrong type
     */
    public void bind() throws TypeException
    {
        if (returnValue != null) {
            // ensure the return value expression has all its bindings
            returnValue.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 should 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 {
        // we need to check the returnValue expression against the type of the trigger method
        type = rule.getReturnType();
        if (returnValue == null && !type.isVoid()) {
            throw new TypeException("ReturnExpression.typeCheck : return expression must supply argument when triggered from method with return type " + type.getName() + getPos());
        } else if (returnValue != null) {
            if (type.isVoid()) {
                throw new TypeException("ReturnExpression.typeCheck : return expression must not supply argument when triggered from void method" + getPos());
            }
            returnValue.typeCheck(type);
        }
        return type;
    }

    /**
     * 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
    {
        // time to take an early bath -- the code compield into the trigger method should
        // catch this and return as appropriate
        if (returnValue != null) {
            Object value = returnValue.interpret(helper);
            Type subtype = returnValue.type;
            if (type.isNumeric()) {
                // make sure we produce the expected type of numeric
                if (type == Type.C && subtype != Type.C) {
                    // ok, transform Number to a Character
                    int number = ((Number)value).intValue();
                    value = Character.valueOf((char)number);
                } else if (subtype == Type.C) {
                    // ok, transform Character to a boxed Numeric if necessary
                    char c = ((Character)value).charValue();
                    if (type == Type.B) {
                        value = Byte.valueOf((byte)c);
                    } else if (type == Type.S) {
                        value = Short.valueOf((short)c);
                    } else if (type == Type.I) {
                        value = Integer.valueOf((int)c);
                    } else if (type == Type.J) {
                        value = Long.valueOf((int)c);
                    } else if (type == Type.F) {
                        value = Float.valueOf((int)c);
                    } else if (type == Type.D) {
                        value = Double.valueOf((int)c);
                    }
                } else {
                    if (type == Type.B && subtype != Type.B) {
                        Number number = (Number)value;
                        value = Byte.valueOf(number.byteValue());
                    } else if (type == Type.S && subtype != Type.S) {
                        Number number = (Number)value;
                        value = Short.valueOf(number.shortValue());
                    } else if (type == Type.I && subtype != Type.I) {
                        Number number = (Number)value;
                        value = Integer.valueOf(number.intValue());
                    } else if (type == Type.J && subtype != Type.J) {
                        Number number = (Number)value;
                        value = Long.valueOf(number.longValue());
                    } else if (type == Type.F && subtype != Type.F) {
                        Number number = (Number)value;
                        value = Float.valueOf(number.floatValue());
                    } else if (type == Type.D && subtype != Type.D) {
                        Number number = (Number)value;
                        value = Double.valueOf(number.doubleValue());
                    }
                }
            }
            throw new EarlyReturnException("return from " + helper.getName(), value);
        } else {
            throw new EarlyReturnException("return from " + helper.getName());
        }
    }

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

        Type valueType = (returnValue == null ? Type.VOID : returnValue.getType());
        int currentStack = compileContext.getStackCount();
        int expected = 1;
        // if value type is inaccessible then treat it as an Object
        if (rule.requiresAccess(valueType)) {
            valueType = Type.OBJECT;
        }

        // ok, we need to create the EarlyReturnException instance and then
        // initialise it using the appropriate return value or null if no
        // return value is needed. strictly we should maybe delay the
        // new until after computing the return expression so we avoid a new
        // if the expression throws an error. but that means we end up doing
        // stack manipulations so lets do it the easy way.

        // create am EarlyReturnException -- adds 1 to stack
        String exceptionClassName = Type.internalName(EarlyReturnException.class);
        mv.visitTypeInsn(Opcodes.NEW, exceptionClassName);
        compileContext.addStackCount(1);
        // copy the exception so we can initialise it -- adds 1 to stack
        mv.visitInsn(Opcodes.DUP);
        compileContext.addStackCount(1);
        // stack a string constant to initialise the exception with -- adds 1 to stack
        mv.visitLdcInsn("return from " + rule.getName());
        compileContext.addStackCount(1);
        // stack any required return value or null -- adds 1 to stack but may use 2 slots
        if (returnValue != null) {
            returnValue.compile(mv, compileContext);
            // we may need to convert from the value type to the return type
            if (valueType != type) {
                compileContext.compileTypeConversion(valueType, type);
            }
            if (type.isPrimitive()) {
                // we need an object not a primitive
                compileContext.compileBox(Type.boxType(type));
            }
        } else {
            // just push null
            mv.visitInsn(Opcodes.ACONST_NULL);
            compileContext.addStackCount(1);
        }
        // construct the exception -- pops 3
        mv.visitMethodInsn(Opcodes.INVOKESPECIAL, exceptionClassName, "<init>", "(Ljava/lang/String;Ljava/lang/Object;)V");
        compileContext.addStackCount(-3);

        // check current stack and increment max stack if necessary
        if (compileContext.getStackCount() != currentStack + expected) {
            throw new CompileException("ReturnExpression.compile : invalid stack height " + compileContext.getStackCount() + " expecting " + (currentStack + expected));
        }

        // now insert the throw instruction and decrement the stack height accordingly
        
        mv.visitInsn(Opcodes.ATHROW);
        compileContext.addStackCount(-1);
    }

    public void writeTo(StringWriter stringWriter) {
        if (returnValue != null) {
            stringWriter.write("RETURN ");
            returnValue.writeTo(stringWriter);
        } else {
            stringWriter.write("RETURN");
        }
    }
}
