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

package org.apache.fop.render.pcl;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.util.Map;

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.util.UnitConv;

import org.apache.fop.apps.FopFactoryConfig;
import org.apache.fop.apps.MimeConstants;
import org.apache.fop.fonts.FontInfo;
import org.apache.fop.fonts.Typeface;
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.java2d.Java2DPainter;
import org.apache.fop.render.java2d.Java2DUtil;
import org.apache.fop.render.pcl.PCLRendererConfig.PCLRendererConfigParser;
import org.apache.fop.render.pcl.extensions.PCLElementMapping;
import org.apache.fop.render.pcl.fonts.PCLSoftFontManager;

/**
 * {@link org.apache.fop.render.intermediate.IFDocumentHandler} implementation
 * that produces PCL 5.
 */
public class PCLDocumentHandler extends AbstractBinaryWritingIFDocumentHandler
            implements PCLConstants {

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

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

    /** Utility class for handling all sorts of peripheral tasks around PCL generation. */
    protected PCLRenderingUtil pclUtil;

    /** The PCL generator */
    private PCLGenerator gen;

    private PCLPageDefinition currentPageDefinition;

    /** contains the pageWith of the last printed page */
    private long pageWidth;
    /** contains the pageHeight of the last printed page */
    private long pageHeight;

    /** the current page image (only set when all-bitmap painting is activated) */
    private BufferedImage currentImage;


    /**
     * Default constructor.
     */
    public PCLDocumentHandler(IFContext context) {
        super(context);
        this.pclUtil = new PCLRenderingUtil(context.getUserAgent());
    }

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

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

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

    /** {@inheritDoc} */
    @Override
    public void setDefaultFontInfo(FontInfo fontInfo) {
        FontInfo fi = Java2DUtil.buildDefaultJava2DBasedFontInfo(fontInfo, getUserAgent());
        setFontInfo(fi);
    }

    PCLRenderingUtil getPCLUtil() {
        return this.pclUtil;
    }

    PCLGenerator getPCLGenerator() {
        return this.gen;
    }

    /** @return the target resolution */
    protected int getResolution() {
        int resolution = Math.round(getUserAgent().getTargetResolution());
        if (resolution <= 300) {
            return 300;
        } else {
            return 600;
        }
    }

    //----------------------------------------------------------------------------------------------

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

            this.gen = new PCLGenerator(out, getResolution());
            this.gen.setDitheringQuality(pclUtil.getDitheringQuality());

            if (!pclUtil.isPJLDisabled()) {
                gen.universalEndOfLanguage();
                gen.writeText("@PJL COMMENT Produced by " + getUserAgent().getProducer() + "\n");
                if (getUserAgent().getTitle() != null) {
                    gen.writeText("@PJL JOB NAME = \"" + getUserAgent().getTitle() + "\"\n");
                }
                gen.writeText("@PJL SET RESOLUTION = " + getResolution() + "\n");
                gen.writeText("@PJL ENTER LANGUAGE = PCL\n");
            }
            gen.resetPrinter();
            gen.setUnitOfMeasure(getResolution());
            gen.setRasterGraphicsResolution(getResolution());
        } catch (IOException e) {
            throw new IFException("I/O error in startDocument()", e);
        }
    }

    /** {@inheritDoc} */
    @Override
    public void endDocumentHeader() throws IFException {
    }

    /** {@inheritDoc} */
    @Override
    public void endDocument() throws IFException {
        try {
            gen.separateJobs();
            gen.resetPrinter();
            if (!pclUtil.isPJLDisabled()) {
                gen.universalEndOfLanguage();
            }

            if (pclUtil.isOptimizeResources()) {
                IOUtils.closeQuietly(gen.getOutputStream());
                rewritePCLFile();
            }
        } catch (IOException ioe) {
            throw new IFException("I/O error in endDocument()", ioe);
        }
        super.endDocument();
    }

    private void rewritePCLFile() throws IOException {
        InputStream in = new BufferedInputStream(getUserAgent().getResourceResolver().getResource(tempURI));
        long offset = 0;
        for (Map.Entry<PCLSoftFontManager, Map<Typeface, Long>> fontManagerMapEntry : gen.fontManagerMap.entrySet()) {
            PCLSoftFontManager softFontManager = fontManagerMapEntry.getKey();
            for (Map.Entry<Typeface, Long> fontEntry : fontManagerMapEntry.getValue().entrySet()) {
                ByteArrayOutputStream fontData = softFontManager.makeSoftFont(fontEntry.getKey(), null);
                long pos = fontEntry.getValue();
                copy(in, pos - offset);
                outputStream.write(fontData.toByteArray());
                offset = pos;
            }
        }
        copy(in, Long.MAX_VALUE);
        this.outputStream.flush();
        IOUtils.closeQuietly(in);
    }

    private void copy(InputStream is, long len) throws IOException {
        while (len > 0) {
            int bufsize = (int) Math.min(1024, len);
            byte[] buf = new byte[bufsize];
            if (is.read(buf) == -1) {
                return;
            }
            outputStream.write(buf);
            len -= bufsize;
        }
    }

    /** {@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 {
            //Paper source
            Object paperSource = getContext().getForeignAttribute(
                    PCLElementMapping.PCL_PAPER_SOURCE);
            if (paperSource != null) {
                gen.selectPaperSource(Integer.parseInt(paperSource.toString()));
            }

            //Output bin
            Object outputBin = getContext().getForeignAttribute(
                    PCLElementMapping.PCL_OUTPUT_BIN);
            if (outputBin != null) {
                gen.selectOutputBin(Integer.parseInt(outputBin.toString()));
            }

            // Is Page duplex?
            Object pageDuplex = getContext().getForeignAttribute(
                    PCLElementMapping.PCL_DUPLEX_MODE);
            if (pageDuplex != null) {
                gen.selectDuplexMode(Integer.parseInt(pageDuplex.toString()));
            }

            //Page size
            final long pagewidth = size.width;
            final long pageheight = size.height;
            selectPageFormat(pagewidth, pageheight);
        } catch (IOException ioe) {
            throw new IFException("I/O error in startPage()", ioe);
        }
    }

    /** {@inheritDoc} */
    public IFPainter startPageContent() throws IFException {
        if (pclUtil.getRenderingMode() == PCLRenderingMode.BITMAP) {
            return createAllBitmapPainter();
        } else {
            return new PCLPainter(this, this.currentPageDefinition);
        }
    }

    private IFPainter createAllBitmapPainter() {
        double scale = gen.getMaximumBitmapResolution()
                / FopFactoryConfig.DEFAULT_TARGET_RESOLUTION;
        Rectangle printArea = this.currentPageDefinition.getLogicalPageRect();
        int bitmapWidth = (int)Math.ceil(
                UnitConv.mpt2px(printArea.width, gen.getMaximumBitmapResolution()));
        int bitmapHeight = (int)Math.ceil(
                UnitConv.mpt2px(printArea.height, gen.getMaximumBitmapResolution()));
        this.currentImage = createBufferedImage(bitmapWidth, bitmapHeight);
        Graphics2D graphics2D = this.currentImage.createGraphics();

        if (!PCLGenerator.isJAIAvailable()) {
            RenderingHints hints = new RenderingHints(null);
            //These hints don't seem to make a difference :-( Not seeing any dithering on Sun Java.
            hints.put(RenderingHints.KEY_DITHERING,
                    RenderingHints.VALUE_DITHER_ENABLE);
            graphics2D.addRenderingHints(hints);
        }

        //Ensure white page background
        graphics2D.setBackground(Color.WHITE);
        graphics2D.clearRect(0, 0, bitmapWidth, bitmapHeight);

        graphics2D.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS,
                RenderingHints.VALUE_FRACTIONALMETRICS_ON);
        graphics2D.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
                RenderingHints.VALUE_STROKE_PURE);
        graphics2D.scale(scale / 1000f, scale / 1000f);
        graphics2D.translate(-printArea.x, -printArea.y);

        return new Java2DPainter(graphics2D, getContext(), getFontInfo(), this);
    }

    private BufferedImage createBufferedImage(int bitmapWidth, int bitmapHeight) {
        int bitmapType;
        if (PCLGenerator.isJAIAvailable()) {
            //TYPE_BYTE_GRAY was used to work around the lack of dithering when using
            //TYPE_BYTE_BINARY. Adding RenderingHints didn't help.
            bitmapType = BufferedImage.TYPE_BYTE_GRAY;
            //bitmapType = BufferedImage.TYPE_INT_RGB; //Use to enable Batik gradients
        } else {
            bitmapType = BufferedImage.TYPE_BYTE_BINARY;
        }
        return new BufferedImage(
                bitmapWidth, bitmapHeight, bitmapType);
    }

    /** {@inheritDoc} */
    public void endPageContent() throws IFException {
        if (this.currentImage != null) {
            try {
                Rectangle printArea = this.currentPageDefinition.getLogicalPageRect();
                gen.setCursorPos(0, 0);
                gen.paintBitmap(this.currentImage, printArea.getSize(), true, pclUtil);
            } catch (IOException ioe) {
                throw new IFException("I/O error while encoding page image", ioe);
            } finally {
                this.currentImage = null;
            }
        }
    }

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

    /** {@inheritDoc} */
    public void handleExtensionObject(Object extension) throws IFException {
        if (false) {
            //TODO Handle extensions
        } else {
            log.debug("Don't know how to handle extension object. Ignoring: "
                    + extension + " (" + extension.getClass().getName() + ")");
        }
    }

    private void selectPageFormat(long pagewidth, long pageheight) throws IOException {
        //Only set the page format if it changes (otherwise duplex printing won't work)
        if ((pagewidth != this.pageWidth) || (pageheight != this.pageHeight))  {
            this.pageWidth = pagewidth;
            this.pageHeight = pageheight;

            this.currentPageDefinition = PCLPageDefinition.getPageDefinition(
                    pagewidth, pageheight, 1000);

            if (this.currentPageDefinition == null) {
                this.currentPageDefinition = PCLPageDefinition.getDefaultPageDefinition();
                log.warn("Paper type could not be determined. Falling back to: "
                        + this.currentPageDefinition.getName());
            }
            if (log.isDebugEnabled()) {
                log.debug("page size: " + currentPageDefinition.getPhysicalPageSize());
                log.debug("logical page: " + currentPageDefinition.getLogicalPageRect());
            }

            if (this.currentPageDefinition.isLandscapeFormat()) {
                gen.writeCommand("&l1O"); //Landscape Orientation
            } else {
                gen.writeCommand("&l0O"); //Portrait Orientation
            }
            gen.selectPageSize(this.currentPageDefinition.getSelector());

            gen.clearHorizontalMargins();
            gen.setTopMargin(0);
        }
    }

}
