/*
 * 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
 *
 *      http://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.pdfbox.pdmodel.interactive.documentnavigation.outline;

import org.apache.pdfbox.cos.COSBase;
import org.apache.pdfbox.cos.COSDictionary;

import org.apache.pdfbox.pdmodel.common.COSObjectable;

/**
 * This represents an node in an outline in a pdf document.
 *
 * @author <a href="mailto:ben@benlitchfield.com">Ben Litchfield</a>
 * @version $Revision: 1.3 $
 */
public class PDOutlineNode implements COSObjectable
{
    /**
     * The dictionary for this node.
     */
    protected COSDictionary node;

    /**
     * Default Constructor.
     */
    public PDOutlineNode()
    {
        node = new COSDictionary();
    }

    /**
     * Default Constructor.
     *
     * @param dict The dictionary storage.
     */
    public PDOutlineNode( COSDictionary dict)
    {
        node = dict;
    }

    /**
     * Convert this standard java object to a COS object.
     *
     * @return The cos object that matches this Java object.
     */
    public COSBase getCOSObject()
    {
        return node;
    }

    /**
     * Convert this standard java object to a COS object.
     *
     * @return The cos object that matches this Java object.
     */
    public COSDictionary getCOSDictionary()
    {
        return node;
    }

    /**
     * Get the parent of this object.  This will either be a DocumentOutline or an OutlineItem.
     *
     * @return The parent of this object, or null if this is the document outline and there
     * is no parent.
     */
    protected PDOutlineNode getParent()
    {
        PDOutlineNode retval = null;
        COSDictionary parent = (COSDictionary)node.getDictionaryObject( "Parent", "P" );
        if( parent != null )
        {
            if( parent.getDictionaryObject( "Parent", "P" ) == null )
            {
                retval = new PDDocumentOutline( parent );
            }
            else
            {
                retval = new PDOutlineItem( parent );
            }
        }

        return retval;
    }

    /**
     * Set the parent of this object, this is maintained by these objects and should not
     * be called by any clients of PDFBox code.
     *
     * @param parent The parent of this object.
     */
    protected void setParent( PDOutlineNode parent )
    {
        node.setItem( "Parent", parent );
    }

    /**
     * append a child node to this node.
     *
     * @param outlineNode The node to add.
     */
    public void appendChild( PDOutlineItem outlineNode )
    {
        outlineNode.setParent( this );
        if( getFirstChild() == null )
        {
            int currentOpenCount = getOpenCount();
            setFirstChild( outlineNode );
            //1 for the item we are adding;
            int numberOfOpenNodesWeAreAdding = 1;
            if( outlineNode.isNodeOpen() )
            {
                numberOfOpenNodesWeAreAdding += outlineNode.getOpenCount();
            }
            if( isNodeOpen() )
            {
                setOpenCount( currentOpenCount + numberOfOpenNodesWeAreAdding );
            }
            else
            {
                setOpenCount( currentOpenCount - numberOfOpenNodesWeAreAdding );
            }
            updateParentOpenCount( numberOfOpenNodesWeAreAdding );
        }
        else
        {
            PDOutlineItem previousLastChild = getLastChild();
            previousLastChild.insertSiblingAfter( outlineNode );
        }
        
        PDOutlineItem lastNode = outlineNode;
        while(lastNode.getNextSibling() != null)
        {
            lastNode = lastNode.getNextSibling();
        }
        setLastChild( lastNode );
    }

    /**
     * Return the first child or null if there is no child.
     *
     * @return The first child.
     */
    public PDOutlineItem getFirstChild()
    {
        PDOutlineItem last = null;
        COSDictionary lastDic = (COSDictionary)node.getDictionaryObject( "First" );
        if( lastDic != null )
        {
            last = new PDOutlineItem( lastDic );
        }
        return last;
    }

    /**
     * Set the first child, this will be maintained by this class.
     *
     * @param outlineNode The new first child.
     */
    protected void setFirstChild( PDOutlineNode outlineNode )
    {
        node.setItem( "First", outlineNode );
    }

    /**
     * Return the last child or null if there is no child.
     *
     * @return The last child.
     */
    public PDOutlineItem getLastChild()
    {
        PDOutlineItem last = null;
        COSDictionary lastDic = (COSDictionary)node.getDictionaryObject( "Last" );
        if( lastDic != null )
        {
            last = new PDOutlineItem( lastDic );
        }
        return last;
    }

    /**
     * Set the last child, this will be maintained by this class.
     *
     * @param outlineNode The new last child.
     */
    protected void setLastChild( PDOutlineNode outlineNode )
    {
        node.setItem( "Last", outlineNode );
    }

    /**
     * Get the number of open nodes.  Or a negative number if this node
     * is closed.  See PDF Reference for more details.  This value
     * is updated as you append children and siblings.
     *
     * @return The Count attribute of the outline dictionary.
     */
    public int getOpenCount()
    {
        return node.getInt( "Count", 0 );
    }

    /**
     * Set the open count.  This number is automatically managed for you
     * when you add items to the outline.
     *
     * @param openCount The new open cound.
     */
    protected void setOpenCount( int openCount )
    {
        node.setInt( "Count", openCount );
    }

    /**
     * This will set this node to be open when it is shown in the viewer.  By default, when
     * a new node is created it will be closed.
     * This will do nothing if the node is already open.
     */
    public void openNode()
    {
        //if the node is already open then do nothing.
        if( !isNodeOpen() )
        {
            int openChildrenCount = 0;
            PDOutlineItem currentChild = getFirstChild();
            while( currentChild != null )
            {
                //first increase by one for the current child
                openChildrenCount++;
                //then increase by the number of open nodes the child has
                if( currentChild.isNodeOpen() )
                {
                    openChildrenCount += currentChild.getOpenCount();
                }
                currentChild = currentChild.getNextSibling();
            }
            setOpenCount( openChildrenCount );
            updateParentOpenCount( openChildrenCount );
        }
    }

    /**
     * Close this node.
     *
     */
    public void closeNode()
    {
        //if the node is already closed then do nothing.
        if( isNodeOpen() )
        {
            int openCount = getOpenCount();
            updateParentOpenCount( -openCount );
            setOpenCount( -openCount );
        }
    }

    /**
     * Node is open if the open count is greater than zero.
     * @return true if this node is open.
     */
    public boolean isNodeOpen()
    {
        return getOpenCount() > 0;
    }

    /**
     * The count parameter needs to be updated when you add or remove elements to
     * the outline.  When you add an element at a lower level then you need to
     * increase all of the parents.
     *
     * @param amount The amount to update by.
     */
    protected void updateParentOpenCount( int amount )
    {
        PDOutlineNode parent = getParent();
        if( parent != null )
        {
            int currentCount = parent.getOpenCount();
            //if the currentCount is negative or it is absent then
            //we will treat it as negative.  The default is to be negative.
            boolean negative = currentCount < 0 ||
                parent.getCOSDictionary().getDictionaryObject( "Count" ) == null;
            currentCount = Math.abs( currentCount );
            currentCount += amount;
            if( negative )
            {
                currentCount = -currentCount;
            }
            parent.setOpenCount( currentCount );
            //recursively call parent to update count, but the parents count is only
            //updated if this is an open node
            if( !negative )
            {
                parent.updateParentOpenCount( amount );
            }
        }
    }
}
