TagTreePointer.java

/*
    This file is part of the iText (R) project.
    Copyright (c) 1998-2025 Apryse Group NV
    Authors: Apryse Software.

    This program is offered under a commercial and under the AGPL license.
    For commercial licensing, contact us at https://itextpdf.com/sales.  For AGPL licensing, see below.

    AGPL licensing:
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
package com.itextpdf.kernel.pdf.tagutils;

import com.itextpdf.io.logs.IoLogMessageConstant;
import com.itextpdf.kernel.exceptions.PdfException;
import com.itextpdf.kernel.exceptions.KernelExceptionMessageConstant;
import com.itextpdf.kernel.pdf.PdfArray;
import com.itextpdf.kernel.pdf.PdfDictionary;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfIndirectReference;
import com.itextpdf.kernel.pdf.PdfName;
import com.itextpdf.kernel.pdf.PdfNumber;
import com.itextpdf.kernel.pdf.PdfObject;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.pdf.PdfStream;
import com.itextpdf.kernel.pdf.annot.PdfAnnotation;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.kernel.pdf.tagging.IStructureNode;
import com.itextpdf.kernel.pdf.tagging.PdfMcr;
import com.itextpdf.kernel.pdf.tagging.PdfMcrDictionary;
import com.itextpdf.kernel.pdf.tagging.PdfMcrNumber;
import com.itextpdf.kernel.pdf.tagging.PdfNamespace;
import com.itextpdf.kernel.pdf.tagging.PdfObjRef;
import com.itextpdf.kernel.pdf.tagging.PdfStructTreeRoot;
import com.itextpdf.kernel.pdf.tagging.PdfStructElem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;

/**
 * {@link TagTreePointer} class is used to modify the document's tag tree. At any given moment, instance of this class
 * 'points' at the specific position in the tree (at the specific tag), however every instance can be freely moved around
 * the tree primarily using {@link #moveToKid} and {@link #moveToParent()} methods. For the current tag you can add new tags,
 * modify it's role and properties, etc. Also, using instance of this class, you can change tag position in the tag structure,
 * you can flush current tag or remove it.
 * <p>
 * There could be any number of the instances of this class, simultaneously pointing to different (or the same) parts of
 * the tag structure. Because of this, you can for example remove the tag at which another instance is currently pointing.
 * In this case, this another instance becomes invalid, and invocation of any method on it will result in exception. To make
 * given instance valid again, use {@link #moveToRoot()} method.
 */
public class TagTreePointer {

    private static final String MCR_MARKER = "MCR";

    private TagStructureContext tagStructureContext;
    private PdfStructElem currentStructElem;
    private PdfPage currentPage;
    private PdfStream contentStream;

    private PdfNamespace currentNamespace;

    // '-1' value of this field means that next new kid will be the last element in the kids array
    private int nextNewKidIndex = -1;

    /**
     * Creates {@code TagTreePointer} instance. After creation {@code TagTreePointer} points at the root tag.
     * <p>
     * The {@link PdfNamespace} for the new tags, which don't explicitly define namespace by the means of
     * {@link DefaultAccessibilityProperties#setNamespace(PdfNamespace)}, is set to the value returned by
     * {@link TagStructureContext#getDocumentDefaultNamespace()} on {@link TagTreePointer} creation.
     * See also {@link TagTreePointer#setNamespaceForNewTags(PdfNamespace)}.
     * @param document the document, at which tag structure this instance will point.
     */
    public TagTreePointer(PdfDocument document) {
        tagStructureContext = document.getTagStructureContext();
        setCurrentStructElem(tagStructureContext.getRootTag());
        setNamespaceForNewTags(tagStructureContext.getDocumentDefaultNamespace());
    }

    /**
     * A copy constructor.
     *
     * @param tagPointer the {@code TagTreePointer} from which current position and page are copied.
     */
    public TagTreePointer(TagTreePointer tagPointer) {
        this.tagStructureContext = tagPointer.tagStructureContext;
        setCurrentStructElem(tagPointer.getCurrentStructElem());
        this.currentPage = tagPointer.currentPage;
        this.contentStream = tagPointer.contentStream;
        this.currentNamespace = tagPointer.currentNamespace;
    }

    TagTreePointer(PdfStructElem structElem, PdfDocument document) {
        tagStructureContext = document.getTagStructureContext();
        setCurrentStructElem(structElem);
    }

    /**
     * Sets a page which content will be tagged with this instance of {@code TagTreePointer}.
     * To tag page content:
     * <ol>
     * <li>Set pointer position to the tag which will be the parent of the page content item;
     * <li>Call {@link #getTagReference()} to obtain the reference to the current tag;
     * <li>Pass {@link TagReference} to the {@link PdfCanvas#openTag(TagReference)} method of the page's {@link PdfCanvas} to start marked content item;
     * <li>Draw content on {@code PdfCanvas};
     * <li>Use {@link PdfCanvas#closeTag()} to finish marked content item.
     * </ol>
     *
     * @param page the page which content will be tagged with this instance of {@code TagTreePointer}.
     * @return this {@link TagTreePointer} instance.
     */
    public TagTreePointer setPageForTagging(PdfPage page) {
        if (page.isFlushed()) {
            throw new PdfException(KernelExceptionMessageConstant.PAGE_ALREADY_FLUSHED);
        }
        this.currentPage = page;

        return this;
    }

    /**
     * @return a page which content will be tagged with this instance of {@code TagTreePointer}.
     */
    public PdfPage getCurrentPage() {
        return currentPage;
    }

    /**
     * Sometimes, tags are desired to be connected with the content that resides not in the page's content stream,
     * but rather in the some appearance stream or in the form xObject stream. In that case, to have a valid tag structure,
     * one shall set not only the page, on which the content will be rendered, but also the content stream in which
     * the tagged content will reside.
     * <br><br>
     * NOTE: It's important to set a {@code null} for this value, when tagging of this stream content is finished.
     *
     * @param contentStream the content stream which content will be tagged with this instance of {@code TagTreePointer}
     *                      or {@code null} if content stream tagging is finished
     * @return current {@link TagTreePointer} instance
     */
    public TagTreePointer setContentStreamForTagging(PdfStream contentStream) {
        this.contentStream = contentStream;
        return this;
    }

    /**
     * @return the content stream which content will be tagged with this instance of {@code TagTreePointer}.
     */
    public PdfStream getCurrentContentStream() {
        return contentStream;
    }

    /**
     * @return the {@link TagStructureContext} associated with the document to which this pointer belongs.
     */
    public TagStructureContext getContext() {
        return tagStructureContext;
    }

    /**
     * @return the document, at which tag structure this instance points.
     */
    public PdfDocument getDocument() {
        return tagStructureContext.getDocument();
    }

    /**
     * Sets a {@link PdfNamespace} which will be set to every new tag created by this {@link TagTreePointer} instance
     * if this tag doesn't explicitly define namespace by the means of {@link DefaultAccessibilityProperties#setNamespace(PdfNamespace)}.
     * <p>
     * This value has meaning only for the PDF documents of version <b>2.0 and higher</b>.
     * <p>
     * It's highly recommended to acquire {@link PdfNamespace} class instances via {@link TagStructureContext#fetchNamespace(String)}.
     *
     * @param namespace a {@link PdfNamespace} to be set for the new tags created. If set to null - new tags will have
     *                  a namespace set only if it is defined in the corresponding {@link AccessibilityProperties}.
     * @return this {@link TagTreePointer} instance.
     * @see TagStructureContext#fetchNamespace(String)
     */
    public TagTreePointer setNamespaceForNewTags(PdfNamespace namespace) {
        this.currentNamespace = namespace;
        return this;
    }

    /**
     * Gets a {@link PdfNamespace} which will be set to every new tag created by this {@link TagTreePointer} instance.
     * @return a {@link PdfNamespace} which is to be set for the new tags created, or null if one is not defined.
     * @see TagTreePointer#setNamespaceForNewTags(PdfNamespace)
     */
    public PdfNamespace getNamespaceForNewTags() {
        return this.currentNamespace;
    }

    /**
     * Adds a new tag with given role to the tag structure.
     * This method call moves this {@code TagTreePointer} to the added kid.
     *
     * @param role role of the new tag.
     * @return this {@link TagTreePointer} instance.
     */
    public TagTreePointer addTag(String role) {
        addTag(-1, role);
        return this;
    }

    /**
     * Adds a new tag with given role to the tag structure.
     * This method call moves this {@code TagTreePointer} to the added kid.
     * <br>
     * This call is equivalent of calling sequentially {@link #setNextNewKidIndex(int)} and {@link #addTag(String)}.
     *
     * @param index zero-based index in kids array of parent tag at which new tag will be added.
     * @param role  role of the new tag.
     * @return this {@link TagTreePointer} instance.
     */
    public TagTreePointer addTag(int index, String role) {
        tagStructureContext.throwExceptionIfRoleIsInvalid(role, currentNamespace);
        setNextNewKidIndex(index);
        setCurrentStructElem(addNewKid(role));
        return this;
    }

    /**
     * Adds a new tag to the tag structure.
     * This method call moves this {@link TagTreePointer} to the added kid.
     * <br>
     * New tag will have a role and attributes defined by the given {@link AccessibilityProperties}.
     *
     * @param properties accessibility properties which define a new tag role and other properties.
     * @return this {@link TagTreePointer} instance.
     */
    public TagTreePointer addTag(AccessibilityProperties properties) {
        addTag(-1, properties);
        return this;
    }

    /**
     * Adds a new tag to the tag structure.
     * This method call moves this {@code TagTreePointer} to the added kid.
     * <br>
     * New tag will have a role and attributes defined by the given {@link AccessibilityProperties}.
     * This call is equivalent of calling sequentially {@link #setNextNewKidIndex(int)} and {@link #addTag(AccessibilityProperties)}.
     *
     * @param index   zero-based index in kids array of parent tag at which new tag will be added.
     * @param properties accessibility properties which define a new tag role and other properties.
     * @return this {@link TagTreePointer} instance.
     */
    public TagTreePointer addTag(int index, AccessibilityProperties properties) {
        tagStructureContext.throwExceptionIfRoleIsInvalid(properties, currentNamespace);
        setNextNewKidIndex(index);
        setCurrentStructElem(addNewKid(properties));
        return this;
    }

    /**
     * Adds a new content item for the given {@code PdfAnnotation} under the current tag.
     * <br><br>
     * By default, when annotation is added to the page it is automatically tagged with auto tagging pointer
     * (see {@link TagStructureContext#getAutoTaggingPointer()}). If you want to add annotation tag manually, be sure to use
     * {@link PdfPage#addAnnotation(int, PdfAnnotation, boolean)} method with <i>false</i> for boolean flag.
     *
     * @param annotation {@code PdfAnnotation} to be tagged.
     * @return this {@link TagTreePointer} instance.
     */
    public TagTreePointer addAnnotationTag(PdfAnnotation annotation) {
        throwExceptionIfCurrentPageIsNotInited();

        // Sometimes the merged field is split into a form field and an annotation, so we should add the annotation
        // instead of the merged field in the tag structure. So the annotation already contains the merged field's
        // StructParent in its dictionary, which we need to take into account. Otherwise, getNextStructParentIndex()
        // will increment the structParentIndex counter and the annotation will be added to the end, but the merged
        // field's StructParent index will disappear from the number tree of the tag structure.
        PdfNumber structParentIndex = annotation.getPdfObject().getAsNumber(PdfName.StructParent);
        PdfObjRef kid = new PdfObjRef(annotation, getCurrentStructElem(),
                structParentIndex != null ? structParentIndex.intValue() :
                        getDocument().getNextStructParentIndex());
        if (!ensureElementPageEqualsKidPage(getCurrentStructElem(), currentPage.getPdfObject())) {
            // Explicitly using object indirect reference here in order to correctly process released objects.
            ((PdfDictionary) kid.getPdfObject()).put(PdfName.Pg, currentPage.getPdfObject().getIndirectReference());
        }
        addNewKid(kid);
        return this;
    }

    /**
     * Sets index of the next added to the current tag kid, which could be another tag or content item.
     * By default, new tag is added at the end of the parent kids array. This property affects only the next added tag,
     * all tags added after will be added with the default behaviour.
     * <br><br>
     * This method could be used with any overload of {@link #addTag(String)} method,
     * with {@link #relocateKid(int, TagTreePointer)} and {@link #addAnnotationTag(PdfAnnotation)}.
     * <br>
     * Keep in mind, that this method set property to the {@code TagTreePointer} and not to the tag itself, which means
     * that if you would move the pointer, this property would be applied to the new current tag.
     *
     * @param nextNewKidIndex index of the next added kid.
     * @return this {@link TagTreePointer} instance.
     */
    public TagTreePointer setNextNewKidIndex(int nextNewKidIndex) {
        if (nextNewKidIndex > -1) {
            this.nextNewKidIndex = nextNewKidIndex;
        }
        return this;
    }

    /**
     * Removes the current tag. If it has kids, they will become kids of the current tag parent.
     * This method call moves this {@code TagTreePointer} to the current tag parent.
     * <br><br>
     * You cannot remove root tag, and also you cannot remove the tag if it's parent is already flushed;
     * in this two cases an exception will be thrown.
     *
     * @return this {@link TagStructureContext} instance.
     */
    public TagTreePointer removeTag() {
        PdfStructElem currentStructElem = getCurrentStructElem();
        IStructureNode parentElem = currentStructElem.getParent();
        if (parentElem instanceof PdfStructTreeRoot) {
            throw new PdfException(KernelExceptionMessageConstant.CANNOT_REMOVE_DOCUMENT_ROOT_TAG);
        }

        List<IStructureNode> kids = currentStructElem.getKids();
        PdfStructElem parent = (PdfStructElem) parentElem;

        if (parent.isFlushed()) {
            throw new PdfException(KernelExceptionMessageConstant.CANNOT_REMOVE_TAG_BECAUSE_ITS_PARENT_IS_FLUSHED);
        }

        // remove waiting tag state if tag is removed
        Object objForStructDict = tagStructureContext.getWaitingTagsManager().getObjForStructDict(currentStructElem.getPdfObject());
        tagStructureContext.getWaitingTagsManager().removeWaitingState(objForStructDict);

        int removedKidIndex = parent.removeKid(currentStructElem);

        PdfIndirectReference indRef = currentStructElem.getPdfObject().getIndirectReference();
        if (indRef != null) {
            // TODO DEVSIX-5472 need to clean references to structure element from
            //  other structure elements /Ref entries and structure destinations
            indRef.setFree();
        }

        for (IStructureNode kid : kids) {
            if (kid instanceof PdfStructElem) {
                parent.addKid(removedKidIndex++, (PdfStructElem) kid);
            } else {
                PdfMcr mcr = prepareMcrForMovingToNewParent((PdfMcr) kid, parent);
                parent.addKid(removedKidIndex++, mcr);
            }
        }
        currentStructElem.getPdfObject().clear();
        setCurrentStructElem(parent);
        return this;
    }

    /**
     * Moves kid of the current tag to the tag at which given {@code TagTreePointer} points.
     * This method doesn't change neither this instance nor pointerToNewParent position.
     *
     * @param kidIndex           zero-based index of the current tag's kid to be relocated.
     * @param pointerToNewParent the {@code TagTreePointer} which is positioned at the tag which will become kid's new parent.
     * @return this {@link TagTreePointer} instance.
     */
    public TagTreePointer relocateKid(int kidIndex, TagTreePointer pointerToNewParent) {
        if (getDocument() != pointerToNewParent.getDocument()) {
            throw new PdfException(
                    KernelExceptionMessageConstant.TAG_CANNOT_BE_MOVED_TO_THE_ANOTHER_DOCUMENTS_TAG_STRUCTURE);
        }
        if (getCurrentStructElem().isFlushed()) {
            throw new PdfException(KernelExceptionMessageConstant.CANNOT_RELOCATE_TAG_WHICH_PARENT_IS_ALREADY_FLUSHED);
        }

        if (isPointingToSameTag(pointerToNewParent)){
            if (kidIndex == pointerToNewParent.nextNewKidIndex) {
                return this;
            } else if (kidIndex < pointerToNewParent.nextNewKidIndex) {
                pointerToNewParent.setNextNewKidIndex(pointerToNewParent.nextNewKidIndex - 1);
            }
        }
        if (getCurrentStructElem().isKidFlushed(kidIndex)) {
            throw new PdfException(KernelExceptionMessageConstant.CANNOT_RELOCATE_TAG_WHICH_IS_ALREADY_FLUSHED);
        }
        IStructureNode removedKid = getCurrentStructElem().removeKid(kidIndex, true);
        if (removedKid instanceof PdfStructElem) {
            pointerToNewParent.addNewKid((PdfStructElem) removedKid);
        } else if (removedKid instanceof PdfMcr) {
            PdfMcr mcrKid = prepareMcrForMovingToNewParent((PdfMcr) removedKid, pointerToNewParent.getCurrentStructElem());
            pointerToNewParent.addNewKid(mcrKid);
        }

        return this;
    }

    /**
     * Moves current tag to the tag at which given {@code TagTreePointer} points.
     * This method doesn't change either this instance or pointerToNewParent position.
     *
     * @param pointerToNewParent the {@code TagTreePointer} which is positioned at the tag
     *                           which will become current tag new parent.
     * @return this {@link TagTreePointer} instance.
     */
    public TagTreePointer relocate(TagTreePointer pointerToNewParent) {
        if (getCurrentStructElem().getPdfObject() == tagStructureContext.getRootTag().getPdfObject()) {
            throw new PdfException(KernelExceptionMessageConstant.CANNOT_RELOCATE_ROOT_TAG);
        }
        if (getCurrentStructElem().isFlushed()) {
            throw new PdfException(KernelExceptionMessageConstant.CANNOT_RELOCATE_TAG_WHICH_IS_ALREADY_FLUSHED);
        }
        int i = getIndexInParentKidsList();
        if (i < 0) {
            throw new PdfException(KernelExceptionMessageConstant.CANNOT_RELOCATE_TAG_WHICH_PARENT_IS_ALREADY_FLUSHED);
        }
        new TagTreePointer(this).moveToParent().relocateKid(i, pointerToNewParent);
        return this;
    }

    /**
     * Creates a reference to the current tag, which could be used to associate a content on the PdfCanvas with current tag.
     * See {@link PdfCanvas#openTag(TagReference)} and {@link #setPageForTagging(PdfPage)}.
     *
     * @return the reference to the current tag.
     */
    public TagReference getTagReference() {
        return getTagReference(-1);
    }

    /**
     * Creates a reference to the current tag, which could be used to associate a content on the PdfCanvas with current tag.
     * See {@link PdfCanvas#openTag(TagReference)} and {@link #setPageForTagging(PdfPage)}.
     *
     * @param index zero-based index in kids array of tag. These indexes define the logical order of the content on the page.
     * @return the reference to the current tag.
     */
    public TagReference getTagReference(int index) {
        return new TagReference(getCurrentElemEnsureIndirect(), this, index);
    }

    /**
     * Moves this {@code TagTreePointer} instance to the document root tag.
     *
     * @return this {@link TagTreePointer} instance.
     */
    public TagTreePointer moveToRoot() {
        setCurrentStructElem(tagStructureContext.getRootTag());
        return this;
    }

    /**
     * Moves this {@link TagTreePointer} instance to the parent of the current tag.
     *
     * @return this {@link TagTreePointer} instance.
     */
    public TagTreePointer moveToParent() {
        if (getCurrentStructElem().getPdfObject() == tagStructureContext.getRootTag().getPdfObject()) {
            throw new PdfException(KernelExceptionMessageConstant.CANNOT_MOVE_TO_PARENT_CURRENT_ELEMENT_IS_ROOT);
        }

        PdfStructElem parent = (PdfStructElem) getCurrentStructElem().getParent();
        if (parent.isFlushed()) {
            Logger logger = LoggerFactory.getLogger(TagTreePointer.class);
            logger.warn(IoLogMessageConstant.ATTEMPT_TO_MOVE_TO_FLUSHED_PARENT);

            moveToRoot();
        } else {
            setCurrentStructElem((PdfStructElem) parent);
        }
        return this;
    }

    /**
     * Moves this {@code TagTreePointer} instance to the kid of the current tag.
     *
     * @param kidIndex zero-based index of the current tag kid to which pointer will be moved.
     * @return this {@link TagTreePointer} instance.
     */
    public TagTreePointer moveToKid(int kidIndex) {
        IStructureNode kid = getCurrentStructElem().getKids().get(kidIndex);
        if (kid instanceof PdfStructElem) {
            setCurrentStructElem((PdfStructElem) kid);
        } else if (kid instanceof PdfMcr) {
            throw new PdfException(KernelExceptionMessageConstant.CANNOT_MOVE_TO_MARKED_CONTENT_REFERENCE);
        } else {
            throw new PdfException(KernelExceptionMessageConstant.CANNOT_MOVE_TO_FLUSHED_KID);
        }
        return this;
    }

    /**
     * Moves this {@link TagTreePointer} instance to the first descendant of the current tag which has the given role.
     * If there are no direct kids of the tag with such role, further descendants are checked in BFS order.
     *
     * @param role role of the current tag descendant to which pointer will be moved.
     *             If there are several descendants with this role, pointer will be moved
     *             to the first kid with such role in BFS order.
     * @return this {@link TagTreePointer} instance.
     */
    public TagTreePointer moveToKid(String role) {
        moveToKid(0, role);
        return this;
    }

    /**
     * Moves this {@link TagTreePointer} instance to the nth descendant of the current tag which has the given role.
     * If there are no direct kids of the tag with such role, further descendants are checked in BFS order.
     *
     * @param n    if there are several descendants with the given role, pointer will be moved to the descendant
     *             which has zero-based index <em>n</em> if you count only the descendants with the given role in BFS order.
     * @param role role of the current tag descendant to which pointer will be moved.
     * @return this {@link TagTreePointer} instance.
     */
    public TagTreePointer moveToKid(int n, String role) {

        // MCR literal could be returned in a list of kid names (see #getKidsRoles())
        if (MCR_MARKER.equals(role)) {
            throw new PdfException(KernelExceptionMessageConstant.CANNOT_MOVE_TO_MARKED_CONTENT_REFERENCE);
        }

        RoleFinderHandler handler = new RoleFinderHandler(n, role);
        TagTreeIterator iterator = new TagTreeIterator(getCurrentStructElem(),
                TagTreeIterator.TreeTraversalOrder.PRE_ORDER);

        iterator.addHandler(handler);
        iterator.traverse();

        PdfStructElem elem = handler.getFoundElement();
        if (elem == null) {
            throw new PdfException(KernelExceptionMessageConstant.NO_KID_WITH_SUCH_ROLE);
        }

        setCurrentStructElem(elem);
        return this;
    }

    /**
     * Gets current tag kids roles.
     * If certain kid is already flushed, at its position there will be a {@code null}.
     * If kid is a content item, at it's position there will be "MCR" string literal (stands for Marked Content Reference).
     *
     * @return current tag kids roles
     */
    public List<String> getKidsRoles() {
        List<String> roles = new ArrayList<>();
        List<IStructureNode> kids = getCurrentStructElem().getKids();
        for (IStructureNode kid : kids) {
            if (kid == null) {
                roles.add(null);
            } else if (kid instanceof PdfStructElem) {
                roles.add(kid.getRole().getValue());
            } else {
                roles.add(MCR_MARKER);
            }
        }
        return roles;
    }

    /**
     * Flushes current tag and all it's descendants.
     * This method call moves this {@code TagTreePointer} to the current tag parent.
     * <p>
     * If some of the descendant tags of the current tag have waiting state (see {@link WaitingTagsManager}),
     * then these tags are considered as not yet finished ones, and they won't be flushed immediately,
     * but they will be flushed, when waiting state is removed.
     *
     * @return this {@link TagTreePointer} instance.
     */
    public TagTreePointer flushTag() {
        if (getCurrentStructElem().getPdfObject() == tagStructureContext.getRootTag().getPdfObject()) {
            throw new PdfException(
                    KernelExceptionMessageConstant.CANNOT_FLUSH_DOCUMENT_ROOT_TAG_BEFORE_DOCUMENT_IS_CLOSED);
        }
        IStructureNode parent = tagStructureContext.getWaitingTagsManager().flushTag(getCurrentStructElem());
        if (parent != null) {
            // parent is not flushed

            setCurrentStructElem((PdfStructElem) parent);
        } else {
            setCurrentStructElem(tagStructureContext.getRootTag());
        }
        return this;
    }

    /**
     * For current tag and all of it's parents consequentially checks if the following constraints apply,
     * and flushes the tag if they do or stops if they don't:
     * <ul>
     *     <li>tag is not already flushed;
     *     <li>tag is not in waiting state (see {@link WaitingTagsManager});
     *     <li>tag is not the root tag;
     *     <li>tag has no kids or all of the kids are either flushed themselves or
     *         (if they are a marked content reference) belong to the flushed page.
     * </ul>
     * It makes sense to use this method in conjunction with {@link TagStructureContext#flushPageTags(PdfPage)}
     * for the tags which have just lost their waiting state and might be not flushed only because they had one.
     * This helps to eliminate hanging (not flushed) tags when they don't have waiting state anymore.
     * @return this {@link TagTreePointer} instance.
     */
    public TagTreePointer flushParentsIfAllKidsFlushed() {
        getContext().flushParentIfBelongsToPage(getCurrentStructElem(), null);
        return this;
    }

    /**
     * Gets accessibility properties of the current tag.
     *
     * @return accessibility properties of the current tag.
     */
    public AccessibilityProperties getProperties() {
        return new BackedAccessibilityProperties(this);
    }

    /**
     * Gets current tag role.
     *
     * @return current tag role.
     */
    public String getRole() {
        return getCurrentStructElem().getRole().getValue();
    }

    /**
     * Sets new role to the current tag.
     *
     * @param role new role to be set.
     * @return this {@link TagTreePointer} instance.
     */
    public TagTreePointer setRole(String role) {
        getCurrentStructElem().setRole(PdfStructTreeRoot.convertRoleToPdfName(role));
        return this;
    }

    /**
     * Defines index of the current tag in the parent's kids list.
     * @return returns index of the current tag in the parent's kids list, or -1
     * if either current tag is a root tag, parent is flushed or it wasn't possible to define index.
     */
    public int getIndexInParentKidsList() {
        if (getCurrentStructElem().getPdfObject() == tagStructureContext.getRootTag().getPdfObject()) {
            return -1;
        }

        PdfStructElem parent = (PdfStructElem) getCurrentStructElem().getParent();
        if (parent.isFlushed()) {
            return -1;
        }
        PdfObject k = parent.getK();
        if (k == getCurrentStructElem().getPdfObject()) {
            return 0;
        }
        if (k.isArray()) {
            PdfArray kidsArr = (PdfArray) k;
            return kidsArr.indexOf(getCurrentStructElem().getPdfObject());
        }
        return -1;
    }

    /**
     * Moves this {@link TagTreePointer} instance to the tag at which given {@link TagTreePointer} instance is pointing.
     *
     * @param tagTreePointer a {@link TagTreePointer} that points at the tag which will become the current tag
     *                       of this instance.
     * @return this {@link TagTreePointer} instance.
     */
    public TagTreePointer moveToPointer(TagTreePointer tagTreePointer) {
        this.currentStructElem = tagTreePointer.currentStructElem;
        return this;
    }

    /**
     * Checks if this {@link TagTreePointer} is pointing at the same tag as the giving {@link TagTreePointer}.
     * @param otherPointer a {@link TagTreePointer} which is checked against this instance on whether they point
     *                     at the same tag.
     * @return true if both {@link TagTreePointer} instances point at the same tag, false otherwise.
     */
    public boolean isPointingToSameTag(TagTreePointer otherPointer) {
        return getCurrentStructElem().getPdfObject().equals(otherPointer.getCurrentStructElem().getPdfObject());
    }

    int createNextMcidForStructElem(PdfStructElem elem, int index) {
        throwExceptionIfCurrentPageIsNotInited();

        PdfMcr mcr;
        if (!markedContentNotInPageStream() && ensureElementPageEqualsKidPage(elem, currentPage.getPdfObject())) {
            mcr = new PdfMcrNumber(currentPage, elem);
        } else {
            mcr = new PdfMcrDictionary(currentPage, elem);
            if (markedContentNotInPageStream()) {
                ((PdfDictionary) mcr.getPdfObject()).put(PdfName.Stm, contentStream);
            }
        }
        elem.addKid(index, mcr);
        return mcr.getMcid();
    }

    TagTreePointer setCurrentStructElem(PdfStructElem structElem) {
        if (structElem.getParent() == null) {
            throw new PdfException(KernelExceptionMessageConstant.STRUCTURE_ELEMENT_SHALL_CONTAIN_PARENT_OBJECT);
        }

        currentStructElem = structElem;
        return this;
    }

    PdfStructElem getCurrentStructElem() {
        if (currentStructElem.isFlushed()) {
            throw new PdfException(
                    KernelExceptionMessageConstant.TAG_TREE_POINTER_IS_IN_INVALID_STATE_IT_POINTS_AT_FLUSHED_ELEMENT_USE_MOVE_TO_ROOT);
        }

        PdfIndirectReference indRef = currentStructElem.getPdfObject().getIndirectReference();
        if (indRef != null && indRef.isFree()) {
            // is removed

            throw new PdfException(
                    KernelExceptionMessageConstant.TAG_TREE_POINTER_IS_IN_INVALID_STATE_IT_POINTS_AT_REMOVED_ELEMENT_USE_MOVE_TO_ROOT);
        }

        return currentStructElem;
    }

    /**
     * Applies properties to the current tag.
     * <p>
     * @param properties the properties to be applied to the current tag.
     */
    public void applyProperties(AccessibilityProperties properties) {
        AccessibilityPropertiesToStructElem.apply(properties, getCurrentStructElem());
    }

    private int getNextNewKidPosition() {
        int nextPos = nextNewKidIndex;
        nextNewKidIndex = -1;
        return nextPos;
    }

    private PdfStructElem addNewKid(String role) {
        PdfStructElem kid = new PdfStructElem(getDocument(), PdfStructTreeRoot.convertRoleToPdfName(role));
        processKidNamespace(kid);
        return addNewKid(kid);
    }

    private PdfStructElem addNewKid(AccessibilityProperties properties) {
        PdfStructElem kid = new PdfStructElem(getDocument(), PdfStructTreeRoot.convertRoleToPdfName(properties.getRole()));
        AccessibilityPropertiesToStructElem.apply(properties, kid);
        processKidNamespace(kid);
        return addNewKid(kid);
    }

    private void processKidNamespace(PdfStructElem kid) {
        PdfNamespace kidNamespace = kid.getNamespace();
        if (currentNamespace != null && kidNamespace == null) {
            kid.setNamespace(currentNamespace);
            kidNamespace = currentNamespace;
        }
        tagStructureContext.ensureNamespaceRegistered(kidNamespace);
    }

    private PdfStructElem addNewKid(PdfStructElem kid) {
        return getCurrentElemEnsureIndirect().addKid(getNextNewKidPosition(), kid);
    }

    private PdfMcr addNewKid(PdfMcr kid) {
        return getCurrentElemEnsureIndirect().addKid(getNextNewKidPosition(), kid);
    }

    private PdfStructElem getCurrentElemEnsureIndirect() {
        PdfStructElem currentStructElem = getCurrentStructElem();
        if (currentStructElem.getPdfObject().getIndirectReference() == null) {
            currentStructElem.makeIndirect(getDocument());
        }
        return currentStructElem;
    }

    private PdfMcr prepareMcrForMovingToNewParent(PdfMcr mcrKid, PdfStructElem newParent) {
        PdfObject mcrObject = mcrKid.getPdfObject();
        PdfDictionary mcrPage = mcrKid.getPageObject();

        PdfDictionary mcrDict = null;
        if (!mcrObject.isNumber()) {
            mcrDict = (PdfDictionary) mcrObject;
        }
        if (mcrDict == null || !mcrDict.containsKey(PdfName.Pg)) {
            if (!ensureElementPageEqualsKidPage(newParent, mcrPage)) {
                if (mcrDict == null) {
                    mcrDict = new PdfDictionary();
                    mcrDict.put(PdfName.Type, PdfName.MCR);
                    mcrDict.put(PdfName.MCID, mcrKid.getPdfObject());
                }
                // Explicitly using object indirect reference here in order to correctly process released objects.
                mcrDict.put(PdfName.Pg, mcrPage.getIndirectReference());
            }
        }

        if (mcrDict != null) {
            if (PdfName.MCR.equals(mcrDict.get(PdfName.Type))) {
                mcrKid = new PdfMcrDictionary(mcrDict, newParent);
            } else if (PdfName.OBJR.equals(mcrDict.get(PdfName.Type))) {
                mcrKid = new PdfObjRef(mcrDict, newParent);
            }
        } else {
            mcrKid = new PdfMcrNumber((PdfNumber) mcrObject, newParent);
        }

        return mcrKid;
    }

    private boolean ensureElementPageEqualsKidPage(PdfStructElem elem, PdfDictionary kidPage) {
        PdfObject pageObject = elem.getPdfObject().get(PdfName.Pg);
        if (pageObject == null) {
            pageObject = kidPage;
            // Explicitly using object indirect reference here in order to correctly process released objects.
            elem.put(PdfName.Pg, kidPage.getIndirectReference());
        }

        return kidPage.equals(pageObject);
    }

    private boolean markedContentNotInPageStream() {
        return contentStream != null;
    }

    private void throwExceptionIfCurrentPageIsNotInited() {
        if (currentPage == null) {
            throw new PdfException(KernelExceptionMessageConstant.PAGE_IS_NOT_SET_FOR_THE_PDF_TAG_STRUCTURE);
        }
    }

    private static class RoleFinderHandler extends AbstractAvoidDuplicatesTagTreeIteratorHandler {
        private final int n;
        private final String role;
        private int foundIdx = 0;
        private PdfStructElem foundElem;

        RoleFinderHandler(int n, String role) {
            this.n = n;
            this.role = role;
        }

        public PdfStructElem getFoundElement() {
            return foundElem;
        }

        @Override
        public boolean accept(IStructureNode node) {
            return getFoundElement() == null && super.accept(node);
        }

        @Override
        public void processElement(IStructureNode elem) {
            if (foundElem != null) {
                return;
            }

            String descendantRole = elem.getRole().getValue();
            if (descendantRole.equals(role) && foundIdx++ == n) {
                foundElem = (PdfStructElem) elem;
            }
        }
    }
}