/*

   Licensed to the Apache Software Foundation (ASF) under one or more
   contributor license agreements.  See the NOTICE file distributed with
   this work for additional information regarding copyright ownership.
   The ASF licenses this file to You under the Apache License, Version 2.0
   (the "License"); you may not use this file except in compliance with
   the License.  You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.

 */
package org.apache.batik.ext.awt.image.rendered;

import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.IndexColorModel;
import java.awt.image.MultiPixelPackedSampleModel;
import java.awt.image.Raster;
import java.awt.image.SampleModel;
import java.awt.image.WritableRaster;
import java.util.Iterator;
import java.util.List;
import java.util.ArrayList;

import org.apache.batik.ext.awt.image.GraphicsUtil;

/**
 * This class implements an adaptive palette generator to reduce images to a
 * specified number of colors.
 *
 * Ideally this would also support a better dither option than just 
 * the JDK's pattern dither.
 *
 * The algorithm used is the 'Median Cut Algorithm' published by
 * Paul Heckbert in early '80s.
 *
 * @author <a href="mailto:deweese@apache.org">Thomas DeWeese</a>
 * @author <a href="mailto:jun@oop-reserch.com">Jun Inamori</a>
 * @version $Id: IndexImage.java 1804130 2017-08-04 14:41:11Z ssteiner $ 
 */

public class IndexImage{

    /**
     * Used to track a color and the number of pixels of that colors
     */
    private static class Counter {

        /**
         * contains the 'packed' rgb-color for this point.
         * Must not change after construction!
         */
        final int val;

        /**
         * the number of image-pixels with this color.
         */
        int count=1;

        Counter(int val) {  this.val = val; }

        boolean add(int val) {
            // See if the value matches us...
            if (this.val != val)
                return false;
            count++;
            return true;
        }

        /**
         * convert the color-point of this counter to an rgb-array.
         * To avoid creating lots of arrays, the caller passes the
         * array to store the result.
         *
         * @param rgb an int[ 3 ] to store the result.
         * @return an int-array with rgb-color-values (same as rgb-parameter)
         */
        int[] getRgb( int[] rgb ){
            rgb[ Cube.RED ] = (val&0xFF0000)>>16;
            rgb[ Cube.GRN ] = (val&0x00FF00)>>8;
            rgb[ Cube.BLU ] = (val&0x0000FF);
            return rgb;
        }
   }

    /**
     * Used to define a cube of the colorspace.  The cube can be split
     * approximagely in half to generate two cubes.
     */
    private static class Cube {
        static final byte[] RGB_BLACK= new byte[]{ 0, 0, 0 };

        int[] min = {0, 0, 0}, max={255,255,255};

        boolean done = false;
        

        /**
         * the colors-array is not modified - in fact, all cubes use
         * the same colors-array.  The Counter contains the
         * rgb-color-code and the count of pixels with this color.
         */
        final Counter[][] colors;

        /**
         * the number of color-points in this cube.
         */
        int count=0;

        static final int RED = 0;
        static final int GRN = 1;
        static final int BLU = 2;

        /**
         * Define a new cube.
         * @param colors contains the 3D color histogram to be subdivided
         * @param count the total number of pixels in the 3D histogram.
         */
        Cube( Counter[][] colors, int count) {
            this.colors = colors;
            this.count = count;
        }

        /**
         * If this returns true then the cube can not be subdivided any
         * further
         */
        public boolean isDone() { return done; }

        /**
         * check, if the color defined by val[] is inside this cube.
         *
         * @param val int[ 3 ] containing r,g,b-values
         * @return true when color is inside this cube
         */
        private boolean contains( int[] val ){

            int vRed = val[ RED ]; // just save some array-accesses
            int vGrn = val[ GRN ];
            int vBlu = val[ BLU ];

            return (
                ( ( min[ RED ] <= vRed ) && ( vRed <= max[ RED ]))&&
                ( ( min[ GRN ] <= vGrn ) && ( vGrn <= max[ GRN ]))&&
                ( ( min[ BLU ] <= vBlu ) && ( vBlu <= max[ BLU ])));
        }

        /**
         * Splits the cube into two parts.  This cube is
         * changed to be one half and the returned cube is the other half.
         * This tries to pick the right channel to split on.
         */
        Cube split() {
            int dr = max[ RED ]-min[ RED ]+1;
            int dg = max[ GRN ]-min[ GRN ]+1;
            int db = max[ BLU ]-min[ BLU ]+1;
            int c0, c1, splitChannel;

            // Figure out which axis is the longest and split along
            // that axis (this tries to keep cubes square-ish).
            if (dr >= dg) {
                if (dr >= db) { splitChannel = RED; c0=GRN; c1=BLU; }
                else          { splitChannel = BLU; c0=RED; c1=GRN; }
            } else if (dg >= db) {
                splitChannel = GRN;
                c0=RED;
                c1=BLU;
            } else {
                splitChannel = BLU;
                c0=GRN;
                c1=RED;
            }

//            System.out.println("Red:" + dr
//                    + " Grn:" + dg
//                    + " Blu:" + db
//                    + " Split:" + splitChannel
//                    + " c0:" + c0
//                    + " c1:" + c1 );

            Cube ret;

            // try to split the longest axis
            ret = splitChannel(splitChannel, c0, c1);
            if (ret != null ) return ret;

            // try to split along the 2nd longest axis
            ret = splitChannel(c0, splitChannel, c1);
            if (ret != null ) return ret;

            // only one left
            ret = splitChannel(c1, splitChannel, c0);
            if (ret != null) return ret;

            // so far, no split was possible trying all 3 colors: this
            // cube can't be split further
            done = true;
            return null;
        }

        /**
         * Adjust (normalize) min/max of this cube so that they span
         * the actual content.  This method is called on the two cubes
         * resulting from a split.  <br> We search the counts[] from
         * min to max for the leftmost non-null entry.  That is the
         * new min.  Then we search counts[] from max to min for the
         * rightmost non-null entry.  That is the new max.  <br>This
         * requires, that {@link #computeCounts } really computes
         * <i>all</i> counts-values (and does not stop after the
         * necessary number of points for a split is found, as it was
         * done in the previous version of this class).
         *
         * @param splitChannel the color used for the last split
         * @param counts contains the number of points along the splitChannel
         *        - only counts[ min .. max ] is valid.
         */
        private void normalize( int splitChannel, int[] counts ){

            if ( count == 0 ){
                // empty cube: nothing to normalize
                return;
            }

            int iMin = min[ splitChannel ];
            int iMax = max[ splitChannel ];
            int loBound = -1;
            int hiBound = -1;

            // we search from left to right for the first non-null
            // entry in counts[]
            for( int i = iMin; i <= iMax; i++ ){
                if ( counts[ i ] == 0 ){
                    // this entry is 0: search more
                    continue;
                }

                // we reached a non-null entry: stop looking further
                loBound = i;
                break;
            }

            // we search from right to left for the first non-null
            // entry in counts[]
            for( int i= iMax; i >= iMin; i-- ){
                if ( counts[ i ] == 0 ){
                    // this entry is 0: search more
                    continue;
                }
                // we reached a non-null entry: stop looking further
                hiBound = i;
                break;
            }

            boolean flagChangedLo = (loBound != -1 ) && ( iMin != loBound );
            boolean flagChangedHi = (hiBound != -1 ) && ( iMax != hiBound );
//            if ( flagChangedLo || flagChangedHi ){
//                System.out.println("old min:" + min[ splitChannel ] + "/max:" + max[ splitChannel ]
//                + " new: " + loBound + "/" + hiBound );
//                StringBuffer buff = new StringBuffer( 100 );
//                for( int i= min[ splitChannel ]; i <= max[ splitChannel]; i++ ){
//                    buff.append( counts[ i ] );
//                    buff.append( ',' );
//                }
//                System.out.println("Counts:" + buff );
//            }

            if ( flagChangedLo ){
                min[ splitChannel ]= loBound;
            }
            if ( flagChangedHi ){
                max[ splitChannel ]= hiBound;
            }
        }


        /**
         * Splits the image according to the parameters.  It tries
         * to find a location where half the pixels are on one side
         * and half the pixels are on the other.
         */
        Cube splitChannel(int splitChannel, int c0, int c1) {

            if (min[splitChannel] == max[splitChannel]) {
                // thickness along the splitChannel is only one point: cannot split
                return null;
            }

            if ( count == 0 ){
                // this Cube has no points: cannot split
                return null;
            }

            // System.out.println( toString() );

            int half = count/2;
            // Each entry is the number of pixels that have that value
            // in the split channel within the cube (so pixels
            // that have that value in the split channel aren't counted
            // if they are outside the cube in the other color channels.
            int[] counts = computeCounts( splitChannel, c0, c1 );

            int tcount=0;
            int lastAdd=-1;
            // These indicate what the top value for the low cube and
            // the low value of the high cube should be in the split channel
            // (they may not be one off if there are 'dead' spots in the
            // counts array.
            int splitLo=min[splitChannel];
            int splitHi=max[splitChannel];
            for (int i=min[splitChannel]; i<=max[splitChannel]; i++) {
                int c = counts[i];
                if (c == 0) {
                    // No counts below this so move up bottom of cube.
                    if ((tcount == 0) && (i < max[splitChannel]))
                        min[splitChannel] = i+1;
                    continue;
                }

                if (tcount+c < half) {
                    lastAdd = i;
                    tcount+=c;
                    continue;
                }
                if ((half-tcount) <= ((tcount+c)-half)) {
                    // Then lastAdd is a better top idx for this then i.
                    if (lastAdd == -1) {
                        // No lower place to break.
                        if (c == count) {
                            // All pixels are at this value so make min/max
                            // reflect that.
                            max[splitChannel] = i;
                            return null; // no split to make.
                        } else {
                            // There are values about this one so
                            // split above.
                            splitLo = i;
                            splitHi = i+1;
                            tcount += c;    // fix 35683
                            break;
                        }
                    }
                    splitLo = lastAdd;
                    splitHi = i;
                } else {
                    if (i == max[splitChannel]) {
                        if ( c == count) {
                            // would move min up but that should
                            // have happened already.
                            return null; // no split to make.
                        } else {
                            // Would like to break between i and i+1
                            // but no i+1 so use lastAdd and i;
                            splitLo = lastAdd;
                            splitHi = i;
                            break;
                        }
                    }
                    // Include c in counts
                    tcount += c;
                    splitLo = i;
                    splitHi = i+1;
                }
                break;
            }

            // System.out.println("Split: " + splitChannel + "@"
            //                    + splitLo + "-"+splitHi +
            //                    " Count: " + tcount  + " of " + count +
            //                    " LA: " + lastAdd);

            // Create the new cube and update everyone's bounds & counts.
            Cube ret = new Cube(colors, tcount);
            count = count-tcount;
            ret.min[splitChannel] = min[splitChannel];
            ret.max[splitChannel] = splitLo;
            min[splitChannel] = splitHi;

            // the cube was split along splitChannel, the other
            // dimensions dont change
            ret.min[c0] = min[c0];
            ret.max[c0] = max[c0];
            ret.min[c1] = min[c1];
            ret.max[c1] = max[c1];

//            if ( count <= 0 ){
//                System.out.println("This cube has no points after split:" + toString() );
//            }
//            if ( ret.count <= 0 ){
//                System.out.println("That cube has no points after split:" + ret.toString() + "    this:" + toString() );
//                System.out.println("SplitLo:"  + splitLo + "  SplitHi:" + splitHi );
//            }

            // after a split we 'normalize' both cubes, so that their
            // min/max reflect the actual bounds of the cube.  comment
            // the next two lines when you want to see the impact of
            // using non-normalized cubes
            normalize( splitChannel, counts );
            ret.normalize( splitChannel, counts );

            return ret;
        }

        /**
         * create an array, which contains the number of pixels for
         * each point along the splitChannel (between min and max of
         * this cube).
         *
         * @param splitChannel one of RED | GRN | BLU
         * @param c0 one of the other channels
         * @param c1 the third channel
         * @return an int[ 255 ] where only int[ min .. max ] contain
         *         valid counts.
         */
        private int[] computeCounts( int splitChannel, int c0, int c1) {

            int splitSh4 = (2-splitChannel)*4;
            int c0Sh4    = (2-c0)*4;
            int c1Sh4    = (2-c1)*4;

            // after split, each half should have half of the cube's points
            int half = count/2;

            // Each entry is the number of pixels that have that value
            // in the split channel within the cube (so pixels
            // that have that value in the split channel aren't counted
            // if they are outside the cube in the other color channels.
            int[] counts = new int[256];
            int tcount = 0;

            int minR=min[0], minG=min[1], minB=min[2];
            int maxR=max[0], maxG=max[1], maxB=max[2];

            int[] minIdx = { minR >> 4, minG >> 4, minB >> 4 };
            int[] maxIdx = { maxR >> 4, maxG >> 4, maxB >> 4 };

            int [] vals = {0, 0, 0};
            for (int i=minIdx[splitChannel]; i<=maxIdx[splitChannel]; i++) {
                int idx1 = i<<splitSh4;
                for (int j=minIdx[c0]; j <=maxIdx[c0]; j++) {
                    int idx2 = idx1 | (j<<c0Sh4);
                    for (int k=minIdx[c1]; k<=maxIdx[c1]; k++) {
                        int idx = idx2 | (k<<c1Sh4);
                        Counter[] v = colors[idx];
                        for (Counter c : v) {
                            vals = c.getRgb(vals);
                            if (contains(vals)) {
                                // The vals[] lies completly within
                                // this cube so count it.
                                counts[vals[splitChannel]] += c.count;
                                tcount += c.count;
                            }
                        }
                    }
                }
                // the next statement-line stops the loop after we
                // found the split-point.  however, we continue to
                // fill the counts[] because that is needed for
                // normalization
//                // We've found the half way point.  Note that the
//                // rest of counts is not filled out.
//                if (( tcount > 0 ) && (tcount >= half)) break;  // fix 35683
            }

            // the result so far is the filled counts[]
            return counts;
        }


        /**
         * convert the cube-content to String-representation for logging.
         * @return the min/max-boundarys of the rgb-channels and
         *         pixel-count of this Cube.
         */
        public String toString() {
            return "Cube: [" +
                    min[ RED ] + '-' + max[ RED ] + "] [" +
                    min[ GRN ] + '-' + max[ GRN ] + "] [" +
                    min[ BLU ] + '-' + max[ BLU ] + "] n:" + count;
        }


        /**
         * Returns the average color for this cube (no alpha).
         */
        public int averageColor() {
            if (count == 0) {
                // cube is empty: return black
                return 0;
            }

            byte[] rgb = averageColorRGB( null );

            return (( rgb[ RED ] << 16 ) & 0x00FF0000)
                 | (( rgb[ GRN ] <<  8 ) & 0x0000FF00)
                 | (( rgb[ BLU ]       ) & 0x000000FF);
        }

        /**
         * Returns the average color for this cube
         */
        public byte[] averageColorRGB( byte[] rgb ) {

            if (count == 0) return RGB_BLACK;

            float red=0, grn=0, blu=0;

            // the boundarys of this cube
            int minR=min[0], minG=min[1], minB=min[2];
            int maxR=max[0], maxG=max[1], maxB=max[2];
            int [] minIdx = {minR>>4, minG>>4, minB>>4};
            int [] maxIdx = {maxR>>4, maxG>>4, maxB>>4};
            int[] vals = new int[3];

            for (int i=minIdx[0]; i<=maxIdx[0]; i++) {
                int idx1 = i<<8;
                for (int j=minIdx[1]; j<=maxIdx[1]; j++) {
                    int idx2 = idx1 | (j<<4);
                    for (int k=minIdx[2]; k<=maxIdx[2]; k++) {
                        int idx = idx2 | k;
                        Counter[] v = colors[idx];
                        for (Counter c : v) {
                            vals = c.getRgb(vals);
                            if (contains(vals)) {
                                float weight = (c.count / (float) count);
                                red += (vals[0] * weight);
                                grn += (vals[1] * weight);
                                blu += (vals[2] * weight);
                            }
                        }
                    }
                }
            }
            byte[] result = (rgb == null) ? new byte[3] : rgb;
            result[ RED ] = (byte)(red + 0.5f);
            result[ GRN ] = (byte)(grn + 0.5f);
            result[ BLU ] = (byte)(blu + 0.5f);

            return result;
        }

    }

    /**
     * create an array of rgb-colors from the cubes-array.
     * The color of each cube is computed as the sum of all colors in the cube,
     * where each pixel is weighted according to it's count.
     *
     * @param nCubes number of entries to use in cubes
     * @param cubes contains the Cubes resulting from running the split-algorithm.
     * @return a byte[][] which is arranged as [ r|g|b ][ 0..nCubes-1 ]
     */
    static byte[][] computeRGB( int nCubes, Cube[] cubes ){

        byte[] r = new byte[nCubes];
        byte[] g = new byte[nCubes];
        byte[] b = new byte[nCubes];

        byte[] rgb = new byte[3];
        for (int i=0; i<nCubes; i++) {
            rgb = cubes[i].averageColorRGB( rgb );
            r[i] = rgb[ Cube.RED ];
            g[i] = rgb[ Cube.GRN ];
            b[i] = rgb[ Cube.BLU ];
        }

        byte[][] result = new byte[3][];
        result[ Cube.RED ] = r;
        result[ Cube.GRN ] = g;
        result[ Cube.BLU ] = b;

//        logRGB( r, g, b );

        return result;
    }

    /**
     * helper-method to print the complete rgb-arrays.
     * @param r
     * @param g
     * @param b
     */
    static void logRGB( byte[] r, byte[] g, byte[] b ){

        StringBuffer buff = new StringBuffer( 100 );
        int nColors = r.length;
        for( int i= 0; i < nColors; i++ ) {
            String rgbStr= "(" + (r[i]+128) + ',' + (g[i] +128 ) + ',' + (b[i] + 128) + ")," ;
            buff.append( rgbStr );
        }
        System.out.println("RGB:" + nColors + buff );
    }


    /**
     * step 1: fill a data-structure with the count of each color in the image.
     * @param bi input-image
     * @return a List[] where each slot is a List of Counters (or null)
     */
    static List[] createColorList( BufferedImage bi ){

        int w= bi.getWidth();
        int h= bi.getHeight();

        // Using 4 bits from RG & B.
        List[] colors = new ArrayList[1<<12];

        for(int i_w=0; i_w<w; i_w++){
            for(int i_h=0; i_h<h; i_h++){
                int rgb=(bi.getRGB(i_w,i_h) & 0x00FFFFFF);  // mask away alpha
                // Get index from high four bits of each component.
                int idx = (((rgb&0xF00000)>>> 12) |
                           ((rgb&0x00F000)>>>  8) |
                           ((rgb&0x0000F0)>>>  4));

                // Get the 'hash vector' for that key.
                List v = colors[idx];
                if (v == null) {
                    // No colors in this bin yet so create list and
                    // add color.
                    v = new ArrayList();
                    v.add(new Counter(rgb));
                    colors[idx] = v;
                } else {
                    // find our color in the bin or create a counter for it.
                    Iterator i = v.iterator();
                    while (true) {
                        if (i.hasNext()) {
                            // try adding our color to each counter...
                            if (((Counter)i.next()).add(rgb)) break;
                        } else {
                            v.add(new Counter(rgb));
                            break;
                        }
                    }
                }
            }
        }

        return colors;
    }


    /**
     * step 2: convert the result of step 1 to an Cube[][] which is
     * more efficient in the following iterations. All slots in the
     * result are filled with at least an empty array - thus we avoid
     * tests for null.  <br>Note: the converted slots in colors are no
     * longer needed and removed.
     *
     * @param colors the data-structure to convert. Note that it is
     * empty after conversion!
     * @return same data as in colors, but Lists are converted to arrays.
     */
    static Counter[][] convertColorList( List[] colors ){

        // used to fill empty slots
        final Counter[] EMPTY_COUNTER = new Counter[0];

        Counter[][] colorTbl= new Counter[ 1<< 12 ][];
        for( int i= 0; i < colors.length; i++ ){
            List cl = colors[ i ];
            if ( cl == null ){
                colorTbl[ i ] = EMPTY_COUNTER;
                continue;
            }
            int nSlots = cl.size();
            colorTbl[i] = (Counter[])cl.toArray( new Counter[ nSlots ] );

            // the colors[ i ] - data is no longer needed: discard
            colors[ i ] = null;
        }

        return colorTbl;
    }

    /**
     * Converts the input image (must be TYPE_INT_RGB or
     * TYPE_INT_ARGB) to an indexed image.  Generating an adaptive
     * palette with number of colors specified.
     * @param bi the image to be processed.
     * @param nColors number of colors in the palette
     */
    public static BufferedImage getIndexedImage( BufferedImage bi, int nColors) {
        int w=bi.getWidth();
        int h=bi.getHeight();

        // Using 4 bits from RG & B.
        List[] colors = createColorList( bi );

        // now we have initialized the colors[] with lists of Counters.
        // from now on, this data-structure is just read, not modified.
        // convert it to Counter[][] for faster iteration
        Counter[][] colorTbl = convertColorList( colors );

        // this is no longer needed: discard
        colors = null;

        int nCubes=1;
        int fCube=0;
        Cube [] cubes = new Cube[nColors];
        cubes[0] = new Cube(colorTbl, w*h);

        while (nCubes < nColors) {
            while (cubes[fCube].isDone()) {
                fCube++;
                if (fCube == nCubes) break;
            }
            if (fCube == nCubes) {
                // System.out.println("fCube == nCubes" + fCube );
                break;
            }
            Cube c = cubes[fCube];
            Cube nc = c.split();
            if (nc != null) {
                // store the cube with less points towards the end of
                // the array, so that fat cubes get more splits
                if (nc.count > c.count) {
                    // new cube has more points: swap
                    Cube tmp = c; c= nc; nc = tmp;
                }
                int j = fCube;
                int cnt = c.count;
                for (int i=fCube+1; i<nCubes; i++) {
                    if (cubes[i].count < cnt)
                        break;
                    cubes[j++] = cubes[i];
                }
                cubes[j++] = c;

                cnt = nc.count;
                while (j<nCubes) {
                    if (cubes[j].count < cnt)
                        break;
                    j++;
                }
                for (int i=nCubes; i>j; i--)
                    cubes[i] = cubes[i-1];
                cubes[j++] = nc;
                nCubes++;
            }
        }

        // convert the remaining cubes to the colors they represent
        byte[][] rgbTbl = computeRGB( nCubes, cubes );

        // The JDK doesn't seem to dither the image correctly if I go
        // below 8bits per pixel.  So I dither to an 8bit palette
        // image that only has nCubes colors.  Then I copy the data to
        // a lower bit depth image that I return.
        IndexColorModel icm= new IndexColorModel( 8, nCubes, rgbTbl[0], rgbTbl[1], rgbTbl[2] );

        BufferedImage indexed =new BufferedImage
            (w, h, BufferedImage.TYPE_BYTE_INDEXED, icm);
        Graphics2D g2d=indexed.createGraphics();
        g2d.setRenderingHint
            (RenderingHints.KEY_DITHERING,
             RenderingHints.VALUE_DITHER_ENABLE);
        g2d.drawImage(bi, 0, 0, null);
        g2d.dispose();


        int bits;
        for (bits=1; bits <=8; bits++) {
            if ((1<<bits) >= nCubes) break;
        }
//        System.out.println("Bits: " + bits + " Cubes: " + nCubes);

        if (bits > 4) {
            // 8 bit image we are done...
            return indexed;
        }

        // Create our low bit depth image...
        if (bits ==3) bits = 4;
        ColorModel cm = new IndexColorModel(bits,nCubes, 
                                            rgbTbl[0], rgbTbl[1], rgbTbl[2] );
        SampleModel sm;
        sm = new MultiPixelPackedSampleModel(DataBuffer.TYPE_BYTE, w, h, bits);
        WritableRaster ras = Raster.createWritableRaster( sm, new Point(0,0));

        // Copy the data to the low bitdepth image.
        bi = indexed;
        indexed = new BufferedImage(cm, ras, bi.isAlphaPremultiplied(), null);
        GraphicsUtil.copyData(bi, indexed);
        return indexed;
    }
}
