/*
 * 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.
 */
package org.openstreetmap.josm.plugins.pdfimport.pdfbox;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.geom.Area;
import java.awt.geom.GeneralPath;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.io.IOException;
import java.util.List;
import java.util.Map;

import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.graphics.PDGraphicsState;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream;
import org.apache.pdfbox.pdmodel.text.PDTextState;
import org.apache.pdfbox.util.Matrix;
import org.apache.pdfbox.util.PDFStreamEngine;
import org.apache.pdfbox.util.ResourceLoader;
import org.apache.pdfbox.util.TextPosition;
import org.openstreetmap.josm.tools.Logging;


/**
 * This will paint a page in a PDF document to a graphics context.
 *
 * @author <a href="mailto:ben@benlitchfield.com">Ben Litchfield</a>
 * @version $Revision: 1.22 $
 */
public class PageDrawer extends PDFStreamEngine {

    private GraphicsProcessor graphics;
    private BasicStroke stroke;
    protected PDPage page;

    private final GeneralPath linePath = new GeneralPath();

    /**
     * Default constructor, loads properties from file.
     *
     * @throws IOException If there is an error loading properties from the file.
     */
    public PageDrawer() throws IOException {
        super(ResourceLoader.loadProperties(
                "resources/org/openstreetmap/josm/plugins/pdfimport/pdfbox/PageDrawer.properties", true));
    }

    /**
     * This will draw the page to the requested context.
     *
     * @param g The graphics context to draw onto.
     * @param p The page to draw.
     *
     * @throws IOException If there is an IO error while drawing the page.
     */
    public void drawPage(GraphicsProcessor g, PDPage p) throws IOException {
        graphics = g;
        page = p;
        // Only if there is some content, we have to process it.
        // Otherwise we are done here and we will produce an empty page
        if (page.getContents() != null) {
            PDResources resources = page.findResources();
            processStream(page, resources, page.getContents().getStream());
        }

        List<?> annotations = page.getAnnotations();
        for (int i = 0; i < annotations.size(); i++) {
            PDAnnotation annot = (PDAnnotation) annotations.get(i);
            String appearanceName = annot.getAppearanceStream();
            PDAppearanceDictionary appearDictionary = annot.getAppearance();
            if (appearDictionary != null) {
                if (appearanceName == null) {
                    appearanceName = "default";
                }
                Map<?, ?> appearanceMap = appearDictionary.getNormalAppearance();
                if (appearanceMap != null) {
                    PDAppearanceStream appearance =
                        (PDAppearanceStream) appearanceMap.get(appearanceName);
                    if (appearance != null) {
                        processSubStream(page, appearance.getResources(), appearance.getStream());
                    }
                }
            }
        }

    }

    /**
     * You should override this method if you want to perform an action when a
     * text is being processed.
     *
     * @param text The text to process
     */
    @Override
    protected void processTextPosition(TextPosition text) {

        Color color = null;

        try {
            switch(this.getGraphicsState().getTextState().getRenderingMode()) {
            case PDTextState.RENDERING_MODE_FILL_TEXT:
                color = this.getGraphicsState().getNonStrokingColor().getJavaColor();
                break;
            case PDTextState.RENDERING_MODE_STROKE_TEXT:
                color = this.getGraphicsState().getStrokingColor().getJavaColor();
                break;
            case PDTextState.RENDERING_MODE_NEITHER_FILL_NOR_STROKE_TEXT:
                //basic support for text rendering mode "invisible"
                Color nsc = this.getGraphicsState().getStrokingColor().getJavaColor();
                float[] components = {Color.black.getRed(), Color.black.getGreen(), Color.black.getBlue()};
                color = new Color(nsc.getColorSpace(), components, 0f);
                break;
            default:
                color = this.getGraphicsState().getNonStrokingColor().getJavaColor();
            }

            Matrix textPos = text.getTextPos().copy();
            float x = textPos.getXPosition();
            float y = textPos.getYPosition();
            graphics.setClip(getGraphicsState().getCurrentClippingPath());
            graphics.drawString(x, y, text.getCharacter(), color);
        } catch (IOException e) {
            Logging.error(e);
        }
    }

    /**
     * Get the page that is currently being drawn.
     *
     * @return The page that is being drawn.
     */
    public PDPage getPage() {
        return page;
    }

    /**
     * Get the current line path to be drawn.
     *
     * @return The current line path to be drawn.
     */
    public GeneralPath getLinePath() {
        return linePath;
    }

    /**
     * This will set the current stroke.
     *
     * @param newStroke The current stroke.
     *
     */
    public void setStroke(BasicStroke newStroke) {
        this.stroke = newStroke;
    }

    public BasicStroke getStroke() {
        return this.stroke;
    }

    public void drawPath(boolean stroke, boolean fill, int windingRule) throws IOException {
        graphics.setClip(getGraphicsState().getCurrentClippingPath());
        GeneralPath path = getLinePath();

        Color strokeColor = getGraphicsState().getStrokingColor().getJavaColor();
        Color fillColor = getGraphicsState().getNonStrokingColor().getJavaColor();
        graphics.drawPath(path, stroke ? strokeColor : null, fill ? fillColor : null, windingRule);

        path.reset();
    }

    /**
     * Draw the AWT image. Called by Invoke.
     * Moved into PageDrawer so that Invoke doesn't have to reach in here for Graphics as that breaks extensibility.
     */
    public void drawImage() {
        graphics.setClip(getGraphicsState().getCurrentClippingPath());
        graphics.drawImage();
    }

    /**
     * Fill with Shading.  Called by SHFill operator.
     *
     * @param ShadingName  The name of the Shading Dictionary to use for this fill instruction.
     *
     * @throws IOException If there is an IO error while shade-filling the path/clipping area.
     */
    public void SHFill(COSName ShadingName) throws IOException {
        this.drawPath(false, true, Path2D.WIND_NON_ZERO);
    }

    //This code generalizes the code Jim Lynch wrote for AppendRectangleToPath
    /**
     * use the current transformation matrix to transform a single point.
     * @param x x-coordinate of the point to be transform
     * @param y y-coordinate of the point to be transform
     * @return the transformed coordinates as Point2D.Double
     */
    public java.awt.geom.Point2D.Double transformedPoint(double x, double y) {
        double[] position = {x, y};
        getGraphicsState().getCurrentTransformationMatrix().createAffineTransform().transform(
                position, 0, position, 0, 1);
        return new Point2D.Double(position[0], position[1]);
    }

    /**
     * Set the clipping Path.
     *
     * @param windingRule The winding rule this path will use.
     *
     */
    public void setClippingPath(int windingRule) {
        PDGraphicsState graphicsState = getGraphicsState();
        GeneralPath clippingPath = (GeneralPath) getLinePath().clone();
        clippingPath.setWindingRule(windingRule);
        // If there is already set a clipping path, we have to intersect the new with the existing one
        if (graphicsState.getCurrentClippingPath() != null) {
            Area currentArea = new Area(getGraphicsState().getCurrentClippingPath());
            Area newArea = new Area(clippingPath);
            currentArea.intersect(newArea);
            graphicsState.setCurrentClippingPath(currentArea);
        } else {
            graphicsState.setCurrentClippingPath(clippingPath);
        }
        getLinePath().reset();
    }
}
