/*
 * Copyright (c) 2008 by Michael Kiefte
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License (LGPL) as published by the Free Software Foundation.
 *
 * This library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, copies are available
 * at http://www.opensource.org.
 */

package VASSAL.tools.imports.adc2;

import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.font.TextAttribute;
import java.awt.geom.GeneralPath;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;

import javax.imageio.ImageIO;

import VASSAL.build.AbstractConfigurable;
import VASSAL.build.GameModule;
import VASSAL.build.module.GlobalOptions;
import VASSAL.build.module.Inventory;
import VASSAL.build.module.Map;
import VASSAL.build.module.PrototypeDefinition;
import VASSAL.build.module.PrototypesContainer;
import VASSAL.build.module.ToolbarMenu;
import VASSAL.build.module.map.BoardPicker;
import VASSAL.build.module.map.LayerControl;
import VASSAL.build.module.map.LayeredPieceCollection;
import VASSAL.build.module.map.SetupStack;
import VASSAL.build.module.map.Zoomer;
import VASSAL.build.module.map.boardPicker.Board;
import VASSAL.build.module.map.boardPicker.board.HexGrid;
import VASSAL.build.module.map.boardPicker.board.MapGrid;
import VASSAL.build.module.map.boardPicker.board.SquareGrid;
import VASSAL.build.module.map.boardPicker.board.ZonedGrid;
import VASSAL.build.module.map.boardPicker.board.MapGrid.BadCoords;
import VASSAL.build.module.map.boardPicker.board.mapgrid.HexGridNumbering;
import VASSAL.build.module.map.boardPicker.board.mapgrid.RegularGridNumbering;
import VASSAL.build.module.map.boardPicker.board.mapgrid.SquareGridNumbering;
import VASSAL.build.module.map.boardPicker.board.mapgrid.Zone;
import VASSAL.build.widget.PieceSlot;
import VASSAL.configure.StringArrayConfigurer;
import VASSAL.counters.BasicPiece;
import VASSAL.counters.GamePiece;
import VASSAL.counters.Immobilized;
import VASSAL.counters.Marker;
import VASSAL.counters.UsePrototype;
import VASSAL.tools.SequenceEncoder;
import VASSAL.tools.filechooser.ExtensionFileFilter;
import VASSAL.tools.imports.FileFormatException;
import VASSAL.tools.imports.Importer;
import VASSAL.tools.io.IOUtils;
import VASSAL.tools.io.TempFileManager;

/**
 * The map board itself.
 * 
 * @author Michael Kiefte
 *
 */
public class MapBoard extends Importer {

  private static final String PLACE_NAME = "Location Names";

  protected class MapLayer {
    private final ArrayList<? extends MapDrawable> elements; 
    private final String name;
    private final boolean switchable;
    protected ArrayList<MapLayer> layers = null;
    protected String imageName;
    boolean shouldDraw = true;
    
    MapLayer(ArrayList<? extends MapDrawable> elements, String name, boolean switchable) {
      this.elements = elements;
      this.name = name;
      this.switchable = switchable;
    }        
    
    void writeToArchive() throws IOException {
      // write piece
      Rectangle r = writeImageToArchive();
      if (imageName != null && r != null && r.width > 0 && r.height > 0) {
        SequenceEncoder se = new SequenceEncoder(';');
        se.append("").append("").append(imageName).append(getName());
        GamePiece gp = new BasicPiece(BasicPiece.ID + se.getValue());
        gp = new Marker(Marker.ID + "Layer", gp);
        gp.setProperty("Layer", getName());
        gp = new Marker(Marker.ID + "Type", gp);
        gp.setProperty("Type", "Layer");
        gp = new Immobilized(gp, Immobilized.ID + "n;V");

        // create layer
        LayeredPieceCollection l = getLayeredPieceCollection();
        String order = l.getAttributeValueString(LayeredPieceCollection.LAYER_ORDER);
        if (order.equals("")) {
          order = getName();
        }
        else {
          order = order + "," + getName();
        }
        l.setAttribute(LayeredPieceCollection.LAYER_ORDER, order);

        Map mainMap = getMainMap();
        Board board = getBoard();
        SetupStack stack = new SetupStack();
        insertComponent(stack, mainMap);
        Point p = new Point(r.x + r.width/2, r.y + r.height/2);
        stack.setAttribute(SetupStack.NAME, getName());
        stack.setAttribute(SetupStack.OWNING_BOARD, board.getConfigureName());
        stack.setAttribute(SetupStack.X_POSITION, Integer.toString(p.x));
        stack.setAttribute(SetupStack.Y_POSITION, Integer.toString(p.y));

        PieceSlot slot = new PieceSlot(gp);
        insertComponent(slot, stack);
        
        if (isSwitchable()) {
          // TODO: initial state of layer visibility
          // add stack layer control
          LayerControl control = new LayerControl();
          insertComponent(control, l);
          control.setAttribute(LayerControl.BUTTON_TEXT, getName());
          control.setAttribute(LayerControl.TOOLTIP, "Toggle " + getName().toLowerCase() + " visibility");
          control.setAttribute(LayerControl.COMMAND, LayerControl.CMD_TOGGLE);
          control.setAttribute(LayerControl.LAYERS, getName());

          // one toolbar menu to control all mapboard elements.
          ToolbarMenu menu = getToolbarMenu();
          String entries = menu.getAttributeValueString(ToolbarMenu.MENU_ITEMS);
          if (entries.equals("")) {
            entries = getName();
          }
          else {
            entries = entries + "," + new SequenceEncoder(getName(), ',').getValue();
          }
          menu.setAttribute(ToolbarMenu.MENU_ITEMS, entries);
        }
      }
    }

    /**
     * @throws IOException
     */
    protected Rectangle writeImageToArchive() throws IOException {
      // write image to archive
      final BufferedImage image = getLayerImage();
      if (image != null) {
        final Rectangle r = getCropRectangle(image);
        if (r.width == 0 || r.height == 0) {
        	return null;
        }
        final File f =
          TempFileManager.getInstance().createTempFile("map", ".png");
        ImageIO.write(image.getSubimage(r.x, r.y, r.width, r.height), "png", f);

        imageName = getUniqueImageFileName(getName(), ".png");
        GameModule.getGameModule()
                  .getArchiveWriter()
                  .addImage(f.getPath(), imageName);
        return r;
      }
      else {
        return null;
      }
    }
    
    protected Rectangle getCropRectangle(BufferedImage image) {
      Rectangle r = new Rectangle(getLayout().getBoardSize());
      leftside:
        while (true) {
          for (int i = r.y; i < r.y + r.height; ++i) {
            if (image.getRGB(r.x, i) != 0) {
              break leftside;
            }
          }
          ++r.x;
          --r.width;
          if (r.width == 0) {
        	  r.height = 0;
        	  return r;
          }
        }
      topside:
        while (true) {
          for (int i = r.x; i < r.x + r.width; ++i) {
            if (image.getRGB(i, r.y) != 0) {
              break topside;
            }
          }
          ++r.y;
          --r.height;
        }
      rightside:
        while (true) {
          for (int i = r.y; i < r.y + r.height; ++i) {
            if (image.getRGB(r.x + r.width - 1, i) != 0) {
              break rightside;
            }
          }
          --r.width;
        }
      bottomside:
        while (true) {
          for (int i = r.x; i < r.x + r.width; ++i) {
            if (image.getRGB(i, r.y + r.height - 1) != 0) {
              break bottomside;
            }
          }
          --r.height;
        }
      return r;
    }

    void overlay(MapLayer layer) {
      if (layers == null) {
        layers = new ArrayList<MapLayer>();
      }
      layers.add(layer);
    }
    
    protected AlphaComposite getComposite() {
      return AlphaComposite.SrcAtop;
    }
    
    BufferedImage getLayerImage() {
      Dimension d = getLayout().getBoardSize();
      BufferedImage image = new BufferedImage(d.width, d.height, BufferedImage.TYPE_INT_ARGB);
      Graphics2D g = image.createGraphics();
      if (draw(g)) {
        if (layers != null) {
          g.setComposite(getComposite());
          for (MapLayer l : layers) {
            l.draw(g);
          }
        }
      }
      else {
        image = null;
      }
      return image;
    }
    
    boolean draw(Graphics2D g) {
      if (shouldDraw) {
        shouldDraw = false;
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,   RenderingHints.VALUE_ANTIALIAS_OFF);
        g.setRenderingHint(RenderingHints.KEY_RENDERING,      RenderingHints.VALUE_RENDER_QUALITY);
        g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);

        for (MapDrawable m : elements) {
          if (m.draw(g)) {
            shouldDraw = true;
          }
        }
      }
      return shouldDraw;
    }
    
    boolean isSwitchable() {
      return switchable;
    }
    
    String getName() {
      return name;
    }
    
    boolean hasElements() {
      return !elements.isEmpty();
    }
  }

  class BaseLayer extends MapLayer {

    BaseLayer() {
      super(null, "Base Layer", false);
    }

    boolean hasBaseMap() {
      File underlay = action.getCaseInsensitiveFile(new File(forceExtension(path, "sml")), null, false, null);
      if (underlay == null) {
        underlay = action.getCaseInsensitiveFile(new File(stripExtension(path) + "-Z" + (zoomLevel+1) + ".bmp"), 
            null, false, null);        
      }
      return underlay != null;
    }
    
    @Override
    boolean draw(Graphics2D g) {
      // set background color
      g.setBackground(tableColor);
      Dimension d = getLayout().getBoardSize();
      g.clearRect(0, 0, d.width, d.height);
      
      // See if map image file exists
      File sml = action.getCaseInsensitiveFile(new File(forceExtension(path, "sml")), null, false, null);
      if (sml != null) {
        try {
          readScannedMapLayoutFile(sml, g);
        }
        // FIXME: review error message
        catch (IOException e) {}
      }
      else if (getSet().underlay != null) {
        // If sml file doesn't exist, see if there is a single-sheet underlay image
    	g.drawImage(getSet().underlay, null, 0, 0);
      }  
      return true;      
    }    

    @Override
    void writeToArchive() throws IOException {
      // write the underlay map image
      writeImageToArchive();
      assert(imageName != null);
      Board board = getBoard();
      board.setAttribute(Board.IMAGE, imageName);
      board.setConfigureName(baseName);
      
      // so we can get hex labels
      getMainMap().setBoards(Collections.singleton(board));
    }

    @Override
    protected Rectangle getCropRectangle(BufferedImage image) {
      return new Rectangle(getLayout().getBoardSize());
    }

    @Override
    protected AlphaComposite getComposite() {
      return AlphaComposite.SrcOver;
    }
  }
  
  /**
   * A layout consisting of squares in a checkerboard pattern (<it>i.e.</it> each
   * square has four neighbours).
   * 
   * @author Michael Kiefte
   *
   */
  protected class GridLayout extends Layout {

    GridLayout(int size, int columns, int rows) {
      super(size, columns, rows);
    }

    @Override
    Point coordinatesToPosition(int x, int y, boolean nullIfOffBoard) {
      if (!nullIfOffBoard || isOnMapBoard(x, y)) {
        int xx = getDeltaX() * x;
        int yy = getDeltaY() * y;
        return new Point(xx, yy);
      } else
        return null;
    }

    @Override
    Dimension getBoardSize() {
      Dimension d = new Dimension();
      d.width = getDeltaX() * nColumns;
      d.height = getDeltaY() * nRows;
      return d;
    }

    @Override
    int getDeltaX() {
      return getHexSize();
    }

    @Override
    int getDeltaY() {
      return getHexSize();
    }

    @Override 
    Point getOrigin() {
      return new Point(getHexSize() / 2, getHexSize() / 2);
    }
    
    @Override
    SquareGrid getGeometricGrid() {
      SquareGrid grid = new SquareGrid();
      grid.setOrigin(getOrigin());
      grid.setDx(getDeltaX());
      grid.setDy(getDeltaY());

      return grid;
    }

    @Override
    Rectangle getRectangle(MapSheet map) {
      Rectangle r = map.getField();

      Point upperLeft = coordinatesToPosition(r.x, r.y, false);
      Point lowerRight = coordinatesToPosition(r.x + r.width - 1, r.y
          + r.height - 1, false);

      // get lower right-hand corner of lower right-hand square
      lowerRight.x += getHexSize() - 1;
      lowerRight.y += getHexSize() - 1;

      constrainRectangle(upperLeft, lowerRight);
      
      return new Rectangle(upperLeft.x, upperLeft.y, lowerRight.x
          - upperLeft.x + 1, lowerRight.y - upperLeft.y + 1);
    }

    @Override
    RegularGridNumbering getGridNumbering() {
      return new SquareGridNumbering();
    }

    @Override
    int getNFaces() {
      return 4;
    }
  }

  /**
   * Redundant information about each hex. So far only used for determining
   * the default order of line definitions for hex sides and hex lines.
   */
  private static class Hex {
    ArrayList<Line> hexLines = new ArrayList<Line>();

    ArrayList<Line> hexSides = new ArrayList<Line>();
  }

  /**
   * Mapboard element based on terrain symbol from <code>SymbolSet</code>. Not necessarily hexagonal but can also be square.
   */ 
  protected class HexData extends MapDrawable {

    final SymbolSet.SymbolData symbol;

    HexData(int index, SymbolSet.SymbolData symbol) {
      super(index);
      assert (symbol != null);
      this.symbol = symbol;
    }    
    
    @Override
    boolean draw(Graphics2D g) {
      Point p = getPosition();
      if (symbol != null && !symbol.isTransparent()) {
        g.drawImage(symbol.getImage(), null, p.x, p.y);
        return true;
      }
      else {
        return false;
      }
    }
  }
  
  /**
   * Symbol that is placed in every hex.
   */
  protected class MapBoardOverlay extends HexData {
    
    @Override
    boolean draw(Graphics2D g) {
      if (symbol != null) {        
        for (int y = 0; y < getNRows(); ++y) {
          for (int x = 0; x < getNColumns(); ++x) {
            Point p = coordinatesToPosition(x, y);
            g.drawImage(symbol.getImage(), null, p.x, p.y);
          }
        }
        return true;
      }
      else {
        return false;
      }
    }

    // doesn't have an index
    MapBoardOverlay(SymbolSet.SymbolData symbol) {
      super(-1, symbol);
    }    
  }

  /**
   * A line from a hex edge to the centre as in the spoke of a wheel. Typically used for terrain
   * features such as roads etc.
   */
  protected class HexLine extends Line {

    private final int direction;

    HexLine(int index, int line, int direction) {
      super(index, line);
      this.direction = direction;
    }

    @Override
    int compare(LineDefinition o1, LineDefinition o2) {
      if (o1 == null && o2 == null)
        return 0;
      else if (o1 == null)
        return 1;
      else if (o2 == null)
        return -1;
      int priority = o1.getHexLineDrawPriority() - o2.getHexLineDrawPriority();
      if (priority != 0)
        return priority;
      else
        return super.compare(o1, o2);
    }

    /* 
     * Under normal circumstances, map board elements get drawn one at a time. Hex sides and hex lines are
     * actually continuous from one hex or square to the next. Although ADC2 draws each segment separately
     * anyway, this creates poor-looking corner and edge effects.  Instead of that, the importer composes longer
     * lines made up of many continuous smaller segments and then draws a single line in one go.  To do this,
     * instead of drawing itself as it's called, each segment adds itself to a line and the last segment in the
     * list calls the draw method for all of the lines thus created.
     */
    @Override
    boolean draw(Graphics2D g) {
      LineDefinition l = getLine();
      boolean result = false;
      
      if (l != null) {
        result = true;
        Point pos = getPosition();
        int size = getLayout().getHexSize();
        pos.translate(size / 2, size / 2);
        Layout lo = getLayout();

        // if ((direction & 0x1) > 0) // horizontal north west
        if ((direction & 0x6) > 0) {// north west; 0x4 = version 1
          Point nw = lo.getNorthWest(hexIndex);
          nw.translate(size / 2, size / 2);
          l.addLine(pos.x, pos.y, (pos.x + nw.x) / 2.0f, (pos.y + nw.y) / 2.0f);
        }
        if ((direction & 0x8) > 0) {// west
          Point w = lo.getWest(hexIndex);
          w.translate(size / 2, size / 2);
          l.addLine(pos.x, pos.y, (pos.x + w.x) / 2.0f, (pos.y + w.y) / 2.0f);
        }
        if ((direction & 0x30) > 0) { // south west; 0x10 = version 1
          Point sw = lo.getSouthWest(hexIndex);
          sw.translate(size / 2, size / 2);
          l.addLine(pos.x, pos.y, (pos.x + sw.x) / 2.0f, (pos.y + sw.y) / 2.0f);
        }
        // if ((direction & 0x40) > 0) // horizontal south west
        if ((direction & 0x80) > 0) {// south
          Point s = lo.getSouth(hexIndex);
          s.translate(size / 2, size / 2);
          l.addLine(pos.x, pos.y, (pos.x + s.x) / 2.0f, (pos.y + s.y) / 2.0f);
        }
        if ((direction & 0x100) > 0) {// north
          Point n = lo.getNorth(hexIndex);
          n.translate(size / 2, size / 2);
          l.addLine(pos.x, pos.y, (pos.x + n.x) / 2.0f,
              (pos.y + n.y) / 2.0f);
        }
        // if ((direction & 0x200) > 0) // horizontal north east
        if ((direction & 0xC00) > 0) { // north east; 0x800 = version 1
          Point ne = lo.getNorthEast(hexIndex);
          ne.translate(size / 2, size / 2);
          l.addLine(pos.x, pos.y, (pos.x + ne.x) / 2.0f, (pos.y + ne.y) / 2.0f);
        }
        if ((direction & 0x1000) > 0) {// east
          Point e = lo.getEast(hexIndex);
          e.translate(size / 2, size / 2);
          l.addLine(pos.x, pos.y, (pos.x + e.x) / 2.0f, (pos.y + e.y) / 2.0f);
        }
        if ((direction & 0x6000) > 0) { // south east; 0x2000 = version 1
          Point se = lo.getSouthEast(hexIndex);
          se.translate(size / 2, size / 2);
          l.addLine(pos.x, pos.y, (pos.x + se.x) / 2.0f, (pos.y + se.y) / 2.0f);
        }
        // if ((direction & 0x8000) > 0) // horizontal south east
      }
      
      // if this is the last one, draw all of the compiled lines.
      if (this == hexLines.get(hexLines.size() - 1)) {
        drawLines(g, BasicStroke.CAP_BUTT);
      }
      
      return result;
    }
    
    @Override
    ArrayList<Line> getLineList(Hex h) {
      return h.hexLines;
    }
  }

  /**
   * The edges of a hex or square.
    */
  protected class HexSide extends Line {

    // flags indicating which side to draw.
    private final int side;

    HexSide(int index, int line, int side) {
      super(index, line);
      this.side = side;
    }

    @Override
    int compare(LineDefinition o1, LineDefinition o2) {
      if (o1 == null && o2 == null)
        return 0;
      else if (o1 == null)
        return 1;
      else if (o2 == null)
        return -1;
      int priority = o1.getHexSideDrawPriority() - o2.getHexSideDrawPriority();
      if (priority != 0)
        return priority;
      else
        return super.compare(o1, o2);
    }

    // see the comments for HexLine.draw(Graphics2D).
    @Override
    boolean draw(Graphics2D g) {

      LineDefinition l = getLine();
      boolean result = false;

      if (l != null) {
        result = true;
        Point p = getPosition();
        int size = getLayout().getHexSize();
        int dX = getLayout().getDeltaX();
        int dY = getLayout().getDeltaY();

        if ((side & 0x1) > 0) { // vertical SW
          Point sw = getLayout().getSouthWest(hexIndex);
          sw.translate(dX, 0);
          Point s = getLayout().getSouth(hexIndex);
          l.addLine(p.x, sw.y, p.x + (size / 5), s.y);
        }
        if ((side & 0x2) > 0) { // vertical NW
          Point sw = getLayout().getSouthWest(hexIndex);
          sw.translate(dX, 0);
          l.addLine(p.x, sw.y, p.x + (size / 5), p.y);
        }
        if ((side & 0x4) > 0) { // vertical N
          l.addLine(p.x + (size / 5), p.y, p.x + dX, p.y);
        }
        if ((side & 0x8) > 0) { // horizontal SW
          Point se = getLayout().getSouthEast(hexIndex);
          l.addLine(p.x, p.y + dY, se.x, p.y + dY + (size / 5));
        }
        if ((side & 0x10) > 0) { // horizontal W
          l.addLine(p.x, p.y + (size / 5), p.x, p.y + dY);
        }
        if ((side & 0x20) > 0) { // horizontal NW
          Point ne = getLayout().getNorthEast(hexIndex);          
          l.addLine(p.x, p.y + (size / 5), ne.x, p.y);
        }
        if ((side & 0x40) > 0) { // square left
          l.addLine(p.x, p.y, p.x, p.y + dY);
        }
        if ((side & 0x80) > 0) { // square top
          l.addLine(p.x, p.y, p.x + dX, p.y);
        }
      }

      // if this is the last one, draw all the lines.
      if (this == hexSides.get(hexSides.size() - 1)) {
        drawLines(g, BasicStroke.CAP_ROUND);
      }
      
      return result;
    }
    
    @Override
    ArrayList<Line> getLineList(Hex h) {
      return h.hexSides;
    }
  }

  /**
   * Hexes aligned along rows.
   */
  protected class HorizontalHexLayout extends HorizontalLayout {

    HorizontalHexLayout(int size, int columns, int rows) {
      super(size, columns, rows);
    }

    @Override
    Dimension getBoardSize() {
      Dimension d = new Dimension();
      d.width = getDeltaX() * nColumns + getHexSize() / 2;
      d.height = getDeltaY() * nRows + getHexSize() / 5 + 1;
      return d;
    }

    @Override
    int getDeltaX() {
      return getHexSize() - (isPreV208Layout()?2:0);
    }

    @Override
    int getDeltaY() {
      return getHexSize() * 4 / 5 - 1;
    }

    @Override
    Point getOrigin() {
      return new Point(getHexSize() / 2, getHexSize() / 2 - (isPreV208Layout() ? 1 : 0));
    }
    
    @Override
    HexGrid getGeometricGrid() {
      HexGrid mg = new HexGrid();
    
      mg.setSideways(true);
    
      // VASSAL defines these sideways. Height always refers to the major
      // dimension, and Dy always refers to height whether they're sideways or not.
      mg.setOrigin(getOrigin());
      mg.setDy(getDeltaX());
      mg.setDx(getDeltaY());
    
      return mg;
    }            
  }
  
  /**
   * A layout consisting of squares in which every second row is shifted to the
   * right by one half-width. Used to approximate hexagons as each square has
   * six neighbours.
   * 
   * @author Michael Kiefte
   *
   */
  protected class GridOffsetRowLayout extends HorizontalLayout {

    GridOffsetRowLayout(int size, int columns, int rows) {
      super(size, columns, rows);
    }

    @Override
    Dimension getBoardSize() {
      Dimension d = new Dimension();
      d.height = getDeltaY() * nRows + 1;
      d.width = getDeltaX() * nColumns + getHexSize()/2 + 1;
      return d;
    }

    @Override
    int getDeltaX() {
      return getHexSize();
    }

    @Override
    int getDeltaY() {
      return getHexSize();
    }

    @Override
    Point getOrigin() {
      return new Point(getHexSize() * 7 / 12, getHexSize() / 2);
    }
    
    @Override
    AbstractConfigurable getGeometricGrid() {
      HexGrid mg = new HexGrid();

      mg.setSideways(true);
      
      mg.setOrigin(getOrigin());
      mg.setDx(getDeltaY());
      mg.setDy(getDeltaX());

      return mg;
    }      
  }
  
  /**
   * A layout in which every second row is offset by one-half hex or square.
   */
  protected abstract class HorizontalLayout extends Layout {

    HorizontalLayout(int size, int columns, int rows) {
      super(size, columns, rows);
    }

    @Override
    int getNFaces() {
      return 6;
    }

    @Override
    void setGridNumberingOffsets(RegularGridNumbering numbering, MapSheet sheet) {
      Point position = coordinatesToPosition(sheet.getField().x, sheet.getField().y, true);
      position.translate(getDeltaX()/2, getDeltaY()/2);
      int rowOffset = numbering.getColumn(position);
      int colOffset = numbering.getRow(position);
      
      rowOffset = -rowOffset + sheet.getTopLeftRow();
      colOffset = -colOffset + sheet.getTopLeftCol();
      
      numbering.setAttribute(RegularGridNumbering.H_OFF, rowOffset);
      numbering.setAttribute(RegularGridNumbering.V_OFF, colOffset);
    }

    @Override
    void initGridNumbering(RegularGridNumbering numbering, MapSheet sheet) {
      super.initGridNumbering(numbering, sheet);
      boolean stagger = false;
      if (sheet.firstHexRight() && (sheet.getField().y&1) == 1)
        stagger = true;
      else if (sheet.firstHexLeft() && sheet.getField().y%2 == 0)
        stagger = true;
      numbering.setAttribute(HexGridNumbering.STAGGER, stagger);
      numbering.setAttribute(RegularGridNumbering.FIRST, sheet.rowsAndCols() ? "H" : "V");
      numbering.setAttribute(RegularGridNumbering.H_TYPE, sheet.numericRows() ? "N" : "A");
      numbering.setAttribute(RegularGridNumbering.V_TYPE, sheet.numericCols() ? "N" : "A");
      numbering.setAttribute(RegularGridNumbering.H_LEADING, sheet.getNRowChars()-1);
      numbering.setAttribute(RegularGridNumbering.V_LEADING, sheet.getNColChars()-1);
    }

    @Override
    HexGridNumbering getGridNumbering() {
      return new HexGridNumbering();
    }

    @Override
    Point coordinatesToPosition(int x, int y, boolean nullIfOffBoard) {
      if (!nullIfOffBoard || isOnMapBoard(x, y)) {
        int xx = getDeltaX() * x + (y % 2) * getDeltaX() / 2;
        int yy = getDeltaY() * y;
        return new Point(xx, yy);
      } else
        return null;
    }

    @Override
    Point getNorthEast(int index) {
      int row = getRow(index);
      int col = getCol(index) + Math.abs(row) % 2;
      --row;
      return coordinatesToPosition(col, row, false);
    }

    @Override
    Point getNorthWest(int index) {
      int row = getRow(index) - 1;
      int col = getCol(index) - Math.abs(row) % 2;
      return coordinatesToPosition(col, row, false);
    }

    @Override
    Rectangle getRectangle(MapSheet map) {
      Rectangle r = map.getField();

      Point upperLeft = coordinatesToPosition(r.x, r.y, false);
      Point lowerRight = coordinatesToPosition(r.x + r.width - 1, r.y
          + r.height - 1, false);

      // adjust for staggering of hexes
      if (map.firstHexLeft()) // next one down is to the left
        upperLeft.x -= getHexSize() / 2;

      // adjust x of bottom right-hand corner
      if (r.y % 2 == (r.y + r.height - 1) % 2) { // both even or both odd
        if (map.firstHexRight())
          lowerRight.x += getHexSize() / 2;
        // check to see if lower right-hand corner is on the wrong
        // square
      } 
      else if ( (r.y&1) == 1 ) { 
        // top is odd and bottom is even
        if (map.firstHexLeft())
          lowerRight.x += getHexSize() / 2;
        else 
          lowerRight.x += getHexSize();
      } 
      else if (map.firstHexLeft() && r.y % 2 == 0) 
        // top is even and bottom is odd
        lowerRight.x -= getHexSize() / 2;

      // get lower right corner of lower right hex
      lowerRight.x += getHexSize() - 1;
      lowerRight.y += getHexSize() - 1;

      // adjust so that we don't overlap the centres of hexes that don't
      // belong to this sheet
      upperLeft.x += getHexSize() / 5;
      lowerRight.x -= getHexSize() / 5;

      constrainRectangle(upperLeft, lowerRight);
      
      return new Rectangle(upperLeft.x, upperLeft.y, lowerRight.x
          - upperLeft.x + 1, lowerRight.y - upperLeft.y + 1);
    }

    @Override
    Point getSouthEast(int index) {
      int row = getRow(index);
      int col = getCol(index) + Math.abs(row) % 2;
      ++row;
      return coordinatesToPosition(col, row, false);
    }

    @Override
    Point getSouthWest(int index) {
      int row = getRow(index) + 1;
      int col = getCol(index) - Math.abs(row) % 2;
      return coordinatesToPosition(col, row, false);
    }
  }

  /**
   * A drawable line such as a river, border, or road.
   */
  protected abstract class Line extends MapDrawable {

    // index of line definition. don't know the actual line definitions until later
    private final int line;

    Line(int index, int line) {
      super(index);
      this.line = line;
      if (hexes == null)
        hexes = new Hex[getNColumns() * getNRows()];
      if (hexes[index] == null)
        hexes[index] = new Hex();
      getLineList(hexes[index]).add(this);
    }

    /**
     * @return The <code>LineDefinition</code> for this line.
     */
    LineDefinition getLine() {
      return getLineDefinition(line);
    }

    /**
     * @return The list of lines by hex.
     */
    abstract ArrayList<Line> getLineList(Hex h);

    // I no longer remember how this works, but I do remember it took a long time to figure out.
    int compare(LineDefinition o1, LineDefinition o2) {
      if (o1 == null && o2 == null)
        return 0;
      else if (o1 == null)
        return 1;
      else if (o2 == null)
        return -1;
      // go through all the hexes
      // and determine file order for lines
      for (Hex h : hexes) {
        if (h == null)
          continue;
        boolean index1 = false;
        boolean index2 = false;
        for (Line hl : getLineList(h)) {
          if (hl.getLine() == o1) {
            if (index2)
              return 1;
            index1 = true;
          } else if (hl.getLine() == o2) {
            if (index1)
              return -1;
            index2 = true;
          }
        }
      }
      return 0;
    }
    
    void drawLines(Graphics2D g, int cap) {
      final ArrayList<LineDefinition> lds =
        new ArrayList<LineDefinition>(Arrays.asList(lineDefinitions));
      
      // find the next line in priority
      while (lds.size() > 0) {
        LineDefinition lowest = null;
        for (LineDefinition ld : lds) {
          if (ld == null)
            continue;
          else if (lowest == null || compare(ld, lowest) < 0)
            lowest = ld;
        }
        if (lowest == null)
          break;
        else {
          lowest.draw(g, cap);
          lowest.clearPoints();
          lds.remove(lowest);
        }
      }      
    }
  }

  /**
   * Line styles for hex sides and hex lines.
   */
  protected static class LineDefinition {

    private final Color color;

    private int hexLineDrawPriority;

    private int hexSideDrawPriority;

    // using floats because we really want to aim for the centre pixel, not necessarily
    // the space between pixels--only important for aliasing effects.
    private ArrayList<ArrayList<Point2D.Float>> points = new ArrayList<ArrayList<Point2D.Float>>();

    // line width
    private final int size;

    private final LineStyle style;

    LineDefinition(Color color, int size, MapBoard.LineStyle style) {
      this.color = color;
      this.size = size;
      this.style = style;
    }

    private void setHexLineDrawPriority(int priority) {
      // only change the priority if it hasn't already been set.
      if (hexLineDrawPriority == 0)
        hexLineDrawPriority = priority;
    }

    private void setHexSideDrawPriority(int priority) {      
      if (hexSideDrawPriority == 0)
        hexSideDrawPriority = priority;
    }

    Color getColor() {
      return color;
    }

    BasicStroke getStroke(int cap) {
      if (size <= 0 || style == null)
        return null;
      return style.getStroke(size, cap);
    }

    void addLine(float x1, float y1, float x2, float y2) {
      addLine(new Point2D.Float(x1, y1), new Point2D.Float(x2, y2));
    }

    void addLine(int x1, int y1, float x2, float y2) {
      addLine(new Point2D.Float((float) x1, (float) y1),
          new Point2D.Float(x2, y2));
    }

    void addLine(int x1, int y1, int x2, int y2) {
      addLine(new Point2D.Float((float) x1, (float) y1),
          new Point2D.Float((float) x2, (float) y2));
    }

    /**
     * Add a line to the line list for later processing. Attach it to already existing line if possible.
     * Otherwise start a new one.
     */
    void addLine(Point2D.Float a, Point2D.Float b) {
      // find out if this line is attached to any other line in the list.
      // if not create a line.
      for (int i = 0; i < points.size(); ++i) {
        ArrayList<Point2D.Float> lineA = points.get(i);
        if (a.equals(lineA.get(0))) { // a at the start of lineA
          // repeated segment?
          if (b.equals(lineA.get(1)))
            return;
          // find out if this segment joins two lines already in
          // existance
          for (int j = 0; j < points.size(); ++j) {
            if (i == j)
              continue;
            ArrayList<Point2D.Float> lineB = points.get(j);
            if (b.equals(lineB.get(0))) { // point A at start of lineA and point B at start of lineB
              if (lineA.size() < lineB.size()) { // insert A before B
                for (int k = 0; k < lineA.size(); ++k)
                  lineB.add(0, lineA.get(k));
                points.remove(i);
              } else { // insert B before A
                for (int k = 0; k < lineB.size(); ++k)
                  lineA.add(0, lineB.get(k));
                points.remove(j);
              }
              return;
            } else if (b.equals(lineB.get(lineB.size() - 1))) { 
              // point A at start of lineA and point B at end of lineB
              lineB.addAll(lineA);
              points.remove(i);
              return;
            }
          }
          // point A at start of lineA and point B is open
          lineA.add(0, b);
          return;
        } else if (a.equals(lineA.get(lineA.size() - 1))) { 
          // Point A is at end of line A
          if (b.equals(lineA.get(lineA.size() - 2))) // repeated segment?
            return;
          for (int j = 0; j < points.size(); ++j) {
            if (i == j) // skip closed loops
              continue;
            ArrayList<Point2D.Float> lineB = points.get(j);
            if (b.equals(lineB.get(0))) { 
              // point A at end of line A and point B at start of lineB
              lineA.addAll(lineB);
              points.remove(j);
              return;
            } else if (b.equals(lineB.get(lineB.size() - 1))) { 
              // point A at end of lineA and point B at end of lineB
              if (lineA.size() < lineB.size()) { // add line A to B
                for (int k = lineA.size() - 1; k >= 0; --k)
                  lineB.add(lineA.get(k));
                points.remove(i);
              } else { // add line B to A
                for (int k = lineB.size() - 1; k >= 0; --k)
                  lineA.add(lineB.get(k));
                points.remove(j);
              }
              return;
            }
          }
          // point A at the end of lineA and point B is open
          lineA.add(b);
          return;
        }
        // find out if the segment already exists
        for (int j = 1; j < lineA.size() - 1; ++j)
          if (a.equals(lineA.get(j))
              && (b.equals(lineA.get(j - 1)) || b.equals(lineA
                  .get(j + 1))))
            return;
      }

      // point A is open (not attached)
      for (ArrayList<Point2D.Float> line : points) {
        if (b.equals(line.get(0))) { // B at the start of the line
          // repeated segment?
          if (a.equals(line.get(1)))
            return;
          line.add(0, a);
          return;
        } else if (b.equals(line.get(line.size() - 1))) { 
          // B at the end of the line
          if (a.equals(line.get(line.size() - 2)))
            return;
          line.add(a);
          return;
        }
      }

      // both A and B are open
      ArrayList<Point2D.Float> newLine = new ArrayList<Point2D.Float>(2);
      newLine.add(a);
      newLine.add(b);
      points.add(newLine);
    }

    /**
     * start fresh.
     */
    void clearPoints() {
      points.clear();
    }

    void draw(Graphics2D g, int cap) {
      g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
      BasicStroke stroke = getStroke(cap);
      if (stroke == null)
        return;
      g.setStroke(stroke);
      g.setColor(getColor());
      GeneralPath gp = new GeneralPath(GeneralPath.WIND_EVEN_ODD);
      for (ArrayList<Point2D.Float> line : points) {
        gp.moveTo(line.get(0).x, line.get(0).y);
        for (Point2D.Float p : line) {
          if (!p.equals(line.get(0)))
            gp.lineTo(p.x, p.y);
          else if (p != line.get(0))
            gp.closePath();
        }
      }
      g.draw(gp);
      g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
    }

    int getHexLineDrawPriority() {
      return hexLineDrawPriority;
    }

    int getHexSideDrawPriority() {
      return hexSideDrawPriority;
    }
  }

  /**
   * line patter such as dashed or dotted or solid
   */
  protected enum LineStyle {
    DASH_DOT(new float[] { 12.0f, 8.0f, 4.0f, 8.0f }), 
    DASH_DOT_DOT(new float[] { 12.f, 4.0f, 4.0f, 4.0f, 4.0f, 4.0f }), 
    DASHED(new float[] { 12.0f, 8.0f }), 
    DOTTED(new float[] { 4.0f, 4.0f }), 
    SOLID(null);

    private float[] dash;

    LineStyle(float[] dash) {
      this.dash = dash;
    }

    BasicStroke getStroke(int size, int cap) {
      if (dash == null)
        // nice effect if it's a solid line
        return new BasicStroke(size, cap, BasicStroke.JOIN_ROUND);
      else
        return new BasicStroke(size, BasicStroke.CAP_BUTT,
            BasicStroke.JOIN_ROUND, 0.0f, dash, 0.0f);
    }
  }

  /**
   * Anything that can be drawn on the map and is associated with a particular hex.
   */ 
  protected abstract class MapDrawable {

    // hex index in row-major order: determines location on map
    protected final int hexIndex;

    MapDrawable(int index) {
      this.hexIndex = index;
    }

    /**
     * Draw the element to the graphics context. Return <code>true</code> if an element was actually drawn.
     */
    abstract boolean draw(Graphics2D g);

    int getHexIndex() {
      return hexIndex;
    }

    /**
     * @return Upper left-hand corner of hex or square.
     */ 
    Point getPosition() {
      return indexToPosition(hexIndex);
    }

    /**
     * @return Enclosing rectangle for hex or square.
     */ 
    Rectangle getRectangle() {
      Rectangle r = new Rectangle(getPosition());
      int width = getLayout().getHexSize();
      r.width = width;
      r.height = width;
      return r;
    }
  }

  /**
   * Defines the numbering system for an ADC2 mapboard.
   */
  protected class MapSheet {

    private int topLeftCol;
    
    private int topLeftRow;
    
    private final Rectangle field;

    private final String name;

    private final int nColChars;

    private final int nRowChars;

    private final int style;

    private Zone zone;

    MapSheet(String name, Rectangle playingFieldPosition, int style, int nColChars, int nRowChars) {
      this.name = name;
      this.field = playingFieldPosition;
      this.style = style;
      this.nColChars = nColChars;
      this.nRowChars = nRowChars;
    }

    /**
     * @return A rectangle giving the bounds of the hex coordinates.
     */
    Rectangle getField() {
      return field;
    }

    /**
     * @return The sheet name.
     */
    String getName() {
      return name;
    }

    /**
     * @return Number of characters in the column label.
     */
    int getNColChars() {
      return nColChars;
    }

    /**
     * @return Number of characters in the row label.
     */
    int getNRowChars() {
      return nRowChars;
    }

    /**
     * Used by the grid numbering system in VASSAL.
     */
    String getRectangleAsString() {
      Rectangle r = getLayout().getRectangle(this);
      if (r == null)
        return null;
      return r.x + "," + r.y + ";" + (r.x + r.width - 1) + "," + r.y
          + ";" + (r.x + r.width - 1) + "," + (r.y + r.height - 1)
          + ";" + r.x + "," + (r.y + r.height - 1);
    }

    /**
     * @return <code>true</code> if column labels are alphabetic.
     */
    boolean alphaCols() {
      return !numericCols();
    }

    /**
     * @return <code>true</code> if row labels are alphabetic.
     */
    boolean alphaRows() {
      return !numericRows();
    }

    /**
     * @return <code>true</code> if columns are first in coordinate label.
     */
    boolean colsAndRows() {
      return (style & 0x2) > 0;
    }

    /**
     * @return <code>true</code> if column labels increase going left.
     */
    boolean colsIncreaseLeft() {
      return !colsIncreaseRight();
    }

    /**
     * @return <code>true</code> if column labels increase going right.
     */
    boolean colsIncreaseRight() {
      return (style & 0x10) > 0;
    }

    /**
     * @return <code>true</code> if the row label of odd-numbered columns is shifted down.
     */
    boolean firstHexDown() {
      return (style & 0x40) > 0 && getLayout() instanceof VerticalLayout;
    }

    /**
     * @return <code>true</code> if the row label of odd-numbered rows is shifted left.
     */
    boolean firstHexLeft() {
      return (style & 0x40) > 0 && getLayout() instanceof HorizontalLayout;
    }

    /**
     * @return <code>true</code> if the row label of odd-numbered rows is shifted right.
     */
    boolean firstHexRight() {
      return (style & 0x40) == 0 && getLayout() instanceof HorizontalLayout;
    }

    /**
     * @return <code>true</code> if the row label of odd-numbered columns is shifted down.
     */
    boolean firstHexUp() {
      return (style & 0x40) == 0 && getLayout() instanceof VerticalLayout;
    }

    RegularGridNumbering getGridNumbering() {
      // numbering system
      RegularGridNumbering gn = getLayout().getGridNumbering();
      getLayout().initGridNumbering(gn, this);
      return gn;
    }

    /**
     * Creates a VASSAL <code>Zone</code> if not already created and initializes settings.
     */
    Zone getZone() {      
      if (zone == null) {
        zone = new Zone();
        zone.setConfigureName(getName());

        String rect = getRectangleAsString();
        if (rect == null)
          return null;
        zone.setAttribute(Zone.PATH, rect);
        zone.setAttribute(Zone.LOCATION_FORMAT, "$name$ $gridLocation$");
        AbstractConfigurable mg = getLayout().getGeometricGrid();

        // add numbering system to grid
        RegularGridNumbering gn = getGridNumbering();
        insertComponent(gn, mg);

        // add grid to zone
        insertComponent(mg, zone);

        getLayout().setGridNumberingOffsets(gn, this);
      }

      return zone;
    }

    /**
     * @return <code>true</code> if column labels are numeric.
     */
    boolean numericCols() {
      return (style & 0x4) > 0;
    }

    /**
     * @return <code>true</code> if row labels are numeric.
     */
    boolean numericRows() {
      return (style & 0x8) > 0;
    }

    /**
     * Rows before columns?
     */
    boolean rowsAndCols() {
      return !colsAndRows();
    }

    /**
     * @return <code>true</code> if row labels increase downward.
     */
    boolean rowsIncreaseDown() {
      return (style & 0x20) > 0;
    }

    /**
     * @return <code>true</code> if row labels increase upward.
     */
    boolean rowsIncreaseUp() {
      return !rowsIncreaseDown();
    }

    /**
     * @return Index of the top left column on the map sheet.
     */
    int getTopLeftCol() {
      return topLeftCol;
    }

    /**
     * Sets the top left column index of the map sheet.
     */
    void setTopLeftCol(int topLeftCol) {
      this.topLeftCol = topLeftCol;
    }

    /**
     * @return Index of the top left row of the map sheet.
     */
    int getTopLeftRow() {
      return topLeftRow;
    }

    /**
     * Sets the top left row index of the map sheet.
     */
    void setTopLeftRow(int topLeftRow) {
      this.topLeftRow = topLeftRow;
    }
  }

  /**
   * Place name element which includes not only the name itself, but the font and style that it
   * should be drawn with.
   */
  protected class PlaceName extends MapDrawable {

    // text colour
    private final Color color;

    // bit flags
    private final int font;

    // position relative to the hex. not really orientation. e.g., can't
    // have vertical text.
    private final PlaceNameOrientation orientation;

    // font size
    private final int size;

    // the actual name
    private final String text;

    PlaceName(int index, String text, Color color, PlaceNameOrientation orientation, int size, int font) {
      super(index);
      this.text = text;
      assert (color != null);
      this.color = color;
      assert (orientation != null);
      this.orientation = orientation;
//      assert (size > 0);
      this.size = size;
      font &= 0x7f;
      int fontIndex = font & 0xf;
      if (fontIndex < 1 || fontIndex > 9) {
        fontIndex = 9;
        font = font & 0xf0 | fontIndex;
      }
      this.font = font;
    }

    Font getFont() {
      int size = getSize();
      return size == 0 ? null : getDefaultFont(getSize(), font);
    }

    /**
     * Get the position based on the hex index, font and orientation.
     */
    Point getPosition(Graphics2D g) {
      Point p = getPosition();
      if (getSize() == 0)
        return p;
      assert (g.getFont() == getFont());
      FontMetrics fm = g.getFontMetrics();
      int size = getLayout().getHexSize();

      switch (orientation) {
      case LOWER_CENTER:
      case UPPER_CENTER:
      case LOWER_RIGHT:
      case UPPER_RIGHT:
      case UPPER_LEFT:
      case LOWER_LEFT:
      case HEX_CENTER:
        p.x += size / 2; // middle of the hex.
        break;
      case CENTER_RIGHT:
        p.x += size; // right of hex
        break;
      case CENTER_LEFT:
        break;
      }
      switch (orientation) {
      case LOWER_CENTER:
      case UPPER_CENTER:
      case HEX_CENTER:
        // text centered
        p.x -= fm.charsWidth(text.toCharArray(), 0, text.length()) / 2;
        break;
      case UPPER_LEFT:
      case LOWER_LEFT:
      case CENTER_LEFT:
        // right justified
        p.x -= fm.charsWidth(text.toCharArray(), 0, text.length());
        break;
      case LOWER_RIGHT:
      case UPPER_RIGHT:
      case CENTER_RIGHT:
        break;
      }
      switch (orientation) {
      case LOWER_CENTER:
      case LOWER_RIGHT:
      case LOWER_LEFT:
        p.y += size + fm.getAscent();
        break;
      case UPPER_CENTER:
      case UPPER_RIGHT:
      case UPPER_LEFT:
        p.y -= fm.getDescent();
        break;
      case CENTER_LEFT:
      case CENTER_RIGHT:
      case HEX_CENTER:
        p.y += size / 2 + fm.getHeight() / 2 - fm.getDescent();
        break;
      }
      return p;
    }

    // scale the size more appropriately--for some reason ADC2 font sizes
    // don't correspond to anything else.
    int getSize() {
      return size <= 5 ? 0 : (size + 1) * 4 / 3 - 1;
    }

    @Override
    boolean draw(Graphics2D g) {
      if (getSize() != 0) {
        g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
                           RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
        g.setFont(getFont());
        g.setColor(color);
        Point p = getPosition(g);
        g.drawString(text, p.x, p.y);
        g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
                           RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
        return true;
      }
      else {
        return false;
      }
    }

    String getText() {
      return text;
    }
  }

  protected enum PlaceNameOrientation {
    CENTER_LEFT, CENTER_RIGHT, HEX_CENTER, LOWER_CENTER, LOWER_LEFT, LOWER_RIGHT, UPPER_CENTER, UPPER_LEFT, UPPER_RIGHT;
  }

  /**
   * A layout consisting of squares in which every second column is shifted
   * downward by one half width.  This is done to approximate hexagons as each
   * square as six neighbours.
   * 
   * @author Michael Kiefte
   *
   */
  protected class GridOffsetColumnLayout extends VerticalLayout {

    GridOffsetColumnLayout(int size, int columns, int rows) {
      super(size, columns, rows);
    }

    @Override
    Dimension getBoardSize() {
      Dimension d = new Dimension();
      d.width = getDeltaX() * nColumns + 1;
      d.height = getDeltaY() * nRows + getHexSize() / 2 + 1;
      return d;
    }

    @Override
    int getDeltaX() {
      return getHexSize();
    }

    @Override
    int getDeltaY() {
      return getHexSize();
    }

    @Override
    Point getOrigin() {
      return new Point(getHexSize() * 7 / 12, getHexSize() / 2);
    }
    
    @Override
    AbstractConfigurable getGeometricGrid() {
      HexGrid mg = new HexGrid();

      mg.setOrigin(getOrigin());
      mg.setDx(getDeltaX());
      mg.setDy(getDeltaY());

      return mg;
    }  
  }

  /**
   * Hexes in columns.
   */
  protected class VerticalHexLayout extends VerticalLayout {

    VerticalHexLayout(int size, int columns, int rows) {
      super(size, columns, rows);
    }

    @Override
    Dimension getBoardSize() {
      Dimension d = new Dimension();
      d.width = getDeltaX() * nColumns + getHexSize() / 5 + 1;
      d.height = getDeltaY() * nRows + getHexSize() / 2 + 1;
      return d;
    }

    @Override
    int getDeltaX() {
      return getHexSize() * 4 / 5 - (isPreV208Layout() ? 1 : 0);
    }

    @Override
    int getDeltaY() {
      return getHexSize() - (isPreV208Layout() ? 2 : 1);
    }

    @Override
    Point getOrigin() {
      return new Point(getHexSize() / 2, getHexSize() / 2 - (isPreV208Layout() ? 1 : 0));
    }
    
    @Override
    HexGrid getGeometricGrid() {
      HexGrid mg = new HexGrid();

      mg.setOrigin(getOrigin());
      mg.setDx(getDeltaX());
      mg.setDy(getDeltaY());
      
      return mg;
    }
  }

  /**
   * A layout in which every second column is offset--either hexes or squares.
   */
  protected abstract class VerticalLayout extends Layout {

    @Override
    int getNFaces() {
      return 6;
    }

    VerticalLayout(int size, int columns, int rows) {
      super(size, columns, rows);
    }

    @Override
    HexGridNumbering getGridNumbering() {
      return new HexGridNumbering();
    }
    
    @Override
    void initGridNumbering(RegularGridNumbering numbering, MapSheet sheet) {
      boolean stagger = false;
      if (sheet.firstHexDown() && (sheet.getField().x&1) == 1)
        stagger = true;
      else if (sheet.firstHexUp() && sheet.getField().x%2 == 0)
        stagger = true;
      numbering.setAttribute(HexGridNumbering.STAGGER, stagger);
      super.initGridNumbering(numbering, sheet);
    }

    @Override
    Point coordinatesToPosition(int x, int y, boolean nullIfOffBoard) {
      if (!nullIfOffBoard || isOnMapBoard(x, y)) {
        int xx = getDeltaX() * x;
        int yy = getDeltaY() * y + x % 2 * getDeltaY() / 2;
        return new Point(xx, yy);
      } else
        return null;
    }

    @Override
    Point getNorthEast(int index) {
      int col = getCol(index) + 1;
      int row = getRow(index) - Math.abs(col) % 2;
      return coordinatesToPosition(col, row, false);
    }

    @Override
    Point getNorthWest(int index) {
      int col = getCol(index) - 1;
      int row = getRow(index) - Math.abs(col) % 2;
      return coordinatesToPosition(col, row, false);
    }

    @Override
    Rectangle getRectangle(MapSheet map) {
      Rectangle r = map.getField();

      if (r.width <= 0 || r.height <= 0)
        return null;
      
      Point upperLeft = coordinatesToPosition(r.x, r.y, false);
      Point lowerRight = coordinatesToPosition(r.x + r.width - 1, r.y
          + r.height - 1, false);

      // adjust for staggering of hexes
      if (map.firstHexUp()) // next one over is above
        upperLeft.y -= getHexSize() / 2;

      // adjust y of bottom right-hand corner
      if (r.x % 2 == (r.x + r.width - 1) % 2) { // both even or both odd
        if (map.firstHexDown())
          lowerRight.y += getHexSize() / 2;
        // check to see if lower right-hand corner is on the wrong
        // square
      } else if ( (r.x&1) == 1) { 
        // left is odd and right is even
        if (map.firstHexDown())
          lowerRight.y += getHexSize();
        else
          lowerRight.y += getHexSize()/2;
      } else if (map.firstHexUp() && r.x % 2 == 0) { 
        // left is even and right is odd
        lowerRight.y -= getHexSize() / 2;
      }

      // get lower right corner of lower right hex
      lowerRight.x += getHexSize() - 1;
      lowerRight.y += getHexSize() - 1;

      // adjust so that we don't overlap the centres of hexes that don't
      // belong to this sheet
      upperLeft.y += getHexSize() / 5;
      lowerRight.y -= getHexSize() / 5;

      constrainRectangle(upperLeft, lowerRight);
      
      return new Rectangle(upperLeft.x, upperLeft.y, lowerRight.x
          - upperLeft.x + 1, lowerRight.y - upperLeft.y + 1);
    }

    @Override
    Point getSouthEast(int index) {
      int col = getCol(index);
      int row = getRow(index) + Math.abs(col) % 2;
      ++col;
      return coordinatesToPosition(col, row, false);
    }

    @Override
    Point getSouthWest(int index) {
      int col = getCol(index);
      int row = getRow(index) + Math.abs(col) % 2;
      --col;
      return coordinatesToPosition(col, row, false);
    }
  }

  /**
   * How the hexes or squares are organized on the map board.
   */
  protected abstract class Layout {

    protected final int nColumns;

    protected final int nRows;

    // Size of the hexes or squares.
    private final int size;

    Layout(int size, int columns, int rows) {
      this.size = size;
      this.nColumns = columns;
      this.nRows = rows;
    }

    protected int getRow(int index) {
      return index / nColumns;
    }
    
    protected int getCol(int index) {
      return index % nColumns;
    }
    
    /**
     * Move the upper left and lower-right points to just within the map board.
     */
    void constrainRectangle(Point upperLeft, Point lowerRight) {
      if (upperLeft.x < 0)
        upperLeft.x = 0;
      if (upperLeft.y < 0)
        upperLeft.y = 0;
      Dimension d = getBoardSize();
      
      if (lowerRight.x >= d.width)
        lowerRight.x = d.width-1;
      if (lowerRight.y >= d.height)
        lowerRight.y = d.height-1;
    }

    /**
     * @return number of flat sides. <i>e.g.</i>, four for squares, six for hexes.
     */
    abstract int getNFaces();
    
    /**
     * Set attributes of the <code>GridNumbering</code> object based on map board parameters.
     */
    void initGridNumbering(RegularGridNumbering numbering, MapSheet sheet) {
      numbering.setAttribute(RegularGridNumbering.FIRST, sheet.colsAndRows() ? "H" : "V");
      numbering.setAttribute(RegularGridNumbering.H_TYPE, sheet.numericCols() ? "N" : "A");
      numbering.setAttribute(RegularGridNumbering.H_LEADING, sheet.getNColChars()-1);      
      numbering.setAttribute(RegularGridNumbering.H_DESCEND, sheet.colsIncreaseLeft());
      numbering.setAttribute(RegularGridNumbering.H_DESCEND, sheet.colsIncreaseLeft());
      numbering.setAttribute(RegularGridNumbering.V_TYPE, sheet.numericRows() ? "N" : "A");
      numbering.setAttribute(RegularGridNumbering.V_LEADING, sheet.getNRowChars()-1);
      numbering.setAttribute(RegularGridNumbering.V_DESCEND, sheet.rowsIncreaseUp());
    }

    /**
     * Set the offset in the grid numbering system according to the specified map sheet.
     */
    void setGridNumberingOffsets(RegularGridNumbering numbering, MapSheet sheet) {  
      Point position = coordinatesToPosition(sheet.getField().x, sheet.getField().y, true);
      // shift to the middle of the hex
      position.translate(getDeltaX()/2, getDeltaY()/2);
      // use the numbering system to find out where we are
      int rowOffset = numbering.getRow(position);
      int colOffset = numbering.getColumn(position);
      
      rowOffset = -rowOffset + sheet.getTopLeftRow();
      colOffset = -colOffset + sheet.getTopLeftCol();
      
      numbering.setAttribute(RegularGridNumbering.H_OFF, colOffset);
      numbering.setAttribute(RegularGridNumbering.V_OFF, rowOffset);
    }
    
    /**
     * @return an uninitialized grid numbering system appropriate for this layout
     */
    abstract RegularGridNumbering getGridNumbering();

    /**
     * Returns a point corresponding the the upper-left corner of the square
     * specified by the coordinates.
     * 
     * @param x column
     * @param y row
     * @param nullIfOffBoard return null if not on the board. Otherwise the point
     *        may not be valid.
     * @return the point corresponding to the upper-left-hand corner of the square.
     */
    abstract Point coordinatesToPosition(int x, int y, boolean nullIfOffBoard);

    /**
     * @return board image size dimensions in pixels.
     */
    abstract Dimension getBoardSize();

    /**
     * @return the distance in pixels to the next square on the right.
     */
    abstract int getDeltaX();

    /**
     * @return the distance in pixels ot the next square below
     */
    abstract int getDeltaY();

    /**
     * Returns the location of the hex or square to the East.
     * 
     * @param index raw index (columns increasing fastest).
     * @return the position in pixels of the next hex or square to the East.
     */
    Point getEast(int index) {
      int row = getRow(index);
      int col = getCol(index) + 1;
      return coordinatesToPosition(col, row, false);
    }

    /**
     * @return an initialized VASSAL hex grid appropriate for the current layout
     */
    abstract AbstractConfigurable getGeometricGrid();

    /**
     * Returns the location of the hex or square to the North.
     * 
     * @param index raw index (columns increasing fastest).
     * @return the position in pixels of the next hex or square to the North.
     */
    Point getNorth(int index) {
      int row = getRow(index) - 1;
      int col = getCol(index);
      return coordinatesToPosition(col, row, false);
    }

    /**
     * Returns the location of the hex or square to the NorthEast.
     * 
     * @param index raw index (columns increasing fastest).
     * @return the position in pixels of the next hex or square to the NorthEast.
     */
    Point getNorthEast(int index) {
      int row = getRow(index) - 1;
      int col = getCol(index) + 1;
      return coordinatesToPosition(col, row, false);
    }

    /**
     * Returns the location of the hex or square to the NorthWest.
     * 
     * @param index raw index (columns increasing fastest).
     * @return the position in pixels of the next hex or square to the NorthWest.
     */
    Point getNorthWest(int index) {
      int row = getRow(index) - 1;
      int col = getCol(index) - 1;
      return coordinatesToPosition(col, row, false);
    }

    /**
     * @return the centre in pixels of a square or hex relative to the top-left corner.
     */
    abstract Point getOrigin();

    /**
     * Returns a rectangle in pixels that encloses the given <code>MapSheet</code>. 
     * Returns null if <code>MapSheet</code> has a negative size.
     */
    abstract Rectangle getRectangle(MapSheet map);

    /**
     * @return the size of the hexes or squares in pixels.
     */
    int getHexSize() {
      return size;
    }

    /**
     * Returns the location of the hex or square to the South.
     * 
     * @param index raw index (columns increasing fastest).
     * @return the position in pixels of the next hex or square to the South.
     */
    Point getSouth(int index) {
      int row = getRow(index) + 1;
      int col = getCol(index);
      return coordinatesToPosition(col, row, false);
    }

    /**
     * Returns the location of the hex or square to the SouthEast.
     * 
     * @param index raw index (columns increasing fastest).
     * @return the position in pixels of the next hex or square to the SouthEast.
     */
    Point getSouthEast(int index) {
      int row = getRow(index) + 1;
      int col = getCol(index) + 1;
      return getLayout().coordinatesToPosition(col, row, false);
    }

    /**
     * Returns the location of the hex or square to the SouthWest.
     * 
     * @param index raw index (columns increasing fastest).
     * @return the position in pixels of the next hex or square to the SouthWest.
     */
    Point getSouthWest(int index) {
      int row = getRow(index) + 1;
      int col = getCol(index) - 1;
      return coordinatesToPosition(col, row, false);
    }

    /**
     * Returns the location of the hex or square to the West.
     * 
     * @param index raw index (columns increasing fastest).
     * @return the position in pixels of the next hex or square to the West.
     */
    Point getWest(int index) {
      int row = getRow(index);
      int col = getCol(index) - 1;
      return coordinatesToPosition(col, row, false);
    }
  }

  // Archive of fonts used for placenames. makes reuse possible and is
  // probably faster as most of the place names use only one of a very few fonts.
  private final static HashMap<Integer, Font> defaultFonts = new HashMap<Integer, Font>();

  // which level to import
  private static final int zoomLevel = 2;

  // fonts available to ADC
  private static final String[] defaultFontNames = { "Courier", "Fixedsys",
      "MS Sans Serif", "MS Serif", "Impact", "Brush Script MT",
      "System", "Times New Roman", "Arial" };

  private static final String PLACE_NAMES = "Place Names";
  
  /**
   * Get a font based on size and font index. If this font has not already been created, then it will be generated.
   * Can be reused later if the same font was already created.
   * 
   * @param size Font size.
   * @param font Font index. See MapBoard.java for format.
   */
  
  /* Binary format for fonts:
   * 
   *             00000000
   *                 ||||_ Font name index (between 1 and 9).
   *                |_____ Bold flag.
   *               |______ Italics flag.
   *              |_______ Underline flag.
   */
  protected static Font getDefaultFont(int size, int font) {
    Integer key = new Integer((size << 8) + font);
    Font f = defaultFonts.get(key);
    if (f == null) {
      int fontIndex = font & 0xf;
      assert (fontIndex >= 1 && fontIndex <= 9);
      boolean isBold = (font & 0x0010) > 0;
      boolean isItalic = (font & 0x0020) > 0;
      boolean isUnderline = (font & 0x0040) > 0;
      String fontName = defaultFontNames[fontIndex - 1];
      int fontStyle = Font.PLAIN;
      if (isItalic)
        fontStyle |= Font.ITALIC;
      if (isBold)
        fontStyle |= Font.BOLD;
      f = new Font(fontName, fontStyle, size);
      if (isUnderline) {
        // TODO: why doesn't underlining doesn't work? Why why why?
        Hashtable<TextAttribute, Object> hash = new Hashtable<TextAttribute, Object>();
        hash.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON);
        f = f.deriveFont(hash);
      }
      defaultFonts.put(key, f);
    }
    return f;
  }

  // tertiary symbols.
  private final ArrayList<HexData> attributes = new ArrayList<HexData>();

  // name of the map board is derived from the file name
  private String baseName;

  // hex data organized by index
  private Hex[] hexes;

  // hexline data
  private final ArrayList<HexLine> hexLines = new ArrayList<HexLine>();

  // hexside data
  private final ArrayList<HexSide> hexSides = new ArrayList<HexSide>();

  // layout of the hexes or squares
  private Layout layout;

  // line definitions needed for hex sides and lines
  private LineDefinition[] lineDefinitions;

  // organizes all the drawable elements in order of drawing priority
  private ArrayList<MapLayer> mapElements = new ArrayList<MapLayer>();

  // grid numbering systems
  private final ArrayList<MapSheet> mapSheets = new ArrayList<MapSheet>();

  // labels; not necessary actual places corresponding to a hex, although
  // that's how it's described by ADC2
  private final ArrayList<PlaceName> placeNames = new ArrayList<PlaceName>();

  // optional place symbol in addition to primary and secondary mapboard
  // symbol
  private final ArrayList<HexData> placeSymbols = new ArrayList<HexData>();

  // primary mapboard symbols. Every hex must have one even if it's null.
  private final ArrayList<HexData> primaryMapBoardSymbols = new ArrayList<HexData>();

  // and secondary mapboard symbols (typically a lot fewer)
  private final ArrayList<HexData> secondaryMapBoardSymbols = new ArrayList<HexData>();
  
  // overlay symbol. there's only one, but we make it an ArrayList<> for consistency
  // with other drawing objects
  private final ArrayList<MapBoardOverlay> overlaySymbol = new ArrayList<MapBoardOverlay>();

  // symbol set associated with this map -- needed for mapboard symbols
  private SymbolSet set;

  // How many hex columns in the map.
  private int columns;

  // How many hex rows in the map.
  private int rows;

  // background map color when hexes are not drawn.
  private Color tableColor;

  // version information needed for rendering hexes and determining hex dimensions
  private boolean isPreV208 = true;

  // map file path
  private String path;
  
  // The VASSAL BoardPicker object which is the tree parent of Board.
  private BoardPicker boardPicker;

  private byte[] drawingPriorities = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

  // initialize the drawing elements which must all be ArrayList<>'s.
  public MapBoard() {
    mapElements.add(new MapLayer(primaryMapBoardSymbols, "Primary MapBoard Symbols", false));
    mapElements.add(new MapLayer(secondaryMapBoardSymbols, "Secondary MapBoard Symbols", false));
    mapElements.add(new MapLayer(hexSides, "Hex Sides", true));
    mapElements.add(new MapLayer(hexLines, "Hex Lines", true));
    mapElements.add(new MapLayer(placeSymbols, "Place Symbols", false));
    mapElements.add(new MapLayer(attributes, "Attributes", false));
    mapElements.add(new MapLayer(overlaySymbol, "Overlay Symbol", true));
    mapElements.add(new MapLayer(placeNames, "Place Names", true));
  }

  /**
   * Many maps are actually just scanned images held in a separate file. The images are often broken up into
   * sections. An extra file describes how the images are pasted together. This function pieces the images
   * together using the <code>Graphics2D</code> object <code>g</code>.
   */
  protected void readScannedMapLayoutFile(File f, Graphics2D g) throws IOException {
    DataInputStream in = null;
    
    try {
      in = new DataInputStream(new BufferedInputStream(new FileInputStream(f)));
      // how many image sections
      int nSheets = ADC2Utils.readBase250Word(in);
      for (int i = 0; i < nSheets; ++i) {
        // image file name
        String name = stripExtension(readWindowsFileName(in));
        File file = action.getCaseInsensitiveFile(new File(name + "-L" + (zoomLevel+1) + ".bmp"), new File(path), true, null);
        if (file == null)
          throw new FileNotFoundException("Unable to find map image.");
        BufferedImage img = ImageIO.read(file);
        int x = 0;
        int y = 0;
        for (int j = 0; j < 3; ++j) {
          int tempx = ADC2Utils.readBase250Integer(in);
          int tempy = ADC2Utils.readBase250Integer(in);
          if (j == zoomLevel) {
            x = tempx;
            y = tempy;
          }
        }
        g.drawImage(img, null, x, y);
      }
      
      in.close();
    }
    finally {
      IOUtils.closeQuietly(in);
    }
  }

  /**
   * Read symbol that is drawn on all hexes.
   */
  protected void readMapBoardOverlaySymbolBlock(DataInputStream in) throws IOException {
    ADC2Utils.readBlockHeader(in, "MapBoard Overlay Symbol");
    
    SymbolSet.SymbolData overlaySymbol = getSet().getMapBoardSymbol(ADC2Utils.readBase250Word(in));
    // the reason we use an ArrayList<> here is so that it is treated like all other drawn elements
    if (overlaySymbol != null)
      this.overlaySymbol.add(new MapBoardOverlay(overlaySymbol));
  }

  /**
   * Block of flags to indicate which elements are actually displayed.
   */
  protected void readMapItemDrawFlagBlock(DataInputStream in) throws IOException {
    ADC2Utils.readBlockHeader(in, "Map Item Draw Flag");

    // obviously, element types can't be sorted before we do this.
    // TODO: check this! If they're turned off in the map, can they be turned on
    // again in the player?
    final ArrayList<MapLayer> elements = new ArrayList<MapLayer>(mapElements);
    if (in.readByte() == 0)
      mapElements.remove(elements.get(drawingPriorities[0]));
    if (in.readByte() == 0)
      mapElements.remove(elements.get(drawingPriorities[1]));
    if (in.readByte() == 0)
      mapElements.remove(elements.get(drawingPriorities[2]));
    if (in.readByte() == 0)
      mapElements.remove(elements.get(drawingPriorities[3]));
    if (in.readByte() == 0)
      mapElements.remove(elements.get(drawingPriorities[4]));
    if (in.readByte() == 0)
      mapElements.remove(elements.get(drawingPriorities[7]));
    if (in.readByte() == 0)
      mapElements.remove(elements.get(drawingPriorities[5]));
  }

  /**
   * Attributes are tertiary symbols, any number of which can be attached to a specific hex.  Otherwise, they
   * function the same as primary or secondary hex symbols.
   */
  protected void readAttributeBlock(DataInputStream in) throws IOException {
    ADC2Utils.readBlockHeader(in, "Attribute Symbol");

    int nAttributes = ADC2Utils.readBase250Word(in);
    for (int i = 0; i < nAttributes; ++i) {
      int index = ADC2Utils.readBase250Word(in);
      SymbolSet.SymbolData symbol = set.getMapBoardSymbol(ADC2Utils.readBase250Word(in));
      if (isOnMapBoard(index) && symbol != null)
        attributes.add(new HexData(index, symbol));
    }
  }

  /**
   * Read primary and secondary symbol information. Each hex may only have one of each. Additional symbols must
   * be tertiary attributes.
   */
  protected void readHexDataBlock(DataInputStream in) throws IOException {
    ADC2Utils.readBlockHeader(in, "Hex Data");

    int count = getNColumns() * getNRows();

    for (int i = 0; i < count; ++i) {
      // primary symbol
      int symbolIndex = ADC2Utils.readBase250Word(in);
      SymbolSet.SymbolData symbol = getSet().getMapBoardSymbol(symbolIndex);
      if (symbol != null)
        primaryMapBoardSymbols.add(new HexData(i, symbol));

      // secondary symbol
      symbolIndex = ADC2Utils.readBase250Word(in);
      symbol = getSet().getMapBoardSymbol(symbolIndex);
      if (symbol != null)
        secondaryMapBoardSymbols.add(new HexData(i, symbol));

      /* int elevation = */ ADC2Utils.readBase250Word(in);
      
      // flags for hexsides, lines, and placenames: completely ignored
      /* int additionalInformation = */ in.readUnsignedByte();
    }
  }

  /**
   * Hex lines are like spokes of the hex and are typically used for things like roads or other elements that
   * traverse from hex to hex.  The direction of each spoke is encoded as bit flags, and while ADC2 could encode
   * each hex with only one record, modules typically have a separate record for every spoke resulting in
   * data inflation.
   */
  protected void readHexLineBlock(DataInputStream in) throws IOException {
    ADC2Utils.readBlockHeader(in, "Hex Line");

    int nHexLines = ADC2Utils.readBase250Word(in);
    for (int i = 0; i < nHexLines; ++i) {
      int index = ADC2Utils.readBase250Word(in);
      int line = in.readUnsignedByte();
      int direction = in.readUnsignedShort();
      if (isOnMapBoard(index))
        hexLines.add(new HexLine(index, line, direction));
    }
  }

  /**
   * Information about hex sides which are used for things like rivers, etc. is read in.
   */
  protected void readHexSideBlock(DataInputStream in) throws IOException {
    ADC2Utils.readBlockHeader(in, "Hex Side");

    int nHexSides = ADC2Utils.readBase250Word(in);
    for (int i = 0; i < nHexSides; ++i) {
      int index = ADC2Utils.readBase250Word(in);
      int line = in.readUnsignedByte();
      int side = in.readUnsignedByte();
      if (isOnMapBoard(index))
        hexSides.add(new HexSide(index, line, side));
    }
  }

  /**
   * Information about the width, colour and style of hex sides and hex lines is read in.
   */
  protected void readLineDefinitionBlock(DataInputStream in) throws IOException {
    ADC2Utils.readBlockHeader(in, "Line Definition");

    int nLineDefinitions = in.readUnsignedByte();
    lineDefinitions = new LineDefinition[nLineDefinitions];
    for (int i = 0; i < nLineDefinitions; ++i) {
      int colorIndex = in.readUnsignedByte();
      Color color = ADC2Utils.getColorFromIndex(colorIndex);
      int size = 0;
      for (int j = 0; j < 3; ++j) {
        int s = in.readByte();
        if (j == zoomLevel)
          size = s;
      }
      // only used when editing ADC2 maps within ADC2.
      /* String name = */ readNullTerminatedString(in);
      int styleByte = in.readUnsignedByte();
      LineStyle style;
      switch (styleByte) {
      case 2:
        style = LineStyle.DOTTED;
        break;
      case 3:
        style = LineStyle.DASH_DOT;
        break;
      case 4:
        style = LineStyle.DASHED;
        break;
      case 5:
        style = LineStyle.DASH_DOT_DOT;
        break;
      default:
        style = LineStyle.SOLID;
      }
      
      if (size > 0)
        lineDefinitions[i] = new LineDefinition(color, size, style);
      else
        lineDefinitions[i] = null;
    }
  }

  /**
   * Read what order to draw the lines in.
   */
  protected void readLineDrawPriorityBlock(DataInputStream in) throws IOException {
    ADC2Utils.readBlockHeader(in, "Line Draw Priority");

    in.readByte(); // unused.

    // there can only be 10 line definitions. however drawning priorities for hex sides and hex lines
    // are completely independent
    for (int i = 1; i <= 10; ++i) {
      int index = in.readUnsignedByte();
      if (index < lineDefinitions.length && lineDefinitions[index] != null)
        lineDefinitions[index].setHexLineDrawPriority(i);
    }

    for (int i = 1; i <= 10; ++i) {
      int index = in.readUnsignedByte();
      if (index < lineDefinitions.length && lineDefinitions[index] != null)
        lineDefinitions[index].setHexSideDrawPriority(i);
    }
  }

  /**
   * Read information on hex numbering.
   */
  protected void readMapSheetBlock(DataInputStream in) throws IOException {
    ADC2Utils.readBlockHeader(in, "Map Sheet");

    int nMapSheets = ADC2Utils.readBase250Word(in);
    for (int i = 0; i < nMapSheets; ++i) {
      int x1 = ADC2Utils.readBase250Word(in);
      int y1 = ADC2Utils.readBase250Word(in);
      int x2 = ADC2Utils.readBase250Word(in);
      int y2 = ADC2Utils.readBase250Word(in);
      Rectangle r = new Rectangle(x1, y1, x2 - x1 + 1, y2 - y1 + 1);
      // must be exactly 9 bytes or 10 if there's a terminating null at the end
      String name = readNullTerminatedString(in, 10);
      if (name.length() < 9)
        in.readFully(new byte[9 - name.length()]);
      int style = in.readUnsignedByte();
      in.readFully(new byte[2]);
      int nColChars = in.readUnsignedByte();
      int nRowChars = in.readUnsignedByte();
      if (i < nMapSheets-1) // the last one is always ignored.
        mapSheets.add(new MapSheet(name, r, style, nColChars, nRowChars));
    }
  }

  /**
   * Read in information on hex sheets which define the hex numbering systems. This represents supplemental
   * information--some map sheet info occurs earlier in the file.  Only the coordinates of the top-left corner
   * are read in here.  
   */
  protected void readHexNumberingBlock(DataInputStream in) throws IOException {
    ADC2Utils.readBlockHeader(in, "Hex Numbering");
    
    for (int i = 0; i < mapSheets.size()+1; ++i) {
      // rare case when integers are not base-250. However, they are big-endian despite being a
      // Windows application.
      int col = 0;
      for (int j = 0; j < 4; ++j) { 
        col <<= 8;
        col += in.readUnsignedByte();
      }
      int row = 0;
      for (int j = 0; j < 4; ++j) {
        row <<= 8;
        row += in.readUnsignedByte();
      }
      if (i < mapSheets.size()) {
        MapSheet ms = mapSheets.get(i);
        ms.setTopLeftCol(col);
        ms.setTopLeftRow(row);
      }
    }
  }

  /**
   * Read and set the order of the drawn element types.
   */
  protected void readMapItemDrawingOrderBlock(DataInputStream in) throws IOException {
    ADC2Utils.readBlockHeader(in, "Map Item Drawing Order");
    
    byte[] priority = new byte[10];
    in.readFully(priority);
    ArrayList<MapLayer> items = new ArrayList<MapLayer>(mapElements.size());
    for (int i = 0; i < mapElements.size(); ++i) {

      // invalid index: abort reordering and switch back to default
      if (priority[i] >= mapElements.size() || priority[i] < 0)
        return;
        
      if (i > 0) {
        // abort reordering and switch back to default if any indeces are repeated
        for (int j = 0; j < i; ++j) {
          if (priority[j] == priority[i])
            return;
        }
      }

      items.add(mapElements.get(priority[i]));
    }
    
    // find out where it moved
    for (int i = 0; i < mapElements.size(); ++i) {
    	drawingPriorities[priority[i]] = (byte) i;
    }
    // swap default order with specified order
    mapElements = items;
  }
  
  /**
   * Crude version information.  Comes near the end of the file!  Actually it's just a flag to indicate whether
   * the version is < 2.08.  In version 2.08, the hexes are abutted slightly differently.
   */
  protected void readVersionBlock(DataInputStream in) throws IOException{
    ADC2Utils.readBlockHeader(in, "File Format Version");
    
    int version = in.readByte();
    isPreV208 = version != 0;
  }

  /**
   * The colour to fill before any elements are drawn. The fast-scroll flag is also read.
   */
  protected void readTableColorBlock(DataInputStream in) throws IOException {
    ADC2Utils.readBlockHeader(in, "Table Color");
    
    /* int fastScrollFlag = */ in.readByte();
    tableColor = ADC2Utils.getColorFromIndex(in.readUnsignedByte());
  }
  
  /**
   * Optional labels that can be added to hexes.  Can also include a symbol that can be added with the label.
   */
  protected void readPlaceNameBlock(DataInputStream in) throws IOException {
    ADC2Utils.readBlockHeader(in, "Place Name");

    int nNames = ADC2Utils.readBase250Word(in);
    for (int i = 0; i < nNames; ++i) {
      int index = ADC2Utils.readBase250Word(in);
      // extra hex symbol
      SymbolSet.SymbolData symbol = getSet().getMapBoardSymbol(ADC2Utils.readBase250Word(in));
      if (symbol != null && isOnMapBoard(index))
        placeSymbols.add(new HexData(index, symbol));
      String text = readNullTerminatedString(in, 25);
      Color color = ADC2Utils.getColorFromIndex(in.readUnsignedByte());

      int size = 0;
      for (int z = 0; z < 3; ++z) {
        int b = in.readUnsignedByte();
        if (z == zoomLevel)
          size = b;
      }

      PlaceNameOrientation orientation = null;
      for (int z = 0; z < 3; ++z) {
        int o = in.readByte();
        if (z == zoomLevel) {
          switch (o) {
          case 1:
            orientation = PlaceNameOrientation.LOWER_CENTER;
            break;
          case 2:
            orientation = PlaceNameOrientation.UPPER_CENTER;
            break;
          case 3:
            orientation = PlaceNameOrientation.LOWER_RIGHT;
            break;
          case 4:
            orientation = PlaceNameOrientation.UPPER_RIGHT;
            break;
          case 5:
            orientation = PlaceNameOrientation.UPPER_LEFT;
            break;
          case 6:
            orientation = PlaceNameOrientation.LOWER_LEFT;
            break;
          case 7:
            orientation = PlaceNameOrientation.CENTER_LEFT;
            break;
          case 8:
            orientation = PlaceNameOrientation.CENTER_RIGHT;
            break;
          case 9:
            orientation = PlaceNameOrientation.HEX_CENTER;
            break;
          }
        }
      }

      int font = in.readUnsignedByte();

      if (!isOnMapBoard(index) || text.length() == 0 || orientation == null)
        continue;

      placeNames.add(new PlaceName(index, text, color, orientation, size, font));
    }
  }
  
  /**
   * @param index Index of hex or square in row-major order starting with the upper-left-hand corner (0-based).
   * @return <code>true</code> if <code>index</code> is valid, <code>false</code> otherwise.
   */
  boolean isOnMapBoard(int index) {
    return isOnMapBoard(index % columns, index / columns);
  }

  /**
   * @param x Hex or square column.
   * @param y Hex or square row.
   * @return <code>true</code> if <code>x</code> and <code>y</code> are within the bounds of the board
   *         <code>false</code> otherwise.
   */
  boolean isOnMapBoard(int x, int y) {
    return x >= 0 && x < columns && y >= 0 && y < rows;
  }

  /**
   * @return The Layout object corresponding to this imported map.
   */
  protected Layout getLayout() {
    return layout;
  }

  /**
   * Returns the LineDefinition object corresponding to the given index.
   */
  protected LineDefinition getLineDefinition(int index) {
    if (index < 0 | index >= lineDefinitions.length)
      return null;
    else
      return lineDefinitions[index];
  }

  /**
   * @return Number of columns in the map.
   */
  int getNColumns() {
    return columns;
  }

  /**
   * @return Number of rows in the map.
   */
  int getNRows() {
    return rows;
  }

  /**
   * @return The <code>SymbolSet</code> needed by this map to render terrain and attribute elements.
   */
  SymbolSet getSet() {
    return set;
  }

  @Override
  protected void load(File f) throws IOException {
    super.load(f);
    DataInputStream in = null;
    
    try {
      in = new DataInputStream(new BufferedInputStream(new FileInputStream(f)));

      baseName = stripExtension(f.getName());
      path = f.getPath();
      int header = in.readByte();
      if (header != -3)
        throw new FileFormatException("Invalid Mapboard File Header");

      // don't know what these do.
      in.readFully(new byte[2]);

      // get the symbol set
      String s = readWindowsFileName(in);
      String symbolSetFileName = forceExtension(s, "set");
      set = new SymbolSet();
      File setFile = action.getCaseInsensitiveFile(new File(symbolSetFileName), f, true, 
          new ExtensionFileFilter(ADC2Utils.SET_DESCRIPTION, new String[] {ADC2Utils.SET_EXTENSION}));
      if (setFile == null)
        throw new FileNotFoundException("Unable to find symbol set file.");
      set.importFile(action, setFile);

      in.readByte(); // ignored

      columns = ADC2Utils.readBase250Word(in);
      rows = ADC2Utils.readBase250Word(in);
      // presumably, they're all the same size (and they're square)
      int hexSize = set.getMapBoardSymbolSize();

      // each block read separately
      readHexDataBlock(in);
      readPlaceNameBlock(in);
      readHexSideBlock(in);
      readLineDefinitionBlock(in);
      readAttributeBlock(in);
      readMapSheetBlock(in);
      readHexLineBlock(in);
      readLineDrawPriorityBlock(in);
      // end of data blocks

      int orientation = in.read();
      switch(orientation) {
      case 0:
      case 1: // vertical hex orientation or grid offset column
        if (set.getMapBoardSymbolShape() == SymbolSet.Shape.SQUARE)
          layout = new GridOffsetColumnLayout(hexSize, columns, rows);
        else
          layout = new VerticalHexLayout(hexSize, columns, rows);
        break;
      case 2: // horizontal hex orientation or grid offset row
        if (set.getMapBoardSymbolShape() == SymbolSet.Shape.SQUARE)
          layout = new GridOffsetRowLayout(hexSize, columns, rows);
        else
          layout = new HorizontalHexLayout(hexSize, columns, rows);
        break;
      default: // square grid -- no offset
        layout = new GridLayout(hexSize, columns, rows);
      }

      /* int saveMapPosition = */ in.readByte();

      /* int mapViewingPosition = */ in.readShort(); // probably base-250

      /* int mapViewingZoomLevel = */ in.readShort(); 

      in.readByte(); // totally unknown

      // strangely, more blocks
      readTableColorBlock(in);
      readHexNumberingBlock(in); 

      // TODO: default map item drawing order appears to be different for different maps. 
      try { // optional blocks
        readMapBoardOverlaySymbolBlock(in);
        readVersionBlock(in);
        readMapItemDrawingOrderBlock(in);
        readMapItemDrawFlagBlock(in);
      }
      catch(ADC2Utils.NoMoreBlocksException e) {}
      
      in.close();
    }
    finally {
      IOUtils.closeQuietly(in);
    }
  }

  /**
   * @return How many sides does each hex (6) or square (4) have?
   */
  int getNFaces() {
    return getLayout().getNFaces();
  }

  /**
   * @return The point corresponding to the hex centre relative to the upper-left-hand corner.
   */
  Point getCenterOffset() {
    return getLayout().getOrigin();
  }
  
  /**
   * Given a row and column for a hex, return the point corresponding to the upper left-hand pixel.
   */
  Point coordinatesToPosition(int x, int y) {
    return getLayout().coordinatesToPosition(x, y, true);
  }

  /**
   * @param index Hex index in row-major order starting with the upper-left-hand corner (0-based).
   * @return upper-left-hand point of the hex or square. Returns <code>null</code> if the index is not valid.
   */
  Point indexToPosition(int index) {
    return getLayout().coordinatesToPosition(index % columns, index / columns, true);
  }

  /**
   * @param index hex index in row-major order starting with the upper-left-hand corner (0-based).
   * @param nullIfOffBoard return <code>null</code> if not a valid index. If <code>false</code> will
   *                       return the point corresponding to the index if it were valid. 
   * @return Point corresponding to the upper left hand corner of the hex or square.
   */
  Point indexToPosition(int index, boolean nullIfOffBoard) {
    return getLayout().coordinatesToPosition(index % columns, index / columns,
        nullIfOffBoard);
  }
  
  /**
   * @param index The hex index in row major order starting with the upper-left-hand corner (0-based).
   * @return <code>Point</code> corresponding to the centre of that hex.
   */
  Point indexToCenterPosition(int index) {
    // get upper-left-hand corner of the hex
    Point p = indexToPosition(index);
    if (p == null)
      return p;
    // shift to the centre
    p.translate(getLayout().getDeltaX()/2, getLayout().getDeltaY()/2);
    return p;
  }
  
  @Override
  public void writeToArchive() throws IOException {

    GameModule module = GameModule.getGameModule();
    
    // merge layers that can and should be merged.
    MapLayer base = new BaseLayer();    
    // if there is no base map image, avoid creating a lot of layers.
    if (!((BaseLayer) base).hasBaseMap()) {
      Iterator<MapLayer> iter = mapElements.iterator();
      while(iter.hasNext()) {
        base.overlay(iter.next());
        iter.remove();
      }
      mapElements.add(base);
    }
    else {
      mapElements.add(0, base);
      Iterator<MapLayer> iter = mapElements.iterator();
      iter.next();
      while(iter.hasNext()) {
        MapLayer next = iter.next();
        if (!next.isSwitchable()) {
          Iterator<MapLayer> iter2 = mapElements.iterator();
          MapLayer under = iter2.next();
          while (under != next) {
            under.overlay(next);
            under = iter2.next();
          }
          iter.remove();
        }
      }
      mapElements.add(0, base);
    }
    
    for (MapLayer layer : mapElements) {
      layer.writeToArchive();
    }    
    
    // map options: log formats
    getMainMap().setAttribute(Map.MOVE_WITHIN_FORMAT, "$pieceName$ moving from [$previousLocation$] to [$location$]");
    getMainMap().setAttribute(Map.MOVE_TO_FORMAT, "$pieceName$ moving from [$previousLocation$] to [$location$]");
    getMainMap().setAttribute(Map.CREATE_FORMAT, "$pieceName$ Added to [$location$]");

    // default grid
    AbstractConfigurable ac = getLayout().getGeometricGrid();
    
    // TODO: set default grid numbering for maps that have no sheets (e.g., Air Assault on Crete).
  
    // ensure that we don't have a singleton null
    if (mapSheets.size() == 1 && mapSheets.get(0) == null)
      mapSheets.remove(0);
        
    // setup grids defined by ADC module
    Board board = getBoard();
    if (mapSheets.size() > 0) {
      ZonedGrid zg = new ZonedGrid();
      for (MapSheet ms : mapSheets) {
        if (ms == null) // the last one is always null
          break;
        Zone z = ms.getZone();
        if (z != null) {
          insertComponent(z, zg);
        }
      }

      // add default grid
      insertComponent(ac, zg);

      // add zoned grid to board
      insertComponent(zg, board);
    } else {
      // add the default grid to the board
      insertComponent(ac, board);
    }

    /* global properties */

    // for testing purposes
    GlobalOptions options = module.getAllDescendantComponentsOf(GlobalOptions.class).toArray(new GlobalOptions[0])[0];
    options.setAttribute(GlobalOptions.AUTO_REPORT, GlobalOptions.ALWAYS);

    // add zoom capability
    if (zoomLevel > 0) {
      Zoomer zoom = new Zoomer();
      String[] s = new String[3];
      for (int i = 0; i < 3; ++i)
        s[i] = Double.toString(set.getZoomFactor(i));
      zoom.setAttribute("zoomLevels", StringArrayConfigurer.arrayToString(s));
      insertComponent(zoom, getMainMap());
    }
    
    // add place name capability
    if (placeNames.size() > 0) {
      writePlaceNames(module);
    }
    
    // set up inventory button
    final Inventory inv = new Inventory();
    insertComponent(inv, module);
    inv.setAttribute(Inventory.BUTTON_TEXT, "Search");
    inv.setAttribute(Inventory.TOOLTIP, "Find place by name");
    inv.setAttribute(Inventory.FILTER, "CurrentMap = Main Map && Type != Layer");
    inv.setAttribute(Inventory.ICON, "");
    inv.setAttribute(Inventory.GROUP_BY, "Type");
  }

  /**
   * Write out place name information as non-stackable pieces which can be searched via
   * the piece inventory.
   * 
   * @param module - Game module to write to.
   */
  protected void writePlaceNames(GameModule module) {
    // write prototype
    PrototypesContainer container = module.getAllDescendantComponentsOf(PrototypesContainer.class).iterator().next();
    PrototypeDefinition def = new PrototypeDefinition();
    insertComponent(def, container);
    def.setConfigureName(PLACE_NAMES);
    
    GamePiece gp = new BasicPiece();
    SequenceEncoder se = new SequenceEncoder(',');
    se.append(ADC2Utils.TYPE);
    gp = new Marker(Marker.ID + se.getValue(), gp);
    gp.setProperty(ADC2Utils.TYPE, PLACE_NAME);
    gp = new Immobilized(gp, Immobilized.ID + "n;V");     
    def.setPiece(gp);
    
    // write place names as pieces with no image.
    getMainMap();
    final Point offset = getCenterOffset();
    final HashSet<String> set = new HashSet<String>();    
    final Board board = getBoard();
    
    for (PlaceName pn : placeNames) {
      String name = pn.getText();
      Point p = pn.getPosition();
      if (p == null)
        continue;
      if (set.contains(name))
        continue;
      set.add(name);
      SetupStack stack = new SetupStack();
      insertComponent(stack, getMainMap());
      p.translate(offset.x, offset.y);
      String location = getMainMap().locationName(p);
      stack.setAttribute(SetupStack.NAME, name);
      stack.setAttribute(SetupStack.OWNING_BOARD, board.getConfigureName());
      
      MapGrid mg = board.getGrid();
      Zone z = null;
      if (mg instanceof ZonedGrid)
        z = ((ZonedGrid) mg).findZone(p);        
      stack.setAttribute(SetupStack.X_POSITION, Integer.toString(p.x));
      stack.setAttribute(SetupStack.Y_POSITION, Integer.toString(p.y));
      if (z != null) {
        try {
          if (mg.getLocation(location) != null) {            
            assert(mg.locationName(mg.getLocation(location)).equals(location));
            stack.setAttribute(SetupStack.USE_GRID_LOCATION, true);
            stack.setAttribute(SetupStack.LOCATION, location);
          }
        }
        catch(BadCoords e) {}
      }      
      
      BasicPiece bp = new BasicPiece();
      se = new SequenceEncoder(BasicPiece.ID, ';');
      se.append("").append("").append("").append(name);
      bp.mySetType(se.getValue());      
      
      se = new SequenceEncoder(UsePrototype.ID.replaceAll(";", ""), ';');
      se.append(PLACE_NAMES);
      gp = new UsePrototype(se.getValue(), bp);
      
      PieceSlot ps = new PieceSlot(gp);
      insertComponent(ps, stack);
    }
  }

  /**
   * Does this map board use old-style hex spacing?
   * 
   * @return <code>true</code> if this board is pre version 2.08, <code>false</code> if V2.08 or later.
   */
  boolean isPreV208Layout() {
    return isPreV208;
  }

  /**
   * @return The VASSAL board object corresponding to the imported map.
   */
  Board getBoard() {
    BoardPicker picker = getBoardPicker();
    String boards[] = picker.getAllowableBoardNames();
    assert(boards.length <= 1);
    Board board = null;
    if (boards.length == 0) {
      board = new Board();
      insertComponent(board, picker);
    }
    else {
      board = picker.getBoard(boards[0]);
    }
    return board;
  }

  private ToolbarMenu getToolbarMenu() {
    List<ToolbarMenu> list = getMainMap().getComponentsOf(ToolbarMenu.class);
    ToolbarMenu menu = null;
    if (list.size() == 0) {
      menu = new ToolbarMenu();
      insertComponent(menu, getMainMap());
      menu.setAttribute(ToolbarMenu.BUTTON_TEXT, "View");
      menu.setAttribute(ToolbarMenu.TOOLTIP, "Toggle visibility of map elements");        
    }
    else {
      assert(list.size() == 1);
      menu = list.get(0);
    }
    return menu;
  }
  
  /**
   * @return The map background colour.
   */
  Color getTableColor() {
    return tableColor;
  }

  /**
   * @return The VASSAL BoardPicker object corresponding to this imported map.
   */
  BoardPicker getBoardPicker() {
    if (boardPicker == null)
      boardPicker = getMainMap().getAllDescendantComponentsOf(BoardPicker.class).toArray(new BoardPicker[0])[0];
    return boardPicker;
  }

  @Override
  public boolean isValidImportFile(File f) throws IOException {
    DataInputStream in = null;
    try {
      in = new DataInputStream(new FileInputStream(f));
      boolean valid = in.readByte() == -3;
      in.close();
      return valid;
    }
    finally {
      IOUtils.closeQuietly(in);
    }
  }
}
