/
maps/
package mapmaker;

import java.util.*;
import java.awt.*;
import java.lang.*;

import mapmaker.mapcmd.*;
import util.*;

/** the Model in mapmaker's MVC pattern
 */
public class ConcreteMap implements Map {

  Room[][] rooms;
  Link[] links;
  Room selected;
  Text desc;

  Hashtable roomPos; // used to get the position of a room
  BufferedObservable bufObs;
  MapFactory factory;
  
  /** @param sizeX the number of rooms stored in width
   * @param sizeY you figure it out ;)
   */
  public ConcreteMap(int sizeX, int sizeY, Observable obs) {
    rooms = new Room[sizeX][sizeY];
    roomPos = new Hashtable(sizeX * sizeY);
    links = new Link[0]; // not links = null !!!
    factory = new MapFactory();
    bufObs = new BufferedObservable(obs);
  } // Map

  /** deletes all rooms and links and the room description
   */
  protected void clearMap() {
    Dimension size = getSize();
    rooms = new Room[size.width][size.height];
    roomPos.clear();
    links = new Link[0]; // not links = null !!!    
    selected = null;
    desc = null;
  } // clearMap

  /** executes the given command from the mapmaker.mapcmd package;
   * the following commands are supported:
   * CompositeCommand,
   * RoomCommand,
   * CmdNewRoom,
   * CmdKillRoom,
   * CmdSelectRoom,
   * CmdUnselectRoom,
   * CmdSwapRooms,
   * CmdLinkRooms,
   * CmdClearMap,
   * CmdSetSize,
   * CmdSetDesc;
   * example: map.execute(new CmdNewRoom(new Point(x, y)));
   * notifies Observers only after the complete command is
   * executed in case of CompositeCommands
   */
  public void execute(MapCommand cmd) {
    bufObs.delayNotify();
    executeWithoutNotify(cmd);
    // make sure some notify occurs
    notifyOfChange(null);
    bufObs.fireNotify();
  } // execute

  protected void executeWithoutNotify(MapCommand cmd) {
    if (cmd instanceof CompositeCommand) {
      Iterator it = ((CompositeCommand)cmd).iterator();
      while (it.hasNext())
	executeWithoutNotify((MapCommand)it.next());
      return;
    }
    if (cmd instanceof RoomCommand) {
       Room target = getRoom(((RoomCommand)cmd).mapPos);
       if (target != null)
	 target.execute((RoomCommand)cmd);
       return;
    }
    if (cmd instanceof CmdNewRoom) {
      newRoom(((CmdNewRoom)cmd).pos);
      return;
    }
    if (cmd instanceof CmdKillRoom) {
      killRoom(((CmdKillRoom)cmd).pos);
      return;
    } 
    if (cmd instanceof CmdSelectRoom) {
      selectRoom(((CmdSelectRoom)cmd).pos);
      return;
    } 
    if (cmd instanceof CmdUnselectRoom) {
      unselectRoom();
      return;
    } 
    if (cmd instanceof CmdSwapRooms) {
      swapRooms(((CmdSwapRooms)cmd).pos1,
		 ((CmdSwapRooms)cmd).pos2);
      return;
    } 
    if (cmd instanceof CmdLinkRooms) {
      CmdLinkRooms cmdLink = (CmdLinkRooms)cmd;
      if (cmdLink.autoLink)
	linkRooms(cmdLink.pos1, cmdLink.pos2);
      else {
	Room room1 = getRoom(cmdLink.pos1);
	Room room2 = getRoom(cmdLink.pos2);
	if (room1 == null || room2 == null)
	  return;
	linkRooms(room1, cmdLink.exit1, room2, cmdLink.exit2);
      }
      return;
    }
    if (cmd instanceof CmdClearMap) {
      clearMap();
      return;
    }
    if (cmd instanceof CmdSetSize) {
      setSize(((CmdSetSize)cmd).dim);
      return;
    }
    if (cmd instanceof CmdSetDesc) {
      setDesc(((CmdSetDesc)cmd).desc);
      return;
    }
  } // executeWithoutNotify

  /** makes a deep-copy of the given map's data (Rooms and Links only)
   * and replaces its own data with it
   * @original the map to be copied from
   */
  public void deepCopyFrom(Map original) {
    // copy rooms
    roomPos.clear();
    Dimension dim = original.getSize();
    rooms = new Room[dim.width][dim.height];
    Room[] orgRooms = original.getRooms();
    for (int i = 0; i < orgRooms.length; i++) {
      Point pos = orgRooms[i].getPos();
      Room newRoom = factory.createRoom(this);
      newRoom.deepCopyFrom(orgRooms[i]);
      setRoom(pos, newRoom);
    }
    // copy selected
    Room orgSelected = original.getSelected();
    if (orgSelected == null)
      selected = null;
    else
      selected = getRoom(orgSelected.getPos());
    // copy links - must be AFTER rooms copied
    Link[] orgLinks = original.getLinks();
    links = new Link[orgLinks.length];
    for (int i = 0; i < orgLinks.length; i++) {
      links[i] = factory.createLink(this, orgLinks[i]);
    }
    // copy description
    desc = (Text)original.getDesc().clone();
    notifyOfChange(MapEvent.ChangeAll);
  } // deepCopyFrom

  public Room getRoom(Point pos) {
    return rooms[pos.x][pos.y];
  } // getRoom

  void setRoom(Point pos, Room room) {
    // sets rooms[pos] to room,
    // automatically validates hashtable

    // check if room already stored -> exception
    if (room != null && roomPos.containsKey(room))
      throw new IllegalArgumentException();

    if (rooms[pos.x][pos.y] != null) 
      roomPos.remove(rooms[pos.x][pos.y]);
    rooms[pos.x][pos.y] = room;
    if (room != null)
      // use a clone to ensure that memorized object stays constant
      roomPos.put(room, new Point(pos));
  } // setRoom

  /** returns the map position of room
   */
  public Point getRoomPos(Room room) {
    // use Hashtable to speed things up
    // check for invalid hashtable too
    Point pos = (Point)roomPos.get(room);
    // use a clone to ensure that memorized object stays constant
    pos = new Point(pos.x, pos.y);
    return pos;
  } // getRoomPos

  /** updates the Hashtable storing the room positions
   */
  protected void updateRoomPos() {
    Dimension dim = getSize();
    roomPos.clear();
    for (int x = 0; x < dim.width; x++)
      for (int y = 0; y < dim.height; y++)
	if (rooms[x][y] != null)
	  roomPos.put(rooms[x][y], new Point(x, y));
  } // updateRoomPos

  /** creates a new Room if the position is empty
   * @param pos position on map where to create the new Room
   */
  protected void newRoom(Point pos) {
    if (getRoom(pos) == null) {
      setRoom(pos, factory.createRoom(this));
      notifyOfChange(MapEvent.ChangeRoom);
    }
  } // newRoom

  /** removes the room at give position if there is one
   */
  protected void killRoom(Point pos) {
    if (getRoom(pos) != null) {
      getRoom(pos).suicide();
      notifyOfChange(MapEvent.ChangeRoom);
    }
  } // killRoom

  /** returns the room currently selected
   */
  public Room getSelected() {
    return selected;
  } // getSelected

  protected void selectRoom(Point pos) {
    selected = getRoom(pos);
    notifyOfChange(MapEvent.ChangeSelect);
  } // selectRoom

  protected void unselectRoom() {
    selected = null;
    notifyOfChange(MapEvent.ChangeSelect);
  } // unselectRoom

  /** returns the map's description
   */
  public Text getDesc() {
    if (desc != null)
      return (Text)desc.clone();
    else
      return null;
  } // getDesc

  /** sets the room's description without notifying observers;
   * used by DescViewer, others use CmdSetDesc
   */
  public void setDesc(Text desc) {
    if (desc != null)
      this.desc = (Text)desc.clone();
    else
      desc = null;
  } // setDesc

  /* swaps the rooms at the given positions
   * ('moves' a room if one position is empty)
   */
  protected void swapRooms(Point pos1, Point pos2) {
    // swap the rooms at given positions
    Room room1 = getRoom(pos1);
    Room room2 = getRoom(pos2);
    if (room1 != null || room2 != null) {
      // make sure that no room is stored twice at any give time
      // messes up hashtable
      setRoom(pos2, null);
      setRoom(pos1, room2);
      setRoom(pos2, room1);
      notifyOfChange(MapEvent.ChangeRoom);
    }
  } // swapRooms

  int getLinkIndex(Link link) {
    for (int i = 0; i < links.length; i++)
      if (links[i] == link)
	return i;
    throw new IllegalArgumentException();
  } // getLinkIndex

  /** should only be called by Link objects committing suicide
   */
  public void removeLink(Link link) {
    // removes link from the links list
    // called by link when it self-destructs
    int index = getLinkIndex(link);
    Link[] linkBuf = new Link[links.length - 1];
    for (int i = 0; i < linkBuf.length; i++)
      linkBuf[i] = links[i];
    if (index < linkBuf.length)
      linkBuf[index] = links[linkBuf.length];
    links = linkBuf;
  } // removeLink

  /** should only be called by Room objects committing suicide
   */
  public void removeRoom(Room room) {
    // removes room from the rooms array
    // called by room when it self-destructs
    setRoom(getRoomPos(room), null);
    // check if selected
    if (selected == room)
      selected = null;
  } // removeRoom

  /** notifys the map's Observers of a certain change
   * @param change passed on to Observers
   */
  public void notifyOfChange(Object change) {
    bufObs.notifyObservers(change);
  } // notifyOfChange

  /** creates a link between the given rooms at give exits
   */
  protected void linkRooms(Room room1, int exit1, Room room2, int exit2) {
    if (room1.exitLinked(exit1) || room2.exitLinked(exit2))
      throw new IllegalArgumentException();
    else {
      Link newLink = factory.createLink(this, room1, exit1, room2, exit2);
      Link[] linkBuf = new Link[links.length + 1];
      for (int i = 0; i < links.length; i++)
	linkBuf[i] = links[i];
      linkBuf[links.length] = newLink;
      links = linkBuf;
      notifyOfChange(MapEvent.ChangeLink);
    }
  } // linkRooms

  /** creates a link between the given rooms
   * automatically chooses exits
   */
  protected void linkRooms(Room room1, Room room2) {
    // automatically choose exit1 and exit2
    if (room1 == null || room2 == null)
      return;
    int exit1 = room1.bestFreeExitTo(room2);
    int exit2 = room2.bestFreeExitTo(room1);
    if (exit1 == -1 || exit2 == -1)
      return;
    // try to make exit1 and exit2 oppose each other
    int opp1 = Dir.opposite(exit1);
    int opp2 = Dir.opposite(exit2);
    if (exit1 != opp2) {
      boolean opp1free = !room2.exitLinked(opp1);
      boolean opp2free = !room1.exitLinked(opp2);
      if (opp1free)
	exit2 = opp1;
      else
	if (opp2free)
	  exit1 = opp2;
    }
    linkRooms(room1, exit1, room2, exit2); 
  } // linkRooms

  /** links rooms at given positions if there are any
   */
  protected void linkRooms(Point pos1, Point pos2) {
    linkRooms(getRoom(pos1), getRoom(pos2));
  } // linkRooms

  /** returns an array containing all rooms of the map
   */
  public Room[] getRooms() {
    // count number of rooms
    int roomNr = 0;
    for (int x = 0; x < rooms.length; x++)
      for (int y = 0; y < rooms[x].length; y++)
	if (rooms[x][y] != null)
	  roomNr++;
    // create array containing rooms
    Room[] roomArray = new Room[roomNr];
    int roomArrayPos = 0;
    for (int x = 0; x < rooms.length; x++)
      for (int y = 0; y < rooms[x].length; y++)
	if (rooms[x][y] != null)
	  roomArray[roomArrayPos++] = rooms[x][y];
    return roomArray;
  } // getRooms
    
  /** returns an array containing all link of the map
   */
  public Link[] getLinks() {
    return links;
  } // getLinks

  /** returns the Dimension of the map in terms of room numbers
   */
  public Dimension getSize() {
    int height = 0;
    if (rooms.length > 0)
      height = rooms[0].length;
    return new Dimension(rooms.length, height);
  } // getSize

  /** changes the map's size to given dimensions if that is possible
   * without deleting rows or collums containing rooms; otherwise
   * reduces size to the minimum size possible without performing
   * such deletions
   */
  protected void setSize(Dimension dim) {
    Dimension oldDim = getSize();
    // check which columns and rows are empty
    boolean[] rowEmpty = new boolean[oldDim.height];
    boolean[] columnEmpty = new boolean[oldDim.width];
    // check rows
    for (int row = 0; row < rowEmpty.length; row++) {
      rowEmpty[row] = true;
      for (int i = 0; i < oldDim.width; i++)
	if (rooms[i][row] != null)
	  rowEmpty[row] = false;
    }
    // check columns
    for (int column = 0; column < columnEmpty.length; column++) {
      columnEmpty[column] = true;
      for (int i = 0; i < oldDim.height; i++)
	if (rooms[column][i] != null)
	  columnEmpty[column] = false;
    }
    // calculate resize information
    int[] YIndex = Compressor.compress(rowEmpty, dim.height);
    int[] XIndex = Compressor.compress(columnEmpty, dim.width);
    Room[][] newRooms = new Room[XIndex.length][YIndex.length];
    // copy rooms into new room array
    for (int x = 0; x < XIndex.length; x++)
      if (XIndex[x] != -1)
	for (int y = 0; y < YIndex.length; y++)
	  if (YIndex[y] != -1)
	    newRooms[x][y] = rooms[XIndex[x]][YIndex[y]];
    // set new data
    rooms = newRooms;
    updateRoomPos();
    notifyOfChange(MapEvent.ChangeAll);
  } // setSize

} // ConcreteMap