/*
* 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.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.util.List;
import java.util.Iterator;
import java.io.StringWriter;
import java.lang.reflect.Array;

/**
 * an expression which identifies an array reference.
 */

public class ArrayExpression extends AssignableExpression
{

    public ArrayExpression(Rule rule, Type type, ParseNode token, Expression arrayRef, List<Expression> idxList)
    {
        super(rule, type, token);
        this.arrayRef = arrayRef;
        this.idxList = idxList;
    }

    /**
     * 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
    {
        // we  have to make sure that any names occuring in the array reference are bound
        // and that the index expressions contain valid bindings
        arrayRef.bind();

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

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

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

        return type;
    }

    public Type typeCheckAssign(Type expected) throws TypeException
    {
        typeCheckAny();
        if (Type.dereference(expected).isDefined() && !type.isAssignableFrom(expected)) {
            throw new TypeException("ArrayExpression.typeCheckAssign : invalid value type " + expected.getName() + " for array assignment " + getPos());
        }

        return type;
    }

    private void typeCheckAny() throws TypeException
    {
        Type arrayType = arrayRef.typeCheck(Type.UNDEFINED);
        Type nextType = arrayType;
        for (Expression expr : idxList) {
            if (!nextType.isArray()) {
                throw new TypeException("ArrayExpression.typeCheck : invalid type for array dereference " + nextType.getName() + getPos());
            }
            nextType = nextType.getBaseType();
            expr.typeCheck(Type.N);
        }
        type = nextType;
    }

    public Object interpret(HelperAdapter helper) throws ExecuteException {
        // evaluate the array expression then evaluate each index expression in turn and
        // dereference to access the array element

        try {
            Object value = arrayRef.interpret(helper);
            Type nextType = arrayRef.getType();
            for (Expression expr : idxList) {
                int idx = ((Number) expr.interpret(helper)).intValue();
                if (value == null) {
                    throw new ExecuteException("ArrayExpression.interpret : attempted array indirection through null value " + arrayRef.token.getText() + getPos());
                }
                value = Array.get(value, idx);
                nextType = nextType.getBaseType();
            }

            return value;
        } catch (ExecuteException e) {
            throw e;
        } catch (IllegalArgumentException e) {
            throw new ExecuteException("ArrayExpression.interpret : failed to evaluate expression " + arrayRef.token.getText() + getPos(), e);
        } catch (ArrayIndexOutOfBoundsException e) {
            throw new ExecuteException("ArrayExpression.interpret : invalid index for array " + arrayRef.token.getText() + getPos(), e);
        } catch (ClassCastException e) {
            throw new ExecuteException("ArrayExpression.interpret : invalid index dereferencing array " + arrayRef.token.getText() + getPos(), e);
        } catch (Exception e) {
            throw new ExecuteException("ArrayExpression.interpret : unexpected exception dereferencing array " + arrayRef.token.getText() + getPos(), e);
        }
    }

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

        Type valueType = arrayRef.getType().getBaseType();
        int currentStack = compileContext.getStackCount();
        int expected = 0;

        // compile load of array reference -- adds 1 to stack height
        arrayRef.compile(mv, compileContext);
        // for each index expression compile the expression and the do an array load
        Iterator<Expression> iterator = idxList.iterator();

        while (iterator.hasNext()) {
            Expression idxExpr = iterator.next();
            // compile expression index -- adds 1 to height
            idxExpr.compile(mv, compileContext);
            // make sure the index is an integer
            compileContext.compileTypeConversion(idxExpr.getType(), Type.I);

            if (valueType.isObject() || valueType.isArray()) {
                // compile load object - pops 2 and adds 1
                mv.visitInsn(Opcodes.AALOAD);
                expected = 1;
            } else if (valueType == Type.Z || valueType == Type.B) {
                // compile load byte - pops 2 and adds 1
                mv.visitInsn(Opcodes.BALOAD);
                expected = 1;
            } else if (valueType == Type.S) {
                // compile load short - pops 2 and adds 1
                mv.visitInsn(Opcodes.SALOAD);
                expected = 1;
            } else if (valueType == Type.C) {
                // compile load char - pops 2 and adds 1
                mv.visitInsn(Opcodes.CALOAD);
                expected = 1;
            } else if (valueType == Type.I) {
                // compile load int - pops 2 and adds 1
                mv.visitInsn(Opcodes.IALOAD);
                expected = 1;
            } else if (valueType == Type.J) {
                // compile load long - pops 2 and adds 2
                mv.visitInsn(Opcodes.LALOAD);
                expected = 2;
            } else if (valueType == Type.F) {
                // compile load float - pops 2 and adds 1
                mv.visitInsn(Opcodes.FALOAD);
                expected = 1;
            } else if (valueType == Type.D) {
                // compile load double - pops 2 and adds 2
                mv.visitInsn(Opcodes.DALOAD);
                expected = 2;
            }
            compileContext.addStackCount(expected - 2);
            if (iterator.hasNext()) {
                assert valueType.isArray();
                valueType =valueType.getBaseType();
            }
        }

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

        // we needed room for an aray and an index or for a one or two word result
        // but the recursive evaluations will have made sure the max stack is big enough
        // so there is no need to update the maximum stack height
    }

    public void writeTo(StringWriter stringWriter) {
        arrayRef.writeTo(stringWriter);
        for (Expression expr : idxList) {
            stringWriter.write("[");
            expr.writeTo(stringWriter);
            stringWriter.write("]");
        }
    }

    Expression arrayRef;
    List<Expression> idxList;

    @Override
    public Object interpretAssign(HelperAdapter helper, Object value) throws ExecuteException {
        // evaluate the array expression then evaluate each index expression in turn up to the last one
        // and dereference to access the array at that index. finally evaluate the final index expression
        // and use it as the position at which to install the supplied value in the dereferenced array

        try {
            Object array = arrayRef.interpret(helper);
            Type nextType = arrayRef.getType();
            int count = idxList.size() - 1;
            for (Expression expr : idxList) {
                int idx = ((Number) expr.interpret(helper)).intValue();
                if (array == null) {
                    throw new ExecuteException("ArrayExpression.interpret : attempted array indirection through null value " + arrayRef.token.getText() + getPos());
                }
                if (count-- >  0)  {
                    array = Array.get(array, idx);
                    nextType = nextType.getBaseType();
                } else {
                    Array.set(array, idx, value);
                }
            }
            return value;
        } catch (ExecuteException e) {
            throw e;
        } catch (IllegalArgumentException e) {
            throw new ExecuteException("ArrayExpression.interpret : failed to evaluate expression " + arrayRef.token.getText() + getPos(), e);
        } catch (ArrayIndexOutOfBoundsException e) {
            throw new ExecuteException("ArrayExpression.interpret : invalid index for array " + arrayRef.token.getText() + getPos(), e);
        } catch (ClassCastException e) {
            throw new ExecuteException("ArrayExpression.interpret : invalid index dereferencing array " + arrayRef.token.getText() + getPos(), e);
        } catch (Exception e) {
            throw new ExecuteException("ArrayExpression.interpret : unexpected exception dereferencing array " + arrayRef.token.getText() + getPos(), e);
        }
    }

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

        Type valueType = arrayRef.getType().getBaseType();
        int currentStack = compileContext.getStackCount();
        boolean isTwoWords = (valueType.getNBytes() > 4);
        int toPop = 0;
        int size = (isTwoWords ? 2 : 1);

        // value to be assigned is TOS and will already be coerced to the correct value type
        // copy it so we can install the copy and leave the original as a a return value on the stack
        if (isTwoWords) {
            // [... val1 val2 ==> ... val1 val2 val1 val2]
            mv.visitInsn(Opcodes.DUP2);
        } else {
            // [... val ==> ... val val]
            mv.visitInsn(Opcodes.DUP);
        }
        compileContext.addStackCount(size);

        // compile load of array reference -- adds 1 to stack height
        arrayRef.compile(mv, compileContext);
        // for each index expression compile the expression and the do an array load
        Iterator<Expression> iterator = idxList.iterator();

        while (iterator.hasNext()) {
            Expression idxExpr = iterator.next();
            if (iterator.hasNext()) {
                // dereference the array to get an embedded array
                // compile expression index -- adds 1 to height
                idxExpr.compile(mv, compileContext);
                // make sure the index is an integer
                compileContext.compileTypeConversion(idxExpr.getType(), Type.I);
                // fetch embedded array pop 2 and add 1
                mv.visitInsn(Opcodes.AALOAD);
                compileContext.addStackCount(-1);
                valueType = valueType.getBaseType();
            } else {
                if (isTwoWords) {
                    // stack is [..., val1, val2, val1, val2, aref ] and we want [..., val1, val2, aref, val1, val2 ]
                    mv.visitInsn(Opcodes.DUP_X2);     // ==>  [..., val1, val2, aref. val1, val2, aref ]
                    compileContext.addStackCount(1);
                    mv.visitInsn(Opcodes.POP);        // ==> [..., val1, val2, aref. val1, val2 ]
                    compileContext.addStackCount(-1);
                } else {
                    // stack is [..., val, val, aref ] and we want [..., val, aref, val ]
                    mv.visitInsn(Opcodes.SWAP);
                }
                // compile expression index -- adds 1 to height
                idxExpr.compile(mv, compileContext);
                // make sure the index is an integer
                compileContext.compileTypeConversion(idxExpr.getType(), Type.I);
                if (isTwoWords) {
                    // stack is [..., val1, val2, aref, val1, val2, idx] and we want [..., val1, val2, aref, idx, val1, val2 ]
                    mv.visitInsn(Opcodes.DUP_X2);     // ==> [..., val1, val2, aref, idx, val1, val2, idx]
                    compileContext.addStackCount(1);
                    mv.visitInsn(Opcodes.POP);        // ==> [..., val1, val2, aref, idx, val1, val2 ]
                    compileContext.addStackCount(-1);
                } else {
                    // stack is [..., val, aref, val, idx] and we want [..., val, aref, idx, val ]
                    mv.visitInsn(Opcodes.SWAP);
                }
                // now we can do the array store
                if (valueType.isObject() || valueType.isArray()) {
                    // compile load object - pops 3
                    mv.visitInsn(Opcodes.AASTORE);
                    toPop =- 3;
                } else if (valueType == Type.Z || valueType == Type.B) {
                    // compile load byte - pops 3
                    mv.visitInsn(Opcodes.BASTORE);
                    toPop = -3;
                } else if (valueType == Type.S) {
                    // compile load short - pops 3
                    mv.visitInsn(Opcodes.SASTORE);
                    toPop = -3;
                } else if (valueType == Type.C) {
                    // compile load char - pops 3
                    mv.visitInsn(Opcodes.CASTORE);
                    toPop = -3;
                } else if (valueType == Type.I) {
                    // compile load int - pops 3
                    mv.visitInsn(Opcodes.IASTORE);
                    toPop = -3;
                } else if (valueType == Type.J) {
                    // compile load long - pops 4
                    mv.visitInsn(Opcodes.LASTORE);
                    toPop = -4;
                } else if (valueType == Type.F) {
                    // compile load float - pops 3
                    mv.visitInsn(Opcodes.FASTORE);
                    toPop = -3;
                } else if (valueType == Type.D) {
                    // compile load double - pops 4
                    mv.visitInsn(Opcodes.DASTORE);
                    toPop = -4;
                }
                compileContext.addStackCount(toPop);
                if (iterator.hasNext()) {
                    assert valueType.isArray();
                    valueType =valueType.getBaseType();
                }
            }
        }

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

        // we needed room for an aray and an index or for a one or two word result
        // but the recursive evaluations will have made sure the max stack is big enough
        // so there is no need to update the maximum stack height
    }

    @Override
    public void bindAssign() throws TypeException {
        bind();
    }
}
