/*
 * 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, version 2, as
 * published by the Free Software Foundation, and/or the GNU Lesser
 * General Public License, version 2.1, also as published by the Free
 * Software Foundation.
 *
 * 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 and the GNU Lesser General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License
 * and the GNU Lesser General Public License along with this program;
 * if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

package org.rhq.enterprise.server.resource.disambiguation;

import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;

import org.rhq.enterprise.server.resource.disambiguation.MutableDisambiguationReport.Resource;

/**
 * This enumerates different strategies that can be used to update the results to produce disambiguated list.
 * 
 * @author Lukas Krejci
 */
public enum DefaultDisambiguationUpdateStrategies implements DisambiguationUpdateStrategy {

    /**
     * The disambiguation policy is followed precisely.
     */
    EXACT {
        public <T> void update(DisambiguationPolicy policy, MutableDisambiguationReport<T> report) {
            updateResources(policy, report, true, true);
            int nofParents = policy.size() - 1;
            if (nofParents < 0)
                nofParents = 0;

            if (nofParents == 0) {
                report.parents.clear();
            } else {
                while (report.parents.size() > nofParents) {
                    report.parents.remove(report.parents.size() - 1);
                }
            }
        }
        
        public <T> boolean partitionFurther(ReportPartitions<T> partitions) {
            return true;
        }
    },

    /**
     * Even if the disambiguation policy determined that parents are not needed to disambiguate the 
     * results, at least one of them is kept in the report.
     */
    KEEP_AT_LEAST_ONE_PARENT {
        public <T> void update(DisambiguationPolicy policy, MutableDisambiguationReport<T> report) {
            updateResources(policy, report, true, true);
            int nofParents = policy.size() - 1;
            if (nofParents < 1)
                nofParents = 1;

            while (report.parents.size() > nofParents) {
                report.parents.remove(report.parents.size() - 1);
            }
        }

        @Override
        public EnumSet<ResourceResolution> resourceLevelRepartitionableResolutions() {
            return EnumSet.allOf(ResourceResolution.class);
        }
        
        public <T> boolean partitionFurther(ReportPartitions<T> partitions) {
            return true;
        }
    },

    /**
     * The parentage of the report is retained at least up to the server/service directly under platform.
     * If the policy needs the platform to stay, it is of course preserved.
     */
    KEEP_PARENTS_TO_TOPMOST_SERVERS {
        public <T> void update(DisambiguationPolicy policy, MutableDisambiguationReport<T> report) {
            updateResources(policy, report, true, true);
            //only remove the platform, if the policy doesn't dictate its presence...
            if (policy.size() > 1 && report.parents.size() > policy.size() - 1) {
                report.parents.remove(report.parents.size() - 1);
            }
        }

        @Override
        public EnumSet<ResourceResolution> resourceLevelRepartitionableResolutions() {
            return EnumSet.allOf(ResourceResolution.class);
        }
        
        public <T> boolean partitionFurther(ReportPartitions<T> partitions) {
            return partitions.getDisambiguationPolicy().size() < Disambiguator.MAXIMUM_DISAMBIGUATED_TREE_DEPTH - 1;
        }
    },

    /**
     * All parents are preserved no matter what the policy says.
     */
    KEEP_ALL_PARENTS {
        public <T> void update(DisambiguationPolicy policy, MutableDisambiguationReport<T> report) {
            updateResources(policy, report, true, true);
            //do nothing to the parents, keep them as they are...
        }
        
        public EnumSet<ResourceResolution> resourceLevelRepartitionableResolutions() {
            return EnumSet.of(ResourceResolution.NAME);
        }

        public EnumSet<ResourceResolution> alwaysRepartitionableResolutions() {
            return EnumSet.of(ResourceResolution.NAME);
        }
        
        public <T> boolean partitionFurther(ReportPartitions<T> partitions) {
            //we always keep all the info about the resources in this strategy
            //so there's no real need to disambiguate anything.
            return false;
        }
    };

    private static final DisambiguationPolicy.Level OVERRIDING_RESOLUTION = new DisambiguationPolicy.Level(ResourceResolution.TYPE);
    
    /**
     * This updates the resources in the report according to the resolutions contained in the policy.
     * This method is called as part of the {@link DisambiguationUpdateStrategy#update(DisambiguationPolicy, MutableDisambiguationReport)}
     * implementations in this enum before the individual enums modify the parent list as they see fit.
     * This method is left public because it is generic enough to be reused by other potential implementations of the
     * {@link DisambiguationUpdateStrategy} interface.
     * 
     * @param <T>
     * @param policy
     * @param report
     * @param honorAmbiguousTypeNamesList whether to honor the list of ambiguous type names as listed in the policy when updating the resources.
     * @param pushDownPluginInfo if true, the plugin information is pushed down as low in the resource hierarchy as possible. This means that if
     * some parent needs plugin disambiguation or is of an ambiguous type and the resource comes from the same plugin, the plugin info is preserved
     * on the resource rather than on the parent. This is mainly useful for the display purposes, because it just
     * looks nicer to have that info at a resource than somewhere in the location string.
     */
    public static <T> void updateResources(DisambiguationPolicy policy, MutableDisambiguationReport<T> report, boolean honorAmbiguousTypeNamesList, boolean pushDownPluginInfo) {
        List<String> ambiguousTypeNames = honorAmbiguousTypeNamesList ? policy.getAmbiguousTypeNames() : Collections.<String>emptyList();
        
        String resourcePlugin = report.resource.resourceType.plugin;
        List<String> parentPlugins = new ArrayList<String>(report.parents.size());
        updateResource(policy.get(0), report.resource, ambiguousTypeNames);
        
        int disambiguationPolicyIndex = 1;
        while (disambiguationPolicyIndex < policy.size() && disambiguationPolicyIndex - 1 < report.parents.size()) {
            DisambiguationPolicy.Level resolutionLevel = policy.get(disambiguationPolicyIndex);
            MutableDisambiguationReport.Resource parent = report.parents.get(disambiguationPolicyIndex - 1);
            
            if (pushDownPluginInfo) {
                parentPlugins.add(parent.resourceType.plugin);
            }
            
            updateResource(resolutionLevel, parent, ambiguousTypeNames);

            disambiguationPolicyIndex++;
        }

        disambiguationPolicyIndex--;

        //because the parents update strategy might leave more parents than this policy requires for disambiguation
        //we need to treat those parents as well. Because they are not needed for disambiguation, treat them as
        //if only the name and type was needed for them.
        for (; disambiguationPolicyIndex < report.parents.size(); ++disambiguationPolicyIndex) {
            if (pushDownPluginInfo) {
                parentPlugins.add(report.parents.get(disambiguationPolicyIndex).resourceType.plugin);
            }
            updateResource(OVERRIDING_RESOLUTION, report.parents.get(disambiguationPolicyIndex), ambiguousTypeNames);
        }

        if (pushDownPluginInfo) {
            for (int i = report.parents.size() - 1; i >= 0; --i) {
                String plugin = report.parents.get(i).resourceType.plugin;
                if (plugin != null && i > 0 && plugin.equals(parentPlugins.get(i - 1))) {
                    report.parents.get(i - 1).resourceType.plugin = plugin;
                    report.parents.get(i).resourceType.plugin = null;
                }
            }
            
            if (report.parents.size() > 0) {
                String plugin = report.parents.get(0).resourceType.plugin;
                if (plugin != null && plugin.equals(resourcePlugin)) {
                    report.resource.resourceType.plugin = resourcePlugin;
                    report.parents.get(0).resourceType.plugin = null;
                }
            }
        }
    }
    
    /**
     * @return a set of resolutions for which the unique reports need to be repartitioned at the resource level.
     * In another words this forces the disambiguation to continue on up the disambiguation chain even if the 
     * it disambiguates the resuts successfully at the resource level.
     */
    public EnumSet<ResourceResolution> resourceLevelRepartitionableResolutions() {
        return EnumSet.noneOf(ResourceResolution.class);
    }

    /**
     * @return a set of resolutions for which uniquely disambiguated reports are to be repartitioned further.
     * The resolutions from this set apply on the parents (on any level), unlike the resolutions from {@link #resourceLevelRepartitionableResolutions()}.
     */
    public EnumSet<ResourceResolution> alwaysRepartitionableResolutions() {
        return EnumSet.noneOf(ResourceResolution.class);
    }


    private static void updateResource(DisambiguationPolicy.Level resolutionLevel, Resource resource, List<String> ambiguousTypeNames) {
        switch (resolutionLevel.getResourceResolution()) {
        case NAME: case TYPE:
            if (!ambiguousTypeNames.contains(resource.resourceType.name)) {
                resource.resourceType.plugin = null;
            }
            break;
        case PLUGIN:
            if (!(resolutionLevel.isDeciding() || ambiguousTypeNames.contains(resource.resourceType.name))) {
                resource.resourceType.plugin = null;
            }
        }
    }    
    
    public static DefaultDisambiguationUpdateStrategies getDefault() {
        return DefaultDisambiguationUpdateStrategies.KEEP_ALL_PARENTS;
    }
}