/*
 * Copyright (c) 2007, 2022, 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.marlin;

import jdk.internal.misc.Unsafe;

/**
 * An object used to cache pre-rendered complex paths.
 *
 * @see Renderer
 */
public final class MarlinCache implements MarlinConst {

    static final boolean FORCE_RLE = MarlinProperties.isForceRLE();
    static final boolean FORCE_NO_RLE = MarlinProperties.isForceNoRLE();
    // minimum width to try using RLE encoding:
    static final int RLE_MIN_WIDTH
        = Math.max(BLOCK_SIZE, MarlinProperties.getRLEMinWidth());
    // maximum width for RLE encoding:
    // values are stored as int [x|alpha] where alpha is 8 bits
    static final int RLE_MAX_WIDTH = 1 << (24 - 1);

    // 4096 (pixels) alpha values (width) x 64 rows / 4 (tile) = 64K bytes
    // x1 instead of 4 bytes (RLE) ie 1/4 capacity or average good RLE compression
    static final long INITIAL_CHUNK_ARRAY = TILE_H * INITIAL_PIXEL_WIDTH >> 2; // 64K

    // The alpha map used by this object (taken out of our map cache) to convert
    // pixel coverage counts gotten from MarlinCache (which are in the range
    // [0, maxalpha]) into alpha values, which are in [0,256).
    static final byte[] ALPHA_MAP;

    static final OffHeapArray ALPHA_MAP_UNSAFE;

    static {
        final byte[] _ALPHA_MAP = buildAlphaMap(MAX_AA_ALPHA);

        ALPHA_MAP_UNSAFE = new OffHeapArray(_ALPHA_MAP, _ALPHA_MAP.length); // 1K
        ALPHA_MAP =_ALPHA_MAP;

        final Unsafe _unsafe = OffHeapArray.UNSAFE;
        final long addr = ALPHA_MAP_UNSAFE.address;

        for (int i = 0; i < _ALPHA_MAP.length; i++) {
            _unsafe.putByte(addr + i, _ALPHA_MAP[i]);
        }
    }

    int bboxX0, bboxY0, bboxX1, bboxY1;

    // 1D dirty arrays
    // row index in rowAAChunk[]
    final long[] rowAAChunkIndex = new long[TILE_H];
    // first pixel (inclusive) for each row
    final int[] rowAAx0 = new int[TILE_H];
    // last pixel (exclusive) for each row
    final int[] rowAAx1 = new int[TILE_H];
    // encoding mode (0=raw, 1=RLE encoding) for each row
    final int[] rowAAEnc = new int[TILE_H];
    // coded length (RLE encoding) for each row
    final long[] rowAALen = new long[TILE_H];
    // last position in RLE decoding for each row (getAlpha):
    final long[] rowAAPos = new long[TILE_H];

    // dirty off-heap array containing pixel coverages for (32) rows (packed)
    // if encoding=raw, it contains alpha coverage values (val) as integer
    // if encoding=RLE, it contains tuples (val, last x-coordinate exclusive)
    // use rowAAx0/rowAAx1 to get row indices within this chunk
    final OffHeapArray rowAAChunk;

    // current position in rowAAChunk array
    long rowAAChunkPos;

    // touchedTile[i] is the sum of all the alphas in the tile with
    // x=j*TILE_SIZE+bboxX0.
    int[] touchedTile;

    // per-thread renderer stats
    final RendererStats rdrStats;

    // touchedTile ref (clean)
    private final ArrayCacheIntClean.Reference touchedTile_ref;

    int tileMin, tileMax;

    boolean useRLE = false;

    MarlinCache(final RendererContext rdrCtx) {
        this.rdrStats = rdrCtx.stats();

        rowAAChunk = rdrCtx.newOffHeapArray(INITIAL_CHUNK_ARRAY); // 64K

        touchedTile_ref = rdrCtx.newCleanIntArrayRef(INITIAL_ARRAY); // 1K = 1 tile line
        touchedTile     = touchedTile_ref.initial;

        // tile used marks:
        tileMin = Integer.MAX_VALUE;
        tileMax = Integer.MIN_VALUE;
    }

    void init(int minx, int miny, int maxx, int maxy)
    {
        // assert maxy >= miny && maxx >= minx;
        bboxX0 = minx;
        bboxY0 = miny;
        bboxX1 = maxx;
        bboxY1 = maxy;

        final int width = (maxx - minx);

        if (FORCE_NO_RLE) {
            useRLE = false;
        } else if (FORCE_RLE) {
            useRLE = true;
        } else {
            // heuristics: use both bbox area and complexity
            // ie number of primitives:

            // fast check min and max width (maxx < 23bits):
            useRLE = (width > RLE_MIN_WIDTH && width < RLE_MAX_WIDTH);
        }

        // the ceiling of (maxy - miny + 1) / TILE_SIZE;
        final int nxTiles = (width + TILE_W) >> TILE_W_LG;

        if (nxTiles > INITIAL_ARRAY) {
            if (DO_STATS) {
                rdrStats.stat_array_marlincache_touchedTile.add(nxTiles);
            }
            touchedTile = touchedTile_ref.getArray(nxTiles);
        }
    }

    /**
     * Disposes this cache:
     * clean up before reusing this instance
     */
    void dispose() {
        // Reset touchedTile if needed:
        resetTileLine(0);

        if (DO_STATS) {
            rdrStats.totalOffHeap += rowAAChunk.length;
        }

        // Return arrays:
        if (touchedTile_ref.doSetRef(touchedTile)) {
            touchedTile = touchedTile_ref.putArrayClean(touchedTile); // already zero filled
        }

        // At last: resize back off-heap rowAA to initial size
        if (rowAAChunk.length != INITIAL_CHUNK_ARRAY) {
            // note: may throw OOME:
            rowAAChunk.resize(INITIAL_CHUNK_ARRAY);
        }
        if (DO_CLEAN_DIRTY) {
            // Force zero-fill dirty arrays:
            rowAAChunk.fill(BYTE_0);
        }
    }

    void resetTileLine(final int pminY) {
        // update bboxY0 to process a complete tile line [0 - 32]
        bboxY0 = pminY;

        // reset current pos
        if (DO_STATS) {
            rdrStats.stat_cache_rowAAChunk.add(rowAAChunkPos);
        }
        rowAAChunkPos = 0L;

        // Reset touchedTile:
        if (tileMin != Integer.MAX_VALUE) {
            if (DO_STATS) {
                rdrStats.stat_cache_tiles.add(tileMax - tileMin);
            }
            // clean only dirty touchedTile:
            if (tileMax == 1) {
                touchedTile[0] = 0;
            } else {
                ArrayCacheInt.fill(touchedTile, tileMin, tileMax, 0);
            }
            // reset tile used marks:
            tileMin = Integer.MAX_VALUE;
            tileMax = Integer.MIN_VALUE;
        }

        if (DO_CLEAN_DIRTY) {
            // Force zero-fill dirty arrays:
            rowAAChunk.fill(BYTE_0);
        }
    }

    void clearAARow(final int y) {
        // process tile line [0 - 32]
        final int row = y - bboxY0;

        // update pixel range:
        rowAAx0[row]  = 0; // first pixel inclusive
        rowAAx1[row]  = 0; //  last pixel exclusive
        rowAAEnc[row] = 0; // raw encoding

        // note: leave rowAAChunkIndex[row] undefined
        // and rowAALen[row] & rowAAPos[row] (RLE)
    }

    /**
     * Copy the given alpha data into the rowAA cache
     * @param alphaRow alpha data to copy from
     * @param y y pixel coordinate
     * @param px0 first pixel inclusive x0
     * @param px1 last pixel exclusive x1
     */
    void copyAARowNoRLE(final int[] alphaRow, final int y,
                   final int px0, final int px1)
    {
        // skip useless pixels above boundary
        final int px_bbox1 = FloatMath.min(px1, bboxX1);

        if (DO_LOG_BOUNDS) {
            MarlinUtils.logInfo("row = [" + px0 + " ... " + px_bbox1
                                + " (" + px1 + ") [ for y=" + y);
        }

        final int row = y - bboxY0;

        // update pixel range:
        rowAAx0[row]  = px0;      // first pixel inclusive
        rowAAx1[row]  = px_bbox1; //  last pixel exclusive
        rowAAEnc[row] = 0; // raw encoding

        // get current position (bytes):
        final long pos = rowAAChunkPos;
        // update row index to current position:
        rowAAChunkIndex[row] = pos;

        // determine need array size:
        // for RLE encoding, position must be aligned to 4 bytes (int):
        // align - 1 = 3 so add +3 and round-off by mask ~3 = -4
        final long needSize = pos + ((px_bbox1 - px0 + 3) & -4);

        // update next position (bytes):
        rowAAChunkPos = needSize;

        // update row data:
        final OffHeapArray _rowAAChunk = rowAAChunk;
        // ensure rowAAChunk capacity:
        if (_rowAAChunk.length < needSize) {
            expandRowAAChunk(needSize);
        }
        if (DO_STATS) {
            rdrStats.stat_cache_rowAA.add(px_bbox1 - px0);
        }

        // rowAA contains only alpha values for range[x0; x1[
        final int[] _touchedTile = touchedTile;
        final int _TILE_SIZE_LG = TILE_W_LG;

        final int from = px0      - bboxX0; // first pixel inclusive
        final int to   = px_bbox1 - bboxX0; //  last pixel exclusive

        final Unsafe _unsafe = OffHeapArray.UNSAFE;
        final long SIZE_BYTE = 1L;
        final long addr_alpha = ALPHA_MAP_UNSAFE.address;
        long addr_off = _rowAAChunk.address + pos;

        // compute alpha sum into rowAA:
        for (int x = from, val = 0; x < to; x++) {
            // alphaRow is in [0; MAX_COVERAGE]
            val += alphaRow[x]; // [from; to[

            // ensure values are in [0; MAX_AA_ALPHA] range
            if (DO_AA_RANGE_CHECK) {
                if (val < 0) {
                    MarlinUtils.logInfo("Invalid coverage = " + val);
                    val = 0;
                }
                if (val > MAX_AA_ALPHA) {
                    MarlinUtils.logInfo("Invalid coverage = " + val);
                    val = MAX_AA_ALPHA;
                }
            }

            // store alpha sum (as byte):
            if (val == 0) {
                _unsafe.putByte(addr_off, (byte)0); // [0-255]
            } else {
                _unsafe.putByte(addr_off, _unsafe.getByte(addr_alpha + val)); // [0-255]

                // update touchedTile
                _touchedTile[x >> _TILE_SIZE_LG] += val;
            }
            addr_off += SIZE_BYTE;
        }

        // update tile used marks:
        int tx = from >> _TILE_SIZE_LG; // inclusive
        if (tx < tileMin) {
            tileMin = tx;
        }

        tx = ((to - 1) >> _TILE_SIZE_LG) + 1; // exclusive (+1 to be sure)
        if (tx > tileMax) {
            tileMax = tx;
        }

        if (DO_LOG_BOUNDS) {
            MarlinUtils.logInfo("clear = [" + from + " ... " + to + "[");
        }

        // Clear alpha row for reuse:
        ArrayCacheInt.fill(alphaRow, from, px1 + 1 - bboxX0, 0);
    }

    void copyAARowRLE_WithBlockFlags(final int[] blkFlags, final int[] alphaRow,
                      final int y, final int px0, final int px1)
    {
        // Copy rowAA data into the piscesCache if one is present
        final int _bboxX0 = bboxX0;

        // process tile line [0 - 32]
        final int row  =   y -  bboxY0;
        final int from = px0 - _bboxX0; // first pixel inclusive

        // skip useless pixels above boundary
        final int px_bbox1 = FloatMath.min(px1, bboxX1);
        final int to       = px_bbox1 - _bboxX0; //  last pixel exclusive

        if (DO_LOG_BOUNDS) {
            MarlinUtils.logInfo("row = [" + px0 + " ... " + px_bbox1
                                + " (" + px1 + ") [ for y=" + y);
        }

        // get current position:
        final long initialPos = startRLERow(row, px0, px_bbox1);

        // determine need array size:
        // pessimistic: max needed size = deltaX x 4 (1 int)
        final long needSize = initialPos + ((to - from) << 2);

        // update row data:
        OffHeapArray _rowAAChunk = rowAAChunk;
        // ensure rowAAChunk capacity:
        if (_rowAAChunk.length < needSize) {
            expandRowAAChunk(needSize);
        }

        final Unsafe _unsafe = OffHeapArray.UNSAFE;
        final long SIZE_INT = 4L;
        final long addr_alpha = ALPHA_MAP_UNSAFE.address;
        long addr_off = _rowAAChunk.address + initialPos;

        final int[] _touchedTile = touchedTile;
        final int _TILE_SIZE_LG = TILE_W_LG;
        final int _BLK_SIZE_LG  = BLOCK_SIZE_LG;

        // traverse flagged blocks:
        final int blkW = (from >> _BLK_SIZE_LG);
        final int blkE = (to   >> _BLK_SIZE_LG) + 1;
        // ensure last block flag = 0 to process final block:
        blkFlags[blkE] = 0;

        // Perform run-length encoding and store results in the piscesCache
        int val = 0;
        int cx0 = from;
        int runLen;

        final int _MAX_VALUE = Integer.MAX_VALUE;
        int last_t0 = _MAX_VALUE;

        int skip = 0;

        for (int t = blkW, blk_x0, blk_x1, cx, delta; t <= blkE; t++) {
            if (blkFlags[t] != 0) {
                blkFlags[t] = 0;

                if (last_t0 == _MAX_VALUE) {
                    last_t0 = t;
                }
                continue;
            }
            if (last_t0 != _MAX_VALUE) {
                // emit blocks:
                blk_x0 = FloatMath.max(last_t0 << _BLK_SIZE_LG, from);
                last_t0 = _MAX_VALUE;

                // (last block pixel+1) inclusive => +1
                blk_x1 = FloatMath.min((t << _BLK_SIZE_LG) + 1, to);

                for (cx = blk_x0; cx < blk_x1; cx++) {
                    if ((delta = alphaRow[cx]) != 0) {
                        alphaRow[cx] = 0;

                        // not first rle entry:
                        if (cx != cx0) {
                            runLen = cx - cx0;

                            // store alpha coverage (ensure within bounds):
                            // as [absX|val] where:
                            // absX is the absolute x-coordinate:
                            // note: last pixel exclusive (>= 0)
                            // note: it should check X is smaller than 23bits (overflow)!

                            // check address alignment to 4 bytes:
                            if (DO_CHECK_UNSAFE) {
                                if ((addr_off & 3) != 0) {
                                    MarlinUtils.logInfo("Misaligned Unsafe address: " + addr_off);
                                }
                            }

                            // special case to encode entries into a single int:
                            if (val == 0) {
                                _unsafe.putInt(addr_off,
                                    ((_bboxX0 + cx) << 8)
                                );
                            } else {
                                _unsafe.putInt(addr_off,
                                    ((_bboxX0 + cx) << 8)
                                    | (((int) _unsafe.getByte(addr_alpha + val)) & 0xFF) // [0-255]
                                );

                                if (runLen == 1) {
                                    _touchedTile[cx0 >> _TILE_SIZE_LG] += val;
                                } else {
                                    touchTile(cx0, val, cx, runLen, _touchedTile);
                                }
                            }
                            addr_off += SIZE_INT;

                            if (DO_STATS) {
                                rdrStats.hist_tile_generator_encoding_runLen
                                    .add(runLen);
                            }
                            cx0 = cx;
                        }

                        // alpha value = running sum of coverage delta:
                        val += delta;

                        // ensure values are in [0; MAX_AA_ALPHA] range
                        if (DO_AA_RANGE_CHECK) {
                            if (val < 0) {
                                MarlinUtils.logInfo("Invalid coverage = " + val);
                                val = 0;
                            }
                            if (val > MAX_AA_ALPHA) {
                                MarlinUtils.logInfo("Invalid coverage = " + val);
                                val = MAX_AA_ALPHA;
                            }
                        }
                    }
                }
            } else if (DO_STATS) {
                skip++;
            }
        }

        // Process remaining RLE run:
        runLen = to - cx0;

        // store alpha coverage (ensure within bounds):
        // as (int)[absX|val] where:
        // absX is the absolute x-coordinate in bits 31 to 8 and val in bits 0..7
        // note: last pixel exclusive (>= 0)
        // note: it should check X is smaller than 23bits (overflow)!

        // check address alignment to 4 bytes:
        if (DO_CHECK_UNSAFE) {
            if ((addr_off & 3) != 0) {
                MarlinUtils.logInfo("Misaligned Unsafe address: " + addr_off);
            }
        }

        // special case to encode entries into a single int:
        if (val == 0) {
            _unsafe.putInt(addr_off,
                ((_bboxX0 + to) << 8)
            );
        } else {
            _unsafe.putInt(addr_off,
                ((_bboxX0 + to) << 8)
                | (((int) _unsafe.getByte(addr_alpha + val)) & 0xFF) // [0-255]
            );

            if (runLen == 1) {
                _touchedTile[cx0 >> _TILE_SIZE_LG] += val;
            } else {
                touchTile(cx0, val, to, runLen, _touchedTile);
            }
        }
        addr_off += SIZE_INT;

        if (DO_STATS) {
            rdrStats.hist_tile_generator_encoding_runLen.add(runLen);
        }

        long len = (addr_off - _rowAAChunk.address);

        // update coded length as bytes:
        rowAALen[row] = (len - initialPos);

        // update current position:
        rowAAChunkPos = len;

        if (DO_STATS) {
            rdrStats.stat_cache_rowAA.add(rowAALen[row]);
            rdrStats.hist_tile_generator_encoding_ratio.add(
                (100 * skip) / (blkE - blkW)
            );
        }

        // update tile used marks:
        int tx = from >> _TILE_SIZE_LG; // inclusive
        if (tx < tileMin) {
            tileMin = tx;
        }

        tx = ((to - 1) >> _TILE_SIZE_LG) + 1; // exclusive (+1 to be sure)
        if (tx > tileMax) {
            tileMax = tx;
        }

        // Clear alpha row for reuse:
        alphaRow[to] = 0;
        if (DO_CHECKS) {
            ArrayCacheInt.check(blkFlags, blkW, blkE, 0);
            ArrayCacheInt.check(alphaRow, from, px1 + 1 - bboxX0, 0);
        }
    }

    long startRLERow(final int row, final int x0, final int x1) {
        // rows are supposed to be added by increasing y.
        rowAAx0[row]  = x0; // first pixel inclusive
        rowAAx1[row]  = x1; // last pixel exclusive
        rowAAEnc[row] = 1; // RLE encoding
        rowAAPos[row] = 0L; // position = 0

        // update row index to current position:
        return (rowAAChunkIndex[row] = rowAAChunkPos);
    }

    private void expandRowAAChunk(final long needSize) {
        if (DO_STATS) {
            rdrStats.stat_array_marlincache_rowAAChunk.add(needSize);
        }

        // note: throw IOOB if neededSize > 2Gb:
        final long newSize = ArrayCacheConst.getNewLargeSize(rowAAChunk.length,
                                                             needSize);

        rowAAChunk.resize(newSize);
    }

    private void touchTile(final int x0, final int val, final int x1,
                           final int runLen,
                           final int[] _touchedTile)
    {
        // the x and y of the current row, minus bboxX0, bboxY0
        // process tile line [0 - 32]
        final int _TILE_SIZE_LG = TILE_W_LG;

        // update touchedTile
        int tx = (x0 >> _TILE_SIZE_LG);

        // handle trivial case: same tile (x0, x0+runLen)
        if (tx == (x1 >> _TILE_SIZE_LG)) {
            // same tile:
            _touchedTile[tx] += val * runLen;
            return;
        }

        final int tx1 = (x1 - 1) >> _TILE_SIZE_LG;

        if (tx <= tx1) {
            final int nextTileXCoord = (tx + 1) << _TILE_SIZE_LG;
            _touchedTile[tx++] += val * (nextTileXCoord - x0);
        }
        if (tx < tx1) {
            // don't go all the way to tx1 - we need to handle the last
            // tile as a special case (just like we did with the first
            final int tileVal = (val << _TILE_SIZE_LG);
            for (; tx < tx1; tx++) {
                _touchedTile[tx] += tileVal;
            }
        }
        // they will be equal unless x0 >> TILE_SIZE_LG == tx1
        if (tx == tx1) {
            final int txXCoord       =  tx      << _TILE_SIZE_LG;
            final int nextTileXCoord = (tx + 1) << _TILE_SIZE_LG;

            final int lastXCoord = (nextTileXCoord <= x1) ? nextTileXCoord : x1;
            _touchedTile[tx] += val * (lastXCoord - txXCoord);
        }
    }

    int alphaSumInTile(final int x) {
        return touchedTile[(x - bboxX0) >> TILE_W_LG];
    }

    @Override
    public String toString() {
        return "bbox = ["
            + bboxX0 + ", " + bboxY0 + " => "
            + bboxX1 + ", " + bboxY1 + "]\n";
    }

    private static byte[] buildAlphaMap(final int maxalpha) {
        // double size !
        final byte[] alMap = new byte[maxalpha << 1];
        final int halfmaxalpha = maxalpha >> 2;
        for (int i = 0; i <= maxalpha; i++) {
            alMap[i] = (byte) ((i * 255 + halfmaxalpha) / maxalpha);
        }
        return alMap;
    }
}
