package mkl.testarea.pdfbox2.form;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;

import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.apache.pdfbox.pdmodel.interactive.form.PDField;
import org.apache.pdfbox.pdmodel.interactive.form.PDNonTerminalField;
import org.apache.pdfbox.pdmodel.interactive.form.PDTerminalField;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;

/**
 * <a href="https://stackoverflow.com/questions/47190189/does-pdfbox-allow-to-remove-one-field-from-acroform">
 * Does PDFBox allow to remove one field from AcroForm?
 * </a>
 * <p>
 * This class contains
 * </p>
 * <ul>
 * <li> {@link #testRemoveFormIntroSsnManually()} - a test that explicitly
 *  contains code to remove a specific field from a PDF;</li>
 * <li> {@link #removeField(PDDocument, String)} and {@link #removeWidgets(PDField)} -
 *  helper methods generalizing the approach in the test above;</li>
 * <li> {@link #testRemoveFormIntroSsn()}, {@link #testRemoveFormIntro()}, and
 *  {@link #testRemoveFormRoot()} - tests of those helper methods applied to a
 *  non-root terminal field, a non-root non-terminal field, and a root non-terminal
 *  field respectively;</li>
 * <li> {@link #testRemoveInvisibleSignature()} and {@link #testRemoveVisibleSignature()} -
 *  tests of those helper methods applied to root terminal fields which are signature
 *  fields, invisible or visible.</li>
 * </ul>
 * 
 * @author mkl
 */
public class RemoveField {
    final static File RESULT_FOLDER = new File("target/test-outputs", "form");

    @BeforeClass
    public static void setUpBeforeClass() throws Exception {
        RESULT_FOLDER.mkdirs();
    }

    @Test
    public void testRemoveFormIntroSsnManually() throws IOException {
        try (   InputStream resource = getClass().getResourceAsStream("GeneralForbearance.pdf")    ) {
            PDDocument document = Loader.loadPDF(resource);
            PDDocumentCatalog documentCatalog = document.getDocumentCatalog();
            PDAcroForm acroForm = documentCatalog.getAcroForm();

            PDField introSsnField = null;

            for (PDField field : acroForm.getFieldTree()) {
                System.out.println(field.getFullyQualifiedName());
                if ("form1[0].#subform[0].FormIntro[0].SSN[0]".equals(field.getFullyQualifiedName())) {
                    introSsnField = field;
                    break;
                }
            }
            Assert.assertNotNull("introSsnField", introSsnField);
            Assert.assertTrue("introSsnField not terminal", introSsnField instanceof PDTerminalField);

            PDNonTerminalField introField = introSsnField.getParent();
            Assert.assertNotNull("introField", introField);
            List<PDField> introChildFields = introField.getChildren();
            boolean removed = false;
            for (PDField field : introChildFields)
            {
                if (field.getCOSObject().equals(introSsnField.getCOSObject())) {
                    removed = introChildFields.remove(field);
                    break;
                }
            }
            introField.setChildren(introChildFields);
            Assert.assertTrue("introSsnField not removed from introField", removed);

            List<PDAnnotationWidget> widgets = ((PDTerminalField)introSsnField).getWidgets();
            for (PDAnnotationWidget widget : widgets) {
                System.out.println("Removing FormIntro field SSN widget from page");
                PDPage page = widget.getPage();
                Assert.assertNotNull("FormIntro field SSN widget has no page", page);
                List<PDAnnotation> annotations = page.getAnnotations();
                removed = false;
                for (PDAnnotation annotation : annotations) {
                    if (annotation.getCOSObject().equals(widget.getCOSObject()))
                    {
                        removed = annotations.remove(annotation);
                        break;
                    }
                }
                Assert.assertTrue("FormIntro field SSN widget not removed from page", removed);
            }

            document.save(new File(RESULT_FOLDER, "GeneralForbearance-RemoveFormIntroSsnManually.pdf"));        
            document.close();
        }
    }

    @Test
    public void testRemoveFormIntroSsn() throws IOException {
        try (   InputStream resource = getClass().getResourceAsStream("GeneralForbearance.pdf")    ) {
            PDDocument document = Loader.loadPDF(resource);

            PDField field = removeField(document, "form1[0].#subform[0].FormIntro[0].SSN[0]");
            Assert.assertNotNull("Field not found", field);

            document.save(new File(RESULT_FOLDER, "GeneralForbearance-RemoveFormIntroSsn.pdf"));        
            document.close();
        }
    }

    @Test
    public void testRemoveFormIntro() throws IOException {
        try (   InputStream resource = getClass().getResourceAsStream("GeneralForbearance.pdf")    ) {
            PDDocument document = Loader.loadPDF(resource);

            PDField field = removeField(document, "form1[0].#subform[0].FormIntro[0]");
            Assert.assertNotNull("Field not found", field);

            document.save(new File(RESULT_FOLDER, "GeneralForbearance-RemoveFormIntro.pdf"));        
            document.close();
        }
    }

    @Test
    public void testRemoveFormRoot() throws IOException {
        try (   InputStream resource = getClass().getResourceAsStream("GeneralForbearance.pdf")    ) {
            PDDocument document = Loader.loadPDF(resource);

            PDField field = removeField(document, "form1[0]");
            Assert.assertNotNull("Field not found", field);

            document.save(new File(RESULT_FOLDER, "GeneralForbearance-RemoveFormRoot.pdf"));        
            document.close();
        }
    }

    @Test
    public void testRemoveInvisibleSignature() throws IOException {
        try (   InputStream resource = getClass().getResourceAsStream("/mkl/testarea/pdfbox2/sign/SignatureVlidationTest.pdf")    ) {
            PDDocument document = Loader.loadPDF(resource);

            PDField field = removeField(document, "Signature1");
            Assert.assertNotNull("Field not found", field);

            document.save(new File(RESULT_FOLDER, "SignatureVlidationTest-RemoveSignature1.pdf"));        
            document.close();
        }
    }

    @Test
    public void testRemoveVisibleSignature() throws IOException {
        try (   InputStream resource = getClass().getResourceAsStream("2g-fix-certified.pdf")    ) {
            PDDocument document = Loader.loadPDF(resource);

            PDField field = removeField(document, "sig");
            Assert.assertNotNull("Field not found", field);

            document.save(new File(RESULT_FOLDER, "2g-fix-certified-RemoveSig.pdf"));        
            document.close();
        }
    }

    PDField removeField(PDDocument document, String fullFieldName) throws IOException {
        PDDocumentCatalog documentCatalog = document.getDocumentCatalog();
        PDAcroForm acroForm = documentCatalog.getAcroForm();

        if (acroForm == null) {
            System.out.println("No form defined.");
            return null;
        }

        PDField targetField = null;

        for (PDField field : acroForm.getFieldTree()) {
            if (fullFieldName.equals(field.getFullyQualifiedName())) {
                targetField = field;
                break;
            }
        }
        if (targetField == null) {
            System.out.println("Form does not contain field with given name.");
            return null;
        }

        PDNonTerminalField parentField = targetField.getParent();
        if (parentField != null) {
            List<PDField> childFields = parentField.getChildren();
            boolean removed = false;
            for (PDField field : childFields)
            {
                if (field.getCOSObject().equals(targetField.getCOSObject())) {
                    removed = childFields.remove(field);
                    parentField.setChildren(childFields);
                    break;
                }
            }
            if (!removed)
                System.out.println("Inconsistent form definition: Parent field does not reference the target field.");
        } else {
            List<PDField> rootFields = acroForm.getFields();
            boolean removed = false;
            for (PDField field : rootFields)
            {
                if (field.getCOSObject().equals(targetField.getCOSObject())) {
                    removed = rootFields.remove(field);
                    break;
                }
            }
            if (!removed)
                System.out.println("Inconsistent form definition: Root fields do not include the target field.");
        }

        removeWidgets(targetField);

        return targetField;
    }

    void removeWidgets(PDField targetField) throws IOException {
        if (targetField instanceof PDTerminalField) {
            List<PDAnnotationWidget> widgets = ((PDTerminalField)targetField).getWidgets();
            for (PDAnnotationWidget widget : widgets) {
                PDPage page = widget.getPage();
                if (page != null) {
                    List<PDAnnotation> annotations = page.getAnnotations();
                    boolean removed = false;
                    for (PDAnnotation annotation : annotations) {
                        if (annotation.getCOSObject().equals(widget.getCOSObject()))
                        {
                            removed = annotations.remove(annotation);
                            break;
                        }
                    }
                    if (!removed)
                        System.out.println("Inconsistent annotation definition: Page annotations do not include the target widget.");
                } else {
                    System.out.println("Widget annotation does not have an associated page; cannot remove widget.");
                    // TODO: In this case iterate all pages and try to find and remove widget in all of them
                }
            }
        } else if (targetField instanceof PDNonTerminalField) {
            List<PDField> childFields = ((PDNonTerminalField)targetField).getChildren();
            for (PDField field : childFields)
                removeWidgets(field);
        } else {
            System.out.println("Target field is neither terminal nor non-terminal; cannot remove widgets.");
        }
    }
}