/*
 * RHQ Management Platform
 * Copyright (C) 2005-2008 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.enterprise.gui.navigation.resource;

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

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.rhq.core.domain.auth.Subject;
import org.rhq.core.domain.measurement.AvailabilityType;
import org.rhq.core.domain.resource.Agent;
import org.rhq.core.domain.resource.Resource;
import org.rhq.core.domain.resource.flyweight.AutoGroupCompositeFlyweight;
import org.rhq.core.domain.resource.flyweight.MembersAvailabilityHint;
import org.rhq.core.domain.resource.flyweight.MembersCategoryHint;
import org.rhq.core.domain.resource.flyweight.ResourceFlyweight;
import org.rhq.core.domain.resource.flyweight.ResourceSubCategoryFlyweight;
import org.rhq.core.domain.resource.flyweight.ResourceTypeFlyweight;
import org.rhq.core.domain.util.PageControl;
import org.rhq.core.gui.util.FacesContextUtility;
import org.rhq.enterprise.gui.util.EnterpriseFacesContextUtility;
import org.rhq.enterprise.server.core.AgentManagerLocal;
import org.rhq.enterprise.server.resource.ResourceManagerLocal;
import org.rhq.enterprise.server.util.HibernatePerformanceMonitor;
import org.rhq.enterprise.server.util.LookupUtil;

/**
 * Backing bean for the left navigation tree for resources
 *
 * @author Greg Hinkle
 */
public class ResourceTreeModelUIBean {
    private static final Log log = LogFactory.getLog(ResourceTreeModelUIBean.class);

    private List<ResourceTreeNode> roots = new ArrayList<ResourceTreeNode>();
    private ResourceTreeNode rootNode = null;

    private ResourceManagerLocal resourceManager = LookupUtil.getResourceManager();
    private AgentManagerLocal agentManager = LookupUtil.getAgentManager();

    private String nodeTitle;

    private void loadTree() {
        int searchId;
        Resource currentResource = EnterpriseFacesContextUtility.getResourceIfExists();
        if (currentResource == null) {
            searchId = Integer.parseInt(FacesContextUtility.getOptionalRequestParameter("parent"));
        } else {
            searchId = currentResource.getId();
        }

        Subject user = EnterpriseFacesContextUtility.getSubject();

        long start = System.currentTimeMillis();
        long monitorId = HibernatePerformanceMonitor.get().start();
        Resource rootResource = resourceManager.getRootResourceForResource(searchId);
        long end = System.currentTimeMillis();
        HibernatePerformanceMonitor.get().stop(monitorId, "ResourceTree root resource");
        log.debug("Found root resource in " + (end - start));

        Agent agent = agentManager.getAgentByResourceId(LookupUtil.getSubjectManager().getOverlord(), rootResource
            .getId());

        start = System.currentTimeMillis();
        monitorId = HibernatePerformanceMonitor.get().start();
        List<ResourceFlyweight> resources = resourceManager.findResourcesByAgent(user, agent.getId(), PageControl
            .getUnlimitedInstance());
        end = System.currentTimeMillis();
        HibernatePerformanceMonitor.get().stop(monitorId, "ResourceTree agent resource");
        log.debug("Loaded " + resources.size() + " raw resources in " + (end - start));

        start = System.currentTimeMillis();
        monitorId = HibernatePerformanceMonitor.get().start();
        rootNode = load(rootResource.getId(), resources);
        end = System.currentTimeMillis();
        HibernatePerformanceMonitor.get().stop(monitorId, "ResourceTree tree construction");
        log.debug("Constructed tree in " + (end - start));
    }

    public static ResourceTreeNode load(int rootId, List<ResourceFlyweight> resources) {
        ResourceFlyweight found = null;
        for (ResourceFlyweight res : resources) {
            if (res.getId() == rootId) {
                found = res;
            }
        }
        ResourceTreeNode root = new ResourceTreeNode(found);
        long start = System.currentTimeMillis();
        load(root);
        return root;
    }

    public static void load(ResourceTreeNode parentNode) {
        if (parentNode.getData() instanceof ResourceFlyweight) {
            ResourceFlyweight parentResource = (ResourceFlyweight) parentNode.getData();

            Map<Object, List<ResourceFlyweight>> children = new HashMap<Object, List<ResourceFlyweight>>();
            for (ResourceFlyweight res : parentResource.getChildResources()) {
                if (res.getResourceType().getSubCategory() != null) {
                    // These are children that have subcategories
                    // Split them by if they are a sub-sub category or just a category
                    ResourceSubCategoryFlyweight categoryKey = res.getResourceType().getSubCategory()
                        .getParentSubCategory();
                    if (categoryKey == null) {
                        categoryKey = res.getResourceType().getSubCategory();
                    }
                    addToList(children, categoryKey, res);
                } else {
                    // These are children without categories of the parent resource
                    // - Add them into groupings by their resource type
                    addToList(children, res.getResourceType(), res);
                }

            }

            Set<String> dupResourceTypeNames = getDuplicateResourceTypeNames(children);

            for (Map.Entry<Object, List<ResourceFlyweight>> entry : children.entrySet()) {
                Object key = entry.getKey();
                List<ResourceFlyweight> resources = entry.getValue();

                double avail = 0;
                for (ResourceFlyweight res : resources) {
                    avail += res.getCurrentAvailability().getAvailabilityType() == AvailabilityType.UP ? 1 : 0;
                }
                avail = avail / resources.size();

                Object nodeData = null;
                if (key instanceof ResourceSubCategoryFlyweight) {
                    nodeData = new AutoGroupCompositeFlyweight(avail, parentResource,
                        (ResourceSubCategoryFlyweight) key, resources.size());
                } else if (key instanceof ResourceTypeFlyweight) {
                    ResourceTypeFlyweight typeKey = (ResourceTypeFlyweight) key;

                    if (typeKey.isSingleton()) {
                        nodeData = resources.get(0);
                    } else {
                        boolean isDupResourceTypeName = dupResourceTypeNames.contains(typeKey.getName());
                        nodeData = new AutoGroupCompositeFlyweight(avail, parentResource, typeKey, resources.size(),
                            isDupResourceTypeName);
                    }
                }
                ResourceTreeNode node = new ResourceTreeNode(nodeData, parentNode);
                load(node);

                if (!recursivelyLocked(node)) {
                    parentNode.getChildren().add(node);
                }
            }
        } else {
            // #####################################################################################

            AutoGroupCompositeFlyweight compositeParent = (AutoGroupCompositeFlyweight) parentNode.getData();

            Map<Object, List<ResourceFlyweight>> children = new HashMap<Object, List<ResourceFlyweight>>();
            log.debug("composite parent" + compositeParent);
            if (compositeParent != null) {

                MembersCategoryHint membersCategory = MembersCategoryHint.NONE;
                MembersAvailabilityHint membersAvailabilityHint = MembersAvailabilityHint.UP;

                for (ResourceFlyweight res : compositeParent.getParentResource().getChildResources()) {
                    boolean process = false;
                    if (compositeParent.getSubcategory() != null) {
                        // parent is a sub category
                        if (res.getResourceType().getSubCategory() != null
                            && compositeParent.getSubcategory().equals(
                                res.getResourceType().getSubCategory().getParentSubCategory())
                            && compositeParent.getParentResource().equals(res.getParentResource())) {

                            // A subSubCategory in a subcategory
                            addToList(children, res.getResourceType().getSubCategory(), res);
                            process = true;
                        } else if (compositeParent.getSubcategory().equals(res.getResourceType().getSubCategory())
                            && compositeParent.getParentResource().equals(res.getParentResource())) {
                            // Direct entries in a subcategory... now group them by autogroup (type)
                            addToList(children, res.getResourceType(), res);
                            process = true;
                        }
                    } else if (compositeParent.getResourceType() != null) {
                        if (compositeParent.getResourceType().equals(res.getResourceType())
                            && compositeParent.getParentResource().getId() == res.getParentResource().getId()) {

                            addToList(children, res.getResourceType(), res);
                            process = true;
                        }
                    }

                    if (process) {
                        //amend the overall category of all the members of the auto group.                
                        switch (membersCategory) {
                        case NONE: //this is the first child, so let's use its category as a starting point
                            membersCategory = MembersCategoryHint.fromResourceCategory(res.getResourceType()
                                .getCategory());
                            break;
                        case MIXED: //this is the "final" state. The children type is not going to change from this.
                            break;
                        default: //check if this child has the same category as its previous siblings.
                            if (MembersCategoryHint.fromResourceCategory(res.getResourceType().getCategory()) != membersCategory) {
                                membersCategory = MembersCategoryHint.MIXED;
                            }
                        }

                        //amend the availability hint of the autogroup. If all resources are up, the hint is UP, if some of the resources
                        //are down, the hint is DOWN, if some of the resources' avail state is unknown, the hint is UNKNOWN.
                        //The down state has the highest priority.
                        switch (membersAvailabilityHint) {
                        case UP:
                            membersAvailabilityHint = MembersAvailabilityHint.fromAvailabilityType(res
                                .getCurrentAvailability().getAvailabilityType());
                            break;
                        case UNKNOWN:
                            if (res.getCurrentAvailability().getAvailabilityType() == AvailabilityType.DOWN) {
                                membersAvailabilityHint = MembersAvailabilityHint.DOWN;
                            }
                            break;
                        case DOWN:
                            ; //a "terminal" state... if some resource is down, the overall state is going to be down as that is the most important information.
                        }
                    }
                }

                compositeParent.setMembersCategoryHint(membersCategory);
                compositeParent.setMembersAvailabilityHint(membersAvailabilityHint);
            }

            AutoGroupCompositeFlyweight compositeParentNode = (AutoGroupCompositeFlyweight) parentNode.getData();

            for (Map.Entry<Object, List<ResourceFlyweight>> entry : children.entrySet()) {
                Object key = entry.getKey();
                List<ResourceFlyweight> resources = entry.getValue();

                if (compositeParentNode.getSubcategory() != null) {
                    double avail = 0;
                    for (ResourceFlyweight res : resources) {
                        avail += res.getCurrentAvailability().getAvailabilityType() == AvailabilityType.UP ? 1 : 0;
                    }
                    avail = avail / resources.size();

                    Object nodeData = null;
                    if (key instanceof ResourceSubCategoryFlyweight) {
                        nodeData = new AutoGroupCompositeFlyweight(avail, compositeParent.getParentResource(),
                            (ResourceSubCategoryFlyweight) key, resources.size());
                    } else if (key instanceof ResourceTypeFlyweight) {
                        ResourceTypeFlyweight typeKey = (ResourceTypeFlyweight) key;
                        if (typeKey.isSingleton()) {
                            nodeData = resources.get(0);
                        } else {
                            nodeData = new AutoGroupCompositeFlyweight(avail, compositeParent.getParentResource(),
                                typeKey, resources.size(), false);
                        }
                    }
                    ResourceTreeNode node = new ResourceTreeNode(nodeData, parentNode);
                    load(node);

                    if (!recursivelyLocked(node)) {
                        parentNode.getChildren().add(node);
                    }
                } else {
                    for (ResourceFlyweight res : resources) {
                        ResourceTreeNode node = new ResourceTreeNode(res, parentNode);
                        load(node);
                        if (!recursivelyLocked(node)) {
                            parentNode.getChildren().add(node);
                        }
                    }
                }
            }
        }
    }

    public static boolean recursivelyLocked(ResourceTreeNode node) {
        if (node.getData() instanceof ResourceFlyweight && !((ResourceFlyweight) node.getData()).isLocked()) {
            return false;
        }

        boolean allLocked = true;
        for (ResourceTreeNode child : node.getChildren()) {
            if (!recursivelyLocked(child))
                allLocked = false;
        }
        return allLocked;
    }

    public ResourceTreeNode getTreeNode() {
        if (rootNode == null) {
            long start = System.currentTimeMillis();
            loadTree();
            log.debug("Loaded full tree in " + (System.currentTimeMillis() - start));
        }

        return rootNode;
    }

    public List<ResourceTreeNode> getRoots() {
        if (roots.isEmpty()) {
            roots.add(getTreeNode());
        }
        return roots;
    }

    public String getNodeTitle() {
        return nodeTitle;
    }

    public void setNodeTitle(String nodeTitle) {
        this.nodeTitle = nodeTitle;
    }

    private static Set<String> getDuplicateResourceTypeNames(Map<Object, List<ResourceFlyweight>> children) {
        Set<String> resourceTypeNames = new HashSet<String>();
        Set<String> dupResourceTypeNames = new HashSet<String>();
        for (Object rsc : children.keySet()) {
            if (rsc instanceof ResourceTypeFlyweight) {
                String resourceTypeName = ((ResourceTypeFlyweight) rsc).getName();
                if (resourceTypeNames.contains(resourceTypeName)) {
                    dupResourceTypeNames.add(resourceTypeName);
                }
                resourceTypeNames.add(resourceTypeName);
            }
        }
        return dupResourceTypeNames;
    }

    private static <K, V> void addToList(Map<K, List<V>> mapOfLists, K key, V value) {
        List<V> list = mapOfLists.get(key);

        if (list == null) {
            list = new ArrayList<V>();
            mapOfLists.put(key, list);
        }

        list.add(value);
    }
}
