/*
 * This file is part of JGrasstools (http://www.jgrasstools.org)
 * 
 * JGrasstools is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.jgrasstools.gears.modules.r.labeler;

import static org.jgrasstools.gears.libs.modules.JGTConstants.isNovalue;

import java.awt.image.RenderedImage;
import java.awt.image.WritableRaster;
import java.util.HashMap;

import javax.media.jai.iterator.RandomIter;
import javax.media.jai.iterator.RandomIterFactory;

import oms3.annotations.Author;
import oms3.annotations.Description;
import oms3.annotations.Execute;
import oms3.annotations.In;
import oms3.annotations.Keywords;
import oms3.annotations.Label;
import oms3.annotations.License;
import oms3.annotations.Name;
import oms3.annotations.Out;
import oms3.annotations.Status;

import org.geotools.coverage.grid.GridCoverage2D;
import org.jgrasstools.gears.libs.modules.JGTConstants;
import org.jgrasstools.gears.libs.modules.JGTModel;
import org.jgrasstools.gears.modules.utils.BinaryFast;
import org.jgrasstools.gears.utils.coverage.CoverageUtilities;

@Description("Connected components labeling operation")
@Author(name = "Simon Horne, Andrea Antonello", contact = "http://homepages.inf.ed.ac.uk/rbf/HIPR2/, www.hydrologis.com")
@Keywords("Labeling, Raster")
@Label(JGTConstants.RASTERPROCESSING)
@Name("omslabeler")
@Status(Status.DRAFT)
@License("http://www.gnu.org/licenses/gpl-3.0.html")
public class OmsLabeler extends JGTModel {

    @Description("The map to label.")
    @In
    public GridCoverage2D inMap = null;

    @Description("The resulting map.")
    @Out
    public GridCoverage2D outMap = null;

    @Execute
    public void process() throws Exception {
        if (!concatOr(outMap == null, doReset)) {
            return;
        }

        final RenderedImage renderedImage = inMap.getRenderedImage();
        int width = renderedImage.getWidth();
        int height = renderedImage.getHeight();
        int[] data = new int[width * height];
        RandomIter iter = RandomIterFactory.create(renderedImage, null);
        int index = 0;
        for( int r = 0; r < height; r++ ) {
            for( int c = 0; c < width; c++ ) {
                double value = iter.getSampleDouble(c, r, 0);
                if (isNovalue(value)) {
                    data[index] = BinaryFast.BACKGROUND;
                } else {
                    data[index] = BinaryFast.FOREGROUND;
                }
                index++;
            }
        }

        int[] labelsArray = doLabel(data, width, height);

        WritableRaster dataWR = CoverageUtilities.createWritableRasterFromArray(width, height, labelsArray);
        HashMap<String, Double> regionMap = CoverageUtilities.getRegionParamsFromGridCoverage(inMap);
        outMap = CoverageUtilities.buildCoverage("labeled", dataWR, regionMap, inMap.getCoordinateReferenceSystem()); //$NON-NLS-1$
    }

    /**
     * Applies the Labeling algorithm plus offset and scaling
     * 
     * <p>The input image is expected to be 8-bit mono 0=black everything else=white
     * 
     * @param data The input pixel array
     * @param width width of the destination image in pixels
     * @param height height of the destination image in pixels
     * @return A pixel array containing the labelled image
     */
    private int[] doLabel( int[] data, int width, int height ) {

        int nextlabel = 1;
        int nbs[] = new int[4];
        int nbls[] = new int[4];

        // Get size of image and make 1d_arrays
        int d_w = width;
        int d_h = height;

        int[] dest_1d = new int[d_w * d_h];
        // the most labels there can be is 1/2 of the points
        // in checkerboard
        int[] labels = new int[d_w * d_h / 2];

        int result = 0;
        // labelsValid = false; // only set to true once we've complete the task
        // initialise labels
        for( int i = 0; i < labels.length; i++ )
            labels[i] = i;

        int count;
        // now Label the image
        for( int i = 0; i < data.length; i++ ) {

            int src1rgb = data[i] & 0x000000ff;

            if (src1rgb == 0) {
                result = 0; // nothing here
            } else {

                // The 4 visited neighbours
                nbs[0] = getNeighbours(data, i, -1, 0, d_w, d_h);
                nbs[1] = getNeighbours(data, i, 0, -1, d_w, d_h);
                nbs[2] = getNeighbours(data, i, -1, -1, d_w, d_h);
                nbs[3] = getNeighbours(data, i, 1, -1, d_w, d_h);

                // Their corresponding labels
                nbls[0] = getNeighbourd(dest_1d, i, -1, 0, d_w, d_h);
                nbls[1] = getNeighbourd(dest_1d, i, 0, -1, d_w, d_h);
                nbls[2] = getNeighbourd(dest_1d, i, -1, -1, d_w, d_h);
                nbls[3] = getNeighbourd(dest_1d, i, 1, -1, d_w, d_h);

                // label the point
                if ((nbs[0] == nbs[1]) && (nbs[1] == nbs[2]) && (nbs[2] == nbs[3]) && (nbs[0] == 0)) {
                    // all neighbours are 0 so gives this point a new label
                    result = nextlabel;
                    nextlabel++;
                } else { // one or more neighbours have already got labels
                    count = 0;
                    int found = -1;
                    for( int j = 0; j < 4; j++ ) {
                        if (nbs[j] != 0) {
                            count += 1;
                            found = j;
                        }
                    }
                    if (count == 1) {
                        // only one neighbour has a label, so assign the same label to this.
                        result = nbls[found];
                    } else {
                        // more than 1 neighbour has a label
                        result = nbls[found];
                        // Equivalence the connected points
                        for( int j = 0; j < 4; j++ ) {
                            if ((nbls[j] != 0) && (nbls[j] != result)) {
                                associate(nbls[j], result, labels);
                            }
                        }
                    }
                }
            }
            dest_1d[i] = result;
        }

        // reduce labels ie 76=23=22=3 -> 76=3
        // done in reverse order to preserve sorting
        for( int i = labels.length - 1; i > 0; i-- ) {
            labels[i] = reduce(i, labels);
        }

        /*now labels will look something like 1=1 2=2 3=2 4=2 5=5.. 76=5 77=5
          this needs to be condensed down again, so that there is no wasted
          space eg in the above, the labels 3 and 4 are not used instead it jumps
          to 5.
          */
        int condensed[] = new int[nextlabel]; // cant be more than nextlabel labels

        count = 0;
        for( int i = 0; i < nextlabel; i++ ) {
            if (i == labels[i])
                condensed[i] = count++;
        }
        // Record the number of labels
        int numberOfLabels = count - 1;

        // now run back through our preliminary results, replacing the raw label
        // with the reduced and condensed one, and do the scaling and offsets too

        // Now generate an array of colours which will be used to label the image
        int[] labelColors = new int[numberOfLabels + 1];

        // Variable used to check if the color generated is acceptable
        // boolean acceptColor = false;

        for( int i = 0; i < labelColors.length; i++ ) {
            // acceptColor = false;
            // while( !acceptColor ) {
            // double tmp = Math.random();
            // labelColors[i] = (int) (tmp * 16777215);
            // if (((labelColors[i] & 0x000000ff) < 200) && (((labelColors[i] & 0x0000ff00) >> 8) <
            // 64) && (((labelColors[i] & 0x00ff0000) >> 16) < 64)) {
            // // Color to be rejected so don't set acceptColor
            // } else {
            // acceptColor = true;
            // }
            // }
            // if (i == 0)
            // labelColors[i] = 0;
            labelColors[i] = i;
        }

        for( int i = 0; i < data.length; i++ ) {
            result = condensed[labels[dest_1d[i]]];
            // result = (int) ( scale * (float) result + oset );
            // truncate if necessary
            // if( result > 255 ) result = 255;
            // if( result < 0 ) result = 0;
            // produce grayscale
            // dest_1d[i] = 0xff000000 | (result + (result << 16) + (result << 8));
            dest_1d[i] = labelColors[result];// + 0xff000000;
        }

        // labelsValid = true; // only set to true now we've complete the task
        return dest_1d;
    }

    /**
     * getNeighbours will get the pixel value of i's neighbour that's ox and oy
     * away from i, if the point is outside the image, then 0 is returned.
     * This version gets from source image.
     * @param d_w 
     * @param d_h 
     */

    private int getNeighbours( int[] src1d, int i, int ox, int oy, int d_w, int d_h ) {
        int x, y, result;

        x = (i % d_w) + ox; // d_w and d_h are assumed to be set to the
        y = (i / d_w) + oy; // width and height of scr1d

        if ((x < 0) || (x >= d_w) || (y < 0) || (y >= d_h)) {
            result = 0;
        } else {
            result = src1d[y * d_w + x] & 0x000000ff;
        }
        return result;
    }

    /**
     * getNeighbourd will get the pixel value of i's neighbour that's ox and oy
     * away from i, if the point is outside the image, then 0 is returned.
     * This version gets from destination image.
     * @param d_w 
     * @param d_h 
     */

    private int getNeighbourd( int[] src1d, int i, int ox, int oy, int d_w, int d_h ) {
        int x, y, result;

        x = (i % d_w) + ox; // d_w and d_h are assumed to be set to the
        y = (i / d_w) + oy; // width and height of scr1d

        if ((x < 0) || (x >= d_w) || (y < 0) || (y >= d_h)) {
            result = 0;
        } else {
            result = src1d[y * d_w + x];
        }
        return result;
    }

    /**
     * Associate(equivalence) a with b.
     *  a should be less than b to give some ordering (sorting)
     * if b is already associated with some other value, then propagate
     * down the list.
     * @param labels 
      */
    private void associate( int a, int b, int[] labels ) {

        if (a > b) {
            associate(b, a, labels);
            return;
        }
        if ((a == b) || (labels[b] == a))
            return;
        if (labels[b] == b) {
            labels[b] = a;
        } else {
            associate(labels[b], a, labels);
            if (labels[b] > a) { // ***rbf new
                labels[b] = a;
            }
        }
    }
    /**
     * Reduces the number of labels.
     * @param labels 
     */
    private int reduce( int a, int[] labels ) {
        if (labels[a] == a) {
            return a;
        } else {
            return reduce(labels[a], labels);
        }
    }

    // /**
    // *getColours
    // *@return the number of unique, non zero colours. -1 if not valid
    // */
    // private int getColours() {
    //
    // if (labelsValid) {
    //
    // return numberOfLabels;
    // } else {
    // return -1;
    // }
    // }
    //
    // /**
    // * Returns the number of labels.
    // */
    // private int getNumberOfLabels() {
    // return numberOfLabels;
    // }

}
