/
maps/
package mapmaker;

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

import util.*;

/** the View in mapmaker's MVC pattern
 */
public class MapGraphics implements MapViewer {

  AreaMap map;
  MapEventHandler meh;
  Graphics g; // stores current Graphics objects to draw on

  Rectangle mapBounds; // stores the bounds of g in room coordinates

  int paintSize = 4;
  // must never be null
  StateController virtualState = 
    new ConstStateController(new Boolean(false));
  
  /** @param map the Model
   * @param meh the Controller
   */
  public MapGraphics(AreaMap map, MapEventHandler meh) {
    this.map = map;
    this.meh = meh;
  } // MapGraphics
  
  /** sets the (abstract) size in which the map will be painted
   */
  public void setPaintSize(int paintSize) {
    if (paintSize < 1)
      paintSize = 1;
    this.paintSize = paintSize;
    // inform map that something changed, although the change is not
    // within the map data ..
    map.notifyOfChange(MapEvent.ChangeView);
  } // setPaintSize

  public void setVirtualStateController(StateController state) {
    virtualState = state;
  } // setVirtualStateController

  public int getPaintSize() {
    return paintSize;
  } // getPaintSize

  public void paint(Graphics g) {
    // set current Graphics object
    this.g = g;
    // only draw if rooms and links lie within the visible area
    // translate visible area into map coordinates
    Rectangle bounds = g.getClipBounds();
    if (bounds == null)
      mapBounds = new Rectangle(map.getSize());
    else {
      int sizePerRoom = sizePerRoom();
      int mapMinX = bounds.x / sizePerRoom;
      int mapMinY = bounds.y / sizePerRoom;
      // mapMaxX and mapMaxY are exclusive
      int mapMaxX = (bounds.x + bounds.width) / sizePerRoom + 1;
      int mapMaxY = (bounds.y + bounds.height) / sizePerRoom + 1;
      mapBounds = 
	new Rectangle(mapMinX, mapMinY, mapMaxX - mapMinX, mapMaxY - mapMinY);
    }
    // paint rooms
    Room[] rooms = map.getRooms();
    for (int i = 0; i < rooms.length; i++)
      paintRoom(rooms[i]);
    // paint links
    Link[] links = map.getLinks();
    for (int i = 0; i < links.length; i++)
      paintLink(links[i]);
    // paint selected room if there is one
    Room selected = map.getSelected();
    if (selected != null)
      paintSelectedRoom(selected);
  } // paint

  int roomSquareSize() {
    // returns the size of the square that holds a room
    // without border
    return 3 + 2 * paintSize;
  } // room SquareSize

  public int sizePerRoom() {
    // returns the size of the square that holds a room
    // including its border
    return 3 * roomSquareSize();
  } // sizePerRoom

  public Point roomPaintPos(Room room) {
    // returns starting opsition for room painting
    int sizePerRoom = sizePerRoom();
    Point pos = room.getPos();
    return new Point(pos.x * sizePerRoom, pos.y * sizePerRoom);
  } // roomPaintPos

  Point squarePaintPos(Room room) {
    // returns position where to start painting of rooms outlines
    int sizePerRoom = sizePerRoom();
    int borderSize = (sizePerRoom - roomSquareSize()) / 2;
    Point pos = roomPaintPos(room);
    return new Point(pos.x + borderSize, pos.y + borderSize);
  } // squarePaintPos

  Point centerPaintPos(Room room) {
    // returns center position of room for painting
    Point pos = roomPaintPos(room);
    int add = (sizePerRoom() - 1) / 2;
    return new Point(pos.x + add, pos.y + add);
  } // centerPaintPos

  int exitPaintSize() {
    return 1 + paintSize/2;
  } // exitPaintSize

  int centerToEdge() {
    // returns the distance of the edge from center for enlarged room
    return (sizePerRoom() - 1) / 2 - exitPaintSize();
  } // centerToEdge

  Point linkPaintPos(Room room, int dir) {
    // returns position where a link at given direction would start
    int halfSize = (roomSquareSize() - 1) / 2;
    Point center = centerPaintPos(room);
    // check for up/down - special position
    int upDownSize = 1 + paintSize / 3;
    if (dir == Dir.up)
      return new Point(center.x, center.y - upDownSize);
    if (dir == Dir.down)
      return new Point(center.x, center.y + upDownSize);
    // now calculate 'normal' directions
    Point screenDir = Dir.screenDir(dir);
    int moveX = screenDir.x * halfSize;
    int moveY = screenDir.y * halfSize;
    return new Point(center.x + moveX, center.y + moveY);
  } // linkpaintPos

  /** draws the edges of the specified rectangel;
   * used only by paintRoom
   */
  private void drawEdges(int x, int y, int dx, int dy) {
    int l = 1; // length
    g.drawLine(x, y, x + l, y);
    g.drawLine(x, y, x, y + l);

    g.drawLine(x + dx, y, x + dx - l, y);
    g.drawLine(x + dx, y, x + dx, y + l);

    g.drawLine(x, y + dy, x + l, y + dy);
    g.drawLine(x, y + dy, x, y + dy - l);

    g.drawLine(x + dx, y + dy, x + dx - l, y + dy);
    g.drawLine(x + dx, y + dy, x + dx, y + dy - l);
  } // drawEdges

  void paintRoom(Room room) {
    // only draw if within bounds
    if (!mapBounds.contains(room.getPos()))
      return;
    Point start = squarePaintPos(room);
    int roomSize = roomSquareSize();

    Color oldColor = g.getColor();

    // draw marked rooms with red border
    if (room.getMarked()) {
      g.setColor(Color.red);
      g.drawRect(start.x - 1, start.y - 1, roomSize + 1, roomSize + 1);
      g.setColor(oldColor);
    }

    // draw room in its color
    Color roomColor = room.getColor();
    if (roomColor != null)
      g.setColor(roomColor);

    if (room instanceof VirtualRoom) {
      if (((Boolean)virtualState.state()).booleanValue())
	drawEdges(start.x, start.y, roomSize - 1, roomSize - 1);
    }
    else
      g.drawRect(start.x, start.y, roomSize - 1, roomSize - 1);

    g.setColor(oldColor);
  } // paint

  void paintExit(Point pos, boolean linked, boolean blocked) {
    // used only by paintSelectedRoom
    int size = exitPaintSize();
    if (linked)
      if (blocked) {
	g.clearRect(pos.x - size, pos.y - size, 2 * size, 2 * size);
	g.drawRect(pos.x - size, pos.y - size, 2 * size, 2 * size);
	// draw cross
	g.drawLine(pos.x - size, pos.y - size, pos.x + size, pos.y + size);
	g.drawLine(pos.x + size, pos.y - size, pos.x - size, pos.y + size);
      }
      else
	g.fillRect(pos.x - size, pos.y - size, 2 * size, 2 * size);
    else {
      g.clearRect(pos.x - size, pos.y - size, 2 * size, 2 * size);
      g.drawRect(pos.x - size, pos.y - size, 2 * size, 2 * size);
    } // else
  } // paintExit

  Point exitPaintPos(int dir) {
    // returns the screenPosition where the exit indicated by dir
    // should be painted in a selected (enlarged) room,
    // relative to the center of the room
    if (dir < Dir.PLANEDIRNR) {
      Point screenDir = Dir.screenDir(dir);
      int stretch = centerToEdge();
      return new Point(screenDir.x * stretch, screenDir.y * stretch);
    } 
    else
      if (dir == Dir.up)
	return new Point(0, -(1 + exitPaintSize()));
      else
	return new Point(0, 1 + exitPaintSize());
  } // exitPaintPos

  void paintSelectedRoom(Room room) {
    // only draw if within bounds
    if (!mapBounds.contains(room.getPos()))
      return;
    // draws the room enlarged
    Point center = centerPaintPos(room);
    Point start = roomPaintPos(room);
    int size = sizePerRoom();
    int centerToEdge = centerToEdge();

    // draw room in its color
    Color oldColor = g.getColor();
    Color roomColor = room.getColor();
    if (roomColor != null)
      g.setColor(roomColor);

    // clear background
    g.clearRect(start.x, start.y, size - 1, size - 1);
    // draw border
    g.drawRect(center.x - centerToEdge, center.y - centerToEdge,
	       centerToEdge * 2, centerToEdge * 2);
    // draw exits
    for (int i = 0; i < Dir.DIRNR; i++) {
      Point pos = exitPaintPos(i);
      pos.translate(center.x, center.y);
      paintExit(pos, room.exitLinked(i), room.exitBlocked(i));
    }

    g.setColor(oldColor);
  } // paintSelectedRoom

  void paintLink(Link link) {
    Room[] rooms = link.getRooms();
    // only draw if within bounds
    if (AwtUtil.outsideSameSide(mapBounds, 
				rooms[0].getPos(), 
				rooms[1].getPos()))
      return;
    int connectSize = paintSize + 1;
    Point[] connect = new Point[2]; // positions after direction indicators
    int[] exits = new int[2];

    for (int i = 0; i < 2; i++) {
      int exit = exits[i] = rooms[i].getExitDir(link);
      Point start = linkPaintPos(rooms[i], exit);

      // no indicators drawn if up/down direction
      // instead draw little square (unless blocked)
      if (exit == Dir.up || exit == Dir.down) {
	if (!rooms[i].exitBlocked(exit)) {
	  int exitSize = 1 + paintSize / 10;
	  g.fillRect(start.x - exitSize, start.y - exitSize, 
		     exitSize * 2, exitSize * 2);
	}
	connect[i] = start;
      }

      else { // normal directions (not up/down)
	// draw direction indicator
	Point screenDir = Dir.screenDir(exit);
	int connectX = start.x + screenDir.x * connectSize;
	int connectY = start.y + screenDir.y * connectSize;
	// draw links to virtualRooms differently
	if (!(rooms[i] instanceof VirtualRoom))
	  g.drawLine(start.x, start.y, connectX, connectY);
	else {
	  Point center = centerPaintPos(rooms[i]);
	  g.drawLine(center.x, center.y, connectX, connectY);
	}

	// if blocked, draw arrowhead
	if (rooms[i].exitBlocked(exit)) {
	  Point[] arrow = new Point[2];
	  arrow[0] = Dir.screenDir(Dir.turnClockwise(exit));
	  arrow[1] = Dir.screenDir(Dir.turnCounterClockwise(exit));
	  int arrowSize = 1 + paintSize / 3;
	  for (int a = 0; a < 2; a++) {
	    arrow[a].x *= arrowSize;
	    arrow[a].y *= arrowSize;
	    arrow[a].translate(start.x, start.y);
	    g.drawLine(start.x, start.y, arrow[a].x, arrow[a].y);
	  }
	} // if blocked
	
	connect[i] = new Point(connectX, connectY);
      }
    } // for

    // connect the 2 connection points
    connectPoints(connect[0], exits[0], connect[1], exits[1]);
  } // paintLink
  
  /** returns wether a direct connection from p1 to p2 would
   * oppose to exit1 at point p1;
   * used by connectPoints
   */
  boolean validConnect(Point p1, Point p2, int exit1) {
    Point lineDir = new Point(p2.x - p1.x, p2.y - p1.y);
    // order of parameters IS important!
    return !MapMath.opposeQuadrant(Dir.screenDir(exit1), lineDir);
  } // validConnect

  /** connects the given points, respecting the exits they come from
   */
  void connectPoints(Point p1, int exit1, Point p2, int exit2) {
    // if the direct connection would be at an invalid angle,
    // try to use two connection lines
    if (!validConnect(p1, p2, exit1) || 
	!validConnect(p2, p1, exit2)) {
      // try to get a valid connection by connecting with two lines
      Point middle = new Point(p1.x, p2.y);
      // if middle not valid, try other middle point
      if (!validConnect(p1, middle, exit1) ||
	  !validConnect(p2, middle, exit2))
	middle = new Point(p2.x, p1.y);
      // if now valid, draw two lines and return
      if (validConnect(p1, middle, exit1) &&
	  validConnect(p2, middle, exit2)) {
	g.drawLine(p1.x, p1.y, middle.x, middle.y);
	g.drawLine(p2.x, p2.y, middle.x, middle.y);
	return;
      }
    }
    g.drawLine(p1.x, p1.y, p2.x, p2.y);
  } // connectPoints

  Point mapPos(Point screenPos) {
    // returns the map-position of the room that covers 
    // screenPos when painted
    int sizePerRoom = sizePerRoom();
    return new Point(screenPos.x / sizePerRoom, screenPos.y / sizePerRoom);
  } // mapPos

  int exitPos(Point screenPos) {
    // if screenPos is on an exit field of selected room,
    // returns this exit
    // otherwise returns -1
    Room selected = map.getSelected();
    if (selected == null || !(selected.getPos().equals(mapPos(screenPos))))
      return -1;
    // now check if screenPos is on exit
    Point centerPos = centerPaintPos(selected);
    Point relPos = new Point(screenPos.x - centerPos.x, 
			     screenPos.y - centerPos.y);
    int vary = exitPaintSize();
    for (int dir = 0; dir < Dir.DIRNR; dir++) {
      Point paintPos = exitPaintPos(dir);
      if (Math.abs(paintPos.x - relPos.x) <= vary &&
	  Math.abs(paintPos.y - relPos.y) <= vary)
	return dir;
    }
    return -1;
  } // exitPos

  /** adds some listeners to speaker-parameter which delegate
   * events to the Controller given in Constructor
   * @param speaker should be the awt/swing - Component 
   * wrapping the View
   */
  public void addListeners(Component speaker) {
    MouseListener ml =  new MouseAdapter() {
	
	Point mousePressedPos = new Point(0,0);
	
	private boolean inScreen(Point pos) {
	  // returns wether pos lies within the visible range of
	  // the map
	  return AwtUtil.contains(getSize(), pos);
	} // inScreen
	
	public void mouseClicked(MouseEvent e) {
	  if (!inScreen(e.getPoint()))
	    return;
	  Point mapPos = mapPos(e.getPoint());
	  // check if click on link
	  int exit = exitPos(e.getPoint());
	  if (exit != -1)
	    meh.linkClicked(e, mapPos, exit);
	  else
	    meh.roomClicked(e, mapPos);
	} // mouseClicked

	public void mousePressed(MouseEvent e) {
	  if (!inScreen(e.getPoint()))
	    return;
	  mousePressedPos = e.getPoint();
	} // mousePressed

	public void mouseReleased(MouseEvent e) {
	  Point pos = e.getPoint();
	  if (!inScreen(pos) ||
	      !inScreen(mousePressedPos))
	    return;
	  // check if drag from link to link
	  int oldExit = exitPos(mousePressedPos);
	  int newExit = exitPos(pos);
	  if (oldExit != -1 && newExit != -1 && oldExit != newExit) {
	    meh.linkDragged(e, mapPos(pos), oldExit, newExit);
	    return;
	  }
	  // check if drag from different rooms
	  Point mapPos1 = mapPos(mousePressedPos);
	  Point mapPos2 = mapPos(pos);
	  if (!mapPos1.equals(mapPos2))
	    meh.roomDragged(e, mapPos1, mapPos2);
	} // mouseReleased

      };
    
    speaker.addMouseListener(new ClickToleranceFilter(ml));

    // create MouseLocator to get current mouse position
    final MouseLocator mouseLocator = new MouseLocator();
    speaker.addMouseMotionListener(mouseLocator);

    speaker.addKeyListener(new KeyAdapter() {

        MouseLocator mouser = mouseLocator;

        // use keyReleased, not keyTyped, as not all keys
	// generate a keyTyped event!!!
	public void keyReleased(KeyEvent e) {
	  // set pos to current mouse position
	  Point pos = new Point(mouser.getX(), mouser.getY()); 
	  Point mapPos = mapPos(pos);
	  // check if on link
	  int exit = exitPos(pos);
	  if (exit != -1)
	    meh.keyTypedOnLink(e, mapPos, exit);
	  else
	    meh.keyTypedOnRoom(e, mapPos);
	} // keyReleased

      });
  } // addListeners
    
  /** returns the Dimension that the map will occupy (in Pixels)
   */
  public Dimension getSize() {
    int factor = sizePerRoom();
    Dimension roomSize = map.getSize();
    return new Dimension(roomSize.width * factor, 
			 roomSize.height * factor);
  } // getSize

} // MapGraphics