/*
* 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.binding.Binding;
import org.jboss.byteman.rule.compiler.CompileContext;
import org.jboss.byteman.rule.type.Type;
import org.jboss.byteman.rule.type.TypeGroup;
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.io.StringWriter;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

/**
 * an expression which identifies an instance field reference
 */
public class FieldExpression extends AssignableExpression
{
    public FieldExpression(Rule rule, Type type, ParseNode fieldTree, String fieldName, Expression owner, String[] pathList) {
        // we cannot process the pathlist until typecheck time
        super(rule, type, fieldTree);
        this.fieldName = fieldName;
        this.owner = owner;
        this.pathList = pathList;
        this.ownerType = null;
        this.indirectStatic = null;
        this.fieldIndex = -1;
        this.isArrayLength = false;
    }

    /**
     * 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 (owner != null) {
            // ensure the owner is bound
            owner.bind();
        } else {
            // see if the path starts with a bound variable and, if so, treat the path as a series
            // of field references and construct a owner expression from it. if not we will have to
            // wait until runtime in order to resolve this as a static field reference
            String leading = pathList[0];
            Binding binding = getBindings().lookup(leading);
            if (binding != null) {
                // create a sequence of field expressions and make it the owner

                int l = pathList.length;
                Expression owner =  new Variable(rule, binding.getType(), token, binding.getName());
                for (int idx = 1; idx < l; idx++) {
                    owner = new FieldExpression(rule, Type.UNDEFINED, token, pathList[idx], owner, null);
                }
                this.owner = owner;
                this.pathList = null;
                // not strictly necessary?
                this.owner.bind();
            }
        }
    }

    /**
     * treat this as a normal bind because an update to a field reference does not update any bindings
     * @throws TypeException if any variable is missing or has the wrong type
     */
    public void bindAssign() throws TypeException
    {
        bind();
    }
    public Type typeCheck(Type expected) throws TypeException {
        checkIndirectStatic();
        if (indirectStatic != null) {
            // this is really a static field reference pointed to by owner so get it to type check
            type = Type.dereference(indirectStatic.typeCheck(expected));
        } else {
            typeCheckAny(false);

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

    public Type typeCheckAssign(Type expected) throws TypeException {
        checkIndirectStatic();
        if (indirectStatic != null) {
            // this is really a static field reference pointed to by owner so get it to type check
            type = Type.dereference(indirectStatic.typeCheckAssign(expected));
            return type;
        } else {
            typeCheckAny(true);

            // we cannot accept an array length access in this position
            if (isArrayLength) {
                throw new TypeException("FieldExpresssion.typeCheck : invalid attempt to update array length " + owner + getPos());
            }

            if (Type.dereference(expected).isDefined() && !type.isAssignableFrom(expected)) {
                throw new TypeException("FieldExpresssion.typeCheck : invalid value type " + expected.getName() + "for assignment" + getPos());
            }
        }
        return type;
    }

    private void checkIndirectStatic() throws TypeException
    {
        if (owner == null && pathList != null) {
            // factor off a typename from the path
            TypeGroup typeGroup = getTypeGroup();
            Type rootType = typeGroup.match(pathList);
            if (rootType == null) {
                throw new TypeException("FieldExpression.typeCheck : invalid path " + getPath(pathList.length) + " to static field " + fieldName + getPos());
            }

            // find out how many of the path elements are included in the type name

            String rootTypeName = rootType.getName();

            int idx = getPathCount(rootTypeName);

            if (idx < pathList.length) {
                // create a static field reference using the type name and the first field name and wrap it with
                // enough field references to use up all the path
                String fieldName = pathList[idx++];
                Expression owner = new StaticExpression(rule, Type.UNDEFINED, token, fieldName, rootTypeName);
                while (idx < pathList.length) {
                    owner = new FieldExpression(rule, Type.UNDEFINED, token, pathList[idx++], owner, null);
                }
                this.owner = owner;
                // not strictly necessary?
                this.owner.bind();
            } else {
                // ok this field reference is actually a static reference -- install the one we just created as
                // owner and mark this one so it sidesteps any further requests to the owner
                this.indirectStatic = new StaticExpression(rule, Type.UNDEFINED, token, this.fieldName, rootTypeName);
                // not strictly necessary?
                this.indirectStatic.bind();
            }
            // get rid of the path list now
            this.pathList = null;
        }
    }

    private void typeCheckAny(boolean isAssign) throws TypeException {

        // ok, type check the owner and then use it to derive the field type

        ownerType = Type.dereference(owner.typeCheck(Type.UNDEFINED));
            
        if (ownerType.isUndefined()) {
            throw new TypeException("FieldExpresssion.typeCheck : unbound owner type for field " + fieldName + getPos());
        }

        Class ownerClazz = ownerType.getTargetClass();
        Class valueClass = null;

        if (ownerType.isArray()) {
            if (fieldName.equals("length")) {
                isArrayLength = true;
                type = Type.I;
            } else {
                throw new TypeException("FieldExpresssion.typeCheck : array type " + ownerType.getName() + " does not accept field reference " + fieldName + getPos());
            }
        } else {
            try {
                field  = lookupField(ownerClazz, isAssign);
            } catch (NoSuchFieldException e) {
                throw new TypeException("FieldExpresssion.typeCheck : invalid field reference " + ownerType.getName() + " ." + fieldName + getPos());
            }

            if ((field.getModifiers() & Modifier.STATIC) != 0) {
                throw new TypeException("FieldExpresssion.typeCheck : field is static " + ownerType.getName() + " ." + fieldName + getPos());
            }

            valueClass = field.getType();
            type = getTypeGroup().ensureType(valueClass);
        }
    }

    public Object interpret(HelperAdapter helper) throws ExecuteException
    {
        if (indirectStatic != null) {
            return indirectStatic.interpret(helper);
        } else if (isArrayLength) {
            Object value = owner.interpret(helper);
            if (value == null) {
                throw new ExecuteException("FieldExpression.interpret : attempted array length indirection through null value " + owner + getPos());
            }
            try {
                return Array.getLength(value);
            } catch (Exception e) {
                throw new ExecuteException("FieldExpression.interpret : exception accessing array length " + owner + getPos(), e);
            }
        } else {
            try {
                // TODO the reference should really be an expression?
                Object value = owner.interpret(helper);

                if (value == null) {
                    throw new ExecuteException("FieldExpression.interpret : attempted field indirection through null value " + owner + getPos());
                }

                if (isPublicField) {
                    return field.get(value);
                } else {
                    return rule.getAccessibleField(value, fieldIndex);
                }
            } catch (ExecuteException e) {
                throw e;
            } catch (IllegalAccessException e) {
                throw new ExecuteException("FieldExpression.interpret : error accessing field " + fieldName + getPos(), e);
            } catch (Exception e) {
                throw new ExecuteException("FieldExpression.interpret : unexpected exception accessing field " + fieldName + 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 = (type.getNBytes() > 4 ? 2 : 1);

        if (indirectStatic != null) {
            // this is just wrapping a static field expression so compile it
            indirectStatic.compile(mv, compileContext);
        } else if (isArrayLength) {
            owner.compile(mv, compileContext);
            mv.visitInsn(Opcodes.ARRAYLENGTH);
            // we removed the owner and replaced with expected words
            compileContext.addStackCount(expected - 1);
        } else {
            if (isPublicField) {
                // we can use GETFIELD to access a public field
                // if the owner type is inaccessible it
                // will have been presented as a generic Object
                // and we need to cast it to the declaring type
                Class<?> declaringClass = field.getDeclaringClass();
                Type ownerType = owner.getType();
                Type declaringType = getTypeGroup().ensureType(declaringClass);
                String declaringTypeName = Type.internalName(declaringClass);
                String fieldName = field.getName();
                String fieldType = Type.internalName(field.getType(), true);
                // compile the owner expression
                owner.compile(mv, compileContext);
                if (rule.requiresAccess(ownerType)) {
                    // we might be hande an inaccessible object but be
                    // accessing a field defined by an accessible parent
                    // class
                    compileContext.compileTypeConversion(Type.OBJECT, declaringType);
                }
                mv.visitFieldInsn(Opcodes.GETFIELD, declaringTypeName, fieldName, fieldType);
                // we removed the owner and replaced with expected words
                compileContext.addStackCount(expected - 1);
            } else {
                // since this is a private field we need to do the access using reflection
                // stack the helper, owner and the field index
                mv.visitVarInsn(Opcodes.ALOAD, 0);
                compileContext.addStackCount(1);
                owner.compile(mv, compileContext);
                mv.visitLdcInsn(fieldIndex);
                compileContext.addStackCount(1);
                // use the HelperAdapter method getAccessibleField to get the field value
                mv.visitMethodInsn(Opcodes.INVOKEINTERFACE,
                        Type.internalName(HelperAdapter.class),
                        "getAccessibleField",
                        "(Ljava/lang/Object;I)Ljava/lang/Object;");
                // we popped three words and added one object as result
                compileContext.addStackCount(-2);
                // convert Object to primitive or cast to subtype if required
                compileContext.compileTypeConversion(Type.OBJECT, type);
            }
        }
        // check the stack height is ok
        if (compileContext.getStackCount() != currentStack + expected) {
            throw new CompileException("FieldExpression.compile : invalid stack height " + compileContext.getStackCount() + " expecting " + (currentStack + expected));
        }
    }

    public String getPath(int len)
    {
        StringBuffer buffer = new StringBuffer();
        buffer.append(pathList[0]);

        for (int i = 1; i < len; i++) {
            buffer.append(".");
            buffer.append(pathList[i]);
        }
        return buffer.toString();
    }

    public int getPathCount(String name)
    {
        // name will be package qualified so check whether the path list also includes the package
        if (name.startsWith(pathList[0])) {
            int charMax = name.length();
            int charCount = 0;
            int dotExtra = 0;
            int idx;
            for (idx = 0; idx < pathList.length; idx++) {
                charCount += (dotExtra + pathList[idx].length());
                if (charCount > charMax) {
                    break;
                }
            }
            return idx;
        } else {
            // name must have been obtained by globalizing an unqualified type name so the typename
            // is the first element in the path list
            return 1;
        }
    }

    public void writeTo(StringWriter stringWriter) {
        // we normally have a owner expression but before binding we have a path
        if (owner != null) {
            owner.writeTo(stringWriter);
        } else {
            String sepr = "";
            for (String field : pathList) {
                stringWriter.write(sepr);
                stringWriter.write(field);
                sepr =".";
            }
        }
        stringWriter.write(".");
        stringWriter.write(fieldName);
    }

    private Expression owner;
    private String[] pathList;
    private String fieldName;
    private Type ownerType;
    private Field field;
    private AssignableExpression indirectStatic;
    private boolean isArrayLength;
    private boolean isClassAccess;

    /**
     * true if this is a public field otherwise false
     */
    private boolean isPublicField;
    /**
     * index used compiled code when reading or writing a non-public field to obtain the field descriptor
     * from the helper's list of accessible field descriptors.
     */
    private int fieldIndex;

    @Override
    public Object interpretAssign(HelperAdapter helperAdapter, Object value) throws ExecuteException
    {
        if (indirectStatic != null) {
            return indirectStatic.interpretAssign(helperAdapter, value);
        } else {
            try {
                Object ownerInstance = owner.interpret(helperAdapter);

                if (ownerInstance == null) {
                    throw new ExecuteException("FieldExpression.interpret : attempted field indirection through null value " + owner + getPos());
                }

                if (isPublicField) {
                    field.set(ownerInstance, value);
                } else {
                    rule.setAccessibleField(ownerInstance, value, fieldIndex);
                }
                return value;
            } catch (ExecuteException e) {
                throw e;
            } catch (IllegalAccessException e) {
                throw new ExecuteException("FieldExpression.interpretAssign : error accessing field " + fieldName + getPos(), e);
            } catch (IllegalArgumentException e) {
                throw new ExecuteException("FieldExpression.interpretAssign : invalid value assigning field " + fieldName + getPos(), e);
            } catch (Exception e) {
                throw new ExecuteException("FieldExpression.interpretAssign : unexpected exception accessing field " + fieldName + getPos(), e);
            }
        }
    }

    @Override
    public void compileAssign(MethodVisitor mv, CompileContext compileContext) throws CompileException
    {
        if (indirectStatic != null) {
            // this is just wrapping a static field expression so compile it
            indirectStatic.compileAssign(mv, compileContext);
        } else {
            // make sure we are at the right source line
            compileContext.notifySourceLine(line);

            int currentStack = compileContext.getStackCount();
            int size = (type.getNBytes() > 4 ? 2 : 1);

            // copy the value so we leave it as a result
            if (size == 1) {
                // this means at the maximum we add 1 to the current stack
                // [.. val] ==> [.. val val]
                mv.visitInsn(Opcodes.DUP);
            } else {
                // [.. val1 val2] ==> [.. val1 val2 val1 val2]
                mv.visitInsn(Opcodes.DUP2);
            }
            compileContext.addStackCount(size);
            // compile the owner expression
            owner.compile(mv, compileContext);
            if (isPublicField) {
                // we can use PUTFIELD to set a public field
                // if the owner type is inaccessible it
                // will have been presented as a generic Object
                // and we need to cast it to the declaring type
                Class<?> declaringClass = field.getDeclaringClass();
                Type ownerType = owner.getType();
                Type declaringType = getTypeGroup().ensureType(declaringClass);
                String declaringTypeName = Type.internalName(declaringClass);
                String fieldName = field.getName();
                String fieldType = Type.internalName(field.getType(), true);
                if (rule.requiresAccess(ownerType)) {
                    // convert from generic Object to the type of the field owner
                    compileContext.compileTypeConversion(Type.OBJECT, declaringType);
                }
                // swap the owner below the value
                if (size == 1) {
                    // [.. val val owner] ==> [.. val owner val]
                    mv.visitInsn(Opcodes.SWAP);
                } else {
                    // we have to use a DUP_X2 and a POP to insert the owner below the two word value
                    // i.e. [.. val1 val2 val1 val2] ==> [.. val1 val2 val1 val2 owner] ==>
                    //              [.. val1 val2 owner val1 val2 owner] ==> [.. val1 val2 owner val1 val2]
                    mv.visitInsn(Opcodes.DUP_X2);
                    compileContext.addStackCount(1);
                    mv.visitInsn(Opcodes.POP);
                    compileContext.addStackCount(-1);
                }
                mv.visitFieldInsn(Opcodes.PUTFIELD, declaringTypeName, fieldName, fieldType);
                // we removed the owner and the value
                compileContext.addStackCount(- (1 + size));
            } else {
                // we don't care what type of object the owner is
                // swap the owner below the value
                if (size == 1) {
                    // [.. val val owner] ==> [.. val owner val]
                    mv.visitInsn(Opcodes.SWAP);
                } else {
                    // we have to use a DUP_X2 and a POP to insert the owner below the two word value
                    // i.e. [.. val1 val2 val1 val2] ==> [.. val1 val2 val1 val2 owner] ==>
                    //              [.. val1 val2 owner val1 val2 owner] ==> [.. val1 val2 owner val1 val2]
                    mv.visitInsn(Opcodes.DUP_X2);
                    compileContext.addStackCount(1);
                    mv.visitInsn(Opcodes.POP);
                    compileContext.addStackCount(-1);
                }
                // since this is a private field we need to do the update using reflection
                // box the value to an object if necessary
                if (type.isPrimitive()) {
                    compileContext.compileBox(Type.boxType(type));
                }
                // stack the helper and then dupx2 it so it goes under the owner and value
                // [.. val(s) owner  valObj ==> val(s) owner valObj helper ]
                mv.visitVarInsn(Opcodes.ALOAD, 0);
                // [.. val(s) owner  valObj helper ==> val(s) helper owner valObj helper ]
                mv.visitInsn(Opcodes.DUP_X2);
                // stack now has 2 more words so count them
                compileContext.addStackCount(2);
                // now pop the redundant top word and stack the field index instead
                // [.. val(s) helper owner valObj helper ==> val(s) helper owner valObj index ]
                mv.visitInsn(Opcodes.POP);
                mv.visitLdcInsn(fieldIndex);
                // use the HelperAdapter method setAccessibleField to set the field value
                mv.visitMethodInsn(Opcodes.INVOKEINTERFACE,
                        Type.internalName(HelperAdapter.class),
                        "setAccessibleField",
                        "(Ljava/lang/Object;Ljava/lang/Object;I)V");
                // we popped four args
                compileContext.addStackCount(-4);
            }

            // check the stack height is ok
            if (compileContext.getStackCount() != currentStack) {
                throw new CompileException("FieldExpression.compileAssign : invalid stack height " + compileContext.getStackCount() + " expecting " + (currentStack));
            }
        }
    }
    
    private Field lookupField(Class<?> ownerClazz, boolean isAssign) throws NoSuchFieldException
    {
        try {
            Field field = ownerClazz.getField(fieldName);
            Type ownerType = getTypeGroup().ensureType(ownerClazz);
            // TODO !!! deal with public fields of inaccessible classes !!!
            // ask rule to check whether we need access
            if (!rule.requiresAccess(ownerType) && !rule.requiresAccess(field)) {
                isPublicField = true;
                return field;
            } else {
                isPublicField = false;
                // register the field with the rule so we can access it later
                if (!isAssign) {
                    fieldIndex = rule.addAccessibleFieldGetter(field);
                } else {
                    fieldIndex = rule.addAccessibleFieldSetter(field);
                }
                return field;
            }
        } catch (NoSuchFieldException nsfe) {
            // look for a protected or private field with the desired name
            Class<?> nextClass = ownerClazz;
            while (nextClass != null) {
                try {
                    field = nextClass.getDeclaredField(fieldName);
                    isPublicField = false;
                    // register the field with the rule so we can access it later
                    if (!isAssign) {
                        fieldIndex = rule.addAccessibleFieldGetter(field);
                    } else {
                        fieldIndex = rule.addAccessibleFieldSetter(field);
                    }
                    return field;
                } catch (NoSuchFieldException e) {
                    // continue
                } catch (SecurityException e) {
                    // continue
                }
                nextClass = nextClass.getSuperclass();
            }

            throw nsfe;
        }
    }
}
