/*
 * 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: PSDocumentHandler.java 1809628 2017-09-25 13:42:23Z ssteiner $ */

package org.apache.fop.render.ps;

import java.awt.Dimension;
import java.awt.geom.Dimension2D;
import java.awt.geom.Rectangle2D;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.transform.Source;

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

import org.apache.xmlgraphics.io.TempResourceURIGenerator;
import org.apache.xmlgraphics.java2d.Dimension2DDouble;
import org.apache.xmlgraphics.ps.DSCConstants;
import org.apache.xmlgraphics.ps.PSDictionary;
import org.apache.xmlgraphics.ps.PSDictionaryFormatException;
import org.apache.xmlgraphics.ps.PSGenerator;
import org.apache.xmlgraphics.ps.PSPageDeviceDictionary;
import org.apache.xmlgraphics.ps.PSProcSets;
import org.apache.xmlgraphics.ps.PSResource;
import org.apache.xmlgraphics.ps.dsc.DSCException;
import org.apache.xmlgraphics.ps.dsc.ResourceTracker;
import org.apache.xmlgraphics.ps.dsc.events.DSCCommentBoundingBox;
import org.apache.xmlgraphics.ps.dsc.events.DSCCommentHiResBoundingBox;

import org.apache.fop.apps.MimeConstants;
import org.apache.fop.render.intermediate.AbstractBinaryWritingIFDocumentHandler;
import org.apache.fop.render.intermediate.IFContext;
import org.apache.fop.render.intermediate.IFDocumentHandlerConfigurator;
import org.apache.fop.render.intermediate.IFException;
import org.apache.fop.render.intermediate.IFPainter;
import org.apache.fop.render.ps.PSRendererConfig.PSRendererConfigParser;
import org.apache.fop.render.ps.extensions.PSCommentAfter;
import org.apache.fop.render.ps.extensions.PSCommentBefore;
import org.apache.fop.render.ps.extensions.PSPageTrailerCodeBefore;
import org.apache.fop.render.ps.extensions.PSSetPageDevice;
import org.apache.fop.render.ps.extensions.PSSetupCode;

/**
 * {@link org.apache.fop.render.intermediate.IFDocumentHandler} implementation
 * that produces PostScript.
 */
public class PSDocumentHandler extends AbstractBinaryWritingIFDocumentHandler {

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

    /**
     * Utility class which enables all sorts of features that are not directly connected to the
     * normal rendering process.
     */
    private PSRenderingUtil psUtil;

    /** The PostScript generator used to output the PostScript */
    PSGenerator gen;

    /** the temporary file in case of two-pass processing */
    private URI tempURI;
    private static final TempResourceURIGenerator TEMP_URI_GENERATOR
            = new TempResourceURIGenerator("ps-optimize");

    private int currentPageNumber;
    private PageDefinition currentPageDefinition;

    /** Is used to determine the document's bounding box */
    private Rectangle2D documentBoundingBox;

    /** Used to temporarily store PSSetupCode instance until they can be written. */
    private List setupCodeList;

    /** This is a cache of PSResource instances of all fonts defined */
    private FontResourceCache fontResources;
    /** This is a map of PSResource instances of all forms (key: uri) */
    private Map formResources;

    /** encapsulation of dictionary used in setpagedevice instruction **/
    private PSPageDeviceDictionary pageDeviceDictionary;

    /** This is a collection holding all document header comments */
    private Collection[] comments = new Collection[4];
    private static final int COMMENT_DOCUMENT_HEADER = 0;
    private static final int COMMENT_DOCUMENT_TRAILER = 1;
    private static final int COMMENT_PAGE_TRAILER = 2;
    private static final int PAGE_TRAILER_CODE_BEFORE = 3;

    private PSEventProducer eventProducer;

    /**
     * Default constructor.
     */
    public PSDocumentHandler(IFContext context) {
        super(context);
        this.psUtil = new PSRenderingUtil(context.getUserAgent());
    }

    /** {@inheritDoc} */
    public boolean supportsPagesOutOfOrder() {
        return false;
    }

    /** {@inheritDoc} */
    public String getMimeType() {
        return MimeConstants.MIME_POSTSCRIPT;
    }

    PSGenerator getGenerator() {
        return gen;
    }

    /** {@inheritDoc} */
    public IFDocumentHandlerConfigurator getConfigurator() {
        return new PSRendererConfigurator(getUserAgent(), new PSRendererConfigParser());
    }

    public PSRenderingUtil getPSUtil() {
        return this.psUtil;
    }

    /** {@inheritDoc} */
    public void startDocument() throws IFException {
        super.startDocument();
        this.fontResources = new FontResourceCache(getFontInfo());
        try {
            final OutputStream out;
            if (psUtil.isOptimizeResources()) {
                tempURI = TEMP_URI_GENERATOR.generate();
                out = new BufferedOutputStream(getUserAgent().getResourceResolver().getOutputStream(tempURI));
            } else {
                out = this.outputStream;
            }

            //Setup for PostScript generation
            this.gen = new FOPPSGeneratorImpl(out);
            this.gen.setPSLevel(psUtil.getLanguageLevel());
            this.gen.setAcrobatDownsample(psUtil.isAcrobatDownsample());
            this.currentPageNumber = 0;
            this.documentBoundingBox = new Rectangle2D.Double();

            //Initial default page device dictionary settings
            this.pageDeviceDictionary = new PSPageDeviceDictionary();
            pageDeviceDictionary.setFlushOnRetrieval(!psUtil.isDSCComplianceEnabled());
            pageDeviceDictionary.put("/ImagingBBox", "null");
        } catch (IOException e) {
            throw new IFException("I/O error in startDocument()", e);
        }
    }

    public interface FOPPSGenerator {
        PSDocumentHandler getHandler();
        BufferedOutputStream getTempStream(URI uri) throws IOException;
        Map<Integer, URI> getImages();
    }

    public class FOPPSGeneratorImpl extends PSGenerator implements FOPPSGenerator {
        private Map<Integer, URI> images = new HashMap<Integer, URI>();
        public FOPPSGeneratorImpl(OutputStream out) {
            super(out);
        }

        /** Need to subclass PSGenerator to have better URI resolution */
        @Override
        public Source resolveURI(String uri) {
            return getUserAgent().resolveURI(uri);
        }

        public PSDocumentHandler getHandler() {
            return PSDocumentHandler.this;
        }

        public BufferedOutputStream getTempStream(URI uri) throws IOException {
            return new BufferedOutputStream(getUserAgent().getResourceResolver().getOutputStream(uri));
        }

        public Map<Integer, URI> getImages() {
            return images;
        }
    }

    private void writeHeader() throws IOException {
        //PostScript Header
        gen.writeln(DSCConstants.PS_ADOBE_30);
        gen.writeDSCComment(DSCConstants.CREATOR, new String[] {getUserAgent().getProducer()});
        gen.writeDSCComment(DSCConstants.CREATION_DATE, new Object[] {new java.util.Date()});
        gen.writeDSCComment(DSCConstants.LANGUAGE_LEVEL, gen.getPSLevel());
        gen.writeDSCComment(DSCConstants.PAGES, new Object[] {DSCConstants.ATEND});
        gen.writeDSCComment(DSCConstants.BBOX, DSCConstants.ATEND);
        gen.writeDSCComment(DSCConstants.HIRES_BBOX, DSCConstants.ATEND);
        gen.writeDSCComment(DSCConstants.DOCUMENT_SUPPLIED_RESOURCES,
                new Object[] {DSCConstants.ATEND});
        writeExtensions(COMMENT_DOCUMENT_HEADER);
        gen.writeDSCComment(DSCConstants.END_COMMENTS);

        //Defaults
        gen.writeDSCComment(DSCConstants.BEGIN_DEFAULTS);
        gen.writeDSCComment(DSCConstants.END_DEFAULTS);

        //Prolog and Setup written right before the first page-sequence, see startPageSequence()
        //Do this only once, as soon as we have all the content for the Setup section!
        //Prolog
        gen.writeDSCComment(DSCConstants.BEGIN_PROLOG);
        PSProcSets.writeStdProcSet(gen);
        PSProcSets.writeEPSProcSet(gen);
        FOPProcSet.INSTANCE.writeTo(gen);
        gen.writeDSCComment(DSCConstants.END_PROLOG);

        //Setup
        gen.writeDSCComment(DSCConstants.BEGIN_SETUP);
        PSRenderingUtil.writeSetupCodeList(gen, setupCodeList, "SetupCode");
        if (!psUtil.isOptimizeResources()) {
            this.fontResources.addAll(PSFontUtils.writeFontDict(gen, fontInfo, eventProducer));
        } else {
            gen.commentln("%FOPFontSetup"); //Place-holder, will be replaced in the second pass
        }
        gen.writeDSCComment(DSCConstants.END_SETUP);
    }

    /** {@inheritDoc} */
    public void endDocumentHeader() throws IFException {
        try {
            writeHeader();
        } catch (IOException ioe) {
            throw new IFException("I/O error writing the PostScript header", ioe);
        }
    }

    /** {@inheritDoc} */
    public void endDocument() throws IFException {
        try {
            //Write trailer
            gen.writeDSCComment(DSCConstants.TRAILER);
            writeExtensions(COMMENT_DOCUMENT_TRAILER);
            gen.writeDSCComment(DSCConstants.PAGES, this.currentPageNumber);
            new DSCCommentBoundingBox(this.documentBoundingBox).generate(gen);
            new DSCCommentHiResBoundingBox(this.documentBoundingBox).generate(gen);
            gen.getResourceTracker().writeResources(false, gen);
            gen.writeDSCComment(DSCConstants.EOF);
            gen.flush();
            log.debug("Rendering to PostScript complete.");
            if (psUtil.isOptimizeResources()) {
                IOUtils.closeQuietly(gen.getOutputStream());
                rewritePostScriptFile();
            }
            if (pageDeviceDictionary != null) {
                pageDeviceDictionary.clear();
            }
        } catch (IOException ioe) {
            throw new IFException("I/O error in endDocument()", ioe);
        }
        super.endDocument();
    }

    /**
     * Used for two-pass production. This will rewrite the PostScript file from the temporary
     * file while adding all needed resources.
     * @throws IOException In case of an I/O error.
     */
    private void rewritePostScriptFile() throws IOException {
        log.debug("Processing PostScript resources...");
        long startTime = System.currentTimeMillis();
        ResourceTracker resTracker = gen.getResourceTracker();
        InputStream in = new BufferedInputStream(getUserAgent().getResourceResolver().getResource(tempURI));
        try {
            try {
                ResourceHandler handler = new ResourceHandler(getUserAgent(), eventProducer,
                        this.fontInfo, resTracker, this.formResources);
                handler.process(in, this.outputStream,
                        this.currentPageNumber, this.documentBoundingBox, psUtil);
                this.outputStream.flush();
            } catch (DSCException e) {
                throw new RuntimeException(e.getMessage());
            }
        } finally {
            IOUtils.closeQuietly(in);
        }
        if (log.isDebugEnabled()) {
            long duration = System.currentTimeMillis() - startTime;
            log.debug("Resource Processing complete in " + duration + " ms.");
        }
    }

    /** {@inheritDoc} */
    public void startPageSequence(String id) throws IFException {
        //nop
    }

    /** {@inheritDoc} */
    public void endPageSequence() throws IFException {
        //nop
    }

    /** {@inheritDoc} */
    public void startPage(int index, String name, String pageMasterName, Dimension size)
                throws IFException {
        try {
            /* if (this.currentPageNumber == 0) {
                //writeHeader();
            } */

            this.currentPageNumber++;

            gen.getResourceTracker().notifyStartNewPage();
            gen.getResourceTracker().notifyResourceUsageOnPage(PSProcSets.STD_PROCSET);
            gen.writeDSCComment(DSCConstants.PAGE, new Object[]
                    {name,
                            this.currentPageNumber});

            double pageWidth = size.width / 1000.0;
            double pageHeight = size.height / 1000.0;
            boolean rotate = false;
            List pageSizes = new java.util.ArrayList();
            if (this.psUtil.isAutoRotateLandscape() && (pageHeight < pageWidth)) {
                rotate = true;
                pageSizes.add(Math.round(pageHeight));
                pageSizes.add(Math.round(pageWidth));
            } else {
                pageSizes.add(Math.round(pageWidth));
                pageSizes.add(Math.round(pageHeight));
            }
            pageDeviceDictionary.put("/PageSize", pageSizes);
            this.currentPageDefinition = new PageDefinition(
                    new Dimension2DDouble(pageWidth, pageHeight), rotate);

            //TODO Handle extension attachments for the page!!!!!!!
            /*
            if (page.hasExtensionAttachments()) {
                for (Iterator iter = page.getExtensionAttachments().iterator();
                    iter.hasNext();) {
                    ExtensionAttachment attachment = (ExtensionAttachment) iter.next();
                    if (attachment instanceof PSSetPageDevice) {*/
                        /**
                         * Extract all PSSetPageDevice instances from the
                         * attachment list on the s-p-m and add all
                         * dictionary entries to our internal representation
                         * of the the page device dictionary.
                         *//*
                        PSSetPageDevice setPageDevice = (PSSetPageDevice)attachment;
                        String content = setPageDevice.getContent();
                        if (content != null) {
                            try {
                                pageDeviceDictionary.putAll(PSDictionary.valueOf(content));
                            } catch (PSDictionaryFormatException e) {
                                PSEventProducer eventProducer = PSEventProducer.Provider.get(
                                        getUserAgent().getEventBroadcaster());
                                eventProducer.postscriptDictionaryParseError(this, content, e);
                            }
                        }
                    }
                }
            }*/

            final Integer zero = 0;
            Rectangle2D pageBoundingBox = new Rectangle2D.Double();
            if (rotate) {
                pageBoundingBox.setRect(0, 0, pageHeight, pageWidth);
                gen.writeDSCComment(DSCConstants.PAGE_BBOX, new Object[] {
                        zero, zero, Math.round(pageHeight),
                        Math.round(pageWidth)});
                gen.writeDSCComment(DSCConstants.PAGE_HIRES_BBOX, new Object[] {
                        zero, zero, pageHeight,
                        pageWidth});
                gen.writeDSCComment(DSCConstants.PAGE_ORIENTATION, "Landscape");
            } else {
                pageBoundingBox.setRect(0, 0, pageWidth, pageHeight);
                gen.writeDSCComment(DSCConstants.PAGE_BBOX, new Object[] {
                        zero, zero, Math.round(pageWidth),
                        Math.round(pageHeight)});
                gen.writeDSCComment(DSCConstants.PAGE_HIRES_BBOX, new Object[] {
                        zero, zero, pageWidth,
                        pageHeight});
                if (psUtil.isAutoRotateLandscape()) {
                    gen.writeDSCComment(DSCConstants.PAGE_ORIENTATION,
                            "Portrait");
                }
            }
            this.documentBoundingBox.add(pageBoundingBox);
            gen.writeDSCComment(DSCConstants.PAGE_RESOURCES,
                    new Object[] {DSCConstants.ATEND});

            gen.commentln("%FOPSimplePageMaster: " + pageMasterName);
        } catch (IOException ioe) {
            throw new IFException("I/O error in startPage()", ioe);
        }
    }

    /** {@inheritDoc} */
    public void startPageHeader() throws IFException {
        super.startPageHeader();

        try {
            gen.writeDSCComment(DSCConstants.BEGIN_PAGE_SETUP);
        } catch (IOException ioe) {
            throw new IFException("I/O error in startPageHeader()", ioe);
        }
    }

    /** {@inheritDoc} */
    public void endPageHeader() throws IFException {
        try {
            // Write any unwritten changes to page device dictionary
            if (!pageDeviceDictionary.isEmpty()) {
                String content = pageDeviceDictionary.getContent();
                if (psUtil.isSafeSetPageDevice()) {
                    content += " SSPD";
                } else {
                    content += " setpagedevice";
                }
                PSRenderingUtil.writeEnclosedExtensionAttachment(gen, new PSSetPageDevice(content));
            }

            double pageHeight = this.currentPageDefinition.dimensions.getHeight();
            if (this.currentPageDefinition.rotate) {
                gen.writeln(gen.formatDouble(pageHeight) + " 0 translate");
                gen.writeln("90 rotate");
            }
            gen.concatMatrix(1, 0, 0, -1, 0, pageHeight);

            gen.writeDSCComment(DSCConstants.END_PAGE_SETUP);
        } catch (IOException ioe) {
            throw new IFException("I/O error in endPageHeader()", ioe);
        }

        super.endPageHeader();
    }

    private void writeExtensions(int which) throws IOException {
        Collection extensions = comments[which];
        if (extensions != null) {
            PSRenderingUtil.writeEnclosedExtensionAttachments(gen, extensions);
            extensions.clear();
        }
    }

    /** {@inheritDoc} */
    public IFPainter startPageContent() throws IFException {
        return new PSPainter(this);
    }

    /** {@inheritDoc} */
    public void endPageContent() throws IFException {
        try {
            gen.showPage();
        } catch (IOException ioe) {
            throw new IFException("I/O error in endPageContent()", ioe);
        }
    }

    /** {@inheritDoc} */
    public void startPageTrailer() throws IFException {
        try {
            writeExtensions(PAGE_TRAILER_CODE_BEFORE);
            super.startPageTrailer();
            gen.writeDSCComment(DSCConstants.PAGE_TRAILER);
        } catch (IOException ioe) {
            throw new IFException("I/O error in startPageTrailer()", ioe);
        }
    }

    /** {@inheritDoc} */
    public void endPageTrailer() throws IFException {
        try {
            writeExtensions(COMMENT_PAGE_TRAILER);
        } catch (IOException ioe) {
            throw new IFException("I/O error in endPageTrailer()", ioe);
        }
        super.endPageTrailer();
    }

    /** {@inheritDoc} */
    public void endPage() throws IFException {
        try {
            gen.getResourceTracker().writeResources(true, gen);
        } catch (IOException ioe) {
            throw new IFException("I/O error in endPage()", ioe);
        }

        this.currentPageDefinition = null;
    }

    private boolean inPage() {
        return this.currentPageDefinition != null;
    }

    /** {@inheritDoc} */
    public void handleExtensionObject(Object extension) throws IFException {
        try {
            if (extension instanceof PSSetupCode) {
                if (inPage()) {
                    PSRenderingUtil.writeEnclosedExtensionAttachment(gen, (PSSetupCode)extension);
                } else {
                    //A special collection for setup code as it's put in a different place
                    //than the "before comments".
                    if (setupCodeList == null) {
                        setupCodeList = new java.util.ArrayList();
                    }
                    if (!setupCodeList.contains(extension)) {
                        setupCodeList.add(extension);
                    }
                }
            } else if (extension instanceof PSSetPageDevice) {
                /**
                 * Extract all PSSetPageDevice instances from the
                 * attachment list on the s-p-m and add all dictionary
                 * entries to our internal representation of the the
                 * page device dictionary.
                 */
                PSSetPageDevice setPageDevice = (PSSetPageDevice)extension;
                String content = setPageDevice.getContent();
                if (content != null) {
                    try {
                        this.pageDeviceDictionary.putAll(PSDictionary.valueOf(content));
                    } catch (PSDictionaryFormatException e) {
                        PSEventProducer eventProducer = PSEventProducer.Provider.get(
                                getUserAgent().getEventBroadcaster());
                        eventProducer.postscriptDictionaryParseError(this, content, e);
                    }
                }
            } else if (extension instanceof PSCommentBefore) {
                if (inPage()) {
                    PSRenderingUtil.writeEnclosedExtensionAttachment(
                            gen, (PSCommentBefore)extension);
                } else {
                    if (comments[COMMENT_DOCUMENT_HEADER] == null) {
                        comments[COMMENT_DOCUMENT_HEADER] = new java.util.ArrayList();
                    }
                    comments[COMMENT_DOCUMENT_HEADER].add(extension);
                }
            } else if (extension instanceof PSCommentAfter) {
                int targetCollection = (inPage() ? COMMENT_PAGE_TRAILER : COMMENT_DOCUMENT_TRAILER);
                if (comments[targetCollection] == null) {
                    comments[targetCollection] = new java.util.ArrayList();
                }
                comments[targetCollection].add(extension);
            } else if (extension instanceof PSPageTrailerCodeBefore) {
                if (comments[PAGE_TRAILER_CODE_BEFORE] == null) {
                    comments[PAGE_TRAILER_CODE_BEFORE] = new ArrayList();
                }
                comments[PAGE_TRAILER_CODE_BEFORE].add(extension);
            }
        } catch (IOException ioe) {
            throw new IFException("I/O error in handleExtensionObject()", ioe);
        }
    }

    /**
     * Returns the PSResource for the given font key.
     * @param key the font key ("F*")
     * @return the matching PSResource
     */
    protected PSFontResource getPSResourceForFontKey(String key) {
        return this.fontResources.getFontResourceForFontKey(key);
    }

    /**
     * Returns a PSResource instance representing a image as a PostScript form.
     * @param uri the image URI
     * @return a PSResource instance
     */
    public PSResource getFormForImage(String uri) {
        if (uri == null || "".equals(uri)) {
            throw new IllegalArgumentException("uri must not be empty or null");
        }
        if (this.formResources == null) {
            this.formResources = new java.util.HashMap();
        }
        PSResource form = (PSResource)this.formResources.get(uri);
        if (form == null) {
            form = new PSImageFormResource(this.formResources.size() + 1, uri);
            this.formResources.put(uri, form);
        }
        return form;
    }

    private static final class PageDefinition {
        private Dimension2D dimensions;
        private boolean rotate;

        private PageDefinition(Dimension2D dimensions, boolean rotate) {
            this.dimensions = dimensions;
            this.rotate = rotate;
        }
    }

}
