/*
 * MegaMek - Copyright (C) 2000-2003 Ben Mazur ([email protected])
 *
 *  This program is free software; you can redistribute it and/or modify it
 *  under the terms of the GNU General Public License as published by the Free
 *  Software Foundation; either version 2 of the License, or (at your option)
 *  any later version.
 *
 *  This program is distributed in the hope that it will be useful, but
 *  WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 *  or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
 *  for more details.
 */
package megamek.client.ui.swing;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Desktop;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.Image;
import java.awt.Insets;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.SystemColor;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.Stack;

import javax.imageio.ImageIO;
import javax.swing.Box;
import javax.swing.ButtonGroup;
import javax.swing.DefaultListCellRenderer;
import javax.swing.DefaultListModel;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.ScrollPaneConstants;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.JToggleButton;
import javax.swing.SwingConstants;
import javax.swing.WindowConstants;
import javax.swing.border.LineBorder;
import javax.swing.border.TitledBorder;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.filechooser.FileFilter;

import megamek.client.event.BoardViewEvent;
import megamek.client.event.BoardViewListenerAdapter;
import megamek.client.ui.Messages;
import megamek.client.ui.swing.boardview.BoardView1;
import megamek.client.ui.swing.util.MegaMekController;
import megamek.common.Configuration;
import megamek.common.Coords;
import megamek.common.Game;
import megamek.common.Hex;
import megamek.common.IBoard;
import megamek.common.IGame;
import megamek.common.IHex;
import megamek.common.ITerrain;
import megamek.common.ITerrainFactory;
import megamek.common.MapSettings;
import megamek.common.Terrains;
import megamek.common.util.BoardUtilities;
import megamek.common.util.ImageUtil;
import megamek.common.util.MegaMekFile;

// TODO: center map
// TODO: background on the whole screen
// TODO: restrict terrains to those with images?
// TODO: Allow drawing of invalid terrain as an override?
// TODO: Allow adding/changing board background images
// TODO: board load time???
// TODO: sluggish hex drawing?
// TODO: the board validation after a board load seems to be influenced by the former board...
// TODO: copy/paste hexes
// TODO: have a button to fix all road/bldg exits

public class BoardEditor extends JComponent
        implements ItemListener, ListSelectionListener, ActionListener, DocumentListener, IMapSettingsObserver {
    
    /**
     * Class to make terrains in JComboBoxes easier.  This enables keeping the terrain type int separate from the name
     * that gets displayed and also provides a way to get tooltips.
     * 
     * @author arlith
     */
    private static class TerrainHelper implements Comparable<TerrainHelper> {
        private int terrainType;

        TerrainHelper (int terrain) {
            terrainType = terrain;
        }

        public int getTerrainType() {
            return terrainType;
        }

        public String toString() {
            return Terrains.getEditorName(terrainType);
        }

        public String getTerrainTooltip() {
            return Terrains.getEditorTooltip(terrainType);
        }

        @Override
        public int compareTo(TerrainHelper o) {
            return toString().compareTo(o.toString());
        }
        
        @Override
        public boolean equals(Object other) {
            if (other instanceof Integer) {
                return getTerrainType() == (Integer) other;
            }
            if (!(other instanceof TerrainHelper)) {
                return false;
            }
            return getTerrainType() == ((TerrainHelper)other).getTerrainType();
        }
    }

    /**
     * Class to make it easier to display a <code>Terrain</code> in a JList or JComboBox.
     *
     * @author arlith
     */
    private static class TerrainTypeHelper implements Comparable<TerrainTypeHelper> {

        ITerrain terrain;

        TerrainTypeHelper(ITerrain terrain) {
            this.terrain = terrain;
        }

        public ITerrain getTerrain() {
            return terrain;
        }

        @Override
        public String toString() {
            String baseString = Terrains.getDisplayName(terrain.getType(), terrain.getLevel());
            if (baseString == null) {
                baseString = Terrains.getEditorName(terrain.getType());
                baseString += " " + terrain.getLevel();
            }
            if (terrain.hasExitsSpecified()) {
                baseString += " (Exits: " + terrain.getExits() + ")";
            }
            return baseString; 
        }

        public String getTooltip() {
            return terrain.toString();
        }

        @Override
        public int compareTo(TerrainTypeHelper o) {
            return toString().compareTo(o.toString());
        }
    }
    
    /**
     *  ListCellRenderer for rendering tooltips for each item in a list or combobox.  Code from SourceForge:
     *  https://stackoverflow.com/questions/480261/java-swing-mouseover-text-on-jcombobox-items 
     */
    private static class ComboboxToolTipRenderer extends DefaultListCellRenderer {
        /**
         * 
         */
        private static final long serialVersionUID = 7428395938750335593L;

        TerrainHelper[] terrains;
        
        List<TerrainTypeHelper> terrainTypes;

        @Override
        public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected,
                boolean cellHasFocus) {

            JComponent comp = (JComponent) super.getListCellRendererComponent(list, value, index, isSelected,
                    cellHasFocus);

            if (-1 < index && null != value && null != terrains) {
                list.setToolTipText(terrains[index].getTerrainTooltip());
            }
            if (-1 < index && null != value && null != terrainTypes) {
                list.setToolTipText(terrainTypes.get(index).getTooltip());
            }
            return comp;
        }

        public void setTerrains(TerrainHelper[] terrains) {
            this.terrains = terrains;
        }
        
        public void setTerrainTypes(List<TerrainTypeHelper> terrainTypes) {
            this.terrainTypes = terrainTypes;
        }
    }
 
    private static final long serialVersionUID = 4689863639249616192L;
    
    GUIPreferences guip = GUIPreferences.getInstance();

    //region action commands
    private static final String FILE_BOARD_EDITOR_EXPAND = "fileBoardExpand";
    private static final String FILE_BOARD_EDITOR_VALIDATE = "fileBoardValidate";
    private static final String FILE_SOURCEFILE = "fileSource";
    //endregion action commands

    // Components
    JFrame frame = new JFrame();
    JScrollPane scrollPane;
    private Game game = new Game();
    IBoard board = game.getBoard();
    BoardView1 bv;
    public static final int [] allDirections = {0,1,2,3,4,5};
    boolean isDragging = false;
    private Component bvc;
    private CommonMenuBar menuBar = new CommonMenuBar();
    private CommonAboutDialog about;
    private CommonHelpDialog help;
    private CommonSettingsDialog setdlg;
    private ITerrainFactory TF = Terrains.getTerrainFactory();
    private JDialog minimapW;
    private MiniMap minimap;
    MegaMekController controller;
    
    // The current files
    private File curfileImage;
    private File curfile;

    // The active hex "brush"
    private HexCanvas canHex;
    IHex curHex = new Hex();
    
    // Easy terrain access buttons
    private JButton buttonLW, buttonLJ;
    private JButton buttonWa, buttonSw, buttonRo;
    private JButton buttonRd, buttonCl, buttonBu;
    private JButton buttonMd, buttonPv, buttonSn;
    private JButton buttonIc, buttonTu, buttonMg;
    private JButton buttonBr, buttonFT;
    private JToggleButton buttonBrush1, buttonBrush2, buttonBrush3;
    private JToggleButton buttonUpDn, buttonOOC;
    // The brush size: 1 = 1 hex, 2 = radius 1, 3 = radius 2  
    int brushSize = 1;
    int hexLeveltoDraw = -1000;
    private Font fontElev = new Font("SansSerif", Font.BOLD, 20); //$NON-NLS-1$
    private Font fontComboTerr = new Font("SansSerif", Font.BOLD, 12); //$NON-NLS-1$
    private EditorTextField texElev;
    private JButton butElevUp;
    private JButton butElevDown;
    private JList<TerrainTypeHelper> lisTerrain;
    private ComboboxToolTipRenderer lisTerrainRenderer;
    private JButton butDelTerrain;
    private JComboBox<TerrainHelper> choTerrainType;
    private EditorTextField texTerrainLevel;
    private JCheckBox cheTerrExitSpecified;
    private EditorTextField texTerrExits;
    private JButton butTerrExits;
    private JCheckBox cheRoadsAutoExit;
    private JButton butExitUp, butExitDown;
    private JComboBox<String> choTheme;
    private JButton butTerrDown, butTerrUp;
    private JButton butAddTerrain;
    private JButton butBoardNew;
    private JButton butBoardOpen;
    private JButton butBoardSave;
    private JButton butBoardSaveAs;
    private JButton butBoardSaveAsImage;
    private JButton butMiniMap;
    private JButton butBoardValidate;
    private JButton butSourceFile;
    private MapSettings mapSettings = MapSettings.getInstance();
    private JButton butExpandMap;
    private Coords lastClicked;
    
    // Undo / Redo
    JButton buttonUndo, buttonRedo;
    private Stack<HashSet<IHex>> undoStack = new Stack<>();
    private Stack<HashSet<IHex>> redoStack = new Stack<>();
    private HashSet<IHex> currentUndoSet;
    private HashSet<Coords> currentUndoCoords;
    
    // Tracker for board changes; unfortunately this is not equal to 
    // undoStack == empty because saving the board doesn't empty the 
    // undo stack but makes the board unchanged.
    /** Tracks if the board has changes over the last saved version. */
    private boolean hasChanges = false;
    /** Tracks if the board can return to the last saved version. */
    private boolean canReturnToSaved = true;
    /** The undo stack size at the last save. Used to track saved status of the board. */
    private int savedUndoStackSize = 0;
    
    // Misc
    private static final int [] defaultBuildingCFs = {0,15,40,90,150};  //TODO: Building also defines such a list
    private String loadPath = "data" + File.separator + "boards";
    
    /**
     * Special purpose indicator, keeps terrain list 
     * from de-selecting when clicking it
     */
    private boolean terrListBlocker = false;
    
    /**
     * Special purpose indicator, prevents an update
     * loop when the terrain level or exits field is changed
     */
    private boolean noTextFieldUpdate = false;
    
    /**
     * A MouseAdapter that closes a JLabel when clicked 
     */
    private MouseAdapter clickToHide = new MouseAdapter() {
        @Override
        public void mouseReleased(MouseEvent e) {
            if (e.getSource() instanceof JLabel)
                ((JLabel) e.getSource()).setVisible(false);
        }
    };

    /**
     * Flag that indicates whether hotkeys should be ignored or not.  This is
     * used for disabling hot keys when various dialogs are displayed.
     */
    private boolean ignoreHotKeys = false;

    /**
     * Creates and lays out a new Board Editor frame.
     */
    public BoardEditor(MegaMekController c) {
        controller = c;
        try {
            bv = new BoardView1(game, controller, null);
            bvc = bv.getComponent(true);
            bv.setDisplayInvalidHexInfo(true);
        } catch (IOException e) {
            JOptionPane
                    .showMessageDialog(
                            frame,
                            Messages.getString("BoardEditor.CouldntInitialize") + e,
                            Messages.getString("BoardEditor.FatalError"), JOptionPane.ERROR_MESSAGE); //$NON-NLS-1$
            //$NON-NLS-2$
            frame.dispose();
        }

        // Add a mouse listener for mouse button release 
        // to handle Undo
        bv.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseReleased(MouseEvent e) {
                if (e.getButton() == MouseEvent.BUTTON1) {
                    // Act only if the user actually drew something
                    if ((currentUndoSet != null) &&
                            !currentUndoSet.isEmpty()) {
                        // Since this draw action is finished, push the
                        // drawn hexes onto the Undo Stack and get ready
                        // for a new draw action
                        undoStack.push(currentUndoSet);
                        currentUndoSet = null;
                        buttonUndo.setEnabled(true);
                        // Drawing something disables any redo actions
                        redoStack.clear();
                        buttonRedo.setEnabled(false);
                        // When Undo (without Redo) has been used after saving
                        // and the user draws on the board, then it can
                        // no longer know if it's been returned to the saved state
                        // and it will always be treated as changed.
                        if (savedUndoStackSize > undoStack.size()) {
                            canReturnToSaved = false;
                        }
                        hasChanges = !canReturnToSaved | (undoStack.size() != savedUndoStackSize);
                    }
                    // Mark the title when the board has changes
                    setFrameTitle();
                }
            }
        });
        bv.addBoardViewListener(new BoardViewListenerAdapter() {
            @Override
            public void hexMoused(BoardViewEvent b) {
                Coords c = b.getCoords();
                // return if there are no or no valid coords or if we click the same hex again
                // unless Raise/Lower Terrain is active which should let us click the same hex 
                if ((c == null) || (c.equals(lastClicked) && !buttonUpDn.isSelected())
                        || !board.contains(c)) {
                    return;
                }
                lastClicked = c;
                bv.cursor(c);
                boolean isALT = (b.getModifiers() & InputEvent.ALT_MASK) != 0;
                boolean isSHIFT = (b.getModifiers() & InputEvent.SHIFT_MASK) != 0;
                boolean isCTRL = (b.getModifiers() & InputEvent.CTRL_MASK) != 0;
                boolean isLMB = (b.getModifiers() & InputEvent.BUTTON1_MASK) != 0;

                // Raise/Lower Terrain is selected
                if (buttonUpDn.isSelected()) {
                    
                    // Mouse Button released
                    if (b.getType() == BoardViewEvent.BOARD_HEX_CLICKED) {
                        hexLeveltoDraw = -1000;
                        isDragging = false;
                    }

                    // Mouse Button clicked or dragged
                    if ((b.getType() == BoardViewEvent.BOARD_HEX_DRAGGED) && isLMB) {
                        if (!isDragging) {
                            hexLeveltoDraw = board.getHex(c).getLevel();
                            if (isALT) hexLeveltoDraw--;
                            else if (isSHIFT) hexLeveltoDraw++;
                            isDragging = true;
                        }
                    }

                    // CORRECTION, click outside the board then drag inside???
                    if (hexLeveltoDraw != -1000) {
                        LinkedList<Coords> allBrushHexes = getBrushCoords(c) ;
                        for (Coords h: allBrushHexes) {
                            if (!buttonOOC.isSelected() || board.getHex(h).isClearHex())
                            {
                                saveToUndo(h);
                                relevelHex(h);
                            }   
                        }
                    }
                    // ------- End Raise/Lower Terrain
                } else {
                    // Normal texture paint
                    if (isALT) { // ALT-Click
                        setCurrentHex(board.getHex(b.getCoords()));
                    } else {
                        LinkedList<Coords> allBrushHexes = getBrushCoords(c);
                        for (Coords h: allBrushHexes) {
                            // test if texture overwriting is active
                            if ((!buttonOOC.isSelected() || board.getHex(h).isClearHex()) && curHex.isValid(null))
                            {
                                saveToUndo(h);
                                if (isCTRL) { // CTRL-Click
                                    paintHex(h);
                                } else if (isSHIFT) { // SHIFT-Click
                                    addToHex(h);
                                } else if (isLMB) { // Normal click
                                    retextureHex(h);
                                }
                            }
                        }
                    }
                }
            }
        });
        
        bv.setUseLOSTool(false);
        setupEditorPanel();
        setupFrame();
        frame.setVisible(true);
        if (GUIPreferences.getInstance().getNagForMapEdReadme()) {
            String title = Messages.getString("BoardEditor.readme.title"); //$NON-NLS-1$
            String body = Messages.getString("BoardEditor.readme.message"); //$NON-NLS-1$
            ConfirmDialog confirm = new ConfirmDialog(frame, title, body, true);
            confirm.setVisible(true);
            if (!confirm.getShowAgain()) {
                GUIPreferences.getInstance().setNagForMapEdReadme(false);
            }
            if (confirm.getAnswer()) {
                showHelp();
            }
        }
    }

    /**
     * Sets up the frame that will display the editor.
     */
    private void setupFrame() {
        scrollPane = new JScrollPane(this, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
                ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
        scrollPane.getVerticalScrollBar().setUnitIncrement(12);

        setFrameTitle();
        frame.getContentPane().setLayout(new BorderLayout());

        frame.getContentPane().add(bvc, BorderLayout.CENTER);
        frame.getContentPane().add(scrollPane, BorderLayout.EAST);
        menuBar.addActionListener(this);
        frame.setJMenuBar(menuBar);
        frame.setBackground(SystemColor.menu);
        frame.setForeground(SystemColor.menuText);
        if (GUIPreferences.getInstance().getWindowSizeHeight() != 0) {
            frame.setLocation(GUIPreferences.getInstance().getWindowPosX(),
                              GUIPreferences.getInstance().getWindowPosY());
            frame.setSize(GUIPreferences.getInstance().getWindowSizeWidth(),
                          GUIPreferences.getInstance().getWindowSizeHeight());
        } else {
            frame.setSize(800, 600);
        }

        frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
        frame.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                // When the board has changes, ask the user 
                if (hasChanges) {
                    ignoreHotKeys = true;
                    int savePrompt = JOptionPane.showConfirmDialog(null,
                            Messages.getString("BoardEditor.exitprompt"), //$NON-NLS-1$
                            Messages.getString("BoardEditor.exittitle"), //$NON-NLS-1$
                            JOptionPane.YES_NO_CANCEL_OPTION,
                            JOptionPane.WARNING_MESSAGE);
                    ignoreHotKeys = false;

                    // When the user cancels or did not actually save the board, don't close 
                    if (((savePrompt == JOptionPane.YES_OPTION) && !boardSave()) || 
                            (savePrompt == JOptionPane.CANCEL_OPTION)) {
                        return;
                    } 
                }

                // otherwise: exit the Map Editor
                minimapW.setVisible(false);
                if (controller != null) {
                    controller.removeAllActions();
                    controller.boardEditor = null;
                }
                frame.dispose();
            }
        });
    }

    /**
     * Sets up JButtons
     */
    private JButton prepareButton(String iconName, String buttonName, ArrayList<JButton> bList) {
        JButton button = new JButton(buttonName);
        button.addActionListener(this);
        // Get the normal icon
        File file = new MegaMekFile(Configuration.widgetsDir(), "/MapEditor/"+iconName+".png").getFile(); //$NON-NLS-1$ //$NON-NLS-2$
        Image imageButton = ImageUtil.loadImageFromFile(file.getAbsolutePath());
        if (imageButton != null) {
            button.setIcon(new ImageIcon(imageButton));
            // When there is an icon, then the text can be removed
            button.setText("");
        }

        // Get the hover icon
        file = new MegaMekFile(Configuration.widgetsDir(), "/MapEditor/"+iconName+"_H.png").getFile(); //$NON-NLS-1$ //$NON-NLS-2$
        imageButton = ImageUtil.loadImageFromFile(file.getAbsolutePath());
        if (imageButton != null) {
            button.setRolloverIcon(new ImageIcon(imageButton));
        }
        
        // Get the disabled icon, if any
        file = new MegaMekFile(Configuration.widgetsDir(), "/MapEditor/"+iconName+"_G.png").getFile(); //$NON-NLS-1$ //$NON-NLS-2$
        imageButton = ImageUtil.loadImageFromFile(file.getAbsolutePath());
        if (imageButton != null) {
            button.setDisabledIcon(new ImageIcon(imageButton));
        }

        String tt = Messages.getString("BoardEditor."+iconName+"TT");
        if (tt.length() != 0) {
            button.setToolTipText(tt); //$NON-NLS-1$ //$NON-NLS-2$
        }
        button.setMargin(new Insets(0,0,0,0));
        if (bList != null) bList.add(button);
        return button;
    }
    
    /**
     * Sets up JToggleButtons
     */
    private JToggleButton addTerrainTButton(String iconName, String buttonName, ArrayList<JToggleButton> bList) {
        JToggleButton button = new JToggleButton(buttonName);
        button.addActionListener(this);
        
        // Get the normal icon
        File file = new MegaMekFile(Configuration.widgetsDir(), "/MapEditor/"+iconName+".png").getFile(); //$NON-NLS-1$ //$NON-NLS-2$
        Image imageButton = ImageUtil.loadImageFromFile(file.getAbsolutePath());
        if (imageButton != null) {
            button.setIcon(new ImageIcon(imageButton));
            // When there is an icon, then the text can be removed
            button.setText("");
        }
        
        // Get the hover icon
        file = new MegaMekFile(Configuration.widgetsDir(), "/MapEditor/"+iconName+"_H.png").getFile(); //$NON-NLS-1$ //$NON-NLS-2$
        imageButton = ImageUtil.loadImageFromFile(file.getAbsolutePath());
        if (imageButton != null)
            button.setRolloverIcon(new ImageIcon(imageButton));
        
        // Get the selected icon
        file = new MegaMekFile(Configuration.widgetsDir(), "/MapEditor/"+iconName+"_S.png").getFile(); //$NON-NLS-1$ //$NON-NLS-2$
        imageButton = ImageUtil.loadImageFromFile(file.getAbsolutePath());
        if (imageButton != null)
            button.setSelectedIcon(new ImageIcon(imageButton));
        
        button.setToolTipText(Messages.getString("BoardEditor."+iconName+"TT")); //$NON-NLS-1$ //$NON-NLS-2$
        if (bList != null) bList.add(button);
        return button;
    }

    /**
     * Sets up the editor panel, which goes on the right of the map and has
     * controls for editing the current square.
     */
    private void setupEditorPanel() {
        // Help Texts
        JLabel genHelpText1 = new JLabel(Messages.getString("BoardEditor.helpText"),SwingConstants.LEFT); //$NON-NLS-1$
        JLabel terrainButtonHelp = new JLabel(Messages.getString("BoardEditor.helpText2"),SwingConstants.LEFT); //$NON-NLS-1$
        genHelpText1.addMouseListener(clickToHide);
        terrainButtonHelp.addMouseListener(clickToHide);

        // Buttons to ease setting common terrain types
        ArrayList<JButton> terrainButtons = new ArrayList<>();
        buttonLW = prepareButton("ButtonLW", "Woods", terrainButtons); //$NON-NLS-1$ //$NON-NLS-2$
        buttonLJ = prepareButton("ButtonLJ", "Jungle", terrainButtons); //$NON-NLS-1$ //$NON-NLS-2$
        buttonWa = prepareButton("ButtonWa", "Water", terrainButtons); //$NON-NLS-1$ //$NON-NLS-2$
        buttonSw = prepareButton("ButtonSw", "Swamp", terrainButtons); //$NON-NLS-1$ //$NON-NLS-2$
        buttonRo = prepareButton("ButtonRo", "Rough", terrainButtons); //$NON-NLS-1$ //$NON-NLS-2$
        buttonMd = prepareButton("ButtonMd", "Mud", terrainButtons); //$NON-NLS-1$ //$NON-NLS-2$
        buttonPv = prepareButton("ButtonPv", "Pavement", terrainButtons); //$NON-NLS-1$ //$NON-NLS-2$
        buttonSn = prepareButton("ButtonSn", "Snow", terrainButtons); //$NON-NLS-1$ //$NON-NLS-2$
        buttonBu = prepareButton("ButtonBu", "Buildings", terrainButtons); //$NON-NLS-1$ //$NON-NLS-2$
        buttonRd = prepareButton("ButtonRd", "Roads", terrainButtons); //$NON-NLS-1$ //$NON-NLS-2$
        buttonBr = prepareButton("ButtonBr", "Bridges", terrainButtons); //$NON-NLS-1$ //$NON-NLS-2$
        buttonFT = prepareButton("ButtonFT", "Fuel Tanks", terrainButtons); //$NON-NLS-1$ //$NON-NLS-2$
        buttonIc = prepareButton("ButtonIc", "Ice", terrainButtons); //$NON-NLS-1$ //$NON-NLS-2$
        buttonTu = prepareButton("ButtonTu", "Tundra", terrainButtons); //$NON-NLS-1$ //$NON-NLS-2$
        buttonMg = prepareButton("ButtonMg", "Magma", terrainButtons); //$NON-NLS-1$ //$NON-NLS-2$
        buttonCl = prepareButton("ButtonCl", "Clear", terrainButtons); //$NON-NLS-1$ //$NON-NLS-2$

        ArrayList<JToggleButton> brushButtons = new ArrayList<>();
        buttonBrush1 = addTerrainTButton("ButtonHex1", "Brush1", brushButtons); //$NON-NLS-1$ //$NON-NLS-2$
        buttonBrush2 = addTerrainTButton("ButtonHex7", "Brush2", brushButtons); //$NON-NLS-1$ //$NON-NLS-2$
        buttonBrush3 = addTerrainTButton("ButtonHex19", "Brush3", brushButtons); //$NON-NLS-1$ //$NON-NLS-2$
        ButtonGroup brushGroup = new ButtonGroup();
        brushGroup.add(buttonBrush1);
        brushGroup.add(buttonBrush2);
        brushGroup.add(buttonBrush3);
        buttonOOC = addTerrainTButton("ButtonOOC", "OOC", brushButtons); //$NON-NLS-1$ //$NON-NLS-2$
        buttonUpDn = addTerrainTButton("ButtonUpDn", "UpDown", brushButtons); //$NON-NLS-1$ //$NON-NLS-2$

        ArrayList<JButton> undoButtons = new ArrayList<>();
        buttonUndo = prepareButton("ButtonUndo", "Undo", undoButtons); //$NON-NLS-1$ //$NON-NLS-2$
        buttonRedo = prepareButton("ButtonRedo", "Redo", undoButtons); //$NON-NLS-1$ //$NON-NLS-2$
        buttonUndo.setEnabled(false);
        buttonRedo.setEnabled(false);

        MouseWheelListener wheelListener = e -> {
            int terrain = Integer.MIN_VALUE;
            if (e.getSource() == buttonRo) terrain = Terrains.ROUGH;
            else if (e.getSource() == buttonSw) terrain = Terrains.SWAMP;
            else if (e.getSource() == buttonWa) terrain = Terrains.WATER;
            else if (e.getSource() == buttonLW) terrain = Terrains.WOODS;
            else if (e.getSource() == buttonLJ) terrain = Terrains.JUNGLE;
            else if (e.getSource() == buttonMd) terrain = Terrains.MUD;
            else if (e.getSource() == buttonPv) terrain = Terrains.PAVEMENT;
            else if (e.getSource() == buttonIc) terrain = Terrains.ICE;
            else if (e.getSource() == buttonSn) terrain = Terrains.SNOW;
            else if (e.getSource() == buttonTu) terrain = Terrains.TUNDRA;
            else if (e.getSource() == buttonMg) terrain = Terrains.MAGMA;

            if (terrain >= 0) {
                IHex saveHex = curHex.duplicate();
                // change the terrain level by wheel direction if present,
                // or set to 1 if not present
                if (curHex.containsTerrain(terrain)) {
                    addSetTerrainEasy(terrain,
                            curHex.getTerrain(terrain).getLevel() +
                            ((e.getWheelRotation() < 0) ? 1 : -1));
                } else {
                    if (!e.isShiftDown())
                        curHex.removeAllTerrains();
                    addSetTerrainEasy(terrain, 1);
                }
                // Reset the terrain to the former state
                // if the new would be invalid.
                if (!curHex.isValid(null)) {
                    curHex = saveHex;
                    refreshTerrainList();
                    repaintWorkingHex();
                }
            }
        };

        buttonSw.addMouseWheelListener(wheelListener);
        buttonWa.addMouseWheelListener(wheelListener);
        buttonRo.addMouseWheelListener(wheelListener);
        buttonLJ.addMouseWheelListener(wheelListener);
        buttonLW.addMouseWheelListener(wheelListener);
        buttonMd.addMouseWheelListener(wheelListener);
        buttonPv.addMouseWheelListener(wheelListener);
        buttonSn.addMouseWheelListener(wheelListener);
        buttonIc.addMouseWheelListener(wheelListener);
        buttonTu.addMouseWheelListener(wheelListener);
        buttonMg.addMouseWheelListener(wheelListener);

        // Mouse wheel behaviour for the BUILDINGS button
        // Always ADDS the building. 
        buttonBu.addMouseWheelListener(e -> {
            // Restore mandatory building parts if some are missing
            setBasicBuilding(false);
            int wheelDir = (e.getWheelRotation() < 0) ? 1 : -1;

            if (e.isShiftDown()) {
                int oldLevel = curHex.getTerrain(Terrains.BLDG_CF).getLevel();
                int newLevel = Math.max(10, oldLevel + wheelDir*5);
                curHex.addTerrain(TF.createTerrain(Terrains.BLDG_CF, newLevel));
            }
            else if (e.isControlDown()) {
                int oldLevel = curHex.getTerrain(Terrains.BUILDING).getLevel();
                int newLevel = Math.max(1, Math.min(4, oldLevel + wheelDir)); // keep between 1 and 4

                if (newLevel != oldLevel) {
                    ITerrain curTerr = curHex.getTerrain(Terrains.BUILDING);
                    curHex.addTerrain(TF.createTerrain(Terrains.BUILDING, 
                            newLevel, curTerr.hasExitsSpecified(), curTerr.getExits()));

                    // Set the CF to the appropriate standard value *IF* it is the appropriate value now,
                    // i.e. if the user has not manually set it to something else
                    int curCF = curHex.getTerrain(Terrains.BLDG_CF).getLevel();
                    if (curCF == defaultBuildingCFs[oldLevel]) 
                        curHex.addTerrain(TF.createTerrain(Terrains.BLDG_CF, defaultBuildingCFs[newLevel]));
                }
                //TODO : Walls
            }
            else {
                int oldLevel = curHex.getTerrain(Terrains.BLDG_ELEV).getLevel();
                int newLevel = Math.max(1, oldLevel + wheelDir);
                curHex.addTerrain(TF.createTerrain(Terrains.BLDG_ELEV, newLevel));
            }

            refreshTerrainList();
            repaintWorkingHex();
        });

        // Mouse wheel behaviour for the BRIDGE button
        buttonBr.addMouseWheelListener(e -> {
            setBasicBridge();
            int wheelDir = (e.getWheelRotation() < 0) ? 1 : -1;
            int terrainType;
            int newLevel;

            if (e.isShiftDown()) {
                terrainType = Terrains.BRIDGE_CF;
                int oldLevel = curHex.getTerrain(terrainType).getLevel();
                newLevel = Math.max(10, oldLevel + wheelDir*10);
            }
            else if (e.isControlDown()) {
                terrainType = Terrains.BRIDGE;
                int oldLevel = curHex.getTerrain(terrainType).getLevel();
                newLevel = Math.max(1, oldLevel + wheelDir);
            }
            else {
                terrainType = Terrains.BRIDGE_ELEV;
                int oldLevel = curHex.getTerrain(terrainType).getLevel();
                newLevel = Math.max(0, oldLevel + wheelDir);
            }

            curHex.addTerrain(TF.createTerrain(terrainType, newLevel));
            refreshTerrainList();
            repaintWorkingHex();
        });

        // Mouse wheel behaviour for the FUELTANKS button
        buttonFT.addMouseWheelListener(e -> {
            setBasicFuelTank();
            int wheelDir = (e.getWheelRotation() < 0) ? 1 : -1;
            int terrainType;
            int newLevel;

            if (e.isShiftDown()) {
                terrainType = Terrains.FUEL_TANK_CF;
                int oldLevel = curHex.getTerrain(terrainType).getLevel();
                newLevel = Math.max(10, oldLevel + wheelDir*10);
            }
            else if (e.isControlDown()) {
                terrainType = Terrains.FUEL_TANK_MAGN;
                int oldLevel = curHex.getTerrain(terrainType).getLevel();
                newLevel = Math.max(10, oldLevel + wheelDir*10);
            }
            else {
                terrainType = Terrains.FUEL_TANK_ELEV;
                int oldLevel = curHex.getTerrain(terrainType).getLevel();
                newLevel = Math.max(1, oldLevel + wheelDir);
            }

            curHex.addTerrain(TF.createTerrain(terrainType, newLevel));
            refreshTerrainList();
            repaintWorkingHex();
        });

        JPanel terrainButtonPanel = new JPanel(new GridLayout(0, 4, 2, 2));
        addManyButtons(terrainButtonPanel, terrainButtons);

        JPanel brushButtonPanel = new JPanel(new GridLayout(0, 3, 2, 2));
        addManyTButtons(brushButtonPanel, brushButtons);
        buttonBrush1.setSelected(true);

        JPanel undoButtonPanel = new JPanel(new GridLayout(1, 2, 2, 2));
        addManyButtons(undoButtonPanel, buttonUndo, buttonRedo);

        // Hex Elevation Control
        texElev = new EditorTextField("0", 3); //$NON-NLS-1$
        texElev.addActionListener(this);
        texElev.getDocument().addDocumentListener(this);

        butElevUp = prepareButton("ButtonHexUP", "Raise Hex Elevation", null); //$NON-NLS-1$ //$NON-NLS-2$
        butElevUp.setName("butElevUp");
        butElevUp.setToolTipText(Messages.getString("BoardEditor.butElevUp.toolTipText"));

        butElevDown = prepareButton("ButtonHexDN", "Lower Hex Elevation", null); //$NON-NLS-1$ //$NON-NLS-2$
        butElevDown.setName("butElevDown");
        butElevDown.setToolTipText(Messages.getString("BoardEditor.butElevDown.toolTipText"));

        // Terrain List
        lisTerrainRenderer = new ComboboxToolTipRenderer();
        lisTerrain = new JList<>(new DefaultListModel<>());
        lisTerrain.addListSelectionListener(this);
        lisTerrain.setCellRenderer(lisTerrainRenderer);
        lisTerrain.setVisibleRowCount(6);
        lisTerrain.setFixedCellWidth(180);
        refreshTerrainList();

        // Terrain List, Preview, Delete
        JPanel panlisHex = new JPanel(new FlowLayout(FlowLayout.LEFT,4,4));
        butDelTerrain = prepareButton("buttonRemT", "Delete Terrain", null);
        butDelTerrain.setEnabled(false);
        canHex = new HexCanvas();
        panlisHex.add(butDelTerrain);
        panlisHex.add(new JScrollPane(lisTerrain));
        panlisHex.add(canHex);

        // Terrain Type Chooser, Level
        TerrainHelper[] terrains = new TerrainHelper[Terrains.SIZE - 1];
        for (int i = 1; i < Terrains.SIZE; i++) {
            terrains[i - 1] = new TerrainHelper(i);
        }
        Arrays.sort(terrains);
        texTerrainLevel = new EditorTextField("0", 2, 0); //$NON-NLS-1$
        texTerrainLevel.addActionListener(this);
        texTerrainLevel.getDocument().addDocumentListener(this);
        choTerrainType = new JComboBox<>(terrains);
        ComboboxToolTipRenderer renderer = new ComboboxToolTipRenderer();
        renderer.setTerrains(terrains);
        choTerrainType.setRenderer(renderer);
        // Selecting a terrain type in the Dropdown should deselect
        // all in the terrain overview list except when selected from there
        choTerrainType.addActionListener(e -> { if (!terrListBlocker) lisTerrain.clearSelection(); });
        choTerrainType.setFont(fontComboTerr);
        butAddTerrain = new JButton(Messages.getString("BoardEditor.butAddTerrain")); //$NON-NLS-1$
        butTerrUp = prepareButton("ButtonTLUP", "Increase Terrain Level", null); //$NON-NLS-1$ //$NON-NLS-2$
        butTerrDown = prepareButton("ButtonTLDN", "Decrease Terrain Level", null); //$NON-NLS-1$ //$NON-NLS-2$

        // Minimap Toggle
        butMiniMap = new JButton(Messages.getString("BoardEditor.butMiniMap")); //$NON-NLS-1$
        butMiniMap.setActionCommand(ClientGUI.VIEW_MINI_MAP);

        // Exits
        cheTerrExitSpecified = new JCheckBox(Messages.getString("BoardEditor.cheTerrExitSpecified")); //$NON-NLS-1$
        cheTerrExitSpecified.addActionListener(e -> {
            noTextFieldUpdate = true;
            updateWhenSelected();
            noTextFieldUpdate = false;
        });
        butTerrExits = prepareButton("ButtonExitA", Messages.getString("BoardEditor.butTerrExits"), null); //$NON-NLS-1$ //$NON-NLS-2$
        texTerrExits = new EditorTextField("0", 2, 0); //$NON-NLS-1$
        texTerrExits.addActionListener(this);
        texTerrExits.getDocument().addDocumentListener(this);
        butExitUp = prepareButton("ButtonEXUP", "Increase Exit / Gfx", null); //$NON-NLS-1$ //$NON-NLS-2$
        butExitDown = prepareButton("ButtonEXDN", "Decrease Exit / Gfx", null); //$NON-NLS-1$ //$NON-NLS-2$

        // Arrows and text fields for type and exits
        JPanel panUP = new JPanel(new GridLayout(1,0,4,4));
        panUP.add(butTerrUp);
        panUP.add(butExitUp);
        panUP.add(butTerrExits);
        JPanel panTex = new JPanel(new GridLayout(1,0,4,4));
        panTex.add(texTerrainLevel);
        panTex.add(texTerrExits);
        panTex.add(cheTerrExitSpecified);
        JPanel panDN = new JPanel(new GridLayout(1,0,4,4));
        panDN.add(butTerrDown);
        panDN.add(butExitDown);
        panDN.add(Box.createHorizontalStrut(5));

        // Auto Exits to Pavement
        cheRoadsAutoExit = new JCheckBox(Messages.getString("BoardEditor.cheRoadsAutoExit")); //$NON-NLS-1$
        cheRoadsAutoExit.addItemListener(this);
        cheRoadsAutoExit.setSelected(true);

        // Theme
        JPanel panTheme = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 4));
        JLabel labTheme = new JLabel(Messages.getString("BoardEditor.labTheme"), SwingConstants.LEFT); //$NON-NLS-1$
        choTheme = new JComboBox<>();
        TilesetManager tileMan = bv.getTilesetManager();
        Set<String> themes = tileMan.getThemes();
        for (String s: themes) choTheme.addItem(s);
        choTheme.addActionListener(this);
        panTheme.add(labTheme);
        panTheme.add(choTheme);

        // The hex settings panel (elevation, theme)
        JPanel panelHexSettings = new JPanel();
        panelHexSettings.setBorder(new TitledBorder(new LineBorder(Color.BLUE, 1), "Hex Settings")); //$NON-NLS-1$
        panelHexSettings.add(butElevUp);
        panelHexSettings.add(texElev);
        panelHexSettings.add(butElevDown);
        panelHexSettings.add(panTheme);

        // The terrain settings panel (type, level, exits)
        JPanel panelTerrSettings = new JPanel(new GridLayout(0, 2, 4, 4));
        panelTerrSettings.setBorder(new TitledBorder(new LineBorder(Color.BLUE, 1), "Terrain Settings")); //$NON-NLS-1$
        panelTerrSettings.add(Box.createVerticalStrut(5));
        panelTerrSettings.add(panUP);

        panelTerrSettings.add(choTerrainType);
        panelTerrSettings.add(panTex);

        panelTerrSettings.add(butAddTerrain);
        panelTerrSettings.add(panDN);

        // The board settings panel (Auto exit roads to pavement)
        JPanel panelBoardSettings = new JPanel();
        panelBoardSettings.setBorder(new TitledBorder(new LineBorder(Color.BLUE, 1), "Board Settings")); //$NON-NLS-1$
        panelBoardSettings.add(cheRoadsAutoExit);

        // Board Buttons (Save, Load...)
        butBoardNew = new JButton(Messages.getString("BoardEditor.butBoardNew")); //$NON-NLS-1$
        butBoardNew.setActionCommand(ClientGUI.FILE_BOARD_NEW);

        butExpandMap = new JButton(Messages.getString("BoardEditor.butExpandMap")); //$NON-NLS-1$
        butExpandMap.setActionCommand(FILE_BOARD_EDITOR_EXPAND);

        butBoardOpen = new JButton(Messages.getString("BoardEditor.butBoardOpen")); //$NON-NLS-1$
        butBoardOpen.setActionCommand(ClientGUI.FILE_BOARD_OPEN);

        butBoardSave = new JButton(Messages.getString("BoardEditor.butBoardSave")); //$NON-NLS-1$
        butBoardSave.setActionCommand(ClientGUI.FILE_BOARD_SAVE);

        butBoardSaveAs = new JButton(Messages.getString("BoardEditor.butBoardSaveAs")); //$NON-NLS-1$
        butBoardSaveAs.setActionCommand(ClientGUI.FILE_BOARD_SAVE_AS);

        butBoardSaveAsImage = new JButton(Messages.getString("BoardEditor.butBoardSaveAsImage")); //$NON-NLS-1$
        butBoardSaveAsImage.setActionCommand(ClientGUI.FILE_BOARD_SAVE_AS_IMAGE);

        butBoardValidate = new JButton(Messages.getString("BoardEditor.butBoardValidate")); //$NON-NLS-1$
        butBoardValidate.setActionCommand(FILE_BOARD_EDITOR_VALIDATE);
        
        butSourceFile = new JButton(Messages.getString("BoardEditor.butSourceFile")); //$NON-NLS-1$
        butSourceFile.setActionCommand(FILE_SOURCEFILE);

        addManyActionListeners(butBoardValidate, butBoardSaveAsImage, butBoardSaveAs, butBoardSave);
        addManyActionListeners(butBoardOpen, butExpandMap, butBoardNew, butMiniMap);
        addManyActionListeners(butDelTerrain, butAddTerrain, butSourceFile);
        

        JPanel panButtons = new JPanel(new GridLayout(4, 2, 2, 2));
        addManyButtons(panButtons, butBoardNew, butBoardSave, butBoardOpen,
                butExpandMap, butBoardSaveAs, butBoardSaveAsImage);
        panButtons.add(butBoardValidate);
        panButtons.add(butMiniMap);
        if (Desktop.isDesktopSupported()) {
            panButtons.add(butSourceFile);
        }

        // ------------------
        // Arrange everything
        //
        setLayout(new GridBagLayout());
        GridBagConstraints cfullLine = new GridBagConstraints();
        GridBagConstraints cYFiller = new GridBagConstraints();

        cfullLine.fill = GridBagConstraints.HORIZONTAL;
        cfullLine.gridwidth = GridBagConstraints.REMAINDER;
        cfullLine.gridx = 0;
        cfullLine.insets = new Insets(4, 4, 1, 1);

        cYFiller.fill = GridBagConstraints.HORIZONTAL;
        cYFiller.gridwidth = GridBagConstraints.REMAINDER;
        cYFiller.gridx = 0;
        cYFiller.weighty = 1;
        cYFiller.insets = new Insets(4, 4, 1, 1);

        // Easy Access Terrain Buttons
        add(genHelpText1, cfullLine);
        add(terrainButtonHelp, cfullLine);
        add(terrainButtonPanel, cfullLine);
        add(brushButtonPanel, cfullLine);
        add(new JLabel(""), cYFiller); //$NON-NLS-1$
        add(undoButtonPanel, cfullLine);
        add(new JLabel(""), cYFiller); //$NON-NLS-1$

        // Terrain and Hex Control
        add(panelBoardSettings, cfullLine);
        add(panelHexSettings, cfullLine);
        add(panelTerrSettings, cfullLine);

        // Terrain List and Preview Hex
        add(panlisHex, cfullLine);

        // Board buttons
        add(panButtons, cfullLine);

        minimapW = new JDialog(frame, Messages
                .getString("BoardEditor.minimapW"), false); //$NON-NLS-1$
        minimapW.setLocation(GUIPreferences.getInstance().getMinimapPosX(),
                             GUIPreferences.getInstance().getMinimapPosY());
        try {
            minimap = new MiniMap(minimapW, game, bv);
        } catch (IOException e) {
            JOptionPane
                    .showMessageDialog(
                            frame,
                            Messages
                                    .getString("BoardEditor.CouldNotInitialiseMinimap") + e,
                            Messages.getString("BoardEditor.FatalError"), JOptionPane.ERROR_MESSAGE); //$NON-NLS-1$
            //$NON-NLS-2$
            frame.dispose();
        }
        minimapW.add(minimap);
        minimapW.setVisible(true);
    }
    
    /**
     * Returns coords that the active brush will paint on;
     * returns only coords that are valid, i.e. on the board
     */
    private LinkedList<Coords> getBrushCoords(Coords center) {
        LinkedList<Coords> coords = new LinkedList<>();
        // The center hex itself is always part of the brush
        coords.add(center);
        // Add surrounding hexes for the big brush
        if (brushSize > 1) {
            for (int dir: allDirections) 
                coords.add(center.translated(dir));
        } 
        // Add the surrounding hexes, radius 2 for the very big brush
        if (brushSize > 2) {
            for (int dir: allDirections) {
                Coords candC = center.translated(dir, 2);
                coords.add(candC);
                coords.add(candC.translated((dir+2)%6));
            }
        } 
        // Remove coords that are not on the board
        LinkedList<Coords> finalCoords = new LinkedList<>();
        for (Coords c: coords) if (board.contains(c)) finalCoords.add(c);
        return finalCoords;
    }

    // Helper to shorten the code
    private void addManyActionListeners(JButton... buttons) {
        for (JButton button: buttons) button.addActionListener(this);
    }
    
    // Helper to shorten the code
    private void addManyButtons(JPanel panel, JButton... buttons) {
        for (JButton button: buttons) panel.add(button);
    }
    
    // Helper to shorten the code
    private void addManyButtons(JPanel panel, ArrayList<JButton> buttonList) {
        for (JButton button: buttonList) panel.add(button);
    }
    
    // Helper to shorten the code
    private void addManyTButtons(JPanel panel, ArrayList<JToggleButton> buttonList) {
        for (JToggleButton button: buttonList) panel.add(button);
    }
    
    /**
     * Save the hex at c into the current undo Set
     */
    private void saveToUndo(Coords c) {
        // Create a new set of hexes to save for undoing
        // This will be filled as long as the mouse is dragged
        if (currentUndoSet == null) {
            currentUndoSet = new HashSet<>();
            currentUndoCoords = new HashSet<>();
        }
        if (!currentUndoCoords.contains(c)) {
            IHex hex = board.getHex(c).duplicate();
            // Newly drawn board hexes do not know their Coords
            hex.setCoords(c);
            currentUndoSet.add(hex);
            currentUndoCoords.add(c);
        }
    }
    
    private void resetUndo() {
        currentUndoSet = null;
        currentUndoCoords = null;
        undoStack.clear();
        redoStack.clear();
        buttonUndo.setEnabled(false);
        buttonRedo.setEnabled(false);
    }
    
    /**
     * Changes the hex level at Coords c. Expects c 
     * to be on the board.
     */
    private void relevelHex(Coords c) {
        IHex newHex = board.getHex(c).duplicate(); 
        newHex.setLevel(hexLeveltoDraw);
        board.resetStoredElevation();
        board.setHex(c, newHex);
        
    }

    /**
     * Apply the current Hex to the Board at the specified location.
     */
    void paintHex(Coords c) {
        board.resetStoredElevation();
        board.setHex(c, curHex.duplicate());
    } 
    
    /**
     * Apply the current Hex to the Board at the specified location.
     */
    public void retextureHex(Coords c) {
        if (board.contains(c)) {
            IHex newHex = curHex.duplicate();
            newHex.setLevel(board.getHex(c).getLevel());
            board.resetStoredElevation();
            board.setHex(c, newHex);
        }
    }

    /**
     * Apply the current Hex to the Board at the specified location.
     */
    public void addToHex(Coords c) {
        if (board.contains(c)) {
            IHex newHex = curHex.duplicate();
            IHex oldHex = board.getHex(c);
            newHex.setLevel(oldHex.getLevel());
            int[] terrainTypes = oldHex.getTerrainTypes();
            for (int terrainID : terrainTypes) {
                if (!newHex.containsTerrain(terrainID) && oldHex.containsTerrain(terrainID)) {
                    newHex.addTerrain(oldHex.getTerrain(terrainID));
                }
            }
            board.resetStoredElevation();
            board.setHex(c, newHex);
        }
    }

    /**
     * Sets the working hex to <code>hex</code>;
     * used for mouse ALT-click (eyedropper function).
     *
     * @param hex hex to set.
     */
    void setCurrentHex(IHex hex) {
        curHex = hex.duplicate();
        texElev.setText(Integer.toString(curHex.getLevel()));
        refreshTerrainList();
        if (lisTerrain.getModel().getSize() > 0) {
            lisTerrain.setSelectedIndex(0);
            refreshTerrainFromList();
        }
        choTheme.setSelectedItem(curHex.getTheme());
        repaint();
        repaintWorkingHex();
    }

    private void repaintWorkingHex() {
        if (curHex != null) {
            TilesetManager tm = bv.getTilesetManager();
            tm.clearHex(curHex);
        }
        canHex.repaint();
        lastClicked = null;
    }

    /**
     * Refreshes the terrain list to match the current hex
     */
    private void refreshTerrainList() {
        
        ((DefaultListModel<TerrainTypeHelper>)lisTerrain.getModel()).removeAllElements();
        lisTerrainRenderer.setTerrainTypes(null);
        int[] terrainTypes = curHex.getTerrainTypes();
        List<TerrainTypeHelper> types = new ArrayList<>();
        for (int terrainType : terrainTypes) {
            ITerrain terrain = curHex.getTerrain(terrainType);
            if (terrain != null) {
                TerrainTypeHelper tth = new TerrainTypeHelper(terrain);
                types.add(tth);
            }
        }
        Collections.sort(types);
        for (TerrainTypeHelper tth : types) {
            ((DefaultListModel<TerrainTypeHelper>) lisTerrain.getModel()).addElement(tth);
        }
        lisTerrainRenderer.setTerrainTypes(types);
    }

    /**
     * Returns a new instance of the terrain that is currently entered in the
     * terrain input fields
     */
    private ITerrain enteredTerrain() {
        int type = ((TerrainHelper)choTerrainType.getSelectedItem()).getTerrainType();
        int level = texTerrainLevel.getNumber();  
        // For the terrain subtypes that only add to a main terrain type exits make no
        // sense at all. Therefore simply do not add them
        if ((type == Terrains.BLDG_ARMOR) || (type == Terrains.BLDG_CF) 
                || (type == Terrains.BLDG_ELEV) || (type == Terrains.BLDG_CLASS)  
                || (type == Terrains.BLDG_BASE_COLLAPSED) || (type == Terrains.BLDG_BASEMENT_TYPE)
                || (type == Terrains.BRIDGE_CF) || (type == Terrains.BRIDGE_ELEV)
                || (type == Terrains.FUEL_TANK_CF) || (type == Terrains.FUEL_TANK_ELEV)
                || (type == Terrains.FUEL_TANK_MAGN)) 
        {
            return Terrains.getTerrainFactory().createTerrain(type, level, false, 0);
        } else {
            boolean exitsSpecified = cheTerrExitSpecified.isSelected();
            int exits = texTerrExits.getNumber();
            return Terrains.getTerrainFactory().createTerrain(type, level, exitsSpecified, exits);
        }
    }

    /**
     * Add or set the terrain to the list based on the fields.
     */
    private void addSetTerrain() {
        ITerrain toAdd = enteredTerrain();
        if (((toAdd.getType() == Terrains.BLDG_ELEV) 
                || (toAdd.getType() == Terrains.BRIDGE_ELEV))
                && toAdd.getLevel() < 0) {
            texTerrainLevel.setNumber(0);
            JOptionPane.showMessageDialog(frame,
                    Messages.getString("BoardEditor.BridgeBuildingElevError"), //$NON-NLS-1$
                    Messages.getString("BoardEditor.invalidTerrainTitle"), //$NON-NLS-1$ 
                    JOptionPane.ERROR_MESSAGE);
            return;
        }
        curHex.addTerrain(toAdd);
        int formerSelection = lisTerrain.getSelectedIndex();
        noTextFieldUpdate = true;
        refreshTerrainList();
        lisTerrain.setSelectedIndex(formerSelection);
        lisTerrain.ensureIndexIsVisible(formerSelection);
        repaintWorkingHex();
        noTextFieldUpdate = false;
    }
    
    /**
     * Add to the terrain from one of the easy access buttons
     */
    private void addSetTerrainEasy(int type, int level) {
        boolean exitsSpecified = cheTerrExitSpecified.isSelected();
        int exits = texTerrExits.getNumber();
        ITerrain toAdd = Terrains.getTerrainFactory().createTerrain(type, level, exitsSpecified, exits);
        curHex.addTerrain(toAdd);
        TerrainTypeHelper toSelect = new TerrainTypeHelper(toAdd);
        refreshTerrainList();
        lisTerrain.setSelectedValue(toSelect, true);
        repaintWorkingHex();
    }
    
    /**
     * Sets valid basic Fuel Tank values as far as they are missing
     */
    private void setBasicFuelTank() {
        // There is only fuel_tank:1, so this can be set
        curHex.addTerrain(TF.createTerrain(Terrains.FUEL_TANK, 1, true, 0));

        if (!curHex.containsTerrain(Terrains.FUEL_TANK_CF)) 
         curHex.addTerrain(TF.createTerrain(Terrains.FUEL_TANK_CF, 40, false, 0));
        
        if (!curHex.containsTerrain(Terrains.FUEL_TANK_ELEV)) 
            curHex.addTerrain(TF.createTerrain(Terrains.FUEL_TANK_ELEV, 1, false, 0));
        
        if (!curHex.containsTerrain(Terrains.FUEL_TANK_MAGN)) 
            curHex.addTerrain(TF.createTerrain(Terrains.FUEL_TANK_MAGN, 100, false, 0));
        
        refreshTerrainList();
        repaintWorkingHex();
    }
    
    /**
     * Sets valid basic bridge values as far as they are missing
     */
    private void setBasicBridge() {
        if (!curHex.containsTerrain(Terrains.BRIDGE_CF)) 
         curHex.addTerrain(TF.createTerrain(Terrains.BRIDGE_CF, 40, false, 0));
        
        if (!curHex.containsTerrain(Terrains.BRIDGE_ELEV)) 
            curHex.addTerrain(TF.createTerrain(Terrains.BRIDGE_ELEV, 1, false, 0));
        
        if (!curHex.containsTerrain(Terrains.BRIDGE)) 
            curHex.addTerrain(TF.createTerrain(Terrains.BRIDGE, 1, false, 0));
        
        refreshTerrainList();
        repaintWorkingHex();
    }
    
    /**
     * Sets valid basic Building values as far as they are missing
     */
    private void setBasicBuilding(boolean ALT_Held) {
        if (!curHex.containsTerrain(Terrains.BLDG_CF)) 
            curHex.addTerrain(TF.createTerrain(Terrains.BLDG_CF, 15, false, 0));

        if (!curHex.containsTerrain(Terrains.BLDG_ELEV)) 
            curHex.addTerrain(TF.createTerrain(Terrains.BLDG_ELEV, 1, false, 0));

        if (!curHex.containsTerrain(Terrains.BUILDING))
            curHex.addTerrain(TF.createTerrain(Terrains.BUILDING, 1, ALT_Held, 0));

        // When clicked with ALT and a Building is present, only toggle the exits
        if (curHex.containsTerrain(Terrains.BUILDING) && ALT_Held) {
            ITerrain curTerr = curHex.getTerrain(Terrains.BUILDING);
            curHex.addTerrain(TF.createTerrain(Terrains.BUILDING, 
                    curTerr.getLevel(), !curTerr.hasExitsSpecified(), curTerr.getExits()));
        }

        refreshTerrainList();
        repaintWorkingHex();
    }

    /**
     * Set all the appropriate terrain field to match the currently selected
     * terrain in the list.
     */
    private void refreshTerrainFromList() {
        if (lisTerrain.isSelectionEmpty()) {
            butDelTerrain.setEnabled(false);
        } else {
            butDelTerrain.setEnabled(true);
            ITerrain terrain = Terrains.getTerrainFactory().createTerrain(
                    lisTerrain.getSelectedValue().getTerrain());
            terrain = curHex.getTerrain(terrain.getType());
            TerrainHelper terrainHelper = new TerrainHelper(terrain.getType());
            terrListBlocker = true;
            choTerrainType.setSelectedItem(terrainHelper);
            texTerrainLevel.setText(Integer.toString(terrain.getLevel()));
            cheTerrExitSpecified.setSelected(terrain.hasExitsSpecified());
            texTerrExits.setNumber(terrain.getExits());
            terrListBlocker = false;
        }
    }

    /**
     * Updates the selected terrain in the terrain list if
     * a terrain is actually selected
     */
    private void updateWhenSelected() {
        if (!lisTerrain.isSelectionEmpty())
            addSetTerrain();
    }

    public void boardNew(boolean showDialog) {
        boolean userCancel = false;
        if (showDialog) {
            RandomMapDialog rmd = new RandomMapDialog(frame, this, null, mapSettings);
            userCancel = rmd.activateDialog(bv.getTilesetManager().getThemes());
        }
        if (!userCancel) {
            board = BoardUtilities.generateRandom(mapSettings);
            game.setBoard(board);
            curfile = null;
            choTheme.setSelectedItem(mapSettings.getTheme());
            setupUiFreshBoard();
        }
    }

    public void boardResize() {
        ResizeMapDialog emd = new ResizeMapDialog(frame, this, null, mapSettings);
        boolean userCancel = emd.activateDialog(bv.getTilesetManager().getThemes());
        if (!userCancel) {
            board = BoardUtilities.generateRandom(mapSettings);

            // Implant the old board
            int west = emd.getExpandWest();
            int north = emd.getExpandNorth();
            int east = emd.getExpandEast();
            int south = emd.getExpandSouth();
            board = implantOldBoard(game, west, north, east, south);

            game.setBoard(board);
            curfile = null;
            setupUiFreshBoard();
        }
    }

    // When we resize a board, implant the old board's hexes where they should be in the new board
    public IBoard implantOldBoard(IGame game, int west, int north, int east, int south) {
        IBoard oldBoard = game.getBoard();
        for (int x = 0; x < oldBoard.getWidth(); x++) {
            for (int y = 0; y < oldBoard.getHeight(); y++) {
                int newX = x+west;
                int odd = x & 1 & west;
                int newY = y+north + odd;
                if (oldBoard.contains(x, y) && board.contains(newX, newY)) {
                    IHex oldHex = oldBoard.getHex(x, y);
                    IHex hex = board.getHex(newX, newY);
                    hex.removeAllTerrains();
                        hex.setLevel(oldHex.getLevel());
                    int[] terrainTypes = oldHex.getTerrainTypes();
                    for (int terrainID : terrainTypes) {
                        if (!hex.containsTerrain(terrainID) &&
                                oldHex.containsTerrain(terrainID)) {
                            hex.addTerrain(oldHex.getTerrain(terrainID));
                        }
                    }
                    hex.setTheme(oldHex.getTheme());
                    board.setHex(newX, newY, hex);
                    board.resetStoredElevation();
                }
            }
        }
        return board;
    }

    public void updateMapSettings(MapSettings newSettings) {
        mapSettings = newSettings;
    }

    public void boardLoad() {
        JFileChooser fc = new JFileChooser(loadPath);
        setDialogSize(fc);
        fc.setDialogTitle(Messages.getString("BoardEditor.loadBoard"));
        fc.setFileFilter(new FileFilter() {
            @Override
            public boolean accept(File dir) {
                return (dir.getName().endsWith(".board") || dir.isDirectory()); //$NON-NLS-1$
            }

            @Override
            public String getDescription() {
                return "*.board";
            }
        });
        ignoreHotKeys = true;
        int returnVal = fc.showOpenDialog(frame);
        ignoreHotKeys = false;
        saveDialogSize(fc);
        if ((returnVal != JFileChooser.APPROVE_OPTION) || (fc.getSelectedFile() == null)) {
            // I want a file, y'know!
            return;
        }
        curfile = fc.getSelectedFile();
        loadPath = curfile.getPath();
        // load!
        try (InputStream is = new FileInputStream(fc.getSelectedFile())) {            
            // tell the board to load!
            StringBuffer errBuff = new StringBuffer();
            board.load(is, errBuff, true);
            if (errBuff.length() > 0) {
                showBoardValidationReport(errBuff);
            }
            // Board generation in a game always calls BoardUtilities.combine
            // This serves no purpose here, but is necessary to create 
            // flipBGVert/flipBGHoriz lists for the board, which is necessary 
            // for the background image to work in the BoardEditor
            board = BoardUtilities.combine(board.getWidth(), board.getHeight(), 1, 1, 
                    new IBoard[]{board}, Collections.singletonList(false), MapSettings.MEDIUM_GROUND);
            game.setBoard(board);
            cheRoadsAutoExit.setSelected(board.getRoadsAutoExit());
            mapSettings.setBoardSize(board.getWidth(), board.getHeight());
            refreshTerrainList();
            setupUiFreshBoard();
        } catch (IOException ex) {
            System.err.println("error opening file to load!"); //$NON-NLS-1$
            System.err.println(ex);
        }
    }
    
    /**
     * Will do board.initializeHex() for all hexes, correcting 
     * building and road connection issues for those hexes that do not have
     * the exits check set.
     */
    private void correctExits() {
        for (int x = 0; x < board.getWidth(); x++) {
            for (int y = 0; y < board.getHeight(); y++) {
                board.initializeHex(x, y);
            }
        }
    }

    /**
     * Checks to see if there is already a path and name stored; if not, calls
     * "save as"; otherwise, saves the board to the specified file.
     */
    private boolean boardSave() {
        // First, correct connection issues and do a validation.
        correctExits();
        StringBuffer errBuff = new StringBuffer();
        board.isValid(errBuff);
        if (errBuff.length() > 0) {
            showBoardValidationReport(errBuff);
        }
        if (curfile == null) {
            return boardSaveAs();
        }
        // save!
        try {
            OutputStream os = new FileOutputStream(curfile);
            board.save(os);// tell the board to save!
            os.close(); // okay, done!
            savedUndoStackSize = undoStack.size();
            hasChanges = false;
            setFrameTitle();
            return true;
        } catch (IOException ex) {
            System.err.println("error opening file to save!"); //$NON-NLS-1$
            System.err.println(ex);
            return false;
        }
    }

    /**
     * Saves the board in PNG image format.
     */
    private void boardSaveImage(boolean ignoreUnits) {
        if (curfileImage == null) {
            boardSaveAsImage(ignoreUnits);
            return;
        }
        JDialog waitD = new JDialog(frame, Messages.getString("BoardEditor.waitDialog.title")); //$NON-NLS-1$
        waitD.add(new JLabel(Messages.getString("BoardEditor.waitDialog.message"))); //$NON-NLS-1$
        waitD.setSize(250, 130);
        // move to middle of screen
        waitD.setLocation(
                (frame.getSize().width / 2) - (waitD.getSize().width / 2),
                (frame.getSize().height / 2) - (waitD.getSize().height / 2));
        waitD.setVisible(true);
        frame.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
        waitD.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
        // save!
        try {
            ImageIO.write(bv.getEntireBoardImage(ignoreUnits), "png", curfileImage);
        } catch (IOException e) {
            e.printStackTrace();
        }
        waitD.setVisible(false);
        frame.setCursor(Cursor.getDefaultCursor());
    }

    /**
     * Opens a file dialog box to select a file to save as; saves the board to
     * the file.
     */
    private boolean boardSaveAs() {
        // First, correct connection issues
        correctExits();
        JFileChooser fc = new JFileChooser("data" + File.separator + "boards");
        setDialogSize(fc);
        fc.setLocation(frame.getLocation().x + 150, frame.getLocation().y + 100);
        fc.setDialogTitle(Messages.getString("BoardEditor.saveBoardAs"));
        fc.setFileFilter(new FileFilter() {
            @Override
            public boolean accept(File dir) {
                return (dir.getName().endsWith(".board") || dir.isDirectory()); //$NON-NLS-1$
            }

            @Override
            public String getDescription() {
                return "*.board";
            }
        });
        int returnVal = fc.showSaveDialog(frame);
        saveDialogSize(fc);
        if ((returnVal != JFileChooser.APPROVE_OPTION) || (fc.getSelectedFile() == null)) {
            return false; // I want a file, y'know!
        }
        curfile = fc.getSelectedFile();
        butSourceFile.setEnabled(true);

        // make sure the file ends in board
        if (!curfile.getName().toLowerCase().endsWith(".board")) { //$NON-NLS-1$
            try {
                curfile = new File(curfile.getCanonicalPath() + ".board"); //$NON-NLS-1$
            } catch (IOException ie) {
                // failure!
                return false;
            }
        }
        return boardSave();
    }

    /**
     * Opens a file dialog box to select a file to save as; saves the board to
     * the file as an image. Useful for printing boards.
     */
    private void boardSaveAsImage(boolean ignoreUnits) {
        JFileChooser fc = new JFileChooser(".");
        setDialogSize(fc);
        fc.setLocation(frame.getLocation().x + 150, frame.getLocation().y + 100);
        fc.setDialogTitle(Messages.getString("BoardEditor.saveAsImage"));
        fc.setFileFilter(new FileFilter() {
            @Override
            public boolean accept(File dir) {
                return (dir.getName().endsWith(".png") || dir.isDirectory()); //$NON-NLS-1$
            }

            @Override
            public String getDescription() {
                return ".png";
            }
        });
        int returnVal = fc.showSaveDialog(frame);
        saveDialogSize(fc);
        if ((returnVal != JFileChooser.APPROVE_OPTION)
            || (fc.getSelectedFile() == null)) {
            // I want a file, y'know!
            return;
        }
        curfileImage = fc.getSelectedFile();

        // make sure the file ends in png
        if (!curfileImage.getName().toLowerCase().endsWith(".png")) { //$NON-NLS-1$
            try {
                curfileImage = new File(curfileImage.getCanonicalPath() + ".png"); //$NON-NLS-1$
            } catch (IOException ie) {
                // failure!
                return;
            }
        }
        boardSaveImage(ignoreUnits);
    }

    //
    // ItemListener
    //
    public void itemStateChanged(ItemEvent ie) {
        if (ie.getSource().equals(cheRoadsAutoExit)) {
            // Set the new value for the option, and refresh the board.
            board.setRoadsAutoExit(cheRoadsAutoExit.isSelected());
            bv.updateBoard();
            repaintWorkingHex();
        }
    }

    //
    // TextListener
    //
    public void changedUpdate(DocumentEvent te) {
        if (te.getDocument().equals(texElev.getDocument())) {
            int value;
            try {
                value = Integer.parseInt(texElev.getText());
            } catch (NumberFormatException ex) {
                return;
            }
            if (value != curHex.getLevel()) {
                curHex.setLevel(value);
                repaintWorkingHex();
            }
        } else if (te.getDocument().equals(texTerrainLevel.getDocument())) {
            // prevent updating the terrain from looping back to
            // update the text fields that have just been edited
            if (!terrListBlocker) {
                noTextFieldUpdate = true;
                updateWhenSelected();
                noTextFieldUpdate = false;
            }
        } else if (te.getDocument().equals(texTerrExits.getDocument())) {
            // prevent updating the terrain from looping back to
            // update the text fields that have just been edited
            if (!terrListBlocker) {
                noTextFieldUpdate = true;
                cheTerrExitSpecified.setSelected(true);
                updateWhenSelected();
                noTextFieldUpdate = false;
            }
        }  
    }
    
    public void insertUpdate(DocumentEvent event) {
        changedUpdate(event);
    }

    public void removeUpdate(DocumentEvent event) {
        changedUpdate(event);
    }

    /** Called when the user selects the "Help->About" menu item. */
    private void showAbout() {
        // Do we need to create the "about" dialog?
        if (about == null) {
            about = new CommonAboutDialog(frame);
        }

        // Show the about dialog.
        about.setVisible(true);
    }

    /** Called when the user selects the "Help->Contents" menu item. */
    private void showHelp() {
        // Do we need to create the "help" dialog?
        if (help == null) {
            File helpFile = new File("docs\\Boards Stuff", "Map Editor-readme.txt"); //$NON-NLS-1$
            help = new CommonHelpDialog(frame, helpFile);
        }

        // Show the help dialog.
        help.setVisible(true);
    }

    /** Called when the user selects the "View->Client Settings" menu item. */
    private void showSettings() {
        // Do we need to create the "settings" dialog?
        if (setdlg == null) {
            setdlg = new CommonSettingsDialog(frame);
        }

        // Show the settings dialog.
        setdlg.setVisible(true);
    }
    
    /** 
     * Adjusts some UI and internal settings for a freshly 
     * loaded or freshly generated board.
     */
    private void setupUiFreshBoard() {
        // Reset the Undo stack and the board has no changes
        savedUndoStackSize = 0;
        canReturnToSaved = true;
        resetUndo();
        hasChanges = false;
        // When a board was loaded, we have a file, otherwise not
        butSourceFile.setEnabled(curfile != null);
        // Adjust the UI
        menuBar.setBoard(true);
        bvc.doLayout();
        setFrameTitle();
    }

    /**
     * Shows a board validation report dialog, reporting either
     * the contents of errBuff or that the board has no errors.
     */
    private void showBoardValidationReport(StringBuffer errBuff) {
        ignoreHotKeys = true;
        if ((errBuff != null) && errBuff.length() > 0) {
            String title = Messages.getString("BoardEditor.invalidBoard.title");
            String msg = Messages.getString("BoardEditor.invalidBoard.report");
            msg += errBuff;
            JTextArea textArea = new JTextArea(msg);
            JScrollPane scrollPane = new JScrollPane(textArea);
            textArea.setLineWrap(true);
            textArea.setWrapStyleWord(true);
            scrollPane.setPreferredSize(new Dimension(getWidth(), getHeight() / 2));
            JOptionPane.showMessageDialog(frame, scrollPane, title, JOptionPane.ERROR_MESSAGE);
        } else {
            String title =  Messages.getString("BoardEditor.validBoard.title");
            String msg = Messages.getString("BoardEditor.validBoard.report");
            JOptionPane.showMessageDialog(frame, msg, title, JOptionPane.INFORMATION_MESSAGE);
        }
        ignoreHotKeys = false;
    }

    //
    // ActionListener
    //
    public void actionPerformed(ActionEvent ae) {
        if (ae.getActionCommand().equals(ClientGUI.FILE_BOARD_NEW)) {
            ignoreHotKeys = true;
            boardNew(true);
            ignoreHotKeys = false;
        } else if (ae.getActionCommand().equals(FILE_BOARD_EDITOR_EXPAND)) {
            ignoreHotKeys = true;
            boardResize();
            ignoreHotKeys = false;
        } else if (ae.getActionCommand().equals(ClientGUI.FILE_BOARD_OPEN)) {
            ignoreHotKeys = true;
            boardLoad();
            ignoreHotKeys = false;
        } else if (ae.getActionCommand().equals(ClientGUI.FILE_BOARD_SAVE)) {
            ignoreHotKeys = true;
            boardSave();
            ignoreHotKeys = false;
        } else if (ae.getActionCommand().equals(ClientGUI.FILE_BOARD_SAVE_AS)) {
            ignoreHotKeys = true;
            boardSaveAs();
            ignoreHotKeys = false;
        } else if (ae.getActionCommand().equals(ClientGUI.FILE_BOARD_SAVE_AS_IMAGE)) {
            ignoreHotKeys = true;
            boardSaveAsImage(false);
            ignoreHotKeys = false;
        } else if (ae.getActionCommand().equals(FILE_SOURCEFILE)) {
            if (curfile != null) {
                try {
                    Desktop.getDesktop().open(curfile);
                } catch (IOException e) {
                    ignoreHotKeys = true;
                    JOptionPane.showMessageDialog(
                            frame,
                            Messages.getString("BoardEditor.OpenFileError", curfile.toString())
                             + e.getMessage());
                    e.printStackTrace();
                    ignoreHotKeys = false;
                }
            }
        } else if (ae.getActionCommand().equals(FILE_BOARD_EDITOR_VALIDATE)) {
            correctExits();
            StringBuffer errBuff = new StringBuffer();
            board.isValid(errBuff);
            showBoardValidationReport(errBuff);
        } else if (ae.getSource().equals(butDelTerrain)
                   && (!lisTerrain.isSelectionEmpty())) {
            ITerrain toRemove = Terrains.getTerrainFactory().createTerrain(
                    lisTerrain.getSelectedValue().getTerrain());
            curHex.removeTerrain(toRemove.getType());
            refreshTerrainList();
            repaintWorkingHex();
        } else if (ae.getSource().equals(butAddTerrain)) {
            addSetTerrain();
        } else if (ae.getSource().equals(butElevUp) && (curHex.getLevel() < 9)) {
            curHex.setLevel(curHex.getLevel() + 1);
            texElev.incValue();
            repaintWorkingHex();
        } else if (ae.getSource().equals(butElevDown) && (curHex.getLevel() > -5)) {
            curHex.setLevel(curHex.getLevel() - 1);
            texElev.decValue();
            repaintWorkingHex();
        } else if (ae.getSource().equals(butTerrUp)) {
            texTerrainLevel.incValue();
            updateWhenSelected();
        } else if (ae.getSource().equals(butTerrDown)) {
            texTerrainLevel.decValue();
            updateWhenSelected();
        } else if (ae.getSource().equals(texTerrainLevel)) {
            updateWhenSelected();
        } else if (ae.getSource().equals(texTerrExits)) {
            int exitsVal = texTerrExits.getNumber();
            if (exitsVal == 0) {
                cheTerrExitSpecified.setSelected(false);
            } else if (exitsVal > 63) {
                texTerrExits.setNumber(63);
            }
            updateWhenSelected();
        } else if (ae.getSource().equals(butTerrExits)) {
            ExitsDialog ed = new ExitsDialog(frame);
            int exitsVal = texTerrExits.getNumber();
            ed.setExits(exitsVal);
            ed.setVisible(true);
            exitsVal = ed.getExits();
            texTerrExits.setNumber(exitsVal);
            cheTerrExitSpecified.setSelected(exitsVal != 0);
            updateWhenSelected();
        } else if (ae.getSource().equals(butExitUp)) {
            cheTerrExitSpecified.setSelected(true);
            if (texTerrExits.getNumber() < 63) {
                texTerrExits.incValue();
            }
            updateWhenSelected();
        } else if (ae.getSource().equals(butExitDown)) {
            texTerrExits.decValue();
            cheTerrExitSpecified.setSelected(texTerrExits.getNumber() != 0);
            updateWhenSelected();
        } else if (ae.getActionCommand().equals(ClientGUI.VIEW_MINI_MAP)) {
            minimapW.setVisible(!minimapW.isVisible());
        } else if (ae.getActionCommand().equals(ClientGUI.HELP_ABOUT)) {
            showAbout();
        } else if (ae.getActionCommand().equals(ClientGUI.HELP_CONTENTS)) {
            showHelp();
        } else if (ae.getActionCommand().equals(ClientGUI.VIEW_CLIENT_SETTINGS)) {
            showSettings();
        } else if (ae.getActionCommand().equals(ClientGUI.VIEW_ZOOM_IN)) {
            bv.zoomIn();
        } else if (ae.getActionCommand().equals(ClientGUI.VIEW_ZOOM_OUT)) {
            bv.zoomOut();
        } else if (ae.getActionCommand().equals(ClientGUI.VIEW_TOGGLE_ISOMETRIC)) {
            bv.toggleIsometric();
        } else if (ae.getActionCommand().equals(ClientGUI.VIEW_CHANGE_THEME)) {
            bv.changeTheme();
        } else if (ae.getSource().equals(choTheme) ) { 
            curHex.setTheme((String)choTheme.getSelectedItem());
            repaintWorkingHex();
        } else if (ae.getSource().equals(buttonLW)) {
            if ((ae.getModifiers() & InputEvent.SHIFT_MASK) == 0) {
                curHex.removeAllTerrains();
            }  
            buttonUpDn.setSelected(false);
            addSetTerrainEasy(Terrains.WOODS, 1);
            
        } else if (ae.getSource().equals(buttonMg)) {
            if ((ae.getModifiers() & InputEvent.SHIFT_MASK) == 0) {
                curHex.removeAllTerrains();
            }
            buttonUpDn.setSelected(false);
            addSetTerrainEasy(Terrains.MAGMA, 1);
            
        } else if (ae.getSource().equals(buttonLJ)) {
            if ((ae.getModifiers() & InputEvent.SHIFT_MASK) == 0) {
                curHex.removeAllTerrains();
            }
            buttonUpDn.setSelected(false);
            addSetTerrainEasy(Terrains.JUNGLE, 1);
            
        } else if (ae.getSource().equals(buttonWa)) {
            buttonUpDn.setSelected(false);
            if ((ae.getModifiers() & InputEvent.CTRL_MASK) != 0) {
                if (curHex.containsTerrain(Terrains.RAPIDS, 1))
                    addSetTerrainEasy(Terrains.RAPIDS, 2);
                else
                    addSetTerrainEasy(Terrains.RAPIDS, 1);
                if (!curHex.containsTerrain(Terrains.WATER) ||
                        curHex.getTerrain(Terrains.WATER).getLevel() == 0)
                    addSetTerrainEasy(Terrains.WATER, 1);
            } else {
                if ((ae.getModifiers() & InputEvent.SHIFT_MASK) == 0) {
                    curHex.removeAllTerrains();
                }
                addSetTerrainEasy(Terrains.WATER, 1);
            }
            
        } else if (ae.getSource().equals(buttonSw)) {
            if ((ae.getModifiers() & InputEvent.SHIFT_MASK) == 0) {
                curHex.removeAllTerrains();
            }
            buttonUpDn.setSelected(false);
            addSetTerrainEasy(Terrains.SWAMP, 1);
            
        } else if (ae.getSource().equals(buttonRo)) {
            if ((ae.getModifiers() & InputEvent.SHIFT_MASK) == 0) {
                curHex.removeAllTerrains();
            }
            buttonUpDn.setSelected(false);
            addSetTerrainEasy(Terrains.ROUGH, 1);
            
        } else if (ae.getSource().equals(buttonPv)) {
            if ((ae.getModifiers() & InputEvent.SHIFT_MASK) == 0) {
                curHex.removeAllTerrains();
            }
            buttonUpDn.setSelected(false);
            addSetTerrainEasy(Terrains.PAVEMENT, 1);
            
        } else if (ae.getSource().equals(buttonMd)) {
            if ((ae.getModifiers() & InputEvent.SHIFT_MASK) == 0) {
                curHex.removeAllTerrains();
            }
            buttonUpDn.setSelected(false);
            addSetTerrainEasy(Terrains.MUD, 1);
        } else if (ae.getSource().equals(buttonTu)) {
            if ((ae.getModifiers() & InputEvent.SHIFT_MASK) == 0) {
                curHex.removeAllTerrains();
            }
            buttonUpDn.setSelected(false);
            addSetTerrainEasy(Terrains.TUNDRA, 1);
            
        } else if (ae.getSource().equals(buttonIc)) {
            if ((ae.getModifiers() & InputEvent.SHIFT_MASK) == 0) {
                curHex.removeAllTerrains();
            }
            buttonUpDn.setSelected(false);
            addSetTerrainEasy(Terrains.ICE, 1);
            
        } else if (ae.getSource().equals(buttonSn)) {
            if ((ae.getModifiers() & InputEvent.SHIFT_MASK) == 0) {
                curHex.removeAllTerrains();
            }
            buttonUpDn.setSelected(false);
            addSetTerrainEasy(Terrains.SNOW, 1);
            
        } else if (ae.getSource().equals(buttonCl)) {
            curHex.removeAllTerrains();
            refreshTerrainList();
            repaintWorkingHex();
            buttonUpDn.setSelected(false);
        } else if (ae.getSource().equals(buttonBrush1)) {
            brushSize = 1;
            lastClicked = null;
        } else if (ae.getSource().equals(buttonBrush2)) {
            brushSize = 2;
            lastClicked = null;
        } else if (ae.getSource().equals(buttonBrush3)) {
            brushSize = 3;
            lastClicked = null;
        } else if (ae.getSource().equals(buttonBu)) { 
            buttonUpDn.setSelected(false);
            if ((ae.getModifiers() & InputEvent.SHIFT_MASK) == 0 && (ae.getModifiers() & InputEvent.ALT_MASK) == 0) 
                curHex.removeAllTerrains();
            if ((ae.getModifiers() & InputEvent.ALT_MASK) != 0) {
                setBasicBuilding(true);
            } else {
                setBasicBuilding(false);
            }
        } else if (ae.getSource().equals(buttonBr)) {
            if ((ae.getModifiers() & InputEvent.SHIFT_MASK) == 0) {
                curHex.removeAllTerrains();
            }
            buttonUpDn.setSelected(false);
            setBasicBridge();
            
        } else if (ae.getSource().equals(buttonFT)) {
            if ((ae.getModifiers() & InputEvent.SHIFT_MASK) == 0) {
                curHex.removeAllTerrains();
            }
            buttonUpDn.setSelected(false);
            setBasicFuelTank();
            
        } else if (ae.getSource().equals(buttonRd)) {
            if ((ae.getModifiers() & InputEvent.SHIFT_MASK) == 0) {
                curHex.removeAllTerrains();
            }
            buttonUpDn.setSelected(false);
            addSetTerrainEasy(Terrains.ROAD, 1);
            
        } else if (ae.getSource().equals(buttonUpDn)) {
            // Not so useful to only do on clear terrain
            buttonOOC.setSelected(false);
            
        } else if (ae.getSource().equals(buttonUndo)) {
            // The button should not be active when the stack is empty, but
            // let's check nevertheless
            if (undoStack.isEmpty()) { 
                buttonUndo.setEnabled(false);
            } else {
                HashSet<IHex> recentHexes = undoStack.pop();
                HashSet<IHex> redoHexes = new HashSet<>(); 
                for (IHex hex: recentHexes) {
                    // Retrieve the board hex for Redo
                    IHex rHex = board.getHex(hex.getCoords()).duplicate();
                    rHex.setCoords(hex.getCoords());
                    redoHexes.add(rHex);
                    // and undo the board hex
                    board.setHex(hex.getCoords(), hex);
                }
                redoStack.push(redoHexes);
                if (undoStack.isEmpty()) {
                    buttonUndo.setEnabled(false);
                }
                hasChanges = !canReturnToSaved | (undoStack.size() != savedUndoStackSize);
                buttonRedo.setEnabled(true);
                currentUndoSet = null; // should be anyway
            }
            setFrameTitle();
            
        } else if (ae.getSource().equals(buttonRedo)) {
            // The button should not be active when the stack is empty, but
            // let's check nevertheless
            if (redoStack.isEmpty()) { 
                buttonRedo.setEnabled(false); 
            } else {
                HashSet<IHex> recentHexes = redoStack.pop();
                HashSet<IHex> undoHexes = new HashSet<>(); 
                for (IHex hex: recentHexes) {
                    IHex rHex = board.getHex(hex.getCoords()).duplicate();
                    rHex.setCoords(hex.getCoords());
                    undoHexes.add(rHex);
                    board.setHex(hex.getCoords(), hex);
                }
                undoStack.push(undoHexes);
                if (redoStack.isEmpty()) buttonRedo.setEnabled(false);
                buttonUndo.setEnabled(true);
                hasChanges = !canReturnToSaved | (undoStack.size() != savedUndoStackSize);
                currentUndoSet = null; // should be anyway
            }
            setFrameTitle();
        }
    }

    public void valueChanged(ListSelectionEvent event) {
        if (event.getValueIsAdjusting()) {
            return;
        }
        if (event.getSource().equals(lisTerrain)) {
            if (!noTextFieldUpdate)
                refreshTerrainFromList();
        }
    }

    /**
     * Displays the currently selected hex picture, in component form
     */
    private class HexCanvas extends JPanel {
        /**
         *
         */
        private static final long serialVersionUID = 3201928357525361191L;

        HexCanvas() {
            setPreferredSize(new Dimension(90, 90));
        }
        
        /** Returns list or an empty list when list is null. */
        private List<Image> safeList(List<Image> list) {
            return list == null ? Collections.emptyList() : list;
        }

        @Override
        public void paintComponent(Graphics g) {
            super.paintComponent(g);
            if (curHex != null) {
                // draw the terrain images
                TilesetManager tm = bv.getTilesetManager();
                g.drawImage(tm.baseFor(curHex), 0, 0, BoardView1.HEX_W, BoardView1.HEX_H, this);
                for (final Image newVar : safeList(tm.supersFor(curHex))) {
                    g.drawImage(newVar, 0, 0, this);
                }
                for (final Image newVar : safeList(tm.orthoFor(curHex))) {
                    g.drawImage(newVar, 0, 0, this);
                }
                // add level and INVALID if necessary
                if (guip.getAntiAliasing()) {
                    ((Graphics2D) g).setRenderingHint(
                            RenderingHints.KEY_ANTIALIASING,
                            RenderingHints.VALUE_ANTIALIAS_ON);
                }
                g.setColor(getForeground());
                g.setFont(new Font("SansSerif", Font.PLAIN, 9)); //$NON-NLS-1$
                g.drawString(Messages.getString("BoardEditor.LEVEL") + curHex.getLevel(), 24, 70); //$NON-NLS-1$
                StringBuffer errBuf = new StringBuffer();
                if (!curHex.isValid(errBuf)) {
                    g.setFont(new Font("SansSerif", Font.BOLD, 14)); //$NON-NLS-1$
                    Point hexCenter = new Point(BoardView1.HEX_W / 2, BoardView1.HEX_H / 2);
                    bv.drawCenteredText((Graphics2D) g, 
                            Messages.getString("BoardEditor.INVALID"), //$NON-NLS-1$
                            hexCenter, 
                            guip.getWarningColor(),
                            false);
                    String tooltip = Messages.getString("BoardEditor.invalidHex") + errBuf; //$NON-NLS-1$
                    tooltip = tooltip.replace("\n", "<br>"); //$NON-NLS-1$ //$NON-NLS-2$
                    setToolTipText(tooltip);
                } else {
                    setToolTipText(null);
                }
            } else {
                g.clearRect(0, 0, 72, 72);
            }
        }

        // Make the hex stubborn when resizing the frame
        @Override
        public Dimension getPreferredSize() {
            return new Dimension(90, 90);
        }
        
        @Override
        public Dimension getMinimumSize() {
            return new Dimension(90, 90);
        }
    }

    /**
     * @return the frame this is displayed in
     */
    public JFrame getFrame() {
        return frame;
    }

    /**
     * Returns true if a dialog is visible on top of the <code>ClientGUI</code>.
     * For example, the <code>MegaMekController</code> should ignore hotkeys
     * if there is a dialog, like the <code>CommonSettingsDialog</code>, open.
     *
     * @return
     */
    public boolean shouldIgnoreHotKeys() {
        return ignoreHotKeys || (about != null && about.isVisible())
                || (help != null && help.isVisible())
                || (setdlg != null && setdlg.isVisible()) || texElev.hasFocus()
                || texTerrainLevel.hasFocus() || texTerrExits.hasFocus();
    }
    
    private void setDialogSize(JFileChooser dialog) {
        int width = guip.getBoardEditLoadWidth();
        int height = guip.getBoardEditLoadHeight();
        dialog.setPreferredSize(new Dimension(width, height));   
    }
    
    private void saveDialogSize(JComponent dialog) {
        guip.setBoardEditLoadHeight(dialog.getHeight());
        guip.setBoardEditLoadWidth(dialog.getWidth());
    }
    
    /** 
     *  Sets the Board Editor frame title, adding the current file name if any
     *  and a "*" if the board has unsaved changes.
     */
    private void setFrameTitle() {
        String title = Messages.getString("BoardEditor.title"); //$NON-NLS-1$
        if (curfile != null) {
            title = Messages.getString("BoardEditor.title0", curfile);  //$NON-NLS-1$ 
        }
        frame.setTitle(title + (hasChanges ? "*" : "")); //$NON-NLS-1$ //$NON-NLS-2$
    }
    
    
    /**
     * Specialized field for the BoardEditor that supports 
     * MouseWheel changes.
     * 
     * @author Simon
     */
    private class EditorTextField extends JTextField {

        /**
         * 
         */
        private static final long serialVersionUID = 4706926692515844105L;

        private int minValue = Integer.MIN_VALUE;
        
        /**
         * Creates an EditorTextField based on JTextField. This is a 
         * specialized field for the BoardEditor that supports 
         * MouseWheel changes.
         * 
         * @param text the initial text
         * @param columns as in JTextField
         * 
         * @see javax.swing.JTextField#JTextField(String, int)
         */
        public EditorTextField(String text, int columns) {
            super(text, columns);
            // Automatically select all text when clicking the text field
            addMouseListener(new MouseAdapter() {
                @Override public void mouseReleased(MouseEvent e) {
                    selectAll();
                }
            });

            addMouseWheelListener(new MouseWheelListener() {
                @Override
                public void mouseWheelMoved(MouseWheelEvent e) {
                    if (e.getWheelRotation() < 0)
                        incValue();
                    else 
                        decValue();
                }
            });
            setMargin(new Insets(1,1,1,1));
            setHorizontalAlignment(JTextField.CENTER);
            setFont(fontElev);
            setCursor(Cursor.getDefaultCursor());
        }
        
        /**
         * Creates an EditorTextField based on JTextField. This is a 
         * specialized field for the BoardEditor that supports 
         * MouseWheel changes.
         * 
         * @param text the initial text
         * @param columns as in JTextField
         * @param minimum a minimum value that the EditorTextField
         * will generally adhere to when its own methods are used
         * to change its value.
         * 
         * @see javax.swing.JTextField#JTextField(String, int)
         * 
         * @author Simon/Juliez
         */
        public EditorTextField(String text, int columns, int minimum) {
            this(text, columns);
            minValue = minimum;
        }
        
        /**
         * Increases the EditorTextField's number by one, if a number
         * is present.
         */
        public void incValue() {
            int newValue = getNumber() + 1;
            setNumber(newValue);
        }

        /**
         * Lowers the EditorTextField's number by one, if a number
         * is present and if that number is higher than the minimum
         * value.
         */
        public void decValue() {
            setNumber(getNumber() - 1);
        }

        /**
         * Sets the text to <code>newValue</code>. If <code>newValue</code> is lower
         * than the EditorTextField's minimum value, the minimum value will
         * be set instead.
         * 
         * @param newValue the value to be set
         */
        public void setNumber(int newValue) {
            int value = Math.max(newValue, minValue);
            setText(Integer.toString(value));
        }
        
        /**
         * Returns the text in the EditorTextField's as an int. 
         * Returns 0 when no parsable number (only letters) are present. 
         */
        public int getNumber() {
            try {
                return Integer.parseInt(getText());
            } catch (NumberFormatException ex) {
                return 0;
            }
        }
    }
}