// --------------------------------------------------------------------------------------------------
//  Copyright (c) 2016 Microsoft Corporation
//  
//  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
//  associated documentation files (the "Software"), to deal in the Software without restriction,
//  including without limitation the rights to use, copy, modify, merge, publish, distribute,
//  sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
//  
//  The above copyright notice and this permission notice shall be included in all copies or
//  substantial portions of the Software.
//  
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
//  NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
//  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
//  DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// --------------------------------------------------------------------------------------------------

package com.microsoft.Malmo.MissionHandlers;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Random;

import net.minecraft.block.BlockDoor;
import net.minecraft.block.state.IBlockState;
import net.minecraft.init.Blocks;
import net.minecraft.server.MinecraftServer;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;

import com.microsoft.Malmo.MissionHandlerInterfaces.IWorldDecorator;
import com.microsoft.Malmo.Schemas.AgentHandlers;
import com.microsoft.Malmo.Schemas.AgentSection;
import com.microsoft.Malmo.Schemas.ClassroomDecorator;
import com.microsoft.Malmo.Schemas.Colour;
import com.microsoft.Malmo.Schemas.Facing;
import com.microsoft.Malmo.Schemas.MissionInit;
import com.microsoft.Malmo.Schemas.PosAndDirection;
import com.microsoft.Malmo.Schemas.Variation;
import com.microsoft.Malmo.Utils.BlockDrawingHelper;
import com.microsoft.Malmo.Utils.BlockDrawingHelper.XMLBlockState;
import com.microsoft.Malmo.Utils.Discrete;
import com.microsoft.Malmo.Utils.SeedHelper;

/**
 * This class provides a decorator that will generate a random building full of rooms, where moving
 * from room to room requires overcoming an obstacle.  The rooms of the building are generated by 
 * slicing walls and floors throughout the entire structure at random points within the building.
 * This creates a 3D grid of rooms.  The algorithm then determines a random path through that grid
 * that defines the route from the starting position to the goal, and joins any orphan rooms back
 * to that main path. The final step is to determine which obstacle shall be used to block the agent's
 * path for each room on a path.  This whole generative process is driven by four
 * distributions: a Gaussian over the overall size of the building, a discrete distribution over
 * the kinds of divisions to make, a discrete distribution over horizontal obstacles, and a discrete
 * distribution over vertical obstacles. These can either be tuned via four complexity parameters or 
 * by manually specifying all of the parameters and distributions.
 * 
 * The complexity parameters are designed such that a vector [0, 0, 0, 0] (corresponding to building,
 * path, division and obstacle complexities of 0) will be a single room with the goal in it, and a
 * vector [1, 1, 1, 1] will be a large building with up to 64 rooms where the goal will be 32 rooms
 * away from the starting position and the remaining 32 rooms will be joined in random subpaths back to
 * that path, creating a difficult maze. As each of the dimensions increases from 0 to 1 it becomes more
 * difficult. Building complexity increases the size of the building and therefore the likelihood of
 * larger numbers of rooms.  Path complexity increases the number of false trails within the building.
 * Division complexity increases the kinds and frequencies of various divisions, from only South-North
 * walls to East-West walls and eventually multiple floors. Obstacle complexity increases the difficulty
 * of the obstacles which will meet the agent, from simple gaps and staircases to bridges over lava,
 * puzzle doors and parkour elements.
 */
public class ClassroomDecoratorImplementation extends HandlerBase implements IWorldDecorator {
    private static int MIN_ROOM_SIZE = 7;
    private static int MAX_ROOMS = 3;
    private static int MAX_BUILDING_SIZE = MAX_ROOMS*(MIN_ROOM_SIZE + 1);
    private static int ROOM_HEIGHT = 4;
    private static int START_X = 0;
    private static int START_Y = 55;
    private static int START_Z = 0;
    private static int FLOOR_HEIGHT = 2;
        
    private Random rand;
    private Discrete divisionTypeDist;
    private Discrete horizontalTypeDist;
    private Discrete verticalTypeDist;
    private double hintLikelihood;
    private int buildingWidth;
    private int buildingHeight;
    private int buildingLength;
    private double buildingComplexity;
    private int pathLength;
    private double pathComplexity;
    private boolean isSpec;
    
    private int[] pathIndices = new int[]{0, 1, 2, 3, 4, 5};
    
    private Palette palette;
    private BlockDrawingHelper drawContext;

    @Override
    public void buildOnWorld(MissionInit missionInit, World world) throws DecoratorException {
        this.drawContext = new BlockDrawingHelper();
        if(this.buildingWidth == 0){
            // We are using complexity so these need to be sampled from the Gaussian
            this.buildingWidth = Math.max((int)(rand.nextGaussian()*2 + this.buildingComplexity*MAX_BUILDING_SIZE + MIN_ROOM_SIZE), MIN_ROOM_SIZE);
            this.buildingLength = Math.max((int)(rand.nextGaussian()*2 + this.buildingComplexity*MAX_BUILDING_SIZE + MIN_ROOM_SIZE), MIN_ROOM_SIZE);
            this.buildingHeight = Math.max(Math.max(this.buildingWidth, this.buildingLength) * ROOM_HEIGHT / MIN_ROOM_SIZE, ROOM_HEIGHT);
        }
        
        // create the room grid
        ArrayList<Room> rooms = this.createRooms();
        
        // randomize the indices used to query different divisions.  This has the effect of making
        // the path to the goal random each time.
        this.shuffleIndices();
        
        // determine how long the path to the goal should be
        if(this.pathLength == 0){
            this.pathLength = (int)((1 - 0.25 * this.pathComplexity)*rooms.size()) - 1;
        }else{
            this.pathLength = Math.min(this.pathLength, rooms.size() - 1);
        }
        
        // find a path to the goal
        ArrayList<Divider> path = new ArrayList<Divider>();        
        Room startRoom, goalRoom;
        if(this.pathLength > 0){                    
            for(Room room : rooms){
                for(Room markRoom : rooms){
                	markRoom.mark = false;
                }
                
                if(findPath(path, room, this.pathLength)){
                    break;
                }               
            }
            
            if(path.size() < this.pathLength){
                // error
                throw new DecoratorException("Unable to find path to goal");
            }
            
            startRoom = path.get(0).getIn();
            goalRoom = path.get(path.size() - 1).getOut();
            
            for(Divider divider : path){
            	divider.setHint(this.rand.nextDouble() < this.hintLikelihood);
            }
        
            // create all of the obstacles along the path
            createObstacles(path);
        }else{
            startRoom = goalRoom = rooms.get(0);
            startRoom.isComplete = true;
        }

        // find orphan rooms
        for(Room room : rooms){
            if(room.isComplete){
                continue;
            }
            
            for(Room markRoom : rooms){
                markRoom.mark = false;
            }    
            
            path.clear();
            
            if(findPath(path, room, rooms.size())){
                // reverse the directions (the algorithm finds a path from the start room
                // to the end room, so we will reverse it so it goes from the completed
                // portion of the building back to the orphan room.
                for(Divider obstacle : path){
                    obstacle.reverse();
                }
                
                // create all of the obstacles along the path
                createObstacles(path);
            }else{
                throw new DecoratorException("Unable to join orphan room to goal path");
            }
        }

        // carve out the building
        this.drawContext.beginDrawing(world);
        for(int x=START_X; x<START_X + this.buildingWidth; x++){
            for(int y=START_Y; y<START_Y + this.buildingHeight; y++){
                for(int z=START_Z; z<START_Z + this.buildingLength; z++){
                    world.setBlockToAir(new BlockPos(x, y, z));
                }
            }
        }
        
        // this should clear all of the torches and levers left over from last time.  It doesn't.
        drawContext.clearEntities(world, START_X - 1, START_Y - 1, START_Z - 1, START_X + this.buildingWidth, START_Y + this.buildingHeight, START_Z + this.buildingLength);
        
        // draw the rooms
        for(Room room : rooms){
            room.draw(world, this.rand, this.palette);
        }
        
        // place goal
        setBlockState(world, new BlockPos(goalRoom.x+this.rand.nextInt(goalRoom.width-4) + 2, goalRoom.y, goalRoom.z + goalRoom.length - 2), this.palette.goal);
        this.drawContext.endDrawing(world);
        // set the agent positions
        PosAndDirection p2 = new PosAndDirection();
        p2.setX(new BigDecimal(startRoom.x + this.rand.nextInt(goalRoom.width-2) + 0.5));
        p2.setY(new BigDecimal(1 + startRoom.y));
        p2.setZ(new BigDecimal(startRoom.z + 0.5));

        // TODO - for the moment, force all players to being at the maze start point - but this needs to be optional.
        for (AgentSection as : missionInit.getMission().getAgentSection())
        {
            p2.setPitch(as.getAgentStart().getPlacement().getPitch());
            p2.setYaw(as.getAgentStart().getPlacement().getYaw());
            as.getAgentStart().setPlacement(p2);
        }
    }
    
    @Override
    public boolean parseParameters(Object params)
    {
        if (params == null || !(params instanceof ClassroomDecorator))
            return false;
        ClassroomDecorator classroomParams = (ClassroomDecorator)params;
        
        
        if(classroomParams.getSeed() == null){
            this.rand = SeedHelper.getRandom();
        }else{
        	try{
        		this.rand = new Random(Integer.parseInt(classroomParams.getSeed()));
        	}catch(NumberFormatException ex){
        		this.rand = SeedHelper.getRandom();
        	}
        }
        
        if(classroomParams.getPalette() != null){        	
        	String palette = classroomParams.getPalette().value();
        	if(palette.equals("random")){
        		this.palette = new Palette(this.rand);
        	}else{        	
        		this.palette = new Palette(palette);
        	}
        }else{
            this.palette = new Palette("default");
        }    
        
        if(classroomParams.getComplexity() != null){            
            ClassroomDecorator.Complexity complexity = classroomParams.getComplexity();
            this.buildingComplexity = complexity.getBuilding();
            this.pathComplexity = complexity.getPath();
            this.divisionTypeDist = new Discrete(this.rand, Division.values().length, complexity.getDivision());
            this.horizontalTypeDist = new Discrete(this.rand, HorizontalObstacle.values().length, complexity.getObstacle());
            this.verticalTypeDist = new Discrete(this.rand, VerticalObstacle.values().length, complexity.getObstacle());
            this.hintLikelihood = 1.0 - complexity.getHint();
        }else{
        	this.isSpec = true;
            ClassroomDecorator.Specification spec = classroomParams.getSpecification();
            this.buildingWidth = spec.getWidth();
            this.buildingHeight = spec.getHeight();
            this.buildingLength = spec.getLength();
            this.pathLength = spec.getPathLength();
            this.divisionTypeDist = new Discrete(this.rand, new int[]{
                    spec.getDivisions().getSouthNorth(),
                    spec.getDivisions().getEastWest(),
                    spec.getDivisions().getAboveBelow()
            });
            
            this.horizontalTypeDist = new Discrete(this.rand, new int[]{
                    spec.getHorizontalObstacles().getGap(),
                    spec.getHorizontalObstacles().getBridge(),
                    spec.getHorizontalObstacles().getDoor(),
                    spec.getHorizontalObstacles().getPuzzle(),
                    spec.getHorizontalObstacles().getJump()        	
            });
            
            this.verticalTypeDist = new Discrete(this.rand, new int[]{
                    spec.getVerticalObstacles().getStairs(),
                    spec.getVerticalObstacles().getLadder(),
                    spec.getVerticalObstacles().getJump()
            });
            
            this.hintLikelihood = spec.getHintLikelihood();
        }
            
        return true;
    }

    @Override
    public void update(World world) {
    }

    private void setBlockState(World world, BlockPos pos, IBlockState state)
    {
        drawContext.setBlockState(world,  pos, new XMLBlockState(state));
    }

    private void setBlockState(World world, BlockPos pos, IBlockState state, Colour c, Facing f, Variation v)
    {
        drawContext.setBlockState(world,  pos,  new XMLBlockState(state, c, f, v));
    }

    private ArrayList<Room> createRooms() throws DecoratorException
    {
        // we will set up the different building dimensions and then split them up sequentially
        ArrayList<Thickness> widths = new ArrayList<Thickness>();
        ArrayList<Thickness> lengths = new ArrayList<Thickness>();
        ArrayList<Thickness> heights = new ArrayList<Thickness>();
                
        widths.add(new Thickness(START_X, START_X + this.buildingWidth));
        heights.add(new Thickness(START_Y, START_Y + ROOM_HEIGHT));
        lengths.add(new Thickness(START_Z, START_Z + this.buildingLength));
            
        // we will keep sampling the divisions until we are unable to accomplish the
        // goal. This has the effect of making it so that we still see simpler structures even
        // in more complex vectors and effectively keep sampling the larger space of buildings.
        while(true){
            Division div = Division.fromOrdinal(this.isSpec ? this.divisionTypeDist.take() : this.divisionTypeDist.sample());
                        
            if(div == Division.AboveBelow){
                // floors are tricky as they have fixed height, so we just
                // stack them on top of each other until we run out of room.
                Thickness topFloor = heights.get(heights.size() - 1);
                if(topFloor.end + FLOOR_HEIGHT + ROOM_HEIGHT > START_Y + this.buildingHeight){
                    break;
                }
                
                heights.add(new Thickness(topFloor.end + FLOOR_HEIGHT, topFloor.end + ROOM_HEIGHT + FLOOR_HEIGHT));
            }else{
                ArrayList<Thickness> thicknesses;
                if(div == Division.SouthNorth){
                    thicknesses = lengths;
                }else if(div ==  Division.EastWest){
                    thicknesses = widths;
                }else{
                    throw new DecoratorException("Invalid division value: " + div);
                }
            
                // we need to see which thicknesses are able to be split. we then will
                // performed a biased sample by potential split location.
                int[] counts = new int[thicknesses.size()];
                int sum = 0;
                for(int i=0; i<counts.length; i++){
                    counts[i] = Math.max(0, thicknesses.get(i).getThickness() - (MIN_ROOM_SIZE * 2));
                    sum += counts[i];
                }
                
                if(sum == 0){
                    break;
                }
                
                Discrete dist = new Discrete(this.rand, counts);
                int index = dist.sample();
                Thickness start = thicknesses.get(index);
                int thickness = this.isSpec ? MIN_ROOM_SIZE : MIN_ROOM_SIZE + this.rand.nextInt(Math.min(3, counts[index]));
                Thickness end = new Thickness(start.start + thickness + 1, start.end);
                start.end = end.start - 1;
                thicknesses.add(index + 1, end);
            }
        }
        
        // create rooms
        ArrayList<Room> rooms = new ArrayList<Room>();
        Room[][][] roomGrid = new Room[heights.size()][][];
        for(int i=0; i<heights.size(); i++){
            roomGrid[i] = new Room[widths.size()][];
            Thickness height = heights.get(i);
            for(int j=0; j<widths.size(); j++){
                roomGrid[i][j] = new Room[lengths.size()];
                Thickness width = widths.get(j);
                for(int k=0; k<lengths.size(); k++){
                    Thickness length = lengths.get(k);
                    Room room = new Room(width.start, height.start, length.start, 
                                         width.getThickness(), height.getThickness(), length.getThickness());
                    roomGrid[i][j][k] = room;
                    rooms.add(room);
                }
            }
        }
        
        // set up walls
        for(int i=0; i<roomGrid.length; i++){
            for(int j=0; j<roomGrid[i].length; j++){
                for(int k=0; k<roomGrid[i][j].length; k++){
                    Room room = roomGrid[i][j][k];
                    
                    if(i == 0){
                        room.belowFloor = new Floor(room, null);
                    }
                    
                    if(j == 0){
                        room.westWall = new EWWall(room, null);
                    }
                    
                    if(k == 0){
                        room.northWall = new SNWall(room, null);
                    }
                    
                    if(i < roomGrid.length - 1){
                        Room above = roomGrid[i+1][j][k];
                        room.aboveFloor = above.belowFloor = new Floor(above, room);
                    }else{
                        room.aboveFloor = new Floor(null, room);
                    }
                    
                    if(j < roomGrid[i].length - 1){
                        Room east = roomGrid[i][j+1][k];
                        room.eastWall = east.westWall = new EWWall(east, room);
                    }else{
                        room.eastWall = new EWWall(null, room);
                    }
                    
                    if(k < roomGrid[i][j].length -1){
                        Room south = roomGrid[i][j][k+1];
                        room.southWall = south.northWall = new SNWall(south, room);
                    }else{
                        room.southWall = new SNWall(null, room);
                    }
                }
            }
        }
        
        return rooms;
    }
    
    private boolean findPath(ArrayList<Divider> path, Divider divider, int target, boolean direction)
    {
        // each divider has a direction that the agent can move through it. As such,
        // we need to set this to verify that the directed path we are building will
        // be correct, but then put it back again if we're wrong.
        boolean originalDirection = divider.getDirection();
        divider.setDirection(direction);
        
        Room room = divider.getOut();
        
        if(room == null){
            divider.setDirection(originalDirection);
            return false;
        }
        
        if(room.mark){
            divider.setDirection(originalDirection);
            return false;
        }
        
        path.add(divider);
        
        if(room.isComplete){
            return true;
        }
        
        return findPath(path, room, target - 1);
    }
    
    private boolean findPath(ArrayList<Divider> path, Room room, int target)
    {   
        if(target == 0){
            return true;
        }
        
        room.mark = true;
        
        // TODO not entirely clear why reshuffling each time causes failure
        // this.shuffleIndices();
        
        // Hate to use parallel arrays, but Java has horrendous support for arrays of
        // generics and five differently bad Tuple implementations. I could create a class
        // for this one situation but I just can't bring myself to do it.  MJ
        Divider[] dividers = new Divider[]{room.southWall, room.northWall, room.eastWall, room.westWall, room.aboveFloor, room.belowFloor};
        boolean[] directions = new boolean[]{true, false, true, false, true, false};
        
        // Try each divider and see if it works for a potential path. This is essentially a depth
        // first search for a path of the desired size.
        for(int i=0; i<pathIndices.length; i++){
            int index = this.pathIndices[i];
            if(findPath(path, dividers[index], target, directions[index]))
            {
                return true;
            }
        }     
        
        path.clear();
        return false;
    }
    
    private void createObstacles(ArrayList<Divider> path)
    {
        // sample from the obstacle distribution and set the obstacles.
        path.get(0).getIn().isComplete = true;
        for(Divider obstacle : path){
            switch(obstacle.getType()){
            case SouthNorth:
                SNWall nsWall = (SNWall)obstacle;
                nsWall.isObstacle = true;
                nsWall.obstacle = HorizontalObstacle.fromOrdinal(this.isSpec ? horizontalTypeDist.take() : horizontalTypeDist.sample());
                break;
                
            case EastWest:
                EWWall ewWall = (EWWall)obstacle;
                ewWall.isObstacle = true;
                ewWall.obstacle = HorizontalObstacle.fromOrdinal(this.isSpec ? horizontalTypeDist.take() : horizontalTypeDist.sample());
                break;
                
            case AboveBelow:
                Floor floor = (Floor)obstacle;
                floor.isObstacle = true;
                floor.obstacle = VerticalObstacle.fromOrdinal(this.isSpec ? verticalTypeDist.take() : verticalTypeDist.sample());
                break;
            }
            
            obstacle.getOut().isComplete = true;
        }
    }
    
    private void shuffleIndices()
    {
        for(int i=0; i<5; i++){			
            int j = this.rand.nextInt(6 - i);
            int tmp = this.pathIndices[i];
            this.pathIndices[i] = this.pathIndices[i+j];
            this.pathIndices[i+j] = tmp;
        }    	
    }
    
    // ENUMS //
    
    public enum HorizontalObstacle { Gap, Bridge, Door, Puzzle, Jump;
        private static HorizontalObstacle[] allValues = values();
        public static HorizontalObstacle fromOrdinal(int n) {return allValues[n];}
    }
    
    
    public enum VerticalObstacle { Stairs, Ladder, Jump;
        private static VerticalObstacle[] allValues = values();
        public static VerticalObstacle fromOrdinal(int n) {return allValues[n];}
    }
    
    
    public enum Division { SouthNorth, EastWest, AboveBelow;
        private static Division[] allValues = values();
        public static Division fromOrdinal(int n) {return allValues[n];}
    }
    
    // CLASSES //

    private class Thickness {
        public int start;
        public int end;
            
        public Thickness(int start, int end)
        {
            this.start = start;
            this.end = end;
        }
        
        public int getThickness()
        {
            return this.end - this.start;
        }
    }
    
    private interface Divider {
        public Room getIn();
        public Room getOut();
        public Division getType();
        public void reverse();
        public boolean getDirection();
        public void setDirection(boolean direction);
        public void setHint(boolean hint);
        public boolean getHint();
    }
    
    private class EWWall implements Divider{
        public Room east;
        public Room west;
        
        public boolean isObstacle;
        public HorizontalObstacle obstacle;
        
        private boolean direction;
        private boolean hint;
        private int front;
        private int back;
        private int bottom;
        private int top;
        private int x;
        
        private boolean isDrawn;
        
        public EWWall(Room east, Room west)
        {
            this.east = east;
            this.west = west;
            
            Room room = east == null ? west : east;
            this.front = room.z;
            this.back = front + room.length;
            this.bottom = room.y;
            this.top = bottom + room.height;
            this.x = room == east ? room.x - 1 : room.x + room.width;
        }
        
        public boolean getHint()
        {
        	return this.hint;
        }
        
        public void setHint(boolean hint)
        {
        	this.hint = hint;
        }
        
        public Room getIn()
        {
            return this.direction ? this.west : this.east;
        }
        
        public Room getOut()
        {
            return this.direction ? this.east : this.west;
        }
        
        public void reverse()
        {
            this.direction = !this.direction;
        }
        
        public Division getType()
        {
            return Division.EastWest;
        }
        
        public boolean getDirection()
        {
            return this.direction;
        }
        
        public void setDirection(boolean direction)
        {
            this.direction = direction;
        }

        public void draw(World world, Random rand, Palette palette)
        {
            if(this.isDrawn){
                return;
            }
            
            this.isDrawn = true;
            
            for(int z=this.front; z<this.back; z++){
                setBlockState(world, new BlockPos(this.x, this.bottom - 1, z), palette.wall);
                setBlockState(world, new BlockPos(this.x, this.top, z), palette.wall);
            }

            for(int y=this.bottom; y<this.top; y++){
                setBlockState(world, new BlockPos(this.x, y, this.front-1), palette.wall);
                setBlockState(world, new BlockPos(this.x, y, this.back), palette.wall);
            }

            if(this.isObstacle){
                switch(this.obstacle){
                    case Gap:
                        this.drawGap(world, rand, palette);
                        break;    
                        
                    case Bridge:
                        this.drawBridge(world,  rand,  palette);
                        break;
                        
                    case Door:
                        this.drawDoor(world,  rand,  palette);
                        break;
                        
                    case Puzzle:
                        this.drawPuzzle(world,  rand, palette);
                        break;
                        
                    case Jump:
                        this.drawJump(world,  palette);
                        break;
                    
                    default:
                        System.out.println(this.obstacle + " not implemented");
                        break;
                }
            }else{
                IBlockState block = this.east == null || this.west == null ? palette.exterior : palette.wall;
                for(int z=this.front; z<this.back; z++){               	                	
                    for(int y=this.bottom; y<this.top; y++){
                        setBlockState(world, new BlockPos(this.x,y,z), block);
                    }
                    
                    if((z - this.front - 1) % 2 == 0 && z < this.back - 1){
                        setBlockState(world, new BlockPos(this.x - 1, this.bottom + ROOM_HEIGHT - 1, z), palette.light, null, Facing.WEST, null);
                        setBlockState(world, new BlockPos(this.x + 1, this.bottom + ROOM_HEIGHT - 1, z), palette.light, null, Facing.EAST, null);                		
                    }
                }
            }
        }
        
        private void drawGap(World world, Random rand, Palette palette)
        {
            int gapStart = this.front + rand.nextInt(this.back - this.front - 2);
            int gapEnd = gapStart + 2;
            for(int z = this.front; z < this.back; z++){
                if(z < gapStart || z >= gapEnd){
                    for(int y=this.bottom - 1; y<this.top; y++){
                        setBlockState(world, new BlockPos(this.x,y,z), palette.wall);
                    }
                    
                    if((z - this.front - 1) % 2 == 0 && z < this.back - 1){
                        setBlockState(world, new BlockPos(this.x - 1, this.bottom + ROOM_HEIGHT - 1, z), palette.light, null, Facing.WEST, null);
                        setBlockState(world, new BlockPos(this.x + 1, this.bottom + ROOM_HEIGHT - 1, z), palette.light, null, Facing.EAST, null);                		
                    }
                }else{
                	IBlockState block = this.hint ? palette.hint : palette.wall;
                    setBlockState(world, new BlockPos(this.x, bottom-1, z), block);
                    for(int y=this.bottom; y<this.top; y++){
                        world.setBlockToAir(new BlockPos(this.x,y,z));
                    }
                }
            }
            
            setBlockState(world, new BlockPos(this.x, this.bottom + ROOM_HEIGHT - 1, gapStart), palette.light, null, Facing.SOUTH, null);
            setBlockState(world, new BlockPos(this.x, this.bottom + ROOM_HEIGHT - 1, gapEnd - 1), palette.light, null, Facing.NORTH, null);        	
        }
        
        private void drawBridge(World world, Random rand, Palette palette)
        {
            int bridgeStart = this.front + rand.nextInt(this.back - this.front - 4) + 1;
            int bridgeEnd = bridgeStart + 2;
            for(int z=this.front; z<this.back; z++){
                setBlockState(world, new BlockPos(this.x - 1, this.bottom - 1, z), palette.moatContainer);
                setBlockState(world, new BlockPos(this.x, this.bottom - 2, z), palette.moatContainer);
                setBlockState(world, new BlockPos(this.x + 1, this.bottom - 1, z), palette.moatContainer);
                if(z < bridgeStart || z >= bridgeEnd){            		
                    setBlockState(world, new BlockPos(this.x,this.bottom - 1,z), palette.moat);
                }else{
                	IBlockState block = this.hint ? palette.hint : palette.moatContainer;
                    setBlockState(world, new BlockPos(this.x,this.bottom - 1,z), block);
                }
            }
            
            setBlockState(world, new BlockPos(this.x, this.bottom + ROOM_HEIGHT - 1, this.front), palette.light, null, Facing.SOUTH, null);
            setBlockState(world, new BlockPos(this.x, this.bottom + ROOM_HEIGHT - 1, this.back - 1), palette.light, null, Facing.NORTH, null);        	
            
            setBlockState(world, new BlockPos(this.x, this.bottom - 1, this.front - 1), palette.moatContainer);
            setBlockState(world, new BlockPos(this.x, this.bottom - 1, this.back), palette.moatContainer);
        }
        
        private void drawDoor(World world, Random rand, Palette palette)
        {
            int doorLocation = this.front + rand.nextInt(this.back - this.front - 2) + 1;
            
            for(int z=this.front; z<this.back; z++){
                for(int y=this.bottom; y<this.top; y++){
                    setBlockState(world, new BlockPos(this.x,y,z), palette.wall);
                }    
                
                if((z - this.front - 1) % 2 == 0 && z < this.back - 1){            		
                    setBlockState(world, new BlockPos(this.x - 1, this.bottom + ROOM_HEIGHT - 1, z), palette.light, null, Facing.WEST, null);
                    setBlockState(world, new BlockPos(this.x + 1, this.bottom + ROOM_HEIGHT - 1, z), palette.light, null, Facing.EAST, null);                		
                }
            }
            
            Facing facing = this.getIn() == this.west ? Facing.WEST : Facing.EAST;
            
            setBlockState(world, new BlockPos(this.x, this.bottom, doorLocation), palette.doorLower, null, facing, null);
            setBlockState(world, new BlockPos(this.x, this.bottom + 1, doorLocation), palette.doorUpper, null, facing, null);
            if(this.hint){
            	setBlockState(world, new BlockPos(this.x, this.bottom - 1, doorLocation), palette.hint);
            	setBlockState(world, new BlockPos(this.x + 1, this.bottom - 1, doorLocation), palette.hint);
            }
        }
        
        private void drawPuzzle(World world, Random rand, Palette palette)
        {
            int doorLocation = this.front + rand.nextInt(this.back - this.front - 4) + 2;
            
            for(int z=this.front; z<this.back; z++){
                for(int y=this.bottom; y<this.top; y++){
                    setBlockState(world, new BlockPos(this.x,y,z), palette.wall);
                }    
                
                if((z - this.front - 1) % 2 == 0 && z < this.back - 1){            		
                    setBlockState(world, new BlockPos(this.x - 1, this.bottom + ROOM_HEIGHT - 1, z), palette.light, null, Facing.WEST, null);
                    setBlockState(world, new BlockPos(this.x + 1, this.bottom + ROOM_HEIGHT - 1, z), palette.light, null, Facing.EAST, null);                		
                }
            }        	
            
            Facing facing = this.getIn() == this.west ? Facing.WEST : Facing.EAST;
            
            setBlockState(world, new BlockPos(this.x, this.bottom, doorLocation), palette.puzzleDoorLower, null, facing, null);
            setBlockState(world, new BlockPos(this.x, this.bottom + 1, doorLocation), palette.puzzleDoorUpper, null, facing, null);
            if(this.hint){
            	setBlockState(world, new BlockPos(this.x, this.bottom - 1, doorLocation), palette.hint);
            	setBlockState(world, new BlockPos(this.x, this.bottom - 1, doorLocation), palette.hint);
            }
            
            Room room = getIn();
            int triggerZ;
            if(rand.nextDouble() < 0.5){
                triggerZ = doorLocation - 1;
            }else{
                triggerZ = doorLocation + 1;
            }
            
            if(room == this.west){
                setBlockState(world, new BlockPos(this.x - 1, this.bottom + 1, triggerZ), palette.trigger, null, Facing.WEST, null);
            }else{
                setBlockState(world, new BlockPos(this.x + 1, this.bottom + 1, triggerZ), palette.trigger, null, Facing.EAST, null);
            } 
        }
        
        private void drawJump(World world, Palette palette)
        {	
            int entrance = rand.nextInt(this.back - this.front - 3);
            int exit = rand.nextInt(this.back - this.front - 3);
            
            for(int z=this.front; z<this.back; z++){
                setBlockState(world, new BlockPos(this.x - 1, this.bottom - 1, z), palette.moatContainer);
                setBlockState(world, new BlockPos(this.x - 1, this.bottom, z), palette.wall);
                setBlockState(world, new BlockPos(this.x - 1, this.bottom + 1, z), palette.wall);
                setBlockState(world, new BlockPos(this.x, this.bottom - 2, z), palette.moatContainer);
                setBlockState(world, new BlockPos(this.x + 1, this.bottom - 1, z), palette.moatContainer);
                setBlockState(world, new BlockPos(this.x + 1, this.bottom, z), palette.wall);
                setBlockState(world, new BlockPos(this.x + 1, this.bottom + 1, z), palette.wall);
                
                if(z % 2 == 0){
                    setBlockState(world, new BlockPos(this.x, this.bottom - 1, z), palette.moatContainer);
                }else{        		
                    setBlockState(world, new BlockPos(this.x, this.bottom - 1, z), palette.moat);
                }
            }
            
            setBlockState(world, new BlockPos(this.x, this.bottom + ROOM_HEIGHT - 1, this.front), palette.light, null, Facing.SOUTH, null);
            setBlockState(world, new BlockPos(this.x, this.bottom + ROOM_HEIGHT - 1, this.back - 1), palette.light, null, Facing.NORTH, null);        	
            
            for(int i=0; i<3; i++){
                world.setBlockToAir(new BlockPos(this.x - 1, this.bottom + 1, this.front + entrance + i));
                world.setBlockToAir(new BlockPos(this.x + 1, this.bottom + 1, this.front + exit + i));
            }
            
            if(this.hint){
            	setBlockState(world, new BlockPos(this.x - 1, this.bottom, this.front + entrance + 1), palette.hint);
            	setBlockState(world, new BlockPos(this.x + 1, this.bottom, this.front + exit + 1), palette.hint);
            }
        }
    }
    
    private class SNWall implements Divider{
        public Room south;
        public Room north;
        
        public boolean isObstacle;
        public HorizontalObstacle obstacle;
        
        private boolean direction;
        private boolean hint;
        private int left;
        private int right;
        private int bottom;
        private int top;
        private int z;
        private boolean isDrawn;
        
        public SNWall(Room south, Room north)
        {
            this.south = south;
            this.north = north;
            
            Room room = south == null ? north : south;			
            
            this.left = room.x;
            this.right = left + room.width;
            this.bottom = room.y;
            this.top = bottom + room.height;
            this.z = room == south ? room.z - 1 : room.z + room.length;
        }
        
        public boolean getHint()
        {
        	return this.hint;
        }
        
        public void setHint(boolean hint)
        {
        	this.hint = hint;
        }
        
        public Room getIn()
        {
            return this.direction ? this.north : this.south;
        }
        
        public Room getOut()
        {
            return this.direction ? this.south : this.north;
        }
        
        public void reverse()
        {
            this.direction = !this.direction;
        }
        
        public Division getType()
        {
            return Division.SouthNorth;
        }
        
        public boolean getDirection()
        {
            return this.direction;
        }
        
        public void setDirection(boolean direction)
        {
            this.direction = direction;
        }
        
        public void draw(World world, Random rand, Palette palette)
        {
            if(this.isDrawn){
                return;
            }
            
            this.isDrawn = true;
            
            for(int x=this.left; x<this.right; x++){
                setBlockState(world, new BlockPos(x, this.bottom - 1, this.z), palette.wall);
                setBlockState(world, new BlockPos(x, this.top, this.z), palette.wall);
            }
            
            for(int y=this.bottom; y < this.top; y++){
                setBlockState(world, new BlockPos(this.left-1, y, this.z), palette.wall);
                setBlockState(world, new BlockPos(this.right, y, this.z), palette.wall);
            }
            
            if(this.isObstacle){
                switch(this.obstacle){
                    case Gap:
                        this.drawGap(world, rand, palette);
                        break;
                        
                    case Bridge:
                        this.drawBridge(world,  rand, palette);
                        break;
                        
                    case Door:
                        this.drawDoor(world, rand, palette);
                        break;
                        
                    case Puzzle:
                        this.drawPuzzle(world, rand, palette);
                        break;
                        
                    case Jump:
                        this.drawJump(world, rand, palette);
                        break;
                    
                    default:
                        System.out.println(this.obstacle + " not implemented");
                        break;
                }         
            }else{
                IBlockState block = this.north == null || this.south == null ? palette.exterior : palette.wall;
                for(int x=this.left; x<this.right; x++){
                    for(int y=this.bottom; y<this.top; y++){
                        setBlockState(world, new BlockPos(x, y, this.z), block);
                    }
                    
                    if((x - this.left - 1) % 2 == 0 && x < this.right - 1){
                        setBlockState(world, new BlockPos(x, this.bottom + ROOM_HEIGHT - 1, this.z - 1), palette.light, null, Facing.NORTH, null);
                        setBlockState(world, new BlockPos(x, this.bottom + ROOM_HEIGHT - 1, this.z + 1), palette.light, null, Facing.SOUTH, null);                		
                    }
                }
            }
        }
        
        private void drawGap(World world, Random rand, Palette palette)
        {
            int gapStart = this.left + rand.nextInt(this.right - this.left - 2);
            int gapEnd = gapStart + 2;
            for(int x=this.left; x<this.right; x++){
                if(x < gapStart || x >= gapEnd){
                    for(int y=this.bottom; y<this.top; y++){    	                	
                        setBlockState(world, new BlockPos(x, y, this.z), palette.wall);
                    }
                    
                    if((x - this.left - 1) % 2 == 0 && x < this.right - 1){
                        setBlockState(world, new BlockPos(x, this.bottom + ROOM_HEIGHT - 1, this.z - 1), palette.light, null, Facing.NORTH, null);
                        setBlockState(world, new BlockPos(x, this.bottom + ROOM_HEIGHT - 1, this.z + 1), palette.light, null, Facing.SOUTH, null);                		
                    }
                }else{
                	IBlockState block = this.hint ? palette.hint : palette.wall;
                    setBlockState(world, new BlockPos(x, bottom-1, this.z), block);
                    for(int y=this.bottom; y<this.top; y++){
                        world.setBlockToAir(new BlockPos(x,y,this.z));
                    }
                }
            }
            
            setBlockState(world, new BlockPos(gapStart, this.bottom + ROOM_HEIGHT - 1, z), palette.light, null, Facing.EAST, null);
            setBlockState(world, new BlockPos(gapEnd - 1, this.bottom + ROOM_HEIGHT - 1, z), palette.light, null, Facing.WEST, null);
        }
        
        private void drawBridge(World world, Random rand, Palette palette)
        {
            int bridgeStart = this.left + rand.nextInt(this.right - this.left - 4) + 1;
            int bridgeEnd = bridgeStart + 2;
            for(int x=this.left; x<this.right; x++){
                setBlockState(world, new BlockPos(x, this.bottom - 1, this.z-1), palette.moatContainer);
                setBlockState(world, new BlockPos(x, this.bottom - 2, this.z), palette.moatContainer);
                setBlockState(world, new BlockPos(x, this.bottom - 1, this.z+1), palette.moatContainer);
                if(x < bridgeStart || x >= bridgeEnd){            		
                    setBlockState(world, new BlockPos(x,this.bottom - 1,this.z), palette.moat);
                }else{
                	IBlockState block = this.hint ? palette.hint : palette.moatContainer;
                    setBlockState(world, new BlockPos(x,this.bottom - 1,this.z), block);
                }
            }
            
            setBlockState(world, new BlockPos(this.left, this.bottom + ROOM_HEIGHT - 1, z), palette.light, null, Facing.EAST, null);
            setBlockState(world, new BlockPos(this.right - 1, this.bottom + ROOM_HEIGHT - 1, z), palette.light, null, Facing.WEST, null);

            
            setBlockState(world, new BlockPos(this.left - 1, this.bottom - 1, this.z), palette.moatContainer);
            setBlockState(world, new BlockPos(this.right, this.bottom - 1, this.z), palette.moatContainer);
        }
        
        private void drawDoor(World world, Random rand, Palette palette)
        {
            int doorLocation = this.left + rand.nextInt(this.right - this.left - 2) + 1;
            
            for(int x=this.left; x<this.right; x++){
                for(int y=this.bottom; y<this.top; y++){
                    setBlockState(world, new BlockPos(x,y,this.z), palette.wall);
                }    
                
                if((x - this.left - 1) % 2 == 0 && x < this.right - 1){            		
                    setBlockState(world, new BlockPos(x, this.bottom + ROOM_HEIGHT - 1, this.z - 1), palette.light, null, Facing.NORTH, null);
                    setBlockState(world, new BlockPos(x, this.bottom + ROOM_HEIGHT - 1, this.z + 1), palette.light, null, Facing.SOUTH, null);                		
                }
            }
            
            Facing facing = this.getIn() == this.north ? Facing.NORTH : Facing.SOUTH;
            setBlockState(world, new BlockPos(doorLocation, this.bottom, this.z), palette.doorLower, null, facing, null);
            setBlockState(world, new BlockPos(doorLocation, this.bottom + 1, this.z), palette.doorUpper, null, facing, null);
            if(this.hint){
            	setBlockState(world, new BlockPos(doorLocation, this.bottom - 1, this.z), palette.hint);
            }
        }     
        
        private void drawPuzzle(World world, Random rand, Palette palette)
        {
            int doorLocation = this.left + rand.nextInt(this.right - this.left - 4) + 2;
            
            for(int x=this.left; x<this.right; x++){
                for(int y=this.bottom; y<this.top; y++){
                    setBlockState(world, new BlockPos(x,y,this.z), palette.wall);
                }    
                
                if((x - this.left - 1) % 2 == 0 && x < this.right - 1){            		
                    setBlockState(world, new BlockPos(x, this.bottom + ROOM_HEIGHT - 1, this.z - 1), palette.light, null, Facing.NORTH, null);
                    setBlockState(world, new BlockPos(x, this.bottom + ROOM_HEIGHT - 1, this.z + 1), palette.light, null, Facing.SOUTH, null);                		
                }
            }
            
            Facing facing = this.getIn() == this.north ? Facing.NORTH : Facing.SOUTH;

            setBlockState(world, new BlockPos(doorLocation, this.bottom, this.z), palette.puzzleDoorLower, null, facing, null);
            setBlockState(world, new BlockPos(doorLocation, this.bottom + 1, this.z), palette.puzzleDoorUpper, null, facing, null);
            if(this.hint){
            	setBlockState(world, new BlockPos(doorLocation, this.bottom - 1, this.z), palette.hint);
            }
            
            Room room = getIn();
            int triggerX;
            if(rand.nextDouble() < 0.5){
                triggerX = doorLocation - 1;
            }else{
                triggerX = doorLocation + 1;
            }
            
            if(room == this.north){
                setBlockState(world, new BlockPos(triggerX, this.bottom + 1, this.z - 1), palette.trigger, null, Facing.NORTH, null);
            }else{
                setBlockState(world, new BlockPos(triggerX, this.bottom + 1, this.z + 1), palette.trigger, null, Facing.SOUTH, null);
            }
        } 
        
        private void drawJump(World world, Random rand, Palette palette)
        {	
            int entrance = rand.nextInt(this.right - this.left - 3);
            int exit = rand.nextInt(this.right - this.left - 3);
            
            for(int x=this.left; x<this.right; x++){
                setBlockState(world, new BlockPos(x, this.bottom - 1, this.z-1), palette.moatContainer);
                setBlockState(world, new BlockPos(x, this.bottom, this.z-1), palette.wall);
                setBlockState(world, new BlockPos(x, this.bottom + 1, this.z-1), palette.wall);
                setBlockState(world, new BlockPos(x, this.bottom - 2, this.z), palette.moatContainer);
                setBlockState(world, new BlockPos(x, this.bottom - 1, this.z + 1), palette.moatContainer);
                setBlockState(world, new BlockPos(x, this.bottom, this.z + 1), palette.wall);
                setBlockState(world, new BlockPos(x, this.bottom + 1, this.z + 1), palette.wall);
                
                if(x % 2 == 0){
                    setBlockState(world, new BlockPos(x, this.bottom - 1, this.z), palette.moatContainer);
                }else{        		
                    setBlockState(world, new BlockPos(x, this.bottom - 1, this.z), palette.moat);
                }
            }
            
            setBlockState(world, new BlockPos(this.left, this.bottom + ROOM_HEIGHT - 1, z), palette.light, null, Facing.EAST, null);
            setBlockState(world, new BlockPos(this.right - 1, this.bottom + ROOM_HEIGHT - 1, z), palette.light, null, Facing.WEST, null);
            
            for(int i=0; i<3; i++){
                world.setBlockToAir(new BlockPos(this.left + entrance + i, this.bottom + 1, this.z - 1));
                world.setBlockToAir(new BlockPos(this.left + exit + i, this.bottom + 1, this.z + 1));               
            }
            
            if(this.hint){
            	setBlockState(world, new BlockPos(this.left + entrance + 1, this.bottom, this.z - 1), palette.hint);
            	setBlockState(world, new BlockPos(this.left + exit + 1, this.bottom, this.z + 1), palette.hint);
            }
        }
    }
    
    private class Floor implements Divider{
        public Room above;
        public Room below;
        
        public boolean isObstacle;
        public VerticalObstacle obstacle;
        
        private boolean direction;
        private boolean hint;
        private int left;
        private int right;
        private int front;
        private int back;
        private int bottom;
        private int top;
        private boolean isDrawn;
        
        public Floor(Room above, Room below)
        {
            this.above = above;
            this.below = below;
            
            Room room = above == null ? below : above;
            this.front = room.z;
            this.back = front + room.length;
            this.left = room.x;
            this.right = left + room.width;
            this.bottom = room == above ? room.y - FLOOR_HEIGHT : room.y + room.height;   
            this.top = this.bottom + FLOOR_HEIGHT;
        }
        
        public boolean getHint()
        {
        	return this.hint;
        }
        
        public void setHint(boolean hint)
        {
        	this.hint = hint;
        }
        
        public Room getIn()
        {
            return this.direction ? this.below : this.above;
        }
        
        public Room getOut()
        {
            return this.direction ? this.above : this.below;
        }
        
        public void reverse()
        {
            this.direction = !this.direction;
        }
        
        public Division getType()
        {
            return Division.AboveBelow;
        }
        
        public boolean getDirection()
        {
            return this.direction;
        }
        
        public void setDirection(boolean direction)
        {
            this.direction = direction;
        }
        
        public void draw(World world, Random rand, Palette palette)
        {
            if(this.isDrawn){
                return;
            }
            
            this.isDrawn = true;
            IBlockState block = this.above == null || this.below == null ? palette.exterior : palette.floor; 	
            
            for(int x=left; x<right; x++){
                for(int y=bottom; y<top; y++){
                    for(int z=front; z<back; z++){
                        setBlockState(world, new BlockPos(x,y,z), block);
                    }
                }
            }
            
            if(this.isObstacle){
                switch(this.obstacle){
                    case Stairs:
                        drawStairs(world, palette);
                        break;

                    case Ladder:
                        drawLadder(world, rand, palette);
                        break;
                        
                    case Jump:
                        drawJump(world, palette);
                        break;
                        
                    default:
                        System.out.println(this.obstacle + " not yet implemented.");
                        break;
                }
            }
        }
        
        private void drawStairs(World world, Palette palette)
        {
            int stairsLeft = (this.left + this.right)/2 - 1;
            int stairsFront = (this.front + this.back)/2 - 1;
            int stairsBottom = this.below.y;
                        
            // only works for a room height of 4 and floor height of 2
            
            if(this.hint){
            	setBlockState(world, new BlockPos(stairsLeft, stairsBottom - 1, stairsFront - 1), palette.hint);
            	setBlockState(world, new BlockPos(stairsLeft + 3, stairsBottom + 5, stairsFront - 1), palette.hint);
            }

            setBlockState(world, new BlockPos(stairsLeft, stairsBottom, stairsFront), palette.stairs, null, Facing.SOUTH, null);
            setBlockState(world, new BlockPos(stairsLeft, stairsBottom + 1, stairsFront + 1), palette.stairs, null, Facing.SOUTH, null);        	
            setBlockState(world, new BlockPos(stairsLeft, stairsBottom + 1, stairsFront + 2), palette.stairsPlatform);
            world.setBlockToAir(new BlockPos(stairsLeft, stairsBottom + 4, stairsFront + 2));
            setBlockState(world, new BlockPos(stairsLeft, stairsBottom + 1, stairsFront + 3), palette.light, null, Facing.SOUTH, null);
            setBlockState(world, new BlockPos(stairsLeft - 1, stairsBottom + 1, stairsFront + 2), palette.light, null, Facing.WEST, null);
            setBlockState(world, new BlockPos(stairsLeft + 1, stairsBottom + 1, stairsFront + 2), palette.light, null, Facing.EAST, null);
            
            setBlockState(world, new BlockPos(stairsLeft + 1, stairsBottom + 2, stairsFront + 2), palette.stairs, null, Facing.EAST, null);
            world.setBlockToAir(new BlockPos(stairsLeft + 1, stairsBottom + 4, stairsFront + 2));
            world.setBlockToAir(new BlockPos(stairsLeft + 1, stairsBottom + 5, stairsFront + 2));
            setBlockState(world, new BlockPos(stairsLeft + 2, stairsBottom + 3, stairsFront + 2), palette.stairs, null, Facing.EAST, null);        	
            world.setBlockToAir(new BlockPos(stairsLeft + 2, stairsBottom + 4, stairsFront + 2));
            world.setBlockToAir(new BlockPos(stairsLeft + 2, stairsBottom + 5, stairsFront + 2));
            setBlockState(world, new BlockPos(stairsLeft + 3, stairsBottom + 3, stairsFront + 2), palette.stairsPlatform);
            world.setBlockToAir(new BlockPos(stairsLeft + 3, stairsBottom + 4, stairsFront + 2));
            world.setBlockToAir(new BlockPos(stairsLeft + 3, stairsBottom + 5, stairsFront + 2));
            setBlockState(world, new BlockPos(stairsLeft + 3, stairsBottom + 3, stairsFront + 1), palette.light, null, Facing.NORTH, null);
            setBlockState(world, new BlockPos(stairsLeft + 3, stairsBottom + 3, stairsFront + 3), palette.light, null, Facing.SOUTH, null);
            
            setBlockState(world, new BlockPos(stairsLeft + 3, stairsBottom + 4, stairsFront + 1), palette.stairs, null, Facing.NORTH, null);
            world.setBlockToAir(new BlockPos(stairsLeft + 3, stairsBottom + 5, stairsFront + 1));
            setBlockState(world, new BlockPos(stairsLeft + 3, stairsBottom + 5, stairsFront), palette.stairs, null, Facing.NORTH, null);
            
            setBlockState(world, new BlockPos(stairsLeft + 1, stairsBottom + 6, stairsFront + 1), palette.light);
        }
        
        private void drawLadder(World world, Random rand, Palette palette)
        {
            int ladderX = (this.left + this.right)/2 + 1;
            int ladderZ = (this.front + this.back)/2 + 1;
            int ladderBottom = this.below.y;
            
            for(int y=ladderBottom; y < this.top; y++){
                setBlockState(world, new BlockPos(ladderX, y, ladderZ), palette.wall);
                setBlockState(world, new BlockPos(ladderX, y, ladderZ), palette.wall);
                                
                setBlockState(world, new BlockPos(ladderX, y, ladderZ + 1), palette.ladder, null, Facing.SOUTH, null);        				
                world.setBlockToAir(new BlockPos(ladderX, y, ladderZ + 2));
            }
            
            setBlockState(world, new BlockPos(ladderX, this.bottom - 1, ladderZ - 1), palette.light, null, Facing.NORTH, null);
            if(this.hint){
            	setBlockState(world, new BlockPos(ladderX, ladderBottom - 1, ladderZ + 1), palette.hint);
            	setBlockState(world, new BlockPos(ladderX, this.bottom + 1, ladderZ), palette.hint);
            }
            
            setBlockState(world, new BlockPos(ladderX, this.top, ladderZ), palette.light);
        }
        
        private void drawJump(World world, Palette palette)
        {
            int platformLeft = (this.left + this.right)/2 - 1;
            int platformFront = (this.front + this.back)/2 - 1;
            int platformBottom = this.below.y;
                        
            // only works for a room height of 4 and floor height of 2
            
            if(this.hint){
            	setBlockState(world, new BlockPos(platformLeft, platformBottom - 1, platformFront - 1), palette.hint);
            	setBlockState(world, new BlockPos(platformLeft, platformBottom + 5, platformFront), palette.hint);
            }
            
            setBlockState(world, new BlockPos(platformLeft, platformBottom, platformFront), palette.platform);
            
            setBlockState(world, new BlockPos(platformLeft, platformBottom, platformFront + 1), palette.light);
            
            setBlockState(world, new BlockPos(platformLeft, platformBottom, platformFront + 2), palette.platform);
            setBlockState(world, new BlockPos(platformLeft, platformBottom + 1, platformFront + 2), palette.platform);
            
            setBlockState(world, new BlockPos(platformLeft + 2, platformBottom + 2, platformFront + 2), palette.platform);
            setBlockState(world, new BlockPos(platformLeft + 3, platformBottom + 2, platformFront + 2), palette.floor);
            setBlockState(world, new BlockPos(platformLeft + 3, platformBottom + 3, platformFront + 2), palette.floor);
            setBlockState(world, new BlockPos(platformLeft + 3, platformBottom + 3, platformFront + 3), palette.light, null, Facing.SOUTH, null);
            
            setBlockState(world, new BlockPos(platformLeft + 3, platformBottom + 3, platformFront), palette.floor);
            setBlockState(world, new BlockPos(platformLeft + 3, platformBottom + 3, platformFront - 1), palette.light, null, Facing.NORTH, null);
            setBlockState(world, new BlockPos(platformLeft + 2, platformBottom + 3, platformFront), palette.platform);
            
            world.setBlockToAir(new BlockPos(platformLeft + 1, platformBottom + 5, platformFront));
            setBlockState(world, new BlockPos(platformLeft + 1, platformBottom + 6, platformFront + 1), palette.light);
            
            for(int i = 0; i<3; i++){
                world.setBlockToAir(new BlockPos(platformLeft + i, platformBottom + 4, platformFront + 2));
                world.setBlockToAir(new BlockPos(platformLeft + i, platformBottom + 5, platformFront + 2));
                world.setBlockToAir(new BlockPos(platformLeft + 2, platformBottom + 4, platformFront + 2 - i));
                world.setBlockToAir(new BlockPos(platformLeft + 2, platformBottom + 5, platformFront + 2 - i));
            }
        }
    }
    
    private class Room implements Comparable<Room> {
        public EWWall eastWall;
        public EWWall westWall;
        public SNWall southWall;
        public SNWall northWall;
        public Floor aboveFloor;
        public Floor belowFloor;	
        
        public int x;
        public int y;
        public int z;
        
        public int width;
        public int height;
        public int length;
        
        public boolean isComplete;
        public boolean mark;	
        
        public Room(int x, int y, int z, int width, int height, int length)
        {
            this.x = x;
            this.y = y;
            this.z = z;
            this.width = width;
            this.height = height;
            this.length = length;
            
        }
        
        public void draw(World world, Random rand, Palette palette)
        {
            this.southWall.draw(world, rand, palette);
            this.northWall.draw(world, rand, palette);
            this.eastWall.draw(world, rand, palette);
            this.westWall.draw(world, rand, palette);
            this.belowFloor.draw(world, rand, palette);
            this.aboveFloor.draw(world, rand, palette);
        }
        
        public int compareTo(Room room)
        {
            if(this.y == room.y){
                if(this.x == room.x){
                    return this.z - room.z;
                }else{
                    return this.x - room.x;
                }
            }else{
                return this.y - room.y;
            }
        }
    }
    
    private class Palette
    {
        public IBlockState exterior;
        public IBlockState floor;
        public IBlockState wall;
        public IBlockState light;
        public IBlockState goal;
        public IBlockState moat;
        public IBlockState moatContainer;
        public IBlockState doorUpper;
        public IBlockState doorLower;
        public IBlockState stairs;
        public IBlockState stairsPlatform;
        public IBlockState ladder;
        public IBlockState trigger;
        public IBlockState puzzleDoorUpper;
        public IBlockState puzzleDoorLower;
        public IBlockState platform;
        public IBlockState hint;
        
        public Palette(String name)
        {
        	if(name.equals("dungeon")){
        		this.setDungeon();
        	}else if(name.equals("pyramid")){
        		this.setPyramid();
        	}else if(name.equals("igloo")){
        		this.setIgloo();
        	}else{
        		this.setDungeon();
        	}
        }
        
        public Palette(Random rand)
        {
        	int pick = rand.nextInt(3);
        	switch(pick){
        		case 0:
        			setDungeon();
        			break;
        			        			
        		case 1:
        			setPyramid();
        			break;
        			
        		case 2:
        			setIgloo();
        			break;
        			
        		default:
        			setDungeon();
        			break;
        	}
        }
        
        private void setDungeon()
        {
            this.floor = Blocks.PLANKS.getDefaultState();
            this.exterior = Blocks.COBBLESTONE.getDefaultState();
            this.wall = Blocks.COBBLESTONE.getDefaultState();
            this.light = Blocks.TORCH.getDefaultState();
            this.goal = Blocks.GOLD_BLOCK.getDefaultState();
            this.moat = Blocks.LAVA.getDefaultState();
            this.moatContainer = Blocks.COBBLESTONE.getDefaultState();
            this.doorUpper = Blocks.OAK_DOOR.getDefaultState().withProperty(BlockDoor.HALF, BlockDoor.EnumDoorHalf.UPPER);
            this.doorLower = Blocks.OAK_DOOR.getDefaultState().withProperty(BlockDoor.HALF, BlockDoor.EnumDoorHalf.LOWER);
            this.stairs = Blocks.STONE_STAIRS.getDefaultState();
            this.stairsPlatform = Blocks.COBBLESTONE.getDefaultState();
            this.ladder = Blocks.LADDER.getDefaultState();
            this.puzzleDoorUpper = Blocks.IRON_DOOR.getDefaultState().withProperty(BlockDoor.HALF, BlockDoor.EnumDoorHalf.UPPER);
            this.puzzleDoorLower = Blocks.IRON_DOOR.getDefaultState().withProperty(BlockDoor.HALF, BlockDoor.EnumDoorHalf.LOWER);
            this.trigger = Blocks.LEVER.getDefaultState();
            this.platform = Blocks.BOOKSHELF.getDefaultState();
            this.hint = Blocks.GOLD_ORE.getDefaultState();
        }
               
        private void setPyramid()
        {
            this.floor = Blocks.RED_SANDSTONE.getDefaultState();
            this.exterior = Blocks.SANDSTONE.getDefaultState();
            this.wall = Blocks.SANDSTONE.getDefaultState();
            this.light = Blocks.TORCH.getDefaultState();
            this.goal = Blocks.DIAMOND_BLOCK.getDefaultState();
            this.moat = Blocks.LAVA.getDefaultState();
            this.moatContainer = Blocks.SANDSTONE.getDefaultState();
            this.doorUpper = Blocks.ACACIA_DOOR.getDefaultState().withProperty(BlockDoor.HALF, BlockDoor.EnumDoorHalf.UPPER);
            this.doorLower = Blocks.ACACIA_DOOR.getDefaultState().withProperty(BlockDoor.HALF, BlockDoor.EnumDoorHalf.LOWER);
            this.stairs = Blocks.SANDSTONE_STAIRS.getDefaultState();
            this.stairsPlatform = Blocks.SANDSTONE.getDefaultState();
            this.ladder = Blocks.LADDER.getDefaultState();
            this.puzzleDoorUpper = Blocks.IRON_DOOR.getDefaultState().withProperty(BlockDoor.HALF, BlockDoor.EnumDoorHalf.UPPER);
            this.puzzleDoorLower = Blocks.IRON_DOOR.getDefaultState().withProperty(BlockDoor.HALF, BlockDoor.EnumDoorHalf.LOWER);
            this.trigger = Blocks.LEVER.getDefaultState();
            this.platform = Blocks.RED_SANDSTONE.getDefaultState();
            this.hint = Blocks.DIAMOND_ORE.getDefaultState();
        }
        
        private void setIgloo()
        {
            this.floor = Blocks.SNOW.getDefaultState();
            this.exterior = Blocks.SNOW.getDefaultState();
            this.wall = Blocks.PACKED_ICE.getDefaultState();
            this.light = Blocks.TORCH.getDefaultState();
            this.goal = Blocks.REDSTONE_BLOCK.getDefaultState();
            this.moat = Blocks.WATER.getDefaultState();
            this.moatContainer = Blocks.GLOWSTONE.getDefaultState();
            this.doorUpper = Blocks.SPRUCE_DOOR.getDefaultState().withProperty(BlockDoor.HALF, BlockDoor.EnumDoorHalf.UPPER);
            this.doorLower = Blocks.SPRUCE_DOOR.getDefaultState().withProperty(BlockDoor.HALF, BlockDoor.EnumDoorHalf.LOWER);
            this.stairs = Blocks.SPRUCE_STAIRS.getDefaultState();
            this.stairsPlatform = Blocks.PACKED_ICE.getDefaultState();
            this.ladder = Blocks.LADDER.getDefaultState();
            this.puzzleDoorUpper = Blocks.IRON_DOOR.getDefaultState().withProperty(BlockDoor.HALF, BlockDoor.EnumDoorHalf.UPPER);
            this.puzzleDoorLower = Blocks.IRON_DOOR.getDefaultState().withProperty(BlockDoor.HALF, BlockDoor.EnumDoorHalf.LOWER);
            this.trigger = Blocks.LEVER.getDefaultState();
            this.platform = Blocks.SNOW.getDefaultState();
            this.hint = Blocks.REDSTONE_ORE.getDefaultState();
        }
    }

    @Override
    public boolean getExtraAgentHandlersAndData(List<Object> handlers, Map<String, String> data)
    {
        return false;
    }

    @Override
    public void prepare(MissionInit missionInit)
    {
    }

    @Override
    public void cleanup()
    {
    }

    @Override
    public boolean targetedUpdate(String nextAgentName)
    {
        return false;   // Does nothing.
    }

    @Override
    public void getTurnParticipants(ArrayList<String> participants, ArrayList<Integer> participantSlots)
    {
        // Does nothing.
    }
}
