/*
 * Copyright (c) 2007, 2010, 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.Color;
import java.awt.GradientPaint;
import java.awt.LinearGradientPaint;
import java.awt.MultipleGradientPaint;
import java.awt.MultipleGradientPaint.ColorSpaceType;
import java.awt.MultipleGradientPaint.CycleMethod;
import java.awt.Paint;
import java.awt.RadialGradientPaint;
import java.awt.TexturePaint;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import sun.awt.image.PixelConverter;
import sun.java2d.SunGraphics2D;
import sun.java2d.SurfaceData;
import sun.java2d.loops.CompositeType;
import sun.java2d.loops.SurfaceType;
import static sun.java2d.pipe.BufferedOpCodes.*;

import javax.tools.annotation.GenerateNativeHeader;

/* No native methods here, but the constants are needed in the supporting JNI code */
@GenerateNativeHeader
public class BufferedPaints {

    static void setPaint(RenderQueue rq, SunGraphics2D sg2d,
                         Paint paint, int ctxflags)
    {
        if (sg2d.paintState <= SunGraphics2D.PAINT_ALPHACOLOR) {
            setColor(rq, sg2d.pixel);
        } else {
            boolean useMask = (ctxflags & BufferedContext.USE_MASK) != 0;
            switch (sg2d.paintState) {
            case SunGraphics2D.PAINT_GRADIENT:
                setGradientPaint(rq, sg2d,
                                 (GradientPaint)paint, useMask);
                break;
            case SunGraphics2D.PAINT_LIN_GRADIENT:
                setLinearGradientPaint(rq, sg2d,
                                       (LinearGradientPaint)paint, useMask);
                break;
            case SunGraphics2D.PAINT_RAD_GRADIENT:
                setRadialGradientPaint(rq, sg2d,
                                       (RadialGradientPaint)paint, useMask);
                break;
            case SunGraphics2D.PAINT_TEXTURE:
                setTexturePaint(rq, sg2d,
                                (TexturePaint)paint, useMask);
                break;
            default:
                break;
            }
        }
    }

    static void resetPaint(RenderQueue rq) {
        // assert rq.lock.isHeldByCurrentThread();
        rq.ensureCapacity(4);
        RenderBuffer buf = rq.getBuffer();
        buf.putInt(RESET_PAINT);
    }

/****************************** Color support *******************************/

    private static void setColor(RenderQueue rq, int pixel) {
        // assert rq.lock.isHeldByCurrentThread();
        rq.ensureCapacity(8);
        RenderBuffer buf = rq.getBuffer();
        buf.putInt(SET_COLOR);
        buf.putInt(pixel);
    }

/************************* GradientPaint support ****************************/

    /**
     * Note: This code is factored out into a separate static method
     * so that it can be shared by both the Gradient and LinearGradient
     * implementations.  LinearGradient uses this code (for the
     * two-color sRGB case only) because it can be much faster than the
     * equivalent implementation that uses fragment shaders.
     *
     * We use OpenGL's texture coordinate generator to automatically
     * apply a smooth gradient (either cyclic or acyclic) to the geometry
     * being rendered.  This technique is almost identical to the one
     * described in the comments for BufferedPaints.setTexturePaint(),
     * except the calculations take place in one dimension instead of two.
     * Instead of an anchor rectangle in the TexturePaint case, we use
     * the vector between the two GradientPaint end points in our
     * calculations.  The generator uses a single plane equation that
     * takes the (x,y) location (in device space) of the fragment being
     * rendered to calculate a (u) texture coordinate for that fragment:
     *     u = Ax + By + Cz + Dw
     *
     * The gradient renderer uses a two-pixel 1D texture where the first
     * pixel contains the first GradientPaint color, and the second pixel
     * contains the second GradientPaint color.  (Note that we use the
     * GL_CLAMP_TO_EDGE wrapping mode for acyclic gradients so that we
     * clamp the colors properly at the extremes.)  The following diagram
     * attempts to show the layout of the texture containing the two
     * GradientPaint colors (C1 and C2):
     *
     *                        +-----------------+
     *                        |   C1   |   C2   |
     *                        |        |        |
     *                        +-----------------+
     *                      u=0  .25  .5   .75  1
     *
     * We calculate our plane equation constants (A,B,D) such that u=0.25
     * corresponds to the first GradientPaint end point in user space and
     * u=0.75 corresponds to the second end point.  This is somewhat
     * non-obvious, but since the gradient colors are generated by
     * interpolating between C1 and C2, we want the pure color at the
     * end points, and we will get the pure color only when u correlates
     * to the center of a texel.  The following chart shows the expected
     * color for some sample values of u (where C' is the color halfway
     * between C1 and C2):
     *
     *       u value      acyclic (GL_CLAMP)      cyclic (GL_REPEAT)
     *       -------      ------------------      ------------------
     *        -0.25              C1                       C2
     *         0.0               C1                       C'
     *         0.25              C1                       C1
     *         0.5               C'                       C'
     *         0.75              C2                       C2
     *         1.0               C2                       C'
     *         1.25              C2                       C1
     *
     * Original inspiration for this technique came from UMD's Agile2D
     * project (GradientManager.java).
     */
    private static void setGradientPaint(RenderQueue rq, AffineTransform at,
                                         Color c1, Color c2,
                                         Point2D pt1, Point2D pt2,
                                         boolean isCyclic, boolean useMask)
    {
        // convert gradient colors to IntArgbPre format
        PixelConverter pc = PixelConverter.ArgbPre.instance;
        int pixel1 = pc.rgbToPixel(c1.getRGB(), null);
        int pixel2 = pc.rgbToPixel(c2.getRGB(), null);

        // calculate plane equation constants
        double x = pt1.getX();
        double y = pt1.getY();
        at.translate(x, y);
        // now gradient point 1 is at the origin
        x = pt2.getX() - x;
        y = pt2.getY() - y;
        double len = Math.sqrt(x * x + y * y);
        at.rotate(x, y);
        // now gradient point 2 is on the positive x-axis
        at.scale(2*len, 1);
        // now gradient point 2 is at (0.5, 0)
        at.translate(-0.25, 0);
        // now gradient point 1 is at (0.25, 0), point 2 is at (0.75, 0)

        double p0, p1, p3;
        try {
            at.invert();
            p0 = at.getScaleX();
            p1 = at.getShearX();
            p3 = at.getTranslateX();
        } catch (java.awt.geom.NoninvertibleTransformException e) {
            p0 = p1 = p3 = 0.0;
        }

        // assert rq.lock.isHeldByCurrentThread();
        rq.ensureCapacityAndAlignment(44, 12);
        RenderBuffer buf = rq.getBuffer();
        buf.putInt(SET_GRADIENT_PAINT);
        buf.putInt(useMask ? 1 : 0);
        buf.putInt(isCyclic ? 1 : 0);
        buf.putDouble(p0).putDouble(p1).putDouble(p3);
        buf.putInt(pixel1).putInt(pixel2);
    }

    private static void setGradientPaint(RenderQueue rq,
                                         SunGraphics2D sg2d,
                                         GradientPaint paint,
                                         boolean useMask)
    {
        setGradientPaint(rq, (AffineTransform)sg2d.transform.clone(),
                         paint.getColor1(), paint.getColor2(),
                         paint.getPoint1(), paint.getPoint2(),
                         paint.isCyclic(), useMask);
    }

/************************** TexturePaint support ****************************/

    /**
     * We use OpenGL's texture coordinate generator to automatically
     * map the TexturePaint image to the geometry being rendered.  The
     * generator uses two separate plane equations that take the (x,y)
     * location (in device space) of the fragment being rendered to
     * calculate (u,v) texture coordinates for that fragment:
     *     u = Ax + By + Cz + Dw
     *     v = Ex + Fy + Gz + Hw
     *
     * Since we use a 2D orthographic projection, we can assume that z=0
     * and w=1 for any fragment.  So we need to calculate appropriate
     * values for the plane equation constants (A,B,D) and (E,F,H) such
     * that {u,v}=0 for the top-left of the TexturePaint's anchor
     * rectangle and {u,v}=1 for the bottom-right of the anchor rectangle.
     * We can easily make the texture image repeat for {u,v} values
     * outside the range [0,1] by specifying the GL_REPEAT texture wrap
     * mode.
     *
     * Calculating the plane equation constants is surprisingly simple.
     * We can think of it as an inverse matrix operation that takes
     * device space coordinates and transforms them into user space
     * coordinates that correspond to a location relative to the anchor
     * rectangle.  First, we translate and scale the current user space
     * transform by applying the anchor rectangle bounds.  We then take
     * the inverse of this affine transform.  The rows of the resulting
     * inverse matrix correlate nicely to the plane equation constants
     * we were seeking.
     */
    private static void setTexturePaint(RenderQueue rq,
                                        SunGraphics2D sg2d,
                                        TexturePaint paint,
                                        boolean useMask)
    {
        BufferedImage bi = paint.getImage();
        SurfaceData dstData = sg2d.surfaceData;
        SurfaceData srcData =
            dstData.getSourceSurfaceData(bi, SunGraphics2D.TRANSFORM_ISIDENT,
                                         CompositeType.SrcOver, null);
        boolean filter =
            (sg2d.interpolationType !=
             AffineTransformOp.TYPE_NEAREST_NEIGHBOR);

        // calculate plane equation constants
        AffineTransform at = (AffineTransform)sg2d.transform.clone();
        Rectangle2D anchor = paint.getAnchorRect();
        at.translate(anchor.getX(), anchor.getY());
        at.scale(anchor.getWidth(), anchor.getHeight());

        double xp0, xp1, xp3, yp0, yp1, yp3;
        try {
            at.invert();
            xp0 = at.getScaleX();
            xp1 = at.getShearX();
            xp3 = at.getTranslateX();
            yp0 = at.getShearY();
            yp1 = at.getScaleY();
            yp3 = at.getTranslateY();
        } catch (java.awt.geom.NoninvertibleTransformException e) {
            xp0 = xp1 = xp3 = yp0 = yp1 = yp3 = 0.0;
        }

        // assert rq.lock.isHeldByCurrentThread();
        rq.ensureCapacityAndAlignment(68, 12);
        RenderBuffer buf = rq.getBuffer();
        buf.putInt(SET_TEXTURE_PAINT);
        buf.putInt(useMask ? 1 : 0);
        buf.putInt(filter ? 1 : 0);
        buf.putLong(srcData.getNativeOps());
        buf.putDouble(xp0).putDouble(xp1).putDouble(xp3);
        buf.putDouble(yp0).putDouble(yp1).putDouble(yp3);
    }

/****************** Shared MultipleGradientPaint support ********************/

    /**
     * The maximum number of gradient "stops" supported by our native
     * fragment shader implementations.
     *
     * This value has been empirically determined and capped to allow
     * our native shaders to run on all shader-level graphics hardware,
     * even on the older, more limited GPUs.  Even the oldest Nvidia
     * hardware could handle 16, or even 32 fractions without any problem.
     * But the first-generation boards from ATI would fall back into
     * software mode (which is unusably slow) for values larger than 12;
     * it appears that those boards do not have enough native registers
     * to support the number of array accesses required by our gradient
     * shaders.  So for now we will cap this value at 12, but we can
     * re-evaluate this in the future as hardware becomes more capable.
     */
    public static final int MULTI_MAX_FRACTIONS = 12;

    /**
     * Helper function to convert a color component in sRGB space to
     * linear RGB space.  Copied directly from the
     * MultipleGradientPaintContext class.
     */
    public static int convertSRGBtoLinearRGB(int color) {
        float input, output;

        input = color / 255.0f;
        if (input <= 0.04045f) {
            output = input / 12.92f;
        } else {
            output = (float)Math.pow((input + 0.055) / 1.055, 2.4);
        }

        return Math.round(output * 255.0f);
    }

    /**
     * Helper function to convert a (non-premultiplied) Color in sRGB
     * space to an IntArgbPre pixel value, optionally in linear RGB space.
     * Based on the PixelConverter.ArgbPre.rgbToPixel() method.
     */
    private static int colorToIntArgbPrePixel(Color c, boolean linear) {
        int rgb = c.getRGB();
        if (!linear && ((rgb >> 24) == -1)) {
            return rgb;
        }
        int a = rgb >>> 24;
        int r = (rgb >> 16) & 0xff;
        int g = (rgb >>  8) & 0xff;
        int b = (rgb      ) & 0xff;
        if (linear) {
            r = convertSRGBtoLinearRGB(r);
            g = convertSRGBtoLinearRGB(g);
            b = convertSRGBtoLinearRGB(b);
        }
        int a2 = a + (a >> 7);
        r = (r * a2) >> 8;
        g = (g * a2) >> 8;
        b = (b * a2) >> 8;
        return ((a << 24) | (r << 16) | (g << 8) | (b));
    }

    /**
     * Converts the given array of Color objects into an int array
     * containing IntArgbPre pixel values.  If the linear parameter
     * is true, the Color values will be converted into a linear RGB
     * color space before being returned.
     */
    private static int[] convertToIntArgbPrePixels(Color[] colors,
                                                   boolean linear)
    {
        int[] pixels = new int[colors.length];
        for (int i = 0; i < colors.length; i++) {
            pixels[i] = colorToIntArgbPrePixel(colors[i], linear);
        }
        return pixels;
    }

/********************** LinearGradientPaint support *************************/

    /**
     * This method uses techniques that are nearly identical to those
     * employed in setGradientPaint() above.  The primary difference
     * is that at the native level we use a fragment shader to manually
     * apply the plane equation constants to the current fragment position
     * to calculate the gradient position in the range [0,1] (the native
     * code for GradientPaint does the same, except that it uses OpenGL's
     * automatic texture coordinate generation facilities).
     *
     * One other minor difference worth mentioning is that
     * setGradientPaint() calculates the plane equation constants
     * such that the gradient end points are positioned at 0.25 and 0.75
     * (for reasons discussed in the comments for that method).  In
     * contrast, for LinearGradientPaint we setup the equation constants
     * such that the gradient end points fall at 0.0 and 1.0.  The
     * reason for this difference is that in the fragment shader we
     * have more control over how the gradient values are interpreted
     * (depending on the paint's CycleMethod).
     */
    private static void setLinearGradientPaint(RenderQueue rq,
                                               SunGraphics2D sg2d,
                                               LinearGradientPaint paint,
                                               boolean useMask)
    {
        boolean linear =
            (paint.getColorSpace() == ColorSpaceType.LINEAR_RGB);
        Color[] colors = paint.getColors();
        int numStops = colors.length;
        Point2D pt1 = paint.getStartPoint();
        Point2D pt2 = paint.getEndPoint();
        AffineTransform at = paint.getTransform();
        at.preConcatenate(sg2d.transform);

        if (!linear && numStops == 2 &&
            paint.getCycleMethod() != CycleMethod.REPEAT)
        {
            // delegate to the optimized two-color gradient codepath
            boolean isCyclic =
                (paint.getCycleMethod() != CycleMethod.NO_CYCLE);
            setGradientPaint(rq, at,
                             colors[0], colors[1],
                             pt1, pt2,
                             isCyclic, useMask);
            return;
        }

        int cycleMethod = paint.getCycleMethod().ordinal();
        float[] fractions = paint.getFractions();
        int[] pixels = convertToIntArgbPrePixels(colors, linear);

        // calculate plane equation constants
        double x = pt1.getX();
        double y = pt1.getY();
        at.translate(x, y);
        // now gradient point 1 is at the origin
        x = pt2.getX() - x;
        y = pt2.getY() - y;
        double len = Math.sqrt(x * x + y * y);
        at.rotate(x, y);
        // now gradient point 2 is on the positive x-axis
        at.scale(len, 1);
        // now gradient point 1 is at (0.0, 0), point 2 is at (1.0, 0)

        float p0, p1, p3;
        try {
            at.invert();
            p0 = (float)at.getScaleX();
            p1 = (float)at.getShearX();
            p3 = (float)at.getTranslateX();
        } catch (java.awt.geom.NoninvertibleTransformException e) {
            p0 = p1 = p3 = 0.0f;
        }

        // assert rq.lock.isHeldByCurrentThread();
        rq.ensureCapacity(20 + 12 + (numStops*4*2));
        RenderBuffer buf = rq.getBuffer();
        buf.putInt(SET_LINEAR_GRADIENT_PAINT);
        buf.putInt(useMask ? 1 : 0);
        buf.putInt(linear  ? 1 : 0);
        buf.putInt(cycleMethod);
        buf.putInt(numStops);
        buf.putFloat(p0);
        buf.putFloat(p1);
        buf.putFloat(p3);
        buf.put(fractions);
        buf.put(pixels);
    }

/********************** RadialGradientPaint support *************************/

    /**
     * This method calculates six m** values and a focusX value that
     * are used by the native fragment shader.  These techniques are
     * based on a whitepaper by Daniel Rice on radial gradient performance
     * (attached to the bug report for 6521533).  One can refer to that
     * document for the complete set of formulas and calculations, but
     * the basic goal is to compose a transform that will convert an
     * (x,y) position in device space into a "u" value that represents
     * the relative distance to the gradient focus point.  The resulting
     * value can be used to look up the appropriate color by linearly
     * interpolating between the two nearest colors in the gradient.
     */
    private static void setRadialGradientPaint(RenderQueue rq,
                                               SunGraphics2D sg2d,
                                               RadialGradientPaint paint,
                                               boolean useMask)
    {
        boolean linear =
            (paint.getColorSpace() == ColorSpaceType.LINEAR_RGB);
        int cycleMethod = paint.getCycleMethod().ordinal();
        float[] fractions = paint.getFractions();
        Color[] colors = paint.getColors();
        int numStops = colors.length;
        int[] pixels = convertToIntArgbPrePixels(colors, linear);
        Point2D center = paint.getCenterPoint();
        Point2D focus = paint.getFocusPoint();
        float radius = paint.getRadius();

        // save original (untransformed) center and focus points
        double cx = center.getX();
        double cy = center.getY();
        double fx = focus.getX();
        double fy = focus.getY();

        // transform from gradient coords to device coords
        AffineTransform at = paint.getTransform();
        at.preConcatenate(sg2d.transform);
        focus = at.transform(focus, focus);

        // transform unit circle to gradient coords; we start with the
        // unit circle (center=(0,0), focus on positive x-axis, radius=1)
        // and then transform into gradient space
        at.translate(cx, cy);
        at.rotate(fx - cx, fy - cy);
        at.scale(radius, radius);

        // invert to get mapping from device coords to unit circle
        try {
            at.invert();
        } catch (Exception e) {
            at.setToScale(0.0, 0.0);
        }
        focus = at.transform(focus, focus);

        // clamp the focus point so that it does not rest on, or outside
        // of, the circumference of the gradient circle
        fx = Math.min(focus.getX(), 0.99);

        // assert rq.lock.isHeldByCurrentThread();
        rq.ensureCapacity(20 + 28 + (numStops*4*2));
        RenderBuffer buf = rq.getBuffer();
        buf.putInt(SET_RADIAL_GRADIENT_PAINT);
        buf.putInt(useMask ? 1 : 0);
        buf.putInt(linear  ? 1 : 0);
        buf.putInt(numStops);
        buf.putInt(cycleMethod);
        buf.putFloat((float)at.getScaleX());
        buf.putFloat((float)at.getShearX());
        buf.putFloat((float)at.getTranslateX());
        buf.putFloat((float)at.getShearY());
        buf.putFloat((float)at.getScaleY());
        buf.putFloat((float)at.getTranslateY());
        buf.putFloat((float)fx);
        buf.put(fractions);
        buf.put(pixels);
    }
}
