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

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

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.pdmodel.common.COSArrayList;
import org.apache.pdfbox.pdmodel.common.COSObjectable;
import org.apache.pdfbox.pdmodel.common.PDTextStream;
import org.apache.pdfbox.pdmodel.fdf.FDFField;
import org.apache.pdfbox.pdmodel.interactive.action.PDFormFieldAdditionalActions;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
import org.apache.pdfbox.util.BitFlagHelper;

/**
 * This is the superclass for a Field element in a PDF. Based on the COS object model from PDFBox.
 * 
 * @author sug
 * 
 */
public abstract class PDField implements COSObjectable
{
    /**
     * A Ff flag.
     */
    public static final int FLAG_READ_ONLY = 1;
    /**
     * A Ff flag.
     */
    public static final int FLAG_REQUIRED = 1 << 1;
    /**
     * A Ff flag.
     */
    public static final int FLAG_NO_EXPORT = 1 << 2;

    private PDAcroForm acroForm;

    private COSDictionary dictionary;

    /**
     * Constructor.
     * 
     * @param theAcroForm The form that this field is part of.
     */
    public PDField(PDAcroForm theAcroForm)
    {
        acroForm = theAcroForm;
        dictionary = new COSDictionary();
        // no required fields in base field class
    }

    /**
     * Creates a COSField from a COSDictionary, expected to be a correct object definition for a field in PDF.
     * 
     * @param theAcroForm The form that this field is part of.
     * @param field the PDF objet to represent as a field.
     */
    public PDField(PDAcroForm theAcroForm, COSDictionary field)
    {
        acroForm = theAcroForm;
        dictionary = field;
    }

    /**
     * Returns the partial name of the field.
     * 
     * @return the name of the field
     */
    public String getPartialName()
    {
        return getDictionary().getString(COSName.T);
    }

    /**
     * This will set the partial name of the field.
     * 
     * @param name The new name for the field.
     */
    public void setPartialName(String name)
    {
        getDictionary().setString(COSName.T, name);
    }

    /**
     * Returns the fully qualified name of the field, which is a concatenation of the names of all the parents fields.
     * 
     * @return the name of the field
     * 
     * @throws IOException If there is an error generating the fully qualified name.
     */
    public String getFullyQualifiedName() throws IOException
    {
        PDField parent = getParent();
        String parentName = null;
        if (parent != null)
        {
            parentName = parent.getFullyQualifiedName();
        }
        String finalName = getPartialName();
        if (parentName != null)
        {
            if (finalName != null)
            {
                finalName = parentName + "." + finalName;
            }
            else
            {
                finalName = parentName;
            }
        }
        return finalName;
    }

    /**
     * Gets the alternate name of the field.
     * 
     * @return the alternate name of the field
     */
    public String getAlternateFieldName()
    {
        return this.getDictionary().getString(COSName.TU);
    }

    /**
     * This will set the alternate name of the field.
     * 
     * @param alternateFieldName the alternate name of the field
     */
    public void setAlternateFieldName(String alternateFieldName)
    {
        this.getDictionary().setString(COSName.TU, alternateFieldName);
    }

    /**
     * Get the FT entry of the field. This is a read only field and is set depending on the actual type. The field type
     * is an inheritable attribute. This method will return only the direct value on this object. Use the findFieldType
     * for an upward recursive search.
     * 
     * @return The Field type.
     * 
     * @see PDField#findFieldType()
     */
    public String getFieldType()
    {
        return getDictionary().getNameAsString(COSName.FT);
    }

    /**
     * Find the field type and optionally do a recursive upward search. Sometimes the fieldtype will be specified on the
     * parent instead of the direct object. This will look at this object for the field type, if none is specified then
     * it will look to the parent if there is a parent. If there is no parent and no field type has been found then this
     * will return null.
     * 
     * @return The field type or null if none was found.
     */
    public String findFieldType()
    {
        return findFieldType(getDictionary());
    }

    private String findFieldType(COSDictionary dic)
    {
        String retval = dic.getNameAsString(COSName.FT);
        if (retval == null)
        {
            COSDictionary parent = (COSDictionary) dic.getDictionaryObject(COSName.PARENT, COSName.P);
            if (parent != null)
            {
                retval = findFieldType(parent);
            }
        }
        return retval;

    }

    /**
     * setValue sets the fields value to a given string.
     * 
     * @param value the string value
     * 
     * @throws IOException If there is an error creating the appearance stream.
     */
    public abstract void setValue(String value) throws IOException;

    /**
     * getValue gets the fields value to as a string.
     * 
     * @return The string value of this field.
     * 
     * @throws IOException If there is an error getting the value.
     */
    public abstract String getValue() throws IOException;

    /**
     * sets the field to be read-only.
     * 
     * @param readonly The new flag for readonly.
     */
    public void setReadonly(boolean readonly)
    {
        BitFlagHelper.setFlag(getDictionary(), COSName.FF, FLAG_READ_ONLY, readonly);
    }

    /**
     * 
     * @return true if the field is readonly
     */
    public boolean isReadonly()
    {
        return BitFlagHelper.getFlag(getDictionary(), COSName.FF, FLAG_READ_ONLY);
    }

    /**
     * sets the flag whether the field is to be required to have a value at the time it is exported
     * by a submit-form action.
     *
     * @param required The new flag for required.
     */
    public void setRequired(boolean required)
    {
        BitFlagHelper.setFlag(getDictionary(), COSName.FF, FLAG_REQUIRED, required);
    }

    /**
     * @return true if the field is required to have a value at the time it is exported by a
     * submit-form action.
     */
    public boolean isRequired()
    {
        return BitFlagHelper.getFlag(getDictionary(), COSName.FF, FLAG_REQUIRED);
    }

    /**
     * sets the field to be not exported..
     * 
     * @param noExport The new flag for noExport.
     */
    public void setNoExport(boolean noExport)
    {
        BitFlagHelper.setFlag(getDictionary(), COSName.FF, FLAG_NO_EXPORT, noExport);
    }

    /**
     * 
     * @return true if the field is not to be exported.
     */
    public boolean isNoExport()
    {
        return BitFlagHelper.getFlag(getDictionary(), COSName.FF, FLAG_NO_EXPORT);
    }

    /**
     * This will get the flags for this field.
     * 
     * @return flags The set of flags.
     */
    public int getFieldFlags()
    {
        int retval = 0;
        COSInteger ff = (COSInteger) getDictionary().getDictionaryObject(COSName.FF);
        if (ff != null)
        {
            retval = ff.intValue();
        }
        return retval;
    }

    /**
     * This will set the flags for this field.
     * 
     * @param flags The new flags.
     */
    public void setFieldFlags(int flags)
    {
        getDictionary().setInt(COSName.FF, flags);
    }

    /**
     * This will import a fdf field from a fdf document.
     * 
     * @param fdfField The fdf field to import.
     * 
     * @throws IOException If there is an error importing the data for this field.
     */
    public void importFDF(FDFField fdfField) throws IOException
    {
        Object fieldValue = fdfField.getValue();
        int fieldFlags = getFieldFlags();

        if (fieldValue != null)
        {
            if (fieldValue instanceof String)
            {
                setValue((String) fieldValue);
            }
            else if (fieldValue instanceof PDTextStream)
            {
                setValue(((PDTextStream) fieldValue).getAsString());
            }
            else
            {
                throw new IOException("Unknown field type:" + fieldValue.getClass().getName());
            }
        }
        Integer ff = fdfField.getFieldFlags();
        if (ff != null)
        {
            setFieldFlags(ff.intValue());
        }
        else
        {
            // these are suppose to be ignored if the Ff is set.
            Integer setFf = fdfField.getSetFieldFlags();

            if (setFf != null)
            {
                int setFfInt = setFf.intValue();
                fieldFlags = fieldFlags | setFfInt;
                setFieldFlags(fieldFlags);
            }

            Integer clrFf = fdfField.getClearFieldFlags();
            if (clrFf != null)
            {
                // we have to clear the bits of the document fields for every bit that is
                // set in this field.
                //
                // Example:
                // docFf = 1011
                // clrFf = 1101
                // clrFfValue = 0010;
                // newValue = 1011 & 0010 which is 0010
                int clrFfValue = clrFf.intValue();
                clrFfValue ^= 0xFFFFFFFF;
                fieldFlags = fieldFlags & clrFfValue;
                setFieldFlags(fieldFlags);
            }
        }

        PDAnnotationWidget widget = getWidget();
        if (widget != null)
        {
            int annotFlags = widget.getAnnotationFlags();
            Integer f = fdfField.getWidgetFieldFlags();
            if (f != null && widget != null)
            {
                widget.setAnnotationFlags(f.intValue());
            }
            else
            {
                // these are suppose to be ignored if the F is set.
                Integer setF = fdfField.getSetWidgetFieldFlags();
                if (setF != null)
                {
                    annotFlags = annotFlags | setF.intValue();
                    widget.setAnnotationFlags(annotFlags);
                }

                Integer clrF = fdfField.getClearWidgetFieldFlags();
                if (clrF != null)
                {
                    // we have to clear the bits of the document fields for every bit that is
                    // set in this field.
                    //
                    // Example:
                    // docF = 1011
                    // clrF = 1101
                    // clrFValue = 0010;
                    // newValue = 1011 & 0010 which is 0010
                    int clrFValue = clrF.intValue();
                    clrFValue ^= 0xFFFFFFFFL;
                    annotFlags = annotFlags & clrFValue;
                    widget.setAnnotationFlags(annotFlags);
                }
            }
        }
        List<FDFField> fdfKids = fdfField.getKids();
        List<COSObjectable> pdKids = getKids();
        for (int i = 0; fdfKids != null && i < fdfKids.size(); i++)
        {
            FDFField fdfChild = fdfKids.get(i);
            String fdfName = fdfChild.getPartialFieldName();
            for (int j = 0; j < pdKids.size(); j++)
            {
                Object pdChildObj = pdKids.get(j);
                if (pdChildObj instanceof PDField)
                {
                    PDField pdChild = (PDField) pdChildObj;
                    if (fdfName != null && fdfName.equals(pdChild.getPartialName()))
                    {
                        pdChild.importFDF(fdfChild);
                    }
                }
            }
        }
    }

    /**
     * This will get the single associated widget that is part of this field. This occurs when the Widget is
     * embedded in the fields dictionary. Sometimes there are multiple sub widgets associated with this field,
     * in which case you want to use {@link #getWidgets()}. If the kids entry is specified, then only first
     * entry in that list will be returned.
     * 
     * @return The widget that is associated with this field.
     * @throws IOException If there is an error getting the widget object.
     */
    public PDAnnotationWidget getWidget() throws IOException
    {
        PDAnnotationWidget retval = null;
        List<COSObjectable> kids = getKids();
        if (kids == null)
        {
            retval = new PDAnnotationWidget(getDictionary());
        }
        else if (kids.size() > 0)
        {
            Object firstKid = kids.get(0);
            if (firstKid instanceof PDAnnotationWidget)
            {
                retval = (PDAnnotationWidget) firstKid;
            }
            else
            {
                retval = ((PDField) firstKid).getWidget();
            }
        }
        else
        {
            retval = null;
        }
        return retval;
    }

    /**
     * Returns the widget annotations associated with this field.
     * 
     * The widget annotations are returned for terminal fields (fields which
     * do not have other fields as kids). In case of non terminal fields an 
     * empty List is returned. 
     * 
     * @return The list of widget annotations.
     */
    public List<PDAnnotationWidget> getWidgets()
    {
        List<PDAnnotationWidget> widgets = new ArrayList<PDAnnotationWidget>();
        
        COSArray kids = (COSArray) getDictionary().getDictionaryObject(COSName.KIDS);
        if (kids == null)
        {
            // the field itself is a widget
            widgets.add(new PDAnnotationWidget(getDictionary()));
        }
        else
        {
            for (int i = 0; i < kids.size(); i++)
            {
                COSDictionary kidDictionary = (COSDictionary) kids.getObject(i);
                
                if (kidDictionary == null)
                {
                   continue;
                }
                
                if ("Widget".equals(kidDictionary.getNameAsString(COSName.SUBTYPE)))
                {
                    widgets.add(new PDAnnotationWidget(kidDictionary));
                }
            }
        }
        return widgets;
    }
    
    /**
     * Get the parent field to this field, or null if none exists.
     * 
     * @return The parent field.
     * 
     * @throws IOException If there is an error creating the parent field.
     */
    public PDField getParent() throws IOException
    {
        PDField parent = null;
        COSDictionary parentDic = (COSDictionary) getDictionary().getDictionaryObject(COSName.PARENT, COSName.P);
        if (parentDic != null)
        {
            parent = PDFieldFactory.createField(getAcroForm(), parentDic);
        }
        return parent;
    }

    /**
     * Set the parent of this field.
     * 
     * @param parent The parent to this field.
     */
    public void setParent(PDField parent)
    {
        getDictionary().setItem("Parent", parent);
    }

    /**
     * This will find one of the child elements. The name array are the components of the name to search down the tree
     * of names. The nameIndex is where to start in that array. This method is called recursively until it finds the end
     * point based on the name array.
     * 
     * @param name An array that picks the path to the field.
     * @param nameIndex The index into the array.
     * @return The field at the endpoint or null if none is found.
     * @throws IOException If there is an error creating the field.
     */
    public PDField findKid(String[] name, int nameIndex) throws IOException
    {
        PDField retval = null;
        COSArray kids = (COSArray) getDictionary().getDictionaryObject(COSName.KIDS);
        if (kids != null)
        {
            for (int i = 0; retval == null && i < kids.size(); i++)
            {
                COSDictionary kidDictionary = (COSDictionary) kids.getObject(i);
                if (name[nameIndex].equals(kidDictionary.getString("T")))
                {
                    retval = PDFieldFactory.createField(acroForm, kidDictionary);
                    if (name.length > nameIndex + 1)
                    {
                        retval = retval.findKid(name, nameIndex + 1);
                    }
                }
            }
        }
        return retval;
    }

    /**
     * This will get all the kids of this field. The values in the list will either be PDWidget or PDField. Normally
     * they will be PDWidget objects unless this is a non-terminal field and they will be child PDField objects.
     * 
     * @return A list of either PDWidget or PDField objects.
     * @throws IOException If there is an error retrieving the kids.
     */
    public List<COSObjectable> getKids() throws IOException
    {
        List<COSObjectable> retval = null;
        COSArray kids = (COSArray) getDictionary().getDictionaryObject(COSName.KIDS);
        
        if (kids != null)
        {
            List<COSObjectable> kidsList = new ArrayList<COSObjectable>();
            for (int i = 0; i < kids.size(); i++)
            {
                COSDictionary kidDictionary = (COSDictionary) kids.getObject(i);
                
                if (kidDictionary == null)
                {
                   continue;
                }
                
                // Decide if the kid is field or a widget annotation.
                // A field dictionary that does not have a partial field name (T entry)
                // of its own shall not be considered a field but simply a Widget annotation.
                if (kidDictionary.getDictionaryObject(COSName.T) != null)
                {
                    PDField field = PDFieldFactory.createField(acroForm, kidDictionary);
                        
                    if (field != null)
                    {
                       kidsList.add(field);
                    }
                }
                else if ("Widget".equals(kidDictionary.getNameAsString(COSName.SUBTYPE)))
                {
                    kidsList.add(new PDAnnotationWidget(kidDictionary));
                }
            }
            retval = new COSArrayList<COSObjectable>(kidsList, kids);
        }
        return retval;
    }

    /**
     * This will set the list of kids.
     * 
     * @param kids The list of child widgets.
     */
    public void setKids(List<COSObjectable> kids)
    {
        COSArray kidsArray = COSArrayList.converterToCOSArray(kids);
        getDictionary().setItem(COSName.KIDS, kidsArray);
    }

    /**
     * This will return a string representation of this field.
     * 
     * @return A string representation of this field.
     */
    @Override
    public String toString()
    {
        return "" + getDictionary().getDictionaryObject(COSName.V);
    }

    /**
     * This will get the acroform that this field is part of.
     * 
     * @return The form this field is on.
     */
    public PDAcroForm getAcroForm()
    {
        return acroForm;
    }

    /**
     * This will set the form this field is on.
     * 
     * @param value The new form to use.
     */
    public void setAcroForm(PDAcroForm value)
    {
        acroForm = value;
    }

    /**
     * This will get the dictionary associated with this field.
     * 
     * @return The dictionary that this class wraps.
     */
    public COSDictionary getDictionary()
    {
        return dictionary;
    }

    /**
     * Convert this standard java object to a COS object.
     * 
     * @return The cos object that matches this Java object.
     */
    public COSBase getCOSObject()
    {
        return dictionary;
    }

    /**
     * Get the additional actions for this field. This will return null if there are no additional actions for this
     * field.
     * 
     * @return The actions of the field.
     */
    public PDFormFieldAdditionalActions getActions()
    {
        COSDictionary aa = (COSDictionary) dictionary.getDictionaryObject(COSName.AA);
        PDFormFieldAdditionalActions retval = null;
        if (aa != null)
        {
            retval = new PDFormFieldAdditionalActions(aa);
        }
        return retval;
    }

    /**
     * Set the actions of the field.
     * 
     * @param actions The field actions.
     */
    public void setActions(PDFormFieldAdditionalActions actions)
    {
        dictionary.setItem(COSName.AA, actions);
    }
    
    /**
     * Set the field type.
     * 
     * @param fieldType the field type string must be one of "Btn", "Ch", "Tx", "Sig"
     */
    protected void setFieldType(String fieldType)
    {
        if (fieldType.compareTo("Btn") != 0 &&
            fieldType.compareTo("Ch") != 0 &&
            fieldType.compareTo("Tx") != 0 &&
            fieldType.compareTo("Sig") != 0
                )
        {
            throw new IllegalArgumentException("Unknown field type given " + fieldType);
        }
        else
        {
            getDictionary().setName(COSName.FT, fieldType);
        }
    }
}
