/* Copyright (C) 2005-2007  The Chemistry Development Kit (CDK) project
 *
 * Contact: cdk-devel@lists.sourceforge.net
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either version 2.1
 * 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 */

package org.openscience.cdk.tools;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;

import javax.vecmath.Point3d;

/**
 * Generates a grid of points in 3D space within given boundaries.
 *
 * @author cho
 * @cdk.githash
 * @cdk.created 2005-09-30
 */
public class GridGenerator {

    double       latticeConstant = 0.5;
    double       extendGrid      = 2;
    double[][][] grid            = null;
    double[]     gridArray       = null;
    double       maxx            = 0;
    double       maxy            = 0;
    double       maxz            = 0;
    double       minx            = 0;
    double       miny            = 0;
    double       minz            = 0;
    int[]        dim             = {0, 0, 0};

    public GridGenerator() {}

    public GridGenerator(double min, double max) {
        setDimension(min, max);
        generateGrid();
    }

    /**
     * @param initialValue used as initial value for the grid points
     */
    public GridGenerator(double min, double max, double initialValue) {
        setDimension(min, max);
        generateGrid();
        initializeGrid(initialValue);
    }

    public GridGenerator(double[] minMax, double initialValue, boolean cubicGridFlag) {
        setDimension(minMax, cubicGridFlag);
        generateGrid();
        initializeGrid(initialValue);
    }

    /**
     * Method sets the maximal 3d dimensions to given min and max values.
     */
    public void setDimension(double min, double max) {
        this.minx = min;
        this.maxx = max;
        this.miny = min;
        this.maxy = max;
        this.minz = min;
        this.maxz = max;
    }

    /**
     * Method sets the maximal 3d dimensions to given min and max values.
     */
    public void setDimension(double[] minMax, boolean cubicGridFlag) {
        if (cubicGridFlag) {
            double min = minMax[0];
            double max = minMax[0];
            for (int i = 0; i < minMax.length; i++) {
                if (minMax[i] < min) {
                    min = minMax[i];
                } else if (minMax[i] > max) {
                    max = minMax[i];
                }
            }
            setDimension(min, max);
        } else {
            this.minx = minMax[0];
            this.maxx = minMax[1];
            this.miny = minMax[2];
            this.maxy = minMax[3];
            this.minz = minMax[4];
            this.maxz = minMax[5];
        }
    }

    /**
     * Method sets the maximal 3d dimensions to given min and max values.
     */
    public void setDimension(double minx, double maxx, double miny, double maxy, double minz, double maxz) {
        this.minx = minx;
        this.maxx = maxx;
        this.miny = miny;
        this.maxy = maxy;
        this.minz = minz;
        this.maxz = maxz;
    }

    /**
     * Main method creates a grid between given boundaries (dimensions).
     * The grid my be extended over the given boundaries with the
     * variable extendGrid.
     */
    public void generateGrid() {
        minx = minx - extendGrid;
        maxx = maxx + extendGrid;
        miny = miny - extendGrid;
        maxy = maxy + extendGrid;
        minz = minz - extendGrid;
        maxz = maxz + extendGrid;

        dim[0] = (int) Math.round(Math.abs(maxx - minx) / latticeConstant);
        dim[1] = (int) Math.round(Math.abs(maxy - miny) / latticeConstant);
        dim[2] = (int) Math.round(Math.abs(maxz - minz) / latticeConstant);

        grid = new double[dim[0] + 1][dim[1] + 1][dim[2] + 1];
    }

    /**
     * Method initialise the given grid points with a value.
     */
    public void initializeGrid(double value) {
        for (int i = 0; i < grid.length; i++) {
            for (int j = 0; j < grid[0].length; j++) {
                for (int k = 0; k < grid[0][0].length; k++) {
                    grid[k][j][i] = value;
                }
            }
        }
    }

    /**
     * Method initialise the given grid points with a value.
     */
    public double[][][] initializeGrid(double grid[][][], double value) {
        for (int i = 0; i < grid.length; i++) {
            for (int j = 0; j < grid[0].length; j++) {
                for (int k = 0; k < grid[0][0].length; k++) {
                    grid[k][j][i] = value;
                }
            }
        }
        return grid;
    }

    /**
     * Method transforms the grid to an array.
     */
    public double[] gridToGridArray(double[][][] grid) {
        if (grid == null) {
            grid = this.grid;
        }
        gridArray = new double[dim[0] * dim[1] * dim[2] + 3];
        int dimCounter = 0;
        for (int z = 0; z < grid[0][0].length; z++) {
            for (int y = 0; y < grid[0].length; y++) {
                for (int x = 0; x < grid.length; x++) {
                    gridArray[dimCounter] = grid[x][y][z];
                    dimCounter++;
                }
            }
        }
        return gridArray;
    }

    /**
     * Method calculates coordinates from a given grid point.
     */
    public Point3d getCoordinatesFromGridPoint(Point3d gridPoint) {
        double dx = minx + latticeConstant * gridPoint.x;
        double dy = miny + latticeConstant * gridPoint.y;
        double dz = minz + latticeConstant * gridPoint.z;
        return new Point3d(dx, dy, dz);
    }

    /**
     * Method calculates coordinates from a given grid array position.
     */
    public Point3d getCoordinatesFromGridPoint(int gridPoint) {
        int dimCounter = 0;
        Point3d point = new Point3d(0, 0, 0);
        for (int z = 0; z < grid[0][0].length; z++) {
            for (int y = 0; y < grid[0].length; y++) {
                for (int x = 0; x < grid.length; x++) {
                    if (dimCounter == gridPoint) {
                        point.x = minx + latticeConstant * x;
                        point.y = miny + latticeConstant * y;
                        point.z = minz + latticeConstant * z;
                        return point;
                    }
                    dimCounter++;
                }
            }
        }
        return point;
    }

    /**
     * Method calculates the nearest grid point from given coordinates.
     */
    public Point3d getGridPointFrom3dCoordinates(Point3d coord) throws Exception {
        Point3d gridPoint = new Point3d();

        if (coord.x >= minx & coord.x <= maxx) {
            gridPoint.x = (int) Math.round(Math.abs(minx - coord.x) / latticeConstant);
        } else {
            throw new Exception("CDKGridError: Given coordinates are not in grid");
        }
        if (coord.y >= miny & coord.y <= maxy) {
            gridPoint.y = (int) Math.round(Math.abs(miny - coord.y) / latticeConstant);
        } else {
            throw new Exception("CDKGridError: Given coordinates are not in grid");
        }
        if (coord.z >= minz & coord.z <= maxz) {
            gridPoint.z = (int) Math.round(Math.abs(minz - coord.z) / latticeConstant);
        } else {
            throw new Exception("CDKGridError: Given coordinates are not in grid");
        }

        return gridPoint;
    }

    /**
     * Method transforms the grid into pmesh format.
     */
    public void writeGridInPmeshFormat(String outPutFileName) throws IOException {
        BufferedWriter writer = new BufferedWriter(new FileWriter(outPutFileName + ".pmesh"));
        int numberOfGridPoints = grid.length * grid[0].length * grid[0][0].length;
        writer.write(numberOfGridPoints + "\n");
        for (int z = 0; z < grid[0][0].length; z++) {
            for (int y = 0; y < grid[0].length; y++) {
                for (int x = 0; x < grid.length; x++) {
                    Point3d coords = getCoordinatesFromGridPoint(new Point3d(x, y, z));
                    writer.write(coords.x + "\t" + coords.y + "\t" + coords.z + "\n");
                }
            }
        }
        writer.close();
    }

    /**
     * Method transforms the grid into pmesh format. Only grid points
     * with specific value defined with cutoff are considered.
     * <pre>{@code
     * cutoff <0, the values considered must be <=cutoff
     * cutoff >0, the values considered must be >=cutoff
     * }</pre>
     */
    public void writeGridInPmeshFormat(String outPutFileName, double cutOff) throws IOException {
        BufferedWriter writer = new BufferedWriter(new FileWriter(outPutFileName + ".pmesh"));
        boolean negative = false;
        if (cutOff < 0) {
            negative = true;
        } else {
            negative = false;
        }
        int numberOfGridPoints = 0;
        for (int z = 0; z < grid[0][0].length; z++) {
            for (int y = 0; y < grid[0].length; y++) {
                for (int x = 0; x < grid.length; x++) {
                    if (negative) {
                        if (grid[x][y][z] <= cutOff) {
                            numberOfGridPoints++;
                        }
                    } else {
                        if (grid[x][y][z] >= cutOff) {
                            numberOfGridPoints++;
                        }
                    }
                }
            }
        }
        writer.write(numberOfGridPoints + "\n");
        for (int z = 0; z < grid[0][0].length; z++) {
            for (int y = 0; y < grid[0].length; y++) {
                for (int x = 0; x < grid.length; x++) {
                    Point3d coords = getCoordinatesFromGridPoint(new Point3d(x, y, z));
                    if (negative) {
                        if (grid[x][y][z] <= cutOff) {
                            writer.write(coords.x + "\t" + coords.y + "\t" + coords.z + "\n");
                        }
                    } else {
                        if (grid[x][y][z] >= cutOff) {
                            writer.write(coords.x + "\t" + coords.y + "\t" + coords.z + "\n");
                        }
                    }
                }
            }
        }
        writer.close();
    }

    @Override
    public String toString() {
        return "Dim:" + dim + " SizeX:" + grid.length + " SizeY:" + grid[0].length + " SizeZ:" + grid[0][0].length
                + "\nminx:" + minx + " maxx:" + maxx + "\nminy:" + miny + " maxy:" + maxy + "\nminz:" + minz + " maxz:"
                + maxz;
    }

    /**
     * @return Returns the dim.
     */
    public int[] getDim() {
        return dim;
    }

    /**
     * @param dim The dim to set.
     */
    public void setDim(int[] dim) {
        this.dim = dim;
    }

    /**
     * @return Returns the extendGrid.
     */
    public double getExtendGrid() {
        return extendGrid;
    }

    /**
     * @param extendGrid The extendGrid to set.
     */
    public void setExtendGrid(double extendGrid) {
        this.extendGrid = extendGrid;
    }

    /**
     * @return Returns the grid.
     */
    public double[][][] getGrid() {
        return grid;
    }

    /**
     * @param grid The grid to set.
     */
    public void setGrid(double[][][] grid) {
        this.grid = grid;
    }

    /**
     * @return Returns the latticeConstant.
     */
    public double getLatticeConstant() {
        return latticeConstant;
    }

    /**
     * @param latticeConstant The latticeConstant to set.
     */
    public void setLatticeConstant(double latticeConstant) {
        this.latticeConstant = latticeConstant;
    }

    /**
     * @return Returns the gridArray.
     */
    public double[] getGridArray() {
        return gridArray;
    }

    /**
     * @return Returns the maxx.
     */
    public double getMaxx() {
        return maxx;
    }

    /**
     * @return Returns the maxy.
     */
    public double getMaxy() {
        return maxy;
    }

    /**
     * @return Returns the maxz.
     */
    public double getMaxz() {
        return maxz;
    }

    /**
     * @return Returns the minx.
     */
    public double getMinx() {
        return minx;
    }

    /**
     * @return Returns the miny.
     */
    public double getMiny() {
        return miny;
    }

    /**
     * @return Returns the minz.
     */
    public double getMinz() {
        return minz;
    }

}
