/*
 * RHQ Management Platform
 * Copyright (C) 2005-2010 Red Hat, Inc.
 * All rights reserved.
 *
 * 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.core.gui.configuration.propset;

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

import org.rhq.core.domain.configuration.AbstractPropertyMap;
import org.rhq.core.domain.configuration.Configuration;
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.PropertyDefinitionList;
import org.rhq.core.domain.configuration.definition.PropertyDefinitionMap;
import org.rhq.core.domain.configuration.definition.PropertyDefinitionSimple;

/**
 * @author Ian Springer
 */
public class ConfigurationSet {
    private ConfigurationDefinition configurationDefinition;
    private List<ConfigurationSetMember> members;
    private Configuration groupConfiguration;

    public ConfigurationSet(ConfigurationDefinition configurationDefinition, List<ConfigurationSetMember> members) {
        if (configurationDefinition == null)
            throw new IllegalArgumentException("configurationDefinition parameter is null.");
        this.configurationDefinition = configurationDefinition;
        if (members == null)
            throw new IllegalArgumentException("members parameter is null.");
        this.members = members;
        this.groupConfiguration = new Configuration();
        calculateGroupConfiguration();
    }

    public void calculateGroupConfiguration() {
        this.groupConfiguration.getMap().clear();
        if (this.members.isEmpty())
            return;
        Map<String, PropertyDefinition> childPropertyDefinitions = this.configurationDefinition
            .getPropertyDefinitions();
        List<AbstractPropertyMap> sourceParentPropertyMaps = new ArrayList<AbstractPropertyMap>();
        for (ConfigurationSetMember member : this.members)
            sourceParentPropertyMaps.add(member.getConfiguration());
        for (PropertyDefinition childPropertyDefinition : childPropertyDefinitions.values())
            calculateGroupProperty(childPropertyDefinition, sourceParentPropertyMaps, this.groupConfiguration);
    }

    public void applyGroupConfiguration() {
        if (this.members.isEmpty())
            return;
        Map<String, PropertyDefinition> childPropertyDefinitions = this.configurationDefinition
            .getPropertyDefinitions();
        List<AbstractPropertyMap> sourceParentPropertyMaps = new ArrayList<AbstractPropertyMap>();
        for (ConfigurationSetMember member : this.members)
            sourceParentPropertyMaps.add(member.getConfiguration());
        for (PropertyDefinition childPropertyDefinition : childPropertyDefinitions.values())
            mergeProperty(childPropertyDefinition, sourceParentPropertyMaps, this.groupConfiguration);
    }

    public ConfigurationDefinition getConfigurationDefinition() {
        return configurationDefinition;
    }

    public List<ConfigurationSetMember> getMembers() {
        return members;
    }

    public Configuration getGroupConfiguration() {
        return groupConfiguration;
    }

    private static void calculateGroupProperty(PropertyDefinition propertyDefinition,
        List<AbstractPropertyMap> sourceParentPropertyMaps, AbstractPropertyMap targetParentPropertyMap) {
        if (propertyDefinition instanceof PropertyDefinitionSimple) {
            String sampleValue = getSimpleValue(sourceParentPropertyMaps.get(0), propertyDefinition.getName());
            boolean valuesHomogenous = true;
            for (int i = 1; i < sourceParentPropertyMaps.size(); i++) {
                String value = getSimpleValue(sourceParentPropertyMaps.get(i), propertyDefinition.getName());
                if ((value == null && sampleValue != null) || (value != null && !value.equals(sampleValue))) {
                    valuesHomogenous = false;
                    break;
                }
            }
            PropertySimple propertySimple = new PropertySimple(propertyDefinition.getName(),
                (valuesHomogenous) ? sampleValue : null);
            targetParentPropertyMap.put(propertySimple);
            if (valuesHomogenous)
                // Set override to true so the config renderer will know the prop is homogenous.
                propertySimple.setOverride(true);
        }
        // If the property is a Map, recurse into it and group together its child properties.
        else if (propertyDefinition instanceof PropertyDefinitionMap) {
            List<AbstractPropertyMap> nestedSourceParentPropertyMaps = new ArrayList<AbstractPropertyMap>();
            for (AbstractPropertyMap sourceParentPropertyMap : sourceParentPropertyMaps) {
                PropertyMap nestedSourceParentPropertyMap = sourceParentPropertyMap
                    .getMap(propertyDefinition.getName());
                nestedSourceParentPropertyMaps
                    .add((nestedSourceParentPropertyMap != null) ? nestedSourceParentPropertyMap : new PropertyMap(
                        propertyDefinition.getName()));
            }
            PropertyMap targetPropertyMap = new PropertyMap(propertyDefinition.getName());
            targetParentPropertyMap.put(targetPropertyMap);
            calculateGroupPropertyMap((PropertyDefinitionMap) propertyDefinition, nestedSourceParentPropertyMaps,
                targetPropertyMap);
        } else if (propertyDefinition instanceof PropertyDefinitionList) {
            PropertyDefinitionList propertyDefinitionList = (PropertyDefinitionList) propertyDefinition;
            PropertyDefinition listMemberPropertyDefinition = propertyDefinitionList.getMemberDefinition();
            PropertyList targetPropertyList = new PropertyList(propertyDefinition.getName());
            targetParentPropertyMap.put(targetPropertyList);
            if (listMemberPropertyDefinition instanceof PropertyDefinitionMap) {
                PropertyDefinitionMap propertyDefinitionMap = (PropertyDefinitionMap) listMemberPropertyDefinition;
                // TODO: How do we group together Lists of Maps? Not trivial...
            }
        }
    }

    private static void calculateGroupPropertyMap(PropertyDefinitionMap propertyDefinitionMap,
        List<AbstractPropertyMap> sourceParentPropertyMaps, AbstractPropertyMap targetParentPropertyMap) {
        Map<String, PropertyDefinition> childPropertyDefinitions = propertyDefinitionMap.getPropertyDefinitions();
        if (!childPropertyDefinitions.isEmpty()) {
            for (PropertyDefinition childPropertyDefinition : childPropertyDefinitions.values())
                calculateGroupProperty(childPropertyDefinition, sourceParentPropertyMaps, targetParentPropertyMap);
        } else {
            calculateGroupOpenPropertyMap(sourceParentPropertyMaps, targetParentPropertyMap);
        }
    }

    private static void calculateGroupOpenPropertyMap(List<AbstractPropertyMap> sourceParentPropertyMaps,
        AbstractPropertyMap targetParentPropertyMap) {
        Map<String, Map<String, Integer>> memberNameValueFrequenciesMap = createMemberNameValueFrequenciesMap(sourceParentPropertyMaps);
        for (String memberName : memberNameValueFrequenciesMap.keySet()) {
            // Add each unique member to the target map, so the renderer will be able to display it on the main
            // config page.
            PropertySimple member = new PropertySimple(memberName, null);
            targetParentPropertyMap.put(member);
            // Also add it to each of the source config maps that don't already contain it, so that they can be
            // rendered as unset on the propSet page.
            for (AbstractPropertyMap map : sourceParentPropertyMaps) {
                if (map.get(memberName) == null)
                    map.put(new PropertySimple(memberName, null));
            }
            Map<String, Integer> valueFrequencies = memberNameValueFrequenciesMap.get(memberName);
            if (valueFrequencies.size() == 1
                && valueFrequencies.values().iterator().next() == sourceParentPropertyMaps.size()) {
                // Set override to true so the renderers will know the prop is homogenous.
                member.setOverride(true);
                // And set the value, so it can be displayed on the main config page.
                member.setStringValue(valueFrequencies.keySet().iterator().next());
            }
        }
    }

    private static void mergeProperty(PropertyDefinition propertyDefinition,
        List<AbstractPropertyMap> memberParentPropertyMaps, AbstractPropertyMap groupParentPropertyMap) {
        if (propertyDefinition instanceof PropertyDefinitionSimple) {
            PropertySimple propertySimple = groupParentPropertyMap.getSimple(propertyDefinition.getName());
            if (propertySimple != null && propertySimple.getOverride() != null && propertySimple.getOverride()) {
                for (AbstractPropertyMap sourceParentPropertyMap : memberParentPropertyMaps) {
                    PropertySimple sourcePropertySimple = sourceParentPropertyMap.getSimple(propertyDefinition
                        .getName());
                    if (sourcePropertySimple == null) {
                        sourcePropertySimple = new PropertySimple(propertyDefinition.getName(), propertySimple
                            .getStringValue());
                        sourceParentPropertyMap.put(sourcePropertySimple);
                    } else {
                        sourcePropertySimple.setStringValue(propertySimple.getStringValue());
                    }
                }
            }
        }
        // If the property is a Map, recurse into it and merge its child properties.
        else if (propertyDefinition instanceof PropertyDefinitionMap) {
            List<AbstractPropertyMap> nestedSourceParentPropertyMaps = new ArrayList<AbstractPropertyMap>();
            for (AbstractPropertyMap sourceParentPropertyMap : memberParentPropertyMaps)
                nestedSourceParentPropertyMaps.add(sourceParentPropertyMap.getMap(propertyDefinition.getName()));
            PropertyMap groupPropertyMap = groupParentPropertyMap.getMap(propertyDefinition.getName());
            groupParentPropertyMap.put(groupPropertyMap);
            mergePropertyMap((PropertyDefinitionMap) propertyDefinition, nestedSourceParentPropertyMaps,
                groupPropertyMap);
        } else if (propertyDefinition instanceof PropertyDefinitionList) {
            PropertyDefinitionList propertyDefinitionList = (PropertyDefinitionList) propertyDefinition;
            PropertyDefinition listMemberPropertyDefinition = propertyDefinitionList.getMemberDefinition();
            PropertyList groupPropertyList = groupParentPropertyMap.getList(propertyDefinition.getName());
            if (listMemberPropertyDefinition instanceof PropertyDefinitionMap) {
                PropertyDefinitionMap propertyDefinitionMap = (PropertyDefinitionMap) listMemberPropertyDefinition;
                // TODO: How do we merge Lists of Maps? Not trivial...
            }
        }
    }

    private static void mergePropertyMap(PropertyDefinitionMap propertyDefinitionMap,
        List<AbstractPropertyMap> memberParentPropertyMaps, AbstractPropertyMap groupParentPropertyMap) {
        Map<String, PropertyDefinition> childPropertyDefinitions = propertyDefinitionMap.getPropertyDefinitions();
        if (!childPropertyDefinitions.isEmpty()) {
            for (PropertyDefinition childPropertyDefinition : childPropertyDefinitions.values())
                mergeProperty(childPropertyDefinition, memberParentPropertyMaps, groupParentPropertyMap);
        } else {
            mergeOpenPropertyMap(memberParentPropertyMaps, groupParentPropertyMap);
        }
    }

    private static void mergeOpenPropertyMap(List<AbstractPropertyMap> memberParentPropertyMaps,
        AbstractPropertyMap groupParentPropertyMap) {
        for (String groupMemberPropertyName : groupParentPropertyMap.getMap().keySet()) {
            PropertySimple groupMemberProperty = groupParentPropertyMap.getSimple(groupMemberPropertyName);
            if (groupMemberProperty != null && groupMemberProperty.getOverride() != null
                && groupMemberProperty.getOverride()) {
                for (AbstractPropertyMap sourceParentPropertyMap : memberParentPropertyMaps) {
                    PropertySimple sourcePropertySimple = sourceParentPropertyMap.getSimple(groupMemberPropertyName);
                    if (sourcePropertySimple == null) {
                        sourcePropertySimple = new PropertySimple(groupMemberPropertyName, groupMemberProperty
                            .getStringValue());
                        sourceParentPropertyMap.put(sourcePropertySimple);
                    } else {
                        sourcePropertySimple.setStringValue(groupMemberProperty.getStringValue());
                    }
                }
            }
        }
    }

    private static Map<String, Map<String, Integer>> createMemberNameValueFrequenciesMap(
        List<AbstractPropertyMap> sourceParentPropertyMaps) {
        Map<String, Map<String, Integer>> nameValueFrequenciesMap = new HashMap<String, Map<String, Integer>>();
        for (AbstractPropertyMap map : sourceParentPropertyMaps) {
            for (String propertyName : map.getMap().keySet()) {
                PropertySimple propertySimple = map.getSimple(propertyName);
                String propertyValue = (propertySimple != null) ? propertySimple.getStringValue() : null;
                Map<String, Integer> valueFrequencies = nameValueFrequenciesMap.get(propertyName);
                if (valueFrequencies == null) {
                    valueFrequencies = new HashMap<String, Integer>();
                    nameValueFrequenciesMap.put(propertyName, valueFrequencies);
                }
                Integer valueFrequency = (valueFrequencies.containsKey(propertyValue)) ? (valueFrequencies
                    .get(propertyValue) + 1) : 1;
                valueFrequencies.put(propertyValue, valueFrequency);
            }
        }
        return nameValueFrequenciesMap;
    }

    private static String getSimpleValue(AbstractPropertyMap parentPropertyMap, String propertyName) {
        PropertySimple samplePropertySimple = parentPropertyMap.getSimple(propertyName);
        return (samplePropertySimple != null) ? samplePropertySimple.getStringValue() : null;
    }
}
