/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.struts2.ognl.accessor;

import org.apache.struts2.ObjectFactory;
import org.apache.struts2.conversion.ObjectTypeDeterminer;
import org.apache.struts2.conversion.impl.XWorkConverter;
import org.apache.struts2.inject.Inject;
import org.apache.struts2.ognl.OgnlUtil;
import org.apache.struts2.util.reflection.ReflectionContextState;
import ognl.ObjectPropertyAccessor;
import ognl.OgnlContext;
import ognl.OgnlException;
import ognl.OgnlRuntime;
import ognl.SetPropertyAccessor;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

/**
 * @author Gabe
 */
public class XWorkCollectionPropertyAccessor extends SetPropertyAccessor {

    private static final Logger LOG = LogManager.getLogger(XWorkCollectionPropertyAccessor.class);

    public static final String KEY_PROPERTY_FOR_CREATION = "makeNew";

    //use a basic object Ognl property accessor here
    //to access properties of the objects in the Set
    //so that nothing is put in the context to screw things up
    private final ObjectPropertyAccessor _accessor = new ObjectPropertyAccessor();

    private XWorkConverter xworkConverter;
    private ObjectFactory objectFactory;
    private ObjectTypeDeterminer objectTypeDeterminer;
    private OgnlUtil ognlUtil;

    @Inject
    public void setXWorkConverter(XWorkConverter conv) {
        this.xworkConverter = conv;
    }

    @Inject
    public void setObjectFactory(ObjectFactory fac) {
        this.objectFactory = fac;
    }

    @Inject
    public void setObjectTypeDeterminer(ObjectTypeDeterminer ot) {
        this.objectTypeDeterminer = ot;
    }

    @Inject
    public void setOgnlUtil(OgnlUtil util) {
        this.ognlUtil = util;
    }

    /**
     * Gets the property of a Collection by indexing the collection
     * based on a key property. For example, if the key property were
     * 'id', this method would convert the key Object to whatever
     * type the id property was, and then access the Set like it was
     * a Map returning a JavaBean with the value of id property matching
     * the input.
     *
     * @see ognl.PropertyAccessor#getProperty(java.util.Map, Object, Object)
     */
    @Override
    public Object getProperty(OgnlContext context, Object target, Object key) throws OgnlException {
        LOG.trace("Entering getProperty()");

        //check if it is a generic type property.
        //if so, return the value from the
        //superclass which will determine this.
        if (!ReflectionContextState.isGettingByKeyProperty(context) && !key.equals(KEY_PROPERTY_FOR_CREATION)) {
            return super.getProperty(context, target, key);
        } else {
            //reset context property
            ReflectionContextState.setGettingByKeyProperty(context, false);
        }
        Collection c = (Collection) target;

        //get the bean that this collection is a property of
        Class lastBeanClass = ReflectionContextState.getLastBeanClassAccessed(context);

        //get the property name that this collection uses
        String lastPropertyClass = ReflectionContextState.getLastBeanPropertyAccessed(context);

        //if one or the other is null, assume that it isn't
        //set up correctly so just return whatever the
        //superclass would
        if (lastBeanClass == null || lastPropertyClass == null) {
            ReflectionContextState.updateCurrentPropertyPath(context, key);
            return super.getProperty(context, target, key);
        }

        //get the key property to index the
        //collection with from the ObjectTypeDeterminer
        String keyProperty = objectTypeDeterminer.getKeyProperty(lastBeanClass, lastPropertyClass);

        //get the collection class of the
        Class collClass = objectTypeDeterminer.getElementClass(lastBeanClass, lastPropertyClass, key);

        Class keyType;
        Class toGetTypeFrom = (collClass != null) ? collClass : c.iterator().next().getClass();
        try {
            keyType = OgnlRuntime.getPropertyDescriptor(toGetTypeFrom, keyProperty).getPropertyType();
        } catch (Exception exc) {
            throw new OgnlException("Error getting property descriptor: " + exc.getMessage());
        }


        if (ReflectionContextState.isCreatingNullObjects(context)) {
            Map collMap = getSetMap(context, c, keyProperty);
            if (key.toString().equals(KEY_PROPERTY_FOR_CREATION)) {
                //this should return the XWorkList
                //for this set that contains new entries
                //then the ListPropertyAccessor will be called
                //to access it in the next sequence
                return collMap.get(null);
            }
            Object realKey = xworkConverter.convertValue(context, key, keyType);
            Object value = collMap.get(realKey);
            if (value == null
                    && ReflectionContextState.isCreatingNullObjects(context)
                    && objectTypeDeterminer
                    .shouldCreateIfNew(lastBeanClass, lastPropertyClass, c, keyProperty, false)) {
                //create a new element and
                //set the value of keyProperty
                //to be the given value
                try {
                    value = objectFactory.buildBean(collClass, context);

                    //set the value of the keyProperty
                    _accessor.setProperty(context, value, keyProperty, realKey);

                    //add the new object to the collection
                    c.add(value);

                    //add to the Map if accessed later
                    collMap.put(realKey, value);


                } catch (Exception exc) {
                    throw new OgnlException("Error adding new element to collection", exc);
                }

            }
            return value;
        } else {
            if (key.toString().equals(KEY_PROPERTY_FOR_CREATION)) {
                return null;
            }
            //with getting do iteration
            //don't assume for now it is
            //optimized to create the Map
            //and unlike setting, there is
            //no easy key for the Set
            Object realKey = xworkConverter.convertValue(context, key, keyType);
            return getPropertyThroughIteration(context, c, keyProperty, realKey);
        }
    }

    /*
     * Gets an indexed Map by a given key property with the key being
     * the value of the property and the value being the
     */
    private Map getSetMap(OgnlContext context, Collection collection, String property) throws OgnlException {
        LOG.trace("getting set Map");

        String path = ReflectionContextState.getCurrentPropertyPath(context);
        Map map = ReflectionContextState.getSetMap(context, path);

        if (map == null) {
            LOG.trace("creating set Map");

            map = new HashMap();
            map.put(null, new SurrugateList(collection));
            for (Object currTest : collection) {
                Object currKey = _accessor.getProperty(context, currTest, property);
                if (currKey != null) {
                    map.put(currKey, currTest);
                }
            }
            ReflectionContextState.setSetMap(context, map, path);
        }
        return map;
    }

    /*
     * gets a bean with the given
     */
    public Object getPropertyThroughIteration(OgnlContext context, Collection collection, String property, Object key)
            throws OgnlException {
        //TODO
        for (Object currTest : collection) {
            if (_accessor.getProperty(context, currTest, property).equals(key)) {
                return currTest;
            }
        }
        //none found
        return null;
    }

    @Override
    public void setProperty(OgnlContext context, Object target, Object name, Object value) throws OgnlException {
        Class lastClass = (Class) context.get(XWorkConverter.LAST_BEAN_CLASS_ACCESSED);
        String lastProperty = (String) context.get(XWorkConverter.LAST_BEAN_PROPERTY_ACCESSED);
        Class convertToClass = objectTypeDeterminer.getElementClass(lastClass, lastProperty, name);

        if (name instanceof String && value.getClass().isArray()) {
            // looks like the input game in the form of "someCollection.foo" and
            // we are expected to define the index values ourselves.
            // So let's do it:

            Collection c = (Collection) target;
            Object[] values = (Object[]) value;
            for (Object v : values) {
                try {
                    Object o = objectFactory.buildBean(convertToClass, context);
                    ognlUtil.setValue((String) name, context, o, v);
                    c.add(o);
                } catch (Exception e) {
                    throw new OgnlException("Error converting given String values for Collection.", e);
                }
            }

            // we don't want to do the normal collection property setting now, since we've already done the work
            // just return instead
            return;
        }

        Object realValue = getRealValue(context, value, convertToClass);

        super.setProperty(context, target, name, realValue);
    }

    private Object getRealValue(OgnlContext context, Object value, Class convertToClass) {
        if (value == null || convertToClass == null) {
            return value;
        }
        return xworkConverter.convertValue(context, value, convertToClass);
    }
}

/**
 * @author Gabe
 */
class SurrugateList extends ArrayList {

    private final Collection surrugate;

    public SurrugateList(Collection surrugate) {
        this.surrugate = surrugate;
    }

    @Override
    public void add(int arg0, Object arg1) {
        if (arg1 != null) {
            surrugate.add(arg1);
        }
        super.add(arg0, arg1);
    }

    @Override
    public boolean add(Object arg0) {
        if (arg0 != null) {
            surrugate.add(arg0);
        }
        return super.add(arg0);
    }

    @Override
    public boolean addAll(Collection arg0) {
        surrugate.addAll(arg0);
        return super.addAll(arg0);
    }

    @Override
    public boolean addAll(int arg0, Collection arg1) {
        surrugate.addAll(arg1);
        return super.addAll(arg0, arg1);
    }

    @Override
    public Object set(int arg0, Object arg1) {
        if (arg1 != null) {
            surrugate.add(arg1);
        }
        return super.set(arg0, arg1);
    }
}
