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

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

import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import org.apache.pdfbox.cos.COSArray;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.cos.COSStream;
import org.apache.pdfbox.cos.COSString;
import org.apache.pdfbox.pdmodel.common.COSObjectable;
import org.apache.pdfbox.pdmodel.common.COSArrayList;
import org.apache.pdfbox.pdmodel.common.filespecification.PDFileSpecification;
import org.apache.pdfbox.pdmodel.common.filespecification.PDSimpleFileSpecification;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * This represents an FDF dictionary that is part of the FDF document.
 *
 * @author Ben Litchfield
 */
public class FDFDictionary implements COSObjectable
{

    private static final Logger LOG = LogManager.getLogger(FDFDictionary.class);

    private final COSDictionary fdf;

    /**
     * Default constructor.
     */
    public FDFDictionary()
    {
        fdf = new COSDictionary();
    }

    /**
     * Constructor.
     *
     * @param fdfDictionary The FDF documents catalog.
     */
    public FDFDictionary(COSDictionary fdfDictionary)
    {
        fdf = fdfDictionary;
    }

    /**
     * This will create an FDF dictionary from an XFDF XML document.
     *
     * @param fdfXML The XML document that contains the XFDF data.
     */
    public FDFDictionary(Element fdfXML)
    {
        this();
        NodeList nodeList = fdfXML.getChildNodes();
        for (int i = 0; i < nodeList.getLength(); i++)
        {
            Node node = nodeList.item(i);
            if (node instanceof Element)
            {
                Element child = (Element) node;
                switch (child.getTagName())
                {
                    case "f":
                        PDSimpleFileSpecification fs = new PDSimpleFileSpecification();
                        fs.setFile(child.getAttribute("href"));
                        setFile(fs);
                        break;
                    case "ids":
                        COSArray ids = new COSArray();
                        String original = child.getAttribute("original");
                        String modified = child.getAttribute("modified");
                        try
                        {
                            ids.add(COSString.parseHex(original));
                        }
                        catch (IOException e)
                        {
                            LOG.warn(() ->
                                    "Error parsing ID entry for attribute 'original' [" + 
                                            original + "]. ID entry ignored.",
                                    e);
                        }
                        try
                        {
                            ids.add(COSString.parseHex(modified));
                        }
                        catch (IOException e)
                        {
                            LOG.warn(() ->
                                    "Error parsing ID entry for attribute 'modified' [ + " + 
                                            modified + "]. ID entry ignored.", 
                                    e);
                        }
                        setID(ids);
                        break;
                    case "fields":
                        NodeList fields = child.getChildNodes();
                        List<FDFField> fieldList = new ArrayList<>();
                        for (int f = 0; f < fields.getLength(); f++)
                        {
                            Node currentNode = fields.item(f);
                            if (currentNode instanceof Element
                                    && ((Element) currentNode).getTagName().equals("field"))
                            {
                                try
                                {
                                    fieldList.add(new FDFField((Element) fields.item(f)));
                                }
                                catch (IOException e)
                                {
                                    LOG.warn(() -> "Error parsing field entry [" + 
                                            currentNode.getNodeValue() + "]. Field ignored.",
                                            e);
                                }
                            }
                        }
                        setFields(fieldList);
                        break;
                    case "annots":
                        NodeList annots = child.getChildNodes();
                        List<FDFAnnotation> annotList = new ArrayList<>();
                        for (int j = 0; j < annots.getLength(); j++)
                        {
                            Node annotNode = annots.item(j);
                            if (annotNode instanceof Element)
                            {
                                // the node name defines the annotation type
                                Element annot = (Element) annotNode;
                                String annotationName = annot.getNodeName();
                                try
                                {
                                    switch (annotationName)
                                    {
                                        case "text":
                                            annotList.add(new FDFAnnotationText(annot));
                                            break;
                                        case "caret":
                                            annotList.add(new FDFAnnotationCaret(annot));
                                            break;
                                        case "freetext":
                                            annotList.add(new FDFAnnotationFreeText(annot));
                                            break;
                                        case "fileattachment":
                                            annotList.add(new FDFAnnotationFileAttachment(annot));
                                            break;
                                        case "highlight":
                                            annotList.add(new FDFAnnotationHighlight(annot));
                                            break;
                                        case "ink":
                                            annotList.add(new FDFAnnotationInk(annot));
                                            break;
                                        case "line":
                                            annotList.add(new FDFAnnotationLine(annot));
                                            break;
                                        case "link":
                                            annotList.add(new FDFAnnotationLink(annot));
                                            break;
                                        case "circle":
                                            annotList.add(new FDFAnnotationCircle(annot));
                                            break;
                                        case "square":
                                            annotList.add(new FDFAnnotationSquare(annot));
                                            break;
                                        case "polygon":
                                            annotList.add(new FDFAnnotationPolygon(annot));
                                            break;
                                        case "polyline":
                                            annotList.add(new FDFAnnotationPolyline(annot));
                                            break;
                                        case "sound":
                                            annotList.add(new FDFAnnotationSound(annot));
                                            break;
                                        case "squiggly":
                                            annotList.add(new FDFAnnotationSquiggly(annot));
                                            break;
                                        case "stamp":
                                            annotList.add(new FDFAnnotationStamp(annot));
                                            break;
                                        case "strikeout":
                                            annotList.add(new FDFAnnotationStrikeOut(annot));
                                            break;
                                        case "underline":
                                            annotList.add(new FDFAnnotationUnderline(annot));
                                            break;
                                        default:
                                            LOG.warn("Unknown or unsupported annotation type '{}'",
                                                    annotationName);
                                            break;
                                    }
                                }
                                catch (IOException e)
                                {
                                    LOG.warn(() ->
                                            "Error parsing annotation information [" +
                                            annot.getNodeValue() + "]. Annotation ignored",
                                            e);
                                }
                            }
                        }
                        setAnnotations(annotList);
                        break;
                    default:
                        break;
                }
            }
        }
    }

    /**
     * This will write this element as an XML document.
     *
     * @param output The stream to write the xml to.
     *
     * @throws IOException If there is an error writing the XML.
     */
    public void writeXML(Writer output) throws IOException
    {
        PDFileSpecification fs = this.getFile();
        if (fs != null)
        {
            output.write("<f href=\"" + fs.getFile() + "\" />\n");
        }
        COSArray ids = this.getID();
        if (ids != null)
        {
            COSString original = (COSString) ids.getObject(0);
            COSString modified = (COSString) ids.getObject(1);
            output.write("<ids original=\"" + original.toHexString() + "\" ");
            output.write("modified=\"" + modified.toHexString() + "\" />\n");
        }
        List<FDFField> fields = getFields();
        if (fields != null && !fields.isEmpty())
        {
            output.write("<fields>\n");
            for (FDFField field : fields)
            {
                field.writeXML(output);
            }
            output.write("</fields>\n");
        }
    }

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

    /**
     * The source file or target file: the PDF document file that this FDF file was exported from or is intended to be
     * imported into.
     *
     * @return The F entry of the FDF dictionary.
     *
     * @throws IOException If there is an error creating the file spec.
     */
    public PDFileSpecification getFile() throws IOException
    {
        return PDFileSpecification.createFS(fdf.getDictionaryObject(COSName.F));
    }

    /**
     * This will set the file specification.
     *
     * @param fs The file specification.
     */
    public final void setFile(PDFileSpecification fs)
    {
        fdf.setItem(COSName.F, fs);
    }

    /**
     * This is the FDF id.
     *
     * @return The FDF ID.
     */
    public COSArray getID()
    {
        return fdf.getCOSArray(COSName.ID);
    }

    /**
     * This will set the FDF id.
     *
     * @param id The new id for the FDF.
     */
    public final void setID(COSArray id)
    {
        fdf.setItem(COSName.ID, id);
    }

    /**
     * This will get the list of FDF Fields. This will return a list of FDFField objects.
     *
     * @return A list of FDF fields.
     */
    public List<FDFField> getFields()
    {
        List<FDFField> retval = null;
        COSArray fieldArray = fdf.getCOSArray(COSName.FIELDS);
        if (fieldArray != null)
        {
            List<FDFField> fields = new ArrayList<>(fieldArray.size());
            for (int i = 0; i < fieldArray.size(); i++)
            {
                fields.add(new FDFField((COSDictionary) fieldArray.getObject(i)));
            }
            retval = new COSArrayList<>(fields, fieldArray);
        }
        return retval;
    }

    /**
     * This will set the list of fields. This should be a list of FDFField objects.
     *
     * @param fields The list of fields.
     */
    public final void setFields(List<FDFField> fields)
    {
        fdf.setItem(COSName.FIELDS, new COSArray(fields));
    }

    /**
     * This will get the status string to be displayed as the result of an action.
     *
     * @return The status.
     */
    public String getStatus()
    {
        return fdf.getString(COSName.STATUS);
    }

    /**
     * This will set the status string.
     *
     * @param status The new status string.
     */
    public void setStatus(String status)
    {
        fdf.setString(COSName.STATUS, status);
    }

    /**
     * This will get the list of FDF Pages. This will return a list of FDFPage objects.
     *
     * @return A list of FDF pages.
     */
    public List<FDFPage> getPages()
    {
        List<FDFPage> retval = null;
        COSArray pageArray = fdf.getCOSArray(COSName.PAGES);
        if (pageArray != null)
        {
            List<FDFPage> pages = new ArrayList<>(pageArray.size());
            for (int i = 0; i < pageArray.size(); i++)
            {
                pages.add(new FDFPage((COSDictionary) pageArray.get(i)));
            }
            retval = new COSArrayList<>(pages, pageArray);
        }
        return retval;
    }

    /**
     * This will set the list of pages. This should be a list of FDFPage objects.
     *
     *
     * @param pages The list of pages.
     */
    public void setPages(List<FDFPage> pages)
    {
        fdf.setItem(COSName.PAGES, new COSArray(pages));
    }

    /**
     * The encoding to be used for a FDF field. The default is PDFDocEncoding and this method will never return null.
     *
     * @return The encoding value.
     */
    public String getEncoding()
    {
        String encoding = fdf.getNameAsString(COSName.ENCODING);
        if (encoding == null)
        {
            encoding = "PDFDocEncoding";
        }
        return encoding;

    }

    /**
     * This will set the encoding.
     *
     * @param encoding The new encoding.
     */
    public void setEncoding(String encoding)
    {
        fdf.setName(COSName.ENCODING, encoding);
    }

    /**
     * This will get the list of FDF Annotations. This will return a list of FDFAnnotation objects or null if the entry
     * is not set.
     *
     * @return A list of FDF annotations.
     *
     * @throws IOException If there is an error creating the annotation list.
     */
    public List<FDFAnnotation> getAnnotations() throws IOException
    {
        List<FDFAnnotation> retval = null;
        COSArray annotArray = fdf.getCOSArray(COSName.ANNOTS);
        if (annotArray != null)
        {
            List<FDFAnnotation> annots = new ArrayList<>(annotArray.size());
            for (int i = 0; i < annotArray.size(); i++)
            {
                annots.add(FDFAnnotation.create((COSDictionary) annotArray.getObject(i)));
            }
            retval = new COSArrayList<>(annots, annotArray);
        }
        return retval;
    }

    /**
     * This will set the list of annotations. This should be a list of FDFAnnotation objects.
     *
     *
     * @param annots The list of annotations.
     */
    public final void setAnnotations(List<FDFAnnotation> annots)
    {
        fdf.setItem(COSName.ANNOTS, new COSArray(annots));
    }

    /**
     * This will get the incremental updates since the PDF was last opened.
     *
     * @return The differences entry of the FDF dictionary.
     */
    public COSStream getDifferences()
    {
        return fdf.getCOSStream(COSName.DIFFERENCES);
    }

    /**
     * This will set the differences stream.
     *
     * @param diff The new differences stream.
     */
    public void setDifferences(COSStream diff)
    {
        fdf.setItem(COSName.DIFFERENCES, diff);
    }

    /**
     * This will get the target frame in the browser to open this document.
     *
     * @return The target frame.
     */
    public String getTarget()
    {
        return fdf.getString(COSName.TARGET);
    }

    /**
     * This will set the target frame in the browser to open this document.
     *
     * @param target The new target frame.
     */
    public void setTarget(String target)
    {
        fdf.setString(COSName.TARGET, target);
    }

    /**
     * This will get the list of embedded FDF entries, or null if the entry is null. This will return a list of
     * PDFileSpecification objects.
     *
     * @return A list of embedded FDF files.
     *
     * @throws IOException If there is an error creating the file spec.
     */
    public List<PDFileSpecification> getEmbeddedFDFs() throws IOException
    {
        List<PDFileSpecification> retval = null;
        COSArray embeddedArray = fdf.getCOSArray(COSName.EMBEDDED_FDFS);
        if (embeddedArray != null)
        {
            List<PDFileSpecification> embedded = new ArrayList<>(embeddedArray.size());
            for (int i = 0; i < embeddedArray.size(); i++)
            {
                embedded.add(PDFileSpecification.createFS(embeddedArray.get(i)));
            }
            retval = new COSArrayList<>(embedded, embeddedArray);
        }
        return retval;
    }

    /**
     * This will set the list of embedded FDFs. This should be a list of PDFileSpecification objects.
     *
     *
     * @param embedded The list of embedded FDFs.
     */
    public void setEmbeddedFDFs(List<PDFileSpecification> embedded)
    {
        fdf.setItem(COSName.EMBEDDED_FDFS, new COSArray(embedded));
    }

    /**
     * This will get the java script entry.
     *
     * @return The java script entry describing javascript commands.
     */
    public FDFJavaScript getJavaScript()
    {
        COSDictionary dic = fdf.getCOSDictionary(COSName.JAVA_SCRIPT);
        return dic != null ? new FDFJavaScript(dic) : null;
    }

    /**
     * This will set the JavaScript entry.
     *
     * @param js The javascript entries.
     */
    public void setJavaScript(FDFJavaScript js)
    {
        fdf.setItem(COSName.JAVA_SCRIPT, js);
    }

}
