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

/* $Id: PDFRenderingUtil.java 1305467 2012-03-26 17:39:20Z vhennebert $ */

package org.apache.fop.render.pdf;

import java.awt.color.ICC_Profile;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.Map;

import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;

import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.apache.xmlgraphics.image.loader.util.ImageUtil;
import org.apache.xmlgraphics.java2d.color.profile.ColorProfileUtil;
import org.apache.xmlgraphics.xmp.Metadata;
import org.apache.xmlgraphics.xmp.schemas.XMPBasicAdapter;
import org.apache.xmlgraphics.xmp.schemas.XMPBasicSchema;

import org.apache.fop.accessibility.Accessibility;
import org.apache.fop.apps.FOUserAgent;
import org.apache.fop.fo.extensions.xmp.XMPMetadata;
import org.apache.fop.pdf.PDFAMode;
import org.apache.fop.pdf.PDFArray;
import org.apache.fop.pdf.PDFConformanceException;
import org.apache.fop.pdf.PDFDictionary;
import org.apache.fop.pdf.PDFDocument;
import org.apache.fop.pdf.PDFEmbeddedFile;
import org.apache.fop.pdf.PDFEmbeddedFiles;
import org.apache.fop.pdf.PDFEncryptionManager;
import org.apache.fop.pdf.PDFEncryptionParams;
import org.apache.fop.pdf.PDFFileSpec;
import org.apache.fop.pdf.PDFICCBasedColorSpace;
import org.apache.fop.pdf.PDFICCStream;
import org.apache.fop.pdf.PDFInfo;
import org.apache.fop.pdf.PDFMetadata;
import org.apache.fop.pdf.PDFNames;
import org.apache.fop.pdf.PDFNumsArray;
import org.apache.fop.pdf.PDFOutputIntent;
import org.apache.fop.pdf.PDFPageLabels;
import org.apache.fop.pdf.PDFReference;
import org.apache.fop.pdf.PDFText;
import org.apache.fop.pdf.PDFXMode;
import org.apache.fop.pdf.Version;
import org.apache.fop.pdf.VersionController;
import org.apache.fop.render.pdf.extensions.PDFEmbeddedFileExtensionAttachment;

/**
 * Utility class which enables all sorts of features that are not directly connected to the
 * normal rendering process.
 */
class PDFRenderingUtil implements PDFConfigurationConstants {

    /** logging instance */
    private static Log log = LogFactory.getLog(PDFRenderingUtil.class);

    private FOUserAgent userAgent;

    /** the PDF Document being created */
    protected PDFDocument pdfDoc;

    /** the PDF/A mode (Default: disabled) */
    protected PDFAMode pdfAMode = PDFAMode.DISABLED;

    /** the PDF/X mode (Default: disabled) */
    protected PDFXMode pdfXMode = PDFXMode.DISABLED;

    /** the (optional) encryption parameters */
    protected PDFEncryptionParams encryptionParams;

    /** Registry of PDF filters */
    protected Map filterMap;

    /** the ICC stream used as output profile by this document for PDF/A and PDF/X functionality. */
    protected PDFICCStream outputProfile;
    /** the default sRGB color space. */
    protected PDFICCBasedColorSpace sRGBColorSpace;
    /** controls whether the sRGB color space should be installed */
    protected boolean disableSRGBColorSpace = false;

    /** Optional URI to an output profile to be used. */
    protected String outputProfileURI;

    protected Version maxPDFVersion;


    PDFRenderingUtil(FOUserAgent userAgent) {
        this.userAgent = userAgent;
        initialize();
    }

    private static boolean booleanValueOf(Object obj) {
        if (obj instanceof Boolean) {
            return ((Boolean)obj).booleanValue();
        } else if (obj instanceof String) {
            return Boolean.valueOf((String)obj).booleanValue();
        } else {
            throw new IllegalArgumentException("Boolean or \"true\" or \"false\" expected.");
        }
    }

    private void initialize() {
        PDFEncryptionParams params
                = (PDFEncryptionParams)userAgent.getRendererOptions().get(ENCRYPTION_PARAMS);
        if (params != null) {
            this.encryptionParams = params; //overwrite if available
        }
        String userPassword = (String)userAgent.getRendererOptions().get(USER_PASSWORD);
        if (userPassword != null) {
            getEncryptionParams().setUserPassword(userPassword);
        }
        String ownerPassword = (String)userAgent.getRendererOptions().get(OWNER_PASSWORD);
        if (ownerPassword != null) {
            getEncryptionParams().setOwnerPassword(ownerPassword);
        }
        Object noPrint = userAgent.getRendererOptions().get(NO_PRINT);
        if (noPrint != null) {
            getEncryptionParams().setAllowPrint(!booleanValueOf(noPrint));
        }
        Object noCopyContent = userAgent.getRendererOptions().get(NO_COPY_CONTENT);
        if (noCopyContent != null) {
            getEncryptionParams().setAllowCopyContent(!booleanValueOf(noCopyContent));
        }
        Object noEditContent = userAgent.getRendererOptions().get(NO_EDIT_CONTENT);
        if (noEditContent != null) {
            getEncryptionParams().setAllowEditContent(!booleanValueOf(noEditContent));
        }
        Object noAnnotations = userAgent.getRendererOptions().get(NO_ANNOTATIONS);
        if (noAnnotations != null) {
            getEncryptionParams().setAllowEditAnnotations(!booleanValueOf(noAnnotations));
        }
        Object noFillInForms = userAgent.getRendererOptions().get(NO_FILLINFORMS);
        if (noFillInForms != null) {
            getEncryptionParams().setAllowFillInForms(!booleanValueOf(noFillInForms));
        }
        Object noAccessContent = userAgent.getRendererOptions().get(NO_ACCESSCONTENT);
        if (noAccessContent != null) {
            getEncryptionParams().setAllowAccessContent(!booleanValueOf(noAccessContent));
        }
        Object noAssembleDoc = userAgent.getRendererOptions().get(NO_ASSEMBLEDOC);
        if (noAssembleDoc != null) {
            getEncryptionParams().setAllowAssembleDocument(!booleanValueOf(noAssembleDoc));
        }
        Object noPrintHQ = userAgent.getRendererOptions().get(NO_PRINTHQ);
        if (noPrintHQ != null) {
            getEncryptionParams().setAllowPrintHq(!booleanValueOf(noPrintHQ));
        }
        String s = (String)userAgent.getRendererOptions().get(PDF_A_MODE);
        if (s != null) {
            this.pdfAMode = PDFAMode.valueOf(s);
        }
        if (this.pdfAMode.isPDFA1LevelA()) {
            //Enable accessibility if PDF/A-1a is enabled because it requires tagged PDF.
            userAgent.getRendererOptions().put(Accessibility.ACCESSIBILITY, Boolean.TRUE);
        }
        s = (String)userAgent.getRendererOptions().get(PDF_X_MODE);
        if (s != null) {
            this.pdfXMode = PDFXMode.valueOf(s);
        }
        s = (String)userAgent.getRendererOptions().get(KEY_OUTPUT_PROFILE);
        if (s != null) {
            this.outputProfileURI = s;
        }
        Object disableSRGBColorSpace = userAgent.getRendererOptions().get(
                KEY_DISABLE_SRGB_COLORSPACE);
        if (disableSRGBColorSpace != null) {
            this.disableSRGBColorSpace = booleanValueOf(disableSRGBColorSpace);
        }
    }

    public FOUserAgent getUserAgent() {
        return this.userAgent;
    }

    /**
     * Sets the PDF/A mode for the PDF renderer.
     * @param mode the PDF/A mode
     */
    public void setAMode(PDFAMode mode) {
        this.pdfAMode = mode;
    }

    /**
     * Sets the PDF/X mode for the PDF renderer.
     * @param mode the PDF/X mode
     */
    public void setXMode(PDFXMode mode) {
        this.pdfXMode = mode;
    }

    /**
     * Sets the output color profile for the PDF renderer.
     * @param outputProfileURI the URI to the output color profile
     */
    public void setOutputProfileURI(String outputProfileURI) {
        this.outputProfileURI = outputProfileURI;
    }

    /**
     * Enables or disables the default sRGB color space needed for the PDF document to preserve
     * the sRGB colors used in XSL-FO.
     * @param disable true to disable, false to enable
     */
    public void setDisableSRGBColorSpace(boolean disable) {
        this.disableSRGBColorSpace = disable;
    }

    /**
     * Sets the filter map to be used by the PDF renderer.
     * @param filterMap the filter map
     */
    public void setFilterMap(Map filterMap) {
        this.filterMap = filterMap;
    }

    /**
     * Gets the encryption parameters used by the PDF renderer.
     * @return encryptionParams the encryption parameters
     */
    PDFEncryptionParams getEncryptionParams() {
        if (this.encryptionParams == null) {
            this.encryptionParams = new PDFEncryptionParams();
        }
        return this.encryptionParams;
    }

    private void updateInfo() {
        PDFInfo info = pdfDoc.getInfo();
        info.setCreator(userAgent.getCreator());
        info.setCreationDate(userAgent.getCreationDate());
        info.setAuthor(userAgent.getAuthor());
        info.setTitle(userAgent.getTitle());
        info.setSubject(userAgent.getSubject());
        info.setKeywords(userAgent.getKeywords());
    }

    private void updatePDFProfiles() {
        pdfDoc.getProfile().setPDFAMode(this.pdfAMode);
        pdfDoc.getProfile().setPDFXMode(this.pdfXMode);
    }

    private void addsRGBColorSpace() throws IOException {
        if (disableSRGBColorSpace) {
            if (this.pdfAMode != PDFAMode.DISABLED
                    || this.pdfXMode != PDFXMode.DISABLED
                    || this.outputProfileURI != null) {
                throw new IllegalStateException("It is not possible to disable the sRGB color"
                        + " space if PDF/A or PDF/X functionality is enabled or an"
                        + " output profile is set!");
            }
        } else {
            if (this.sRGBColorSpace != null) {
                return;
            }
            //Map sRGB as default RGB profile for DeviceRGB
            this.sRGBColorSpace = PDFICCBasedColorSpace.setupsRGBAsDefaultRGBColorSpace(pdfDoc);
        }
    }

    private void addDefaultOutputProfile() throws IOException {
        if (this.outputProfile != null) {
            return;
        }
        ICC_Profile profile;
        InputStream in = null;
        if (this.outputProfileURI != null) {
            this.outputProfile = pdfDoc.getFactory().makePDFICCStream();
            Source src = getUserAgent().resolveURI(this.outputProfileURI);
            if (src == null) {
                throw new IOException("Output profile not found: " + this.outputProfileURI);
            }
            if (src instanceof StreamSource) {
                in = ((StreamSource)src).getInputStream();
            } else {
                in = new URL(src.getSystemId()).openStream();
            }
            try {
                profile = ColorProfileUtil.getICC_Profile(in);
            } finally {
                IOUtils.closeQuietly(in);
            }
            this.outputProfile.setColorSpace(profile, null);
        } else {
            //Fall back to sRGB profile
            outputProfile = sRGBColorSpace.getICCStream();
        }
    }

    /**
     * Adds an OutputIntent to the PDF as mandated by PDF/A-1 when uncalibrated color spaces
     * are used (which is true if we use DeviceRGB to represent sRGB colors).
     * @throws IOException in case of an I/O problem
     */
    private void addPDFA1OutputIntent() throws IOException {
        addDefaultOutputProfile();

        String desc = ColorProfileUtil.getICCProfileDescription(this.outputProfile.getICCProfile());
        PDFOutputIntent outputIntent = pdfDoc.getFactory().makeOutputIntent();
        outputIntent.setSubtype(PDFOutputIntent.GTS_PDFA1);
        outputIntent.setDestOutputProfile(this.outputProfile);
        outputIntent.setOutputConditionIdentifier(desc);
        outputIntent.setInfo(outputIntent.getOutputConditionIdentifier());
        pdfDoc.getRoot().addOutputIntent(outputIntent);
    }

    /**
     * Adds an OutputIntent to the PDF as mandated by PDF/X when uncalibrated color spaces
     * are used (which is true if we use DeviceRGB to represent sRGB colors).
     * @throws IOException in case of an I/O problem
     */
    private void addPDFXOutputIntent() throws IOException {
        addDefaultOutputProfile();

        String desc = ColorProfileUtil.getICCProfileDescription(this.outputProfile.getICCProfile());
        int deviceClass = this.outputProfile.getICCProfile().getProfileClass();
        if (deviceClass != ICC_Profile.CLASS_OUTPUT) {
            throw new PDFConformanceException(pdfDoc.getProfile().getPDFXMode() + " requires that"
                    + " the DestOutputProfile be an Output Device Profile. "
                    + desc + " does not match that requirement.");
        }
        PDFOutputIntent outputIntent = pdfDoc.getFactory().makeOutputIntent();
        outputIntent.setSubtype(PDFOutputIntent.GTS_PDFX);
        outputIntent.setDestOutputProfile(this.outputProfile);
        outputIntent.setOutputConditionIdentifier(desc);
        outputIntent.setInfo(outputIntent.getOutputConditionIdentifier());
        pdfDoc.getRoot().addOutputIntent(outputIntent);
    }

    public void renderXMPMetadata(XMPMetadata metadata) {
        Metadata docXMP = metadata.getMetadata();
        Metadata fopXMP = PDFMetadata.createXMPFromPDFDocument(pdfDoc);
        //Merge FOP's own metadata into the one from the XSL-FO document
        fopXMP.mergeInto(docXMP);
        XMPBasicAdapter xmpBasic = XMPBasicSchema.getAdapter(docXMP);
        //Metadata was changed so update metadata date
        xmpBasic.setMetadataDate(new java.util.Date());
        PDFMetadata.updateInfoFromMetadata(docXMP, pdfDoc.getInfo());

        PDFMetadata pdfMetadata = pdfDoc.getFactory().makeMetadata(
                docXMP, metadata.isReadOnly());
        pdfDoc.getRoot().setMetadata(pdfMetadata);
    }

    public void generateDefaultXMPMetadata() {
        if (pdfDoc.getRoot().getMetadata() == null) {
            //If at this time no XMP metadata for the overall document has been set, create it
            //from the PDFInfo object.
            Metadata xmp = PDFMetadata.createXMPFromPDFDocument(pdfDoc);
            PDFMetadata pdfMetadata = pdfDoc.getFactory().makeMetadata(
                    xmp, true);
            pdfDoc.getRoot().setMetadata(pdfMetadata);
        }
    }

    public PDFDocument setupPDFDocument(OutputStream out) throws IOException {
        if (this.pdfDoc != null) {
            throw new IllegalStateException("PDFDocument already set up");
        }

        String producer = userAgent.getProducer() != null ? userAgent.getProducer() : "";

        if (maxPDFVersion == null) {
            this.pdfDoc = new PDFDocument(producer);
        } else {
            VersionController controller
                    = VersionController.getFixedVersionController(maxPDFVersion);
            this.pdfDoc = new PDFDocument(producer, controller);
        }
        updateInfo();
        updatePDFProfiles();
        pdfDoc.setFilterMap(filterMap);
        pdfDoc.outputHeader(out);

        //Setup encryption if necessary
        PDFEncryptionManager.setupPDFEncryption(encryptionParams, pdfDoc);

        addsRGBColorSpace();
        if (this.outputProfileURI != null) {
            addDefaultOutputProfile();
        }
        if (pdfXMode != PDFXMode.DISABLED) {
            log.debug(pdfXMode + " is active.");
            log.warn("Note: " + pdfXMode
                    + " support is work-in-progress and not fully implemented, yet!");
            addPDFXOutputIntent();
        }
        if (pdfAMode.isPDFA1LevelB()) {
            log.debug("PDF/A is active. Conformance Level: " + pdfAMode);
            addPDFA1OutputIntent();
        }

        this.pdfDoc.enableAccessibility(userAgent.isAccessibilityEnabled());

        return this.pdfDoc;
    }

    /**
     * Generates a page label in the PDF document.
     * @param pageIndex the index of the page
     * @param pageNumber the formatted page number
     */
    public void generatePageLabel(int pageIndex, String pageNumber) {
        //Produce page labels
        PDFPageLabels pageLabels = this.pdfDoc.getRoot().getPageLabels();
        if (pageLabels == null) {
            //Set up PageLabels
            pageLabels = this.pdfDoc.getFactory().makePageLabels();
            this.pdfDoc.getRoot().setPageLabels(pageLabels);
        }
        PDFNumsArray nums = pageLabels.getNums();
        PDFDictionary dict = new PDFDictionary(nums);
        dict.put("P", pageNumber);
        //TODO If the sequence of generated page numbers were inspected, this could be
        //expressed in a more space-efficient way
        nums.put(pageIndex, dict);
    }

    /**
     * Adds an embedded file to the PDF file.
     * @param embeddedFile the object representing the embedded file to be added
     * @throws IOException if an I/O error occurs
     */
    public void addEmbeddedFile(PDFEmbeddedFileExtensionAttachment embeddedFile)
            throws IOException {
        this.pdfDoc.getProfile().verifyEmbeddedFilesAllowed();
        PDFNames names = this.pdfDoc.getRoot().getNames();
        if (names == null) {
            //Add Names if not already present
            names = this.pdfDoc.getFactory().makeNames();
            this.pdfDoc.getRoot().setNames(names);
        }

        //Create embedded file
        PDFEmbeddedFile file = new PDFEmbeddedFile();
        this.pdfDoc.registerObject(file);
        Source src = getUserAgent().resolveURI(embeddedFile.getSrc());
        InputStream in = ImageUtil.getInputStream(src);
        if (in == null) {
            throw new FileNotFoundException(embeddedFile.getSrc());
        }
        try {
            OutputStream out = file.getBufferOutputStream();
            IOUtils.copyLarge(in, out);
        } finally {
            IOUtils.closeQuietly(in);
        }
        PDFDictionary dict = new PDFDictionary();
        dict.put("F", file);
        String filename = PDFText.toPDFString(embeddedFile.getFilename(), '_');
        PDFFileSpec fileSpec = new PDFFileSpec(filename);
        fileSpec.setEmbeddedFile(dict);
        if (embeddedFile.getDesc() != null) {
            fileSpec.setDescription(embeddedFile.getDesc());
        }
        this.pdfDoc.registerObject(fileSpec);

        //Make sure there is an EmbeddedFiles in the Names dictionary
        PDFEmbeddedFiles embeddedFiles = names.getEmbeddedFiles();
        if (embeddedFiles == null) {
            embeddedFiles = new PDFEmbeddedFiles();
            this.pdfDoc.assignObjectNumber(embeddedFiles);
            this.pdfDoc.addTrailerObject(embeddedFiles);
            names.setEmbeddedFiles(embeddedFiles);
        }

        //Add to EmbeddedFiles in the Names dictionary
        PDFArray nameArray = embeddedFiles.getNames();
        if (nameArray == null) {
            nameArray = new PDFArray();
            embeddedFiles.setNames(nameArray);
        }
        String name = PDFText.toPDFString(filename);
        nameArray.add(name);
        nameArray.add(new PDFReference(fileSpec));
    }

    /**
     * Sets the PDF version of the output document. See {@link Version} for the format of
     * <code>version</code>.
     * @param version the PDF version
     * @throws IllegalArgumentException if the format of version doesn't conform to that specified
     * by {@link Version}
     */
    public void setPDFVersion(String version) {
        maxPDFVersion = Version.getValueOf(version);
    }
}
