/*
 * 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.sejda.sambox.pdmodel.interactive.documentnavigation.outline;

import java.awt.Color;
import java.io.IOException;

import org.sejda.sambox.cos.COSArray;
import org.sejda.sambox.cos.COSDictionary;
import org.sejda.sambox.cos.COSFloat;
import org.sejda.sambox.cos.COSName;
import org.sejda.sambox.pdmodel.PDDocument;
import org.sejda.sambox.pdmodel.PDPage;
import org.sejda.sambox.pdmodel.documentinterchange.logicalstructure.PDStructureElement;
import org.sejda.sambox.pdmodel.graphics.color.PDColor;
import org.sejda.sambox.pdmodel.graphics.color.PDDeviceRGB;
import org.sejda.sambox.pdmodel.interactive.action.PDAction;
import org.sejda.sambox.pdmodel.interactive.action.PDActionFactory;
import org.sejda.sambox.pdmodel.interactive.action.PDActionGoTo;
import org.sejda.sambox.pdmodel.interactive.documentnavigation.destination.PDDestination;
import org.sejda.sambox.pdmodel.interactive.documentnavigation.destination.PDNamedDestination;
import org.sejda.sambox.pdmodel.interactive.documentnavigation.destination.PDPageDestination;
import org.sejda.sambox.pdmodel.interactive.documentnavigation.destination.PDPageXYZDestination;

/**
 * This represents an outline item in a pdf document. The items at each level of the hierarchy form an iterable linked
 * list, chained together through their Prev and Next entries.
 *
 * @author Ben Litchfield
 */
public final class PDOutlineItem extends PDOutlineNode
{
    private static final int ITALIC_FLAG = 1;
    private static final int BOLD_FLAG = 2;

    public PDOutlineItem()
    {
        super();
    }

    /**
     * Constructor for an existing outline item.
     *
     * @param dic The storage dictionary.
     */
    public PDOutlineItem(COSDictionary dic)
    {
        super(dic);
    }

    /**
     * Insert a single sibling after this node.
     *
     * @param newSibling The item to insert.
     * @throws IllegalArgumentException if the given sibling node is part of a list (i.e. if it has a previous or a next
     * sibling)
     */
    public void insertSiblingAfter(PDOutlineItem newSibling)
    {
        requireSingleNode(newSibling);
        PDOutlineNode parent = getParent();
        newSibling.setParent(parent);
        PDOutlineItem next = getNextSibling();
        setNextSibling(newSibling);
        newSibling.setPreviousSibling(this);
        if (next != null)
        {
            newSibling.setNextSibling(next);
            next.setPreviousSibling(newSibling);
        }
        else if (parent != null)
        {
            getParent().setLastChild(newSibling);
        }
        updateParentOpenCountForAddedChild(newSibling);
    }

    /**
     * Insert a single sibling before this node.
     *
     * @param newSibling The item to insert.
     * @throws IllegalArgumentException if the given sibling node is part of a list (i.e. if it has a previous or a next
     * sibling)
     */
    public void insertSiblingBefore(PDOutlineItem newSibling)
    {
        requireSingleNode(newSibling);
        PDOutlineNode parent = getParent();
        newSibling.setParent(parent);
        PDOutlineItem previous = getPreviousSibling();
        setPreviousSibling(newSibling);
        newSibling.setNextSibling(this);
        if (previous != null)
        {
            previous.setNextSibling(newSibling);
            newSibling.setPreviousSibling(previous);
        }
        else if (parent != null)
        {
            getParent().setFirstChild(newSibling);
        }
        updateParentOpenCountForAddedChild(newSibling);
    }

    /**
     * Return the previous sibling or null if there is no sibling.
     *
     * @return The previous sibling.
     */
    public PDOutlineItem getPreviousSibling()
    {
        return getOutlineItem(COSName.PREV);
    }

    /**
     * Set the previous sibling, this will be maintained by this class.
     *
     * @param outlineNode The new previous sibling.
     */
    void setPreviousSibling(PDOutlineNode outlineNode)
    {
        getCOSObject().setItem(COSName.PREV, outlineNode);
    }

    /**
     * @return The next sibling or null if there is no next sibling.
     */
    public PDOutlineItem getNextSibling()
    {
        return getOutlineItem(COSName.NEXT);
    }

    /**
     * Set the next sibling, this will be maintained by this class.
     *
     * @param outlineNode The new next sibling.
     */
    void setNextSibling(PDOutlineNode outlineNode)
    {
        getCOSObject().setItem(COSName.NEXT, outlineNode);
    }

    /**
     * Get the title of this node.
     *
     * @return The title of this node.
     */
    public String getTitle()
    {
        return getCOSObject().getString(COSName.TITLE);
    }

    /**
     * Set the title for this node.
     *
     * @param title The new title for this node.
     */
    public void setTitle(String title)
    {
        getCOSObject().setString(COSName.TITLE, title);
    }

    /**
     * Get the page destination of this node.
     *
     * @return The page destination of this node.
     * @throws IOException If there is an error creating the destination.
     */
    public PDDestination getDestination() throws IOException
    {
        return PDDestination.create(getCOSObject().getDictionaryObject(COSName.DEST));
    }

    /**
     * Set the page destination for this node.
     *
     * @param dest The new page destination for this node.
     */
    public void setDestination(PDDestination dest)
    {
        getCOSObject().setItem(COSName.DEST, dest);
    }

    /**
     * A convenience method that will create an XYZ destination using only the defaults.
     *
     * @param page The page to refer to.
     */
    public void setDestination(PDPage page)
    {
        PDPageXYZDestination dest = null;
        if (page != null)
        {
            dest = new PDPageXYZDestination();
            dest.setPage(page);
        }
        setDestination(dest);
    }

    /**
     * This method will attempt to find the page in this PDF document that this outline points to. If the outline does
     * not point to anything then this method will return null. If the outline is an action that is not a GoTo action
     * then this method will also return null.
     *
     * @param doc The document to get the page from.
     *
     * @return The page that this outline will go to when activated or null if it does not point to anything.
     * @throws IOException If there is an error when trying to find the page.
     */
    public PDPage findDestinationPage(PDDocument doc) throws IOException
    {
        PDDestination dest = getDestination();
        if (dest == null)
        {
            PDAction outlineAction = getAction();
            if (outlineAction instanceof PDActionGoTo)
            {
                dest = ((PDActionGoTo) outlineAction).getDestination();
            }
        }
        if (dest == null)
        {
            return null;
        }

        PDPageDestination pageDestination = null;
        if (dest instanceof PDNamedDestination)
        {
            pageDestination = doc.getDocumentCatalog()
                    .findNamedDestinationPage((PDNamedDestination) dest);
            if (pageDestination == null)
            {
                return null;
            }
        }
        else if (dest instanceof PDPageDestination)
        {
            pageDestination = (PDPageDestination) dest;
        }
        else
        {
            throw new IOException("Error: Unknown destination type " + dest);
        }

        PDPage page = pageDestination.getPage();
        if (page == null)
        {
            // Malformed PDF: local destinations must have a page object,
            // not a page number, these are meant for remote destinations.
            int pageNumber = pageDestination.getPageNumber();
            if (pageNumber != -1)
            {
                page = doc.getPage(pageNumber);
            }
        }
        return page;
    }

    /**
     * Get the action of this node.
     *
     * @return The action of this node.
     */
    public PDAction getAction()
    {
        return PDActionFactory
                .createAction(getCOSObject().getDictionaryObject(COSName.A, COSDictionary.class));
    }

    /**
     * Set the action for this node.
     *
     * @param action The new action for this node.
     */
    public void setAction(PDAction action)
    {
        getCOSObject().setItem(COSName.A, action);
    }

    /**
     * Get the structure element of this node.
     *
     * @return The structure element of this node.
     */
    public PDStructureElement getStructureElement()
    {
        PDStructureElement se = null;
        COSDictionary dic = getCOSObject().getDictionaryObject(COSName.SE, COSDictionary.class);
        if (dic != null)
        {
            se = new PDStructureElement(dic);
        }
        return se;
    }

    /**
     * Set the structure element for this node.
     *
     * @param structureElement The new structure element for this node.
     */
    public void setStructureElement(PDStructureElement structureElement)
    {
        getCOSObject().setItem(COSName.SE, structureElement);
    }

    /**
     * Get the RGB text color of this node. Default is black and this method will never return null.
     *
     * @return The structure element of this node.
     */
    public PDColor getTextColor()
    {
        COSArray csValues = getCOSObject().getDictionaryObject(COSName.C, COSArray.class);
        if (csValues == null)
        {
            csValues = new COSArray();
            csValues.growToSize(3, new COSFloat(0));
            getCOSObject().setItem(COSName.C, csValues);
        }
        return new PDColor(csValues, PDDeviceRGB.INSTANCE);
    }

    /**
     * Set the RGB text color for this node.
     *
     * @param textColor The text color for this node.
     */
    public void setTextColor(PDColor textColor)
    {
        getCOSObject().setItem(COSName.C, textColor.toComponentsCOSArray());
    }

    /**
     * Set the RGB text color for this node.
     *
     * @param textColor The text color for this node.
     */
    public void setTextColor(Color textColor)
    {
        COSArray array = new COSArray();
        array.add(new COSFloat(textColor.getRed() / 255f));
        array.add(new COSFloat(textColor.getGreen() / 255f));
        array.add(new COSFloat(textColor.getBlue() / 255f));
        getCOSObject().setItem(COSName.C, array);
    }

    /**
     * A flag telling if the text should be italic.
     *
     * @return The italic flag.
     */
    public boolean isItalic()
    {
        return getCOSObject().getFlag(COSName.F, ITALIC_FLAG);
    }

    /**
     * Set the italic property of the text.
     *
     * @param italic The new italic flag.
     */
    public void setItalic(boolean italic)
    {
        getCOSObject().setFlag(COSName.F, ITALIC_FLAG, italic);
    }

    /**
     * A flag telling if the text should be bold.
     *
     * @return The bold flag.
     */
    public boolean isBold()
    {
        return getCOSObject().getFlag(COSName.F, BOLD_FLAG);
    }

    /**
     * Set the bold property of the text.
     *
     * @param bold The new bold flag.
     */
    public void setBold(boolean bold)
    {
        getCOSObject().setFlag(COSName.F, BOLD_FLAG, bold);
    }

}
