/*
 *   Licensed to the Apache Software Foundation (ASF) under one
 *   or more contributor license agreements.  See the NOTICE file
 *   distributed with this work for additional information
 *   regarding copyright ownership.  The ASF licenses this file
 *   to you under the Apache License, Version 2.0 (the
 *   "License"); you may not use this file except in compliance
 *   with the License.  You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing,
 *   software distributed under the License is distributed on an
 *   "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *   KIND, either express or implied.  See the License for the
 *   specific language governing permissions and limitations
 *   under the License.
 *
 */
package org.apache.directory.fortress.core.impl;


import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.locks.ReadWriteLock;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.directory.fortress.core.model.Graphable;
import org.apache.directory.fortress.core.model.Hier;
import org.apache.directory.fortress.core.model.Relationship;
import org.jgrapht.graph.SimpleDirectedGraph;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.directory.fortress.core.GlobalErrIds;
import org.apache.directory.fortress.core.SecurityException;
import org.apache.directory.fortress.core.ValidationException;


/**
 * This utility performs base hierarchical processing using this software <a href="http://www.jgrapht.org/">JGraphT</a></li>.
 * <p>
 * It is used to provide hierarchical processing APIs for the following data sets:
 * <ol>
 * <li>RBAC Role relations are stored in {@code cn=Hierarchies,ou=Roles,ou=RBAC} ldap node and cached as singleton in {@link RoleUtil}</li>
 * <li>ARBAC Admin Role relations are stored in {@code cn=Hierarchies,ou=AdminRoles,ou=ARBAC} ldap node and cached as singleton in {@link AdminRoleUtil}</li>
 * <li>User Organizational Unit relations are stored in {@code cn=Hierarchies,ou=OS-U,ou=ARBAC} node and cached as {@link org.apache.directory.fortress.core.impl.UsoUtil}</li>
 * <li>Permission Organizational Unit relations are stored in {@code cn=Hierarchies,ou=OS-P,ou=ARBAC} node and cached as {@link org.apache.directory.fortress.core.impl.PsoUtil}</li>
 * </ol>
 * This class...
 * <ol>
 * <li>manipulates data that is stored as singleton inside other classes with vertices of {@code String}, and edges, as {@link org.apache.directory.fortress.core.model.Relationship}s</li>
 * <li>utilizes open source library, see <a href="http://www.jgrapht.org/">JGraphT</a>.</li>
 * <li>processes general hierarchical data structure i.e. allows multiple inheritance with parents.</li>
 * <li>constructs and parses simple directed graphs.</li>
 * </ol>
 * Static methods on this class are intended for use by other Fortress classes, and cannot be directly invoked by outside programs.
 * <p>
 * This class is thread safe.
 *
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
final class HierUtil
{
    /**
     * Constants used within this class:
     */
    private static final String CLS_NM = HierUtil.class.getName();
    private static final Logger LOG = LoggerFactory.getLogger( CLS_NM );
    private static final String VERTEX = "Vertex";

    /**
     * The 'Type' attribute corresponds to what type of hierarchy is being referred to.
     */
    enum Type
    {
        ROLE,
        ARLE,
        USO,
        PSO
    }

    private static final Map<String, ReadWriteLock> synchMap = new HashMap<String, ReadWriteLock>();


    /**
     * Private constructor
     *
     */
    private HierUtil()
    {
    }

    /**
     * This api is used to determine parentage for Hierarchical processing.
     * It evaluates three relationship expressions:
     * <ol>
     * <li>If child equals parent</li>
     * <li>If mustExist true and parent-child relationship exists</li>
     * <li>If mustExist false and parent-child relationship does not exist</li>
     * </ol>
     * Method will throw {@link org.apache.directory.fortress.core.ValidationException} if rule check fails meaning caller failed validation
     * attempt to add/remove hierarchical relationship failed.
     *
     * @param graph     contains a reference to simple digraph {@code org.jgrapht.graph.SimpleDirectedGraph}.
     * @param child     contains name of child.
     * @param parent    contains name of parent.
     * @param mustExist boolean is used to specify if relationship must be true.
     * @throws org.apache.directory.fortress.core.ValidationException
     *          in the event it fails one of the 3 checks.
     */
    static void validateRelationship( SimpleDirectedGraph<String, Relationship> graph, String child, String parent,
        boolean mustExist )
        throws ValidationException
    {
        // Ensure the two nodes aren't the same:
        if ( child.equalsIgnoreCase( parent ) )
        {
            String error = "validateRelationship child [" + child + "] same as parent [" + parent + "]";
            throw new ValidationException( GlobalErrIds.HIER_REL_INVLD, error );
        }
        Relationship rel = new Relationship( child.toUpperCase(), parent.toUpperCase() );
        // Ensure there is a valid child to parent relationship.
        if ( mustExist && !isRelationship( graph, rel ) )
        {
            String error = "validateRelationship child [" + child + "] does not have parent [" + parent + "]";
            throw new ValidationException( GlobalErrIds.HIER_REL_NOT_EXIST, error );
        }
        // Ensure the child doesn't already have the parent as an ascendant.
        else if ( !mustExist && isAscendant( child, parent, graph ) )
        {
            String error = "validateRelationship child [" + child + "] already has parent [" + parent + "]";
            throw new ValidationException( GlobalErrIds.HIER_REL_EXIST, error );
        }
        // Prevent cycles by making sure the child isn't an ascendant of parent.
        else if ( !mustExist && isDescedant( parent, child, graph ) )
        {
            String error = "validateRelationship child [" + child + "] is parent of [" + parent + "]";
            throw new ValidationException( GlobalErrIds.HIER_REL_CYCLIC, error );
        }
    }


    /**
     * This method converts from physical ldap entity format, {@link Hier} to logical {@code org.jgrapht.graph.SimpleDirectedGraph}.
     *
     * @param hier contains parent-child relationship in preparation to storing in ldap {@code ftRels} attribute of {@code ftHier} object class.
     * @return {@code org.jgrapht.graph.SimpleDirectedGraph} containing the vertices of {@code String}, and edges, as {@link Relationship}s that correspond to relational data.
     */
    private static SimpleDirectedGraph<String, Relationship> toGraph( Hier hier )
    {
        LOG.debug( "toGraph" );
        SimpleDirectedGraph<String, Relationship> graph =
            new SimpleDirectedGraph<>( Relationship.class );
        List<Relationship> edges = hier.getRelationships();
        if ( edges != null && edges.size() > 0 )
        {
            for ( Relationship edge : edges )
            {
                String child = edge.getChild();
                String parent = edge.getParent();

                try
                {
                    graph.addVertex( child );
                    graph.addVertex( parent );
                    graph.addEdge( child, parent, edge );
                }
                catch (java.lang.IllegalArgumentException e)
                {
                    String error = "toGraph child: " + child + " parent: " + parent + " caught IllegalArgumentException=" + e;
                    LOG.error( error );
                }

                LOG.debug( "toGraph child={}, parent={}", child, parent );
            }
        }
        return graph;
    }


    /**
     * This method is synchronized and adds an edge and its associated vertices to simple directed graph stored in static memory of this process.
     *
     * @param graph synchronized parameter contains a reference to simple digraph {@code org.jgrapht.graph.SimpleDirectedGraph}.
     * @param relation contains parent-child relationship targeted for addition.
     * @return {@code org.jgrapht.graph.SimpleDirectedGraph} containing the vertices of {@code String}, and edges, as {@link Relationship}s that correspond to relational data.
     */
    private static void addEdge( SimpleDirectedGraph<String, Relationship> graph, Relationship relation )
    {
        LOG.debug( "addEdge" );
        synchronized ( graph )
        {
            graph.addVertex( relation.getChild().toUpperCase() );
            graph.addVertex( relation.getParent().toUpperCase() );
            graph.addEdge( relation.getChild().toUpperCase(), relation.getParent().toUpperCase(), relation );
        }
    }


    /**
     * This method is synchronized and removes an edge from a simple directed graph stored in static memory of this process.
     *
     * @param graph synchronized parameter contains a reference to simple digraph {@code org.jgrapht.graph.SimpleDirectedGraph}.
     * @param relation contains parent-child relationship targeted for removal.
     * @return {@code org.jgrapht.graph.SimpleDirectedGraph} containing the vertices of {@code String}, and edges, as {@link Relationship}s that correspond to relational data.
     */
    private static void removeEdge( SimpleDirectedGraph<String, Relationship> graph, Relationship relation )
    {
        LOG.debug( "removeEdge" );
        synchronized ( graph )
        {
            graph.removeEdge( relation );
        }
    }


    /**
     * Return number of children (direct descendants) a given parent node has.
     *
     * @param name  contains the vertex of graph to gather descendants from.
     * @param graph contains a reference to simple digraph {@code org.jgrapht.graph.SimpleDirectedGraph}.
     * @return int value contains the number of children of a given parent vertex.
     */
    static int numChildren( String name, SimpleDirectedGraph<String, Relationship> graph )
    {
        Map<String, String> vx = new HashMap<>();
        vx.put( VERTEX, name.toUpperCase() );
        return numChildren( vx, graph );
    }


    /**
     * Determine if parent-child relationship exists in supplied digraph.
     *
     * @param graph contains a reference to simple digraph {@code org.jgrapht.graph.SimpleDirectedGraph}.
     * @param rel   contains parent and child names.
     * @return boolean value.  true indicates parent-child relationship exists in digraph.
     */
    private static boolean isRelationship( SimpleDirectedGraph<String, Relationship> graph, Relationship rel )
    {
        return graph.containsEdge( rel );
    }


    /**
     * Determine how many children a given parent node has.
     *
     * @param vertex of parent.
     * @param graph  contains a reference to simple digraph {@code org.jgrapht.graph.SimpleDirectedGraph}.
     * @return
     */
    private static int numChildren( Map<String, String> vertex, SimpleDirectedGraph<String, Relationship> graph )
    {
        int numChildren = 0;
        try
        {
            String v = vertex.get( VERTEX );
            if ( v == null )
            {
                return 0;
            }
            LOG.debug( "hasChildren [{}]", v );
            numChildren = graph.inDegreeOf( v );
        }
        catch ( java.lang.IllegalArgumentException e )
        {
            // vertex is leaf.
        }
        return numChildren;
    }


    /**
     * Recursively traverse the hierarchical graph and return all of the ascendants of a given node.
     *
     * @param childName maps to vertex to determine parentage.
     * @param graph     contains a reference to simple digraph {@code org.jgrapht.graph.SimpleDirectedGraph}.
     * @return Set of names that are parents of given child.
     */
    static Set<String> getAscendants( String childName, SimpleDirectedGraph<String, Relationship> graph )
    {
        Map<String, String> vx = new HashMap<>();
        // TreeSet will return in sorted order with case insensitive comparator:
        Set<String> parents = new TreeSet<>( String.CASE_INSENSITIVE_ORDER );
        vx.put( VERTEX, childName.toUpperCase() );
        getAscendants( vx, graph, parents );
        return parents;
    }


    /**
     * Utility function recursively traverses a given digraph to build a set of all ascendant names.
     *
     * @param vertex     contains the position of the cursor for traversal of graph.
     * @param graph      contains a reference to simple digraph {@code org.jgrapht.graph.SimpleDirectedGraph}.
     * @param ascendants contains the result set of ascendant names.
     * @return value contains the vertex of current position.
     */
    private static String getAscendants( Map<String, String> vertex, SimpleDirectedGraph<String, Relationship> graph,
        Set<String> ascendants )
    {
        String v = vertex.get( VERTEX );
        if ( v == null )
        {
            return null;
        }
        else if ( graph == null )
        {
            return null;
        }
        LOG.debug( "getAscendants [{}]", v);
        Set<Relationship> edges;
        try
        {
            edges = graph.outgoingEdgesOf( v );

        }
        catch ( java.lang.IllegalArgumentException iae )
        {
            // vertex is leaf.
            return null;
        }
        for ( Relationship edge : edges )
        {
            vertex.put( VERTEX, edge.getParent() );
            ascendants.add( edge.getParent() );
            v = getAscendants( vertex, graph, ascendants );
        }
        return v;
    }


    /**
     * Recursively traverse the hierarchical graph and return all of the descendants for a given node.
     *
     * @param parentName maps to vertex to determine parentage.
     * @param graph      contains a reference to simple digraph {@code org.jgrapht.graph.SimpleDirectedGraph}.
     * @return Set of names that are children of given parent.
     */
    static Set<String> getDescendants( String parentName, SimpleDirectedGraph<String, Relationship> graph )
    {
        Map<String, String> vx = new HashMap<>();
        // TreeSet will return in sorted order with case insensitive comparator:
        Set<String> children = new TreeSet<>( String.CASE_INSENSITIVE_ORDER );
        vx.put( VERTEX, parentName.toUpperCase() );
        getDescendants( vx, graph, children );
        return children;
    }


    /**
     * Recursively traverse the hierarchical graph and determine child node contains a given parent as one of its ascendants.
     *
     * @param childName maps to vertex to determine parentage.
     * @param parentName maps to vertex to determine parentage.
     * @param graph      contains a reference to simple digraph {@code org.jgrapht.graph.SimpleDirectedGraph}.
     * @return Set of names that are children of given parent.
     */
    private static boolean isAscendant( String childName, String parentName,
        SimpleDirectedGraph<String, Relationship> graph )
    {
        boolean isAscendant = false;
        Set<String> ascendants = getAscendants( childName, graph );
        if ( ascendants.contains( parentName ) )
        {
            isAscendant = true;
        }
        return isAscendant;
    }


    /**
     * Recursively traverse the hierarchical graph and determine if parent node contains a given child as one of its descendants.
     *
     * @param childName maps to vertex to determine parentage.
     * @param parentName maps to vertex to determine parentage.
     * @param graph      contains a reference to simple digraph {@code org.jgrapht.graph.SimpleDirectedGraph}.
     * @return Set of names that are children of given parent.
     */
    private static boolean isDescedant( String childName, String parentName,
        SimpleDirectedGraph<String, Relationship> graph )
    {
        boolean isDescendant = false;
        Set<String> descendants = getDescendants( parentName, graph );
        if ( descendants.contains( childName ) )
        {
            isDescendant = true;
        }
        return isDescendant;
    }


    /**
     * Utility function recursively traverses a given digraph to build a set of all descendants names.
     *
     * @param vertex      contains the position of the cursor for traversal of graph.
     * @param graph       contains a reference to simple digraph {@code org.jgrapht.graph.SimpleDirectedGraph}.
     * @param descendants contains the result set of names of all descendants of node.
     * @return value contains the vertex of current position.
     */
    private static String getDescendants( Map<String, String> vertex, SimpleDirectedGraph<String, Relationship> graph,
        Set<String> descendants )
    {
        String v = vertex.get( VERTEX );
        if ( v == null )
        {
            return null;
        }
        else if ( graph == null )
        {
            return null;
        }
        LOG.debug( "getDescendants [{}]", v);
        Set<Relationship> edges;
        try
        {
            edges = graph.incomingEdgesOf( v );
        }
        catch ( java.lang.IllegalArgumentException iae )
        {
            // vertex is leaf.
            return null;
        }
        for ( Relationship edge : edges )
        {
            vertex.put( VERTEX, edge.getChild() );
            descendants.add( edge.getChild() );
            v = getDescendants( vertex, graph, descendants );
        }
        return v;
    }


    /**
     * Utility function returns a set of all children (direct descendant) names.
     *
     * @param vertex contains the position of the cursor for traversal of graph.
     * @param graph  contains a reference to simple digraph {@code org.jgrapht.graph.SimpleDirectedGraph}.
     * @return value contains the vertex of current position.
     */
    static Set<String> getChildren( String vertex, SimpleDirectedGraph<String, Relationship> graph )
    {
        Set<String> descendants = new HashSet<>();
        if ( graph == null )
        {
            return null;
        }

        LOG.debug( "getChildren [{}]", vertex );
        Set<Relationship> edges;
        try
        {
            edges = graph.incomingEdgesOf( vertex );
        }
        catch ( java.lang.IllegalArgumentException iae )
        {
            // vertex is leaf.
            return null;
        }
        for ( Relationship edge : edges )
        {
            descendants.add( edge.getChild() );
        }
        return descendants;
    }


    /**
     * Recursively traverse the hierarchical graph and return all of the ascendants of a given node.
     *
     * @param childName   maps to vertex to determine parentage.
     * @param parentName  points to top most ascendant where traversal must stop.
     * @param isInclusive if set to true will include the parentName in the result set.  False will not return specified parentName.
     * @param graph       contains a reference to simple digraph {@code org.jgrapht.graph.SimpleDirectedGraph}.
     * @return Set of names that are parents of given child.
     */
    static Set<String> getAscendants( String childName, String parentName, boolean isInclusive,
        SimpleDirectedGraph<String, Relationship> graph )
    {
        Map<String, String> vx = new HashMap<>();
        // TreeSet will return in sorted order with case insensitive comparator:
        Set<String> parents = new TreeSet<>( String.CASE_INSENSITIVE_ORDER );

        vx.put( VERTEX, childName.toUpperCase() );
        getAscendants( vx, graph, parents, parentName, isInclusive );
        return parents;
    }


    /**
     * Private utility to recursively traverse the hierarchical graph and return all of the ascendants of a given child node.
     *
     * @param vertex      contains node name and acts as cursor for current location.
     * @param graph       contains a reference to simple digraph {@code org.jgrapht.graph.SimpleDirectedGraph}.
     * @param parents     contains the result set of parent nodes.
     * @param stopName    contains the name of node where traversal ends.
     * @param isInclusive if set to true will include the parentName in the result set. False will not return specified parentName.
     * @return Set of names that are parents of given child.
     */
    private static String getAscendants( Map<String, String> vertex, SimpleDirectedGraph<String, Relationship> graph,
        Set<String> parents, String stopName, boolean isInclusive )
    {
        String v = vertex.get( VERTEX );
        if ( v == null )
        {
            return null;
        }
        else if ( graph == null )
        {
            return null;
        }
        LOG.debug( "getAscendants [{}]", v);
        Set<Relationship> edges;
        try
        {
            edges = graph.outgoingEdgesOf( v );
        }
        catch ( java.lang.IllegalArgumentException iae )
        {
            // vertex is leaf.
            return null;
        }
        for ( Relationship edge : edges )
        {
            if ( edge.getParent().equalsIgnoreCase( stopName ) )
            {
                if ( isInclusive )
                {
                    parents.add( edge.getParent() );
                }
                break;
            }
            else
            {
                vertex.put( VERTEX, edge.getParent() );
                parents.add( edge.getParent() );
                v = getAscendants( vertex, graph, parents, stopName, isInclusive );
            }
        }
        return v;
    }


    /**
     * Private utility to return the parents (direct ascendants) of a given child node.
     *
     * @param vertex contains node name and acts as cursor for current location.
     * @param graph  contains a reference to simple digraph {@code org.jgrapht.graph.SimpleDirectedGraph}.
     * @return Set of names that are parents of given child.
     */
    static Set<String> getParents( String vertex, SimpleDirectedGraph<String, Relationship> graph )
    {
        Set<String> parents = new HashSet<>();
        if ( graph == null )
        {
            return null;
        }
        LOG.debug( "getParents [{}]", vertex);
        Set<Relationship> edges;
        try
        {
            edges = graph.outgoingEdgesOf( vertex );
        }
        catch ( java.lang.IllegalArgumentException iae )
        {
            // vertex is leaf.
            return null;
        }
        for ( Relationship edge : edges )
        {
            parents.add( edge.getParent() );
        }
        return parents;
    }


    /**
     * This method will retrieve the list of all parent-child relationships for a given node.  If the node was not found in
     * ldap this method will create a new node and store default data.
     * The following ldap nodes are currently storing hierarchical data:
     * <ol>
     * <li>RBAC Role relations are stored in {@code cn=Hierarchies,ou=Roles,ou=RBAC} ldap node and cached as singleton in {@link RoleUtil}</li>
     * <li>ARBAC Admin Role relations are stored in {@code cn=Hierarchies,ou=AdminRoles,ou=ARBAC} ldap node and cached as singleton in {@link AdminRoleUtil}</li>
     * <li>User Organizational Unit relations are stored in {@code cn=Hierarchies,ou=OS-U,ou=ARBAC} node and cached as {@link org.apache.directory.fortress.core.impl.UsoUtil}</li>
     * <li>Permission Organizational Unit relations are stored in {@code cn=Hierarchies,ou=OS-P,ou=ARBAC} node and cached as {@link org.apache.directory.fortress.core.impl.PsoUtil}</li>
     * </ol>
     *
     * @param contextId maps to sub-tree in DIT, e.g. ou=contextId, dc=example, dc=com.
     * @return reference the the Hier result set retrieved from ldap.
     */
    static Hier loadHier( String contextId, List<Graphable> descendants )
    {
        Hier hier = new Hier();
        if ( CollectionUtils.isNotEmpty( descendants ) )
        {
            hier.setContextId( contextId );
            for ( Graphable descendant : descendants )
            {
                Set<String> parents = descendant.getParents();
                if ( CollectionUtils.isNotEmpty( parents ) )
                {
                    for ( String parent : parents )
                    {
                        Relationship relationship = new Relationship();
                        relationship.setChild( descendant.getName().toUpperCase() );
                        relationship.setParent( parent.toUpperCase() );
                        hier.setRelationship( relationship );
                    }
                }
            }
        }
        return hier;
    }


    /**
     * This api allows synchronized access to allow updates to hierarchical relationships.
     * Method will update the hierarchical data set and reload the JGraphT simple digraph with latest.
     *
     * @param graph contains a reference to simple digraph {@code org.jgrapht.graph.SimpleDirectedGraph}.
     * @param relationship contains parent-child relationship targeted for addition.
     * @param op   used to pass the ldap op {@link Hier.Op#ADD}, {@link Hier.Op#MOD}, {@link org.apache.directory.fortress.core.model.Hier.Op#REM}
     * @throws org.apache.directory.fortress.core.SecurityException in the event of a system error.
     */
    static void updateHier( SimpleDirectedGraph<String, Relationship> graph, Relationship relationship, Hier.Op op )
        throws SecurityException
    {
        if ( op == Hier.Op.ADD )
            HierUtil.addEdge( graph, relationship );
        else if ( op == Hier.Op.REM )
            HierUtil.removeEdge( graph, relationship );
        else
            throw new SecurityException( GlobalErrIds.HIER_CANNOT_PERFORM, CLS_NM
                + "updateHier Cannot perform hierarchical operation" );
    }


    /**
     * Method instantiates a new digraph, {@code org.jgrapht.graph.SimpleDirectedGraph}, using data passed in via
     * {@link Hier} entity.
     *
     * @param hier contains the source data for digraph.
     * @return reference to {@code org.jgrapht.graph.SimpleDirectedGraph}.
     */
    static SimpleDirectedGraph<String, Relationship> buildGraph( Hier hier )
    {
        SimpleDirectedGraph<String, Relationship> graph;
        LOG.debug( "buildGraph is initializing" );
        if ( hier == null )
        {
            String error = "buildGraph detected null hier=";
            LOG.error( error );
            return null;
        }
        graph = toGraph( hier );
        LOG.debug( "buildGraph is success" );
        return graph;
    }
}
