import java.awt.Point;
import java.awt.Dimension;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

/**
 * This class is an ADT that represents a maze.  This class is initialized
 * by passing a map configuration file to <code>initialize()</code>.  Once
 * the <code>Maze</code> is initialized, various operations may be
 * performed.<p>
 * For operations involving coordinates, the x coordinate starts at zero,
 * and increases as it moves to the right.  The y coordinate starts at 
 * zero, and increases as it moves down.  Thus, in the maze below, A is
 * at (0,0), B is at (2,0), and C is at (0,2).<p>
 * <pre>
 * AxB
 * xxx
 * Cxx
 * </pre>
 */
public class Maze{

    /**
     * predefined char constant for maze wall ('x')
     */
    public static final char WALL = 'x';

    /**
     * predefined char constant for maze starting
     * location ('S')
     */     
    public static final char START = 'S';

    /**
     * predefined char constant for maze finishing
     * location ('F')
     */
    public static final char FINISH = 'F';

    /**
     * predefined char constant for a open path
     * in the maze (' ') - a space
     */
    public static final char PATH = ' ';

    /**
     * predefined char constant for a path that
     * was visited but not part of the found
     * solution ('-')
     */
    public static final char VISITED = '-';

    /**
     * predefined char constant for a path
     * that was part of the found solution ('.')
     */
    public static final char SOLUTION = '.';

    /**
     * holds the maze information. first index
     * is the row. second index is the column
     */
    private char[][] _maze;

    /**
     * holds the location of the starting
     * location in the maze
     */
    private Point _start;

    /**
     * holds the location of the finishing
     * location in the maze
     */
    private Point _finish;

    /**
     * holds the size of the maze
     */
    private Dimension _size;

    /**
     * true if debugging, otherwise false
     */
    private boolean _debug;

    /**
     * creates a <code>Maze</code> object with no debugging
     */
    public Maze(){
	//nothing
    }

    /**
     * creates a <code>Maze</code> object with debugging preference
     * @param debug true if debugging should be turned on, false otherwise
     */
    public Maze(boolean debug){
	this();
	_debug = debug;
    } 

    /**
     * initializes the maze from a filename.  Exceptions are outputted to
     * stardard error.
     * 
     * @param filename the name of the file that holds the map configuration
     * @return true if the initialization is successful, otherwise false
     */
    public boolean initialize(String filename){
	int numColumns = 0;
	int numRows = 0;
	BufferedReader br = null;

	//
	// used for iteration
	//
	int row = -1;
	int column = -1;
	try{
	    br = new BufferedReader(new FileReader(filename));
	    
	    //
	    // read dimensions
	    //
	    numColumns = Integer.parseInt(br.readLine());
	    if (numColumns == 0){
		throw new IllegalArgumentException("maze width must be greater than zero");
	    }
	    numRows = Integer.parseInt(br.readLine());
	    if (numRows == 0){
		throw new IllegalArgumentException("maze height must be greater than zero");
	    }
	    _maze = new char[numRows][numColumns];
	    _size = new Dimension(numColumns, numRows);

	    //
	    // read maze configuration
	    //	    
	    for (row = 0; row < numRows; row++){
		for (column = 0; column < numColumns; column++){
		    int charInt;

		    //
		    // skips end of line characters
		    //
		    do{
			charInt = br.read();  //read returns an integer between 0x0000-0xffff (unicode)
		    }while (!isMazeChar(charInt) && charInt != -1 );

		    if (charInt == -1){  //read returns -1 at the end of the stream
			throw new IOException("unexpected end of file at row " + row + ", column " + column);
		    }
		    
		    char c = (char)charInt;

		    if (_debug) System.out.println("Maze.initialize(): reading row " + row + ", column " + column + " = " + charInt + " (" + c + ")");
		    
		    _maze[row][column] = c;
		    if (c == START){
			_start = new Point(column, row);
		    }
		    else if (c == FINISH){
			_finish = new Point(column, row);
		    }
		}
	    }
	   
	    return true;
	}
	catch(IOException e){
	    handleExpectedException(e);
	    return false;
	}
	catch(NumberFormatException e){
	    handleExpectedException(new NumberFormatException("unable to read row or column information"));
	    return false;
	}
	catch(IllegalArgumentException e){
	    handleExpectedException(e);
	    return false;
	}
	catch(Exception e){
	    //
	    // only expecting the two above exceptions.  
	    // if something else happens, want details
	    //
	    System.out.println("while reading row " + row + ", column " + column);
	    e.printStackTrace();
	    return false;
	}
	finally{
	    try{
		br.close();
	    }
	    catch(IOException e){
		e.printStackTrace();
	    }
	}

    }//end initialize

    /**
     * method to handle known generated execptions
     * in initialize.
     *
     * @param the exception to handle
     */
    private void handleExpectedException(Exception e){
	System.err.println(e);
    }

    /**
     * returns the location of the start of the maze.
     *
     * @return the location of the maze entrance or null if no entrance
     *         has been defined
     */
    public Point getStart(){
	return _start == null ? null : (Point)_start.clone();
    }

    /**
     * returns the location of the end of the maze.
     *
     * @return the location of the maze exit or null if no exit has been defined
     */
    public Point getFinish(){
	return _finish == null ? null : (Point)_finish.clone();
    }

    /**
     * returns the size of the maze.
     *
     * @return the size of the maze
     */
    public Dimension getSize(){
	return (Dimension)_size.clone();
    }
    
    /**
     * allows a view of a maze location.
     *
     * @param location the location to <code>peek</code> at
     * @return the char at the request maze location
     * @throws IllegalArgumentException if attempt is made to
     *         <code>peek</code> outside of maze boundries
     */
    public char peek(Point location){
        if (isInMaze(location)){	    
	    return _maze[location.y][location.x];
	}
	else{
	    throw new IllegalArgumentException(location.toString());
	}
    }

    /**
     * checks if a location coordinate is in this maze
     * @param location the location to check
     * @returns true if the location is legal for this maze,
     *          otherwise false
     */
    public boolean isInMaze(Point location){
        return
          location.x < _size.width &&
          location.y < _size.height &&
          location.x >= 0 &&
          location.y >= 0;
    }

    /**
     * allows modification of a maze PATH, VISITED, or SOLUTION location
     *
     * @param location the coordinates of the space to set
     * @param c the value to place into the space
     * @throws IllegalArgumentException if attempt is made to
     *         <code>setSpace</code> 
     *	       outside of maze boundries or an illegal modification
     *         attempt is made
     */
    public void setSpace(Point location, char c){
	if (!isInMaze(location)){
	    throw new IllegalArgumentException(location.toString());
        }

	if (_maze[location.y][location.x] == PATH ||
	    _maze[location.y][location.x] == SOLUTION||
	    _maze[location.y][location.x] == VISITED){

	    _maze[location.y][location.x] = c;
	}
	else{
	    throw new IllegalArgumentException("maze location at row " + location.y + ", column " + location.x + " is not a PATH, SOLUTION, or VISITED location");
	}
    }	    

    /**
     * prints out the current maze.
     */
    public void printMaze(){
	for (int row = 0; row < _size.height; row++){
	    System.out.println(String.valueOf(_maze[row]));
	}
    }

    /**
     * for testing purposes.  Default test file is maze1.txt.  Different file may be entered on command line.
     */
    public static void main(String[] args){
	Maze maze = new Maze();
	String mazeConfigFilename = args.length > 0 ? args[0] : "maze1.txt";
	System.out.println("reading maze configuration file (" + mazeConfigFilename + ")...");
	if (maze.initialize(mazeConfigFilename)){
	    System.out.println("OK!");
            maze.printMaze();
	}
	else{
	    System.out.println("error reading maze");
        }
    }
	
    /**
     * returns whether or not the character is a
     * defined maze character.
     *
     * @param c the character to test
     * @return true if this is a defined maze character
     */
    public boolean isMazeChar(int c){
	return 
	    c == PATH ||
	    c == SOLUTION ||
	    c == VISITED ||
	    c == WALL ||
	    c == START ||
	    c == FINISH;
    }
}//end class Maze








