/* Copyright (c) 2007 Olivier Chafik, All Rights Reserved
 * Copyright (c) 2008 Timothy Wall, All Rights Reserved
 *
 * The contents of this file is dual-licensed under 2
 * alternative Open Source/Free licenses: LGPL 2.1 or later and
 * Apache License 2.0. (starting with JNA version 4.0.0).
 *
 * You can freely decide which license you want to apply to
 * the project.
 *
 * You may obtain a copy of the LGPL License at:
 *
 * http://www.gnu.org/licenses/licenses.html
 *
 * A copy is also included in the downloadable source code package
 * containing JNA, in file "LGPL2.1".
 *
 * You may obtain a copy of the Apache License at:
 *
 * http://www.apache.org/licenses/
 *
 * A copy is also included in the downloadable source code package
 * containing JNA, in file "AL2.0".
 */
package com.sun.jna.platform;

import java.awt.Rectangle;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.DataBufferInt;
import java.awt.image.MultiPixelPackedSampleModel;
import java.awt.image.Raster;
import java.awt.image.SampleModel;
import java.awt.image.SinglePixelPackedSampleModel;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.TreeSet;

/**
 * Methods that are useful to decompose a raster into a set of rectangles.
 * An occupied pixel has two possible meanings, depending on the raster :
 * <ul>
 * <li>if the raster has an alpha layer, occupied means with alpha not null</li>
 * <li>if the raster doesn't have any alpha layer, occupied means not completely black</li>
 * </ul>
 * @author Olivier Chafik
 */
public class RasterRangesUtils {
    /// Masks used to isolate the current column in a set of 8 binary columns packed in a byte
    private static final int[] subColMasks = new int[] {
        0x0080, 0x0040, 0x0020, 0x0010,
        0x0008, 0x0004, 0x0002, 0x0001
    };

    private static final Comparator<Object> COMPARATOR = new Comparator<Object>() {
        @Override
        public int compare(Object o1, Object o2) {
            return ((Rectangle)o1).x - ((Rectangle)o2).x;
        }
    };

    /**
     * Abstraction of a sink for ranges.
     */
    public static interface RangesOutput {
        /**
         * Output a rectangular range.
         * @param x x coordinate of the top-left corner of the range
         * @param y y coordinate of the top-left corner of the range
         * @param w width of the range
         * @param h height of the range
         * @return true if the output succeeded, false otherwise
         */
        boolean outputRange(int x, int y, int w, int h);
    }

    /**
     * Outputs ranges of occupied pixels.
     * In a raster that has an alpha layer, a pixel is occupied if its alpha value is not null.
     * In a raster without alpha layer, a pixel is occupied if it is not completely black.
     * @param raster image to be segmented in non black or non-transparent ranges
     * @param out destination of the non null ranges
     * @return true if the output succeeded, false otherwise
     */
    public static boolean outputOccupiedRanges(Raster raster, RangesOutput out) {
        Rectangle bounds = raster.getBounds();
        SampleModel sampleModel = raster.getSampleModel();
        boolean hasAlpha = sampleModel.getNumBands() == 4;

        // Try to use the underlying data array directly for a few common raster formats
        if (raster.getParent() == null && bounds.x == 0 && bounds.y == 0) {
            // No support for subraster (as obtained with Image.getSubimage(...))

            DataBuffer data = raster.getDataBuffer();
            if (data.getNumBanks() == 1) {
                // There is always a single bank for all BufferedImage types, except maybe TYPE_CUSTOM

                if (sampleModel instanceof MultiPixelPackedSampleModel) {
                    MultiPixelPackedSampleModel packedSampleModel = (MultiPixelPackedSampleModel)sampleModel;
                    if (packedSampleModel.getPixelBitStride() == 1) {
                        // TYPE_BYTE_BINARY
                        return outputOccupiedRangesOfBinaryPixels(((DataBufferByte)data).getData(), bounds.width, bounds.height, out);
                    }
                } else if (sampleModel instanceof SinglePixelPackedSampleModel) {
                    if (sampleModel.getDataType() == DataBuffer.TYPE_INT) {
                        // TYPE_INT_ARGB, TYPE_INT_ARGB_PRE, TYPE_INT_BGR or TYPE_INT_RGB
                        return outputOccupiedRanges(((DataBufferInt)data).getData(), bounds.width, bounds.height, hasAlpha ? 0xff000000 : 0xffffff, out);
                    }
                    // TODO could easily handle cases of TYPE_USHORT_GRAY and TYPE_BYTE_GRAY.
                }
            }
        }

        // Fallback behaviour : copy pixels of raster
        int[] pixels = raster.getPixels(0, 0, bounds.width, bounds.height, (int[])null);
        return outputOccupiedRanges(pixels, bounds.width, bounds.height, hasAlpha ? 0xff000000 : 0xffffff, out);
    }

    /**
     * Output the non-null values of a binary image as ranges of contiguous values.
     * @param binaryBits byte-packed binary bits of an image
     * @param w width of the image (in pixels)
     * @param h height of the image
     * @param out
     * @return true if the output succeeded, false otherwise
     */
    public static boolean outputOccupiedRangesOfBinaryPixels(byte[] binaryBits, int w, int h, RangesOutput out) {
        Set<Rectangle> rects = new HashSet<>();
        Set<Rectangle> prevLine = Collections.<Rectangle>emptySet();
        int scanlineBytes = binaryBits.length / h;
        for (int row = 0; row < h; row++) {
            Set<Rectangle> curLine = new TreeSet<>(COMPARATOR);
            int rowOffsetBytes = row * scanlineBytes;
            int startCol = -1;
            // Look at each batch of 8 columns in this row
            for (int byteCol = 0; byteCol < scanlineBytes; byteCol++) {
                int firstByteCol = byteCol << 3;
                byte byteColBits = binaryBits[rowOffsetBytes + byteCol];
                if (byteColBits == 0) {
                    // all 8 bits are zeroes
                    if (startCol >= 0) {
                        // end of current region
                        curLine.add(new Rectangle(startCol, row, firstByteCol - startCol, 1));
                        startCol = -1;
                    }
                } else if (byteColBits == 0xff) {
                    // all 8 bits are ones
                    if (startCol < 0) {
                        // start of new region
                        startCol = firstByteCol;
                    }
                } else {
                    // mixed case : some bits are ones, others are zeroes
                    for (int subCol = 0; subCol < 8; subCol++) {
                        int col = firstByteCol | subCol;
                        if ((byteColBits & subColMasks[subCol]) != 0) {
                            if (startCol < 0) {
                                // start of new region
                                startCol = col;
                            }
                        } else {
                            if (startCol >= 0) {
                                // end of current region
                                curLine.add(new Rectangle(startCol, row, col - startCol, 1));
                                startCol = -1;
                            }
                        }
                    }
                }
            }
            if (startCol >= 0) {
                // end of last region
                curLine.add(new Rectangle(startCol, row, w - startCol, 1));
            }
            Set<Rectangle> unmerged = mergeRects(prevLine, curLine);
            rects.addAll(unmerged);
            prevLine = curLine;
        }
        // Add anything left over
        rects.addAll(prevLine);
        for (Iterator<Rectangle> i=rects.iterator();i.hasNext();) {
            Rectangle r = i.next();
            if (!out.outputRange(r.x, r.y, r.width, r.height)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Output the occupied values of an integer-pixels image as ranges of contiguous values.
     * A pixel is considered occupied if the bitwise AND of its integer value with the provided occupationMask is not null.
     * @param pixels integer values of the pixels of an image
     * @param w width of the image (in pixels)
     * @param h height of the image
     * @param occupationMask mask used to select which bits are used in a pixel to check its occupied status. 0xff000000 would only take the alpha layer into account, for instance.
     * @param out where to output all the contiguous ranges of non occupied pixels
     * @return true if the output succeeded, false otherwise
     */
    public static boolean outputOccupiedRanges(int[] pixels, int w, int h, int occupationMask, RangesOutput out) {
        Set<Rectangle> rects = new HashSet<>();
        Set<Rectangle> prevLine = Collections.<Rectangle>emptySet();
        for (int row = 0; row < h; row++) {
            Set<Rectangle> curLine = new TreeSet<>(COMPARATOR);
            int idxOffset = row * w;
            int startCol = -1;

            for (int col = 0; col < w; col++) {
                if ((pixels[idxOffset + col] & occupationMask) != 0) {
                    if (startCol < 0) {
                        startCol = col;
                    }
                } else {
                    if (startCol >= 0) {
                        // end of current region
                        curLine.add(new Rectangle(startCol, row, col-startCol, 1));
                        startCol = -1;
                    }
                }
            }
            if (startCol >= 0) {
                // end of last region of current row
                curLine.add(new Rectangle(startCol, row, w-startCol, 1));
            }
            Set<Rectangle> unmerged = mergeRects(prevLine, curLine);
            rects.addAll(unmerged);
            prevLine = curLine;
        }
        // Add anything left over
        rects.addAll(prevLine);
        for (Iterator<Rectangle> i=rects.iterator();i.hasNext();) {
            Rectangle r = i.next();
            if (!out.outputRange(r.x, r.y, r.width, r.height)) {
                return false;
            }
        }
        return true;
    }

    private static Set<Rectangle> mergeRects(Set<Rectangle> prev, Set<Rectangle> current) {
        Set<Rectangle> unmerged = new HashSet<>(prev);
        if (!prev.isEmpty() && !current.isEmpty()) {
            Rectangle[] pr = prev.toArray(new Rectangle[0]);
            Rectangle[] cr = current.toArray(new Rectangle[0]);
            int ipr = 0;
            int icr = 0;
            while (ipr < pr.length && icr < cr.length) {
                while (cr[icr].x < pr[ipr].x) {
                    if (++icr == cr.length) {
                        return unmerged;
                    }
                }
                if (cr[icr].x == pr[ipr].x && cr[icr].width == pr[ipr].width) {
                    unmerged.remove(pr[ipr]);
                    cr[icr].y = pr[ipr].y;
                    cr[icr].height = pr[ipr].height + 1;
                    ++icr;
                }
                else {
                    ++ipr;
                }
            }
        }
        return unmerged;
    }

}
