/*
 *  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.api.ldap.util.tree;


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

import org.apache.directory.api.i18n.I18n;
import org.apache.directory.api.ldap.model.exception.LdapException;
import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException;
import org.apache.directory.api.ldap.model.exception.LdapUnwillingToPerformException;
import org.apache.directory.api.ldap.model.message.ResultCodeEnum;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.directory.api.ldap.model.name.Rdn;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * A class storing nodes in a tree designed to map DNs.<br>
 * Branch nodes in this tree refers to child nodes. Leaf nodes in the tree
 * don't have any children. <br>
 * A node may contain a reference to an object whose suffix is the path through the
 * nodes of the tree from the root. <br>
 * A node may also have no attached element.<br>
 * Each child node is referenced by a Rdn, and holds the full Dn corresponding to its position<br>
 *
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 * @param <N> The type of node we store
 */
public class DnNode<N>
{
    /** The logger for this class */
    private static final Logger LOG = LoggerFactory.getLogger( DnNode.class );

    /** The stored element */
    private N nodeElement;

    /** The node's key */
    private Rdn nodeRdn;

    /** The node's Dn */
    private Dn nodeDn;

    /** The node's depth in the tree */
    private int depth;

    /** The parent, if any */
    private DnNode<N> parent;

    /** Stores the list of all the descendant */
    private Map<String, DnNode<N>> children;


    //-------------------------------------------------------------------------
    // Constructors
    //-------------------------------------------------------------------------
    /**
     * Creates a new instance of DnNode.
     */
    public DnNode()
    {
        children = new HashMap<>();
        nodeDn = Dn.EMPTY_DN;
        nodeRdn = Rdn.EMPTY_RDN;
    }


    /**
     * Creates a new instance of DnNode.
     *
     * @param element the element to store
     */
    public DnNode( N element )
    {
        this.nodeElement = element;
        children = new HashMap<>();
    }


    /**
     * Creates a new instance of DnNode.
     *
     * @param dn the node's Dn
     * @param element the element to store
     */
    public DnNode( Dn dn, N element )
    {
        if ( ( dn == null ) || ( dn.isEmpty() ) )
        {
            children = new HashMap<>();
            this.nodeDn = Dn.EMPTY_DN;

            return;
        }

        try
        {
            DnNode<N> rootNode = createNode( dn, element, dn.size() );

            // Now copy back the created node into this
            this.children = rootNode.children;
            this.depth = rootNode.depth;
            this.nodeDn = rootNode.nodeDn;
            this.nodeElement = rootNode.nodeElement;
            this.nodeRdn = rootNode.nodeRdn;
            this.parent = null;
        }
        catch ( LdapException le )
        {
            // Special cas e: the Dn is empty, this is not allowed
            throw new IllegalArgumentException( le.getMessage(), le );
        }
    }


    //-------------------------------------------------------------------------
    // Helper methods
    //-------------------------------------------------------------------------
    /**
     * Check that the Dn is not null
     * 
     * @param dn The Dn to check
     * @throws LdapException If teh Dn is null or empty
     */
    private void checkDn( Dn dn ) throws LdapException
    {
        if ( ( dn == null ) || dn.isEmpty() )
        {
            String message = I18n.err( I18n.ERR_12000_CANNOT_PROCESS_EMPTY_DN );
            LOG.error( message );
            throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM, message );
        }
    }


    /**
     * Create a new DnNode, recursively creating all the intermediate nodes.
     *
     * @param dn The nod's Dn
     * @param element The element to set
     * @param nbRdns The number of RDNs in the Dn
     * @param nbRdns The number of level to create
     * @return The created Node
     * @throws LdapException If the Dn is null or empty
     */
    private DnNode<N> createNode( Dn dn, N element, int nbRdns ) throws LdapException
    {
        checkDn( dn );

        DnNode<N> rootNode = null;

        // No parent : add from the current position
        for ( Rdn rdn : dn.getRdns() )
        {
            if ( nbRdns == 0 )
            {
                break;
            }

            if ( rootNode == null )
            {
                // Create the new top node
                DnNode<N> node = new DnNode<>( element );
                node.nodeRdn = rdn;
                node.nodeDn = dn;
                node.depth = dn.size() + depth;

                rootNode = node;
            }
            else
            {
                DnNode<N> node = new DnNode<>();
                node.nodeRdn = rdn;
                node.nodeDn = rootNode.nodeDn.getParent();
                node.depth = node.nodeDn.size() + depth;
                rootNode.parent = node;
                node.children.put( rootNode.nodeRdn.getNormName(), rootNode );
                rootNode = node;
            }

            nbRdns--;
        }

        return rootNode;
    }


    /**
     * Store the given element into the node
     * 
     * @param element The element to set
     */
    private synchronized void setElement( N element )
    {
        this.nodeElement = element;
    }


    /**
     * Tells if the implementation is a leaf node. If it's a branch node
     * then false is returned.
     *
     * @return <code>true</code> if the class is a leaf node, false otherwise.
     */
    public synchronized boolean isLeaf()
    {
        return !hasChildren();
    }


    /**
     * Tells if the implementation is a leaf node. If it's a branch node
     * then false is returned.
     *
     * @param dn The Dn we want to check
     * @return <code>true</code> if this is a leaf node, false otherwise.
     */
    public synchronized boolean isLeaf( Dn dn )
    {
        DnNode<N> node = getNode( dn );

        if ( node == null )
        {
            return false;
        }

        return node.children.size() == 0;
    }


    /**
     * Returns the number of entries under this node. It includes
     * the node itself, plus the number of all it children and descendants.
     *
     * @return The number of descendents
     */
    public synchronized int size()
    {
        // The node itself
        int size = 1;

        // Iterate through the children if any
        if ( children.size() != 0 )
        {
            for ( DnNode<N> node : children.values() )
            {
                size += node.size();
            }
        }

        return size;
    }


    /**
     * @return Return the stored element, if any
     */
    public synchronized N getElement()
    {
        return nodeElement;
    }


    /**
     * @return Return the stored element, if any
     * @param dn The Dn we want to get the element for
     */
    public synchronized N getElement( Dn dn )
    {
        DnNode<N> node = getNode( dn );

        if ( node == null )
        {
            return null;
        }

        return node.nodeElement;
    }


    /**
     * @return True if the Node stores an element. BranchNode may not hold any
     * element.
     */
    public synchronized boolean hasElement()
    {
        return nodeElement != null;
    }


    /**
     * @return True if the Node stores an element. BranchNode may not hold any
     * element.
     * @param dn The Dn we want to get the element for
     */
    public synchronized boolean hasElement( Dn dn )
    {
        DnNode<N> node = getNode( dn );

        if ( node == null )
        {
            return false;
        }

        return node.nodeElement != null;
    }


    /**
     * Recursively check if the node has a descendant having an element
     * 
     * @param node The node to start from
     * @return <tt>true</tt> if the node has some descendant
     */
    private synchronized boolean hasDescendantElement( DnNode<N> node )
    {
        if ( node == null )
        {
            return false;
        }

        if ( node.hasElement() )
        {
            return true;
        }

        for ( DnNode<N> child : node.getChildren().values() )
        {
            if ( hasDescendantElement( child ) )
            {
                return true;
            }
        }

        // Nothing found ...
        return false;
    }


    /**
     * @return True if one of the node below the current node has one element, 
     * False otherwise
     * @param dn The Dn we want to get the element for
     */
    public synchronized boolean hasDescendantElement( Dn dn )
    {
        DnNode<N> node = getNode( dn );

        if ( node == null )
        {
            return false;
        }

        // We must be at the right place in the tree
        if ( node.getDn().size() != dn.size() )
        {
            return false;
        }

        if ( node.hasChildren() )
        {
            for ( DnNode<N> child : node.getChildren().values() )
            {
                if ( hasDescendantElement( child ) )
                {
                    return true;
                }
            }
        }

        return false;
    }


    /**
     * Recursively get all the elements from nodes having an element
     * 
     * @param node The node to start with
     * @param descendants The list of descendant to fulfill
     */
    private synchronized void getDescendantElements( DnNode<N> node, List<N> descendants )
    {
        if ( node == null )
        {
            return;
        }

        if ( node.hasElement() )
        {
            descendants.add( node.getElement() );

            // Stop here
            return;
        }

        for ( DnNode<N> child : node.getChildren().values() )
        {
            getDescendantElements( child, descendants );
        }
    }


    /**
     * @return True if one of the node below the current node has one element, 
     * False otherwise
     * @param dn The Dn we want to get the element for
     */
    public synchronized List<N> getDescendantElements( Dn dn )
    {
        List<N> descendants = new ArrayList<>();

        DnNode<N> node = getNode( dn );

        if ( node == null )
        {
            return descendants;
        }

        // We must be at the right place in the tree
        if ( node.getDn().size() != dn.size() )
        {
            return descendants;
        }

        if ( node.hasChildren() )
        {
            for ( DnNode<N> child : node.getChildren().values() )
            {
                getDescendantElements( child, descendants );
            }
        }

        return descendants;
    }


    /**
     * Tells if the current DnNode has some children or not
     *
     * @return <code>true</code> if the node has some children
     */
    public synchronized boolean hasChildren()
    {
        return ( children != null ) && children.size() != 0;
    }


    /**
     * Tells if a node has some children or not.
     *
     * @param dn the node's Dn
     * @return <code>true</code> if the node has some children
     * @throws LdapException if the Dn is null or empty
     */
    public synchronized boolean hasChildren( Dn dn ) throws LdapException
    {
        checkDn( dn );

        DnNode<N> node = getNode( dn );

        return ( node != null ) && node.hasChildren();
    }


    /**
     * @return The list of DnNode
     */
    public synchronized Map<String, DnNode<N>> getChildren()
    {
        return children;
    }


    /**
     * @return The parent DnNode, if any
     */
    public synchronized DnNode<N> getParent()
    {
        return parent;
    }


    /**
     * @return True if the current DnNode has a parent
     */
    public synchronized boolean hasParent()
    {
        return parent != null;
    }


    /**
     * Tells if there is a parent for a given Dn,. This parent should be a
     * subset of the given dn.<br>
     * For instance, if we have stored dc=acme, dc=org into the tree,
     * the Dn: ou=example, dc=acme, dc=org will have a parent
     * <br>For the Dn ou=apache, dc=org, there is no parent, so false will be returned.
     *
     * @param dn the normalized distinguished name to resolve to a parent
     * @return true if there is a parent associated with the normalized dn
     */
    public synchronized boolean hasParent( Dn dn )
    {
        List<Rdn> rdns = dn.getRdns();

        DnNode<N> currentNode = this;
        DnNode<N> parentNode = null;

        // Iterate through all the Rdn until we find the associated element
        for ( int i = rdns.size() - 1; i >= 0; i-- )
        {
            Rdn rdn = rdns.get( i );

            if ( rdn.equals( currentNode.nodeRdn ) )
            {
                parentNode = currentNode;
            }
            else if ( currentNode.hasChildren() )
            {
                currentNode = currentNode.children.get( rdn.getNormName() );

                if ( currentNode == null )
                {
                    break;
                }

                parentNode = currentNode;
            }
            else
            {
                break;
            }
        }

        return parentNode != null;
    }


    /**
     * Add a new node in the tree. The added node won't have any element.
     *
     * @param dn The node's Dn
     * @return the corresponding node
     * @throws LdapException if the Dn is null or empty
     */
    public synchronized DnNode<N> add( Dn dn ) throws LdapException
    {
        return add( dn, null );
    }


    /**
     * Add a new node in the tree. We can't add a node if its Dn is empty. The
     * added element is attached to the node, which is named by the Dn's Rdn.<br>
     *
     * @param dn The node's Dn
     * @param element The element to associate with this Node. Can be null.
     * @return the corresponding node
     * @throws LdapException if the Dn is null or empty
     */
    public synchronized DnNode<N> add( Dn dn, N element ) throws LdapException
    {
        checkDn( dn );

        // We first have to find the Node which will be the parent
        DnNode<N> parentNode = getNode( dn );

        if ( parentNode == null )
        {
            // No parent : add a new node to the root
            DnNode<N> childNode = createNode( dn, element, dn.size() );
            childNode.parent = this;
            children.put( childNode.nodeRdn.getNormName(), childNode );
            
            return childNode;
        }
        else
        {
            // We have a parent. Add the new node to the found parent
            int nbRdns = dn.size() - parentNode.depth;

            if ( nbRdns == 0 )
            {
                // That means the added Dn is already present. Check if it already has an element
                if ( parentNode.hasElement() )
                {
                    String message = I18n.err( I18n.ERR_12001_CANNOT_ADD_NODE_CHILD_EXISTS );
                    LOG.error( message );
                    throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM, message );
                }
                // We may try to add twice the same Dn, without any element
                else if ( element == null )
                {
                    String message = I18n.err( I18n.ERR_12002_CANNOT_ADD_NODE_ALREADY_EXISTS );
                    LOG.error( message );
                    throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM, message );
                }
                // All is fine : we are just injecting some data into an existing node
                else
                {
                    parentNode.setElement( element );
                    
                    return parentNode;
                }
            }
            else
            {
                DnNode<N> childNode = createNode( dn, element, nbRdns );

                // done. now, add the newly created tree to the parent node
                childNode.parent = parentNode;
                parentNode.children.put( childNode.nodeRdn.getNormName(), childNode );

                return childNode;
            }
        }
    }


    /**
     * Removes a node from the tree.
     *
     * @param dn the node's Dn
     * @throws LdapException if the Dn is null or empty
     */
    public synchronized void remove( Dn dn ) throws LdapException
    {
        checkDn( dn );

        // Find the parent first : we won't be able to remove
        // a node if it's not present in the tree !
        DnNode<N> parentNode = getNode( dn );

        if ( parentNode == null )
        {
            return;
        }

        // Now, check that this parent has the same Dn than the one
        // we gave and that there is no children
        if ( ( dn.size() != parentNode.depth ) || parentNode.hasChildren() )
        {
            return;
        }

        // Ok, no children, same Dn, let's remove what we can.
        parentNode = parentNode.getParent();

        for ( Rdn rdn : dn.getRdns() )
        {
            parentNode.children.remove( rdn.getNormName() );

            if ( parentNode.children.size() > 0 )
            {
                // We have to stop here, because the parent's node is shared with other Node.
                break;
            }

            parentNode = parentNode.getParent();
        }
    }


    /**
     * Tells if the current DnBranchNode contains another node associated
     * with an rdn.
     *
     * @param rdn The name we are looking for
     * @return <code>true</code> if the tree instance contains this name
     */
    public synchronized boolean contains( Rdn rdn )
    {
        return children.containsKey( rdn.getNormName() );
    }


    /**
     * Get's a child using an rdn string.
     *
     * @param rdn the rdn to use as the node key
     * @return the child node corresponding to the rdn.
     */
    public synchronized DnNode<N> getChild( Rdn rdn )
    {
        if ( children.containsKey( rdn.getNormName() ) )
        {
            return children.get( rdn.getNormName() );
        }

        return null;
    }


    /**
     * @return The Node's Rdn
     */
    public synchronized Rdn getRdn()
    {
        return nodeRdn;
    }


    /**
     * Get the Node for a given Dn, if present in the tree.<br>
     * For instance, if we have stored dc=acme, dc=org into the tree,
     * the Dn: ou=example, dc=acme, dc=org will have a parent, and
     * dc=acme, dc=org will be returned.
     * <br>For the Dn ou=apache, dc=org, there is no parent, so null will be returned.
     *
     * @param dn the normalized distinguished name to resolve to a parent
     * @return the Node associated with the normalized dn
     */
    public synchronized DnNode<N> getNode( Dn dn )
    {
        DnNode<N> currentNode = this;
        DnNode<N> parentNode = null;

        // Iterate through all the Rdn until we find the associated partition
        for ( int i = dn.size() - 1; i >= 0; i-- )
        {
            Rdn rdn = dn.getRdn( i );

            if ( currentNode.hasChildren() )
            {
                currentNode = currentNode.children.get( rdn.getNormName() );

                if ( currentNode == null )
                {
                    break;
                }

                parentNode = currentNode;
            }
            else
            {
                break;
            }
        }

        return parentNode;
    }


    /**
     * Get the closest Node for a given Dn which has an element, if present in the tree.<br>
     * For instance, if we have stored dc=acme, dc=org into the tree,
     * the Dn: ou=example, dc=acme, dc=org will have a parent, and
     * dc=acme, dc=org will be returned if it has an associated element.
     * <br>For the Dn ou=apache, dc=org, there is no parent, so null will be returned.
     *
     * @param dn the normalized distinguished name to resolve to a parent
     * @return the Node associated with the normalized dn
     */
    public synchronized boolean hasParentElement( Dn dn )
    {
        List<Rdn> rdns = dn.getRdns();

        DnNode<N> currentNode = this;
        boolean hasElement = false;

        // Iterate through all the Rdn until we find the associated partition
        for ( int i = rdns.size() - 1; i >= 0; i-- )
        {
            Rdn rdn = rdns.get( i );

            if ( currentNode.hasChildren() )
            {
                currentNode = currentNode.children.get( rdn.getNormName() );

                if ( currentNode == null )
                {
                    break;
                }

                if ( currentNode.hasElement() )
                {
                    hasElement = true;
                }

                parent = currentNode;
            }
            else
            {
                break;
            }
        }

        return hasElement;
    }


    /**
     * Get the closest Node for a given Dn which has an element, if present in the tree.<br>
     * For instance, if we have stored dc=acme, dc=org into the tree,
     * the Dn: ou=example, dc=acme, dc=org will have a parent, and
     * dc=acme, dc=org will be returned if it has an associated element.
     * <br>For the Dn ou=apache, dc=org, there is no parent, so null will be returned.
     *
     * @param dn the normalized distinguished name to resolve to a parent
     * @return the Node associated with the normalized dn
     */
    public synchronized DnNode<N> getParentWithElement( Dn dn )
    {
        List<Rdn> rdns = dn.getRdns();

        DnNode<N> currentNode = this;
        DnNode<N> element = null;

        // Iterate through all the Rdn until we find the associated partition
        for ( int i = rdns.size() - 1; i >= 1; i-- )
        {
            Rdn rdn = rdns.get( i );

            if ( currentNode.hasChildren() )
            {
                currentNode = currentNode.children.get( rdn.getNormName() );

                if ( currentNode == null )
                {
                    break;
                }

                if ( currentNode.hasElement() )
                {
                    element = currentNode;
                }

                parent = currentNode;
            }
            else
            {
                break;
            }
        }

        return element;
    }


    /**
     * Get the closest Node for a given Dn which has an element, if present in the tree.<br>
     * For instance, if we have stored dc=acme, dc=org into the tree,
     * the Dn: ou=example, dc=acme, dc=org will have a parent, and
     * dc=acme, dc=org will be returned if it has an associated element.
     * <br>For the Dn ou=apache, dc=org, there is no parent, so null will be returned.
     *
     * @return the Node associated with the normalized dn
     */
    public synchronized DnNode<N> getParentWithElement()
    {
        DnNode<N> currentNode = parent;

        while ( currentNode != null )
        {
            if ( currentNode.nodeElement != null )
            {
                return currentNode;
            }

            currentNode = currentNode.parent;
        }

        return null;
    }


    /**
     * rename the DnNode's Dn
     * 
     * @param newRdn the new Rdn of this node
     * @throws LdapException If the rename failed
     */
    public synchronized void rename( Rdn newRdn ) throws LdapException
    {
        Dn temp = nodeDn.getParent();
        temp = temp.add( newRdn );

        Rdn oldRdn = nodeRdn;

        nodeRdn = temp.getRdn();
        nodeDn = temp;

        if ( parent != null )
        {
            parent.children.remove( oldRdn.getNormName() );
            parent.children.put( nodeRdn.getNormName(), this );
        }

        updateAfterModDn( nodeDn );
    }


    /**
     * move the DnNode's Dn
     *
     * @param newParent the new parent Dn
     * @throws LdapException If the move failed
     */
    public synchronized void move( Dn newParent ) throws LdapException
    {
        DnNode<N> tmp = null;

        Dn tmpDn = null;

        // check if the new parent Dn is child of the parent
        if ( newParent.isDescendantOf( parent.nodeDn ) )
        {
            tmp = parent;
            tmpDn = parent.nodeDn;
        }

        // if yes, then drill for the new parent node
        if ( tmpDn != null )
        {
            int parentNodeSize = tmpDn.size();
            int count = newParent.size() - parentNodeSize;

            while ( count-- > 0 )
            {
                tmp = tmp.getChild( newParent.getRdn( parentNodeSize++ ) );
            }
        }

        // if not, we have to traverse all the way up to the 
        // root node and then find the new parent node
        if ( tmp == null )
        {
            tmp = this;
            while ( tmp.parent != null )
            {
                tmp = tmp.parent;
            }

            tmp = tmp.getNode( newParent );
        }

        nodeDn = newParent.add( nodeRdn );
        updateAfterModDn( nodeDn );

        if ( parent != null )
        {
            parent.children.remove( nodeRdn.getNormName() );
        }

        parent = tmp;
        parent.children.put( nodeRdn.getNormName(), this );
    }


    /**
     * update the children's Dn based on the new parent Dn created
     * after a rename or move operation
     * 
     * @param newParentDn The new parent's Dn
     * @throws LdapInvalidDnException The parent DN is invalid
     */
    private synchronized void updateAfterModDn( Dn newParentDn ) throws LdapInvalidDnException
    {
        if ( children != null )
        {
            for ( DnNode<N> child : children.values() )
            {
                child.nodeDn = newParentDn.add( child.nodeRdn );
                child.updateAfterModDn( child.nodeDn );
            }
        }
    }


    private String toString( String tabs )
    {
        if ( nodeRdn == null )
        {
            return tabs;
        }

        StringBuilder sb = new StringBuilder();
        sb.append( tabs );

        boolean hasChildren = hasChildren();

        if ( isLeaf() )
        {
            sb.append( "Leaf[" ).append( nodeDn ).append( "]: " ).append( "'" ).append( nodeElement ).append( "'" );
            return sb.toString();
        }

        sb.append( "Branch[" ).append( nodeDn ).append( "]: " );

        if ( nodeElement != null )
        {
            sb.append( "'" ).append( nodeElement ).append( "'" );
        }

        tabs += "    ";

        sb.append( '\n' );

        boolean isFirst = true;

        if ( hasChildren )
        {
            for ( Map.Entry<String, DnNode<N>> entry : children.entrySet() )
            {
                if ( isFirst )
                {
                    isFirst = false;
                }
                else
                {
                    sb.append( "\n" );
                }

                DnNode<N> child = entry.getValue();

                sb.append( child.toString( tabs ) );
            }
        }

        return sb.toString();
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public String toString()
    {
        return toString( "" );
    }


    /**
     * @return the dn
     */
    public synchronized Dn getDn()
    {
        return nodeDn;
    }
}
