/*
 * 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.documentinterchange.logicalstructure;

import java.util.Collections;
import java.util.Iterator;
import java.util.Map;

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.cos.COSObject;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.documentinterchange.markedcontent.PDMarkedContent;
import org.apache.pdfbox.pdmodel.documentinterchange.taggedpdf.StandardStructureTypes;

/**
 * A structure element.
 *
 * @author Ben Litchfield
 * @author Johannes Koch
 */
public class PDStructureElement extends PDStructureNode
{
    
    public static final String TYPE = "StructElem";

    /**
     * Constructor with required values.
     *
     * @param structureType The structure type. Constants are available in the
     * {@link StandardStructureTypes} class.
     * @param parent The parent structure node.
     */
    public PDStructureElement(String structureType, PDStructureNode parent)
    {
        super(TYPE);
        this.setStructureType(structureType);
        this.setParent(parent);
    }

    /**
     * Constructor for an existing structure element.
     *
     * @param dic The existing dictionary.
     */
    public PDStructureElement( COSDictionary dic )
    {
        super(dic);
    }


    /**
     * Returns the structure type (S).
     * 
     * @return the structure type
     */
    public String getStructureType()
    {
        return this.getCOSObject().getNameAsString(COSName.S);
    }

    /**
     * Sets the structure type (S).
     * 
     * @param structureType the structure type
     */
    public final void setStructureType(String structureType)
    {
        this.getCOSObject().setName(COSName.S, structureType);
    }

    /**
     * Returns the parent in the structure hierarchy (P).
     * 
     * @return the parent in the structure hierarchy
     */
    public PDStructureNode getParent()
    {
        COSDictionary parent = getCOSObject().getCOSDictionary(COSName.P);
        return parent != null ? PDStructureNode.create(parent) : null;
    }

    /**
     * Sets the parent in the structure hierarchy (P).
     * 
     * @param structureNode the parent in the structure hierarchy
     */
    public final void setParent(PDStructureNode structureNode)
    {
        this.getCOSObject().setItem(COSName.P, structureNode);
    }

    /**
     * Returns the element identifier (ID).
     * 
     * @return the element identifier
     */
    public String getElementIdentifier()
    {
        return this.getCOSObject().getString(COSName.ID);
    }

    /**
     * Sets the element identifier (ID).
     * 
     * @param id the element identifier
     */
    public void setElementIdentifier(String id)
    {
        this.getCOSObject().setString(COSName.ID, id);
    }

    /**
     * Returns the page on which some or all of the content items designated by
     *  the K entry shall be rendered (Pg).
     * 
     * @return the page on which some or all of the content items designated by
     *  the K entry shall be rendered
     */
    public PDPage getPage()
    {
        COSDictionary page = getCOSObject().getCOSDictionary(COSName.PG);
        return page != null ? new PDPage(page) : null;
    }

    /**
     * Sets the page on which some or all of the content items designated by
     *  the K entry shall be rendered (Pg).
     * @param page the page on which some or all of the content items designated
     *  by the K entry shall be rendered.
     */
    public void setPage(PDPage page)
    {
        this.getCOSObject().setItem(COSName.PG, page);
    }

    /**
     * Returns the attributes together with their revision numbers (A).
     * 
     * @return the attributes as a list, never null.
     */
    public Revisions<PDAttributeObject> getAttributes()
    {
        Revisions<PDAttributeObject> attributes = new Revisions<>();
        COSBase a = this.getCOSObject().getDictionaryObject(COSName.A);
        if (a instanceof COSArray)
        {
            COSArray aa = (COSArray) a;
            Iterator<COSBase> it = aa.iterator();
            PDAttributeObject ao = null;
            while (it.hasNext())
            {
                COSBase item = it.next();
                if (item instanceof COSObject)
                {
                    item = ((COSObject) item).getObject();
                }
                if (item instanceof COSDictionary)
                {
                    ao = PDAttributeObject.create((COSDictionary) item);
                    ao.setStructureElement(this);
                    attributes.addObject(ao, 0);
                }
                else if (item instanceof COSInteger)
                {
                    attributes.setRevisionNumber(ao, ((COSNumber) item).intValue());
                }
            }
        }
        if (a instanceof COSDictionary)
        {
            PDAttributeObject ao = PDAttributeObject.create((COSDictionary) a);
            ao.setStructureElement(this);
            attributes.addObject(ao, 0);
        }
        return attributes;
    }

    /**
     * Sets the attributes together with their revision numbers (A).
     * 
     * @param attributes the attributes
     */
    public void setAttributes(Revisions<PDAttributeObject> attributes)
    {
        COSName key = COSName.A;
        if ((attributes.size() == 1) && (attributes.getRevisionNumber(0) == 0))
        {
            PDAttributeObject attributeObject = attributes.getObject(0);
            attributeObject.setStructureElement(this);
            this.getCOSObject().setItem(key, attributeObject);
            return;
        }
        COSArray array = new COSArray();
        for (int i = 0; i < attributes.size(); i++)
        {
            PDAttributeObject attributeObject = attributes.getObject(i);
            attributeObject.setStructureElement(this);
            int revisionNumber = attributes.getRevisionNumber(i);
            if (revisionNumber < 0)
            {
                throw new IllegalArgumentException("The revision number shall be > -1");
            }
            array.add(attributeObject);
            array.add(COSInteger.get(revisionNumber));
        }
        this.getCOSObject().setItem(key, array);
    }

    /**
     * Adds an attribute object.
     * 
     * @param attributeObject the attribute object
     */
    public void addAttribute(PDAttributeObject attributeObject)
    {
        COSName key = COSName.A;
        attributeObject.setStructureElement(this);
        COSBase a = this.getCOSObject().getDictionaryObject(key);
        COSArray array;
        if (a instanceof COSArray)
        {
            array = (COSArray) a;
        }
        else
        {
            array = new COSArray();
            if (a != null)
            {
                array.add(a);
                array.add(COSInteger.get(0));
            }
        }
        this.getCOSObject().setItem(key, array);
        array.add(attributeObject);
        array.add(COSInteger.get(this.getRevisionNumber()));
    }

    /**
     * Removes an attribute object.
     * 
     * @param attributeObject the attribute object
     */
    public void removeAttribute(PDAttributeObject attributeObject)
    {
        COSName key = COSName.A;
        COSBase a = this.getCOSObject().getDictionaryObject(key);
        if (a instanceof COSArray)
        {
            COSArray array = (COSArray) a;
            array.remove(attributeObject.getCOSObject());
            if ((array.size() == 2) && (array.getInt(1) == 0))
            {
                this.getCOSObject().setItem(key, array.getObject(0));
            }
        }
        else
        {
            COSBase directA = a;
            if (a instanceof COSObject)
            {
                directA = ((COSObject) a).getObject();
            }
            if (attributeObject.getCOSObject().equals(directA))
            {
                this.getCOSObject().setItem(key, null);
            }
        }
        attributeObject.setStructureElement(null);
    }

    /**
     * Updates the revision number for the given attribute object.
     * 
     * @param attributeObject the attribute object
     */
    public void attributeChanged(PDAttributeObject attributeObject)
    {
        COSName key = COSName.A;
        COSBase a = this.getCOSObject().getDictionaryObject(key);
        if (a instanceof COSArray)
        {
            COSArray array = (COSArray) a;
            for (int i = 0; i < array.size(); i++)
            {
                COSBase entry = array.getObject(i);
                if (entry.equals(attributeObject.getCOSObject()))
                {
                    COSBase next = array.get(i + 1);
                    if (next instanceof COSInteger)
                    {
                        array.set(i + 1, COSInteger.get(this.getRevisionNumber()));
                    }
                }
            }
        }
        else
        {
            COSArray array = new COSArray();
            array.add(a);
            array.add(COSInteger.get(this.getRevisionNumber()));
            this.getCOSObject().setItem(key, array);
        }
    }

    /**
     * Returns the class names together with their revision numbers (C).
     * 
     * @return the class names as a list, never null.
     */
    public Revisions<String> getClassNames()
    {
        COSName key = COSName.C;
        Revisions<String> classNames = new Revisions<>();
        COSBase c = this.getCOSObject().getDictionaryObject(key);
        if (c instanceof COSName)
        {
            classNames.addObject(((COSName) c).getName(), 0);
        }
        if (c instanceof COSArray)
        {
            COSArray array = (COSArray) c;
            Iterator<COSBase> it = array.iterator();
            String className = null;
            while (it.hasNext())
            {
                COSBase item = it.next();
                if (item instanceof COSObject)
                {
                    item = ((COSObject) item).getObject();
                }
                if (item instanceof COSName)
                {
                    className = ((COSName) item).getName();
                    classNames.addObject(className, 0);
                }
                else if (item instanceof COSInteger)
                {
                    classNames.setRevisionNumber(className, ((COSNumber) item).intValue());
                }
            }
        }
        return classNames;
    }

    /**
     * Sets the class names together with their revision numbers (C).
     * 
     * @param classNames the class names
     */
    public void setClassNames(Revisions<String> classNames)
    {
        if (classNames == null)
        {
            return;
        }
        COSName key = COSName.C;
        if ((classNames.size() == 1) && (classNames.getRevisionNumber(0) == 0))
        {
            String className = classNames.getObject(0);
            this.getCOSObject().setName(key, className);
            return;
        }
        COSArray array = new COSArray();
        for (int i = 0; i < classNames.size(); i++)
        {
            String className = classNames.getObject(i);
            int revisionNumber = classNames.getRevisionNumber(i);
            if (revisionNumber < 0)
            {
                throw new IllegalArgumentException("The revision number shall be > -1");
            }
            array.add(COSName.getPDFName(className));
            array.add(COSInteger.get(revisionNumber));
        }
        this.getCOSObject().setItem(key, array);
    }

    /**
     * Adds a class name.
     * 
     * @param className the class name
     */
    public void addClassName(String className)
    {
        if (className == null)
        {
            return;
        }
        COSName key = COSName.C;
        COSBase c = this.getCOSObject().getDictionaryObject(key);
        COSArray array;
        if (c instanceof COSArray)
        {
            array = (COSArray) c;
        }
        else
        {
            array = new COSArray();
            if (c != null)
            {
                array.add(c);
                array.add(COSInteger.get(0));
            }
        }
        this.getCOSObject().setItem(key, array);
        array.add(COSName.getPDFName(className));
        array.add(COSInteger.get(this.getRevisionNumber()));
    }

    /**
     * Removes a class name.
     * 
     * @param className the class name
     */
    public void removeClassName(String className)
    {
        if (className == null)
        {
            return;
        }
        COSName key = COSName.C;
        COSBase c = this.getCOSObject().getDictionaryObject(key);
        COSName name = COSName.getPDFName(className);
        if (c instanceof COSArray)
        {
            COSArray array = (COSArray) c;
            array.remove(name);
            if ((array.size() == 2) && (array.getInt(1) == 0))
            {
                this.getCOSObject().setItem(key, array.getObject(0));
            }
        }
        else
        {
            COSBase directC = c;
            if (c instanceof COSObject)
            {
                directC = ((COSObject) c).getObject();
            }
            if (name.equals(directC))
            {
                this.getCOSObject().setItem(key, null);
            }
        }
    }

    /**
     * Returns the revision number (R).
     * 
     * @return the revision number
     */
    public int getRevisionNumber()
    {
        return this.getCOSObject().getInt(COSName.R, 0);
    }

    /**
     * Sets the revision number (R).
     * 
     * @param revisionNumber the revision number
     */
    public void setRevisionNumber(int revisionNumber)
    {
        if (revisionNumber < 0)
        {
            throw new IllegalArgumentException("The revision number shall be > -1");
        }
        this.getCOSObject().setInt(COSName.R, revisionNumber);
    }

    /**
     * Increments th revision number.
     */
    public void incrementRevisionNumber()
    {
        this.setRevisionNumber(this.getRevisionNumber() + 1);
    }

    /**
     * Returns the title (T).
     * 
     * @return the title
     */
    public String getTitle()
    {
        return this.getCOSObject().getString(COSName.T);
    }

    /**
     * Sets the title (T).
     * 
     * @param title the title
     */
    public void setTitle(String title)
    {
        this.getCOSObject().setString(COSName.T, title);
    }

    /**
     * Returns the language (Lang).
     * 
     * @return the language
     */
    public String getLanguage()
    {
        return this.getCOSObject().getString(COSName.LANG);
    }

    /**
     * Sets the language (Lang).
     * 
     * @param language the language
     */
    public void setLanguage(String language)
    {
        this.getCOSObject().setString(COSName.LANG, language);
    }

    /**
     * Returns the alternate description (Alt).
     * 
     * @return the alternate description
     */
    public String getAlternateDescription()
    {
        return this.getCOSObject().getString(COSName.ALT);
    }

    /**
     * Sets the alternate description (Alt).
     * 
     * @param alternateDescription the alternate description
     */
    public void setAlternateDescription(String alternateDescription)
    {
        this.getCOSObject().setString(COSName.ALT, alternateDescription);
    }

    /**
     * Returns the expanded form (E).
     * 
     * @return the expanded form
     */
    public String getExpandedForm()
    {
        return this.getCOSObject().getString(COSName.E);
    }

    /**
     * Sets the expanded form (E).
     * 
     * @param expandedForm the expanded form
     */
    public void setExpandedForm(String expandedForm)
    {
        this.getCOSObject().setString(COSName.E, expandedForm);
    }

    /**
     * Returns the actual text (ActualText).
     * 
     * @return the actual text
     */
    public String getActualText()
    {
        return this.getCOSObject().getString(COSName.ACTUAL_TEXT);
    }

    /**
     * Sets the actual text (ActualText).
     * 
     * @param actualText the actual text
     */
    public void setActualText(String actualText)
    {
        this.getCOSObject().setString(COSName.ACTUAL_TEXT, actualText);
    }

    /**
     * Returns the standard structure type, the actual structure type is mapped
     * to in the role map.
     * 
     * @return the standard structure type
     */
    public String getStandardStructureType()
    {
        String type = this.getStructureType();
        Map<String,Object> roleMap = getRoleMap();
        if (roleMap.containsKey(type))
        {
            Object mappedValue = getRoleMap().get(type);
            if (mappedValue instanceof String)
            {
                type = (String)mappedValue;
            }
        }
        return type;
    }

    /**
     * Appends a marked-content sequence kid.
     * 
     * @param mcid the marked-content id (MCID).
     * @throws IllegalArgumentException if the mcid is negative.
     */
    public void appendKid(int mcid)
    {
        if (mcid < 0)
        {
            throw new IllegalArgumentException("MCID should not be negative");
        }
        this.appendKid(COSInteger.get(mcid));
    }

    /**
     * Appends a marked-content sequence kid.
     * 
     * @param markedContent the marked-content sequence with the MCID.
     * @throws IllegalArgumentException if the mcid is negative or doesn't exist.
     */
    public void appendKid(PDMarkedContent markedContent)
    {
        if (markedContent == null)
        {
            return;
        }
        int mcid = markedContent.getMCID();
        if (mcid < 0)
        {
            throw new IllegalArgumentException("MCID is negative or doesn't exist");
        }
        this.appendKid(COSInteger.get(mcid));
    }

    /**
     * Appends a marked-content reference kid.
     * 
     * @param markedContentReference the marked-content reference
     */
    public void appendKid(PDMarkedContentReference markedContentReference)
    {
        this.appendObjectableKid(markedContentReference);
    }

    /**
     * Appends an object reference kid.
     * 
     * @param objectReference the object reference
     */
    public void appendKid(PDObjectReference objectReference)
    {
        this.appendObjectableKid(objectReference);
    }

    /**
     * Inserts a marked-content identifier kid before a reference kid.
     * 
     * @param markedContentIdentifier the marked-content identifier
     * @param refKid the reference kid
     */
    public void insertBefore(COSInteger markedContentIdentifier, Object refKid)
    {
        this.insertBefore((COSBase) markedContentIdentifier, refKid);
    }

    /**
     * Inserts a marked-content reference kid before a reference kid.
     * 
     * @param markedContentReference the marked-content reference
     * @param refKid the reference kid
     */
    public void insertBefore(PDMarkedContentReference markedContentReference,
        Object refKid)
    {
        this.insertObjectableBefore(markedContentReference, refKid);
    }

    /**
     * Inserts an object reference kid before a reference kid.
     * 
     * @param objectReference the object reference
     * @param refKid the reference kid
     */
    public void insertBefore(PDObjectReference objectReference, Object refKid)
    {
        this.insertObjectableBefore(objectReference, refKid);
    }

    /**
     * Removes a marked-content identifier kid.
     * 
     * @param markedContentIdentifier the marked-content identifier
     */
    public void removeKid(COSInteger markedContentIdentifier)
    {
        this.removeKid((COSBase) markedContentIdentifier);
    }

    /**
     * Removes a marked-content reference kid.
     * 
     * @param markedContentReference the marked-content reference
     */
    public void removeKid(PDMarkedContentReference markedContentReference)
    {
        this.removeObjectableKid(markedContentReference);
    }

    /**
     * Removes an object reference kid.
     * 
     * @param objectReference the object reference
     */
    public void removeKid(PDObjectReference objectReference)
    {
        this.removeObjectableKid(objectReference);
    }


    /**
     * Returns the structure tree root.
     * 
     * @return the structure tree root
     */
    private PDStructureTreeRoot getStructureTreeRoot()
    {
        PDStructureNode parent = this.getParent();
        while (parent instanceof PDStructureElement)
        {
            parent = ((PDStructureElement) parent).getParent();
        }
        if (parent instanceof PDStructureTreeRoot)
        {
            return (PDStructureTreeRoot) parent;
        }
        return null;
    }

    /**
     * Returns the role map.
     * 
     * @return the role map
     */
    private Map<String, Object> getRoleMap()
    {
        PDStructureTreeRoot root = this.getStructureTreeRoot();
        if (root != null)
        {
            return root.getRoleMap();
        }
        return Collections.emptyMap();
    }

}
