// --------------------------------------------------------------------------
// Copyright (c) 1998-2004, Drew Davidson and Luke Blanshard
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
// Neither the name of the Drew Davidson nor the names of its contributors
// may be used to endorse or promote products derived from this software
// without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
// COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
// OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
// DAMAGE.
// --------------------------------------------------------------------------
package ognl;

import ognl.enhance.ExpressionCompiler;
import ognl.enhance.UnsupportedCompilationException;

import java.beans.IndexedPropertyDescriptor;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.Iterator;

/**
 * @author Luke Blanshard (blanshlu@netscape.net)
 * @author Drew Davidson (drew@ognl.org)
 */
public class ASTProperty extends SimpleNode implements NodeType
{
    private boolean _indexedAccess = false;

    private Class _getterClass;
    private Class _setterClass;

    public ASTProperty(int id)
    {
        super(id);
    }

    public void setIndexedAccess(boolean value)
    {
        _indexedAccess = value;
    }

    /**
     * Returns true if this property is itself an index reference.
     */
    public boolean isIndexedAccess()
    {
        return _indexedAccess;
    }

    /**
     * Returns true if this property is described by an IndexedPropertyDescriptor and that if
     * followed by an index specifier it will call the index get/set methods rather than go through
     * property accessors.
     */
    public int getIndexedPropertyType(OgnlContext context, Object source)
            throws OgnlException
    {
        Class type = context.getCurrentType();
        Class prevType = context.getPreviousType();
        try
        {
            if (!isIndexedAccess())
            {
                Object property = getProperty(context, source);

                if (property instanceof String)
                {
                    return OgnlRuntime.getIndexedPropertyType(context, (source == null)
                            ? null
                            : OgnlRuntime.getCompiler().getInterfaceClass(source.getClass()), (String) property);
                }
            }

            return OgnlRuntime.INDEXED_PROPERTY_NONE;
        } finally
        {
            context.setCurrentObject(source);
            context.setCurrentType(type);
            context.setPreviousType(prevType);
        }
    }

    public Object getProperty(OgnlContext context, Object source)
            throws OgnlException
    {
        return _children[0].getValue(context, context.getRoot());
    }

    protected Object getValueBody(OgnlContext context, Object source)
            throws OgnlException
    {
        Object property = getProperty(context, source);

        Object result = OgnlRuntime.getProperty(context, source, property);

        if (result == null)
        {
            result = OgnlRuntime.getNullHandler(OgnlRuntime.getTargetClass(source)).nullPropertyValue(context, source, property);
        }

        return result;
    }

    protected void setValueBody(OgnlContext context, Object target, Object value)
            throws OgnlException
    {
        OgnlRuntime.setProperty(context, target, getProperty(context, target), value);
    }

    public boolean isNodeSimpleProperty(OgnlContext context)
            throws OgnlException
    {
        return (_children != null) && (_children.length == 1) && ((SimpleNode) _children[0]).isConstant(context);
    }

    public Class getGetterClass()
    {
        return _getterClass;
    }

    public Class getSetterClass()
    {
        return _setterClass;
    }

    public String toString()
    {
        String result;

        if (isIndexedAccess())
        {
            result = "[" + _children[0] + "]";
        } else
        {
            result = ((ASTConst) _children[0]).getValue().toString();
        }
        return result;
    }

    public String toGetSourceString(OgnlContext context, Object target)
    {
        if (context.getCurrentObject() == null)
            throw new UnsupportedCompilationException("Current target is null.");

        String result = "";
        Method m = null;

        try {
            /* System.out.println("astproperty is indexed? : " + isIndexedAccess() + " child: " + _children[0].getClass().getName()
+ " target: " + target.getClass().getName() + " current object: " + context.getCurrentObject().getClass().getName());*/

            if (isIndexedAccess())
            {
                Object value = _children[0].getValue(context, context.getRoot());

                if (value == null || DynamicSubscript.class.isAssignableFrom(value.getClass()))
                    throw new UnsupportedCompilationException("Value passed as indexed property was null or not supported.");

                // Get root cast string if the child is a type that needs it (like a nested ASTProperty)

                String srcString = _children[0].toGetSourceString(context, context.getRoot());
                srcString = ExpressionCompiler.getRootExpression(_children[0], context.getRoot(), context) + srcString;

                if (ASTChain.class.isInstance(_children[0]))
                {
                    String cast = (String)context.remove(ExpressionCompiler.PRE_CAST);
                    if (cast != null)
                        srcString = cast + srcString;
                }

                if (ASTConst.class.isInstance(_children[0]) && String.class.isInstance(context.getCurrentObject()))
                    srcString = "\"" + srcString + "\"";

                // System.out.println("indexed getting with child srcString: " + srcString + " value class: " + value.getClass() + " and child: " + _children[0].getClass());

                if (context.get("_indexedMethod") != null)
                {
                    m = (Method)context.remove("_indexedMethod");
                    _getterClass = m.getReturnType();

                    Object indexedValue = OgnlRuntime.callMethod(context, target, m.getName(), new Object[]{value});

                    context.setCurrentType(_getterClass);
                    context.setCurrentObject(indexedValue);
                    context.setCurrentAccessor(OgnlRuntime.getCompiler().getSuperOrInterfaceClass(m, m.getDeclaringClass()));

                    return "." + m.getName() + "(" + srcString + ")";
                } else
                {
                    PropertyAccessor p = OgnlRuntime.getPropertyAccessor(target.getClass());

//                    System.out.println("child value : " + _children[0].getValue(context, context.getCurrentObject()) + " using propaccessor " + p.getClass().getName()
//                        + " and srcString " + srcString + " on target: " +  target);

                    Object currObj = context.getCurrentObject();
                    Class currType = context.getCurrentType();
                    Class prevType = context.getPreviousType();

                    Object indexVal = p.getProperty(context, target, value);

                    // reset current object for accessor

                    context.setCurrentObject(currObj);
                    context.setCurrentType(currType);
                    context.setPreviousType(prevType);

/*                    System.out.println("astprop srcString: " + srcString
   + " from child class " + _children[0].getClass().getName()
   + " and indexVal " + indexVal
   + " propertyAccessor : " + p.getClass().getName() + " context obj " + context.getCurrentObject()
   + " context obj is array? : " + context.getCurrentObject().getClass().isArray()
              + " current type: " + context.getCurrentType());*/

                    if (ASTConst.class.isInstance(_children[0]) && Number.class.isInstance(context.getCurrentObject()))
                        context.setCurrentType(OgnlRuntime.getPrimitiveWrapperClass(context.getCurrentObject().getClass()));

                    result = p.getSourceAccessor(context, target, srcString);
                    _getterClass = context.getCurrentType();
                    context.setCurrentObject(indexVal);

                    return result;
                }
            }

            String name =  ((ASTConst) _children[0]).getValue().toString();

            if (!Iterator.class.isAssignableFrom(context.getCurrentObject().getClass())
                || (Iterator.class.isAssignableFrom(context.getCurrentObject().getClass()) && name.indexOf("next") < 0))
            {
                Object currObj = target;

                try
                {
                    target = getValue(context, context.getCurrentObject());
                } catch (NoSuchPropertyException e)
                {
                    try {
                        target = getValue(context, context.getRoot());
                    } catch (NoSuchPropertyException ex) {
                        // ignore
                    }
                } finally
                {
                    context.setCurrentObject(currObj);
                }
            }

            PropertyDescriptor pd = OgnlRuntime.getPropertyDescriptor(context.getCurrentObject().getClass(), name);

            if (pd != null && pd.getReadMethod() != null
                && !context.getMemberAccess().isAccessible(context, context.getCurrentObject(), pd.getReadMethod(), name))
            {
                throw new UnsupportedCompilationException("Member access forbidden for property " + name + " on class " + context.getCurrentObject().getClass());
            }

            if (this.getIndexedPropertyType(context, context.getCurrentObject()) > 0 && pd != null)
            {
                // if an indexed method accessor need to use special property descriptors to find methods

                if (pd instanceof IndexedPropertyDescriptor)
                {
                    m = ((IndexedPropertyDescriptor) pd).getIndexedReadMethod();
                } else
                {
                    if (pd instanceof ObjectIndexedPropertyDescriptor)
                        m = ((ObjectIndexedPropertyDescriptor) pd).getIndexedReadMethod();
                    else
                        throw new OgnlException("property '" + name + "' is not an indexed property");
                }

                if (_parent == null)
                {
                    // the above pd will be the wrong result sometimes, such as methods like getValue(int) vs String[] getValue()
                    m = OgnlRuntime.getReadMethod(context.getCurrentObject().getClass(), name);

                    result = m.getName() + "()";
                    _getterClass = m.getReturnType();
                } else
                {
                    context.put("_indexedMethod", m);
                }
            } else
            {

                /*    System.out.println("astproperty trying to get " + name + " on object target: " + context.getCurrentObject().getClass().getName()
       + " current type " + context.getCurrentType() + " current accessor " + context.getCurrentAccessor()
   + " prev type " + context.getPreviousType() + " prev accessor " + context.getPreviousAccessor());*/

                PropertyAccessor pa = OgnlRuntime.getPropertyAccessor(context.getCurrentObject().getClass());

                if (context.getCurrentObject().getClass().isArray())
                {
                    if (pd == null)
                    {
                        pd = OgnlRuntime.getProperty(context.getCurrentObject().getClass(), name);

                        if (pd != null && pd.getReadMethod() != null)
                        {
                            m = pd.getReadMethod();
                            result = pd.getName();
                        } else
                        {
                            _getterClass = int.class;
                            context.setCurrentAccessor(context.getCurrentObject().getClass());
                            context.setCurrentType(int.class);
                            result = "." + name;
                        }
                    }
                } else
                {
                    if (pd != null && pd.getReadMethod() != null)
                    {
                        m = pd.getReadMethod();
                        result = "." + m.getName() + "()";
                    } else if (pa != null)
                    {
                        Object currObj = context.getCurrentObject();
                        Class currType = context.getCurrentType();
                        Class prevType = context.getPreviousType();

                        String srcString = _children[0].toGetSourceString(context, context.getRoot());

                        if (ASTConst.class.isInstance(_children[0]) &&
                            String.class.isInstance(context.getCurrentObject()))
                            srcString = "\"" + srcString + "\"";

                        context.setCurrentObject(currObj);
                        context.setCurrentType(currType);
                        context.setPreviousType(prevType);

                        result = pa.getSourceAccessor(context, context.getCurrentObject(), srcString);

                        _getterClass = context.getCurrentType();
                    }
                }
            }

        } catch (Throwable t)
        {
            throw OgnlOps.castToRuntime(t);
        }

        // set known property types for NodeType interface when possible

        if (m != null)
        {
            _getterClass = m.getReturnType();

            context.setCurrentType(m.getReturnType());
            context.setCurrentAccessor(OgnlRuntime.getCompiler().getSuperOrInterfaceClass(m, m.getDeclaringClass()));
        }

        context.setCurrentObject(target);

        return result;
    }

    Method getIndexedWriteMethod(PropertyDescriptor pd)
    {
        if (IndexedPropertyDescriptor.class.isInstance(pd))
        {
            return ((IndexedPropertyDescriptor)pd).getIndexedWriteMethod();
        } else if (ObjectIndexedPropertyDescriptor.class.isInstance(pd))
        {
            return ((ObjectIndexedPropertyDescriptor)pd).getIndexedWriteMethod();
        }

        return null;
    }

    public String toSetSourceString(OgnlContext context, Object target)
    {
        String result = "";
        Method m = null;

        if (context.getCurrentObject() == null)
            throw new UnsupportedCompilationException("Current target is null.");

        /*System.out.println("astproperty(setter) is indexed? : " + isIndexedAccess() + " child: " + _children[0].getClass().getName()
                           + " target: " + target.getClass().getName() + " children length: " + _children.length);*/

        try {

            if (isIndexedAccess())
            {
                Object value = _children[0].getValue(context, context.getRoot());

                if (value == null)
                    throw new UnsupportedCompilationException("Value passed as indexed property is null, can't enhance statement to bytecode.");

                String srcString = _children[0].toGetSourceString(context, context.getRoot());
                srcString = ExpressionCompiler.getRootExpression(_children[0], context.getRoot(), context) + srcString;

                if (ASTChain.class.isInstance(_children[0]))
                {
                    String cast = (String)context.remove(ExpressionCompiler.PRE_CAST);
                    if (cast != null)
                        srcString = cast + srcString;
                }

                if (ASTConst.class.isInstance(_children[0]) && String.class.isInstance(context.getCurrentObject()))
                {
                    srcString = "\"" + srcString + "\"";
                }

//                System.out.println("astproperty setter using indexed value " + value + " and srcString: " + srcString);

                if (context.get("_indexedMethod") != null)
                {
                    m = (Method)context.remove("_indexedMethod");
                    PropertyDescriptor pd = (PropertyDescriptor)context.remove("_indexedDescriptor");

                    boolean lastChild = lastChild(context);
                    if (lastChild)
                    {
                        m = getIndexedWriteMethod(pd);

                        if (m == null)
                            throw new UnsupportedCompilationException("Indexed property has no corresponding write method.");
                    }

                    _setterClass = m.getParameterTypes()[0];

                    Object indexedValue = null;
                    if (!lastChild)
                        indexedValue = OgnlRuntime.callMethod(context, target, m.getName(), new Object[]{value});

                    context.setCurrentType(_setterClass);
                    context.setCurrentAccessor(OgnlRuntime.getCompiler().getSuperOrInterfaceClass(m, m.getDeclaringClass()));

                    if (!lastChild)
                    {
                        context.setCurrentObject(indexedValue);
                        return "." + m.getName() + "(" + srcString + ")";
                    } else {
                        return "." + m.getName() + "(" + srcString + ", $3)";
                    }
                } else
                {
                    PropertyAccessor p = OgnlRuntime.getPropertyAccessor(target.getClass());

                    Object currObj = context.getCurrentObject();
                    Class currType = context.getCurrentType();
                    Class prevType = context.getPreviousType();

                    Object indexVal = p.getProperty(context, target, value);

                    // reset current object for accessor

                    context.setCurrentObject(currObj);
                    context.setCurrentType(currType);
                    context.setPreviousType(prevType);

                    if (ASTConst.class.isInstance(_children[0]) && Number.class.isInstance(context.getCurrentObject()))
                        context.setCurrentType(OgnlRuntime.getPrimitiveWrapperClass(context.getCurrentObject().getClass()));

                    result = lastChild(context) ? p.getSourceSetter(context, target, srcString) : p.getSourceAccessor(context, target, srcString);

                    /*System.out.println("ASTProperty using propertyaccessor and isLastChild? " + lastChild(context)
                                       + " generated source of: " + result + " using accessor class: " + p.getClass().getName());*/

                    //result = p.getSourceAccessor(context, target, srcString);
                    _getterClass = context.getCurrentType();
                    context.setCurrentObject(indexVal);

                    /* PropertyAccessor p = OgnlRuntime.getPropertyAccessor(target.getClass());

                    if (ASTConst.class.isInstance(_children[0]) && Number.class.isInstance(context.getCurrentObject()))
                    {
                        context.setCurrentType(OgnlRuntime.getPrimitiveWrapperClass(context.getCurrentObject().getClass()));
                    }

                    result = p.getSourceSetter(context, target, srcString);

                    context.setCurrentObject(value);
                    context.setCurrentType(_getterClass);*/
                    return result;
                }
            }

            String name = ((ASTConst) _children[0]).getValue().toString();

//            System.out.println(" astprop(setter) : trying to set " + name + " on object target " + context.getCurrentObject().getClass().getName());

            if (!Iterator.class.isAssignableFrom(context.getCurrentObject().getClass())
                || (Iterator.class.isAssignableFrom(context.getCurrentObject().getClass()) &&  name.indexOf("next") < 0)) {

                Object currObj = target;

                try {
                    target = getValue(context, context.getCurrentObject());
                } catch (NoSuchPropertyException e) {
                    try {

                        target = getValue(context, context.getRoot());

                    } catch (NoSuchPropertyException ex) { }
                } finally {

                    context.setCurrentObject(currObj);
                }
            }

            PropertyDescriptor pd = OgnlRuntime.getPropertyDescriptor(OgnlRuntime.getCompiler().getInterfaceClass(context.getCurrentObject().getClass()), name);

            if (pd != null)
            {
                Method pdMethod = lastChild(context) ? pd.getWriteMethod() : pd.getReadMethod();

                if (pdMethod != null && !context.getMemberAccess().isAccessible(context, context.getCurrentObject(), pdMethod, name))
                {
                    throw new UnsupportedCompilationException("Member access forbidden for property " + name + " on class " + context.getCurrentObject().getClass());
                }
            }

            if (pd != null && this.getIndexedPropertyType(context, context.getCurrentObject()) > 0)
            {
                // if an indexed method accessor need to use special property descriptors to find methods

                if (pd instanceof IndexedPropertyDescriptor)
                {
                    IndexedPropertyDescriptor ipd = (IndexedPropertyDescriptor)pd;
                    m = lastChild(context) ? ipd.getIndexedWriteMethod() : ipd.getIndexedReadMethod();
                } else
                {
                    if (pd instanceof ObjectIndexedPropertyDescriptor)
                    {
                        ObjectIndexedPropertyDescriptor opd = (ObjectIndexedPropertyDescriptor)pd;

                        m = lastChild(context) ? opd.getIndexedWriteMethod() : opd.getIndexedReadMethod();
                    } else
                    {
                        throw new OgnlException("property '" + name + "' is not an indexed property");
                    }
                }

                if (_parent == null)
                {
                    // the above pd will be the wrong result sometimes, such as methods like getValue(int) vs String[] getValue() 

                    m = OgnlRuntime.getWriteMethod(context.getCurrentObject().getClass(), name);
                    Class parm = m.getParameterTypes()[0];
                    String cast = parm.isArray() ? ExpressionCompiler.getCastString(parm) : parm.getName();

                    result = m.getName() + "((" + cast + ")$3)";
                    _setterClass = parm;
                } else
                {
                    context.put("_indexedMethod", m);
                    context.put("_indexedDescriptor", pd);
                }

            } else {
                PropertyAccessor pa = OgnlRuntime.getPropertyAccessor(context.getCurrentObject().getClass());

                /*System.out.println("astproperty trying to set " + name + " on object target: " + context.getCurrentObject().getClass().getName()
                                   + " using propertyaccessor type: " + pa);*/

                if (target != null)
                    _setterClass = target.getClass();

                if (_parent != null && pd != null && pa == null)
                {
                    m = pd.getReadMethod();
                    result = m.getName() + "()";
                } else
                {
                    if (context.getCurrentObject().getClass().isArray())
                    {
                        result = "";
                    } else if (pa != null)
                    {
                        Object currObj = context.getCurrentObject();
                        //Class currType = context.getCurrentType();
                        //Class prevType = context.getPreviousType();

                        String srcString = _children[0].toGetSourceString(context, context.getRoot());

                        if (ASTConst.class.isInstance(_children[0]) && String.class.isInstance(context.getCurrentObject()))
                        {
                            srcString = "\"" + srcString + "\"";
                        }

                        context.setCurrentObject(currObj);
                        //context.setCurrentType(currType);
                        //context.setPreviousType(prevType);

                        if (!lastChild(context))
                        {
                            result = pa.getSourceAccessor(context, context.getCurrentObject(), srcString);
                        } else
                        {
                            result = pa.getSourceSetter(context, context.getCurrentObject(), srcString);
                        }

                        _getterClass = context.getCurrentType();
                    }
                }
            }

        } catch (Throwable t)
        {
            throw OgnlOps.castToRuntime(t);
        }

        context.setCurrentObject(target);

        if (m != null)
        {
            context.setCurrentType(m.getReturnType());
            context.setCurrentAccessor(OgnlRuntime.getCompiler().getSuperOrInterfaceClass(m, m.getDeclaringClass()));
        }

        return result;
    }
}
