/*
 * Maze
 * Copyright (C) 2000  Paul Davis, pdavis@lpccomp.bc.ca
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

import java.awt.*;
import java.awt.event.*;

/**
 * Display a 3D maze using a 2D projection as an AWT component.
 * Line drawings are used to represent walls and corners.
 * The orientation of the view is variable: the player can stand on the
 * walls or ceiling.
 * The player can click on the left or right side to pivot left or right,
 * near the top or bottom to pivot backward or forward or near the
 * center to move forward.
 * The following keystrokes are also accepted:
 * <DL>
 * <LI> SPACE - move forward
 * <LI> L - pivot left
 * <LI> R - pivot right
 * <LI> B - roll back
 * <LI> F - roll forward
 * <LI> G - roll right
 * <LI> H - roll left
 * <LI> S - spin around
 * <LI> U - slide up
 * <LI> D - slide down
 * <LI> I - forward until a choice
 * <LI> W - forward until a wall
 * <LI> Z - follow until a choice
 * <LI> ? - show 2D maze
 * </DL>
 */
public class Maze3D extends Component {

	/**
	 * The maze model.
	 * @see Maze
	 */
	private MazeModel model;

	private Coordinate3D size,current;
	private byte front,right,up;
	private boolean showMarks;
	private int myWidth,myHeight;
	private Frame mapFrame;

	/**
	 * Create a 3D maze display.  Defaults to a size of 300x300.
	 * @param model The MazeModel to use for the maze contents.
	 */
	public Maze3D(MazeModel model) {
		this(model,300,300);
	}

	/**
	 * Create a 3D maze display.
	 * @param model The MazeModel to use for the maze contents.
	 * @param width The display width of the Component.
	 * @param height The display width of the Component.
	 */
	public Maze3D(MazeModel model, int width, int height) {
		this.model = model;
		myWidth = width;
		myHeight = height;
		size = model.getSize();
		current = new Coordinate3D(0,size.y-1,0);
		model.setCurrent(current);
		front = MazeModel.YMI;
		right = MazeModel.XPL;
		up = MazeModel.ZPL;
		showMarks = false;
		setSize(width,height);

		addMouseListener(new MouseListener() {
			public void mouseClicked(MouseEvent e) {
				//System.out.println("Mouse at " +
				//	String.valueOf(e.getX()) +
				//	"," + String.valueOf(e.getY()));
				handleClick(e.getX(),e.getY());
			}
			public void mouseEntered(MouseEvent e) {
			}
			public void mouseExited(MouseEvent e) {
			}
			public void mousePressed(MouseEvent e) {
			}
			public void mouseReleased(MouseEvent e) {
			}
		} );
		addKeyListener(new KeyListener() {
			public void keyPressed(KeyEvent e) {
				//System.out.println("key pressed");
			}
			public void keyReleased(KeyEvent e) {
				handleKeyCode(e.getKeyCode());
				//System.out.println("key released");
			}
			public void keyTyped(KeyEvent e) {
				//System.out.println("key typed");
			}
		} );
	}

	/**
	 * From Component, this component can get keyboard input.
	 */
	public boolean isFocusTraversable() {
		return true;
	}

	/**
	 * Handle a mouse click.
	 * @param x The component x co-ordinate of the click.
	 * @param y The component y co-ordinate of the click.
	 */
	private void handleClick(int x,int y) {
		Dimension s = getSize();
		if ( x < s.width/3 ) {		// Left
			turnLeft();
			repaint();
		}
		else if ( x > s.width*2/3 ) {	// Right
			turnRight();
			repaint();
		}
		else if ( y < s.height/3 ) {		// Up
			if ( goUp() )
				repaint();
		}
		else if ( y > s.height*2/3 ) {	// Down
			if ( goDown() )
				repaint();
		}
		else {				// Forward
			if ( goForward() )
				repaint();
		}
		//System.out.println("Now at " + String.valueOf(current.x) +
		//	"," + String.valueOf(current.y) +
		//	"," + String.valueOf(current.z) +
		//	" " + Maze.directionString(front));
	}

	/**
	 * Handle a keyboard key.
	 * @param keyCode The keycode from the keyboard.
	 */
	private void handleKeyCode(int keyCode) {

		//System.out.println("key code " + String.valueOf(keyCode));
		switch(keyCode) {
		case KeyEvent.VK_SPACE:		// SPACE - move forward
			if( goForward() )
				repaint();
			break;
		case KeyEvent.VK_L:		// L - pivot left
			turnLeft();
			repaint();
			break;
		case KeyEvent.VK_R:		// R - pivot right
			turnRight();
			repaint();
			break;
		case KeyEvent.VK_B:		// B - roll back
			rollBack();
			repaint();
			break;
		case KeyEvent.VK_F:		// F - roll forward
			rollForward();
			repaint();
			break;
		case KeyEvent.VK_G:		// G - roll right
			rollRight();
			repaint();
			break;
		case KeyEvent.VK_H:		// H - roll left
			rollLeft();
			repaint();
			break;
		case KeyEvent.VK_S:		// S - spin around
			front = Maze.invert(front);
			right = Maze.invert(right);
			repaint();
			break;
		case KeyEvent.VK_U:		// U - slide up
			if ( goUp() )
				repaint();
			break;
		case KeyEvent.VK_D:		// D - slide down
			if ( goDown() )
				repaint();
			break;
		case KeyEvent.VK_I:		// I - forward until a choice
			if ( ! goForward() )
				break;
			while ( !isOpen(right) &&
				!isOpen(Maze.invert(right)) &&
				!isOpen(up) &&
				!isOpen(Maze.invert(up)) &&
				isOpen(front) )
				Maze.forward(current,front);
			model.setCurrent(current);
			repaint();
			break;
		case KeyEvent.VK_W:		// W - forward until a wall
			while ( isOpen(front) )
				Maze.forward(current,front);
			model.setCurrent(current);
			repaint();
			break;
		case KeyEvent.VK_Z:		// Z - follow until a choice
			if ( ! goForward() )
				break;
			while ( Maze.count(model.grid(current)) == 2 ) {
				if ( isOpen(front) )
					Maze.forward(current,front);
				else if ( isOpen(right) ) {
					turnRight();
					Maze.forward(current,front);
				}
				else if ( isOpen(Maze.invert(right)) ) {
					turnLeft();
					Maze.forward(current,front);
				}
				else if ( isOpen(up) ) {
					rollBack();
					Maze.forward(current,front);
				}
				else if ( isOpen(Maze.invert(up)) ) {
					rollBack();
					Maze.forward(current,front);
				}
			}
			model.setCurrent(current);
			repaint();
			break;
		case KeyEvent.VK_SLASH:	// ? - show 2D maze
			mapFrame = new Frame();
			Dialog d = new Dialog(mapFrame,"Map");
			int w = 600 / size.x;
			if ( w > 20 )
				w = 20;
			int h = 450 / size.y;
			if ( h > 20 )
				h = 20;
			if ( w>h )
				d.add(new Maze2D(model,w,15,15));
			else
				d.add(new Maze2D(model,h,15,15));
			d.addWindowListener(new WindowAdapter() {
				public void windowClosing(WindowEvent e) {
					//System.out.println("close: " +
					//	e.getSource().toString());
					Window win = (Window)e.getSource();
					win.dispose();
				}
			});
			d.pack();
			d.show();
			break;
		}
	}
	/**
	 * From Component.getPreferredSize.
	 */
	public Dimension getPreferredSize() {
		return new Dimension(myWidth,myHeight);
	}

	/*
	public Dimension getSize() {
		return new Dimension(myWidth,myHeight);
	}
	*/

	/*
	public void setPosition(Coordinate3D current) {
		this.current = current;
	}
	*/

	/**
	 * Set the orientation of the view.
	 * The parameters determine which direction is viewed and
	 * which "floor" or "wall" the player is standing on.
	 * @param front The dimension to the front of the player: XPL etc.
	 * @param right The dimension to the right of the player.
	 * @param up The dimension above the player.
	 */
	public void setOrientation(byte front, byte right, byte up) {
		this.front = front;
		this.right = right;
		this.up = up;
	}

	/*
	public Coordinate3D getCurrent() {
		return current;
	}
	*/

	/**
	 * Change the view orientation to pivot to the left.
	 */
	private void turnLeft() {
		byte temp = front;
		front = Maze.invert(right);
		right = temp;
	}

	/**
	 * Change the view orientation to pivot to the right.
	 */
	private void turnRight() {
		byte temp = front;
		front = right;
		right = Maze.invert(temp);
	}

	/**
	 * Change the view orientation to pivot backwards.
	 */
	private void rollBack() {
		byte temp = front;
		front = up;
		up = Maze.invert(temp);
	}

	/**
	 * Change the view orientation to pivot frontwards.
	 */
	private void rollForward() {
		byte temp = front;
		front = Maze.invert(up);
		up = temp;
	}

	/**
	 * Change the view orientation to roll onto your right side.
	 */
	private void rollRight() {
		byte temp = up;
		up = right;
		right = Maze.invert(temp);
	}

	/**
	 * Change the view orientation to roll onto your left side.
	 */
	private void rollLeft() {
		byte temp = up;
		up = Maze.invert(right);
		right = temp;
	}

	/**
	 * Move forward in the maze.  Keep the same orientation.
	 * @return True if movement possible, false if blocked by a wall.
	 */
	private boolean goForward() {
		if ( isOpen(model.grid(current),front) ) {
			Maze.forward(current,front);
			model.setCurrent(current);
			return true;
		}
		return false;
	}

	/**
	 * Move backwards in maze.  Keep the same orientation.
	 * @return True if movement possible, false if blocked by a wall.
	 */
	private boolean goBackward() {
		if ( isOpen(model.grid(current),Maze.invert(front)) ) {
			Maze.forward(current,Maze.invert(front));
			model.setCurrent(current);
			return true;
		}
		return false;
	}

	/**
	 * Move up a level in the maze.  Keep the same orientation.
	 * @return True if movement possible, false if blocked by a wall.
	 */
	private boolean goUp() {
		if ( isOpen(model.grid(current),up) ) {
			Maze.forward(current,up);
			model.setCurrent(current);
			return true;
		}
		return false;
	}

	/**
	 * Move down a level in the maze.  Keep the same orientation.
	 * @return True if movement possible, false if blocked by a wall.
	 */
	private boolean goDown() {
		if ( isOpen(model.grid(current),Maze.invert(up)) ) {
			Maze.forward(current,Maze.invert(up));
			model.setCurrent(current);
			return true;
		}
		return false;
	}

	/**
	 * Check if the given direction is open from the given cell value.
	 * @param value The cell value to use.
	 * @param direction The direction: XPL etc.
	 * @return True if there is no wall in that direction.
	 */
	private boolean isOpen(byte value,byte direction) {
		return (value & direction) != 0;
	}

	/**
	 * Check if the given direction is open from the current cell.
	 * @param direction The direction: XPL etc.
	 * @return True if there is no wall in that direction.
	 */
	private boolean isOpen(byte direction) {
		return isOpen(model.grid(current),direction);
	}

	/**
	 * From Component, paint the 2D projection of the maze based
	 * on the current position and view orientation.
	 */
	public void paint(Graphics g) {
		Dimension s = getSize();
		//System.out.println("Maze3D size: " + s.toString());
		int x,y;
		g.setColor(Color.white);
		g.fillRect(0,0,s.width,s.height);

		int depth=0;
		Coordinate3D cr = new Coordinate3D(current.x,current.y,current.z);
		g.setColor(Color.black);
		while (true) {
			drawLayer(g,cr,depth++);
			if ( !isOpen(model.grid(cr),front) || depth>50 )
				break;
			Maze.forward(cr,front);
		}
		requestFocus();
	}

	/**
	 * Draw one layer of the projection.
	 * Each layer is one cell further away from the current view point.
	 * @param g The Graphics context to use.
	 * @param cr The position of the cell to draw.
	 * @param depth The number of cells away from the viewpoint.
	 */
	private void drawLayer(Graphics g, Coordinate3D cr, int depth) {

		Dimension s = getSize();
		byte value;
		int x1,x2,y1,y2,x15,y15;
		byte left,down;
		int limx=s.width-1,limy=s.height-1;
		double factor = 0.7;
		double fdepth = (double)depth-factor;
		double denom1=(fdepth+1.71)*2;
		double denom2=(fdepth+2.71)*2;

		/*
		if ( depth>3 )
			return;
		*/
		left = Maze.invert(right);
		down = Maze.invert(up);
		value = model.grid(cr);
		x1 = (int)Math.floor(fdepth*s.width/denom1+0.5);
		x2 = (int)Math.floor((fdepth+1)*s.width/denom2+0.5);
		y1 = (int)Math.floor(fdepth*s.height/denom1+0.5);
		y2 = (int)Math.floor((fdepth+1)*s.height/denom2+0.5);
		x15 = (x1+x2)/2;
		y15 = (y1+y2)/2;
		//fprintf(fp,"depth: %d at %d,%d,%d : %d,%d %d,%d\n",
		//	depth,cr.x,cr.y,cr.z,y1,x1,y2,x2);
		/*
		xpos1 = ax[depth];
		xpos2 = ax[depth+1];
		ypos1 = ay[depth];
		ypos2 = ay[depth+1];
		len0 = 0;
		len1 = len[depth];
		len2 = len[depth+1];
		len3 = (len1-len2)/2;
		*/

		if ( isOpen(value,up) ) {
			g.drawLine(x1,y1,limx-x1,y1);
			g.drawLine(x2,y1,x2,y2);
			g.drawLine(limx-x2,y1,limx-x2,y2);
		}
		if ( isOpen(value,right) ) {
			g.drawLine(limx-x1,y1,limx-x1,limy-y1);
			g.drawLine(limx-x1,limy-y2,limx-x2,limy-y2);
			g.drawLine(limx-x1,y2,limx-x2,y2);
		}
		if ( isOpen(value,down) ) {
			g.drawLine(x1,limy-y1,limx-x1,limy-y1);
			g.drawLine(x2,limy-y1,x2,limy-y2);
			g.drawLine(limx-x2,limy-y1,limx-x2,limy-y2);
		}
		if ( isOpen(value,left) ) {
			g.drawLine(x1,y1,x1,limy-y1);
			g.drawLine(x1,limy-y2,x2,limy-y2);
			g.drawLine(x1,y2,x2,y2);
		}
		if ( !( isOpen(value,up) ^ isOpen(value,front)) )
			g.drawLine(x2,y2,limx-x2,y2);
		if ( !( isOpen(value,right) ^ isOpen(value,front)) )
			g.drawLine(limx-x2,y2,limx-x2,limy-y2);
		if ( !( isOpen(value,down) ^ isOpen(value,front)) )
			g.drawLine(x2,limy-y2,limx-x2,limy-y2);
		if ( !( isOpen(value,left) ^ isOpen(value,front)) )
			g.drawLine(x2,y2,x2,limy-y2);

		if ( !( isOpen(value,up) ^ isOpen(value,left)))
			g.drawLine(x1,y1,x2,y2);
		if ( !( isOpen(value,up) ^ isOpen(value,right)))
			g.drawLine(limx-x1,y1,limx-x2,y2);
		if ( !( isOpen(value,right) ^ isOpen(value,down)))
			g.drawLine(limx-x1,limy-y1,limx-x2,limy-y2);
		if ( !( isOpen(value,down) ^ isOpen(value,left)))
			g.drawLine(x1,limy-y1,x2,limy-y2);

		/* Floor markers
		if ( up==ZMI && !(value&up))
			g.drawLine(limx/2,y15,limx/2,y15);
		if ( right==ZMI && !(value&right))
			g.drawLine(limx-x15,limy/2,limx-x15,limy/2);
		if ( down==ZMI && !(value&down))
			g.drawLine(limx/2,limy-y15,limx/2,limy-y15);
		if ( left==ZMI && !(value&left))
			g.drawLine(x15,limy/2,x15,limy/2);
		if ( front==ZMI && !(value&front))
			g.drawLine(limx/2,limy/2,limx/2,limy/2);
		*/

		Coordinate3D finish = model.getFinish();
		if ( finish!=null && cr.x==finish.x && cr.y==finish.y && cr.z==finish.z )
			chest(g,x1,y1,x2,y2);

		if ( depth>0 && model.isMarked(cr) ) {
			if ( up==MazeModel.ZMI )
				block(g,limx/2,y15,limx/2,y15,x2-x1);
			if ( right==MazeModel.ZMI )
				block(g,limx-x15,limy/2,limx-x15,limy/2,x2-x1);
			if ( down==MazeModel.ZMI )
				block(g,limx/2,limy-y15,limx/2,limy-y15,x2-x1);
			if ( left==MazeModel.ZMI )
				block(g,x15,limy/2,x15,limy/2,x2-x1);
			if ( front==MazeModel.ZMI )
				block(g,limx/2,limy/2,limx/2,limy/2,x2-x1);
		}
	}

	/**
	 * Draw the "treasure chest", a floating box at the maze finish.
	 * @param g The Graphics context.
	 */
	private void chest(Graphics g,int x1,int y1,int x2,int y2)
	{
		Dimension s = getSize();

		int sy1=(s.height/2-y1)/5;
		int sx1=(s.width/2-x1)/5;
		int sy2=(s.height/2-y2)/5;
		int sx2=(s.width/2-x2)/5;
		int x=s.width/2;
		int y=s.height/2;

		g.drawLine(x-sx1,y-sy1,x+sx1,y-sy1);
		g.drawLine(x+sx1,y-sy1,x+sx1,y+sy1);
		g.drawLine(x+sx1,y+sy1,x-sx1,y+sy1);
		g.drawLine(x-sx1,y+sy1,x-sx1,y-sy1);

		g.drawLine(x-sx1,y-sy1,x-sx2,y-sy2);
		g.drawLine(x+sx1,y-sy1,x+sx2,y-sy2);
		g.drawLine(x+sx1,y+sy1,x+sx2,y+sy2);
		g.drawLine(x-sx1,y+sy1,x-sx2,y+sy2);

		g.drawLine(x-sx2,y-sy2,x+sx2,y-sy2);
		g.drawLine(x+sx2,y-sy2,x+sx2,y+sy2);
		g.drawLine(x+sx2,y+sy2,x-sx2,y+sy2);
		g.drawLine(x-sx2,y+sy2,x-sx2,y-sy2);
	}

	/**
	 * Draw a marker on the floor for the cells that have been visited.
	 */
	private void block(Graphics g, int x1, int y1, int x2, int y2, int d) {
		d /= 10;
		if ( d < 1 )
			d = 1;
		int e = 0;
		g.drawLine(x1-d+e,y1-d,x1-d,y1+d);	// Left
		g.drawLine(x1-d,y1+d,x1+d,y1+d);	// Bottom
		g.drawLine(x1+d,y1+d,x1+d-e,y1-d);	// Right
		g.drawLine(x1+d-e,y1-d,x1-d+e,y1-d);	// Top
	}

}