/*
 * 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: AFPPaintingState.java 1866691 2019-09-09 13:20:08Z ssteiner $ */

package org.apache.fop.afp;

import java.awt.Point;
import java.io.IOException;
import java.io.ObjectInputStream;

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

import org.apache.xmlgraphics.java2d.color.ColorConverter;
import org.apache.xmlgraphics.java2d.color.DefaultColorConverter;

import org.apache.fop.afp.fonts.AFPPageFonts;
import org.apache.fop.util.AbstractPaintingState;

/**
 * This keeps information about the current painting state when writing to an
 * AFP datastream.
 */
public class AFPPaintingState extends org.apache.fop.util.AbstractPaintingState {

    private static final long serialVersionUID = 8206711712452344473L;

    private static Log log = LogFactory.getLog("org.apache.xmlgraphics.afp");

    /** the portrait rotation */
    private int portraitRotation;

    /** the landscape rotation */
    private int landscapeRotation = 270;

    /** color image support */
    private boolean colorImages;

    /** dithering quality setting (0.0f..1.0f) */
    private float ditheringQuality;

    /** image encoding quality setting (0.0f..1.0f) */
    private float bitmapEncodingQuality;

    /** color image handler */
    private transient ColorConverter colorConverter;

    /**
     * true if certain image formats may be embedded unchanged in their native
     * format.
     */
    private boolean nativeImagesSupported;

    private boolean canEmbedJpeg;

    /**
     * true if CMYK images (requires IOCA FS45 suppport on the target platform)
     * may be generated
     */
    private boolean cmykImagesSupported;

    /** default value for image depth */
    private int bitsPerPixel = 8;

    /** the output resolution */
    private int resolution = 240; // 240 dpi

    /**
     * A configurable value to correct the line width so that the output matches the expected. Different
     * devices may need different values.
     */
    private float lineWidthCorrection = AFPConstants.LINE_WIDTH_CORRECTION;

    /** determines whether GOCA is enabled or disabled  */
    private boolean gocaEnabled = true;
    /** determines whether to stroke text in GOCA mode or to use text operators where possible */
    private boolean strokeGocaText;


    /** use page segment with F11 and F45 images*/
    private boolean pSeg;

    private boolean gocaPSeg;

    /** use FS45 images*/
    private boolean fs45;

    /** the current page */
    private transient AFPPagePaintingState pagePaintingState;

    // /** reference orientation */
    // private int orientation = 0;

    /** a unit converter */
    private final transient AFPUnitConverter unitConv;

    public AFPPaintingState() {
        colorConverter = GrayScaleColorConverter.getInstance();
        pagePaintingState = new AFPPagePaintingState();
        unitConv = new AFPUnitConverter(this);
    }

    private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException {
        ois.defaultReadObject();
    }

    /**
     * Sets the rotation to be used for portrait pages, valid values are 0
     * (default), 90, 180, 270.
     *
     * @param rotation
     *            The rotation in degrees.
     */
    public void setPortraitRotation(int rotation) {
        if (rotation == 0 || rotation == 90 || rotation == 180 || rotation == 270) {
            portraitRotation = rotation;
        } else {
            throw new IllegalArgumentException("The portrait rotation must be one"
                    + " of the values 0, 90, 180, 270");

        }
    }

    /**
     * Returns the rotation to be used for portrait pages
     *
     * @return the rotation to be used for portrait pages
     */
    protected int getPortraitRotation() {
        return this.portraitRotation;
    }

    /**
     * Sets the rotation to be used for landscape pages, valid values are 0, 90,
     * 180, 270 (default).
     *
     * @param rotation
     *            The rotation in degrees.
     */
    public void setLandscapeRotation(int rotation) {
        if (rotation == 0 || rotation == 90 || rotation == 180 || rotation == 270) {
            landscapeRotation = rotation;
        } else {
            throw new IllegalArgumentException("The landscape rotation must be one"
                    + " of the values 0, 90, 180, 270");
        }
    }

    /**
     * Returns the landscape rotation
     *
     * @return the landscape rotation
     */
    protected int getLandscapeRotation() {
        return this.landscapeRotation;
    }

    /**
     * Sets the number of bits used per pixel
     *
     * @param bitsPerPixel
     *            number of bits per pixel
     */
    public void setBitsPerPixel(int bitsPerPixel) {
        switch (bitsPerPixel) {
        case 1:
        case 4:
        case 8:
            this.bitsPerPixel = bitsPerPixel;
            break;
        default:
            log.warn("Invalid bits_per_pixel value, must be 1, 4 or 8.");
            this.bitsPerPixel = 8;
            break;
        }
    }

    /**
     * Returns the number of bits per pixel
     *
     * @return the number of bits per pixel
     */
    public int getBitsPerPixel() {
        return this.bitsPerPixel;
    }

    /**
     * Sets whether images are color or not and instantiates a ColorHandler
     *
     * @param colorImages
     *            color image output
     */
    public void setColorImages(boolean colorImages) {
        this.colorImages = colorImages;

        if (colorImages) {
            this.colorConverter = DefaultColorConverter.getInstance();
        }

    }

    /**
     * Returns true if color images are to be used
     *
     * @return true if color images are to be used
     */
    public boolean isColorImages() {
        return this.colorImages;
    }

    /**
     * Used to convert color in respect of the colorImages flag
     *
     * @return the color converter
     */
    public ColorConverter getColorConverter() {
        return this.colorConverter;
    }

    /**
     * Sets whether images are natively supported or not in the AFP environment
     *
     * @param nativeImagesSupported
     *            true if images are natively supported in this AFP environment
     */
    public void setNativeImagesSupported(boolean nativeImagesSupported) {
        this.nativeImagesSupported = nativeImagesSupported;
    }

    /**
     * Returns true if images are supported natively in this AFP environment
     *
     * @return true if images are supported natively in this AFP environment
     */
    public boolean isNativeImagesSupported() {
        return this.nativeImagesSupported;
    }

    /**
     * Set whether or not JPEG images can be embedded within an AFP document.
     *
     * @param canEmbed true if the JPEG image can be embedded
     */
    public void setCanEmbedJpeg(boolean canEmbed) {
        canEmbedJpeg = canEmbed;
    }

    /**
     * Returns true if JPEGs can be embedded in an AFP document.
     *
     * @return true if JPEG embedding is allowed
     */
    public boolean canEmbedJpeg() {
        return canEmbedJpeg;
    }

    /**
     * Controls whether CMYK images (IOCA FS45) are enabled. By default, support
     * is disabled for wider compatibility. When disabled, any CMYK image is
     * converted to the selected color format.
     *
     * @param value
     *            true to enabled CMYK images
     */
    public void setCMYKImagesSupported(boolean value) {
        this.cmykImagesSupported = value;
    }

    /**
     * Indicates whether CMYK images (IOCA FS45) are enabled.
     *
     * @return true if IOCA FS45 is enabled
     */
    public boolean isCMYKImagesSupported() {
        return this.cmykImagesSupported;
    }

    /**
     * Gets the dithering quality setting to use when converting images to monochrome images.
     * @return the dithering quality (a value between 0.0f and 1.0f)
     */
    public float getDitheringQuality() {
        return this.ditheringQuality;
    }

    /**
     * Sets the dithering quality setting to use when converting images to monochrome images.
     * @param quality Defines the desired quality level for the conversion.
     *                  Valid values: a value between 0.0f (fastest) and 1.0f (best)
     */
    public void setDitheringQuality(float quality) {
        quality = Math.max(quality, 0.0f);
        quality = Math.min(quality, 1.0f);
        this.ditheringQuality = quality;
    }

    /**
     * Gets the image encoding quality setting to use when encoding bitmap images.
     * @return the encoding quality (a value between 0.0f and 1.0f, 1.0 meaning loss-less)
     */
    public float getBitmapEncodingQuality() {
        return this.bitmapEncodingQuality;
    }

    /**
     * Sets the image encoding quality setting to use when encoding bitmap images.
     * @param quality Defines the desired quality level for the conversion.
     *                  Valid values: a value between 0.0f (lowest) and 1.0f (best, loss-less)
     */
    public void setBitmapEncodingQuality(float quality) {
        quality = Math.max(quality, 0.0f);
        quality = Math.min(quality, 1.0f);
        this.bitmapEncodingQuality = quality;
    }

    /**
     * Sets the output/device resolution
     *
     * @param resolution
     *            the output resolution (dpi)
     */
    public void setResolution(int resolution) {
        if (log.isDebugEnabled()) {
            log.debug("renderer-resolution set to: " + resolution + "dpi");
        }
        this.resolution = resolution;
    }

    /**
     * Sets the line width correction
     *
     * @param correction the line width multiplying factor correction
     */
    public void setLineWidthCorrection(float correction) {
        if (log.isDebugEnabled()) {
            log.debug("line width correction set to: " + correction);
        }
        this.lineWidthCorrection = correction;
    }

    /**
     * Returns the output/device resolution.
     *
     * @return the resolution in dpi
     */
    public int getResolution() {
        return this.resolution;
    }

    /**
     * Returns the line width correction.
     * @return the correction
     */
    public float getLineWidthCorrection() {
        return this.lineWidthCorrection;
    }

    /**
     * Controls whether GOCA is enabled or disabled.
     * @param enabled true if GOCA is enabled, false if it is disabled
     */
    public void setGOCAEnabled(boolean enabled) {
        this.gocaEnabled = enabled;
    }

    /**
     * Indicates whether GOCA is enabled or disabled.
     * @return true if GOCA is enabled, false if GOCA is disabled
     */
    public boolean isGOCAEnabled() {
        return this.gocaEnabled;
    }

    /**
     * Controls whether to stroke text in GOCA mode or to use text operators where possible.
     * @param stroke true to stroke, false to paint with text operators where possible
     */
    public void setStrokeGOCAText(boolean stroke) {
        this.strokeGocaText = stroke;
    }

    /**
     * Indicates whether to stroke text in GOCA mode or to use text operators where possible.
     * @return true to stroke, false to paint with text operators where possible
     */
    public boolean isStrokeGOCAText() {
        return this.strokeGocaText;
    }

    /**
     * Whether FS11 and SF45 non-inline images should be wrapped in a page segment
     * @return true iff images should be wrapped
     */
    public boolean getWrapPSeg() {
        return pSeg;
    }

    /**
     * Sets whether FS11 and FS45 non-inline images should be wrapped in a page segment
     * @param pSeg true iff images should be wrapped
     */
    public void setWrapPSeg(boolean pSeg) {
        this.pSeg = pSeg;
    }

    public boolean getWrapGocaPSeg() {
        return gocaPSeg;
    }

    public void setWrapGocaPSeg(boolean pSeg) {
        this.gocaPSeg = pSeg;
    }

    /**
     * gets whether images should be FS45
     * @return true iff images should be FS45
     */
    public boolean getFS45() {
        return fs45;
    }

    /**
     * sets whether images should be FS45
     * @param fs45 true iff images should be FS45
     */
    public void setFS45(boolean fs45) {
        this.fs45 = fs45;
    }



    /** {@inheritDoc} */
    @Override
    protected AbstractData instantiateData() {
        return new AFPData();
    }

    /** {@inheritDoc} */
    @Override
    protected AbstractPaintingState instantiate() {
        return new AFPPaintingState();
    }

    /**
     * Returns the painting state of the current page
     *
     * @return the painting state of the current page
     */
    protected AFPPagePaintingState getPagePaintingState() {
        return this.pagePaintingState;
    }

    /**
     * Gets the current page fonts
     *
     * @return the current page fonts
     */
    public AFPPageFonts getPageFonts() {
        return pagePaintingState.getFonts();
    }

    /**
     * Sets the page width
     *
     * @param pageWidth
     *            the page width
     */
    public void setPageWidth(int pageWidth) {
        pagePaintingState.setWidth(pageWidth);
    }

    /**
     * Returns the page width
     *
     * @return the page width
     */
    public int getPageWidth() {
        return pagePaintingState.getWidth();
    }

    /**
     * Sets the page height
     *
     * @param pageHeight
     *            the page height
     */
    public void setPageHeight(int pageHeight) {
        pagePaintingState.setHeight(pageHeight);
    }

    /**
     * Returns the page height
     *
     * @return the page height
     */
    public int getPageHeight() {
        return pagePaintingState.getHeight();
    }

    /**
     * Returns the page rotation
     *
     * @return the page rotation
     */
    public int getPageRotation() {
        return pagePaintingState.getOrientation();
    }

    /**
     * Sets the uri of the current image
     *
     * @param uri
     *            the uri of the current image
     */
    public void setImageUri(String uri) {
        ((AFPData) getData()).imageUri = uri;
    }

    /**
     * Gets the uri of the current image
     *
     * @return the uri of the current image
     */
    public String getImageUri() {
        return ((AFPData) getData()).imageUri;
    }

    /**
     * Returns the currently derived rotation
     *
     * @return the currently derived rotation
     */
    public int getRotation() {
        return getData().getDerivedRotation();
    }

    /**
     * Returns the unit converter
     *
     * @return the unit converter
     */
    public AFPUnitConverter getUnitConverter() {
        return this.unitConv;
    }

    /**
     * Returns a point on the current page, taking the current painting state
     * into account.
     *
     * @param x
     *            the X-coordinate
     * @param y
     *            the Y-coordinate
     * @return a point on the current page
     */
    public Point getPoint(int x, int y) {
        Point p = new Point();
        int rotation = getRotation();
        switch (rotation) {
        case 90:
            p.x = y;
            p.y = getPageWidth() - x;
            break;
        case 180:
            p.x = getPageWidth() - x;
            p.y = getPageHeight() - y;
            break;
        case 270:
            p.x = getPageHeight() - y;
            p.y = x;
            break;
        default:
            p.x = x;
            p.y = y;
            break;
        }
        return p;
    }

    /** {@inheritDoc} */
    @Override
    public Object clone() {
        AFPPaintingState paintingState = (AFPPaintingState) super.clone();
        paintingState.pagePaintingState = (AFPPagePaintingState) this.pagePaintingState.clone();
        paintingState.portraitRotation = this.portraitRotation;
        paintingState.landscapeRotation = this.landscapeRotation;
        paintingState.bitsPerPixel = this.bitsPerPixel;
        paintingState.colorImages = this.colorImages;
        paintingState.colorConverter = this.colorConverter;
        paintingState.resolution = this.resolution;
        return paintingState;
    }

    /** {@inheritDoc} */
    @Override
    public String toString() {
        return "AFPPaintingState{" + "portraitRotation=" + portraitRotation
                + ", landscapeRotation=" + landscapeRotation + ", colorImages=" + colorImages
                + ", bitsPerPixel=" + bitsPerPixel + ", resolution=" + resolution + ", pageState="
                + pagePaintingState + super.toString() + "}";
    }

    /**
     * Page level state data
     */
    private class AFPPagePaintingState implements Cloneable {
        /** page width */
        private int width;

        /** page height */
        private int height;

        /** page fonts */
        private AFPPageFonts fonts = new AFPPageFonts();

        /** page font count */
        private int fontCount;

        /** page orientation */
        private int orientation;

        /**
         * Returns the page width
         *
         * @return the page width
         */
        protected int getWidth() {
            return width;
        }

        /**
         * Sets the page width
         *
         * @param width
         *            the page width
         */
        protected void setWidth(int width) {
            this.width = width;
        }

        /**
         * Returns the page height
         *
         * @return the page height
         */
        protected int getHeight() {
            return height;
        }

        /**
         * Sets the page height
         *
         * @param height
         *            the page height
         */
        protected void setHeight(int height) {
            this.height = height;
        }

        /**
         * Returns the page fonts
         *
         * @return the page fonts
         */
        protected AFPPageFonts getFonts() {
            return fonts;
        }

        /**
         * Sets the current page fonts
         *
         * @param fonts
         *            the current page fonts
         */
        protected void setFonts(AFPPageFonts fonts) {
            this.fonts = fonts;
        }

        /**
         * Increments and returns the current page font count
         *
         * @return increment and return the current page font count
         */
        protected int incrementFontCount() {
            return ++fontCount;
        }

        /**
         * Returns the current page orientation
         *
         * @return the current page orientation
         */
        protected int getOrientation() {
            return orientation;
        }

        /**
         * Sets the current page orientation
         *
         * @param orientation
         *            the current page orientation
         */
        protected void setOrientation(int orientation) {
            this.orientation = orientation;
        }

        /** {@inheritDoc} */
        @Override
        public Object clone() {
            AFPPagePaintingState state = new AFPPagePaintingState();
            state.width = this.width;
            state.height = this.height;
            state.orientation = this.orientation;
            state.fonts = new AFPPageFonts(this.fonts);
            state.fontCount = this.fontCount;
            return state;
        }

        /** {@inheritDoc} */
        @Override
        public String toString() {
            return "AFPPagePaintingState{width=" + width + ", height=" + height + ", orientation="
                    + orientation + ", fonts=" + fonts + ", fontCount=" + fontCount + "}";
        }
    }

    /**
     * Block level state data
     */
    // @SuppressFBWarnings("SE_INNER_CLASS")
    private class AFPData extends org.apache.fop.util.AbstractPaintingState.AbstractData {
        private static final long serialVersionUID = -1789481244175275686L;

        /** The current fill status */
        private boolean filled;

        private String imageUri;

        /** {@inheritDoc} */
        @Override
        public Object clone() {
            AFPData obj = (AFPData) super.clone();
            obj.filled = this.filled;
            obj.imageUri = this.imageUri;
            return obj;
        }

        /** {@inheritDoc} */
        @Override
        public String toString() {
            return "AFPData{" + super.toString() + ", filled=" + filled + ", imageUri=" + imageUri
                    + "}";
        }

        /** {@inheritDoc} */
        @Override
        protected AbstractData instantiate() {
            return new AFPData();
        }
    }

}
