package ray2;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

import javax.vecmath.Vector2f;
import javax.vecmath.Vector3f;

/**
 * @author ags
 */
public class Ray2Reader {

    public static Object readScene(String filename) throws IOException {
        return readTree(new FileInputStream(filename));
    }
    
    public static Object readTree(InputStream stream) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(stream));
        resetStaticData();
        
        //TODO This call will return the entire scene, it should be saved and
        //returned to the calling method.
        readTransformation(br, true);
        Token last = pop(br);
        if (last.type != Token.EOF) {
            throwException("Extra data found", last);
        }
        
        return null;
    }
 
    private static Object readTransformation(BufferedReader br, boolean top) throws IOException {
        readString(br, "Transformation");
        readType(br, Token.LBRACK);

        //TODO Read in the type of transformation into the transformation being returned
        int type = readInt(br);
        switch(type) {
        //Nothing to do if type == IDENTITY
        case 0: break;
        //Read in all 16 elements if type == GENERAL
        case 1:
            for (int yCtr = 0; yCtr < 4; yCtr++) {
                for (int xCtr = 0; xCtr < 4; xCtr++) {
                    //TODO Save the next element into the matrix
                    readFloat(br);
                    if (xCtr != 3) {
                        readType(br, Token.COMMA);
                    }
                }
            }
            break;
        //Read in the axis and angle if type == ROTATION
        case 2:
            Vector3f axis = readVector3(br);
        	float angle = readFloat(br);
            //TODO Set the axis and angle of the transformation
            break;
        //Read in the scale factors if type == SCALING
        case 3:
            Vector3f scale = readVector3(br);
        	//TODO Set the scale factors of the transformation
            break;
        //Read in the translation factors if type == TRANSLATION
        case 4:
            Vector3f translate = readVector3(br);
        	//TODO Set the translation factors of the transformation
            break;
        default:
            throwException("Bad transform type");
        }

        readType(br, Token.RBRACK);
        readType(br, Token.LBRACE);
        
        while(peek(br).type == Token.STRING) {
            Token current = peek(br);
            if (current.value.equals("Transformation")) {
                //TODO Add the sub transformation to the current transformation
                readTransformation(br, false);
            } else if (current.value.equals("Cube")) {
                //TODO Add the cube to the current transformation
                readCube(br);
            } else if (current.value.equals("Sphere")) {
                //TODO Add the sphere to the current transformation
                readSphere(br);
            } else if (current.value.equals("Cylinder")) {
                //TODO Add the cylinder to the current transformation
                readCylinder(br);
            } else if (current.value.equals("BezierRotation")) {
                //TODO Add the bezier rotation to the current transformation
                readBezierRotation(br);
            } else if (current.value.equals("Light")) {
                //TODO Add the light to the current transformation
                readLight(br);
            } else if (current.value.equals("PerspectiveCamera")) {
                if (!top) {
                    throwException("PerspectiveCamera too deep", current);
                }
                //TODO Add the cube to the current transformation
                readPerspectiveCamera(br);
            } else if (current.value.equals("TriangleMesh")) {
                //TODO Add the triangle mesh to the current transformation
                readTriangleMesh(br);
            } else {
                System.out.println("Found a " + current.value +
                                   ", don't know how to parse it. Skipping past enclosing []'s");
                int numBracks = 1;
                readType(br, Token.LBRACK);
                while (numBracks > 0) {
                    current = pop(br);
                    if (current.type == Token.LBRACK) numBracks++;
                    if (current.type == Token.RBRACK) numBracks--;
                }
            }
        }
        readType(br, Token.RBRACE);
        
        //TODO Return the transformation
        return null;
    }

    private static Object readCube(BufferedReader br) throws IOException  {
        readString(br, "Cube");
        readType(br, Token.LBRACK);

        if (peek(br).type == Token.LPAREN) {
            //TODO Set the material to be a lambertian
            //material based on this color
            readVector3(br);
        } else {
            //TODO Set the general material of the cube
            readMaterial(br);
        }

        readType(br, Token.RBRACK);
        
        //TODO Return the cube
        return null;
    }
    
    private static Object readSphere(BufferedReader br) throws IOException  {
        readString(br, "Sphere");
        readType(br, Token.LBRACK);
        
        if (peek(br).type == Token.LPAREN) {
            //TODO Set the material to be a lambertian
            //material based on this color
            readVector3(br);
        } else {
            //TODO Set the general material of the sphere
            readMaterial(br);
        }

        readType(br, Token.RBRACK);
        
        //TODO Return the sphere
        return null;
    }
    
    private static Object readCylinder(BufferedReader br) throws IOException  {
        readString(br, "Cylinder");
        readType(br, Token.LBRACK);
        
        if (peek(br).type == Token.LPAREN) {
            //TODO Set the material to be a lambertian
            //material based on this color
            readVector3(br);
        } else {
            //TODO Set the general material of the cylinder
            readMaterial(br);
        }

        readType(br, Token.RBRACK);

        //TODO Return the cylinder
        return null;
    }
    
    private static Object readBezierRotation(BufferedReader br) throws IOException {
        readString(br, "BezierRotation");
        readType(br, Token.LBRACK);
        
        if (peek(br).type == Token.LPAREN) {
            //TODO Set the material to be a lambertian
            //material based on this color
            readVector3(br);
        } else {
            //TODO Set the general material of the rotation
            readMaterial(br);
        }

        if (peek(br).type != Token.STRING || !peek(br).value.equals("BezierSegment")) {
            throwException("Need at least one BezierSegment in BezierRotation", peek(br));
        }
        
        do {
            //TODO Save all of the segments read in
            readBezierSegment(br);
        } while(peek(br).type == Token.STRING && peek(br).value.equals("BezierSegment"));
        
        while (peek(br).type == Token.STRING) {
            Token current = pop(br);
            //TODO Put Boolean objects into the "smooth" vector of the bezier rotation
            if (current.value.equals("true")) {
                new Boolean(true);
            } else if (current.value.equals("false")) {
                new Boolean(false);
            } else {
                throwException("Unrecognized boolean value", current);
            }
        }
        
        //TODO Read in the threshold
        readFloat(br);
        readType(br, Token.RBRACK);
        
        return null;
    }
    
    private static Object readTriangleMesh(BufferedReader br) throws IOException {
        readString(br, "TriangleMesh");
        readType(br, Token.LBRACK);
        Token current = pop(br);
        if (current.type != Token.STRING) {
            throwException("String expected: type found = " + current, current);
        }
        
        //TODO Grab the resultant mesh
        readMesh(current.value);
        
        if (peek(br).type == Token.LPAREN) {
            //TODO Set the material to be a lambertian
            //material based on this color
            readVector3(br);
        } else {
            //TODO Set the general material of the mesh
            readMaterial(br);
        }
        
        readType(br, Token.RBRACK);
        
        //TODO Return the mesh
        return null;
    }
    
    private static Object readMesh(String filename) throws IOException {
        BufferedReader fr = new BufferedReader(new FileReader(filename));
        int nPoints = Integer.parseInt(fr.readLine());
        int nPolys = Integer.parseInt(fr.readLine());
        
        float[] vertices = new float[nPoints*3];
        int[] triangles = new int[nPolys*3];
        float[] normals = new float[nPoints*3];
        float[] texcoords = null;
        
        // read vertices
        if (!fr.readLine().equals("vertices")) throw new RuntimeException("Broken file - vertices expected"); 
        for (int i=0; i<vertices.length; ++i) {
            vertices[i] = Float.parseFloat(fr.readLine());
        }
        
        // read triangles
        if (!fr.readLine().equals("triangles")) throw new RuntimeException("Broken file - triangles expected.");
        for (int i=0; i<triangles.length; ++i) {
            triangles[i] = Integer.parseInt(fr.readLine());
        }
        
        // read texcoords
        String line = fr.readLine();
        if (line != null && line.equals("texcoords")) {
            texcoords = new float[nPoints*2];
            line = null;
            for (int i=0; i<texcoords.length; ++i) {
                texcoords[i] = Float.parseFloat(fr.readLine());
            }
        }
        
        
        //Make sure that if tex coords were missing, but normals were
        //still there, we can still read in the normals, rather than losing
        //the "normals" keyword
        if (line == null) {
            line = fr.readLine();
        }
        if (line != null && line.equals("normals")) {
            for (int i=0; i<normals.length; ++i) {
                normals[i] = Float.parseFloat(fr.readLine());
            }
        }
        
        //TODO Form the mesh object and return it
        return null;
    }

    private static Object readMaterial(BufferedReader br) throws IOException {
        Token current = peek(br);
        if (current.type == Token.STRING) {
            String mat = current.value;
            if (mat.equals("Phong")) {
                return readPhong(br);
            } else if (mat.equals("Lambertian")) {
                return readLambertian(br);
            } else {
                return null;
            }
        }
        
        throwException("String expected: type found = " + current, current);
        //Dummy line - never reached
        return null;
    }
    
    private static Object readPhong(BufferedReader br) throws IOException {
        readString(br, "Phong");
        readType(br, Token.LBRACK);
        
        //TODO Set the lambertian component of the phong material
        readVector3(br);
        //TODO Set the specular component of the phong material
        readVector3(br);
        //TODO Set the specular exponent of the phong material
        readInt(br);
        
        readType(br, Token.RBRACK);
        
        //TODO Return the phong material
        return null;
    }
    
    private static Object readLambertian(BufferedReader br) throws IOException {
        readString(br, "Lambertian");
        readType(br, Token.LBRACK);
        
        //TODO Set the lambertian color
        readVector3(br);
        
        readType(br, Token.RBRACK);
        
        //TODO Return the lambertian material
        return null;
    }
    
    private static Object readLight(BufferedReader br) throws IOException  {
        readString(br, "Light");
        readType(br, Token.LBRACK);

        //TODO Read in the light's position
        readVector3(br);
        //TODO Read in the light's diffuse component
        readVector3(br);
        //TODO Read in the light's specular component (not used in RT)
        readVector3(br);
        
        readType(br, Token.RBRACK);
        
        //TODO Return the light
        return null;
    }
    
    private static Object readPerspectiveCamera(BufferedReader br) throws IOException {
        readString(br, "PerspectiveCamera");
        readType(br, Token.LBRACK);
        
        Token current = pop(br);
        if (current.type != Token.STRING) {
            throwException("String expected: type found = " + current, current);
        }
        //TODO Set the camera's name to current.value - not used in RT
        
        //TODO Set the camera's eye point
        readVector3(br);
        //TODO Set the camera's target point
        readVector3(br);
        //TODO Set the camera's up vector
        readVector3(br);
        
        //TODO Set the camera's near plane, not used in RT
        readFloat(br);
        //TODO Set the camera's far plane, not used in RT
        readFloat(br);
        //TODO Set the camera's aspect ratio (not used; overridden by image size)
        readFloat(br);
        //TODO Set the camera's height, which is 2 tan(yFieldOfView / 2)
        readFloat(br);

        //The following code to optionally read the number of samples
        //and image size while ignoring other named parameters is an
        //example of how you can handle this task for the many types
        //of objects that have optional parameters.
        while (peek(br).type != Token.RBRACK) {
            String parameterName = readString(br);
            if (parameterName.equals("samples")) {
                readType(br, Token.LBRACK);
                readInt(br); // TODO Set number of samples for antialiasing
                readType(br, Token.RBRACK);
            } else if (parameterName.equals("image")) {
                readType(br, Token.LBRACK);
                readInt(br);
                readInt(br); // TODO Set image size
                readType(br, Token.RBRACK);
            } else {
                //Silently ignore unsupported parameter by reading
                //a matching pair of square brackets.
                readType(br, Token.LBRACK);
                while (peek(br).type != Token.RBRACK) pop(br);
                readType(br, Token.RBRACK);
            }
        }

        readType(br, Token.RBRACK);

        //TODO Return the camera
        return null;
    }
    
    private static Object readBezierSegment(BufferedReader br) throws IOException {
        readString(br, "BezierSegment");
        readType(br, Token.LBRACK);
      
        //TODO Read the first control point
        readVector2(br);
        //TODO Read the second control point
        readVector2(br);
        //TODO Read the third control point
        readVector2(br);
        //TODO Read the fourth control point
        readVector2(br);
        
        readType(br, Token.RBRACK);
        
        //TODO Return the segment
        return null;
    }
    
    private static String readString(BufferedReader br) throws IOException {
        Token current = pop(br);
        if (current.type != Token.STRING) {
            throwException("String expected: type found = " + current, current);
        }
        return current.value;
    }

    private static void readString(BufferedReader br, String goal) throws IOException {
        Token current = pop(br);
        if (current.type != Token.STRING) {
            throwException("String expected: type found = " + current, current);
        }
        if (!current.value.equals(goal)) {
            throwException("\"" + goal + "\" expected, \"" + current.value + "\" found",
                           current);
        }
    }
    
    private static void readType(BufferedReader br, int type) throws IOException {
        Token current = pop(br);
        if (current.type != type) {
            throwException("\"" + Token.toString(type) + "\" expected: type found = " + current, current);
        }
    }
    
    private static int readInt(BufferedReader br) throws IOException {
        Token current = pop(br);
        if (current.type != Token.NUMBER) {
            throwException("Number expected: type found = " + current, current);
        }
        
        return Integer.parseInt(current.value);
    }
    
    private static float readFloat(BufferedReader br) throws IOException {
        Token current = pop(br);
        if (current.type != Token.NUMBER) {
            throwException("Number expected: type found = " + current, current);
        }
        
        return Float.parseFloat(current.value);
    }
    
    private static Vector3f readVector3(BufferedReader br) throws IOException {
        Vector3f toReturn = new Vector3f();
        
        readType(br, Token.LPAREN);
        toReturn.x = readFloat(br);
        readType(br, Token.COMMA);
        toReturn.y = readFloat(br);
        readType(br, Token.COMMA);
        toReturn.z = readFloat(br);
        readType(br, Token.RPAREN);
        
        return toReturn;
    }
    
    private static Vector2f readVector2(BufferedReader br) throws IOException {
        Vector2f toReturn = new Vector2f();
        
        readType(br, Token.LPAREN);
        toReturn.x = readFloat(br);
        readType(br, Token.COMMA);
        toReturn.y = readFloat(br);
        readType(br, Token.RPAREN);
        
        return toReturn;
    }
    
    private static Token peek(BufferedReader br) throws IOException {
        if (currentTokenValid) {
            return currentToken;
        }
            
        currentToken = parseToken(br);
        currentTokenValid = true;
        return currentToken;
    }
    
    private static Token pop(BufferedReader br) throws IOException {
        if (currentTokenValid) {
            currentTokenValid = false;
            return currentToken;
        }
        
        return parseToken(br);
    }
    
    private static boolean currentTokenValid = false;
    private static Token currentToken;
    
    private static Token parseToken(BufferedReader br) throws IOException {
        Token token = new Token();
        
        skipWhitespace(br);
        
        token.line = currentLine;
        token.column = currentColumn;
        
        switch(peekChar(br)) {
        case  -1: token.type = Token.EOF; return token;
        case '(': popChar(br); token.type = Token.LPAREN; return token;
        case ')': popChar(br); token.type = Token.RPAREN; return token;
        case '{': popChar(br); token.type = Token.LBRACE; return token;
        case '}': popChar(br); token.type = Token.RBRACE; return token;
        case '[': popChar(br); token.type = Token.LBRACK; return token;
        case ']': popChar(br); token.type = Token.RBRACK; return token;
        case ',': popChar(br); token.type = Token.COMMA; return token;
        }
        
        if (Character.isLetter((char)peekChar(br))) {
            token.type = Token.STRING;
            token.value = "" + (char)popChar(br);
            while (Character.isLetter((char)peekChar(br))) {
                token.value += (char)popChar(br);
            }
        } else if (peekChar(br) == '"') {
            token.type = Token.STRING;
            token.value = "";
            popChar(br);
            while (peekChar(br) != '\n' && peekChar(br) != '\r' && peekChar(br) != '"') {
                token.value += (char)popChar(br);
            }
            if (peekChar(br) == '"')
                popChar(br);
            else
                throwException("Newline in string: ", token);
        } else if (Character.isDigit((char)peekChar(br)) ||
                   peekChar(br) == '-') {
            token.type = Token.NUMBER;
            token.value = "" + (char)popChar(br);
            while (Character.isDigit((char)peekChar(br)) || peekChar(br) == '.' ||
                   peekChar(br) == 'E' || peekChar(br) == '-') {
                token.value += (char)popChar(br);
            }
        } else {
            throwException("Unrecognized character: " + ((char)peekChar(br)), token);
        }
        
        return token;
    }
    
    private static void skipWhitespace(BufferedReader br) throws IOException {
        while (peekChar(br) != -1 && Character.isWhitespace((char)peekChar(br))) {
            popChar(br);
        }
    }
    
    private static int peekChar(BufferedReader br) throws IOException {
        if (currentCharValid) {
            return currentChar;
        }
        
        currentChar = br.read();
        currentCharValid = true;
        advancePosition(currentChar);
        return currentChar;
    }
    
    private static int popChar(BufferedReader br) throws IOException {
        if (currentCharValid) {
            currentCharValid = false;
            return currentChar;
        }
        
        currentChar = br.read();
        advancePosition(currentChar);
        return currentChar;
    }
    
    private static boolean currentCharValid = false;
    private static int currentChar;
    
    private static int currentLine;
    private static int currentColumn;
    
    private static void throwException(String msg, Token token) {
        throw new RuntimeException
        (msg + " on line " + token.line + " at column " + token.column);
    }
    
    private static void throwException(String msg) {
        throw new RuntimeException
        (msg + " on line " + currentLine + " at column " + currentColumn);
    }
    
    private static void advancePosition(int character) {
        currentColumn++;
        if (character == '\n') {
            currentLine++;
            currentColumn = 0;
        }
    }
    
    private static void resetStaticData() {
        currentCharValid = false;
        currentTokenValid = false;
        currentLine = 1;
        currentColumn = 0;
    }
    
    static class Token {
        
        public static final int EOF = 0;
        public static final int LPAREN = 1;
        public static final int RPAREN = 2;
        public static final int LBRACE = 3;
        public static final int RBRACE = 4;
        public static final int LBRACK = 5;
        public static final int RBRACK = 6;
        public static final int COMMA = 7;
        public static final int STRING = 8;
        public static final int NUMBER = 9;
        
        public int type;
        public String value;
        public int line;
        public int column;
    
        public static String toString(int type) {
            switch(type) {
            case EOF: return "<EOF>";
            case LPAREN: return "(";
            case RPAREN: return ")";
            case LBRACE: return "{";
            case RBRACE: return "}";
            case LBRACK: return "[";
            case RBRACK: return "]";
            case STRING: return "String";
            case NUMBER: return "Number";
            case COMMA: return ",";
            default: return "Unknown";
            }
        }
        
        public String toString() {
            return toString(type);
        }

    }
    
}
