/*
 * MegaMek - Copyright (C) 2000,2001,2002,2003,2004 Ben Mazur (bmazur@sev.org)
 *
 *  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.
 */

package megamek.common;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Serializable;
import java.io.StreamTokenizer;
import java.io.Writer;
import java.util.Collection;
import java.util.Date;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.Vector;

import megamek.common.event.BoardEvent;
import megamek.common.event.BoardListener;

public class Board implements Serializable, IBoard {

    private static final long serialVersionUID = -5744058872091016636L;

    public static final String BOARD_REQUEST_ROTATION = "rotate:";

    public static final int BOARD_MAX_WIDTH = Coords.MAX_BOARD_WIDTH;
    public static final int BOARD_MAX_HEIGHT = Coords.MAX_BOARD_HEIGHT;

    protected int width;
    protected int height;

    //MapType
    public static final int T_GROUND = 0;
    public static final int T_ATMOSPHERE = 1;
    public static final int T_SPACE = 2;
    
    private static final String[] typeNames = { "Ground", "Low Atmosphere", "Space" };
    
    private int mapType = T_GROUND;
    
    private IHex[] data;

    /** Building data structures. */
    private Vector<Building> buildings = new Vector<Building>();
    private transient Hashtable<Coords, Building> bldgByCoords = new Hashtable<Coords, Building>();

    protected transient Vector<BoardListener> boardListeners = new Vector<BoardListener>();

    /**
     * Record the infernos placed on the board.
     */
    private Hashtable<Coords, InfernoTracker> infernos = new Hashtable<Coords, InfernoTracker>();

    private Hashtable<Coords, Collection<SpecialHexDisplay>> specialHexes = new Hashtable<Coords, Collection<SpecialHexDisplay>>();

    /** Option to turn have roads auto-exiting to pavement. */
    private boolean roadsAutoExit = true;

    /**
     * Creates a new board with zero as its width and height parameters.
     */
    public Board() {
        this(0, 0);
    }

    /**
     * Creates a new board of the specified dimensions. All hexes in the board
     * will be null until otherwise set.
     * 
     * @param width the width dimension.
     * @param height the height dimension.
     */
    public Board(int width, int height) {
        this.width = width;
        this.height = height;
        data = new IHex[width * height];
    }

    /**
     * Creates a new board of the specified dimensions and specified hex data.
     * 
     * @param width the width dimension.
     * @param height the height dimension.
     * @param data
     */
    public Board(int width, int height, IHex[] data) {
        this.width = width;
        this.height = height;
        this.data = new IHex[width * height];
        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                this.data[y * width + x] = data[y * width + x];
            }
        }
    }

    /**
     * Creates a new board of the specified dimensions, hexes, buildings, and
     * inferno trackers. Do *not* use this method unless you have carefully
     * examined this class.
     * 
     * @param width The <code>int</code> width dimension in hexes.
     * @param height The <code>int</code> height dimension in hexes.
     * @param hexes The array of <code>Hex</code>es for this board. This
     *            object is used directly without being copied. This value
     *            should only be <code>null</code> if either
     *            <code>width</code> or <code>height</code> is zero.
     * @param bldgs The <code>Vector</code> of <code>Building</code>s for
     *            this board. This object is used directly without being copied.
     * @param infMap The <code>Hashtable</code> that map <code>Coords</code>
     *            to <code>InfernoTracker</code>s for this board. This object
     *            is used directly without being copied.
     */
    public Board(int width, int height, IHex[] hexes, Vector<Building> bldgs,
            Hashtable<Coords, InfernoTracker> infMap) {
        this.width = width;
        this.height = height;
        data = hexes;
        buildings = bldgs;
        infernos = infMap;
        createBldgByCoords();
    }

    /*
     * (non-Javadoc)
     * 
     * @see megamek.common.IBoard#getHeight()
     */
    public int getHeight() {
        return height;
    }

    /*
     * (non-Javadoc)
     * 
     * @see megamek.common.IBoard#getWidth()
     */
    public int getWidth() {
        return width;
    }

    /*
     * (non-Javadoc)
     * 
     * @see megamek.common.IBoard#newData(int, int, megamek.common.IHex[])
     */
    public void newData(int width, int height, IHex[] data) {
        this.width = width;
        this.height = height;
        this.data = data;

        initializeAll();
        processBoardEvent(new BoardEvent(this, null, BoardEvent.BOARD_NEW_BOARD));
    }

    /*
     * (non-Javadoc)
     * 
     * @see megamek.common.IBoard#newData(int, int)
     */
    public void newData(int width, int height) {
        newData(width, height, new IHex[width * height]);
    }

    public Enumeration<Coords> getHexesAtDistance(Coords coords, int distance) {
        // Initialize the one necessary variable.
        Vector<Coords> retVal = new Vector<Coords>();

        // Handle boundary conditions.
        if (distance < 0) {
            return retVal.elements();
        }
        if (distance == 0) {
            retVal.add(coords);
            return retVal.elements();
        }

        // Okay, handle the "real" case.
        // This is a bit of a cludge. Is there a better way to do this?
        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
                if (coords.distance(x, y) == distance) {
                    retVal.add(new Coords(x, y));
                    // retVal.add(getHex(x, y));
                }
            }
        }
        return retVal.elements();
    }

    /**
     * Determines if this Board contains the (x, y) Coords, and if so, returns
     * the Hex at that position.
     * 
     * @return the Hex, if this Board contains the (x, y) location; null
     *         otherwise.
     * @param x the x Coords.
     * @param y the y Coords.
     */
    public IHex getHex(int x, int y) {
        if (contains(x, y)) {
            return data[y * width + x];
        }
        return null;
    }

    /**
     * Gets the hex in the specified direction from the specified starting
     * coordinates.
     */
    public IHex getHexInDir(Coords c, int dir) {
        return getHexInDir(c.x, c.y, dir);
    }

    /**
     * Gets the hex in the specified direction from the specified starting
     * coordinates. Avoids calls to Coords.translated, and thus, object
     * construction.
     */
    public IHex getHexInDir(int x, int y, int dir) {
        return getHex(Coords.xInDir(x, y, dir), Coords.yInDir(x, y, dir));
    }

    /**
     * Initialize all hexes
     */
    protected void initializeAll() {
        // Initialize all buildings.
        buildings.removeAllElements();
        if (bldgByCoords == null) {
            bldgByCoords = new Hashtable<Coords, Building>();
        } else {
            bldgByCoords.clear();
        }
        // Walk through the hexes, creating buildings.
        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                // Does this hex contain a building?
                IHex curHex = getHex(x, y);
                if (curHex != null
                        && (curHex.containsTerrain(Terrains.BUILDING))) {
                    // Yup, but is it a repeat?
                    Coords coords = new Coords(x, y);
                    if (!bldgByCoords.containsKey(coords)) {

                        // Nope. Try to create an object for the new building.
                        try {
                            Building bldg = new Building(coords, this,
                                    Terrains.BUILDING);
                            buildings.addElement(bldg);

                            // Each building will identify the hexes it covers.
                            Enumeration<Coords> iter = bldg.getCoords();
                            while (iter.hasMoreElements()) {
                                bldgByCoords.put(iter.nextElement(), bldg);
                            }
                        } catch (IllegalArgumentException excep) {
                            // Log the error and remove the
                            // building from the board.
                            System.err.println("Unable to create building.");
                            excep.printStackTrace();
                            curHex.removeTerrain(Terrains.BUILDING);
                        }
                    } // End building-is-new
                } // End hex-has-building
                if (curHex != null
                        && (curHex.containsTerrain(Terrains.FUEL_TANK))) {
                    // Yup, but is it a repeat?
                    Coords coords = new Coords(x, y);
                    if (!bldgByCoords.containsKey(coords)) {

                        // Nope. Try to create an object for the new building.
                        try {
                            int magnitude = curHex.getTerrain(
                                    Terrains.FUEL_TANK_MAGN).getLevel();
                            FuelTank bldg = new FuelTank(coords, this,
                                    Terrains.FUEL_TANK, magnitude);
                            buildings.addElement(bldg);

                            // Each building will identify the hexes it covers.
                            Enumeration<Coords> iter = bldg.getCoords();
                            while (iter.hasMoreElements()) {
                                bldgByCoords.put(iter.nextElement(), bldg);
                            }
                        } catch (IllegalArgumentException excep) {
                            // Log the error and remove the
                            // building from the board.
                            System.err.println("Unable to create building.");
                            excep.printStackTrace();
                            curHex.removeTerrain(Terrains.BUILDING);
                        }
                    } // End building-is-new
                } // End hex-has-building
                if (curHex != null && curHex.containsTerrain(Terrains.BRIDGE)) {

                    // Yup, but is it a repeat?
                    Coords coords = new Coords(x, y);
                    if (!bldgByCoords.containsKey(coords)) {

                        // Nope. Try to create an object for the new building.
                        try {
                            Building bldg = new Building(coords, this,
                                    Terrains.BRIDGE);
                            buildings.addElement(bldg);

                            // Each building will identify the hexes it covers.
                            Enumeration<Coords> iter = bldg.getCoords();
                            while (iter.hasMoreElements()) {
                                bldgByCoords.put(iter.nextElement(), bldg);
                            }
                        } catch (IllegalArgumentException excep) {
                            // Log the error and remove the
                            // building from the board.
                            System.err.println("Unable to create bridge.");
                            excep.printStackTrace();
                            curHex.removeTerrain(Terrains.BRIDGE);
                        }

                    } // End bridge-is-new

                } // End hex-has-bridge
            }
        }
        // Initialize all exits.
        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                initializeHex(x, y);
            }
        }
        // good time to ensure hex cache
        IdealHex.ensureCacheSize(width + 1, height + 1);

    } // End private void initializeAll()

    /**
     * Initialize a hex and the hexes around it
     */
    public void initializeAround(int x, int y) {
        initializeHex(x, y);
        for (int i = 0; i < 6; i++) {
            initializeInDir(x, y, i);
        }
    }

    /**
     * Initializes a hex in a specific direction from an origin hex
     */
    private void initializeInDir(int x, int y, int dir) {
        initializeHex(Coords.xInDir(x, y, dir), Coords.yInDir(x, y, dir));
    }

    /**
     * Initializes a hex in its surroundings. Currently sets the connects
     * parameter appropriately to the surrounding hexes. If a surrounding hex is
     * off the board, it checks the hex opposite the missing hex.
     */
    private void initializeHex(int x, int y) {
        IHex hex = getHex(x, y);

        if (hex == null) {
            return;
        }

        hex.clearExits();
        for (int i = 0; i < 6; i++) {
            IHex other = getHexInDir(x, y, i);
            hex.setExits(other, i, this.roadsAutoExit);
        }
        processBoardEvent(new BoardEvent(this, new Coords(x, y),
                BoardEvent.BOARD_CHANGED_HEX));
    }

    /**
     * Determines whether this Board "contains" the specified Coords.
     * 
     * @param x the x Coords.
     * @param y the y Coords.
     */
    public boolean contains(int x, int y) {
        return x >= 0 && y >= 0 && x < width && y < height;
    }

    /**
     * Determines whether this Board "contains" the specified Coords.
     * 
     * @param c the Coords.
     */
    public boolean contains(Coords c) {
        if (c == null)
            return false;
        return contains(c.x, c.y);
    }

    /**
     * Returns the Hex at the specified Coords.
     * 
     * @param c the Coords.
     */
    public IHex getHex(Coords c) {
        if (c == null)
            return null;
        return getHex(c.x, c.y);
    }

    /**
     * Determines if this Board contains the (x, y) Coords, and if so, sets the
     * specified Hex into that position and initializes it.
     * 
     * @param x the x Coords.
     * @param y the y Coords.
     * @param hex the hex to be set into position.
     */
    public void setHex(int x, int y, IHex hex) {
        data[y * width + x] = hex;
        initializeAround(x, y);
    }

    /**
     * Sets the hex into the location specified by the Coords.
     * 
     * @param c the Coords.
     * @param hex the hex to be set into position.
     */
    public void setHex(Coords c, IHex hex) {
        setHex(c.x, c.y, hex);
    }

    /**
     * Checks if a file in data/boards is the specified size
     */
    public static boolean boardIsSize(String filename, int x, int y) {
        int boardx = 0;
        int boardy = 0;
        try {
            // make inpustream for board
            Reader r = new BufferedReader(new FileReader("data/boards"
                    + File.separator + filename));
            // read board, looking for "size"
            StreamTokenizer st = new StreamTokenizer(r);
            st.eolIsSignificant(true);
            st.commentChar('#');
            st.quoteChar('"');
            st.wordChars('_', '_');
            while (st.nextToken() != StreamTokenizer.TT_EOF) {
                if (st.ttype == StreamTokenizer.TT_WORD
                        && st.sval.equalsIgnoreCase("size")) {
                    st.nextToken();
                    boardx = (int) st.nval;
                    st.nextToken();
                    boardy = (int) st.nval;
                    break;
                }
            }
            r.close();
        } catch (IOException ex) {
            return false;
        }

        // check and return
        return boardx == x && boardy == y;
    }

    /**
     * Can the player deploy an entity here? There are no canon rules for the
     * deployment phase (?!). I'm using 3 hexes from map edge.
     */
    public boolean isLegalDeployment(Coords c, Player p) {
        if (c == null || p == null || !contains(c))
            return false;

        int nLimit = 3;
        int nDir = p.getStartingPos();
        int minx = 0;
        int maxx = width;
        int miny = 0;
        int maxy = height;
        if (nDir > 10) {
            // Deep deployment, the board is effectively smaller
            nDir -= 10;
            minx = width / 5;
            maxx -= width / 5;
            miny = height / 5;
            maxy -= height / 5;
            if (c.x < minx || c.y < miny || c.x >= maxx || c.y >= maxy)
                return false;
        }
        switch (nDir) {
            case 0: // Any
                return true;
            case 1: // NW
                return (c.x < minx + nLimit && c.x >= minx && c.y < height / 2)
                        || (c.y < miny + nLimit && c.y >= miny && c.x < width / 2);
            case 2: // N
                return c.y < miny + nLimit && c.y >= miny;
            case 3: // NE
                return (c.x > (maxx - nLimit) && c.x < maxx && c.y < height / 2)
                        || (c.y < miny + nLimit && c.y >= miny && c.x > width / 2);
            case 4: // E
                return c.x >= (maxx - nLimit) && c.x < maxx;
            case 5: // SE
                return (c.x >= (maxx - nLimit) && c.x < maxx && c.y > height / 2)
                        || (c.y >= (maxy - nLimit) && c.y < maxy && c.x > width / 2);
            case 6: // S
                return c.y >= (maxy - nLimit) && c.y < maxy;
            case 7: // SW
                return (c.x < minx + nLimit && c.x >= minx && c.y > height / 2)
                        || (c.y >= (maxy - nLimit) && c.y < maxy && c.x < width / 2);
            case 8: // W
                return c.x < minx + nLimit && c.x >= minx;
            case 9: // Edge
                return (c.x < minx + nLimit && c.x >= minx)
                        || (c.y < miny + nLimit && c.y >= miny)
                        || (c.x >= (maxx - nLimit) && c.x < maxx)
                        || (c.y >= (maxy - nLimit) && c.y < maxy);
            case 10: // Centre
                return c.x >= width / 3 && c.x <= 2 * width / 3
                        && c.y >= height / 3 && c.y <= 2 * height / 3;
            default: // ummm. .
                return false;
        }

    }

    /**
     * Loads this board from a filename in data/boards
     */
    public void load(String filename) {
        // load a board
        try {
            java.io.InputStream is = new java.io.FileInputStream(
                    new java.io.File("data/boards", filename));
            // tell the board to load!
            load(is);
            // okay, done!
            is.close();
        } catch (java.io.IOException ex) {
            System.err.println("error opening file to load board!");
            System.err.println(ex);
        }
    }

    /**
     * Loads this board from an InputStream
     */
    public void load(InputStream is) {
        int nw = 0, nh = 0, di = 0;
        IHex[] nd = new IHex[0];
        try {
            Reader r = new BufferedReader(new InputStreamReader(is));
            StreamTokenizer st = new StreamTokenizer(r);
            st.eolIsSignificant(true);
            st.commentChar('#');
            st.quoteChar('"');
            st.wordChars('_', '_');
            int count1 = 1;
            int count2 = 1;
            while (st.nextToken() != StreamTokenizer.TT_EOF) {
                if (st.ttype == StreamTokenizer.TT_WORD
                        && st.sval.equalsIgnoreCase("size")) {
                    // read rest of line
                    String[] args = { "0", "0" };
                    int i = 0;
                    while (st.nextToken() == StreamTokenizer.TT_WORD
                            || st.ttype == '"'
                            || st.ttype == StreamTokenizer.TT_NUMBER) {
                        args[i++] = st.ttype == StreamTokenizer.TT_NUMBER ? (int) st.nval
                                + ""
                                : st.sval;
                    }
                    nw = Integer.parseInt(args[0]);
                    nh = Integer.parseInt(args[1]);
                    nd = new IHex[nw * nh];
                    di = 0;
                } else if (st.ttype == StreamTokenizer.TT_WORD
                        && st.sval.equalsIgnoreCase("option")) {
                    // read rest of line
                    String[] args = { "", "" };
                    int i = 0;
                    while (st.nextToken() == StreamTokenizer.TT_WORD
                            || st.ttype == '"'
                            || st.ttype == StreamTokenizer.TT_NUMBER) {
                        args[i++] = st.ttype == StreamTokenizer.TT_NUMBER ? (int) st.nval
                                + ""
                                : st.sval;
                    }
                    // Only expect certain options.
                    if (args[0].equalsIgnoreCase("exit_roads_to_pavement")) {
                        if (args[1].equalsIgnoreCase("false")) {
                            this.roadsAutoExit = false;
                        } else {
                            this.roadsAutoExit = true;
                        }
                    } // End exit_roads_to_pavement-option
                } else if (st.ttype == StreamTokenizer.TT_WORD
                        && st.sval.equalsIgnoreCase("hex")) {
                    // read rest of line
                    String[] args = { "", "0", "", "" };
                    int i = 0;
                    while (st.nextToken() == StreamTokenizer.TT_WORD
                            || st.ttype == '"'
                            || st.ttype == StreamTokenizer.TT_NUMBER) {
                        args[i++] = st.ttype == StreamTokenizer.TT_NUMBER ? (int) st.nval
                                + ""
                                : st.sval;
                    }
                    int elevation = Integer.parseInt(args[1]);
                    int newIndex = indexFor(args[0], nw, count2);
                    nd[newIndex] = new Hex(elevation, args[2], args[3]);
                    count1++;
                    if (count1 > nw) {
                        count2++;
                        count1 = 1;
                    }
                } else if (st.ttype == StreamTokenizer.TT_WORD
                        && st.sval.equalsIgnoreCase("end")) {
                    break;
                }
            }
        } catch (IOException ex) {
            System.err.println("i/o error reading board");
            System.err.println(ex);
        }
        System.out.println("loading board,loaded, processing" + new Date());

        // fill nulls with blank hexes
        for (int i = 0; i < nd.length; i++) {
            if (nd[i] == null) {
                nd[i] = new Hex();
            }
        }

        // check data integrity
        if (nw > 1 || nh > 1 || di == nw * nh) {
            newData(nw, nh, nd);
        } else {
            System.err.println("board data invalid");
        }

    }

    private int indexFor(String hexNum, int width, int row) {
        int substringDiff = 2;
        if (row > 99)
            substringDiff = Integer.toString(width).length();
        int x = Integer.parseInt(hexNum.substring(0, hexNum.length()
                - substringDiff)) - 1;
        int y = Integer.parseInt(hexNum.substring(hexNum.length()
                - substringDiff)) - 1;
        return y * width + x;
    }

    /**
     * Writes data for the board, as text to the OutputStream
     */
    public void save(OutputStream os) {
        try {
            Writer w = new OutputStreamWriter(os);
            // write
            w.write("size " + width + " " + height + "\r\n");
            if (!this.roadsAutoExit) {
                w.write("option exit_roads_to_pavement false\r\n");
            }
            for (int i = 0; i < data.length; i++) {
                IHex hex = data[i];
                boolean firstTerrain = true;

                StringBuffer hexBuff = new StringBuffer("hex ");
                hexBuff.append(new Coords(i % width, i / width).getBoardNum());
                hexBuff.append(" ");
                hexBuff.append(hex.getElevation());
                hexBuff.append(" \"");
                for (int j = 0; j < Terrains.SIZE; j++) {
                    ITerrain terrain = hex.getTerrain(j);
                    if (terrain != null) {
                        if (!firstTerrain) {
                            hexBuff.append(";");
                        }
                        hexBuff.append(terrain.toString());
                        // Do something funky to save building exits.
                        if (((Terrains.BUILDING == j) || (j == Terrains.FUEL_TANK))
                                && !terrain.hasExitsSpecified()
                                && terrain.getExits() != 0) {
                            hexBuff.append(":").append(terrain.getExits());
                        }
                        firstTerrain = false;
                    }
                }
                hexBuff.append("\" \"");
                if (hex.getTheme() != null) {
                    hexBuff.append(hex.getTheme());
                }
                hexBuff.append("\"\r\n");

                w.write(hexBuff.toString());
                // w.write("hex \"" + hex.getTerrain().name + "\" " +
                // Terrain.TERRAIN_NAMES[hex.getTerrainType()] + " \"" +
                // hex.getTerrain().picfile + "\" " + hex.getElevation() +
                // "\r\n");
            }
            w.write("end\r\n");
            // make sure it's written
            w.flush();
        } catch (IOException ex) {
            System.err.println("i/o error writing board");
            System.err.println(ex);
        }
    }

    /**
     * Writes data for the board, as serialization, to the OutputStream
     */
    public void save2(OutputStream os) {
        try {
            ObjectOutputStream oos = new ObjectOutputStream(os);
            oos.writeObject(this);
            oos.flush();
        } catch (IOException ex) {
            System.err.println("i/o error writing board");
            System.err.println(ex);
        }
    }

    /**
     * Record that the given coordinates have recieved a hit from an inferno.
     * 
     * @param coords - the <code>Coords</code> of the hit.
     * @param round - the kind of round that hit the hex.
     * @param hits - the <code>int</code> number of rounds that hit. If a
     *            negative number is passed, then an
     *            <code>IllegalArgumentException</code> will be thrown.
     */
    public void addInfernoTo(Coords coords, InfernoTracker.Inferno round,
            int hits) {
        // Declare local variables.
        InfernoTracker tracker = null;

        // Make sure the # of hits is valid.
        if (hits < 0) {
            throw new IllegalArgumentException(
                    "Board can't track negative hits. ");
        }

        // Do nothing if the coords aren't on this board.
        if (!this.contains(coords)) {
            return;
        }

        // Do we already have a tracker for those coords?
        tracker = this.infernos.get(coords);
        if (null == tracker) {
            // Nope. Make one.
            tracker = new InfernoTracker();
            this.infernos.put(coords, tracker);
        }

        // Update the tracker.
        tracker.add(round, hits);

    }

    public void removeInfernoFrom(Coords coords) {
        // Do nothing if the coords aren't on this board.
        if (!this.contains(coords)) {
            return;
        }
        infernos.remove(coords);
    }

    /**
     * Determine if the given coordinates has a burning inferno.
     * 
     * @param coords - the <code>Coords</code> being checked.
     * @return <code>true</code> if those coordinates have a burning inferno
     *         round. <code>false</code> if no inferno has hit those
     *         coordinates or if it has burned out.
     */
    public boolean isInfernoBurning(Coords coords) {
        boolean result = false;
        InfernoTracker tracker = null;

        // Get the tracker for those coordinates
        // and see if the fire is still burning.
        tracker = this.infernos.get(coords);
        if (null != tracker) {
            if (tracker.isStillBurning()) {
                result = true;
            }
        }

        return result;
    }

    /**
     * Record that a new round of burning has passed for the given coordinates.
     * This routine also determines if the fire is still burning.
     * 
     * @param coords - the <code>Coords</code> being checked.
     * @return <code>true</code> if those coordinates have a burning inferno
     *         round. <code>false</code> if no inferno has hit those
     *         coordinates or if it has burned out.
     */
    public boolean burnInferno(Coords coords) {
        boolean result = false;
        InfernoTracker tracker = null;

        // Get the tracker for those coordinates, record the round
        // of burning and see if the fire is still burning.
        tracker = this.infernos.get(coords);
        if (null != tracker) {
            tracker.newRound(-1);
            if (tracker.isStillBurning()) {
                result = true;
            } else {
                infernos.remove(coords);
            }
        }

        return result;
    }

    /**
     * Get an enumeration of all coordinates with infernos still burning.
     * 
     * @return an <code>Enumeration</code> of <code>Coords</code> that have
     *         infernos still burning.
     */
    public Enumeration<Coords> getInfernoBurningCoords() {
        // Only include *burning* inferno trackers.
        Vector<Coords> burning = new Vector<Coords>();
        Enumeration<Coords> iter = this.infernos.keys();
        while (iter.hasMoreElements()) {
            final Coords coords = iter.nextElement();
            if (this.isInfernoBurning(coords)) {
                burning.addElement(coords);
            }
        }
        return burning.elements();
    }

    /**
     * Determine the remaining number of turns the given coordinates will have a
     * burning inferno.
     * 
     * @param coords - the <code>Coords</code> being checked. This value must
     *            not be <code>null</code>. Unchecked.
     * @return the <code>int</code> number of burn turns left for all infernos
     *         This value will be non-negative.
     */
    public int getInfernoBurnTurns(Coords coords) {
        int turns = 0;
        InfernoTracker tracker = null;

        // Get the tracker for those coordinates
        // and see if the fire is still burning.
        tracker = this.infernos.get(coords);
        if (null != tracker) {
            turns = tracker.getTurnsLeftToBurn();
        }
        return turns;
    }

    /**
     * Determine the remaining number of turns the given coordinates will have a
     * burning Inferno IV round.
     * 
     * @param coords - the <code>Coords</code> being checked. This value must
     *            not be <code>null</code>. Unchecked.
     * @return the <code>int</code> number of burn turns left for Arrow IV
     *         infernos. This value will be non-negative.
     */
    public int getInfernoIVBurnTurns(Coords coords) {
        int turns = 0;
        InfernoTracker tracker = null;

        // Get the tracker for those coordinates
        // and see if the fire is still burning.
        tracker = this.infernos.get(coords);
        if (null != tracker) {
            turns = tracker.getArrowIVTurnsLeftToBurn();
        }
        return turns;
    }

    /**
     * Get an enumeration of all buildings on the board.
     * 
     * @return an <code>Enumeration</code> of <code>Building</code>s.
     */
    public Enumeration<Building> getBuildings() {
        return this.buildings.elements();
    }

    /**
     * Get the building at the given coordinates.
     * 
     * @param coords - the <code>Coords</code> being examined.
     * @return a <code>Building</code> object, if there is one at the given
     *         coordinates, otherwise a <code>null</code> will be returned.
     */
    public Building getBuildingAt(Coords coords) {
        return this.bldgByCoords.get(coords);
    }

    /**
     * Get the local object for the given building. Call this routine any time
     * the input <code>Building</code> is suspect.
     * 
     * @param other - a <code>Building</code> object which may or may not be
     *            represented on this board. This value may be <code>null</code>.
     * @return The local <code>Building</code> object if we can find a match.
     *         If the other building is not on this board, a <code>null</code>
     *         is returned instead.
     */
    private Building getLocalBuilding(Building other) {

        // Handle garbage input.
        if (other == null) {
            return null;
        }

        // ASSUMPTION: it is better to use the Hashtable than the Vector.
        Building local = null;
        Enumeration<Coords> coords = other.getCoords();
        if (coords.hasMoreElements()) {
            local = bldgByCoords.get(coords.nextElement());
            if (!other.equals(local)) {
                local = null;
            }
        }

        // TODO: if local is still null, try the Vector.
        return local;
    }

    /**
     * Collapse an array of buildings.
     * 
     * @param bldgs - the <code>Vector</code> of <code>Building</code>
     *            objects to be collapsed.
     */
    public void collapseBuilding(Vector<Building> bldgs) {

        // Walk through the vector of buildings.
        Enumeration<Building> loop = bldgs.elements();
        while (loop.hasMoreElements()) {
            final Building other = loop.nextElement();

            // Find the local object for the given building.
            Building bldg = this.getLocalBuilding(other);

            // Handle garbage input.
            if (bldg == null) {
                System.err.print("Could not find a match for ");
                System.err.print(other);
                System.err.println(" to collapse.");
                continue;
            }

            // Update the building.
            this.collapseBuilding(bldg);

        } // Handle the next building.

    }

    /**
     * The given building hex has collapsed. Remove it from the board and
     * replace it with rubble.
     * 
     * @param other - the <code>Building</code> that has collapsed.
     */
    public void collapseBuilding(Coords coords) {
        final IHex curHex = this.getHex(coords);
        int elevation = curHex.getElevation();

        // Remove the building from the building map.
        this.bldgByCoords.remove(coords);

        // determine type of rubble
        // Terrain type can be a max of 4 for harded building
        // I have know idea how to fix it for Walls need Coelocanth
        // Or someone else to fix my hacky hack. -- Torren.
        int type = Math.min(curHex.terrainLevel(Terrains.BUILDING), 4);
        type = Math.max(type, curHex.terrainLevel(Terrains.BRIDGE));
        type = Math.max(type, curHex.terrainLevel(Terrains.FUEL_TANK));

        // Remove the building terrain.
        curHex.removeTerrain(Terrains.BUILDING);
        curHex.removeTerrain(Terrains.BLDG_CF);
        curHex.removeTerrain(Terrains.BLDG_ELEV);
        curHex.removeTerrain(Terrains.FUEL_TANK);
        curHex.removeTerrain(Terrains.FUEL_TANK_CF);
        curHex.removeTerrain(Terrains.FUEL_TANK_ELEV);
        curHex.removeTerrain(Terrains.BRIDGE);
        curHex.removeTerrain(Terrains.BRIDGE_CF);
        curHex.removeTerrain(Terrains.BRIDGE_ELEV);

        // Add rubble terrain that matches the building type.
        if (type > 0) {
            curHex.addTerrain(Terrains.getTerrainFactory().createTerrain(
                    Terrains.RUBBLE, type));
        }

        // Any basement reduces the hex's elevation.
        if (curHex.containsTerrain(Terrains.BLDG_BASEMENT)) {
            elevation -= curHex.terrainLevel(Terrains.BLDG_BASEMENT);
            curHex.removeTerrain(Terrains.BLDG_BASEMENT);
            curHex.setElevation(elevation);
        }

        // Update the hex.
        // TODO : Do I need to initialize it???
        // ASSUMPTION: It's faster to update one at a time.
        this.setHex(coords, curHex);

    }

    /**
     * The given building has collapsed. Remove it from the board and replace it
     * with rubble.
     * 
     * @param other - the <code>Building</code> that has collapsed.
     */
    public void collapseBuilding(Building bldg) {

        // Remove the building from our building vector.
        this.buildings.removeElement(bldg);

        // Walk through the building's hexes.
        Enumeration<Coords> bldgCoords = bldg.getCoords();
        while (bldgCoords.hasMoreElements()) {
            final Coords coords = bldgCoords.nextElement();
            collapseBuilding(coords);
        } // Handle the next building hex.

    } // End public void collapseBuilding( Building )

    /**
     * Update the construction factors on an array of buildings.
     * 
     * @param bldgs - the <code>Vector</code> of <code>Building</code>
     *            objects to be updated.
     */
    public void updateBuildingCF(Vector<Building> bldgs) {

        // Walk through the vector of buildings.
        Enumeration<Building> loop = bldgs.elements();
        while (loop.hasMoreElements()) {
            final Building other = loop.nextElement();

            // Find the local object for the given building.
            Building bldg = this.getLocalBuilding(other);

            // Handle garbage input.
            if (bldg == null) {
                System.err.print("Could not find a match for ");
                System.err.print(other);
                System.err.println(" to update.");
                continue;
            }

            // Set the current and phase CFs of the building.
            bldg.setCurrentCF(other.getCurrentCF());
            bldg.setPhaseCF(other.getPhaseCF());

        } // Handle the next building.

    }

    /**
     * Get the current value of the "road auto-exit" option.
     * 
     * @return <code>true</code> if roads should automatically exit onto all
     *         adjacent pavement hexes. <code>false</code> otherwise.
     */
    public boolean getRoadsAutoExit() {
        return this.roadsAutoExit;
    }

    /**
     * Set the value of the "road auto-exit" option.
     * 
     * @param value - The value to set for the option; <code>true</code> if
     *            roads should automatically exit onto all adjacent pavement
     *            hexes. <code>false</code> otherwise.
     */
    public void setRoadsAutoExit(boolean value) {
        this.roadsAutoExit = value;
    }

    /**
     * Populate the <code>bldgByCoords</code> member from the current
     * <code>Vector</code> of buildings. Use this method after de- serializing
     * a <code>Board</code> object.
     */
    private void createBldgByCoords() {

        // Make a new hashtable.
        bldgByCoords = new Hashtable<Coords, Building>();

        // Walk through the vector of buildings.
        Enumeration<Building> loop = buildings.elements();
        while (loop.hasMoreElements()) {
            final Building bldg = loop.nextElement();

            // Each building identifies the hexes it covers.
            Enumeration<Coords> iter = bldg.getCoords();
            while (iter.hasMoreElements()) {
                bldgByCoords.put(iter.nextElement(), bldg);
            }

        }

    }

    /**
     * Override the default deserialization to populate the transient
     * <code>bldgByCoords</code> member.
     * 
     * @param in - the <code>ObjectInputStream</code> to read.
     * @throws <code>IOException</code>
     * @throws <code>ClassNotFoundException</code>
     */
    private void readObject(ObjectInputStream in) throws IOException,
            ClassNotFoundException {
        in.defaultReadObject();

        // Restore bldgByCoords from buildings.
        createBldgByCoords();
    }

    /*
     * (non-Javadoc)
     * 
     * @see megamek.common.IBoard#addBoardListener(megamek.common.BoardListener)
     */
    public void addBoardListener(BoardListener listener) {
        if (boardListeners == null) {
            boardListeners = new Vector<BoardListener>();
        }
        boardListeners.addElement(listener);
    }

    /*
     * (non-Javadoc)
     * 
     * @see megamek.common.IBoard#removeBoardListener(megamek.common.BoardListener)
     */
    public void removeBoardListener(BoardListener listener) {
        if (boardListeners != null) {
            boardListeners.removeElement(listener);
        }
    }

    protected void processBoardEvent(BoardEvent event) {
        if (boardListeners == null) {
            return;
        }
        for (Enumeration<BoardListener> e = boardListeners.elements(); e
                .hasMoreElements();) {
            BoardListener l = e.nextElement();
            switch (event.getType()) {
                case BoardEvent.BOARD_CHANGED_HEX:
                    l.boardChangedHex(event);
                    break;
                case BoardEvent.BOARD_NEW_BOARD:
                    l.boardNewBoard(event);
                    break;
            }
        }
    }

    protected Vector<BoardListener> getListeners() {
        if (boardListeners == null) {
            boardListeners = new Vector<BoardListener>();
        }
        return boardListeners;
    }

    // Is there any way I can get around using this?
    public Hashtable<Coords, InfernoTracker> getInfernos() {
        return infernos;
    }

    public void setBridgeCF(int value) {
        for (Enumeration<Building> ebldg = buildings.elements(); ebldg
                .hasMoreElements();) {
            Building bldg = ebldg.nextElement();
            for (Enumeration<Coords> coords = bldg.getCoords(); coords
                    .hasMoreElements();) {
                Coords c = coords.nextElement();
                IHex h = getHex(c);
                if (h.containsTerrain(Terrains.BRIDGE)) {
                    bldg.setCurrentCF(value);
                }
            }
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see megamek.common.IBoard#getSpecialHexDisplay(megamek.common.Coords)
     */
    public Collection<SpecialHexDisplay> getSpecialHexDisplay(Coords coords) {
        return specialHexes.get(coords);
    }

    /*
     * (non-Javadoc)
     * 
     * @see megamek.common.IBoard#addSpecialHexDisplay(megamek.common.Coords,
     *      megamek.common.SpecialHexDisplay)
     */
    public void addSpecialHexDisplay(Coords coords, SpecialHexDisplay shd) {
        Collection<SpecialHexDisplay> col;
        if (!specialHexes.containsKey(coords)) {
            col = new LinkedList<SpecialHexDisplay>();
            specialHexes.put(coords, col);
        } else {
            col = specialHexes.get(coords);
        }

        col.add(shd);
    }

    /*
     * (non-Javadoc)
     * 
     * @see megamek.common.IBoard#getSpecialHexDisplayTable()
     */
    public Hashtable<Coords, Collection<SpecialHexDisplay>> getSpecialHexDisplayTable() {
        return specialHexes;
    }

    /*
     * (non-Javadoc)
     * 
     * @see megamek.common.IBoard#setSpecialHexDisplayTable(java.util.Hashtable)
     */
    public void setSpecialHexDisplayTable(
            Hashtable<Coords, Collection<SpecialHexDisplay>> shd) {
        specialHexes = shd;
    }
    
    public void setType(int t) {
        this.mapType = t;
    }
    
    public int getType() {
        return mapType;
    }
    
    public static String getTypeName(int t) {
        return typeNames[t];
    }
    
    //some convenience functions   
    public boolean onGround() {
        return (mapType == T_GROUND);
    }
    
    public boolean inAtmosphere() {
        return (mapType == T_ATMOSPHERE);
    }
    
    public boolean inSpace() {
        return (mapType == T_SPACE);
    }
    
}
