/*
 * Copyright (c) 2011, 2021, 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 com.sun.javafx.sg.prism;

import com.sun.prism.Graphics;
import com.sun.prism.Image;
import com.sun.prism.Texture;
import com.sun.prism.paint.Color;
import com.sun.scenario.effect.Color4f;
import com.sun.scenario.effect.DropShadow;
import com.sun.scenario.effect.Effect;
import com.sun.scenario.effect.InnerShadow;

/**
 */
class EffectUtil {
    // We must use a power-of-2 size so that we can get true CLAMP_TO_EDGE on all platforms
    private static final int TEX_SIZE = 256;

    private static Texture itex;
    private static Texture dtex;

    /**
     * If possible, uses an optimized codepath to render the an
     * effect (InnerShadow or DropShadow) on the given rectangular node
     * (NGRectangle, NGImageView, etc).  If successful, returns true;
     * otherwise returns false to indicate that the caller should fall
     * back on existing methods to render the effect.
     */
    static boolean renderEffectForRectangularNode(NGNode node,
                                                  Graphics g,
                                                  Effect effect,
                                                  float alpha, boolean aa,
                                                  float rx, float ry,
                                                  float rw, float rh)
    {
        if (!g.getTransformNoClone().is2D() && g.isDepthBuffer() && g.isDepthTest()) {
            // TODO: Both of our optimizations below rely on layering of
            // 2 primitives that are dispatched with x and y coordinates
            // that are calculated displacements of the rx,ry,rw,rh.
            // Given that we are doing the calculations with IEEE floating
            // point (even if we used double precision) we are going to
            // end up with cases where interpolating between the calculated
            // coordinates generates Z values that are 1 ulp above or below
            // the Z value that is calculated for the primitive and it will
            // be essentially random whether we see the primitive or the
            // shadow on a per-pixel basis.  In practice, there tends to be
            // a sharp cutover from one to the other along some horizontal
            // or vertical threshold rather than random noise, but we cannot
            // force the latter rendered objects to overwrite the previously
            // rendered objects if we have depth buffers on.
            // Note that a Z offset could be used to force layering, but
            // that has 2 issues:
            // - What Z offset do we use?  It is dependent on the precision
            //   of the depth buffer and also on the 3D transform and probably
            //   the camera and near/far clipping planes.  We can try to
            //   install a pre-translation (translate by N device units),
            //   but we still may choose a value that does not quite "bump"
            //   the Z coordinates far enough since floating point has a
            //   scaled mantissa.
            // - If the primitive turns around it will lose its inner shadow
            //   or be covered completly by the drop shadow.  Both results
            //   are inconsistent with the image-based results delivered by
            //   Decora.  This could also be adjusted by examining the facing
            //   of the primitive and then changing the direction we bump the
            //   shadow, but the simplest and most compatible solution is to
            //   just turn off the optimizations when we have a 3D depth buffer.
            // (RT-26982)
            return false;
        }
        if (effect instanceof InnerShadow && !aa) {
            // TODO: Handle AA, or at least case when rectangle is pixel aligned...
            // (RT-26982)
            InnerShadow shadow = (InnerShadow)effect;
            float radius = shadow.getRadius();
            if (radius > 0f &&
                radius < rw/2 &&
                radius < rh/2 &&
                shadow.getChoke() == 0f &&
                shadow.getShadowSourceInput() == null &&
                shadow.getContentInput() == null)
            {
                node.renderContent(g);
                EffectUtil.renderRectInnerShadow(g, shadow, alpha, rx, ry, rw, rh);
                return true;
            }
        } else if (effect instanceof DropShadow) {
            DropShadow shadow = (DropShadow)effect;
            float radius = shadow.getRadius();
            if (radius > 0f &&
                radius < rw/2 &&
                radius < rh/2 &&
                shadow.getSpread() == 0f &&
                shadow.getShadowSourceInput() == null &&
                shadow.getContentInput() == null)
            {
                EffectUtil.renderRectDropShadow(g, shadow, alpha, rx, ry, rw, rh);
                node.renderContent(g);
                return true;
            }
        }
        return false;
    }

    static void renderRectInnerShadow(Graphics g, InnerShadow shadow, float alpha,
                                      float rx, float ry, float rw, float rh)
    {
        if (itex == null || itex.isSurfaceLost()) {
            byte[] sdata = new byte[TEX_SIZE * TEX_SIZE];
            fillGaussian(sdata, TEX_SIZE, TEX_SIZE/2, shadow.getChoke(), true);
            Image img = Image.fromByteAlphaData(sdata, TEX_SIZE, TEX_SIZE);
            itex = g.getResourceFactory().createTexture(img,
                                                        Texture.Usage.STATIC,
                                                        Texture.WrapMode.CLAMP_TO_EDGE);
            // We use a power-of-2 size so that we can get true CLAMP_TO_EDGE on all platforms
            assert itex.getWrapMode() == Texture.WrapMode.CLAMP_TO_EDGE;
            itex.contentsUseful();
            itex.makePermanent();
        }
        float r = shadow.getRadius();
        int texsize = itex.getPhysicalWidth();
        int tcx1 = itex.getContentX();
        int tcx2 = tcx1 + itex.getContentWidth();
        float t1 = (tcx1 + 0.5f) / texsize;
        float t2 = (tcx2 - 0.5f) / texsize; // end of image
        float cx1 = rx;
        float cy1 = ry;
        float cx2 = rx + rw;
        float cy2 = ry + rh;
        float ox1 = cx1 + shadow.getOffsetX();
        float oy1 = cy1 + shadow.getOffsetY();
        float ox2 = ox1 + rw;
        float oy2 = oy1 + rh;
        g.setPaint(toPrismColor(shadow.getColor(), alpha));
        // TODO: The "outer edge" slices below overlap at the corners... (RT-26982)
        drawClippedTexture(g, itex,
                           cx1,   cy1,   cx2,   cy2,
                           cx1,   cy1,   cx2,   oy1-r,
                           t1,    t1,    t1,    t1); // outside above
        drawClippedTexture(g, itex,
                           cx1,   cy1,   cx2,   cy2,
                           ox1-r, oy1-r, ox1+r, oy1+r,
                           t1,    t1,    t2,    t2); // top-left corner
        drawClippedTexture(g, itex,
                           cx1,   cy1,   cx2,   cy2,
                           ox1+r, oy1-r, ox2-r, oy1+r,
                           t2,    t1,    t2,    t2); // top edge
        drawClippedTexture(g, itex,
                           cx1,   cy1,   cx2,   cy2,
                           ox2-r, oy1-r, ox2+r, oy1+r,
                           t2,    t1,    t1,    t2); // top-right corner
        drawClippedTexture(g, itex,
                           cx1,   cy1,   cx2,   cy2,
                           cx1,   oy1-r, ox1-r, oy2+r,
                           t1,    t1,    t1,    t1); // outside left
        drawClippedTexture(g, itex,
                           cx1,   cy1,   cx2,   cy2,
                           ox1-r, oy1+r, ox1+r, oy2-r,
                           t1,    t2,    t2,    t2); // left edge
        drawClippedTexture(g, itex,
                           cx1,   cy1,   cx2,   cy2,
                           ox2-r, oy1+r, ox2+r, oy2-r,
                           t2,    t2,    t1,    t2); // right edge
        drawClippedTexture(g, itex,
                           cx1,   cy1,   cx2,   cy2,
                           ox2+r, oy1-r, cx2,   oy2+r,
                           t1,    t1,    t1,    t1); // outside right
        drawClippedTexture(g, itex,
                           cx1,   cy1,   cx2,   cy2,
                           ox1-r, oy2-r, ox1+r, oy2+r,
                           t1,    t2,    t2,    t1); // bot-left corner
        drawClippedTexture(g, itex,
                           cx1,   cy1,   cx2,   cy2,
                           ox1+r, oy2-r, ox2-r, oy2+r,
                           t2,    t2,    t2,    t1); // bot edge
        drawClippedTexture(g, itex,
                           cx1,   cy1,   cx2,   cy2,
                           ox2-r, oy2-r, ox2+r, oy2+r,
                           t2,    t2,    t1,    t1); // bot-right corner
        drawClippedTexture(g, itex,
                           cx1,   cy1,   cx2,   cy2,
                           cx1,   oy2+r, cx2,   cy2,
                           t1,    t1,    t1,    t1); // outside below
    }

    static void drawClippedTexture(Graphics g, Texture tex,
                                   float cx1, float cy1, float cx2, float cy2,
                                   float ox1, float oy1, float ox2, float oy2,
                                   float tx1, float ty1, float tx2, float ty2)
    {
        if (ox1 >= ox2 || oy1 >= oy2 || cx1 >= cx2 || cy1 >= cy2) return;
        if (ox2 > cx1 && ox1 < cx2) {
            if (ox1 < cx1) {
                tx1 += (tx2 - tx1) * (cx1 - ox1) / (ox2 - ox1);
                ox1 = cx1;
            }
            if (ox2 > cx2) {
                tx2 -= (tx2 - tx1) * (ox2 - cx2) / (ox2 - ox1);
                ox2 = cx2;
            }
        } else {
            return;
        }
        if (oy2 > cy1 && oy1 < cy2) {
            if (oy1 < cy1) {
                ty1 += (ty2 - ty1) * (cy1 - oy1) / (oy2 - oy1);
                oy1 = cy1;
            }
            if (oy2 > cy2) {
                ty2 -= (ty2 - ty1) * (oy2 - cy2) / (oy2 - oy1);
                oy2 = cy2;
            }
        } else {
            return;
        }
        g.drawTextureRaw(tex, ox1, oy1, ox2, oy2, tx1, ty1, tx2, ty2);
    }

    static void renderRectDropShadow(Graphics g, DropShadow shadow, float alpha,
                                     float rx, float ry, float rw, float rh)
    {
        if (dtex == null || dtex.isSurfaceLost()) {
            byte[] sdata = new byte[TEX_SIZE * TEX_SIZE];
            fillGaussian(sdata, TEX_SIZE, TEX_SIZE / 2, shadow.getSpread(), false);
            //fillTestPattern(sdata, imgsize);
            Image img = Image.fromByteAlphaData(sdata, TEX_SIZE, TEX_SIZE);
            dtex = g.getResourceFactory().createTexture(img,
                                                        Texture.Usage.STATIC,
                                                        Texture.WrapMode.CLAMP_TO_EDGE);
            // We use a power-of-2 size so that we can get true CLAMP_TO_EDGE on all platforms
            assert dtex.getWrapMode() == Texture.WrapMode.CLAMP_TO_EDGE;
            dtex.contentsUseful();
            dtex.makePermanent();
        }
        float r = shadow.getRadius();
        int texsize = dtex.getPhysicalWidth();
        int cx1 = dtex.getContentX();
        int cx2 = cx1 + dtex.getContentWidth();
        float t1 = (cx1 + 0.5f) / texsize;
        float t2 = (cx2 - 0.5f) / texsize; // end of image
        float x1 = rx + shadow.getOffsetX();
        float y1 = ry + shadow.getOffsetY();
        float x2 = x1 + rw;
        float y2 = y1 + rh;
        g.setPaint(toPrismColor(shadow.getColor(), alpha));
        g.drawTextureRaw(dtex,
                         x1-r, y1-r, x1+r, y1+r,
                         t1,   t1,   t2,   t2); // top-left corner
        g.drawTextureRaw(dtex,
                         x2-r, y1-r, x2+r, y1+r,
                         t2,   t1,   t1,   t2); // top-right corner
        g.drawTextureRaw(dtex,
                         x2-r, y2-r, x2+r, y2+r,
                         t2,   t2,   t1,   t1); // bot-right corner
        g.drawTextureRaw(dtex,
                         x1-r, y2-r, x1+r, y2+r,
                         t1,   t2,   t2,   t1); // bot-left corner
        g.drawTextureRaw(dtex,
                         x1+r, y1+r, x2-r, y2-r,
                         t2,   t2,   t2,   t2); // center section
        g.drawTextureRaw(dtex,
                         x1-r, y1+r, x1+r, y2-r,
                         t1,   t2,   t2,   t2); // left edge
        g.drawTextureRaw(dtex,
                         x2-r, y1+r, x2+r, y2-r,
                         t2,   t2,   t1,   t2); // right edge
        g.drawTextureRaw(dtex,
                         x1+r, y1-r, x2-r, y1+r,
                         t2,   t1,   t2,   t2); // top edge
        g.drawTextureRaw(dtex,
                         x1+r, y2-r, x2-r, y2+r,
                         t2,   t2,   t2,   t1); // bottom edge
    }

    private static void fillGaussian(byte[] pixels, int dim,
                                     float r, float spread,
                                     boolean inner)
    {
        // Only works right if w==r and h==r
        float sigma = r / 3;
        float sigma22 = 2 * sigma * sigma;
        if (sigma22 < Float.MIN_VALUE) {
            // Avoid divide by 0 below (can generate NaN values)
            sigma22 = Float.MIN_VALUE;
        }
        // Really should just be new float[r]
        float kvals[] = new float[dim];
        int center = (dim+1)/2;
        float total = 0.0f;
        for (int i = 0; i < kvals.length; i++) {
            int d = center - i;
            total += (float) Math.exp(-(d * d) / sigma22);
            kvals[i] = total;
        }
//        total += (kvals[kvals.length-1] - total) * spread;
        for (int i = 0; i < kvals.length; i++) {
            kvals[i] /= total;
        }
        for (int y = 0; y < dim; y++) {
            for (int x = 0; x < dim; x++) {
                float v = kvals[y] * kvals[x];
                if (inner) {
                    // For inner shadow, invert
                    v = 1.0f - v;
                }
                int a = (int) (v * 255);
                //System.err.println(x + " " + y + " " + v + " " + kvals[x] + " " + kvals[y]);
                if (a < 0) a = 0; else if (a > 255) a = 255;
                pixels[y*dim+x] = (byte)a;
           }
        }
    }

    private static Color toPrismColor(Color4f decoraColor, float alpha) {
        float r = decoraColor.getRed();
        float g = decoraColor.getGreen();
        float b = decoraColor.getBlue();
        float a = decoraColor.getAlpha() * alpha;
        return new Color(r, g, b, a);
    }

    private EffectUtil() {
    }
}
