/*
 * 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: DataStream.java 1761021 2016-09-16 11:40:57Z ssteiner $ */

package org.apache.fop.afp;

import java.awt.Color;
import java.awt.Point;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.Map;

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

import org.apache.fop.afp.fonts.AFPFont;
import org.apache.fop.afp.fonts.AFPFontAttributes;
import org.apache.fop.afp.fonts.CharacterSet;
import org.apache.fop.afp.modca.AbstractPageObject;
import org.apache.fop.afp.modca.Document;
import org.apache.fop.afp.modca.InterchangeSet;
import org.apache.fop.afp.modca.Overlay;
import org.apache.fop.afp.modca.PageGroup;
import org.apache.fop.afp.modca.PageObject;
import org.apache.fop.afp.modca.ResourceGroup;
import org.apache.fop.afp.modca.TagLogicalElement;
import org.apache.fop.afp.modca.triplets.FullyQualifiedNameTriplet;
import org.apache.fop.afp.ptoca.PtocaBuilder;
import org.apache.fop.afp.ptoca.PtocaProducer;
import org.apache.fop.fonts.Font;
import org.apache.fop.util.CharUtilities;

/**
 * A data stream is a continuous ordered stream of data elements and objects
 * conforming to a given format. Application programs can generate data streams
 * destined for a presentation service, archive library, presentation device or
 * another application program. The strategic presentation data stream
 * architectures used is Mixed Object Document Content Architecture (MO:DCA).
 *
 * The MO:DCA architecture defines the data stream used by applications to
 * describe documents and object envelopes for interchange with other
 * applications and application services. Documents defined in the MO:DCA format
 * may be archived in a database, then later retrieved, viewed, annotated and
 * printed in local or distributed systems environments. Presentation fidelity
 * is accommodated by including resource objects in the documents that reference
 * them.
 */
public class DataStream {

    /** Static logging instance */
    protected static final Log LOG = LogFactory.getLog("org.apache.xmlgraphics.afp");

    /** Boolean completion indicator */
    private boolean complete;

    /** The AFP document object */
    private Document document;

    /** The current page group object */
    private PageGroup currentPageGroup;

    /** The current page object */
    private PageObject currentPageObject;

    /** The current overlay object */
    private Overlay currentOverlay;

    /** The current page */
    private AbstractPageObject currentPage;

    /** The MO:DCA interchange set in use (default to MO:DCA-P IS/2 set) */
    private InterchangeSet interchangeSet
    = InterchangeSet.valueOf(InterchangeSet.MODCA_PRESENTATION_INTERCHANGE_SET_2);

    private final Factory factory;

    private OutputStream outputStream;

    /** the afp painting state */
    private final AFPPaintingState paintingState;

    /**
     * Default constructor for the AFPDocumentStream.
     *
     * @param factory the resource factory
     * @param paintingState the AFP painting state
     * @param outputStream the outputstream to write to
     */
    public DataStream(Factory factory, AFPPaintingState paintingState, OutputStream outputStream) {
        this.paintingState = paintingState;
        this.factory = factory;
        this.outputStream = outputStream;
    }

    /**
     * Returns the outputstream
     *
     * @return the outputstream
     */
    public OutputStream getOutputStream() {
        return this.outputStream;
    }

    /**
     * Returns the document object
     *
     * @return the document object
     */
    private Document getDocument() {
        return this.document;
    }

    /**
     * Returns the current page
     *
     * @return the current page
     */
    public AbstractPageObject getCurrentPage() {
        return this.currentPage;
    }

    /**
     * The document is started by invoking this method which creates an instance
     * of the AFP Document object.
     *
     * @param name
     *            the name of this document.
     */
    public void setDocumentName(String name) {
        if (name != null) {
            getDocument().setFullyQualifiedName(
                    FullyQualifiedNameTriplet.TYPE_BEGIN_DOCUMENT_REF,
                    FullyQualifiedNameTriplet.FORMAT_CHARSTR, name);
        }
    }

    /**
     * Helper method to mark the end of the current document.
     *
     * @throws IOException thrown if an I/O exception of some sort has occurred
     */
    public void endDocument() throws IOException {
        if (complete) {
            String msg = "Invalid state - document already ended.";
            LOG.warn("endDocument():: " + msg);
            throw new IllegalStateException(msg);
        }

        if (currentPageObject != null) {
            // End the current page if necessary
            endPage();
        }

        if (currentPageGroup != null) {
            // End the current page group if necessary
            endPageGroup();
        }

        // Write out document
        if (document != null) {
            document.endDocument();
            document.writeToStream(this.outputStream);
        }

        this.outputStream.flush();

        this.complete = true;

        this.document = null;

        this.outputStream = null;
    }

    /**
     * Start a new page. When processing has finished on the current page, the
     * {@link #endPage()}method must be invoked to mark the page ending.
     *
     * @param pageWidth
     *            the width of the page
     * @param pageHeight
     *            the height of the page
     * @param pageRotation
     *            the rotation of the page
     * @param pageWidthRes
     *            the width resolution of the page
     * @param pageHeightRes
     *            the height resolution of the page
     */
    public void startPage(int pageWidth, int pageHeight, int pageRotation,
            int pageWidthRes, int pageHeightRes) {
        currentPageObject = factory.createPage(pageWidth, pageHeight,
                pageRotation, pageWidthRes, pageHeightRes);
        currentPage = currentPageObject;
        currentOverlay = null;
    }

    /**
     * Start a new overlay. When processing has finished on the current overlay,
     * the {@link #endOverlay()}method must be invoked to mark the overlay
     * ending.
     *
     * @param x
     *            the x position of the overlay on the page
     * @param y
     *            the y position of the overlay on the page
     * @param width
     *            the width of the overlay
     * @param height
     *            the height of the overlay
     * @param widthRes
     *            the width resolution of the overlay
     * @param heightRes
     *            the height resolution of the overlay
     * @param overlayRotation
     *            the rotation of the overlay
     */
    public void startOverlay(int x, int y, int width, int height, int widthRes,
            int heightRes, int overlayRotation) {
        this.currentOverlay = factory.createOverlay(
                width, height, widthRes, heightRes, overlayRotation);

        String overlayName = currentOverlay.getName();
        currentPageObject.createIncludePageOverlay(overlayName, x, y, 0);
        currentPage = currentOverlay;
    }

    /**
     * Helper method to mark the end of the current overlay.
     *
     * @throws IOException thrown if an I/O exception of some sort has occurred
     */
    public void endOverlay() throws IOException {
        if (currentOverlay != null) {
            currentOverlay.endPage();
            currentOverlay = null;
            currentPage = currentPageObject;
        }
    }

    /**
     * Helper method to save the current page.
     *
     * @return current page object that was saved
     */
    public PageObject savePage() {
        PageObject pageObject = currentPageObject;
        if (currentPageGroup != null) {
            currentPageGroup.addPage(currentPageObject);
        } else {
            document.addPage(currentPageObject);
        }
        currentPageObject = null;
        currentPage = null;
        return pageObject;
    }

    /**
     * Helper method to restore the current page.
     *
     * @param pageObject
     *            page object
     */
    public void restorePage(PageObject pageObject) {
        currentPageObject = pageObject;
        currentPage = pageObject;
    }

    /**
     * Helper method to mark the end of the current page.
     *
     * @throws IOException thrown if an I/O exception of some sort has occurred
     */
    public void endPage() throws IOException {
        if (currentPageObject != null) {
            currentPageObject.endPage();
            if (currentPageGroup != null) {
                currentPageGroup.addPage(currentPageObject);
                currentPageGroup.writeToStream(this.outputStream);
            } else {
                document.addPage(currentPageObject);
                document.writeToStream(this.outputStream);
            }
            currentPageObject = null;
            currentPage = null;
        }
    }

    /**
     * Creates the given page fonts in the current page
     *
     * @param pageFonts
     *            a collection of AFP font attributes
     */
    public void addFontsToCurrentPage(Map pageFonts) {
        for (Object o : pageFonts.values()) {
            AFPFontAttributes afpFontAttributes = (AFPFontAttributes) o;
            createFont(afpFontAttributes.getFontReference(), afpFontAttributes
                    .getFont(), afpFontAttributes.getPointSize());
        }
    }

    /**
     * Helper method to create a map coded font object on the current page, this
     * method delegates the construction of the map coded font object to the
     * active environment group on the current page.
     *
     * @param fontReference
     *            the font number used as the resource identifier
     * @param font
     *            the font
     * @param size
     *            the point size of the font
     */
    public void createFont(int fontReference, AFPFont font, int size) {
        currentPage.createFont(fontReference, font, size);
    }

    /**
     * Returns a point on the current page
     *
     * @param x the X-coordinate
     * @param y the Y-coordinate
     * @return a point on the current page
     */
    private Point getPoint(int x, int y) {
        return paintingState.getPoint(x, y);
    }

    /**
     * Helper method to create text on the current page, this method delegates
     * to the current presentation text object in order to construct the text.
     *
     * @param textDataInfo the afp text data
     * @param letterSpacing letter spacing to draw text with
     * @param wordSpacing word Spacing to draw text with
     * @param font is the font to draw text with
     * @param charSet is the AFP Character Set to use with the text
     * @throws UnsupportedEncodingException thrown if character encoding is not supported
     */
    public void createText(final AFPTextDataInfo textDataInfo, final int letterSpacing,
            final int wordSpacing, final Font font, final CharacterSet charSet)
            throws UnsupportedEncodingException {
        int rotation = paintingState.getRotation();
        if (rotation != 0) {
            textDataInfo.setRotation(rotation);
            Point p = getPoint(textDataInfo.getX(), textDataInfo.getY());
            textDataInfo.setX(p.x);
            textDataInfo.setY(p.y);
        }
        // use PtocaProducer to create PTX records
        PtocaProducer producer = new PtocaProducer() {

            public void produce(PtocaBuilder builder) throws IOException {
                builder.setTextOrientation(textDataInfo.getRotation());
                builder.absoluteMoveBaseline(textDataInfo.getY());
                builder.absoluteMoveInline(textDataInfo.getX());

                builder.setExtendedTextColor(textDataInfo.getColor());
                builder.setCodedFont((byte)textDataInfo.getFontReference());

                int l = textDataInfo.getString().length();
                StringBuffer sb = new StringBuffer();

                int interCharacterAdjustment = 0;
                AFPUnitConverter unitConv = paintingState.getUnitConverter();
                if (letterSpacing != 0) {
                    interCharacterAdjustment = Math.round(unitConv.mpt2units(letterSpacing));
                }
                builder.setInterCharacterAdjustment(interCharacterAdjustment);

                int spaceWidth = font.getCharWidth(CharUtilities.SPACE);
                int spacing = spaceWidth + letterSpacing;
                int fixedSpaceCharacterIncrement = Math.round(unitConv.mpt2units(spacing));
                int varSpaceCharacterIncrement = fixedSpaceCharacterIncrement;
                if (wordSpacing != 0) {
                    varSpaceCharacterIncrement = Math.round(unitConv.mpt2units(
                            spaceWidth + wordSpacing + letterSpacing));
                }
                builder.setVariableSpaceCharacterIncrement(varSpaceCharacterIncrement);

                boolean fixedSpaceMode = false;

                for (int i = 0; i < l; i++) {
                    char orgChar = textDataInfo.getString().charAt(i);
                    float glyphAdjust = 0;
                    if (CharUtilities.isFixedWidthSpace(orgChar)) {
                        flushText(builder, sb, charSet);
                        builder.setVariableSpaceCharacterIncrement(
                                fixedSpaceCharacterIncrement);
                        fixedSpaceMode = true;
                        sb.append(CharUtilities.SPACE);
                        int charWidth = font.getCharWidth(orgChar);
                        glyphAdjust += (charWidth - spaceWidth);
                    } else {
                        if (fixedSpaceMode) {
                            flushText(builder, sb, charSet);
                            builder.setVariableSpaceCharacterIncrement(
                                    varSpaceCharacterIncrement);
                            fixedSpaceMode = false;
                        }
                        char ch;
                        if (orgChar == CharUtilities.NBSPACE) {
                            ch = ' '; //converted to normal space to allow word spacing
                        } else {
                            ch = orgChar;
                        }
                        sb.append(ch);
                    }

                    if (glyphAdjust != 0) {
                        flushText(builder, sb, charSet);
                        int increment = Math.round(unitConv.mpt2units(glyphAdjust));
                        builder.relativeMoveInline(increment);
                    }
                }
                flushText(builder, sb, charSet);
            }

            private void flushText(PtocaBuilder builder, StringBuffer sb,
                    final CharacterSet charSet) throws IOException {
                if (sb.length() > 0) {
                    builder.addTransparentData(charSet.encodeChars(sb));
                    sb.setLength(0);
                }
            }

        };

        currentPage.createText(producer);
    }

    /**
     * Method to create a line on the current page.
     *
     * @param lineDataInfo the line data information.
     */
    public void createLine(AFPLineDataInfo lineDataInfo) {
        currentPage.createLine(lineDataInfo);
    }

    /**
     * This method will create shading on the page using the specified
     * coordinates (the shading contrast is controlled via the red, green, blue
     * parameters, by converting this to grey scale).
     *
     * @param x
     *            the x coordinate of the shading
     * @param y
     *            the y coordinate of the shading
     * @param w
     *            the width of the shaded area
     * @param h
     *            the height of the shaded area
     * @param col
     *            the shading color
     */
    public void createShading(int x, int y, int w, int h, Color col) {
        currentPageObject.createShading(x, y, w, h, col.getRed(), col.getGreen(), col.getBlue());
    }

    /**
     * Helper method which allows creation of the MPO object, via the AEG. And
     * the IPO via the Page. (See actual object for descriptions.)
     *
     * @param name
     *            the name of the static overlay
     * @param x x-coordinate
     * @param y y-coordinate
     */
    public void createIncludePageOverlay(String name, int x, int y) {
        currentPageObject.createIncludePageOverlay(name, x, y, paintingState.getRotation());
        currentPageObject.getActiveEnvironmentGroup().createOverlay(name);
    }

    /**
     * Helper method which allows creation of the IMM object.
     *
     * @param name
     *            the name of the medium map
     */
    public void createInvokeMediumMap(String name) {
        currentPageGroup.createInvokeMediumMap(name);
    }

    /**
     * Creates an IncludePageSegment on the current page.
     *
     * @param name
     *            the name of the include page segment
     * @param x
     *            the x coordinate for the overlay
     * @param y
     *            the y coordinate for the overlay
     * @param width
     *            the width of the image
     * @param height
     *            the height of the image
     */
    public void createIncludePageSegment(String name, int x, int y, int width, int height) {
        int xOrigin;
        int yOrigin;
        int orientation = paintingState.getRotation();
        switch (orientation) {
        case 90:
            xOrigin = x - height;
            yOrigin = y;
            break;
        case 180:
            xOrigin = x - width;
            yOrigin = y - height;
            break;
        case 270:
            xOrigin = x;
            yOrigin = y - width;
            break;
        default:
            xOrigin = x;
            yOrigin = y;
            break;
        }
        boolean createHardPageSegments = true;
        currentPage.createIncludePageSegment(name, xOrigin, yOrigin, createHardPageSegments);
    }



    /**
     * Creates a TagLogicalElement on the current page.
     *
     * @param attributes
     *            the array of key value pairs.
     */

    public void createPageTagLogicalElement(TagLogicalElement.State[] attributes) {
        for (TagLogicalElement.State attribute : attributes) {

            currentPage.createTagLogicalElement(attribute);
        }
    }

    /**
     * Creates a TagLogicalElement on the current page group.
     *
     * @param attributes
     *            the array of key value pairs.
     */
    public void createPageGroupTagLogicalElement(TagLogicalElement.State[] attributes) {
        for (TagLogicalElement.State attribute : attributes) {
            currentPageGroup.createTagLogicalElement(attribute);
        }
    }

    /**
     * Creates a TagLogicalElement on the current page or page group
     *
     * @param name
     *            The tag name
     * @param value
     *            The tag value
     * @param encoding The CCSID character set encoding
     */
    public void createTagLogicalElement(String name, String value, int encoding) {

        TagLogicalElement.State tleState = new  TagLogicalElement.State(name, value, encoding);
        if (currentPage != null) {

            currentPage.createTagLogicalElement(tleState);

        } else {
            currentPageGroup.createTagLogicalElement(tleState);
        }
    }

    /**
     * Creates a NoOperation item
     *
     * @param content
     *            byte data
     */
    public void createNoOperation(String content) {
        if (currentPage != null) {
            currentPage.createNoOperation(content);
        } else if (currentPageGroup != null) {
            currentPageGroup.createNoOperation(content);
        } else {
            document.createNoOperation(content);
        }
    }

    /**
     * Returns the current page group
     *
     * @return the current page group
     */
    public PageGroup getCurrentPageGroup() {
        return this.currentPageGroup;
    }

    /**
     * Start a new document.
     *
     * @throws IOException thrown if an I/O exception of some sort has occurred
     */
    public void startDocument() throws IOException {
        this.document = factory.createDocument();
        document.writeToStream(this.outputStream);
    }

    /**
     * Start a new page group. When processing has finished on the current page
     * group the {@link #endPageGroup()}method must be invoked to mark the page
     * group ending.
     *
     * @throws IOException thrown if an I/O exception of some sort has occurred
     */
    public void startPageGroup() throws IOException {
        endPageGroup();
        this.currentPageGroup = factory.createPageGroup();
    }

    /**
     * Helper method to mark the end of the page group.
     *
     * @throws IOException thrown if an I/O exception of some sort has occurred
     */
    public void endPageGroup() throws IOException {
        if (currentPageGroup != null) {
            currentPageGroup.endPageGroup();
            document.addPageGroup(currentPageGroup);
            currentPageGroup = null;
        }
        document.writeToStream(outputStream); //Flush objects
    }

    /**
     * Sets the MO:DCA interchange set to use
     *
     * @param interchangeSet the MO:DCA interchange set
     */
    public void setInterchangeSet(InterchangeSet interchangeSet) {
        this.interchangeSet = interchangeSet;
    }

    /**
     * Returns the MO:DCA interchange set in use
     *
     * @return the MO:DCA interchange set in use
     */
    public InterchangeSet getInterchangeSet() {
        return this.interchangeSet;
    }

    /**
     * Returns the resource group for a given resource info
     *
     * @param level a resource level
     * @return a resource group for the given resource info
     */
    public ResourceGroup getResourceGroup(AFPResourceLevel level) {
        ResourceGroup resourceGroup = null;
        if (level.isDocument()) {
            resourceGroup = document.getResourceGroup();
        } else if (level.isPageGroup()) {
            resourceGroup = currentPageGroup.getResourceGroup();
        } else if (level.isPage()) {
            resourceGroup = currentPageObject.getResourceGroup();
        }
        return resourceGroup;
    }

}
