/*
 * 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.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.common.PDDictionaryWrapper;

/**
 * Base class for a node in the outline of a PDF document.
 *
 * @author Ben Litchfield
 */
public abstract class PDOutlineNode extends PDDictionaryWrapper
{
    private static final Logger LOG = LogManager.getLogger(PDOutlineNode.class);

    /**
     * Default Constructor.
     */
    protected PDOutlineNode()
    {
    }

    /**
     * @param dict The dictionary storage.
     */
    protected PDOutlineNode(COSDictionary dict)
    {
        super(dict);
    }

    /**
     * @return The parent of this node or null if there is no parent.
     */
    PDOutlineNode getParent()
    {
        COSDictionary parent = getCOSObject().getCOSDictionary(COSName.PARENT);
        if (parent != null)
        {
            if (COSName.OUTLINES.equals(parent.getCOSName(COSName.TYPE)))
            {
                return new PDDocumentOutline(parent);
            }
            return new PDOutlineItem(parent);
        }
        return null;
    }

    void setParent(PDOutlineNode parent)
    {
        getCOSObject().setItem(COSName.PARENT, parent);
    }

    /**
     * Adds the given node to the bottom of the children list.
     *
     * @param newChild The node to add.
     * @throws IllegalArgumentException if the given node is part of a list (i.e. if it has a previous or a next
     * sibling)
     */
    public void addLast(PDOutlineItem newChild)
    {
        requireSingleNode(newChild);
        append(newChild);
        updateParentOpenCountForAddedChild(newChild);
    }

    /**
     * Adds the given node to the top of the children list.
     * 
     * @param newChild The node to add.
     * @throws IllegalArgumentException if the given node is part of a list (i.e. if it has a previous or a next
     * sibling)
     */
    public void addFirst(PDOutlineItem newChild)
    {
        requireSingleNode(newChild);
        prepend(newChild);
        updateParentOpenCountForAddedChild(newChild);
    }

    /**
     * @param node
     * @throws IllegalArgumentException if the given node is part of a list (i.e. if it has a previous or a next
     * sibling)
     */
    void requireSingleNode(PDOutlineItem node)
    {
        if (node.getNextSibling() != null || node.getPreviousSibling() != null)
        {
            throw new IllegalArgumentException("A single node with no siblings is required");
        }
    }

    /**
     * Appends the child to the linked list of children. This method only adjust pointers but doesn't take care of the
     * Count key in the parent hierarchy.
     * 
     * @param newChild
     */
    private void append(PDOutlineItem newChild)
    {
        newChild.setParent(this);
        if (!hasChildren())
        {
            setFirstChild(newChild);
        }
        else
        {
            PDOutlineItem previousLastChild = getLastChild();
            previousLastChild.setNextSibling(newChild);
            newChild.setPreviousSibling(previousLastChild);
        }
        setLastChild(newChild);
    }

    /**
     * Prepends the child to the linked list of children. This method only adjust pointers but doesn't take care of the
     * Count key in the parent hierarchy.
     * 
     * @param newChild
     */
    private void prepend(PDOutlineItem newChild)
    {
        newChild.setParent(this);
        if (!hasChildren())
        {
            setLastChild(newChild);
        }
        else
        {
            PDOutlineItem previousFirstChild = getFirstChild();
            newChild.setNextSibling(previousFirstChild);
            previousFirstChild.setPreviousSibling(newChild);
        }
        setFirstChild(newChild);
    }

    void updateParentOpenCountForAddedChild(PDOutlineItem newChild)
    {
        int delta = 1;
        if (newChild.isNodeOpen())
        {
            delta += newChild.getOpenCount();
        }
        newChild.updateParentOpenCount(delta);
    }

    /**
     * @return true if the node has at least one child
     */
    public boolean hasChildren()
    {
        return getCOSObject().getCOSDictionary(COSName.FIRST) != null;
    }

    PDOutlineItem getOutlineItem(COSName name)
    {
        COSDictionary outline = getCOSObject().getCOSDictionary(name);
        return outline != null ? new PDOutlineItem(outline) : null;
    }

    /**
     * @return The first child or null if there is no child.
     */
    public PDOutlineItem getFirstChild()
    {
        return getOutlineItem(COSName.FIRST);
    }

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

    /**
     * @return The last child or null if there is no child.
     */
    public PDOutlineItem getLastChild()
    {
        return getOutlineItem(COSName.LAST);
    }

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

    /**
     * Get the number of open nodes or a negative number if this node is closed.
     * See PDF Reference 32000-1:2008 table 152 and 153 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 getCOSObject().getInt(COSName.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 count.
     */
    void setOpenCount(int openCount)
    {
        getCOSObject().setInt(COSName.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() )
        {
            switchNodeCount();
        }
    }

    /**
     * Close this node.
     *
     */
    public void closeNode()
    {
        if (isNodeOpen())
        {
            switchNodeCount();
        }
    }

    private void switchNodeCount()
    {
        int openCount = getOpenCount();
        setOpenCount(-openCount);
        updateParentOpenCount(-openCount);
    }

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

    /**
     * The count parameter needs to be updated when you add, remove, open or close outline items.
     *
     * @param delta The amount to update by.
     */
    void updateParentOpenCount(int delta)
    {
        PDOutlineNode parent = getParent();
        if (parent != null)
        {
            if (getCOSObject() == parent.getCOSObject())
            {
                // PDFBOX-5939
                LOG.warn("Outline parent points to itself");
                return;
            }
            if (parent.isNodeOpen())
            {
                parent.setOpenCount(parent.getOpenCount() + delta);
                parent.updateParentOpenCount(delta);
            }
            else
            {
                parent.setOpenCount(parent.getOpenCount() - delta);
            }
        }
    }

    /**
     * @return An {@link Iterable} view of the items children
     */
    public Iterable<PDOutlineItem> children()
    {
        return () -> new PDOutlineItemIterator(getFirstChild());
    }
}
