/*
 *  Adapted from: 
 *  SurfaceIO class
 *  Alex Mathews, amathews@jhu.edu
 *  Pranav Lakshminarayanan, plakshm1@jhu.edu
 *  Computer Integrated Surgery I - Fall 2014
 */
package baseclasses;

import baseclasses.Triangle;
import baseclasses.Point;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;

/**
 *
 * @author Alex
 * Class reads .obj files, and has methods to remove colored points.
 */
public class ObjImport {
    
    //Global Variables========================================================//
    
    /**
     * Filename and location.
     */
    private final String fileName;
    
    /**
     * Buffered reader for reading in data files.
     */
    private BufferedReader input;
    
    /**
     * Points specified by the surface.
     * POINTS ARE INDEXED STARTING AT 1. 
     */
    private Point[] points;
    
    /**
     * Triangles specified by the surface.
     */
    private Triangle[] triangles;
    
    /**
     * Number of Vertices.
     */
    private int vertNumber = 0;
    
    /**
     * Number of faces.
     */
    private int triNumber = 0;
    
    /**
     * Output directory for output files.
     */
    private final String inputDir = "./Input/";
    //========================================================================//
    
    
    //Constructors============================================================//
    
    /**
     * PointIO - default constructor will throw IOException demanding file.
     * @throws IOException 
     */
    public ObjImport() throws IOException {
        throw new IOException("PointIO - initialize PointIO using a filename");
    }
    
    /**
     * PointIO - proper constructor using filename,location as parameter.
     * @param fileNameParam - string of file location.
     */
    public ObjImport(String fileNameParam) {
        this.fileName = fileNameParam;
    } 
    //========================================================================//
    
    
    //Methods=================================================================//
    
    /**
     * read - read from specified filename.
     * @throws IllegalStateException - possibly IO from reading.
     * @throws IOException - in case of file error.
     */
    public void read() throws IllegalStateException, IOException {
        System.out.println("Importing: " + this.fileName);
        this.input = new BufferedReader(new FileReader(this.inputDir
                + this.fileName));
        
        this.determineProp();
        
        System.out.println("Number of Vertices: " + this.vertNumber);
        System.out.println("Reading Points...");
        this.points = new Point[this.vertNumber];
        this.pointDataEntry(this.vertNumber);
        System.out.println("Point import complete!");
        
        System.out.println("Number of Faces: " + this.triNumber);
        System.out.println("Reading Faces...");
        this.triangles = new Triangle[this.triNumber];
        this.triangleDataEntry(this.triNumber);
        System.out.println("Face import complete!");
        
        this.input.close();
    }
    
    /**
     * Determine the properties of the file being read.
     */
    private void determineProp() {
        try {
            BufferedReader tempIn = new BufferedReader(new FileReader(
                    this.inputDir + this.fileName));
            String firstLine = tempIn.readLine();
            if (firstLine.equals("# Generated.")) {
                System.out.println("File is from meshlab, must determine "
                        + "properties.");
                while (tempIn.ready()) {
                    String line = tempIn.readLine();
                    if (line.startsWith("v")) {
                        this.vertNumber++;
                    } else if (line.startsWith("f")) {
                        this.triNumber++;
                    }
                }
            } else {
                String line = tempIn.readLine();
                while (line.startsWith("#") && tempIn.ready()) {
                    if (line.contains("Vertices")) {
                        String[] spli = this.splitLine(line, " ");
                        this.vertNumber = new Integer(spli[spli.length - 1]);
                    } else if (line.contains("Faces")) {
                        String[] spli = this.splitLine(line, " ");
                        this.triNumber = new Integer(spli[spli.length - 1]);
                    }
                    line = tempIn.readLine();
                }
            }
            
            tempIn.close();
        } catch (IOException e) {
            System.out.println("bad file name");
        }
    }
    
    /**
     * dataEntry - create a new dataset from file.
     * @param numPts - number of points to read in.
     * @throws IOException - possibly IO from reading.
     */
    private void pointDataEntry(int numPts) throws IOException {
        String line = this.input.readLine();
        while (!line.startsWith("v")) {
            line = this.input.readLine();
        }
        int i = 0;
        while (line.startsWith("v")) {
            Point nPoint = this.lineToPoint(line);
            this.points[i] = nPoint;
            i++;
            line = this.input.readLine();
        }
    }
    
    /**
     * dataEntry - create a new dataset from file.
     * @param numTri - number of points to read in.
     * @throws IOException - possibly IO from reading.
     */
    private void triangleDataEntry(int numTri) throws IOException {
        String line = this.input.readLine();
        if (line == null) {
            return;
        }
        try {
            while (!line.startsWith("f")) {
                line = this.input.readLine();
            }
            int i = 0;
            while (line.startsWith("f")) {
                Triangle nTri = this.linetoTriangle(line, i);
                int[] vertInd = nTri.getVertIndices();
                Point[] verts = new Point[3];
                for (int j = 0; j < 3; j++) {
                    verts[j] = this.points[vertInd[j]];
                }
                nTri.setVertices(verts);
                this.addPointNeighbors(vertInd, verts);
                this.triangles[i] = nTri;
                i++;
                line = this.input.readLine();
            }
        } catch (NullPointerException e) {
            System.out.println("nofaces");
        }
    }
    
    /**
     * lineToPoint - creates a point from a line from data file.
     * @param line - point values.
     * @return - point values as point.
     */
    private Point lineToPoint(String line) {
        String[] parts;
        if (line.contains(",")) {
            parts = this.splitLine(line, ","); 
        } else {
            parts = this.splitLine(line, " "); 
        }
        if (parts.length >= 7) {
            return new Point(new Double(parts[1]), new Double(parts[2]), 
                    new Double(parts[3]), new Double(parts[4]), 
                    new Double(parts[5]), new Double(parts[6]));
        } else {
            return new Point(new Double(parts[1]), new Double(parts[2]), 
                    new Double(parts[3]), 0.0, 0.0, 0.0);
        }
    }
    
    /**
     * lineToPoint - creates a point from a line from data file.
     * @param line - point values.
     * @param ind - index of the line.
     * @return - point values as point.
     */
    private Triangle linetoTriangle(String line, int ind) {
        String[] parts;
        if (line.contains(",")) {
            parts = this.splitLine(line, ","); 
        } else {
            parts = this.splitLine(line, " "); 
        }
        //NOTE: POINT INDEXING BEGINS AT 0 UNLIKE OBJ SPEC WHICH BEGINS AT 1.
        int[] ptInd = new int[]{((new Integer(parts[1])) - 1), 
            ((new Integer(parts[2])) - 1), ((new Integer(parts[3])) - 1)};
        Triangle ans = new Triangle(ind, ptInd);
        
        return ans;
    }

    /**
     * Add neighbors to points.
     * @param neigh array of length 3.
     * @param pNeigh array of length 3 with Points.
     */
    private void addPointNeighbors(int[] neigh, Point[] pNeigh) {
        this.points[neigh[0]].addNeighbor(pNeigh[1]);
        this.points[neigh[0]].addNeighbor(pNeigh[2]);
        this.points[neigh[1]].addNeighbor(pNeigh[0]);
        this.points[neigh[1]].addNeighbor(pNeigh[2]);
        this.points[neigh[2]].addNeighbor(pNeigh[1]);
        this.points[neigh[2]].addNeighbor(pNeigh[0]);
        
        this.points[neigh[0]].addNeighborInd(neigh[1]);
        this.points[neigh[0]].addNeighborInd(neigh[2]);
        this.points[neigh[1]].addNeighborInd(neigh[0]);
        this.points[neigh[1]].addNeighborInd(neigh[2]);
        this.points[neigh[2]].addNeighborInd(neigh[1]);
        this.points[neigh[2]].addNeighborInd(neigh[0]);
    }
    
    /**
     * Split a string by a delimiter.
     * @param filePropLine like to split.
     * @param split delimiter.
     * @return an array of delimited.
     */
    private String[] splitLine(String filePropLine, String split) {
        String[] parts = filePropLine.split(split);
        for (int i = 0; i < parts.length; i++) {
            parts[i] = parts[i].replaceAll("\\s+", "");
        }
        return this.cleanEmpty(parts);
    }
    
    /**
     * Remove empty spaces from array of Strings.
     * @param toClean String array to be cleaned.
     * @return clean the array.
     */
    private String[] cleanEmpty(String[] toClean) {
        int count = 0;
        for (int i = 0; i < toClean.length; i++) {
            if (!toClean[i].equals("")) {
                count++;
            }
        }
        String[] ans = new String[count];
        count = 0;
        for (int i = 0; i < toClean.length; i++) {
            if (!toClean[i].equals("")) {
                ans[count] = toClean[i];
                count++;
            }
        }
        return ans;
    }
    
    /**
     * Boolean to determine if faces are within color tolerances.
     * @param in Triangle with points that have colors.
     * @param h hue.
     * @param s saturation.
     * @param b brightness.
     * @return true if Triangle meets requirements.
     */
    public boolean isGoodColor(Triangle in, double h, double s, double b) {
        Point[] verts = in.getVertices();
        int good = 0;
        for (int i = 0; i < verts.length; i++) {
            float[] j = verts[i].getColor();
            float hDiff = Math.abs(j[0] - (float) h) / ((float) h);
            float sDiff = Math.abs(j[1] - (float) s) / ((float) s);
            float bDiff = Math.abs(j[2] - (float) b) / ((float) b);
            if (hDiff < .2  && bDiff < .4 && sDiff < .5) {
                good++;
            }
            if (good >= 2) {
                return true;
            }
        }
        return false;
    }
    
    
    /**
     * Boolean to determine if faces are within color tolerances.
     * @param in Triangle with points that have colors.
     * @param hsb float representation of HSB.
     * @param tol tolerances for color segmentation.
     * @return true if Triangle meets requirements.
     */
    public boolean isGoodColor(Triangle in, float[]  hsb, double[] tol) {
        Point[] verts = in.getVertices();
        int good = 0;
        for (int i = 0; i < verts.length; i++) {
            float[] j = verts[i].getColor();
            float hDiff = Math.abs(j[0] - hsb[0]);
            float sDiff = Math.abs(j[1] - hsb[1]);
            float bDiff = Math.abs(j[2] - hsb[2]);
            
            if (hDiff < tol[0] * 360  && bDiff < tol[1] * 100 
                    && sDiff < tol[2] * 100) {
                good++;
            }
            
            if (good >= 2) {
                return true;
            }
        }
        return false;
    }
    
    /**
     * Accessor for Color segmented faced.
     * @param h hue.
     * @param s saturation.
     * @param b brightness.
     * @return an ArrayList of triangles with good colors.
     */
    public Triangle[] getColorPoints(double h, double s, double b) {
        ArrayList<Triangle> good = new ArrayList<Triangle>();
        for (Triangle j : this.triangles) {
            if (this.isGoodColor(j, h, s, b)) {
                good.add(j);
            }
        }
        Triangle[] a = good.toArray(new Triangle[good.size()]);
        return a;
    }
    
    /**
     * Accessor for Color segmented faced.
     * @param hsb array of float HSB values.
     * @param tol tolerances for color finding.
     * @return an ArrayList of triangles with good colors.
     */
    public Triangle[] getColorPoints(float[] hsb, double[] tol) {
        ArrayList<Triangle> good = new ArrayList<Triangle>();
        for (Triangle j : this.triangles) {
            if (this.isGoodColor(j, hsb, tol)) {
                good.add(j);
            }
        }
        Triangle[] a = good.toArray(new Triangle[good.size()]);
        return a;
    }
    
    /**
     * Return the points collected from surface file.
     * @return the points read in from surface.
     */
    public Point[] getPoints() {
        return this.points;
    }
    
    /**
     * Return the triangles collected from surface file.
     * @return and array of the triangles read in from surface.
     */
    public Triangle[] getTriangles() {
        return this.triangles;
    }
    
    
}
