/*
 * RHQ Management Platform
 * Copyright 2010-2011, Red Hat Middleware LLC, and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation version 2 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */
package org.rhq.enterprise.gui.coregui.client.components.configuration;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;

import com.smartgwt.client.types.Alignment;
import com.smartgwt.client.types.ListGridEditEvent;
import com.smartgwt.client.types.ListGridFieldType;
import com.smartgwt.client.widgets.IButton;
import com.smartgwt.client.widgets.events.ClickEvent;
import com.smartgwt.client.widgets.form.DynamicForm;
import com.smartgwt.client.widgets.form.fields.ButtonItem;
import com.smartgwt.client.widgets.form.fields.CheckboxItem;
import com.smartgwt.client.widgets.form.fields.FormItem;
import com.smartgwt.client.widgets.form.fields.FormItemIcon;
import com.smartgwt.client.widgets.form.fields.PasswordItem;
import com.smartgwt.client.widgets.form.fields.SelectItem;
import com.smartgwt.client.widgets.form.fields.SpacerItem;
import com.smartgwt.client.widgets.form.fields.StaticTextItem;
import com.smartgwt.client.widgets.form.fields.TextAreaItem;
import com.smartgwt.client.widgets.form.fields.TextItem;
import com.smartgwt.client.widgets.form.fields.events.ChangeEvent;
import com.smartgwt.client.widgets.form.fields.events.ChangeHandler;
import com.smartgwt.client.widgets.form.fields.events.ChangedEvent;
import com.smartgwt.client.widgets.form.fields.events.ChangedHandler;
import com.smartgwt.client.widgets.form.fields.events.ClickHandler;
import com.smartgwt.client.widgets.form.fields.events.FormItemClickHandler;
import com.smartgwt.client.widgets.form.fields.events.FormItemIconClickEvent;
import com.smartgwt.client.widgets.form.validator.Validator;
import com.smartgwt.client.widgets.grid.CellFormatter;
import com.smartgwt.client.widgets.grid.ListGrid;
import com.smartgwt.client.widgets.grid.ListGridField;
import com.smartgwt.client.widgets.grid.ListGridRecord;
import com.smartgwt.client.widgets.grid.events.RowEditorExitEvent;
import com.smartgwt.client.widgets.grid.events.RowEditorExitHandler;

import org.rhq.core.domain.configuration.Configuration;
import org.rhq.core.domain.configuration.Property;
import org.rhq.core.domain.configuration.PropertyList;
import org.rhq.core.domain.configuration.PropertyMap;
import org.rhq.core.domain.configuration.PropertySimple;
import org.rhq.core.domain.configuration.definition.ConfigurationDefinition;
import org.rhq.core.domain.configuration.definition.PropertyDefinition;
import org.rhq.core.domain.configuration.definition.PropertyDefinitionEnumeration;
import org.rhq.core.domain.configuration.definition.PropertyDefinitionList;
import org.rhq.core.domain.configuration.definition.PropertyDefinitionSimple;
import org.rhq.enterprise.gui.coregui.client.CoreGUI;
import org.rhq.enterprise.gui.coregui.client.ImageManager;
import org.rhq.enterprise.gui.coregui.client.PopupWindow;
import org.rhq.enterprise.gui.coregui.client.util.StringUtility;
import org.rhq.enterprise.gui.coregui.client.util.message.Message;
import org.rhq.enterprise.gui.coregui.client.util.message.Message.Severity;
import org.rhq.enterprise.gui.coregui.client.util.selenium.LocatableToolStrip;
import org.rhq.enterprise.gui.coregui.client.util.selenium.LocatableVLayout;

/**
 * A SmartGWT widget for editing a group of RHQ {@link Configuration}s that conform to the same
 * {@link ConfigurationDefinition}.
 *
 * @author Ian Springer
 */
public class GroupConfigurationEditor extends ConfigurationEditor {

    private static final String RHQ_STATIC_ITEM_NAME_ATTRIBUTE = "rhq:staticItemName";
    private static final String RHQ_UNSET_ITEM_NAME_ATTRIBUTE = "rhq:unsetItemName";

    private static final String MEMBER_VALUES_EDITOR_FIELD_PREFIX = "memberValuesEditor-";
    private static final String FIELD_ID = MEMBER_VALUES_EDITOR_FIELD_PREFIX + "idField";
    private static final String FIELD_MEMBER = MEMBER_VALUES_EDITOR_FIELD_PREFIX + "memberField";
    private static final String FIELD_UNSET = MEMBER_VALUES_EDITOR_FIELD_PREFIX + "unsetField";
    private static final String FIELD_VALUE = MEMBER_VALUES_EDITOR_FIELD_PREFIX + "valueField";
    private static final String ATTR_CONFIG_OBJ = "configObject";

    private List<GroupMemberConfiguration> memberConfigurations;

    public GroupConfigurationEditor(String locatorId, ConfigurationDefinition configurationDefinition,
        List<GroupMemberConfiguration> memberConfigurations) {
        super(locatorId, configurationDefinition, buildAggregateConfiguration(memberConfigurations,
            configurationDefinition));
        this.memberConfigurations = memberConfigurations;
    }

    private static Configuration buildAggregateConfiguration(List<GroupMemberConfiguration> memberConfigurations,
        ConfigurationDefinition configurationDefinition) {
        List<Configuration> configurations = new ArrayList<Configuration>(memberConfigurations.size());
        for (GroupMemberConfiguration memberConfiguration : memberConfigurations) {
            Configuration configuration = memberConfiguration.getConfiguration();
            configurations.add(configuration);
        }
        return AggregateConfigurationBuilder.buildAggregateConfiguration(configurations, configurationDefinition);
    }

    @Override
    protected List<FormItem> buildFieldsForPropertySimple(PropertyDefinition propertyDefinition,
        PropertyDefinitionSimple propertyDefinitionSimple, PropertySimple propertySimple) {
        List<FormItem> fields = super.buildFieldsForPropertySimple(propertyDefinition, propertyDefinitionSimple,
            propertySimple);

        if (isAggregateProperty(propertySimple)) {
            // Create the "MEMBER VALUES DIFFER" label that is displayed when member values are heterogeneous.
            final StaticTextItem staticItem = new StaticTextItem("static-" + propertySimple.getName());
            staticItem.setValue(MSG.view_groupConfigEdit_valsDiff());
            staticItem.setTextBoxStyle("InlineNote");
            staticItem.setShowTitle(false);
            staticItem.setTooltip(MSG.view_groupConfigEdit_tooltip_1());
            Boolean isHomogeneous = isHomogeneous(propertySimple);
            staticItem.setVisible(!isHomogeneous);

            FormItem valueItem = fields.get(2);
            FormItemIcon icon = buildEditMemberValuesIcon(propertyDefinitionSimple, propertySimple, valueItem);
            staticItem.setIcons(icon);

            valueItem.setAttribute(RHQ_STATIC_ITEM_NAME_ATTRIBUTE, staticItem.getName());
            fields.add(3, staticItem);
        }

        return fields;
    }

    @Override
    protected List<FormItem> buildFieldsForPropertyList(String locatorId, PropertyDefinition propertyDefinition,
        boolean oddRow, PropertyDefinitionList propertyDefinitionList, PropertyDefinition memberDefinition,
        PropertyList propertyList) {
        List<FormItem> fields = new ArrayList<FormItem>();

        StaticTextItem nameItem = buildNameItem(propertyDefinition);
        fields.add(nameItem);

        StaticTextItem staticTextItem = new StaticTextItem();
        staticTextItem.setShowTitle(false);
        staticTextItem.setValue(MSG.view_groupConfigEdit_noListProps());
        staticTextItem.setColSpan(3);
        staticTextItem.setEndRow(true);
        fields.add(staticTextItem);

        return fields;
    }

    @Override
    protected FormItem buildSimpleField(final PropertyDefinitionSimple propertyDefinitionSimple,
        final PropertySimple propertySimple) {
        final FormItem item = super.buildSimpleField(propertyDefinitionSimple, propertySimple);

        boolean isAggregate = isAggregateProperty(propertySimple);
        if (isAggregate) {
            // Add the icon that user can click to edit the member values.
            FormItemIcon icon = buildEditMemberValuesIcon(propertyDefinitionSimple, propertySimple, item);

            item.setIcons(icon);

            item.addChangedHandler(new ChangedHandler() {
                public void onChanged(ChangedEvent changedEvent) {
                    Object value = changedEvent.getValue();
                    updateMemberProperties(propertySimple, value);
                }
            });

            Boolean isHomogeneous = isHomogeneous(propertySimple);
            item.setVisible(isHomogeneous);
        }

        return item;
    }

    private FormItemIcon buildEditMemberValuesIcon(final PropertyDefinitionSimple propertyDefinitionSimple,
        final PropertySimple propertySimple, final FormItem dynamicItem) {
        FormItemIcon icon = new FormItemIcon();

        icon.setSrc(ImageManager.getEditIcon());
        icon.setName("Edit Member Values");
        icon.setNeverDisable(true);
        icon.addFormItemClickHandler(new FormItemClickHandler() {
            public void onFormItemClick(FormItemIconClickEvent event) {
                // TODO: Pass the actual index, rather than null, if the prop is inside a list.
                displayMemberValuesEditor(extendLocatorId("MemberValuesEditor"), propertyDefinitionSimple,
                    propertySimple, null, dynamicItem);
            }
        });

        // TODO: Figure out a way to add a tooltip to the icon.

        return icon;
    }

    private void updateMemberProperties(PropertySimple propertySimple, Object value) {
        for (GroupMemberConfiguration memberConfiguration : this.memberConfigurations) {
            Configuration configuration = memberConfiguration.getConfiguration();
            PropertySimple memberPropertySimple = (PropertySimple) getProperty(configuration, propertySimple, null);
            memberPropertySimple.setValue(value);
        }
    }

    @Override
    protected FormItem buildUnsetItem(final PropertyDefinitionSimple propertyDefinitionSimple,
        final PropertySimple propertySimple, final FormItem valueItem) {
        final FormItem unsetItem = super.buildUnsetItem(propertyDefinitionSimple, propertySimple, valueItem);
        if (unsetItem instanceof CheckboxItem && !isHomogeneous(propertySimple) && isAggregateProperty(propertySimple)) {
            // non-homogeneous aggregate property (i.e. members have mixed values)
            unsetItem.setValue(false);
            unsetItem.setDisabled(true);
        }
        valueItem.setAttribute(RHQ_UNSET_ITEM_NAME_ATTRIBUTE, unsetItem.getName());
        return unsetItem;
    }

    private void displayMemberValuesEditor(String locatorId, final PropertyDefinitionSimple propertyDefinitionSimple,
        final PropertySimple aggregatePropertySimple, Integer index, final FormItem aggregateValueItem) {

        final PopupWindow popup = new PopupWindow(extendLocatorId("memberValuesEditor"), null);
        popup.setTitle(MSG.view_groupConfigEdit_valsDiffForProp(propertyDefinitionSimple.getName()));
        popup.setWidth(800);
        popup.setHeight(600);

        LocatableVLayout layout = buildMemberEditor(locatorId, propertyDefinitionSimple, aggregatePropertySimple,
            index, aggregateValueItem, popup);

        popup.addItem(layout);
        popup.show();
    }

    private LocatableVLayout buildMemberEditor(String locatorId,
        final PropertyDefinitionSimple propertyDefinitionSimple, final PropertySimple aggregatePropertySimple,
        final Integer index, final FormItem aggregateValueItem, final PopupWindow popup) {

        final boolean propertyIsReadOnly = isReadOnly(propertyDefinitionSimple, aggregatePropertySimple);

        LocatableVLayout layout = new LocatableVLayout(extendLocatorId(locatorId));
        layout.setHeight100();
        layout.setWidth100();
        layout.setMargin(11);

        // create the header strip - will contain "set all values to" form
        LocatableToolStrip headerStrip = null;
        if (!propertyIsReadOnly) {
            headerStrip = new LocatableToolStrip(extendLocatorId("headerStrip"));
            headerStrip.setWidth100();
            headerStrip.setPadding(5);
            headerStrip.setMembersMargin(10);
            layout.addMember(headerStrip);
        }

        // create the list grid that contains all the member resource names and values
        final ListGridRecord[] data = getMemberValuesGridData(this.memberConfigurations, propertyDefinitionSimple,
            aggregatePropertySimple, index);
        final ListGrid memberValuesGrid = new ListGrid();
        memberValuesGrid.setHeight100();
        memberValuesGrid.setWidth100();
        memberValuesGrid.setData(data);
        memberValuesGrid.setEditEvent(ListGridEditEvent.CLICK);
        memberValuesGrid.setEditByCell(false); // selecting any cell allows the user to edit the entire row

        ListGridField idField = new ListGridField(FIELD_ID, MSG.common_title_id());
        idField.setHidden(true);
        idField.setCanEdit(false);

        ListGridField memberField = new ListGridField(FIELD_MEMBER, MSG.view_groupConfigEdit_member());
        memberField.setCanEdit(false);

        final ListGridField valueField = new ListGridField(FIELD_VALUE, MSG.common_title_value());
        valueField.setRequired(propertyDefinitionSimple.isRequired());
        valueField.setCanEdit(true);
        final FormItem editorItem = buildEditorFormItem(valueField, propertyDefinitionSimple);
        valueField.setEditorType(editorItem);
        valueField.setCellFormatter(new CellFormatter() {
            public String format(Object value, ListGridRecord record, int rowNum, int colNum) {
                if (value == null) {
                    return "";
                }
                List<PropertyDefinitionEnumeration> enumeratedValues = propertyDefinitionSimple.getEnumeratedValues();
                if (enumeratedValues != null && !enumeratedValues.isEmpty()) {
                    for (PropertyDefinitionEnumeration option : enumeratedValues) {
                        if (option.getValue().equals(value.toString())) {
                            return option.getName();
                        }
                    }
                    return value.toString();
                } else {
                    switch (propertyDefinitionSimple.getType()) {
                    case BOOLEAN: {
                        return BOOLEAN_PROPERTY_ITEM_VALUE_MAP.get(value.toString());
                    }
                    case PASSWORD: {
                        return "******";
                    }
                    default:
                        return value.toString();
                    }
                }
            }
        });

        ListGridField unsetField = new ListGridField(FIELD_UNSET, MSG.view_groupConfigEdit_unset());
        unsetField.setType(ListGridFieldType.BOOLEAN);
        if (propertyDefinitionSimple.isRequired()) {
            unsetField.setHidden(true);
            unsetField.setCanEdit(false);
        } else {
            unsetField.setHidden(false);
            unsetField.setCanEdit(true);
            unsetField.setCanToggle(false); // otherwise, our handler won't get the form

            // add handler to handle when the user flips the unset bit
            unsetField.addChangedHandler(new com.smartgwt.client.widgets.grid.events.ChangedHandler() {
                public void onChanged(com.smartgwt.client.widgets.grid.events.ChangedEvent event) {
                    Boolean isUnset = (Boolean) event.getValue();
                    FormItem valueItem = event.getForm().getField(FIELD_VALUE);
                    if (isUnset) {
                        int rowNum = event.getRowNum();
                        memberValuesGrid.setEditValue(rowNum, FIELD_VALUE, (String) null);
                    } else {
                        valueItem.focusInItem();
                    }
                    valueItem.redraw();
                }
            });

            // add handler when user clears the value field - this should turn on the "unset" button
            valueField.addChangeHandler(new com.smartgwt.client.widgets.grid.events.ChangeHandler() {
                public void onChange(com.smartgwt.client.widgets.grid.events.ChangeEvent event) {
                    int rowNum = event.getRowNum();
                    if (event.getValue() == null || event.getValue().toString().length() == 0) {
                        memberValuesGrid.setEditValue(rowNum, FIELD_UNSET, true);
                    } else {
                        memberValuesGrid.setEditValue(rowNum, FIELD_UNSET, false);
                    }
                }
            });
        }

        idField.setWidth("75");
        memberField.setWidth("*");
        unsetField.setWidth("50");
        valueField.setWidth("45%");
        memberValuesGrid.setFields(idField, memberField, unsetField, valueField);
        layout.addMember(memberValuesGrid);

        // create the footer strip - will contain ok and cancel buttons
        LocatableToolStrip footerStrip = new LocatableToolStrip(extendLocatorId("footerStrip"));
        footerStrip.setWidth100();
        footerStrip.setPadding(5);
        footerStrip.setMembersMargin(10);
        layout.addMember(footerStrip);

        // footer OK button
        final IButton okButton = new IButton(MSG.common_button_ok());
        if (!propertyIsReadOnly) {
            // if its read-only, allow the ok button to be enabled to just let the user close the window
            // otherwise, disable it now and we will enable it after we know things are ready to be saved
            okButton.setDisabled(true);
        }
        okButton.addClickHandler(new com.smartgwt.client.widgets.events.ClickHandler() {
            public void onClick(ClickEvent clickEvent) {
                if (!propertyIsReadOnly) {
                    boolean valuesHomogeneous = true;
                    boolean isValid = true;

                    String firstValue = data[0].getAttribute(FIELD_VALUE);
                    int i = 0;
                    for (ListGridRecord valueItem : data) {
                        String value = valueItem.getAttribute(FIELD_VALUE);
                        if (valuesHomogeneous) {
                            if ((value != null && !value.equals(firstValue)) || (value == null && firstValue != null)) {
                                valuesHomogeneous = false;
                            }
                        }
                        isValid = isValid && memberValuesGrid.validateRow(i);
                        Configuration config = (Configuration) valueItem.getAttributeAsObject(ATTR_CONFIG_OBJ);

                        PropertySimple memberPropertySimple = (PropertySimple) getProperty(config,
                            aggregatePropertySimple, index);
                        memberPropertySimple.setValue(value);
                        memberPropertySimple.setErrorMessage(null);
                    }

                    String aggregateStaticItemName = aggregateValueItem.getAttribute(RHQ_STATIC_ITEM_NAME_ATTRIBUTE);
                    FormItem aggregateStaticItem = aggregateValueItem.getForm().getField(aggregateStaticItemName);

                    String aggregateUnsetItemName = aggregateValueItem.getAttribute(RHQ_UNSET_ITEM_NAME_ATTRIBUTE);
                    FormItem aggregateUnsetItem = aggregateValueItem.getForm().getField(aggregateUnsetItemName);
                    aggregateUnsetItem.setDisabled(!valuesHomogeneous);

                    if (valuesHomogeneous) {
                        // Update the value of the aggregate property and set its override flag to true.
                        aggregatePropertySimple.setValue(firstValue);
                        aggregatePropertySimple.setOverride(true);

                        boolean isUnset = (firstValue == null);
                        aggregateUnsetItem.setValue(isUnset);

                        // Set the aggregate value item's value to the homogeneous value, unhide it, and enable it
                        // unless it's unset.
                        setValue(aggregateValueItem, firstValue);
                        aggregateValueItem.show();
                        aggregateValueItem.setDisabled(isUnset);

                        aggregateStaticItem.hide();
                    } else {
                        aggregatePropertySimple.setValue(null);
                        aggregatePropertySimple.setOverride(false);

                        aggregateValueItem.hide();

                        aggregateStaticItem.show();
                    }

                    CoreGUI.getMessageCenter().notify(
                        new Message(MSG.view_groupConfigEdit_saveReminder(), Severity.Info));

                    // this has the potential to send another message to the message center
                    firePropertyChangedEvent(aggregatePropertySimple, propertyDefinitionSimple, isValid);
                }

                popup.destroy();
            }
        });
        footerStrip.addMember(okButton);

        // track errors in grid - enable/disable OK button accordingly
        // I tried many ways to get this to work right - this is what I came up with
        memberValuesGrid.addRowEditorExitHandler(new RowEditorExitHandler() {
            public void onRowEditorExit(RowEditorExitEvent event) {
                memberValuesGrid.validateRow(event.getRowNum());
                if (memberValuesGrid.hasErrors()) {
                    okButton.disable();
                } else {
                    okButton.enable();
                }
            }
        });

        // if the data can be changed, add some additional widgets; if not, make the value grid read only
        if (propertyIsReadOnly) {
            memberValuesGrid.setCanEdit(false);
        } else {
            // put a cancel button in the footer strip to allow the user to exit without saving changes
            final IButton cancelButton = new IButton(MSG.common_button_cancel());
            cancelButton.focus();
            cancelButton.addClickHandler(new com.smartgwt.client.widgets.events.ClickHandler() {
                public void onClick(ClickEvent clickEvent) {
                    popup.destroy();
                }
            });
            footerStrip.addMember(cancelButton);

            // create a small form in the header strip to allow the user to apply a value to ALL rows
            final DynamicForm setAllForm = new DynamicForm();
            setAllForm.setNumCols(3);
            setAllForm.setCellPadding(5);
            setAllForm.setColWidths("20%", 50, "*"); // button, unset, value

            // the "set all" value field
            PropertySimple masterPropertySimple = new PropertySimple(propertyDefinitionSimple.getName(), null);
            final FormItem masterValueItem = super.buildSimpleField(propertyDefinitionSimple, masterPropertySimple);
            masterValueItem.setName(MEMBER_VALUES_EDITOR_FIELD_PREFIX + masterValueItem.getName());
            masterValueItem.setValidateOnChange(true);
            masterValueItem.setDisabled(false);
            masterValueItem.setAlign(Alignment.LEFT);
            masterValueItem.setStartRow(false);
            masterValueItem.setEndRow(true);

            // the "set all" unset field
            FormItem masterUnsetItem;
            if (!propertyDefinitionSimple.isRequired()) {
                final CheckboxItem unsetItem = new CheckboxItem(MEMBER_VALUES_EDITOR_FIELD_PREFIX + "unsetall");
                boolean unset = (masterPropertySimple == null || masterPropertySimple.getStringValue() == null);
                unsetItem.setValue(unset);
                unsetItem.setDisabled(isReadOnly(propertyDefinitionSimple, masterPropertySimple));
                unsetItem.setShowLabel(false);
                unsetItem.setLabelAsTitle(false);

                unsetItem.addChangedHandler(new ChangedHandler() {
                    public void onChanged(ChangedEvent changedEvent) {
                        Boolean isUnset = (Boolean) changedEvent.getValue();
                        masterValueItem.setDisabled(isUnset);
                        if (isUnset) {
                            setValue(masterValueItem, null);
                        }
                        masterValueItem.redraw();
                    }
                });

                masterValueItem.addChangeHandler(new ChangeHandler() {
                    public void onChange(ChangeEvent event) {
                        if (event.getValue() == null || event.getValue().toString().length() == 0) {
                            unsetItem.setValue(true);
                        } else {
                            unsetItem.setValue(false);
                        }
                    }
                });

                masterUnsetItem = unsetItem;
            } else {
                masterUnsetItem = new SpacerItem();
            }

            masterUnsetItem.setShowTitle(false);
            masterUnsetItem.setStartRow(false);
            masterUnsetItem.setEndRow(false);
            masterUnsetItem.setAlign(Alignment.CENTER);
            if (masterUnsetItem instanceof CheckboxItem) {
                masterUnsetItem.setValue(false);
            } else {
                masterUnsetItem.setVisible(false);
            }

            // the "set all values to" apply button
            final ButtonItem applyButton = new ButtonItem("apply", MSG.view_groupConfigEdit_setAll());
            applyButton.setDisabled(true);
            applyButton.setAlign(Alignment.RIGHT);
            applyButton.setStartRow(true);
            applyButton.setEndRow(false);

            applyButton.addClickHandler(new ClickHandler() {
                public void onClick(com.smartgwt.client.widgets.form.fields.events.ClickEvent event) {
                    if (masterValueItem.validate()) {
                        Object value = masterValueItem.getValue();

                        // Update the member property value items.
                        for (ListGridRecord field : data) {
                            String stringValue = (value != null) ? value.toString() : null;
                            field.setAttribute(FIELD_VALUE, stringValue);
                            field.setAttribute(FIELD_UNSET, value == null);
                        }

                        // since we know the master value is ok and has validated,
                        // we can clear all validation errors and re-enable the OK button
                        memberValuesGrid.discardAllEdits();
                        okButton.enable();

                        // redraw grid with the new values
                        memberValuesGrid.redraw();
                    }
                }
            });

            masterValueItem.addChangedHandler(new ChangedHandler() {
                public void onChanged(ChangedEvent changedEvent) {
                    applyButton.enable();
                }
            });

            masterUnsetItem.addChangedHandler(new ChangedHandler() {
                public void onChanged(ChangedEvent changedEvent) {
                    applyButton.enable();
                    if ((Boolean) changedEvent.getValue()) {
                        applyButton.focusInItem(); // unsetting value, nothing to enter so go to apply button
                    } else {
                        masterValueItem.focusInItem(); // if turning off "unset", go to value field to enter a value to set
                    }
                }
            });

            setAllForm.setFields(applyButton, masterUnsetItem, masterValueItem);
            headerStrip.addMember(setAllForm);
        }

        return layout;
    }

    @Override
    protected boolean shouldFireEventOnPropertyValueChange(FormItem formItem,
        PropertyDefinitionSimple propertyDefinitionSimple, PropertySimple propertySimple) {
        if (formItem.getName().startsWith(MEMBER_VALUES_EDITOR_FIELD_PREFIX)) {
            return false; // this is a form item in our member values editor dialog - don't fire event for it
        }
        boolean isAgg = isAggregateProperty(propertySimple);
        return isAgg && super.shouldFireEventOnPropertyValueChange(formItem, propertyDefinitionSimple, propertySimple);
    }

    @Override
    protected void updatePropertySimpleValue(FormItem formItem, Object value, PropertySimple propertySimple,
        PropertyDefinitionSimple propertyDefinitionSimple) {
        if ((propertySimple.getConfiguration() != null) || (propertySimple.getParentMap() != null)
            || (propertySimple.getParentList() != null)) {
            // Update the aggregate property. this could also be a member values editor field - but we do the same thing
            super.updatePropertySimpleValue(formItem, value, propertySimple, propertyDefinitionSimple);
            propertySimple.setOverride(true);

            // Update all the member properties if this is a true aggregate property
            if (!formItem.getName().startsWith(MEMBER_VALUES_EDITOR_FIELD_PREFIX)) {
                for (GroupMemberConfiguration memberConfiguration : this.memberConfigurations) {
                    Configuration configuration = memberConfiguration.getConfiguration();
                    PropertySimple memberPropertySimple = (PropertySimple) getProperty(configuration, propertySimple,
                        null);
                    memberPropertySimple.setErrorMessage(null);
                    memberPropertySimple.setValue(value);
                }
            }
        }
    }

    @Override
    protected void removePropertyFromDynamicMap(PropertySimple propertySimple) {
        // Remove the aggregate property.
        super.removePropertyFromDynamicMap(propertySimple);

        // Remove the member properties.
        for (GroupMemberConfiguration memberConfiguration : this.memberConfigurations) {
            Configuration configuration = memberConfiguration.getConfiguration();
            PropertySimple memberPropertySimple = (PropertySimple) getProperty(configuration, propertySimple, null);
            PropertyMap parentMap = memberPropertySimple.getParentMap();
            parentMap.getMap().remove(memberPropertySimple.getName());
        }
    }

    @Override
    protected void addPropertyToDynamicMap(PropertySimple propertySimple, PropertyMap propertyMap) {
        // Add the aggregate property.
        super.addPropertyToDynamicMap(propertySimple, propertyMap);

        // Add the member properties.
        for (GroupMemberConfiguration memberConfiguration : this.memberConfigurations) {
            Configuration configuration = memberConfiguration.getConfiguration();
            // The below call will create the member property and add it to the appropriate parent property in the member config.
            PropertySimple memberPropertySimple = (PropertySimple) getProperty(configuration, propertySimple, null);
        }
    }

    private Property getProperty(Configuration configuration, Property referenceProperty, Integer index) {
        LinkedList<Property> propertyHierarchy = new LinkedList<Property>();
        Property currentProperty = referenceProperty;
        propertyHierarchy.add(currentProperty);
        do {
            if (currentProperty.getParentMap() != null) {
                currentProperty = currentProperty.getParentMap();
            } else if (currentProperty.getParentList() != null) {
                currentProperty = currentProperty.getParentList();
            } else if (currentProperty.getConfiguration() == null) {
                throw new IllegalStateException(currentProperty + " has no parent.");
            }
            propertyHierarchy.addFirst(currentProperty);
        } while (currentProperty.getConfiguration() == null);

        Property property = configuration.get(propertyHierarchy.get(0).getName());
        for (int i = 1, propertyHierarchySize = propertyHierarchy.size(); i < propertyHierarchySize; i++) {
            String childPropertyName = propertyHierarchy.get(i).getName();
            if (property instanceof PropertyMap) {
                PropertyMap propertyMap = (PropertyMap) property;
                property = propertyMap.get(childPropertyName);
                if (property == null) {
                    property = new PropertySimple(childPropertyName, null);
                    propertyMap.put(property);
                }
            } else if (property instanceof PropertyList) {
                PropertyList propertyList = (PropertyList) property;
                if (index < propertyList.getList().size()) {
                    property = propertyList.getList().get(index);
                } else {
                    property = new PropertySimple(childPropertyName, null);
                    propertyList.add(property);
                }
            }
        }

        return property;
    }

    private boolean isAggregateProperty(PropertySimple propertySimple) {
        return (getConfiguration(propertySimple) == getConfiguration());
    }

    private static Boolean isHomogeneous(PropertySimple propertySimple) {
        Boolean override = propertySimple.getOverride();
        return (override != null && override);
    }

    private static Configuration getConfiguration(Property property) {
        Property topLevelProperty = getTopLevelProperty(property);
        return topLevelProperty.getConfiguration();
    }

    private ListGridRecord[] getMemberValuesGridData(List<GroupMemberConfiguration> memberConfigs,
        PropertyDefinitionSimple propDef, PropertySimple aggregateProp, Integer propertyIndex) {

        ListGridRecord[] records;
        if (memberConfigs == null || memberConfigs.isEmpty()) {
            records = new ListGridRecord[0];
        } else {
            records = new ListGridRecord[memberConfigs.size()];
            int i = 0;
            for (GroupMemberConfiguration item : memberConfigs) {
                ListGridRecord record = new ListGridRecord();
                record.setAttribute(FIELD_ID, item.getId());
                record.setAttribute(FIELD_MEMBER, item.getLabel());

                Configuration configuration = item.getConfiguration();
                PropertySimple memberProp = (PropertySimple) getProperty(configuration, aggregateProp, propertyIndex);
                if (!propDef.isRequired()) {
                    boolean isUnset = (memberProp == null || memberProp.getStringValue() == null);
                    record.setAttribute(FIELD_UNSET, isUnset);
                }

                String value = memberProp.getStringValue();
                if (null == value && null != propDef.getDefaultValue()) {
                    value = propDef.getDefaultValue();
                }
                if (!propDef.isReadOnly()) {
                    // user-editable, so escape HTML, to prevent XSS attacks.
                    value = StringUtility.escapeHtml(value);
                }
                record.setAttribute(FIELD_VALUE, value);
                record.setAttribute(ATTR_CONFIG_OBJ, configuration);
                records[i++] = record;
            }
        }
        return records;
    }

    /**
     * Creates the editor type for the given value field. Validators will be added appropriately.
     *
     * @param valueField the field that is editable
     * @param propDef the definition of the property to be edited in the field
     * @return the editor item
     */
    protected FormItem buildEditorFormItem(ListGridField valueField, final PropertyDefinitionSimple propDef) {
        FormItem editorItem = null;

        // note: we don't have to worry about "read only" properties - if the members
        // value editor is in read-only mode or viewing read-only properties, the list grid
        // isn't editable anyway.

        List<PropertyDefinitionEnumeration> enumeratedValues = propDef.getEnumeratedValues();
        if (enumeratedValues != null && !enumeratedValues.isEmpty()) {
            LinkedHashMap<String, String> valueOptions = new LinkedHashMap<String, String>();
            for (PropertyDefinitionEnumeration option : enumeratedValues) {
                valueOptions.put(option.getValue(), option.getName());
            }

            if (valueOptions.size() > 5) {
                editorItem = new SelectItem();
            } else {
                // TODO: we want RadioGroupItem, but smartgwt seems to have a bug and it won't render this when
                //       our listgrid does not have the "unset" boolean field also present. If its just the value
                //       field, the radio group editor won't show.
                editorItem = new SelectItem(); // new RadioGroupItem();
            }
            editorItem.setValueMap(valueOptions);
        } else {
            switch (propDef.getType()) {
            case STRING:
            case FILE:
            case DIRECTORY:
                editorItem = new TextItem();
                break;
            case LONG_STRING:
                editorItem = new TextAreaItem();
                break;
            case PASSWORD:
                editorItem = new PasswordItem();
                break;
            case BOOLEAN:
                // TODO: we want RadioGroupItem, but smartgwt seems to have a bug and it won't render this when
                //       our listgrid does not have the "unset" boolean field also present. If its just the value
                //       field, the radio group editor won't show.
                // RadioGroupItem radioGroupItem = new RadioGroupItem();
                // radioGroupItem.setVertical(false);
                // editorItem = radioGroupItem;
                editorItem = new SelectItem();
                editorItem.setValueMap(BOOLEAN_PROPERTY_ITEM_VALUE_MAP);
                break;
            case INTEGER:
            case LONG:
                editorItem = new TextItem(); // could not get smartgwt listgrid editing to work with IntegerItem
                break;
            case FLOAT:
            case DOUBLE:
                editorItem = new TextItem(); // could not get smartgwt listgrid editing to work with FloatItem
                break;
            }
        }

        editorItem.setName(MEMBER_VALUES_EDITOR_FIELD_PREFIX + "editor");

        List<Validator> validators = buildValidators(propDef, new PropertySimple());
        valueField.setValidators(validators.toArray(new Validator[validators.size()]));

        return editorItem;
    }
}
