/*
 * 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: TXTRenderer.java 1761019 2016-09-16 10:43:45Z ssteiner $ */

package org.apache.fop.render.txt;

import java.awt.Color;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
import java.util.Map;

import org.apache.xmlgraphics.util.UnitConv;

import org.apache.fop.apps.FOPException;
import org.apache.fop.apps.FOUserAgent;
import org.apache.fop.area.Area;
import org.apache.fop.area.CTM;
import org.apache.fop.area.PageViewport;
import org.apache.fop.area.inline.Image;
import org.apache.fop.area.inline.TextArea;
import org.apache.fop.render.AbstractPathOrientedRenderer;
import org.apache.fop.render.txt.border.AbstractBorderElement;
import org.apache.fop.render.txt.border.BorderManager;

/**
 * <p>Renderer that renders areas to plain text.</p>
 *
 * <p>This work was authored by Art Welch and
 * Mark Lillywhite (mark-fop@inomial.com) [to use the new Renderer interface].</p>
 */
public class TXTRenderer extends AbstractPathOrientedRenderer {

    private static final char LIGHT_SHADE = '\u2591';

    private static final char MEDIUM_SHADE = '\u2592';

    private static final char DARK_SHADE = '\u2593';

    private static final char FULL_BLOCK = '\u2588';

    private static final char IMAGE_CHAR = '#';

    /**The stream for output */
    private OutputStream outputStream;

    /** The current stream to add Text commands to. */
    private TXTStream currentStream;

    /** Buffer for text. */
    private StringBuffer[] charData;

    /** Buffer for background and images. */
    private StringBuffer[] decoData;

    /** Leading of line containing Courier font size of 10pt. */
    public static final int LINE_LEADING = 1070;

    /** Height of one symbol in Courier font size of 10pt. */
    public static final int CHAR_HEIGHT = 7860;

    /** Width of one symbol in Courier font size of 10pt. */
    public static final int CHAR_WIDTH = 6000;

    /** Current processing page width. */
    private int pageWidth;

    /** Current processing page height. */
    private int pageHeight;

    /**
     * Every line except the last line on a page (which will end with
     * pageEnding) will be terminated with this string.
     */
    private static final String LINE_ENDING = "\r\n";

    /** Every page except the last one will end with this string. */
    private static final String PAGE_ENDING = "\f";

    /** Equals true, if current page is first. */
    private boolean firstPage;

    /** Manager for storing border's information. */
    private BorderManager bm;

    /** Char for current filling. */
    private char fillChar;

    /** Saves current coordinate transformation. */
    private final TXTState currentState = new TXTState();

    private String encoding;

    /**
     * Constructs a newly allocated <code>TXTRenderer</code> object.
     *
     * @param userAgent the user agent that contains configuration details. This cannot be null.
     */
    public TXTRenderer(FOUserAgent userAgent) {
        super(userAgent);
    }

    /** {@inheritDoc} */
    public String getMimeType() {
        return "text/plain";
    }

    /**
     * Sets the encoding of the target file.
     * @param encoding the encoding, null to select the default encoding (UTF-8)
     */
    public void setEncoding(String encoding) {
        this.encoding = encoding;
    }

    /**
     * Indicates if point (x, y) lay inside currentPage.
     *
     * @param x x coordinate
     * @param y y coordinate
     * @return <b>true</b> if point lay inside page
     */
    public boolean isLayInside(int x, int y) {
        return (x >= 0) && (x < pageWidth) && (y >= 0) && (y < pageHeight);
    }

    /**
     * Add char to text buffer.
     *
     * @param x  x coordinate
     * @param y  y coordinate
     * @param ch  char to add
     * @param ischar boolean, repersenting is character adding to text buffer
     */
    protected void addChar(int x, int y, char ch, boolean ischar) {
        Point point = currentState.transformPoint(x, y);
        putChar(point.x, point.y, ch, ischar);
    }

    /**
     * Add char to text or background buffer.
     *
     * @param x x coordinate
     * @param y x coordinate
     * @param ch char to add
     * @param ischar indicates if it char or background
     */
    protected void putChar(int x, int y, char ch, boolean ischar) {
        if (isLayInside(x, y)) {
            StringBuffer sb = ischar ? charData[y] : decoData[y];
            while (sb.length() <= x) {
                sb.append(' ');
            }
            sb.setCharAt(x, ch);
        }
    }

    /**
     * Adds string to text buffer (<code>charData</code>). <p>
     * Chars of string map in turn.
     *
     * @param row x coordinate
     * @param col y coordinate
     * @param s string to add
     */
    protected void addString(int row, int col, String s) {
        for (int l = 0; l < s.length(); l++) {
            addChar(col + l, row, s.charAt(l), true);
        }
    }

    /**
     * Render TextArea to Text.
     *
     * @param area  inline area to render
     */
    protected void renderText(TextArea area) {
        int col = Helper.ceilPosition(this.currentIPPosition, CHAR_WIDTH);
        int row = Helper.ceilPosition(this.currentBPPosition - LINE_LEADING, CHAR_HEIGHT + 2 * LINE_LEADING);

        String s = area.getText();

        addString(row, col, s);

        super.renderText(area);
    }

    /**
     * {@inheritDoc}
     */
    public void renderPage(PageViewport page) throws IOException, FOPException {
        if (firstPage) {
            firstPage = false;
        } else {
            currentStream.add(PAGE_ENDING);
        }

        Rectangle2D bounds = page.getViewArea();
        double width = bounds.getWidth();
        double height = bounds.getHeight();

        pageWidth = Helper.ceilPosition((int) width, CHAR_WIDTH);
        pageHeight = Helper.ceilPosition((int) height, CHAR_HEIGHT + 2 * LINE_LEADING);

        // init buffers
        charData = new StringBuffer[pageHeight];
        decoData = new StringBuffer[pageHeight];
        for (int i = 0; i < pageHeight; i++) {
            charData[i] = new StringBuffer();
            decoData[i] = new StringBuffer();
        }

        bm = new BorderManager(pageWidth, pageHeight, currentState);

        super.renderPage(page);

        flushBorderToBuffer();
        flushBuffer();
    }

    /**
     * Projects current page borders (i.e.<code>bm</code>) to buffer for
     * background and images (i.e.<code>decoData</code>).
     */
    private void flushBorderToBuffer() {
        for (int x = 0; x < pageWidth; x++) {
            for (int y = 0; y < pageHeight; y++) {
                Character c = bm.getCharacter(x, y);
                if (c != null) {
                    putChar(x, y, c, false);
                }
            }
        }
    }

    /**
     * Write out the buffer to output stream.
     */
    private void flushBuffer() {
        for (int row = 0; row < pageHeight; row++) {
            StringBuffer cr = charData[row];
            StringBuffer dr = decoData[row];
            StringBuffer outr = null;

            if (cr != null && dr == null) {
                outr = cr;
            } else if (dr != null && cr == null) {
                outr = dr;
            } else if (cr != null && dr != null) {
                int len = dr.length();
                if (cr.length() > len) {
                    len = cr.length();
                }
                outr = new StringBuffer();
                for (int countr = 0; countr < len; countr++) {
                    if (countr < cr.length() && cr.charAt(countr) != ' ') {
                        outr.append(cr.charAt(countr));
                    } else if (countr < dr.length()) {
                        outr.append(dr.charAt(countr));
                    } else {
                        outr.append(' ');
                    }
                }
            }

            if (outr != null) {
                currentStream.add(outr.toString());
            }
            if (row < pageHeight) {
                currentStream.add(LINE_ENDING);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    public void startRenderer(OutputStream os) throws IOException {
        log.info("Rendering areas to TEXT.");
        this.outputStream = os;
        currentStream = new TXTStream(os);
        currentStream.setEncoding(this.encoding);
        firstPage = true;
    }

    /**
     * {@inheritDoc}
     */
    public void stopRenderer() throws IOException {
        log.info("writing out TEXT");
        outputStream.flush();
        super.stopRenderer();
    }

    /**
     * Does nothing.
     * {@inheritDoc}
     */
    protected void restoreStateStackAfterBreakOut(List breakOutList) {
    }

    /**
     * Does nothing.
     * @return null
     * {@inheritDoc}
     */
    protected List breakOutOfStateStack() {
        return null;
    }

    /**
     * Does nothing.
     * {@inheritDoc}
     */
    protected void saveGraphicsState() {
        currentState.push(new CTM());
    }

    /**
     * Does nothing.
     * {@inheritDoc}
     */
    protected void restoreGraphicsState() {
        currentState.pop();
    }

    /**
     * Does nothing.
     * {@inheritDoc}
     */
    protected void beginTextObject() {
    }

    /**
     * Does nothing.
     * {@inheritDoc}
     */
    protected void endTextObject() {
    }

    /**
     * Does nothing.
     * {@inheritDoc}
     */
    protected void clip() {
    }

    /**
     * Does nothing.
     * {@inheritDoc}
     */
    protected void clipRect(float x, float y, float width, float height) {
    }

    /**
     * Does nothing.
     * {@inheritDoc}
     */
    protected void moveTo(float x, float y) {
    }

    /**
     * Does nothing.
     * {@inheritDoc}
     */
    protected void lineTo(float x, float y) {
    }

    /**
     * Does nothing.
     * {@inheritDoc}
     */
    protected void closePath() {
    }

    /**
     * Fills rectangle startX, startY, width, height with char
     * <code>charToFill</code>.
     *
     * @param startX x-coordinate of upper left point
     * @param startY y-coordinate of upper left point
     * @param width width of rectangle
     * @param height height of rectangle
     * @param charToFill filling char
     */
    private void fillRect(int startX, int startY, int width, int height,
            char charToFill) {
        for (int x = startX; x < startX + width; x++) {
            for (int y = startY; y < startY + height; y++) {
                addChar(x, y, charToFill, false);
            }
        }
    }

    /**
     * Fills a rectangular area with the current filling char.
     * {@inheritDoc}
     */
    protected void fillRect(float x, float y, float width, float height) {
        fillRect(bm.getStartX(), bm.getStartY(), bm.getWidth(), bm.getHeight(),
                fillChar);
    }

    /**
     * Changes current filling char.
     * {@inheritDoc}
     */
    protected void updateColor(Color col, boolean fill) {
        if (col == null) {
            return;
        }
        // fillShade evaluation was taken from fop-0.20.5
        // TODO: This fillShase is catually the luminance component of the color
        // transformed to the YUV (YPrBb) Colorspace. It should use standard
        // Java methods for its conversion instead of the formula given here.
        double fillShade = 0.30f / 255f * col.getRed()
                         + 0.59f / 255f * col.getGreen()
                         + 0.11f / 255f * col.getBlue();
        fillShade = 1 - fillShade;

        if (fillShade > 0.8f) {
            fillChar = FULL_BLOCK;
        } else if (fillShade > 0.6f) {
            fillChar = DARK_SHADE;
        } else if (fillShade > 0.4f) {
            fillChar = MEDIUM_SHADE;
        } else if (fillShade > 0.2f) {
            fillChar = LIGHT_SHADE;
        } else {
            fillChar = ' ';
        }
    }

    /** {@inheritDoc} */
    protected void drawImage(String url, Rectangle2D pos, Map foreignAttributes) {
        //No images are painted here
    }

    /**
     * Fills image rectangle with a <code>IMAGE_CHAR</code>.
     *
     * @param   image   the base image
     * @param   pos     the position of the image
     */
    public void renderImage(Image image, Rectangle2D pos) {
        int x1 = Helper.ceilPosition(currentIPPosition, CHAR_WIDTH);
        int y1 = Helper.ceilPosition(currentBPPosition - LINE_LEADING, CHAR_HEIGHT + 2 * LINE_LEADING);
        int width = Helper.ceilPosition((int) pos.getWidth(), CHAR_WIDTH);
        int height = Helper.ceilPosition((int) pos.getHeight(), CHAR_HEIGHT + 2 * LINE_LEADING);

        fillRect(x1, y1, width, height, IMAGE_CHAR);
    }


    /**
     * Returns the closest integer to the multiplication of a number and 1000.
     *
     * @param x  the value of the argument, multiplied by
     *            1000 and rounded
     * @return the value of the argument multiplied by
     *         1000 and rounded to the nearest integer
     */
    protected int toMilli(float x) {
        return Math.round(x * 1000f);
    }

    /**
     * Adds one element of border.
     *
     * @param x  x coordinate
     * @param y  y coordinate
     * @param style  integer, representing border style
     * @param type  integer, representing border element type
     */
    private void addBitOfBorder(int x, int y, int style, int type) {
        Point point = currentState.transformPoint(x, y);
        if (isLayInside(point.x, point.y)) {
            bm.addBorderElement(point.x, point.y, style, type);
        }
    }

    /**
     * {@inheritDoc}
     */
    protected void drawBorderLine(float x1, float y1, float x2, float y2,
            boolean horz, boolean startOrBefore, int style, Color col) {

        int borderHeight = bm.getHeight();
        int borderWidth = bm.getWidth();
        int borderStartX = bm.getStartX();
        int borderStartY = bm.getStartY();

        int x;
        int y;
        if (horz && startOrBefore) { // BEFORE
            x = borderStartX;
            y = borderStartY;
        } else if (horz && !startOrBefore) { // AFTER
            x = borderStartX;
            y = borderStartY + borderHeight - 1;
        } else if (!horz && startOrBefore) { // START
            x = borderStartX;
            y = borderStartY;
        } else { // END
            x = borderStartX + borderWidth - 1;
            y = borderStartY;
        }

        int dx;
        int dy;
        int length;
        int startType;
        int endType;
        if (horz) {
            length = borderWidth;
            dx = 1;
            dy = 0;
            startType = 1 << AbstractBorderElement.RIGHT;
            endType = 1 << AbstractBorderElement.LEFT;
        } else {
            length = borderHeight;
            dx = 0;
            dy = 1;
            startType = 1 << AbstractBorderElement.DOWN;
            endType = 1 << AbstractBorderElement.UP;
        }

        addBitOfBorder(x, y, style, startType);
        for (int i = 0; i < length - 2; i++) {
            x += dx;
            y += dy;
            addBitOfBorder(x, y, style, startType + endType);
        }
        x += dx;
        y += dy;
        addBitOfBorder(x, y, style, endType);
    }

    /**
     * {@inheritDoc}
     */
    protected void drawBackAndBorders(Area area, float startx, float starty,
            float width, float height) {
        bm.setWidth(Helper.ceilPosition(toMilli(width), CHAR_WIDTH));
        bm.setHeight(Helper.ceilPosition(toMilli(height), CHAR_HEIGHT + 2 * LINE_LEADING));
        bm.setStartX(Helper.ceilPosition(toMilli(startx), CHAR_WIDTH));
        bm.setStartY(Helper.ceilPosition(toMilli(starty), CHAR_HEIGHT + 2 * LINE_LEADING));

        super.drawBackAndBorders(area, startx, starty, width, height);
    }

    /**
     * {@inheritDoc}
     */
    protected void startVParea(CTM ctm, Rectangle clippingRect) {
        currentState.push(ctm);
    }

    /**
     * {@inheritDoc}
     */
    protected void endVParea() {
        currentState.pop();
    }

    /** {@inheritDoc} */
    protected void startLayer(String layer) {
    }

    /** {@inheritDoc} */
    protected void endLayer() {
    }

    /** {@inheritDoc} */
    protected void concatenateTransformationMatrix(AffineTransform at) {
        currentState.push(new CTM(UnitConv.ptToMpt(at)));
    }

}
