/*
 * 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.TestElementProperty;
import org.apache.jmeter.util.JMeterUtils;
import org.apache.jorphan.util.Converter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 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 = LoggerFactory.getLogger(TestBeanHelper.class);

    /**
     * Prepare the bean for work by populating the bean's properties from the
     * property value map.
     * <p>
     *
     * @param el the TestElement to be prepared
     */
    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 '{}' in {}", desc.getName(), 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(), e);
        } catch (UnsatisfiedLinkError ule) { // Can occur running headless on Jenkins
            log.error("Couldn't set properties for {}", el.getClass());
            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<>();
            for (JMeterProperty jMeterProperty : prop) {
                try {
                    values.add(unwrapProperty(null, jMeterProperty, Class.forName(type)));
                }
                catch(Exception e) {
                    log.error("Couldn't convert object: {} to {}", prop.getObjectValue(), type, e);
                }
            }
            return values;
        }
        return null;
    }

    /**
     * Utility method that invokes a method and does the error handling around
     * the invocation.
     *
     * @param invokee
     *            the object on which the method should be invoked
     * @param method
     *            the method which should be invoked
     * @param params
     *            the parameters for the method
     * @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 | IllegalAccessException | 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 the {@link PropertyDescriptor} to be checked
     * @return <code>true</code> 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;
    }
}
