/*
 * Copyright (c) 2007, 2020, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package com.sun.openpisces;

import com.sun.javafx.geom.PathConsumer2D;
import java.util.Arrays;

/**
 */
public final class Renderer implements PathConsumer2D {

    private final class ScanlineIterator {

        private int[] crossings;
        private int[] edgePtrs;
        private int edgeCount;

        // crossing bounds. The bounds are not necessarily tight (the scan line
        // at minY, for example, might have no crossings). The x bounds will
        // be accumulated as crossings are computed.
        private int nextY;

        private static final int INIT_CROSSINGS_SIZE = 10;

        private ScanlineIterator() {
            crossings = new int[INIT_CROSSINGS_SIZE];
            edgePtrs = new int[INIT_CROSSINGS_SIZE];
            reset();
        }

        public void reset() {
            // We don't care if we clip some of the line off with ceil, since
            // no scan line crossings will be eliminated (in fact, the ceil is
            // the y of the first scan line crossing).
            nextY = sampleRowMin;
            edgeCount = 0;
        }

        private int next() {
            // TODO: make function that convert from y value to bucket idx?
            // (RT-26922)
            int cury = nextY++;
            int bucket = cury - boundsMinY;
            int count = this.edgeCount;
            int ptrs[] = this.edgePtrs;
            float edges[] = Renderer.this.edges;
            int bucketcount = edgeBuckets[bucket*2 + 1];
            if ((bucketcount & 0x1) != 0) {
                int newCount = 0;
                for (int i = 0; i < count; i++) {
                    int ecur = ptrs[i];
                    if (edges[ecur+YMAX] > cury) {
                        ptrs[newCount++] = ecur;
                    }
                }
                count = newCount;
            }
            ptrs = Helpers.widenArray(ptrs, count, bucketcount >> 1);
            for (int ecur = edgeBuckets[bucket*2];
                 ecur != 0;
                 ecur = (int)edges[ecur+NEXT])
            {
                ptrs[count++] = --ecur;
                // REMIND: Adjust start Y if necessary
            }
            this.edgePtrs = ptrs;
            this.edgeCount = count;
//            if ((count & 0x1) != 0) {
//                System.out.println("ODD NUMBER OF EDGES!!!!");
//            }
            int xings[] = this.crossings;
            if (xings.length < count) {
                this.crossings = xings = new int[ptrs.length];
            }
            for (int i = 0; i < count; i++) {
                int ecur = ptrs[i];
                float curx = edges[ecur+CURX];
                int cross = ((int) Math.ceil(curx - 0.5f)) << 1;
                edges[ecur+CURX] = curx + edges[ecur+SLOPE];
                if (edges[ecur+OR] > 0) {
                    cross |= 1;
                }
                int j = i;
                while (--j >= 0) {
                    int jcross = xings[j];
                    if (jcross <= cross) {
                        break;
                    }
                    xings[j+1] = jcross;
                    ptrs[j+1] = ptrs[j];
                }
                xings[j+1] = cross;
                ptrs[j+1] = ecur;
            }
            return count;
        }

        private boolean hasNext() {
            return nextY < sampleRowMax;
        }

        private int curY() {
            return nextY - 1;
        }
    }


//////////////////////////////////////////////////////////////////////////////
//  EDGE LIST
//////////////////////////////////////////////////////////////////////////////
// TODO(maybe): very tempting to use fixed point here. A lot of opportunities
// for shifts and just removing certain operations altogether. (RT-26922)

    // common to all types of input path segments.
    private static final int YMAX = 0;
    private static final int CURX = 1;
    // NEXT and OR are meant to be indeces into "int" fields, but arrays must
    // be homogenous, so every field is a float. However floats can represent
    // exactly up to 24 bit ints, so we're ok as long as we check for overflow.
    private static final int OR   = 2;
    private static final int SLOPE = 3;
    private static final int NEXT = 4;
    private static final int SIZEOF_EDGE = 5;

    private static final int MAX_EDGE_IDX = 1 << 24;

    private int sampleRowMin;
    private int sampleRowMax;
    private float edgeMinX;
    private float edgeMaxX;

    private float[] edges;
    private int[] edgeBuckets;
    private int numEdges;

    private static final float DEC_BND = 1.0f;
    private static final float INC_BND = 0.4f;

    // each bucket is a linked list. this method adds eptr to the
    // start "bucket"th linked list.
    private void addEdgeToBucket(final int eptr, final int bucket) {
        // we could implement this in terms of insertEdge, but this is a special
        // case, so we optimize a bit.
        if (edgeBuckets[bucket*2] >= MAX_EDGE_IDX) {
            throw new ArrayIndexOutOfBoundsException(edgeBuckets[bucket*2]);
        }
        edges[eptr+NEXT] = edgeBuckets[bucket*2];
        edgeBuckets[bucket*2] = eptr + 1;
        edgeBuckets[bucket*2 + 1] += 2;
    }

    // Flattens using adaptive forward differencing. This only carries out
    // one iteration of the AFD loop. All it does is update AFD variables (i.e.
    // X0, Y0, D*[X|Y], COUNT; not variables used for computing scanline crossings).
    private void quadBreakIntoLinesAndAdd(float x0, float y0,
                                          final Curve c,
                                          final float x2, final float y2)
    {
        final float QUAD_DEC_BND = 32;
        final int countlg = 4;
        int count = 1 << countlg;
        int countsq = count * count;
        float maxDD = Math.max(c.dbx / countsq, c.dby / countsq);
        while (maxDD > QUAD_DEC_BND) {
            maxDD /= 4;
            count <<= 1;
        }

        countsq = count * count;
        final float ddx = c.dbx / countsq;
        final float ddy = c.dby / countsq;
        float dx = c.bx / countsq + c.cx / count;
        float dy = c.by / countsq + c.cy / count;

        while (count-- > 1) {
            float x1 = x0 + dx;
            dx += ddx;
            float y1 = y0 + dy;
            dy += ddy;
            addLine(x0, y0, x1, y1);
            x0 = x1;
            y0 = y1;
        }
        addLine(x0, y0, x2, y2);
    }

    // x0, y0 and x3,y3 are the endpoints of the curve. We could compute these
    // using c.xat(0),c.yat(0) and c.xat(1),c.yat(1), but this might introduce
    // numerical errors, and our callers already have the exact values.
    // Another alternative would be to pass all the control points, and call c.set
    // here, but then too many numbers are passed around.
    private void curveBreakIntoLinesAndAdd(float x0, float y0,
                                           final Curve c,
                                           final float x3, final float y3)
    {
        final int countlg = 3;
        int count = 1 << countlg;

        // the dx and dy refer to forward differencing variables, not the last
        // coefficients of the "points" polynomial
        float dddx, dddy, ddx, ddy, dx, dy;
        dddx = 2f * c.dax / (1 << (3 * countlg));
        dddy = 2f * c.day / (1 << (3 * countlg));

        ddx = dddx + c.dbx / (1 << (2 * countlg));
        ddy = dddy + c.dby / (1 << (2 * countlg));
        dx = c.ax / (1 << (3 * countlg)) + c.bx / (1 << (2 * countlg)) + c.cx / (1 << countlg);
        dy = c.ay / (1 << (3 * countlg)) + c.by / (1 << (2 * countlg)) + c.cy / (1 << countlg);

        // we use x0, y0 to walk the line
        float x1 = x0, y1 = y0;
        while (count > 0) {
            while (Math.abs(ddx) > DEC_BND || Math.abs(ddy) > DEC_BND) {
                dddx /= 8;
                dddy /= 8;
                ddx = ddx/4 - dddx;
                ddy = ddy/4 - dddy;
                dx = (dx - ddx) / 2;
                dy = (dy - ddy) / 2;
                count <<= 1;
            }
            // can only do this on even "count" values, because we must divide count by 2
            while (count % 2 == 0 && Math.abs(dx) <= INC_BND && Math.abs(dy) <= INC_BND) {
                dx = 2 * dx + ddx;
                dy = 2 * dy + ddy;
                ddx = 4 * (ddx + dddx);
                ddy = 4 * (ddy + dddy);
                dddx = 8 * dddx;
                dddy = 8 * dddy;
                count >>= 1;
            }
            count--;
            if (count > 0) {
                x1 += dx;
                dx += ddx;
                ddx += dddx;
                y1 += dy;
                dy += ddy;
                ddy += dddy;
            } else {
                x1 = x3;
                y1 = y3;
            }
            addLine(x0, y0, x1, y1);
            x0 = x1;
            y0 = y1;
        }
    }

    private void addLine(float x1, float y1, float x2, float y2) {
        float or = 1; // orientation of the line. 1 if y increases, 0 otherwise.
        if (y2 < y1) {
            or = y2; // no need to declare a temp variable. We have or.
            y2 = y1;
            y1 = or;
            or = x2;
            x2 = x1;
            x1 = or;
            or = 0;
        }
        final int firstCrossing = Math.max((int) Math.ceil(y1 - 0.5f), boundsMinY);
        final int lastCrossing = Math.min((int) Math.ceil(y2 - 0.5f), boundsMaxY);
        if (firstCrossing >= lastCrossing) {
            return;
        }
        if (firstCrossing < sampleRowMin) { sampleRowMin = firstCrossing; }
        if (lastCrossing > sampleRowMax) { sampleRowMax = lastCrossing; }

        final float slope = (x2 - x1) / (y2 - y1);

        if (slope > 0) { // <==> x1 < x2
            if (x1 < edgeMinX) { edgeMinX = x1; }
            if (x2 > edgeMaxX) { edgeMaxX = x2; }
        } else {
            if (x2 < edgeMinX) { edgeMinX = x2; }
            if (x1 > edgeMaxX) { edgeMaxX = x1; }
        }

        final int bucketIdx = firstCrossing - boundsMinY;
        final int nextCurrEdge = edgeBuckets[bucketIdx*2];
        if (nextCurrEdge >= MAX_EDGE_IDX) {
            throw new ArrayIndexOutOfBoundsException(nextCurrEdge);
        }

        final int ptr = numEdges * SIZEOF_EDGE;
        edges = Helpers.widenArray(edges, ptr, SIZEOF_EDGE);
        numEdges++;
        edges[ptr+OR] = or;
        edges[ptr+CURX] = x1 + (firstCrossing + 0.5f - y1) * slope;
        edges[ptr+SLOPE] = slope;
        edges[ptr+YMAX] = lastCrossing;
        addEdgeToBucket(ptr, bucketIdx);
        edgeBuckets[(lastCrossing - boundsMinY)*2 + 1] |= 1;
    }

// END EDGE LIST
//////////////////////////////////////////////////////////////////////////////


    public static final int WIND_EVEN_ODD = 0;
    public static final int WIND_NON_ZERO = 1;

    // Antialiasing
    final private int SUBPIXEL_LG_POSITIONS_X;
    final private int SUBPIXEL_LG_POSITIONS_Y;
    final private int SUBPIXEL_POSITIONS_X;
    final private int SUBPIXEL_POSITIONS_Y;
    final private int SUBPIXEL_MASK_X;
    final private int SUBPIXEL_MASK_Y;
    final int MAX_AA_ALPHA;

    // Bounds of the drawing region, at subpixel precision.
    private int boundsMinX, boundsMinY, boundsMaxX, boundsMaxY;

    // Current winding rule
    private int windingRule;

    // Current drawing position, i.e., final point of last segment
    private float x0, y0;

    // Position of most recent 'moveTo' command
    private float pix_sx0, pix_sy0;

    public Renderer(int subpixelLgPositionsX, int subpixelLgPositionsY)
    {
        this.SUBPIXEL_LG_POSITIONS_X = subpixelLgPositionsX;
        this.SUBPIXEL_LG_POSITIONS_Y = subpixelLgPositionsY;
        this.SUBPIXEL_POSITIONS_X = 1 << (SUBPIXEL_LG_POSITIONS_X);
        this.SUBPIXEL_POSITIONS_Y = 1 << (SUBPIXEL_LG_POSITIONS_Y);
        this.SUBPIXEL_MASK_X = SUBPIXEL_POSITIONS_X - 1;
        this.SUBPIXEL_MASK_Y = SUBPIXEL_POSITIONS_Y - 1;
        this.MAX_AA_ALPHA = (SUBPIXEL_POSITIONS_X * SUBPIXEL_POSITIONS_Y);
    }

    public Renderer(int subpixelLgPositionsX, int subpixelLgPositionsY,
            int pix_boundsX, int pix_boundsY,
            int pix_boundsWidth, int pix_boundsHeight,
            int windingRule)
    {
        this(subpixelLgPositionsX, subpixelLgPositionsY);
        reset(pix_boundsX, pix_boundsY, pix_boundsWidth, pix_boundsHeight,
              windingRule);
    }

    public void reset(int pix_boundsX, int pix_boundsY,
                      int pix_boundsWidth, int pix_boundsHeight,
                      int windingRule)
    {
        this.windingRule = windingRule;

        this.boundsMinX = pix_boundsX * SUBPIXEL_POSITIONS_X;
        this.boundsMinY = pix_boundsY * SUBPIXEL_POSITIONS_Y;
        this.boundsMaxX = (pix_boundsX + pix_boundsWidth) * SUBPIXEL_POSITIONS_X;
        this.boundsMaxY = (pix_boundsY + pix_boundsHeight) * SUBPIXEL_POSITIONS_Y;

        this.edgeMinX = Float.POSITIVE_INFINITY;
        this.edgeMaxX = Float.NEGATIVE_INFINITY;
        this.sampleRowMax = boundsMinY;
        this.sampleRowMin = boundsMaxY;

        int numBuckets = boundsMaxY - boundsMinY;
        if (edgeBuckets == null || edgeBuckets.length < numBuckets*2+2) {
            // The last 2 entries are ignored and only used to store unused
            // values for segments ending on the last line of the bounds
            // so we can avoid having to check the bounds on this array.
            edgeBuckets = new int[numBuckets*2 + 2];
        } else {
            // Only need to fill the first numBuckets*2 entries since the
            // last 2 entries are write-only for overflow avoidance only.
            Arrays.fill(edgeBuckets, 0, numBuckets*2, 0);
        }
        if (edges == null) {
            edges = new float[SIZEOF_EDGE * 32];
        }
        numEdges = 0;
        pix_sx0 = pix_sy0 = x0 = y0 = 0f;
    }

    private float tosubpixx(float pix_x) {
        return pix_x * SUBPIXEL_POSITIONS_X;
    }
    private float tosubpixy(float pix_y) {
        return pix_y * SUBPIXEL_POSITIONS_Y;
    }

    public void moveTo(float pix_x0, float pix_y0) {
        closePath();
        this.pix_sx0 = pix_x0;
        this.pix_sy0 = pix_y0;
        this.y0 = tosubpixy(pix_y0);
        this.x0 = tosubpixx(pix_x0);
    }

    public void lineTo(float pix_x1, float pix_y1) {
        float x1 = tosubpixx(pix_x1);
        float y1 = tosubpixy(pix_y1);
        addLine(x0, y0, x1, y1);
        x0 = x1;
        y0 = y1;
    }

    private Curve c = new Curve();
    @Override public void curveTo(float x1, float y1,
                                  float x2, float y2,
                                  float x3, float y3)
    {
        final float xe = tosubpixx(x3);
        final float ye = tosubpixy(y3);
        c.set(x0, y0, tosubpixx(x1), tosubpixy(y1), tosubpixx(x2), tosubpixy(y2), xe, ye);
        curveBreakIntoLinesAndAdd(x0, y0, c, xe, ye);
        x0 = xe;
        y0 = ye;
    }

    @Override public void quadTo(float x1, float y1, float x2, float y2) {
        final float xe = tosubpixx(x2);
        final float ye = tosubpixy(y2);
        c.set(x0, y0, tosubpixx(x1), tosubpixy(y1), xe, ye);
        quadBreakIntoLinesAndAdd(x0, y0, c, xe, ye);
        x0 = xe;
        y0 = ye;
    }

    public void closePath() {
        // lineTo expects its input in pixel coordinates.
        lineTo(pix_sx0, pix_sy0);
    }

    public void pathDone() {
        closePath();
    }

    private int savedAlpha[];
    private ScanlineIterator savedIterator;
    public void produceAlphas(AlphaConsumer ac) {
        ac.setMaxAlpha(MAX_AA_ALPHA);

        // Mask to determine the relevant bit of the crossing sum
        // 0x1 if EVEN_ODD, all bits if NON_ZERO
        int mask = (windingRule == WIND_EVEN_ODD) ? 0x1 : ~0x0;

        // add 2 to better deal with the last pixel in a pixel row.
        int width = ac.getWidth();
        int alpha[] = savedAlpha;
        if (alpha == null || alpha.length < width+2) {
            savedAlpha = alpha = new int[width+2];
        } else {
            Arrays.fill(alpha, 0, width+2, 0);
        }

        int bboxx0 = ac.getOriginX() << SUBPIXEL_LG_POSITIONS_X;
        int bboxx1 = bboxx0 + (width << SUBPIXEL_LG_POSITIONS_X);

        // Now we iterate through the scanlines. We must tell emitRow the coord
        // of the first non-transparent pixel, so we must keep accumulators for
        // the first and last pixels of the section of the current pixel row
        // that we will emit.
        // We also need to accumulate pix_bbox*, but the iterator does it
        // for us. We will just get the values from it once this loop is done
        int pix_maxX = bboxx1 >> SUBPIXEL_LG_POSITIONS_X;
        int pix_minX = bboxx0 >> SUBPIXEL_LG_POSITIONS_Y;

        int y = boundsMinY; // needs to be declared here so we emit the last row properly.
        ScanlineIterator it = savedIterator;
        if (it == null) {
            savedIterator = it = new ScanlineIterator();
        } else {
            it.reset();
        }
        for ( ; it.hasNext(); ) {
            int numCrossings = it.next();
            int[] crossings = it.crossings;
            y = it.curY();

            if (numCrossings > 0) {
                int lowx = crossings[0] >> 1;
                int highx = crossings[numCrossings - 1] >> 1;
                int x0 = Math.max(lowx, bboxx0);
                int x1 = Math.min(highx, bboxx1);

                pix_minX = Math.min(pix_minX, x0 >> SUBPIXEL_LG_POSITIONS_X);
                pix_maxX = Math.max(pix_maxX, x1 >> SUBPIXEL_LG_POSITIONS_X);
            }

            int sum = 0;
            int prev = bboxx0;
            for (int i = 0; i < numCrossings; i++) {
                int curxo = crossings[i];
                int curx = curxo >> 1;
                int crorientation = ((curxo & 0x1) << 1) - 1;
                if ((sum & mask) != 0) {
                    int x0 = Math.max(prev, bboxx0);
                    int x1 = Math.min(curx, bboxx1);
                    if (x0 < x1) {
                        x0 -= bboxx0; // turn x0, x1 from coords to indices
                        x1 -= bboxx0; // in the alpha array.

                        int pix_x = x0 >> SUBPIXEL_LG_POSITIONS_X;
                        int pix_xmaxm1 = (x1 - 1) >> SUBPIXEL_LG_POSITIONS_X;

                        if (pix_x == pix_xmaxm1) {
                            // Start and end in same pixel
                            alpha[pix_x] += (x1 - x0);
                            alpha[pix_x+1] -= (x1 - x0);
                        } else {
                            int pix_xmax = x1 >> SUBPIXEL_LG_POSITIONS_X;
                            alpha[pix_x] += SUBPIXEL_POSITIONS_X - (x0 & SUBPIXEL_MASK_X);
                            alpha[pix_x+1] += (x0 & SUBPIXEL_MASK_X);
                            alpha[pix_xmax] -= SUBPIXEL_POSITIONS_X - (x1 & SUBPIXEL_MASK_X);
                            alpha[pix_xmax+1] -= (x1 & SUBPIXEL_MASK_X);
                        }
                    }
                }
                sum += crorientation;
                prev = curx;
            }

            // even if this last row had no crossings, alpha will be zeroed
            // from the last emitRow call. But this doesn't matter because
            // maxX < minX, so no row will be emitted to the cache.
            if ((y & SUBPIXEL_MASK_Y) == SUBPIXEL_MASK_Y) {
                ac.setAndClearRelativeAlphas(alpha, y >> SUBPIXEL_LG_POSITIONS_Y,
                                             pix_minX, pix_maxX);
                pix_maxX = bboxx1 >> SUBPIXEL_LG_POSITIONS_X;
                pix_minX = bboxx0 >> SUBPIXEL_LG_POSITIONS_Y;
            }
        }

        // Emit final row.
        // Note, if y is on a MASK row then it was already sent above...
        if ((y & SUBPIXEL_MASK_Y) < SUBPIXEL_MASK_Y) {
            ac.setAndClearRelativeAlphas(alpha, y >> SUBPIXEL_LG_POSITIONS_Y,
                                         pix_minX, pix_maxX);
        }
    }

    public int getSubpixMinX() {
        int sampleColMin = (int) Math.ceil(edgeMinX - 0.5f);
        if (sampleColMin < boundsMinX) sampleColMin = boundsMinX;
    return sampleColMin;
    }

    public int getSubpixMaxX() {
        int sampleColMax = (int) Math.ceil(edgeMaxX - 0.5f);
        if (sampleColMax > boundsMaxX) sampleColMax = boundsMaxX;
    return sampleColMax;
    }

    public int getSubpixMinY() {
    return sampleRowMin;
    }

    public int getSubpixMaxY() {
    return sampleRowMax;
    }

    public int getOutpixMinX() {
        return (getSubpixMinX() >> SUBPIXEL_LG_POSITIONS_X);
    }

    public int getOutpixMaxX() {
        return (getSubpixMaxX() + SUBPIXEL_MASK_X) >> SUBPIXEL_LG_POSITIONS_X;
    }

    public int getOutpixMinY() {
        return (sampleRowMin >> SUBPIXEL_LG_POSITIONS_Y);
    }

    public int getOutpixMaxY() {
        return (sampleRowMax + SUBPIXEL_MASK_Y) >> SUBPIXEL_LG_POSITIONS_Y;
    }
}
