/*
 * Copyright (c) 2010, 2024, 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.xr;

import java.awt.AlphaComposite;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.Transparency;
import java.awt.geom.AffineTransform;
import java.awt.image.ColorModel;
import java.awt.image.DirectColorModel;
import java.awt.image.Raster;

import sun.awt.SunHints;
import sun.awt.SunToolkit;
import sun.awt.X11ComponentPeer;
import sun.awt.image.PixelConverter;
import sun.font.FontManagerNativeLibrary;
import sun.java2d.InvalidPipeException;
import sun.java2d.SunGraphics2D;
import sun.java2d.SurfaceData;
import sun.java2d.SurfaceDataProxy;
import sun.java2d.loops.CompositeType;
import sun.java2d.loops.MaskFill;
import sun.java2d.loops.RenderLoops;
import sun.java2d.loops.SurfaceType;
import sun.java2d.loops.XORComposite;
import sun.java2d.pipe.PixelToShapeConverter;
import sun.java2d.pipe.Region;
import sun.java2d.pipe.ShapeDrawPipe;
import sun.java2d.pipe.TextPipe;
import sun.java2d.pipe.ValidatePipe;
import sun.java2d.x11.XSurfaceData;

public abstract class XRSurfaceData extends XSurfaceData {
    X11ComponentPeer peer;
    XRGraphicsConfig graphicsConfig;
    XRBackend renderQueue;

    private RenderLoops solidloops;

    protected int depth;

    private static native void initIDs();

    protected native void XRInitSurface(int depth, int width, int height,
                                        long drawable, int pictFormat);

    native void initXRPicture(long xsdo, int pictForm);

    native void freeXSDOPicture(long xsdo);

    public static final String DESC_BYTE_A8_X11 = "Byte A8 Pixmap";
    public static final String DESC_INT_RGB_X11 = "Integer RGB Pixmap";
    public static final String DESC_INT_ARGB_X11 = "Integer ARGB-Pre Pixmap";

    public static final SurfaceType
        ByteA8X11 = SurfaceType.ByteGray.deriveSubType(DESC_BYTE_A8_X11);
    public static final SurfaceType
        IntRgbX11 = SurfaceType.IntRgb.deriveSubType(DESC_INT_RGB_X11,
                                              PixelConverter.ArgbPre.instance);
    public static final SurfaceType
        IntArgbPreX11 = SurfaceType.IntArgbPre.deriveSubType(DESC_INT_ARGB_X11);

    @Override
    public Raster getRaster(int x, int y, int w, int h) {
        throw new InternalError("not implemented yet");
    }

    protected XRRenderer xrpipe;
    protected PixelToShapeConverter xrtxpipe;
    protected TextPipe xrtextpipe;
    protected XRDrawImage xrDrawImage;

    protected ShapeDrawPipe aaShapePipe;
    protected PixelToShapeConverter aaPixelToShapeConv;

    public static void initXRSurfaceData() {
        if (!isX11SurfaceDataInitialized()) {
            FontManagerNativeLibrary.load();
            initIDs();
            XRPMBlitLoops.register();
            XRMaskFill.register();
            XRMaskBlit.register();

            setX11SurfaceDataInitialized();
        }
    }

    /**
     * Synchronized accessor method for isDrawableValid.
     */
    protected boolean isXRDrawableValid() {
        try {
            SunToolkit.awtLock();
            return isDrawableValid();
        } finally {
            SunToolkit.awtUnlock();
        }
    }

    @Override
    public SurfaceDataProxy makeProxyFor(SurfaceData srcData) {
        return XRSurfaceDataProxy.createProxy(srcData, graphicsConfig);
    }

    @Override
    public void validatePipe(SunGraphics2D sg2d) {
        TextPipe textpipe;
        boolean validated = false;

        /*
         * The textpipe for now can't handle TexturePaint when extra-alpha is
         * specified nore XOR mode
         */
        if ((textpipe = getTextPipe(sg2d)) == null)
        {
            super.validatePipe(sg2d);
            textpipe = sg2d.textpipe;
            validated = true;
        }

        PixelToShapeConverter txPipe = null;
        XRRenderer nonTxPipe = null;

        /*
         * TODO: Can we rely on the GC for ARGB32 surfaces?
         */
        if (sg2d.antialiasHint != SunHints.INTVAL_ANTIALIAS_ON) {
            if (sg2d.paintState <= SunGraphics2D.PAINT_ALPHACOLOR) {
                if (sg2d.compositeState <= SunGraphics2D.COMP_XOR) {
                    txPipe = xrtxpipe;
                    nonTxPipe = xrpipe;
                }
            } else if (sg2d.compositeState <= SunGraphics2D.COMP_ALPHA) {
                if (XRPaints.isValid(sg2d)) {
                    txPipe = xrtxpipe;
                    nonTxPipe = xrpipe;
                }
                // custom paints handled by super.validatePipe() below
            }
        }

        if (txPipe != null) {
            if (sg2d.transformState >= SunGraphics2D.TRANSFORM_TRANSLATESCALE) {
                sg2d.drawpipe = txPipe;
                sg2d.fillpipe = txPipe;
            } else if (sg2d.strokeState != SunGraphics2D.STROKE_THIN) {
                sg2d.drawpipe = txPipe;
                sg2d.fillpipe = nonTxPipe;
            } else {
                sg2d.drawpipe = nonTxPipe;
                sg2d.fillpipe = nonTxPipe;
            }
            sg2d.shapepipe = nonTxPipe;
        } else {
            if (!validated) {
                super.validatePipe(sg2d);
            }
        }

        // install the text pipe based on our earlier decision
        sg2d.textpipe = textpipe;

        // always override the image pipe with the specialized XRender pipe
        sg2d.imagepipe = xrDrawImage;
    }

    protected TextPipe getTextPipe(SunGraphics2D sg2d) {
        boolean supportedPaint = sg2d.compositeState <= SunGraphics2D.COMP_ALPHA
                && (sg2d.paintState <= SunGraphics2D.PAINT_ALPHACOLOR || sg2d.composite == null);

        boolean supportedCompOp = false;
        if (sg2d.composite instanceof AlphaComposite) {
            int compRule = ((AlphaComposite) sg2d.composite).getRule();
            supportedCompOp = XRUtils.isMaskEvaluated(XRUtils.j2dAlphaCompToXR(compRule))
                    || (compRule == AlphaComposite.SRC
                                && sg2d.paintState <= SunGraphics2D.PAINT_ALPHACOLOR);
        }

        return (supportedPaint && supportedCompOp) ? xrtextpipe : null;
    }

    @Override
    protected MaskFill getMaskFill(SunGraphics2D sg2d) {
        AlphaComposite aComp = null;
        if (sg2d.composite instanceof AlphaComposite alphaComposite) {
            aComp = alphaComposite;
        }

        boolean supportedPaint = sg2d.paintState <= SunGraphics2D.PAINT_ALPHACOLOR
                || XRPaints.isValid(sg2d);

        boolean supportedCompOp = false;
        if(aComp != null) {
            int rule = aComp.getRule();
            supportedCompOp = XRUtils.isMaskEvaluated(XRUtils.j2dAlphaCompToXR(rule));
        }

        return (supportedPaint && supportedCompOp) ?  super.getMaskFill(sg2d) : null;
    }

    @Override
    public RenderLoops getRenderLoops(SunGraphics2D sg2d) {
        if (sg2d.paintState <= SunGraphics2D.PAINT_ALPHACOLOR &&
            sg2d.compositeState <= SunGraphics2D.COMP_ALPHA)
        {
            return solidloops;
        }

        return super.getRenderLoops(sg2d);
    }

    @Override
    public GraphicsConfiguration getDeviceConfiguration() {
        return graphicsConfig;
    }

    /**
     * Method for instantiating a Window SurfaceData
     */
    public static XRWindowSurfaceData createData(X11ComponentPeer peer) {
        XRGraphicsConfig gc = getGC(peer);
        return new XRWindowSurfaceData(peer, gc, gc.getSurfaceType());
    }

    /**
     * Method for instantiating a Pixmap SurfaceData (offscreen).
     * If the surface * is opaque a 24-bit/RGB surface is chosen,
     * otherwise a 32-bit ARGB surface.
     */
    public static XRPixmapSurfaceData createData(XRGraphicsConfig gc,
                                                 int width, int height,
                                                 ColorModel cm, Image image,
                                                 long drawable,
                                                 int transparency,
                                                 boolean isTexture) {
        int depth;
        // If we have a 32 bit color model for the window it needs
        // alpha to support translucency of the window so we need
        //  to upgrade what was requested for the surface.
        if (gc.getColorModel().getPixelSize() == 32) {
           depth = 32;
           transparency = Transparency.TRANSLUCENT;
        } else {
            depth = transparency > Transparency.OPAQUE ? 32 : 24;
        }

        if (depth == 24) {
            cm = new DirectColorModel(depth,
                                      0x00FF0000, 0x0000FF00, 0x000000FF);
        } else {
            cm = new DirectColorModel(depth, 0x00FF0000, 0x0000FF00,
                                      0x000000FF, 0xFF000000);
        }

        return new XRPixmapSurfaceData
            (gc, width, height, image, getPixmapSurfaceType(transparency),
             cm, drawable, transparency,
             XRUtils.getPictureFormatForTransparency(transparency), depth, isTexture);
    }

    protected XRSurfaceData(X11ComponentPeer peer, XRGraphicsConfig gc,
        SurfaceType sType, ColorModel cm, int depth, int transparency)
    {
        super(sType, cm);
        this.peer = peer;
        this.graphicsConfig = gc;
        this.solidloops = graphicsConfig.getSolidLoops(sType);
        this.depth = depth;
        initOps(peer, graphicsConfig, depth);
        setBlitProxyCache(gc.getSurfaceDataProxyCache());
    }

    protected XRSurfaceData(XRBackend renderQueue) {
        super(XRSurfaceData.IntRgbX11,
              new DirectColorModel(24, 0x00FF0000, 0x0000FF00, 0x000000FF));
        this.renderQueue = renderQueue;
    }

    /**
     * Inits the XRender-data-structures which belong to the XRSurfaceData.
     *
     * @param pictureFormat
     */
    public void initXRender(int pictureFormat) {
        try {
            SunToolkit.awtLock();
            initXRPicture(getNativeOps(), pictureFormat);
            renderQueue = XRCompositeManager.getInstance(this).getBackend();
            maskBuffer = XRCompositeManager.getInstance(this);
        } catch (Throwable ex) {
            ex.printStackTrace();
        } finally {
            SunToolkit.awtUnlock();
        }
    }

    public static XRGraphicsConfig getGC(X11ComponentPeer peer) {
        if (peer != null) {
            return (XRGraphicsConfig) peer.getGraphicsConfiguration();
        } else {
            GraphicsEnvironment env =
                GraphicsEnvironment.getLocalGraphicsEnvironment();
            GraphicsDevice gd = env.getDefaultScreenDevice();
            return (XRGraphicsConfig) gd.getDefaultConfiguration();
        }
    }

    /**
     * Returns a boolean indicating whether or not a copyArea from the given
     * rectangle source coordinates might be incomplete and result in X11
     * GraphicsExposure events being generated from XCopyArea. This method
     * allows the SurfaceData copyArea method to determine if it needs to set
     * the GraphicsExposures attribute of the X11 GC to True or False to receive
     * or avoid the events.
     *
     * @return true if there is any chance that an XCopyArea from the given
     *         source coordinates could produce any X11 Exposure events.
     */
    public abstract boolean canSourceSendExposures(int x, int y, int w, int h);

    /**
     * CopyArea is implemented using the "old" X11 GC, therefore clip and
     * needExposures have to be validated against that GC. Pictures and GCs
     * don't share state.
     */
    public void validateCopyAreaGC(Region gcClip, boolean needExposures) {
        if (validatedGCClip != gcClip) {
            if (gcClip != null)
                renderQueue.setGCClipRectangles(xgc, gcClip);
            validatedGCClip = gcClip;
        }

        if (validatedExposures != needExposures) {
            validatedExposures = needExposures;
            renderQueue.setGCExposures(xgc, needExposures);
        }

        if (validatedXorComp != null) {
            renderQueue.setGCMode(xgc, true);
            renderQueue.setGCForeground(xgc, validatedGCForegroundPixel);
            validatedXorComp = null;
        }
    }

    @Override
    public boolean copyArea(SunGraphics2D sg2d, int x, int y, int w, int h,
                            int dx, int dy) {
        if (xrpipe == null) {
            if (!isXRDrawableValid()) {
                return true;
            }
            makePipes();
        }
        CompositeType comptype = sg2d.imageComp;
        if (CompositeType.SrcOverNoEa.equals(comptype) ||
             CompositeType.SrcNoEa.equals(comptype))
        {
            try {
                SunToolkit.awtLock();
                boolean needExposures = canSourceSendExposures(x, y, w, h);
                validateCopyAreaGC(sg2d.getCompClip(), needExposures);
                renderQueue.copyArea(xid, xid, xgc, x, y, w, h, x + dx, y + dy);
            } finally {
                SunToolkit.awtUnlock();
            }
            return true;
        }
        return false;
    }

    /**
     * Returns the XRender SurfaceType which is able to fulfill the specified
     * transparency requirement.
     */
    public static SurfaceType getPixmapSurfaceType(int transparency) {
        SurfaceType sType = null;

        switch (transparency) {
        case Transparency.OPAQUE:
            sType = XRSurfaceData.IntRgbX11;
            break;

        case Transparency.BITMASK:
        case Transparency.TRANSLUCENT:
            sType = XRSurfaceData.IntArgbPreX11;
            break;
        }

        return sType;
    }

    @Override
    public void invalidate() {
        if (isValid()) {
            setInvalid();
            super.invalidate();
        }
    }

    private long xgc; // GC is still used for copyArea
    private int validatedGCForegroundPixel = 0;
    private XORComposite validatedXorComp;
    private int xid;
    public int picture;
    public XRCompositeManager maskBuffer;

    private Region validatedClip;
    private Region validatedGCClip;
    private boolean validatedExposures = true;

    boolean transformInUse = false;
    AffineTransform validatedSourceTransform = new AffineTransform();
    AffineTransform staticSrcTx = null;
    int validatedRepeat = XRUtils.RepeatNone;
    int validatedFilter = XRUtils.FAST;

    /**
     * Validates an XRSurfaceData when used as source. Note that the clip is
     * applied when used as source as well as destination.
     */
    void validateAsSource(AffineTransform sxForm, int repeat, int filter) {

        if (validatedClip != null) {
            validatedClip = null;
            renderQueue.setClipRectangles(picture, null);
        }

        if (validatedRepeat != repeat && repeat != -1) {
            validatedRepeat = repeat;
            renderQueue.setPictureRepeat(picture, repeat);
        }

        if (sxForm == null) {
            if (transformInUse) {
                validatedSourceTransform.setToIdentity();
                renderQueue.setPictureTransform(picture,
                                                validatedSourceTransform);
                transformInUse = false;
            }
        } else if (!transformInUse ||
                   (transformInUse && !sxForm.equals(validatedSourceTransform))) {

            validatedSourceTransform.setTransform(sxForm.getScaleX(),
                                                  sxForm.getShearY(),
                                                  sxForm.getShearX(),
                                                  sxForm.getScaleY(),
                                                  sxForm.getTranslateX(),
                                                  sxForm.getTranslateY());

            AffineTransform srcTransform = validatedSourceTransform;
            if(staticSrcTx != null) {
                // Apply static transform set when used as texture or gradient.
                // Create a copy to not modify validatedSourceTransform as
                // this would confuse the validation logic.
                srcTransform = new AffineTransform(validatedSourceTransform);
                srcTransform.preConcatenate(staticSrcTx);
            }

            renderQueue.setPictureTransform(picture, srcTransform);
            transformInUse = true;
        }

        if (filter != validatedFilter && filter != -1) {
            renderQueue.setFilter(picture, filter);
            validatedFilter = filter;
        }
    }

    /**
     * Validates the Surface when used as destination.
     */
    public void validateAsDestination(SunGraphics2D sg2d, Region clip) {
        if (!isValid()) {
            throw new InvalidPipeException("bounds changed");
        }

        boolean updateGCClip = false;
        if (clip != validatedClip) {
            renderQueue.setClipRectangles(picture, clip);
            validatedClip = clip;
            updateGCClip = true;
        }

        if (sg2d != null && sg2d.compositeState == SunGraphics2D.COMP_XOR) {
            if (validatedXorComp != sg2d.getComposite()) {
                validatedXorComp = (XORComposite) sg2d.getComposite();
                renderQueue.setGCMode(xgc, false);
            }

            // validate pixel
            int pixel = sg2d.pixel;
            if (validatedGCForegroundPixel != pixel) {
                int xorpixelmod = validatedXorComp.getXorPixel();
                renderQueue.setGCForeground(xgc, pixel ^ xorpixelmod);
                validatedGCForegroundPixel = pixel;
            }

            if (updateGCClip) {
                renderQueue.setGCClipRectangles(xgc, clip);
            }
        }
    }

    public synchronized void makePipes() { /*
                                            * TODO: Why is this synchronized,
                                            * but access not?
                                            */
        if (xrpipe == null) {
            try {
                SunToolkit.awtLock();
                xgc = XCreateGC(getNativeOps());

                xrpipe = new XRRenderer(maskBuffer.getMaskBuffer());
                xrtxpipe = new PixelToShapeConverter(xrpipe);
                xrtextpipe = maskBuffer.getTextRenderer();
                xrDrawImage = new XRDrawImage();

            } finally {
                SunToolkit.awtUnlock();
            }
        }
    }

    public static final class XRWindowSurfaceData extends XRSurfaceData {

        protected final int scale;

        public XRWindowSurfaceData(X11ComponentPeer peer,
                                   XRGraphicsConfig gc, SurfaceType sType) {
            super(peer, gc, sType, peer.getColorModel(),
                  peer.getColorModel().getPixelSize(), Transparency.OPAQUE);

            this.scale = gc.getScale();

            if (isXRDrawableValid()) {
                // If we have a 32 bit color model for the window it needs
                // alpha to support translucency of the window so we need
                // to get the ARGB32 XRender picture format else for
                // 24 bit colormodel we need RGB24 or OPAQUE pictureformat.
                if (peer.getColorModel().getPixelSize() == 32) {
                     initXRender(XRUtils.
                      getPictureFormatForTransparency(Transparency.TRANSLUCENT));
                 }
                 else {
                     initXRender(XRUtils.
                       getPictureFormatForTransparency(Transparency.OPAQUE));
                 }
                makePipes();
            }
        }

        @Override
        public SurfaceData getReplacement() {
            return peer.getSurfaceData();
        }

        @Override
        public Rectangle getBounds() {
            Rectangle r = peer.getBounds();
            r.x = r.y = 0;
            r.width *= scale;
            r.height *= scale;
            return r;
        }

        @Override
        public boolean canSourceSendExposures(int x, int y, int w, int h) {
            return true;
        }

        /**
         * Returns destination Component associated with this SurfaceData.
         */
        @Override
        public Object getDestination() {
            return peer.getTarget();
        }

       @Override
       public void invalidate() {
           try {
               SunToolkit.awtLock();
               freeXSDOPicture(getNativeOps());
           }finally {
               SunToolkit.awtUnlock();
           }

           super.invalidate();
       }

        @Override
        public double getDefaultScaleX() {
            return scale;
        }

        @Override
        public double getDefaultScaleY() {
            return scale;
        }
    }

    public static final class XRInternalSurfaceData extends XRSurfaceData {
        public XRInternalSurfaceData(XRBackend renderQueue, int pictXid) {
          super(renderQueue);
          this.picture = pictXid;
          this.transformInUse = false;
        }

        @Override
        public boolean canSourceSendExposures(int x, int y, int w, int h) {
            return false;
        }

        @Override
        public Rectangle getBounds() {
            return null;
        }

        @Override
        public Object getDestination() {
            return null;
        }

        @Override
        public SurfaceData getReplacement() {
            return null;
        }
    }

    public static final class XRPixmapSurfaceData extends XRSurfaceData {
        Image offscreenImage;
        int width;
        int height;
        int transparency;
        private final int scale;

        public XRPixmapSurfaceData(XRGraphicsConfig gc, int width, int height,
                                   Image image, SurfaceType sType,
                                   ColorModel cm, long drawable,
                                   int transparency, int pictFormat,
                                   int depth, boolean isTexture) {
            super(null, gc, sType, cm, depth, transparency);
            this.scale = isTexture ? 1 : gc.getDevice().getScaleFactor();
            this.width = width * scale;
            this.height = height * scale;
            offscreenImage = image;
            this.transparency = transparency;
            initSurface(depth, this.width, this.height, drawable, pictFormat);

            initXRender(pictFormat);
            makePipes();
        }

        public void initSurface(int depth, int width, int height,
                                long drawable, int pictFormat) {
            try {
                SunToolkit.awtLock();
                XRInitSurface(depth, width, height, drawable, pictFormat);
            } finally {
                SunToolkit.awtUnlock();
            }
        }

        @Override
        public SurfaceData getReplacement() {
            return restoreContents(offscreenImage);
        }

        /**
         * Need this since the surface data is created with the color model of
         * the target GC, which is always opaque. But in SunGraphics2D.blitSD we
         * choose loops based on the transparency on the source SD, so it could
         * choose wrong loop (blit instead of blitbg, for example).
         */
        @Override
        public int getTransparency() {
            return transparency;
        }

        @Override
        public Rectangle getBounds() {
            return new Rectangle(width, height);
        }

        @Override
        public boolean canSourceSendExposures(int x, int y, int w, int h) {
            return (x < 0 || y < 0 || (x + w) > width || (y + h) > height);
        }

        @Override
        public void flush() {
            /*
             * We need to invalidate the surface before disposing the native
             * Drawable and Picture. This way if an application tries to render
             * to an already flushed XRSurfaceData, we will notice in the
             * validate() method above that it has been invalidated, and we will
             * avoid using those native resources that have already been
             * disposed.
             */
            invalidate();
            flushNativeSurface();
        }

        /**
         * Returns destination Image associated with this SurfaceData.
         */
        @Override
        public Object getDestination() {
            return offscreenImage;
        }

        @Override
        public double getDefaultScaleX() {
            return scale;
        }

        @Override
        public double getDefaultScaleY() {
            return scale;
        }
    }

    public long getGC() {
        return xgc;
    }

    public static final class LazyPipe extends ValidatePipe {
        @Override
        public boolean validate(SunGraphics2D sg2d) {
            XRSurfaceData xsd = (XRSurfaceData) sg2d.surfaceData;
            if (!xsd.isXRDrawableValid()) {
                return false;
            }
            xsd.makePipes();
            return super.validate(sg2d);
        }
    }

    public int getPicture() {
        return picture;
    }

    public int getXid() {
        return xid;
    }

    public XRGraphicsConfig getGraphicsConfig() {
        return graphicsConfig;
    }

    public void setStaticSrcTx(AffineTransform staticSrcTx) {
        this.staticSrcTx = staticSrcTx;
    }
}
