/*
 * 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.jmeter.testbeans;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.LinkedList;

import org.apache.jmeter.testbeans.gui.GenericTestBeanCustomizer;
import org.apache.jmeter.testbeans.gui.TableEditor;
import org.apache.jmeter.testelement.TestElement;
import org.apache.jmeter.testelement.property.CollectionProperty;
import org.apache.jmeter.testelement.property.JMeterProperty;
import org.apache.jmeter.testelement.property.MultiProperty;
import org.apache.jmeter.testelement.property.NullProperty;
import org.apache.jmeter.testelement.property.PropertyIterator;
import org.apache.jmeter.testelement.property.TestElementProperty;
import org.apache.jmeter.util.JMeterUtils;
import org.apache.jorphan.logging.LoggingManager;
import org.apache.jorphan.util.Converter;
import org.apache.log.Logger;

/**
 * This is an experimental class. An attempt to address the complexity of
 * writing new JMeter components.
 * <p>
 * TestBean currently extends AbstractTestElement to support
 * backward-compatibility, but the property-value-map may later on be separated
 * from the test beans themselves. To ensure this will be doable with minimum
 * damage, all inherited methods are deprecated.
 *
 */
public class TestBeanHelper {
    protected static final Logger log = LoggingManager.getLoggerForClass();

    /**
     * Prepare the bean for work by populating the bean's properties from the
     * property value map.
     * <p>
     *
     * @deprecated to limit it's usage in expectation of moving it elsewhere.
     */
    @Deprecated
    public static void prepare(TestElement el) {
        if (!(el instanceof TestBean)) {
            return;
        }
        try {
            BeanInfo beanInfo = Introspector.getBeanInfo(el.getClass());
            PropertyDescriptor[] descs = beanInfo.getPropertyDescriptors();

            if (log.isDebugEnabled()) {
                log.debug("Preparing " + el.getClass());
            }

            for (PropertyDescriptor desc : descs) {
                if (isDescriptorIgnored(desc)) {
                    if (log.isDebugEnabled()) {
                        log.debug("Ignoring property '" + desc.getName() + "' in " + el.getClass().getCanonicalName());
                    }
                    continue;
                }
                // Obtain a value of the appropriate type for this property.
                JMeterProperty jprop = el.getProperty(desc.getName());
                Class<?> type = desc.getPropertyType();
                Object value = unwrapProperty(desc, jprop, type);

                if (log.isDebugEnabled()) {
                    log.debug("Setting " + jprop.getName() + "=" + value);
                }

                // Set the bean's property to the value we just obtained:
                if (value != null || !type.isPrimitive())
                // We can't assign null to primitive types.
                {
                    Method writeMethod = desc.getWriteMethod();
                    if (writeMethod!=null) {
                        invokeOrBailOut(el, writeMethod, new Object[] {value});
                    }
                }
            }
        } catch (IntrospectionException e) {
            log.error("Couldn't set properties for " + el.getClass().getName(), e);
        } catch (UnsatisfiedLinkError ule) { // Can occur running headless on Jenkins
            log.error("Couldn't set properties for " + el.getClass().getName());
            throw ule;
        }
    }

    private static Object unwrapProperty(PropertyDescriptor desc, JMeterProperty jprop, Class<?> type) {
        Object value;
        if(jprop instanceof TestElementProperty)
        {
            TestElement te = ((TestElementProperty)jprop).getElement();
            if(te instanceof TestBean)
            {
                prepare(te);
            }
            value = te;
        }
        else if(jprop instanceof MultiProperty)
        {
            value = unwrapCollection((MultiProperty)jprop,(String)desc.getValue(TableEditor.CLASSNAME));
        }
        // value was not provided, and this is allowed
        else if (jprop instanceof NullProperty &&
                // use negative condition so missing (null) value is treated as FALSE
                ! Boolean.TRUE.equals(desc.getValue(GenericTestBeanCustomizer.NOT_UNDEFINED))) {    
            value=null;
        } else {
            value = Converter.convert(jprop.getStringValue(), type);
        }
        return value;
    }

    private static Object unwrapCollection(MultiProperty prop,String type)
    {
        if(prop instanceof CollectionProperty)
        {
            Collection<Object> values = new LinkedList<Object>();
            PropertyIterator iter = prop.iterator();
            while(iter.hasNext())
            {
                try
                {
                    values.add(unwrapProperty(null,iter.next(),Class.forName(type)));
                }
                catch(Exception e)
                {
                    log.error("Couldn't convert object: " + prop.getObjectValue() + " to " + type,e);
                }
            }
            return values;
        }
        return null;
    }

    /**
     * Utility method that invokes a method and does the error handling around
     * the invocation.
     *
     * @param method
     * @param params
     * @return the result of the method invocation.
     */
    private static Object invokeOrBailOut(Object invokee, Method method, Object[] params) {
        try {
            return method.invoke(invokee, params);
        } catch (IllegalArgumentException e) {
            throw new Error(createMessage(invokee, method, params), e);
        } catch (IllegalAccessException e) {
            throw new Error(createMessage(invokee, method, params), e);
        } catch (InvocationTargetException e) {
            throw new Error(createMessage(invokee, method, params), e);
        }
    }

    private static String createMessage(Object invokee, Method method, Object[] params){
        StringBuilder sb = new StringBuilder();
        sb.append("This should never happen. Tried to invoke:\n");
        sb.append(invokee.getClass().getName());
        sb.append("#");
        sb.append(method.getName());
        sb.append("(");
        for(Object o : params) {
            if (o != null) {
                sb.append(o.getClass().getSimpleName());
                sb.append(' ');
            }
            sb.append(o);
            sb.append(' ');
        }
        sb.append(")");
        return sb.toString();
    }

    /**
     * Checks whether the descriptor should be ignored, i.e.
     * <ul>
     * <li>isHidden</li>
     * <li>isExpert and JMeter not using expert mode</li>
     * <li>no read method</li>
     * <li>no write method</li>
     * </ul>
     * @param descriptor
     * @return true if the descriptor should be ignored
     */
    public static boolean isDescriptorIgnored(PropertyDescriptor descriptor) {
        return descriptor.isHidden() 
            || (descriptor.isExpert() && !JMeterUtils.isExpertMode())
            || descriptor.getReadMethod() == null 
            || descriptor.getWriteMethod() == null;
    }
}
