/*
 * 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;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.pdfbox.cos.COSArray;
import org.apache.pdfbox.cos.COSBase;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSInteger;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.cos.COSNumber;

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

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

/**
 * This represents a page node in a pdf document.
 *
 * @author <a href="mailto:ben@benlitchfield.com">Ben Litchfield</a>
 * @version $Revision: 1.8 $
 */
public class PDPageNode implements COSObjectable
{
    private COSDictionary page;

    /**
     * Log instance.
     */
    private static final Log log = LogFactory.getLog(PDPageNode.class);

    /**
     * Creates a new instance of PDPage.
     */
    public PDPageNode()
    {
        page = new COSDictionary();
        page.setItem( COSName.TYPE, COSName.PAGES );
        page.setItem( COSName.KIDS, new COSArray() );
        page.setItem( COSName.COUNT, COSInteger.ZERO );
    }

    /**
     * Creates a new instance of PDPage.
     *
     * @param pages The dictionary pages.
     */
    public PDPageNode( COSDictionary pages )
    {
        page = pages;
    }

    /**
     * This will update the count attribute of the page node.  This only needs to
     * be called if you add or remove pages.  The PDDocument will call this for you
     * when you use the PDDocumnet persistence methods.  So, basically most clients
     * will never need to call this.
     *
     * @return The update count for this node.
     */
    public long updateCount()
    {
        long totalCount = 0;
        List kids = getKids();
        Iterator kidIter = kids.iterator();
        while( kidIter.hasNext() )
        {
            Object next = kidIter.next();
            if( next instanceof PDPage )
            {
                totalCount++;
            }
            else
            {
                PDPageNode node = (PDPageNode)next;
                totalCount += node.updateCount();
            }
        }
        page.setLong( COSName.COUNT, totalCount );
        return totalCount;
    }

    /**
     * This will get the count of descendent page objects.
     *
     * @return The total number of descendent page objects.
     */
    public long getCount()
    {
        if(page == null)
        {
            return 0L;
        }
        COSBase num = page.getDictionaryObject(COSName.COUNT);
        if(num == null)
        {
            return 0L;
        }
        return ((COSNumber) num).intValue();
    }

    /**
     * This will get the underlying dictionary that this class acts on.
     *
     * @return The underlying dictionary for this class.
     */
    public COSDictionary getDictionary()
    {
        return page;
    }

    /**
     * This is the parent page node.
     *
     * @return The parent to this page.
     */
    public PDPageNode getParent()
    {
        PDPageNode parent = null;
        COSDictionary parentDic = (COSDictionary)page.getDictionaryObject(COSName.PARENT, COSName.P);
        if( parentDic != null )
        {
            parent = new PDPageNode( parentDic );
        }
        return parent;
    }

    /**
     * This will set the parent of this page.
     *
     * @param parent The parent to this page node.
     */
    public void setParent( PDPageNode parent )
    {
        page.setItem( COSName.PARENT, parent.getDictionary() );
    }

    /**
     * {@inheritDoc}
     */
    public COSBase getCOSObject()
    {
        return page;
    }

    /**
     * This will return all kids of this node, either PDPageNode or PDPage.
     *
     * @return All direct descendents of this node.
     */
    public List getKids()
    {
        List actuals = new ArrayList();
        COSArray kids = getAllKids(actuals, page, false, new HashSet<COSBase>());
        return new COSArrayList( actuals, kids );
    }

    /**
     * This will return all kids of this node as PDPage.
     *
     * @param result All direct and indirect descendents of this node are added to this list.
     */
    public void getAllKids(List result)
    {
        getAllKids(result, page, true, new HashSet<COSBase>());
    }

    /**
     * This will return all kids of the given page node as PDPage.
     *
     * @param result All direct and optionally indirect descendents of this node are added to this list.
     * @param page Page dictionary of a page node.
     * @param recurse if true indirect descendents are processed recursively
     * @param seen set of objects that have been added
     */
    private static COSArray getAllKids(List result, COSDictionary page, boolean recurse, Set<COSBase> seen)
    {
        if(page == null)
            return null;
        COSArray kids = (COSArray)page.getDictionaryObject( COSName.KIDS );
        if ( kids == null)
        {
            log.error("No Kids found in getAllKids(). Probably a malformed pdf.");
            return null;
        }
        for( int i=0; i<kids.size(); i++ )
        {
            // ignore duplicates (from malformed PDFs)
            if (!seen.contains(kids.get(i)))
            {
                COSBase obj = kids.getObject( i );
                if (obj instanceof COSDictionary)
                {
                    COSDictionary kid = (COSDictionary)obj;
                    if( COSName.PAGE.equals( kid.getDictionaryObject( COSName.TYPE ) ) )
                    {
                        result.add( new PDPage( kid ) );
                    }
                    else
                    {
                        if (recurse)
                        {
                            getAllKids(result, kid, recurse, seen);
                        }
                        else
                        {
                            result.add( new PDPageNode( kid ) );
                        }
                    }
                }
                seen.add(kids.get(i));
            }
        }
        return kids;
    }

    /**
     * This will get the resources at this page node and not look up the hierarchy.
     * This attribute is inheritable, and findResources() should probably used.
     * This will return null if no resources are available at this level.
     *
     * @return The resources at this level in the hierarchy.
     */
    public PDResources getResources()
    {
        PDResources retval = null;
        COSDictionary resources = (COSDictionary)page.getDictionaryObject( COSName.RESOURCES );
        if( resources != null )
        {
            retval = new PDResources( resources );
        }
        return retval;
    }

    /**
     * This will find the resources for this page by looking up the hierarchy until
     * it finds them.
     *
     * @return The resources at this level in the hierarchy.
     */
    public PDResources findResources()
    {
        PDResources retval = getResources();
        PDPageNode parent = getParent();
        if( retval == null && parent != null )
        {
            retval = parent.findResources();
        }
        return retval;
    }

    /**
     * This will set the resources for this page.
     *
     * @param resources The new resources for this page.
     */
    public void setResources( PDResources resources )
    {
        if( resources == null )
        {
            page.removeItem( COSName.RESOURCES );
        }
        else
        {
            page.setItem( COSName.RESOURCES, resources.getCOSDictionary() );
        }
    }

    /**
     * This will get the MediaBox at this page and not look up the hierarchy.
     * This attribute is inheritable, and findMediaBox() should probably used.
     * This will return null if no MediaBox are available at this level.
     *
     * @return The MediaBox at this level in the hierarchy.
     */
    public PDRectangle getMediaBox()
    {
        PDRectangle retval = null;
        COSArray array = (COSArray)page.getDictionaryObject( COSName.MEDIA_BOX );
        if( array != null )
        {
            retval = new PDRectangle( array );
        }
        return retval;
    }

    /**
     * This will find the MediaBox for this page by looking up the hierarchy until
     * it finds them.
     *
     * @return The MediaBox at this level in the hierarchy.
     */
    public PDRectangle findMediaBox()
    {
        PDRectangle retval = getMediaBox();
        PDPageNode parent = getParent();
        if( retval == null && parent != null )
        {
            retval = parent.findMediaBox();
        }
        return retval;
    }

    /**
     * This will set the mediaBox for this page.
     *
     * @param mediaBox The new mediaBox for this page.
     */
    public void setMediaBox( PDRectangle mediaBox )
    {
        if( mediaBox == null )
        {
            page.removeItem( COSName.MEDIA_BOX  );
        }
        else
        {
            page.setItem( COSName.MEDIA_BOX , mediaBox.getCOSArray() );
        }
    }

    /**
     * This will get the CropBox at this page and not look up the hierarchy.
     * This attribute is inheritable, and findCropBox() should probably used.
     * This will return null if no CropBox is available at this level.
     *
     * @return The CropBox at this level in the hierarchy.
     */
    public PDRectangle getCropBox()
    {
        PDRectangle retval = null;
        COSArray array = (COSArray)page.getDictionaryObject( COSName.CROP_BOX );
        if( array != null )
        {
            retval = new PDRectangle( array );
        }
        return retval;
    }

    /**
     * This will find the CropBox for this page by looking up the hierarchy until
     * it finds them.
     *
     * @return The CropBox at this level in the hierarchy.
     */
    public PDRectangle findCropBox()
    {
        PDRectangle retval = getCropBox();
        PDPageNode parent = getParent();
        if( retval == null && parent != null )
        {
            retval = findParentCropBox( parent );
        }

        //default value for cropbox is the media box
        if( retval == null )
        {
            retval = findMediaBox();
        }
        return retval;
    }

    /**
     * This will search for a crop box in the parent and return null if it is not
     * found.  It will NOT default to the media box if it cannot be found.
     *
     * @param node The node
     */
    private PDRectangle findParentCropBox( PDPageNode node )
    {
        PDRectangle rect = node.getCropBox();
        PDPageNode parent = node.getParent();
        if( rect == null && parent != null )
        {
            rect = findParentCropBox( node );
        }
        return rect;
    }

    /**
     * This will set the CropBox for this page.
     *
     * @param cropBox The new CropBox for this page.
     */
    public void setCropBox( PDRectangle cropBox )
    {
        if( cropBox == null )
        {
            page.removeItem( COSName.CROP_BOX );
        }
        else
        {
            page.setItem( COSName.CROP_BOX, cropBox.getCOSArray() );
        }
    }

    /**
     * A value representing the rotation.  This will be null if not set at this level
     * The number of degrees by which the page should
     * be rotated clockwise when displayed or printed. The value must be a multiple
     * of 90.
     *
     * This will get the rotation at this page and not look up the hierarchy.
     * This attribute is inheritable, and findRotation() should probably used.
     * This will return null if no rotation is available at this level.
     *
     * @return The rotation at this level in the hierarchy.
     */
    public Integer getRotation()
    {
        Integer retval = null;
        COSNumber value = (COSNumber)page.getDictionaryObject( COSName.ROTATE );
        if( value != null )
        {
            retval = new Integer( value.intValue() );
        }
        return retval;
    }

    /**
     * This will find the rotation for this page by looking up the hierarchy until
     * it finds them.
     *
     * @return The rotation at this level in the hierarchy.
     */
    public int findRotation()
    {
        int retval = 0;
        Integer rotation = getRotation();
        if( rotation != null )
        {
            retval = rotation.intValue();
        }
        else
        {
            PDPageNode parent = getParent();
            if( parent != null )
            {
                retval = parent.findRotation();
            }
        }

        return retval;
    }

    /**
     * This will set the rotation for this page.
     *
     * @param rotation The new rotation for this page.
     */
    public void setRotation( int rotation )
    {
        page.setInt( COSName.ROTATE, rotation );
    }
}
