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

import java.awt.BorderLayout;
import java.awt.Component;
import java.beans.BeanDescriptor;
import java.beans.BeanInfo;
import java.beans.Customizer;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.beans.PropertyEditorManager;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.swing.JPopupMenu;

import org.apache.commons.collections.map.LRUMap;
import org.apache.jmeter.assertions.Assertion;
import org.apache.jmeter.assertions.gui.AbstractAssertionGui;
import org.apache.jmeter.config.ConfigElement;
import org.apache.jmeter.config.ConfigTestElement;
import org.apache.jmeter.config.gui.AbstractConfigGui;
import org.apache.jmeter.control.Controller;
import org.apache.jmeter.control.gui.AbstractControllerGui;
import org.apache.jmeter.gui.AbstractJMeterGuiComponent;
import org.apache.jmeter.gui.JMeterGUIComponent;
import org.apache.jmeter.gui.util.MenuFactory;
import org.apache.jmeter.processor.PostProcessor;
import org.apache.jmeter.processor.PreProcessor;
import org.apache.jmeter.processor.gui.AbstractPostProcessorGui;
import org.apache.jmeter.processor.gui.AbstractPreProcessorGui;
import org.apache.jmeter.reporters.AbstractListenerElement;
import org.apache.jmeter.samplers.Sampler;
import org.apache.jmeter.samplers.gui.AbstractSamplerGui;
import org.apache.jmeter.testbeans.BeanInfoSupport;
import org.apache.jmeter.testbeans.TestBean;
import org.apache.jmeter.testelement.TestElement;
import org.apache.jmeter.testelement.property.AbstractProperty;
import org.apache.jmeter.testelement.property.JMeterProperty;
import org.apache.jmeter.testelement.property.PropertyIterator;
import org.apache.jmeter.timers.Timer;
import org.apache.jmeter.timers.gui.AbstractTimerGui;
import org.apache.jmeter.util.JMeterUtils;
import org.apache.jmeter.visualizers.Visualizer;
import org.apache.jmeter.visualizers.gui.AbstractVisualizer;
import org.apache.jorphan.logging.LoggingManager;
import org.apache.jorphan.util.JOrphanUtils;
import org.apache.log.Logger;

/**
 * JMeter GUI element editing for TestBean elements.
 * <p>
 * The actual GUI is always a bean customizer: if the bean descriptor provides
 * one, it will be used; otherwise, a GenericTestBeanCustomizer will be created
 * for this purpose.
 * <p>
 * Those customizers deviate from the standards only in that, instead of a bean,
 * they will receive a Map in the setObject call. This will be a property name
 * to value Map. The customizer is also in charge of initializing empty Maps
 * with sensible initial values.
 * <p>
 * If the provided Customizer class implements the SharedCustomizer interface,
 * the same instance of the customizer will be reused for all beans of the type:
 * setObject(map) can then be called multiple times. Otherwise, one separate
 * instance will be used for each element. For efficiency reasons, most
 * customizers should implement SharedCustomizer.
 * 
 */
public class TestBeanGUI extends AbstractJMeterGuiComponent implements JMeterGUIComponent {
	private static final Logger log = LoggingManager.getLoggerForClass();

	private Class testBeanClass;

	private transient BeanInfo beanInfo;

	private Class customizerClass;

	/**
	 * The single customizer if the customizer class implements
	 * SharedCustomizer, null otherwise.
	 */
	private Customizer customizer = null;

	/**
	 * TestElement to Customizer map if customizer is null. This is necessary to
	 * avoid the cost of creating a new customizer on each edit. The cache size
	 * needs to be limited, though, to avoid memory issues when editing very
	 * large test plans.
	 */
	private Map customizers = new LRUMap(20);

	/**
	 * Index of the customizer in the JPanel's child component list:
	 */
	private int customizerIndexInPanel;

	/**
	 * The property name to value map that the active customizer edits:
	 */
	private Map propertyMap = new HashMap();

	/**
	 * Whether the GUI components have been created.
	 */
	private boolean initialized = false;

	static {
		List paths = new LinkedList();
		paths.add("org.apache.jmeter.testbeans.gui");// $NON-NLS-1$
		paths.addAll(Arrays.asList(PropertyEditorManager.getEditorSearchPath()));
		String s = JMeterUtils.getPropDefault("propertyEditorSearchPath", null);// $NON-NLS-1$
		if (s != null) {
			paths.addAll(Arrays.asList(JOrphanUtils.split(s, ",", "")));// $NON-NLS-1$ // $NON-NLS-2$
		}
		PropertyEditorManager.setEditorSearchPath((String[]) paths.toArray(new String[0]));
	}

	// Dummy for JUnit test
	public TestBeanGUI() {
		log.warn("Constructor only for use in testing");// $NON-NLS-1$
	}

	public TestBeanGUI(Class testBeanClass) {
		super();
		log.debug("testing class: " + testBeanClass.getName());
		// A quick verification, just in case:
		if (!TestBean.class.isAssignableFrom(testBeanClass)) {
			Error e = new Error();
			log.error("This should never happen!", e);
			throw e; // Programming error: bail out.
		}

		this.testBeanClass = testBeanClass;

		// Get the beanInfo:
		try {
			beanInfo = Introspector.getBeanInfo(testBeanClass);
		} catch (IntrospectionException e) {
			log.error("Can't get beanInfo for " + testBeanClass.getName(), e);
			throw new Error(e.toString()); // Programming error. Don't
											// continue.
		}

		customizerClass = beanInfo.getBeanDescriptor().getCustomizerClass();

		// Creation of the customizer and GUI initialization is delayed until
		// the
		// first
		// configure call. We don't need all that just to find out the static
		// label, menu
		// categories, etc!
		initialized = false;
	}

	private Customizer createCustomizer() {
		try {
			return (Customizer) customizerClass.newInstance();
		} catch (InstantiationException e) {
			log.error("Could not instantiate customizer of class " + customizerClass, e);
			throw new Error(e.toString());
		} catch (IllegalAccessException e) {
			log.error("Could not instantiate customizer of class " + customizerClass, e);
			throw new Error(e.toString());
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.apache.jmeter.gui.JMeterGUIComponent#getStaticLabel()
	 */
	public String getStaticLabel() {
		if (beanInfo == null){
		    return "null";// $NON-NLS-1$
		}
		return beanInfo.getBeanDescriptor().getDisplayName();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.apache.jmeter.gui.JMeterGUIComponent#createTestElement()
	 */
	public TestElement createTestElement() {
		try {
			TestElement element = (TestElement) testBeanClass.newInstance();
			// configure(element);
			// super.clear(); // set name, enabled.
			modifyTestElement(element); // put the default values back into the
			// new element
			return element;
		} catch (InstantiationException e) {
			log.error("Can't create test element", e);
			throw new Error(e.toString()); // Programming error. Don't
											// continue.
		} catch (IllegalAccessException e) {
			log.error("Can't create test element", e);
			throw new Error(e.toString()); // Programming error. Don't
											// continue.
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.apache.jmeter.gui.JMeterGUIComponent#modifyTestElement(org.apache.jmeter.testelement.TestElement)
	 */
	public void modifyTestElement(TestElement element) {
		// Fetch data from screen fields
		if (customizer instanceof GenericTestBeanCustomizer) {
			GenericTestBeanCustomizer gtbc = (GenericTestBeanCustomizer) customizer;
			gtbc.saveGuiFields(); 
		}
		configureTestElement(element);

		// Copy all property values from the map into the element:
		PropertyDescriptor[] props = beanInfo.getPropertyDescriptors();
		for (int i = 0; i < props.length; i++) {
			String name = props[i].getName();
			Object value = propertyMap.get(name);
			log.debug("Modify " + name + " to " + value);
			if (value == null) {
				Object valueNotUnDefined = props[i].getValue(BeanInfoSupport.NOT_UNDEFINED);
				if (valueNotUnDefined != null && ((Boolean) valueNotUnDefined).booleanValue()) {
					setPropertyInElement(element, name, props[i].getValue(BeanInfoSupport.DEFAULT));
				} else {
					element.removeProperty(name);
				}
			} else {
				setPropertyInElement(element, name, propertyMap.get(name));
			}
		}
	}

	/**
	 * @param element
	 * @param name
	 */
	private void setPropertyInElement(TestElement element, String name, Object value) {
		JMeterProperty jprop = AbstractProperty.createProperty(value);
		jprop.setName(name);
		element.setProperty(jprop);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.apache.jmeter.gui.JMeterGUIComponent#createPopupMenu()
	 */
	public JPopupMenu createPopupMenu() {
		// TODO: this menu is too wide (allows, e.g. to add controllers, no
		// matter what the type of the element).
        // Change to match the actual bean's capabilities.
		if (Timer.class.isAssignableFrom(testBeanClass))// HACK: Fix one such problem
		{
			return MenuFactory.getDefaultTimerMenu();
		}
        else if(Sampler.class.isAssignableFrom(testBeanClass))
        {
            return MenuFactory.getDefaultSamplerMenu();
        }
        else if(ConfigTestElement.class.isAssignableFrom(testBeanClass))
        {
            return MenuFactory.getDefaultConfigElementMenu();
        }
        else if(Assertion.class.isAssignableFrom(testBeanClass))
        {
            return MenuFactory.getDefaultAssertionMenu();
        }
        else if(PostProcessor.class.isAssignableFrom(testBeanClass) || 
                PreProcessor.class.isAssignableFrom(testBeanClass))
        {
            return MenuFactory.getDefaultExtractorMenu();
        }
        else if(AbstractListenerElement.class.isAssignableFrom(testBeanClass))
        {
            return MenuFactory.getDefaultVisualizerMenu();
        }
        else {
            return MenuFactory.getDefaultControllerMenu();
        }
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.apache.jmeter.gui.JMeterGUIComponent#configure(org.apache.jmeter.testelement.TestElement)
	 */
	public void configure(TestElement element) {
		if (!initialized){
		    init();
		}
		clearGui();

		super.configure(element);

		// Copy all property values into the map:
		for (PropertyIterator jprops = element.propertyIterator(); jprops.hasNext();) {
			JMeterProperty jprop = jprops.next();
			propertyMap.put(jprop.getName(), jprop.getObjectValue());
		}

		if (customizer != null) {
			customizer.setObject(propertyMap);
		} else {
			if (initialized){
			    remove(customizerIndexInPanel);
			}
			Customizer c = (Customizer) customizers.get(element);
			if (c == null) {
				c = createCustomizer();
				c.setObject(propertyMap);
				customizers.put(element, c);
			}
			add((Component) c, BorderLayout.CENTER);
		}

		initialized = true;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.apache.jmeter.gui.JMeterGUIComponent#getMenuCategories()
	 */
	public Collection getMenuCategories() {
		List menuCategories = new LinkedList();
		BeanDescriptor bd = beanInfo.getBeanDescriptor();

		// We don't want to show expert beans in the menus unless we're
		// in expert mode:
		if (bd.isExpert() && !JMeterUtils.isExpertMode()) {
			return null;
		}

		int matches = 0; // How many classes can we assign from?
		// TODO: there must be a nicer way...
		if (Assertion.class.isAssignableFrom(testBeanClass)) {
			menuCategories.add(MenuFactory.ASSERTIONS);
			bd.setValue(TestElement.GUI_CLASS, AbstractAssertionGui.class.getName());
			matches++;
		}
		if (ConfigElement.class.isAssignableFrom(testBeanClass)) {
			menuCategories.add(MenuFactory.CONFIG_ELEMENTS);
			bd.setValue(TestElement.GUI_CLASS, AbstractConfigGui.class.getName());
			matches++;
		}
		if (Controller.class.isAssignableFrom(testBeanClass)) {
			menuCategories.add(MenuFactory.CONTROLLERS);
			bd.setValue(TestElement.GUI_CLASS, AbstractControllerGui.class.getName());
			matches++;
		}
		if (Visualizer.class.isAssignableFrom(testBeanClass)) {
			menuCategories.add(MenuFactory.LISTENERS);
			bd.setValue(TestElement.GUI_CLASS, AbstractVisualizer.class.getName());
			matches++;
		}
		if (PostProcessor.class.isAssignableFrom(testBeanClass)) {
			menuCategories.add(MenuFactory.POST_PROCESSORS);
			bd.setValue(TestElement.GUI_CLASS, AbstractPostProcessorGui.class.getName());
			matches++;
		}
		if (PreProcessor.class.isAssignableFrom(testBeanClass)) {
			matches++;
			menuCategories.add(MenuFactory.PRE_PROCESSORS);
			bd.setValue(TestElement.GUI_CLASS, AbstractPreProcessorGui.class.getName());
		}
		if (Sampler.class.isAssignableFrom(testBeanClass)) {
			matches++;
			menuCategories.add(MenuFactory.SAMPLERS);
			bd.setValue(TestElement.GUI_CLASS, AbstractSamplerGui.class.getName());
		}
		if (Timer.class.isAssignableFrom(testBeanClass)) {
			matches++;
			menuCategories.add(MenuFactory.TIMERS);
			bd.setValue(TestElement.GUI_CLASS, AbstractTimerGui.class.getName());
		}
		if (matches == 0) {
			log.error("Could not assign GUI class to " + testBeanClass.getName());
		} else if (matches > 1) {// may be impossible, but no harm in
									// checking ...
			log.error("More than 1 GUI class found for " + testBeanClass.getName());
		}
		return menuCategories;
	}

	private void init() {
		setLayout(new BorderLayout(0, 5));

		setBorder(makeBorder());
		add(makeTitlePanel(), BorderLayout.NORTH);

		customizerIndexInPanel = getComponentCount();

		if (customizerClass == null) {
			customizer = new GenericTestBeanCustomizer(beanInfo);
		} else if (SharedCustomizer.class.isAssignableFrom(customizerClass)) {
			customizer = createCustomizer();
		}

		if (customizer != null){
		    add((Component) customizer, BorderLayout.CENTER);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.apache.jmeter.gui.JMeterGUIComponent#getLabelResource()
	 */
	public String getLabelResource() {
		// @see getStaticLabel
		return null;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.apache.jmeter.gui.JMeterGUIComponent#clearGui()
	 */
	public void clearGui() {
		super.clearGui();
		if (customizer instanceof GenericTestBeanCustomizer) {
			GenericTestBeanCustomizer gtbc = (GenericTestBeanCustomizer) customizer;
			gtbc.clearGuiFields(); 
		}
	}
}
