/*
 * Copyright (c) 2005, 2013, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package sun.java2d.pipe;

import java.awt.BasicStroke;
import java.awt.Polygon;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Arc2D;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Path2D;
import java.awt.geom.IllegalPathStateException;
import java.awt.geom.PathIterator;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import sun.java2d.SunGraphics2D;
import sun.java2d.loops.ProcessPath;
import static sun.java2d.pipe.BufferedOpCodes.*;

/**
 * Base class for enqueuing rendering operations in a single-threaded
 * rendering environment.  Instead of each operation being rendered
 * immediately by the underlying graphics library, the operation will be
 * added to the provided RenderQueue, which will be processed at a later
 * time by a single thread.
 *
 * This class provides implementations of drawLine(), drawRect(), drawPoly(),
 * fillRect(), draw(Shape), and fill(Shape), which are useful for a
 * hardware-accelerated renderer.  The other draw*() and fill*() methods
 * simply delegate to draw(Shape) and fill(Shape), respectively.
 */
public abstract class BufferedRenderPipe
    implements PixelDrawPipe, PixelFillPipe, ShapeDrawPipe, ParallelogramPipe
{
    ParallelogramPipe aapgrampipe = new AAParallelogramPipe();

    static final int BYTES_PER_POLY_POINT = 8;
    static final int BYTES_PER_SCANLINE = 12;
    static final int BYTES_PER_SPAN = 16;

    protected RenderQueue rq;
    protected RenderBuffer buf;
    private BufferedDrawHandler drawHandler;

    public BufferedRenderPipe(RenderQueue rq) {
        this.rq = rq;
        this.buf = rq.getBuffer();
        this.drawHandler = new BufferedDrawHandler();
    }

    public ParallelogramPipe getAAParallelogramPipe() {
        return aapgrampipe;
    }

    /**
     * Validates the state in the provided SunGraphics2D object and sets up
     * any special resources for this operation (e.g. enabling gradient
     * shading).
     */
    protected abstract void validateContext(SunGraphics2D sg2d);
    protected abstract void validateContextAA(SunGraphics2D sg2d);

    public void drawLine(SunGraphics2D sg2d,
                         int x1, int y1, int x2, int y2)
    {
        int transx = sg2d.transX;
        int transy = sg2d.transY;
        rq.lock();
        try {
            validateContext(sg2d);
            rq.ensureCapacity(20);
            buf.putInt(DRAW_LINE);
            buf.putInt(x1 + transx);
            buf.putInt(y1 + transy);
            buf.putInt(x2 + transx);
            buf.putInt(y2 + transy);
        } finally {
            rq.unlock();
        }
    }

    public void drawRect(SunGraphics2D sg2d,
                         int x, int y, int width, int height)
    {
        rq.lock();
        try {
            validateContext(sg2d);
            rq.ensureCapacity(20);
            buf.putInt(DRAW_RECT);
            buf.putInt(x + sg2d.transX);
            buf.putInt(y + sg2d.transY);
            buf.putInt(width);
            buf.putInt(height);
        } finally {
            rq.unlock();
        }
    }

    public void fillRect(SunGraphics2D sg2d,
                         int x, int y, int width, int height)
    {
        rq.lock();
        try {
            validateContext(sg2d);
            rq.ensureCapacity(20);
            buf.putInt(FILL_RECT);
            buf.putInt(x + sg2d.transX);
            buf.putInt(y + sg2d.transY);
            buf.putInt(width);
            buf.putInt(height);
        } finally {
            rq.unlock();
        }
    }

    public void drawRoundRect(SunGraphics2D sg2d,
                              int x, int y, int width, int height,
                              int arcWidth, int arcHeight)
    {
        draw(sg2d, new RoundRectangle2D.Float(x, y, width, height,
                                              arcWidth, arcHeight));
    }

    public void fillRoundRect(SunGraphics2D sg2d,
                              int x, int y, int width, int height,
                              int arcWidth, int arcHeight)
    {
        fill(sg2d, new RoundRectangle2D.Float(x, y, width, height,
                                              arcWidth, arcHeight));
    }

    public void drawOval(SunGraphics2D sg2d,
                         int x, int y, int width, int height)
    {
        draw(sg2d, new Ellipse2D.Float(x, y, width, height));
    }

    public void fillOval(SunGraphics2D sg2d,
                         int x, int y, int width, int height)
    {
        fill(sg2d, new Ellipse2D.Float(x, y, width, height));
    }

    public void drawArc(SunGraphics2D sg2d,
                        int x, int y, int width, int height,
                        int startAngle, int arcAngle)
    {
        draw(sg2d, new Arc2D.Float(x, y, width, height,
                                   startAngle, arcAngle,
                                   Arc2D.OPEN));
    }

    public void fillArc(SunGraphics2D sg2d,
                        int x, int y, int width, int height,
                        int startAngle, int arcAngle)
    {
        fill(sg2d, new Arc2D.Float(x, y, width, height,
                                   startAngle, arcAngle,
                                   Arc2D.PIE));
    }

    protected void drawPoly(final SunGraphics2D sg2d,
                            final int[] xPoints, final int[] yPoints,
                            final int nPoints, final boolean isClosed)
    {
        if (xPoints == null || yPoints == null) {
            throw new NullPointerException("coordinate array");
        }
        if (xPoints.length < nPoints || yPoints.length < nPoints) {
            throw new ArrayIndexOutOfBoundsException("coordinate array");
        }

        if (nPoints < 2) {
            // render nothing
            return;
        } else if (nPoints == 2 && !isClosed) {
            // render a simple line
            drawLine(sg2d, xPoints[0], yPoints[0], xPoints[1], yPoints[1]);
            return;
        }

        rq.lock();
        try {
            validateContext(sg2d);

            int pointBytesRequired = nPoints * BYTES_PER_POLY_POINT;
            int totalBytesRequired = 20 + pointBytesRequired;

            if (totalBytesRequired <= buf.capacity()) {
                if (totalBytesRequired > buf.remaining()) {
                    // process the queue first and then enqueue the points
                    rq.flushNow();
                }
                buf.putInt(DRAW_POLY);
                // enqueue parameters
                buf.putInt(nPoints);
                buf.putInt(isClosed ? 1 : 0);
                buf.putInt(sg2d.transX);
                buf.putInt(sg2d.transY);
                // enqueue the points
                buf.put(xPoints, 0, nPoints);
                buf.put(yPoints, 0, nPoints);
            } else {
                // queue is too small to accommodate all points; perform the
                // operation directly on the queue flushing thread
                rq.flushAndInvokeNow(new Runnable() {
                    public void run() {
                        drawPoly(xPoints, yPoints,
                                 nPoints, isClosed,
                                 sg2d.transX, sg2d.transY);
                    }
                });
            }
        } finally {
            rq.unlock();
        }
    }

    protected abstract void drawPoly(int[] xPoints, int[] yPoints,
                                     int nPoints, boolean isClosed,
                                     int transX, int transY);

    public void drawPolyline(SunGraphics2D sg2d,
                             int[] xPoints, int[] yPoints,
                             int nPoints)
    {
        drawPoly(sg2d, xPoints, yPoints, nPoints, false);
    }

    public void drawPolygon(SunGraphics2D sg2d,
                            int[] xPoints, int[] yPoints,
                            int nPoints)
    {
        drawPoly(sg2d, xPoints, yPoints, nPoints, true);
    }

    public void fillPolygon(SunGraphics2D sg2d,
                            int[] xPoints, int[] yPoints,
                            int nPoints)
    {
        fill(sg2d, new Polygon(xPoints, yPoints, nPoints));
    }

    private class BufferedDrawHandler
        extends ProcessPath.DrawHandler
    {
        BufferedDrawHandler() {
            // these are bogus values; the caller will use validate()
            // to ensure that they are set properly prior to each usage
            super(0, 0, 0, 0);
        }

        /**
         * This method needs to be called prior to each draw/fillPath()
         * operation to ensure the clip bounds are up to date.
         */
        void validate(SunGraphics2D sg2d) {
            Region clip = sg2d.getCompClip();
            setBounds(clip.getLoX(), clip.getLoY(),
                      clip.getHiX(), clip.getHiY(),
                      sg2d.strokeHint);
        }

        /**
         * drawPath() support...
         */

        public void drawLine(int x1, int y1, int x2, int y2) {
            // assert rq.lock.isHeldByCurrentThread();
            rq.ensureCapacity(20);
            buf.putInt(DRAW_LINE);
            buf.putInt(x1);
            buf.putInt(y1);
            buf.putInt(x2);
            buf.putInt(y2);
        }

        public void drawPixel(int x, int y) {
            // assert rq.lock.isHeldByCurrentThread();
            rq.ensureCapacity(12);
            buf.putInt(DRAW_PIXEL);
            buf.putInt(x);
            buf.putInt(y);
        }

        /**
         * fillPath() support...
         */

        private int scanlineCount;
        private int scanlineCountIndex;
        private int remainingScanlines;

        private void resetFillPath() {
            buf.putInt(DRAW_SCANLINES);
            scanlineCountIndex = buf.position();
            buf.putInt(0);
            scanlineCount = 0;
            remainingScanlines = buf.remaining() / BYTES_PER_SCANLINE;
        }

        private void updateScanlineCount() {
            buf.putInt(scanlineCountIndex, scanlineCount);
        }

        /**
         * Called from fillPath() to indicate that we are about to
         * start issuing drawScanline() calls.
         */
        public void startFillPath() {
            rq.ensureCapacity(20); // to ensure room for at least a scanline
            resetFillPath();
        }

        public void drawScanline(int x1, int x2, int y) {
            if (remainingScanlines == 0) {
                updateScanlineCount();
                rq.flushNow();
                resetFillPath();
            }
            buf.putInt(x1);
            buf.putInt(x2);
            buf.putInt(y);
            scanlineCount++;
            remainingScanlines--;
        }

        /**
         * Called from fillPath() to indicate that we are done
         * issuing drawScanline() calls.
         */
        public void endFillPath() {
            updateScanlineCount();
        }
    }

    protected void drawPath(SunGraphics2D sg2d,
                            Path2D.Float p2df, int transx, int transy)
    {
        rq.lock();
        try {
            validateContext(sg2d);
            drawHandler.validate(sg2d);
            ProcessPath.drawPath(drawHandler, p2df, transx, transy);
        } finally {
            rq.unlock();
        }
    }

    protected void fillPath(SunGraphics2D sg2d,
                            Path2D.Float p2df, int transx, int transy)
    {
        rq.lock();
        try {
            validateContext(sg2d);
            drawHandler.validate(sg2d);
            drawHandler.startFillPath();
            ProcessPath.fillPath(drawHandler, p2df, transx, transy);
            drawHandler.endFillPath();
        } finally {
            rq.unlock();
        }
    }

    private native int fillSpans(RenderQueue rq, long buf,
                                 int pos, int limit,
                                 SpanIterator si, long iterator,
                                 int transx, int transy);

    protected void fillSpans(SunGraphics2D sg2d, SpanIterator si,
                             int transx, int transy)
    {
        rq.lock();
        try {
            validateContext(sg2d);
            rq.ensureCapacity(24); // so that we have room for at least a span
            int newpos = fillSpans(rq, buf.getAddress(),
                                   buf.position(), buf.capacity(),
                                   si, si.getNativeIterator(),
                                   transx, transy);
            buf.position(newpos);
        } finally {
            rq.unlock();
        }
    }

    public void fillParallelogram(SunGraphics2D sg2d,
                                  double ux1, double uy1,
                                  double ux2, double uy2,
                                  double x, double y,
                                  double dx1, double dy1,
                                  double dx2, double dy2)
    {
        rq.lock();
        try {
            validateContext(sg2d);
            rq.ensureCapacity(28);
            buf.putInt(FILL_PARALLELOGRAM);
            buf.putFloat((float) x);
            buf.putFloat((float) y);
            buf.putFloat((float) dx1);
            buf.putFloat((float) dy1);
            buf.putFloat((float) dx2);
            buf.putFloat((float) dy2);
        } finally {
            rq.unlock();
        }
    }

    public void drawParallelogram(SunGraphics2D sg2d,
                                  double ux1, double uy1,
                                  double ux2, double uy2,
                                  double x, double y,
                                  double dx1, double dy1,
                                  double dx2, double dy2,
                                  double lw1, double lw2)
    {
        rq.lock();
        try {
            validateContext(sg2d);
            rq.ensureCapacity(36);
            buf.putInt(DRAW_PARALLELOGRAM);
            buf.putFloat((float) x);
            buf.putFloat((float) y);
            buf.putFloat((float) dx1);
            buf.putFloat((float) dy1);
            buf.putFloat((float) dx2);
            buf.putFloat((float) dy2);
            buf.putFloat((float) lw1);
            buf.putFloat((float) lw2);
        } finally {
            rq.unlock();
        }
    }

    private class AAParallelogramPipe implements ParallelogramPipe {
        public void fillParallelogram(SunGraphics2D sg2d,
                                      double ux1, double uy1,
                                      double ux2, double uy2,
                                      double x, double y,
                                      double dx1, double dy1,
                                      double dx2, double dy2)
        {
            rq.lock();
            try {
                validateContextAA(sg2d);
                rq.ensureCapacity(28);
                buf.putInt(FILL_AAPARALLELOGRAM);
                buf.putFloat((float) x);
                buf.putFloat((float) y);
                buf.putFloat((float) dx1);
                buf.putFloat((float) dy1);
                buf.putFloat((float) dx2);
                buf.putFloat((float) dy2);
            } finally {
                rq.unlock();
            }
        }

        public void drawParallelogram(SunGraphics2D sg2d,
                                      double ux1, double uy1,
                                      double ux2, double uy2,
                                      double x, double y,
                                      double dx1, double dy1,
                                      double dx2, double dy2,
                                      double lw1, double lw2)
        {
            rq.lock();
            try {
                validateContextAA(sg2d);
                rq.ensureCapacity(36);
                buf.putInt(DRAW_AAPARALLELOGRAM);
                buf.putFloat((float) x);
                buf.putFloat((float) y);
                buf.putFloat((float) dx1);
                buf.putFloat((float) dy1);
                buf.putFloat((float) dx2);
                buf.putFloat((float) dy2);
                buf.putFloat((float) lw1);
                buf.putFloat((float) lw2);
            } finally {
                rq.unlock();
            }
        }
    }

    public void draw(SunGraphics2D sg2d, Shape s) {
        if (sg2d.strokeState == SunGraphics2D.STROKE_THIN) {
            if (s instanceof Polygon) {
                if (sg2d.transformState < SunGraphics2D.TRANSFORM_TRANSLATESCALE) {
                    Polygon p = (Polygon)s;
                    drawPolygon(sg2d, p.xpoints, p.ypoints, p.npoints);
                    return;
                }
            }
            Path2D.Float p2df;
            int transx, transy;
            if (sg2d.transformState <= SunGraphics2D.TRANSFORM_INT_TRANSLATE) {
                if (s instanceof Path2D.Float) {
                    p2df = (Path2D.Float)s;
                } else {
                    p2df = new Path2D.Float(s);
                }
                transx = sg2d.transX;
                transy = sg2d.transY;
            } else {
                p2df = new Path2D.Float(s, sg2d.transform);
                transx = 0;
                transy = 0;
            }
            drawPath(sg2d, p2df, transx, transy);
        } else if (sg2d.strokeState < SunGraphics2D.STROKE_CUSTOM) {
            ShapeSpanIterator si = LoopPipe.getStrokeSpans(sg2d, s);
            try {
                fillSpans(sg2d, si, 0, 0);
            } finally {
                si.dispose();
            }
        } else {
            fill(sg2d, sg2d.stroke.createStrokedShape(s));
        }
    }

    public void fill(SunGraphics2D sg2d, Shape s) {
        int transx, transy;

        if (sg2d.strokeState == SunGraphics2D.STROKE_THIN) {
            // Here we are able to use fillPath() for
            // high-quality fills.
            Path2D.Float p2df;
            if (sg2d.transformState <= SunGraphics2D.TRANSFORM_INT_TRANSLATE) {
                if (s instanceof Path2D.Float) {
                    p2df = (Path2D.Float)s;
                } else {
                    p2df = new Path2D.Float(s);
                }
                transx = sg2d.transX;
                transy = sg2d.transY;
            } else {
                p2df = new Path2D.Float(s, sg2d.transform);
                transx = 0;
                transy = 0;
            }
            fillPath(sg2d, p2df, transx, transy);
            return;
        }

        AffineTransform at;
        if (sg2d.transformState <= SunGraphics2D.TRANSFORM_INT_TRANSLATE) {
            // Transform (translation) will be done by FillSpans (we could
            // delegate to fillPolygon() here, but most hardware accelerated
            // libraries cannot handle non-convex polygons, so we will use
            // the FillSpans approach by default)
            at = null;
            transx = sg2d.transX;
            transy = sg2d.transY;
        } else {
            // Transform will be done by the PathIterator
            at = sg2d.transform;
            transx = transy = 0;
        }

        ShapeSpanIterator ssi = LoopPipe.getFillSSI(sg2d);
        try {
            // Subtract transx/y from the SSI clip to match the
            // (potentially untranslated) geometry fed to it
            Region clip = sg2d.getCompClip();
            ssi.setOutputAreaXYXY(clip.getLoX() - transx,
                                  clip.getLoY() - transy,
                                  clip.getHiX() - transx,
                                  clip.getHiY() - transy);
            ssi.appendPath(s.getPathIterator(at));
            fillSpans(sg2d, ssi, transx, transy);
        } finally {
            ssi.dispose();
        }
    }
}