/*
 * Copyright (C) 2005-2010 Alfresco Software Limited.
 *
 * This file is part of Alfresco
 *
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Alfresco 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see .
 */
package org.alfresco.repo.node.db;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.alfresco.repo.domain.node.NodeDAO;
import org.alfresco.service.cmr.repository.AssociationRef;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.Pair;
import org.springframework.dao.ConcurrencyFailureException;
/**
 * Class that walks down a hierarchy gathering view details for later processing.
 * 
 * This class is not threadsafe and should be used sequentially on a single
 * thread and then discarded.
 * 
 * @author Derek Hulley
 * @since 4.1.1
 */
public class NodeHierarchyWalker
{
    private final NodeDAO nodeDAO;
    /** Store for all nodes by ID */
    private final Map nodesVisitedById = new HashMap(59);
    /** Store for all nodes by ID */
    private final Map nodesVisitedByNodeRef = new HashMap(59);
    /** Store all the nodes visited from the leaf nodes up */
    private final List nodesLeafToParent = new ArrayList(67);
    /** Store all the nodes visited from parent down */
    private final List nodesParentToLeaf = new ArrayList(67);
    /**
     * @param nodeDAO           the low-leve query service
     */
    public NodeHierarchyWalker(NodeDAO nodeDAO)
    {
        this.nodeDAO = nodeDAO;
    }
    
    /**
     * @return                  the node data for the node ID or null if not visited
     */
    public VisitedNode getNode(Long id)
    {
        return nodesVisitedById.get(id);
    }
    
    /**
     * @return                  the node data for the node reference or null if not visited
     */
    public VisitedNode getNode(NodeRef nodeRef)
    {
        return nodesVisitedByNodeRef.get(nodeRef);
    }
    
    /**
     * Return the IDs of the nodes visited in desired order
     * 
     * @param leafFirst         true to list the leaf nodes first
     * @return                  the IDs of the nodes visited
     */
    public List getNodes(boolean leafFirst)
    {
        if (leafFirst)
        {
            return nodesLeafToParent;
        }
        else
        {
            return nodesParentToLeaf;
        }
    }
    
    /**
     * Walk a hierachy
     */
    public void walkHierarchy(Pair nodePair, Pair parentAssocPair)
    {
        Long nodeId = nodePair.getFirst();
        NodeRef nodeRef = nodePair.getSecond();
        QName nodeType = nodeDAO.getNodeType(nodeId);
        Long nodeAclId = nodeDAO.getNodeAclId(nodeId);
        // Record the first node (parent)
        VisitedNode visitedNode = new VisitedNode(nodeId, nodeRef, nodeType, nodeAclId, parentAssocPair);
        nodesVisitedById.put(nodeId, visitedNode);
        nodesVisitedByNodeRef.put(nodeRef, visitedNode);
        // Now walk
        walkNode(nodeId);
    }
    
    /**
     * Recursive method to gather data about nodes from the leafs upwards
     */
    private void walkNode(Long nodeId)
    {
        VisitedNode nodeVisited = nodesVisitedById.get(nodeId);
        if (nodeVisited == null)
        {
            throw new IllegalStateException("Parent node has not been visited: " + nodeId);
        }
        nodesParentToLeaf.add(nodeVisited);
        
        final List nodesVisitedWorking = new ArrayList(59);
        // We have to get to the bottom of the hierarchy
        NodeDAO.ChildAssocRefQueryCallback walkChildAssocs = new NodeDAO.ChildAssocRefQueryCallback()
        {
            public final boolean preLoadNodes()
            {
                return false;
            }
            @Override
            public final boolean orderResults()
            {
                return false;
            }
            public final boolean handle(
                    Pair childAssocPair,
                    Pair parentNodePair,
                    Pair childNodePair
                    )
            {
                if (childAssocPair.getSecond().isPrimary())
                {
                    Long childNodeId = childNodePair.getFirst();
                    NodeRef childNodeRef = childNodePair.getSecond();
                    QName childNodeType = nodeDAO.getNodeType(childNodeId);
                    Long childNodeAclId = nodeDAO.getNodeAclId(childNodeId);
                    // Keep the IDs of the nodes for recursion
                    nodesVisitedWorking.add(childNodeId);
                    // We have a node in the hierarchy to record
                    VisitedNode visitedNode = new VisitedNode(childNodeId, childNodeRef, childNodeType, childNodeAclId, childAssocPair);
                    nodesVisitedById.put(childNodeId, visitedNode);
                    nodesVisitedByNodeRef.put(childNodeRef, visitedNode);
                }
                else
                {
                    Long parentNodeId = parentNodePair.getFirst();
                    // We don't recurse down secondary associations, so the parent
                    // must be a previously-visted node
                    VisitedNode nodeVisitedWorking = nodesVisitedById.get(parentNodeId);
                    if (nodeVisitedWorking == null)
                    {
                        // We came here how?
                        throw new IllegalStateException(
                                "Came to secondary association without having found primary parent before: \n" +
                                "   parent: " + parentNodePair + "\n" +
                                "   child:  " + childNodePair);
                    }
                    // Record the secondary association
                    nodeVisitedWorking.secondaryChildAssocs.add(childAssocPair);
                }
                // Record this node
                // More results
                return true;
            }
            public final void done()
            {
            }                               
        };
        // Gather all child associations
        nodeDAO.getChildAssocs(nodeId, null, null, null, null, null, walkChildAssocs);
        
        // Dig down to primary children
        for (Long visitedNodeId : nodesVisitedWorking)
        {
            walkNode(visitedNodeId);
        }
        
        // The bottom has been reached.
        nodesLeafToParent.add(nodeVisited);
        // Record parent associations
        NodeDAO.ChildAssocRefQueryCallback getParentAssocs = new NodeDAO.ChildAssocRefQueryCallback()
        {
            @Override
            public final boolean preLoadNodes()
            {
                return false;
            }
            @Override
            public boolean orderResults()
            {
                return false;
            }
            @Override
            public boolean handle(
                    Pair childAssocPair,
                    Pair parentNodePair, Pair childNodePair)
            {
                VisitedNode visitedNode = nodesVisitedById.get(childNodePair.getFirst());
                if (visitedNode == null)
                {
                    throw new IllegalStateException("Querying upwards found nodes not visited: " + childNodePair);
                }
                if (childAssocPair.getSecond().isPrimary())
                {
                    // Double check the primary association
                    if (!visitedNode.primaryParentAssocPair.equals(childAssocPair))
                    {
                        // The primary parent association for the node has changed
                        throw new ConcurrencyFailureException("Node parent changed while hierarchy was being examined: " + childNodePair);
                    }
                }
                else
                {
                    // Record all secondary parent associations
                    visitedNode.secondaryParentAssocs.add(childAssocPair);
                }
                // More results
                return true;
            }
            @Override
            public void done()
            {
            }
        };
        nodeDAO.getParentAssocs(nodeId, null, null, null, getParentAssocs);
        
        VisitedNode visitedNode = nodesVisitedById.get(nodeId);
        if (visitedNode == null)
        {
            throw new IllegalStateException("Querying upwards found nodes not visited: " + nodeId);
        }
        
        Collection> targetAssocs = nodeDAO.getTargetNodeAssocs(nodeId, null);
        visitedNode.targetAssocs.addAll(targetAssocs);
        Collection> sourceAssocs = nodeDAO.getSourceNodeAssocs(nodeId, null);
        visitedNode.sourceAssocs.addAll(sourceAssocs);
    }
    
    /**
     * Carries data about a node in the hierarchy
     * 
     * @author Derek Hulley
     * @since 4.1.1
     */
    public class VisitedNode
    {
        public final Long id;
        public final NodeRef nodeRef;
        public final QName nodeType;
        public final Long aclId;
        public final Pair primaryParentAssocPair;
        public final List> secondaryParentAssocs;
        public final List> secondaryChildAssocs;
        public final List> targetAssocs;
        public final List> sourceAssocs;
        
        private VisitedNode(
                Long id,
                NodeRef nodeRef,
                QName type,
                Long aclId,
                Pair primaryParentAssocPair)
        {
            this.id = id;
            this.nodeRef = nodeRef;
            this.nodeType = type;
            this.aclId = aclId;
            this.primaryParentAssocPair = primaryParentAssocPair;
            this.secondaryParentAssocs = new ArrayList>(17);
            this.secondaryChildAssocs = new ArrayList>(17);
            this.targetAssocs = new ArrayList>();
            this.sourceAssocs = new ArrayList>();
        }
    }
}