package edu.oregonstate.cartography.grid;

/**
 *
 * @author Bernhard Jenny, Institute of Cartography, ETH Zurich.
 */
public class Convolution5x5 {

    /** 
     * kernel weights. A standard deviation of 1 would result in (9 elements):
     * 0.0001 0.0044 0.054 0.242 0.3989 0.242 0.054 0.0044 0.0001
     */
    private final float wa = 0.4f;
    private final float wb = 0.25f;
    private final float wc = 0.05f;

    public Convolution5x5() {
    }

    public Grid convolveToHalfSize(Grid grid) {

        final int rows = grid.getRows();
        final int cols = grid.getCols();
        if (cols < 4 || rows < 4) {
            return null;
        }

        final int newCols = cols / 2 + cols % 2;
        final int newRows = rows / 2 + rows % 2;
        Grid convGrid = new Grid(newCols, newRows, grid.getCellSize() * 2);
        convGrid.setWest(grid.getWest());
        convGrid.setSouth(grid.getSouth());

        // top and bottom rows
        for (int c = 0; c < cols; c += 2) {
            float g = this.convolveBorder(grid, c, 0);
            convGrid.setValue(g, c / 2, 0);
            g = this.convolveBorder(grid, c, rows - 1);
            convGrid.setValue(g, c / 2, rows / 2 - 1 + rows % 2);
        }

        // left and right columns
        for (int r = 0; r < rows; r += 2) {
            float g = this.convolveBorder(grid, 0, r);
            convGrid.setValue(g, 0, r / 2);
            g = this.convolveBorder(grid, cols - 1, r);
            convGrid.setValue(g, cols / 2 - 1 + cols % 2, r / 2);
        }

        // interior of grid
        for (int r = 2; r < rows - 2; r += 2) {
            for (int c = 2; c < cols - 2; c += 2) {
                final float g = this.convolve(grid, c, r);
                convGrid.setValue(g, c / 2, r / 2);
            }
        }

        return convGrid;
    }

    public Grid convolve(Grid geoGrid) {

        final int rows = geoGrid.getRows();
        final int cols = geoGrid.getCols();
        Grid convoluted = new Grid(cols, rows, geoGrid.getCellSize());
        //convoluted.setWest(geoGrid.getWest());
        //convoluted.setNorth(geoGrid.getNorth());

        // top and bottom rows
        for (int c = 0; c < cols; c++) {
            convoluted.setValue(this.convolveBorder(geoGrid, c, 0), c, 0);
            convoluted.setValue(this.convolveBorder(geoGrid, c, 1), c, 1);
            convoluted.setValue(this.convolveBorder(geoGrid, c, rows - 1), c, rows - 1);
            convoluted.setValue(this.convolveBorder(geoGrid, c, rows - 2), c, rows - 2);
        }

        // left and right columns
        for (int r = 0; r < rows; r++) {
            convoluted.setValue(this.convolveBorder(geoGrid, 0, r), 0, r);
            convoluted.setValue(this.convolveBorder(geoGrid, 1, r), 1, r);
            convoluted.setValue(this.convolveBorder(geoGrid, cols - 1, r), cols - 1, r);
            convoluted.setValue(this.convolveBorder(geoGrid, cols - 2, r), cols - 2, r);
        }

        // interior of grid
        for (int r = 2; r < rows - 2; r++) {
            for (int c = 2; c < cols - 2; c++) {
                convoluted.setValue(this.convolve(geoGrid, c, r), c, r);
            }
        }

        return convoluted;

    }

    private float convolveBorder(Grid geoGrid, int col, int row) {

        final int rows = geoGrid.getRows();

        // mirror values
        /*
        final int r0 = row - 2 < 0 ? row + 2 : row - 2;
        final int r1 = row - 1 < 0 ? row + 1 : row - 1;
        final int r3 = row + 1 >= rows ? row - 1 : row + 1;
        final int r4 = row + 2 >= rows ? row - 2 : row + 2;
         */

        // use border values. First derivative is zero.
        final int r0 = row - 2 < 0 ? 0 : row - 2;
        final int r1 = row - 1 < 0 ? 0 : row - 1;
        final int r3 = row + 1 >= rows ? rows - 1 : row + 1;
        final int r4 = row + 2 >= rows ? rows - 1 : row + 2;

        float v0 = convolveBorderRow(geoGrid, col, r0);
        float v1 = convolveBorderRow(geoGrid, col, r1);
        float v2 = convolveBorderRow(geoGrid, col, row);
        float v3 = convolveBorderRow(geoGrid, col, r3);
        float v4 = convolveBorderRow(geoGrid, col, r4);
        final float res = wc * (v0 + v4) + wb * (v1 + v3) + wa * v2;
        return Float.isNaN(res) ? convolveWithVoid(geoGrid, col, row) : res;
    }

    private float convolveBorderRow(Grid geoGrid, int col, int row) {

        final int cols = geoGrid.getCols();

        // mirror
        /*
        final int c0 = col - 2 < 0 ? col + 2 : col - 2;
        final int c1 = col - 1 < 0 ? col + 1 : col - 1;
        final int c3 = col + 1 >= cols ? col - 1 : col + 1;
        final int c4 = col + 2 >= cols ? col - 2 : col + 2;
         */

        final int c0 = col - 2 < 0 ? 0 : col - 2;
        final int c1 = col - 1 < 0 ? 0 : col - 1;
        final int c3 = col + 1 >= cols ? cols - 1 : col + 1;
        final int c4 = col + 2 >= cols ? cols - 1 : col + 2;

        final float v0 = geoGrid.getValue(c0, row);
        final float v1 = geoGrid.getValue(c1, row);
        final float v2 = geoGrid.getValue(col, row);
        final float v3 = geoGrid.getValue(c3, row);
        final float v4 = geoGrid.getValue(c4, row);
        return wc * (v0 + v4) + wb * (v1 + v3) + wa * v2;

    }

    private float convolve(Grid geoGrid, int col, int row) {
        
        final float v0 = convolveRow(geoGrid, col, row - 2);
        final float v1 = convolveRow(geoGrid, col, row - 1);
        final float v2 = convolveRow(geoGrid, col, row);
        final float v3 = convolveRow(geoGrid, col, row + 1);
        final float v4 = convolveRow(geoGrid, col, row + 2);
        final float res = wc * (v0 + v4) + wb * (v1 + v3) + wa * v2;
        return Float.isNaN(res) ? convolveWithVoid(geoGrid, col, row) : res;
        
    }

    private float convolveRow(Grid geoGrid, int col, int row) {
        
        final float v0 = geoGrid.getValue(col - 2, row);
        final float v1 = geoGrid.getValue(col - 1, row);
        final float v2 = geoGrid.getValue(col, row);
        final float v3 = geoGrid.getValue(col + 1, row);
        final float v4 = geoGrid.getValue(col + 2, row);
        return wc * (v0 + v4) + wb * (v1 + v3) + wa * v2;
        
    }

    private float convolveWithVoid(float v0, float v1, float v2, float v3, float v4) {

        float totW = 0f;
        float res = 0f;
        if (!Float.isNaN(v0)) {
            res = wc * v0;
            totW = wc;
        }
        if (!Float.isNaN(v1)) {
            res += wb * v1;
            totW += wb;
        }
        if (!Float.isNaN(v2)) {
            res += wa * v2;
            totW += wa;
        }
        if (!Float.isNaN(v3)) {
            res += wb * v3;
            totW += wb;
        }
        if (!Float.isNaN(v4)) {
            res += wc * v4;
            totW += wc;
        }
        if (totW == 0) {
            return Float.NaN;
        }
        final float scale = (2 * (wc + wb) + wa) / totW;
        return res * scale;

    }

    private float convolveWithVoid(Grid geoGrid, int col, int row) {

        final int rows = geoGrid.getRows();
        final float v0 = convolveRowWithVoid(geoGrid, col, Math.max(0, row - 2));
        final float v1 = convolveRowWithVoid(geoGrid, col, Math.max(0, row - 1));
        final float v2 = convolveRowWithVoid(geoGrid, col, row);
        final float v3 = convolveRowWithVoid(geoGrid, col, Math.min(rows - 1, row + 1));
        final float v4 = convolveRowWithVoid(geoGrid, col, Math.min(rows - 1, row + 2));
        return convolveWithVoid(v0, v1, v2, v3, v4);

    }

    private float convolveRowWithVoid(Grid geoGrid, int col, int row) {

        final int cols = geoGrid.getCols();
        final float v0 = geoGrid.getValue(Math.max(0, col - 2), row);
        final float v1 = geoGrid.getValue(Math.max(0, col - 1), row);
        final float v2 = geoGrid.getValue(col, row);
        final float v3 = geoGrid.getValue(Math.min(cols - 1, col + 1), row);
        final float v4 = geoGrid.getValue(Math.min(cols - 1, col + 2), row);
        return convolveWithVoid(v0, v1, v2, v3, v4);

    }
}
