package games.strategy.triplea.ui;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Image;
import java.awt.Insets;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.Vector;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.ButtonModel;
import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTabbedPane;
import javax.swing.JTextArea;
import javax.swing.JToggleButton;
import javax.swing.JToolTip;
import javax.swing.ListCellRenderer;
import javax.swing.ListSelectionModel;
import javax.swing.Popup;
import javax.swing.PopupFactory;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import javax.swing.border.EmptyBorder;
import javax.swing.border.EtchedBorder;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreeNode;

import com.google.common.base.Joiner;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;

import games.strategy.debug.ClientLogger;
import games.strategy.engine.chat.ChatPanel;
import games.strategy.engine.chat.PlayerChatRenderer;
import games.strategy.engine.data.Change;
import games.strategy.engine.data.GameData;
import games.strategy.engine.data.PlayerID;
import games.strategy.engine.data.ProductionRule;
import games.strategy.engine.data.RepairRule;
import games.strategy.engine.data.Resource;
import games.strategy.engine.data.ResourceCollection;
import games.strategy.engine.data.Route;
import games.strategy.engine.data.Territory;
import games.strategy.engine.data.TerritoryEffect;
import games.strategy.engine.data.Unit;
import games.strategy.engine.data.changefactory.ChangeFactory;
import games.strategy.engine.data.events.GameDataChangeListener;
import games.strategy.engine.data.events.GameStepListener;
import games.strategy.engine.framework.ClientGame;
import games.strategy.engine.framework.GameDataManager;
import games.strategy.engine.framework.GameDataUtils;
import games.strategy.engine.framework.GameRunner;
import games.strategy.engine.framework.HistorySynchronizer;
import games.strategy.engine.framework.IGame;
import games.strategy.engine.framework.LocalPlayers;
import games.strategy.engine.framework.ServerGame;
import games.strategy.engine.framework.startup.ui.InGameLobbyWatcherWrapper;
import games.strategy.engine.framework.system.SystemProperties;
import games.strategy.engine.gamePlayer.IGamePlayer;
import games.strategy.engine.gamePlayer.IPlayerBridge;
import games.strategy.engine.history.HistoryNode;
import games.strategy.engine.history.Renderable;
import games.strategy.engine.history.Round;
import games.strategy.engine.history.Step;
import games.strategy.sound.ClipPlayer;
import games.strategy.sound.SoundPath;
import games.strategy.thread.ThreadPool;
import games.strategy.triplea.Properties;
import games.strategy.triplea.TripleAPlayer;
import games.strategy.triplea.TripleAUnit;
import games.strategy.triplea.ai.proAI.ProAI;
import games.strategy.triplea.attachments.AbstractConditionsAttachment;
import games.strategy.triplea.attachments.AbstractTriggerAttachment;
import games.strategy.triplea.attachments.PoliticalActionAttachment;
import games.strategy.triplea.attachments.TerritoryAttachment;
import games.strategy.triplea.attachments.UnitAttachment;
import games.strategy.triplea.attachments.UserActionAttachment;
import games.strategy.triplea.delegate.AbstractEndTurnDelegate;
import games.strategy.triplea.delegate.AirThatCantLandUtil;
import games.strategy.triplea.delegate.BaseEditDelegate;
import games.strategy.triplea.delegate.BattleDelegate;
import games.strategy.triplea.delegate.GameStepPropertiesHelper;
import games.strategy.triplea.delegate.IBattle.BattleType;
import games.strategy.triplea.delegate.Matches;
import games.strategy.triplea.delegate.TerritoryEffectHelper;
import games.strategy.triplea.delegate.UnitBattleComparator;
import games.strategy.triplea.delegate.dataObjects.FightBattleDetails;
import games.strategy.triplea.delegate.dataObjects.MoveDescription;
import games.strategy.triplea.delegate.dataObjects.TechResults;
import games.strategy.triplea.delegate.dataObjects.TechRoll;
import games.strategy.triplea.delegate.remote.IEditDelegate;
import games.strategy.triplea.delegate.remote.IPoliticsDelegate;
import games.strategy.triplea.delegate.remote.IUserActionDelegate;
import games.strategy.triplea.formatter.MyFormatter;
import games.strategy.triplea.image.TileImageFactory;
import games.strategy.triplea.settings.ClientSetting;
import games.strategy.triplea.ui.export.ScreenshotExporter;
import games.strategy.triplea.ui.history.HistoryDetailsPanel;
import games.strategy.triplea.ui.history.HistoryLog;
import games.strategy.triplea.ui.history.HistoryPanel;
import games.strategy.triplea.ui.menubar.HelpMenu;
import games.strategy.triplea.ui.menubar.TripleAMenuBar;
import games.strategy.triplea.ui.screen.UnitsDrawer;
import games.strategy.triplea.util.TuvUtils;
import games.strategy.ui.ImageScrollModel;
import games.strategy.ui.SwingAction;
import games.strategy.ui.SwingComponents;
import games.strategy.ui.Util;
import games.strategy.util.EventThreadJOptionPane;
import games.strategy.util.IntegerMap;
import games.strategy.util.LocalizeHtml;
import games.strategy.util.ThreadUtil;
import games.strategy.util.Tuple;

/**
 * Main frame for the triple a game.
 */
public class TripleAFrame extends MainGameFrame {
  private static final long serialVersionUID = 7640069668264418976L;
  private final GameData data;
  private final IGame game;
  private final MapPanel mapPanel;
  private final MapPanelSmallView smallView;
  private final JLabel message = new JLabel("No selection");
  private final JLabel status = new JLabel("");
  private final JLabel step = new JLabel("xxxxxx");
  private final JLabel round = new JLabel("xxxxxx");
  private final JLabel player = new JLabel("xxxxxx");
  private final ActionButtons actionButtons;
  private final JPanel gameMainPanel = new JPanel();
  private final JPanel rightHandSidePanel = new JPanel();
  private final JTabbedPane tabsPanel = new JTabbedPane();
  private final StatPanel statsPanel;
  private final EconomyPanel economyPanel;
  private ObjectivePanel objectivePanel;
  private final NotesPanel notesPanel;
  private final TerritoryDetailPanel details;
  private final JPanel historyComponent = new JPanel();
  private final JPanel gameSouthPanel;
  private HistoryPanel historyPanel;
  private boolean inHistory = false;
  private boolean inGame = true;
  private HistorySynchronizer historySyncher;
  private final IUIContext uiContext;
  private final JPanel mapAndChatPanel;
  private ChatPanel chatPanel;
  private final CommentPanel commentPanel;
  private final JSplitPane chatSplit;
  private JSplitPane commentSplit;
  private final EditPanel editPanel;
  private final ButtonModel editModeButtonModel;
  private final ButtonModel showCommentLogButtonModel;
  private IEditDelegate editDelegate;
  private final JSplitPane gameCenterPanel;
  private Territory territoryLastEntered;
  private List<Unit> unitsBeingMousedOver;
  private PlayerID lastStepPlayer;
  private PlayerID currentStepPlayer;
  private final Map<PlayerID, Boolean> requiredTurnSeries = new HashMap<>();
  private final ThreadPool messageAndDialogThreadPool;
  private final TripleAMenuBar menu;
  private boolean isCtrlPressed = false;

  /** Creates new TripleAFrame. */
  public TripleAFrame(final IGame game, final LocalPlayers players) {
    super("TripleA - " + game.getData().getGameName(), players);
    this.game = game;
    data = game.getData();
    messageAndDialogThreadPool = new ThreadPool(1);
    addZoomKeyboardShortcuts();
    this.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
    final WindowListener windowListener = new WindowAdapter() {
      @Override
      public void windowClosing(final WindowEvent e) {
        leaveGame();
      }
    };
    this.addWindowListener(windowListener);
    uiContext = new UIContext();
    uiContext.setDefaultMapDir(game.getData());
    uiContext.getMapData().verify(data);
    uiContext.setLocalPlayers(players);
    this.setCursor(uiContext.getCursor());
    editModeButtonModel = new JToggleButton.ToggleButtonModel();
    editModeButtonModel.setEnabled(false);
    showCommentLogButtonModel = new JToggleButton.ToggleButtonModel();
    final AbstractAction showCommentLogAction = new AbstractAction() {
      private static final long serialVersionUID = 3964381772343872268L;

      @Override
      public void actionPerformed(final ActionEvent ae) {
        if (showCommentLogButtonModel.isSelected()) {
          showCommentLog();
        } else {
          hideCommentLog();
        }
      }

      private void hideCommentLog() {
        if (chatPanel != null) {
          commentSplit.setBottomComponent(null);
          chatSplit.setBottomComponent(chatPanel);
          chatSplit.validate();
        } else {
          mapAndChatPanel.removeAll();
          chatSplit.setTopComponent(null);
          chatSplit.setBottomComponent(null);
          mapAndChatPanel.add(mapPanel, BorderLayout.CENTER);
          mapAndChatPanel.validate();
        }
      }

      private void showCommentLog() {
        if (chatPanel != null) {
          commentSplit.setBottomComponent(chatPanel);
          chatSplit.setBottomComponent(commentSplit);
          chatSplit.validate();
        } else {
          mapAndChatPanel.removeAll();
          chatSplit.setTopComponent(mapPanel);
          chatSplit.setBottomComponent(commentPanel);
          mapAndChatPanel.add(chatSplit, BorderLayout.CENTER);
          mapAndChatPanel.validate();
        }
      }
    };
    showCommentLogButtonModel.addActionListener(showCommentLogAction);
    showCommentLogButtonModel.setSelected(false);
    menu = new TripleAMenuBar(this);
    this.setJMenuBar(menu);
    final ImageScrollModel model = new ImageScrollModel();
    model.setScrollX(uiContext.getMapData().scrollWrapX());
    model.setScrollY(uiContext.getMapData().scrollWrapY());
    model.setMaxBounds(uiContext.getMapData().getMapDimensions().width,
        uiContext.getMapData().getMapDimensions().height);
    final Image small = uiContext.getMapImage().getSmallMapImage();
    smallView = new MapPanelSmallView(small, model);
    mapPanel = new MapPanel(data, smallView, uiContext, model, this::computeScrollSpeed);
    mapPanel.addMapSelectionListener(mapSelectionListener);
    final MouseOverUnitListener mouseOverUnitListener = (units, territory, me) -> unitsBeingMousedOver = units;
    mapPanel.addMouseOverUnitListener(mouseOverUnitListener);
    // link the small and large images
    mapPanel.initSmallMap();
    mapAndChatPanel = new JPanel();
    mapAndChatPanel.setLayout(new BorderLayout());
    commentPanel = new CommentPanel(this, data);
    chatSplit = new JSplitPane();
    chatSplit.setOrientation(JSplitPane.VERTICAL_SPLIT);
    chatSplit.setOneTouchExpandable(true);
    chatSplit.setDividerSize(8);
    chatSplit.setResizeWeight(0.95);
    if (GameRunner.hasChat()) {
      commentSplit = new JSplitPane();
      commentSplit.setOrientation(JSplitPane.VERTICAL_SPLIT);
      commentSplit.setOneTouchExpandable(true);
      commentSplit.setDividerSize(8);
      commentSplit.setResizeWeight(0.5);
      commentSplit.setTopComponent(commentPanel);
      commentSplit.setBottomComponent(null);
      chatPanel = new ChatPanel(GameRunner.getChat());
      chatPanel.setPlayerRenderer(new PlayerChatRenderer(this.game, uiContext));
      final Dimension chatPrefSize = new Dimension((int) chatPanel.getPreferredSize().getWidth(), 95);
      chatPanel.setPreferredSize(chatPrefSize);
      chatSplit.setTopComponent(mapPanel);
      chatSplit.setBottomComponent(chatPanel);
      mapAndChatPanel.add(chatSplit, BorderLayout.CENTER);
    } else {
      mapAndChatPanel.add(mapPanel, BorderLayout.CENTER);
    }
    gameMainPanel.setLayout(new BorderLayout());
    this.getContentPane().setLayout(new BorderLayout());
    this.getContentPane().add(gameMainPanel, BorderLayout.CENTER);
    gameSouthPanel = new JPanel();
    gameSouthPanel.setLayout(new BorderLayout());
    message.setBorder(new EtchedBorder(EtchedBorder.RAISED));
    message.setPreferredSize(message.getPreferredSize());
    message.setText("some text to set a reasonable preferred size");
    status.setText("some text to set a reasonable preferred size for movement error messages");
    message.setPreferredSize(message.getPreferredSize());
    status.setPreferredSize(message.getPreferredSize());
    message.setText("");
    status.setText("");
    final JPanel bottomMessagePanel = new JPanel();
    bottomMessagePanel.setLayout(new GridBagLayout());
    bottomMessagePanel.setBorder(BorderFactory.createEmptyBorder());
    bottomMessagePanel.add(message, new GridBagConstraints(0, 0, 1, 1, .35, 1, GridBagConstraints.WEST,
        GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
    bottomMessagePanel.add(status, new GridBagConstraints(1, 0, 1, 1, .65, 1, GridBagConstraints.CENTER,
        GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
    gameSouthPanel.add(bottomMessagePanel, BorderLayout.CENTER);
    status.setBorder(new EtchedBorder(EtchedBorder.RAISED));
    final JPanel stepPanel = new JPanel();
    stepPanel.setLayout(new GridBagLayout());
    stepPanel.add(step, new GridBagConstraints(1, 0, 1, 1, 0, 0, GridBagConstraints.EAST, GridBagConstraints.BOTH,
        new Insets(0, 0, 0, 0), 0, 0));
    stepPanel.add(player, new GridBagConstraints(0, 0, 1, 1, 0, 0, GridBagConstraints.EAST, GridBagConstraints.BOTH,
        new Insets(0, 0, 0, 0), 0, 0));
    stepPanel.add(round, new GridBagConstraints(2, 0, 1, 1, 0, 0, GridBagConstraints.EAST, GridBagConstraints.BOTH,
        new Insets(0, 0, 0, 0), 0, 0));
    step.setBorder(new EtchedBorder(EtchedBorder.RAISED));
    round.setBorder(new EtchedBorder(EtchedBorder.RAISED));
    player.setBorder(new EtchedBorder(EtchedBorder.RAISED));
    step.setHorizontalTextPosition(SwingConstants.LEADING);
    gameSouthPanel.add(stepPanel, BorderLayout.EAST);
    gameMainPanel.add(gameSouthPanel, BorderLayout.SOUTH);
    rightHandSidePanel.setLayout(new BorderLayout());
    final FocusAdapter focusToMapPanelFocusListener = new FocusAdapter() {
      @Override
      public void focusGained(final FocusEvent e) {
        // give the focus back to the map panel
        mapPanel.requestFocusInWindow();
      }
    };
    rightHandSidePanel.addFocusListener(focusToMapPanelFocusListener);
    smallView.addFocusListener(focusToMapPanelFocusListener);
    tabsPanel.addFocusListener(focusToMapPanelFocusListener);
    rightHandSidePanel.add(smallView, BorderLayout.NORTH);
    tabsPanel.setBorder(null);
    rightHandSidePanel.add(tabsPanel, BorderLayout.CENTER);

    final MovePanel movePanel = new MovePanel(data, mapPanel, this);
    actionButtons = new ActionButtons(data, mapPanel, movePanel, this);

    final List<KeyListener> keyListeners =
        ImmutableList.of(getArrowKeyListener(), movePanel.getCustomKeyListeners(), getFlagToggleKeyListener(this));
    for (final KeyListener keyListener : keyListeners) {
      mapPanel.addKeyListener(keyListener);
    }

    tabsPanel.addTab("Actions", actionButtons);
    actionButtons.setBorder(null);
    statsPanel = new StatPanel(data, uiContext);
    tabsPanel.addTab("Stats", statsPanel);
    economyPanel = new EconomyPanel(data);
    tabsPanel.addTab("Economy", economyPanel);
    objectivePanel = new ObjectivePanel(data);
    if (objectivePanel.isEmpty()) {
      objectivePanel.removeDataChangeListener();
      objectivePanel = null;
    } else {
      tabsPanel.addTab(objectivePanel.getName(), objectivePanel);
    }
    notesPanel = new NotesPanel(HelpMenu.gameNotesPane);
    tabsPanel.addTab("Notes", notesPanel);
    details = new TerritoryDetailPanel(mapPanel, data, uiContext, this);
    tabsPanel.addTab("Territory", null, details, TerritoryDetailPanel.getHoverText());
    editPanel = new EditPanel(data, mapPanel, this);
    // Register a change listener
    tabsPanel.addChangeListener(evt -> {
      final JTabbedPane pane = (JTabbedPane) evt.getSource();
      // Get current tab
      final int sel = pane.getSelectedIndex();
      if (sel == -1) {
        return;
      }
      if (pane.getComponentAt(sel).equals(notesPanel)) {
        notesPanel.layoutNotes();
      } else {
        // for memory management reasons the notes are in a SoftReference,
        // so we must remove our hard reference link to them so it can be reclaimed if needed
        notesPanel.removeNotes();
      }
      if (pane.getComponentAt(sel).equals(editPanel)) {
        final PlayerID player1;
        data.acquireReadLock();
        try {
          player1 = data.getSequence().getStep().getPlayerId();
        } finally {
          data.releaseReadLock();
        }
        actionButtons.getCurrent().setActive(false);
        editPanel.display(player1);
      } else {
        actionButtons.getCurrent().setActive(true);
        editPanel.setActive(false);
      }
    });
    rightHandSidePanel.setPreferredSize(
        new Dimension((int) smallView.getPreferredSize().getWidth(), (int) mapPanel.getPreferredSize().getHeight()));
    gameCenterPanel = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, mapAndChatPanel, rightHandSidePanel);
    gameCenterPanel.setOneTouchExpandable(true);
    gameCenterPanel.setDividerSize(8);
    gameCenterPanel.setResizeWeight(1.0);
    gameMainPanel.add(gameCenterPanel, BorderLayout.CENTER);
    gameCenterPanel.resetToPreferredSizes();
    // set up the edit mode overlay text
    this.setGlassPane(new JComponent() {
      private static final long serialVersionUID = 6724687534214427291L;

      @Override
      protected void paintComponent(final Graphics g) {
        g.setFont(new Font("Ariel", Font.BOLD, 50));
        g.setColor(new Color(255, 255, 255, 175));
        final Dimension size = mapPanel.getSize();
        g.drawString("Edit Mode", (int) ((size.getWidth() - 200) / 2), (int) ((size.getHeight() - 100) / 2));
      }
    });
    // force a data change event to update the UI for edit mode
    dataChangeListener.gameDataChanged(ChangeFactory.EMPTY_CHANGE);
    data.addDataChangeListener(dataChangeListener);
    game.addGameStepListener(stepListener);
    updateStep();
    uiContext.addShutdownWindow(this);
  }


  private static KeyListener getFlagToggleKeyListener(final TripleAFrame frame) {
    return new KeyListener() {
      private boolean blockInputs = false;
      private long timeSinceLastPressEvent = 0;
      private boolean running = true;

      @Override
      public void keyTyped(final KeyEvent e) {/* Do nothing */}

      @Override
      public void keyPressed(final KeyEvent e) {
        timeSinceLastPressEvent = 0;
        if (!blockInputs) {
          resetFlagsOnTimeOut(e.getKeyCode());
          toggleFlags(e.getKeyCode());
          blockInputs = true;
        }
      }

      private void resetFlagsOnTimeOut(final int keyCode) {
        new Thread(() -> {
          running = true;
          while (running) {
            timeSinceLastPressEvent++;
            if (timeSinceLastPressEvent > 5) {
              running = false;
              toggleFlags(keyCode);
              blockInputs = false;
            }
            ThreadUtil.sleep(100);
          }
        }).start();
      }

      @Override
      public void keyReleased(final KeyEvent e) {
        toggleFlags(e.getKeyCode());
        blockInputs = false;
        running = false;
      }

      private void toggleFlags(final int keyCode) {
        if (keyCode == KeyEvent.VK_L) {
          UnitsDrawer.enabledFlags = !UnitsDrawer.enabledFlags;
          frame.getMapPanel().resetMap();
        }
      }
    };

  }

  private void addZoomKeyboardShortcuts() {
    final Runnable zoomIn = () -> {
      if (getScale() < 100) {
        setScale(getScale() + 10);
      }
    };
    SwingComponents.addKeyListenerWithMetaAndCtrlMasks(this, '+', zoomIn);

    final Runnable zoomOut = () -> {
      if (getScale() > 16) {
        setScale(getScale() - 10);
      }
    };

    SwingComponents.addKeyListenerWithMetaAndCtrlMasks(this, '-', zoomOut);
  }

  /**
   * @param value
   *        a number between 15 and 100.
   */
  public void setScale(final double value) {
    getMapPanel().setScale(value / 100);
  }

  /**
   * @return a scale between 15 and 100.
   */
  private double getScale() {
    return getMapPanel().getScale() * 100;
  }

  @Override
  public void stopGame() {
    // we have already shut down
    if (uiContext == null) {
      return;
    }
    this.setVisible(false);
    TripleAFrame.this.dispose();
    if (SystemProperties.isMac()) {
      // this frame should not handle shutdowns anymore
      MacQuitMenuWrapper.unregisterShutdownHandler();
    }
    messageAndDialogThreadPool.shutDown();
    uiContext.shutDown();
    if (chatPanel != null) {
      chatPanel.setPlayerRenderer(null);
      chatPanel.setChat(null);
    }
    if (historySyncher != null) {
      historySyncher.deactivate();
      historySyncher = null;
    }
    ProAI.gameOverClearCache();
  }

  @Override
  public void shutdown() {
    final int selectedOption = EventThreadJOptionPane.showConfirmDialog(this,
        "Are you sure you want to exit TripleA?\nUnsaved game data will be lost.", "Exit Program",
        JOptionPane.YES_NO_OPTION, getUiContext().getCountDownLatchHandler());
    if (selectedOption != JOptionPane.OK_OPTION) {
      return;
    }
    stopGame();
    System.exit(0);
  }

  @Override
  public void leaveGame() {
    final int selectedOption = EventThreadJOptionPane.showConfirmDialog(this,
        "Are you sure you want to leave the current game?\nUnsaved game data will be lost.", "Leave Game",
        JOptionPane.YES_NO_OPTION, getUiContext().getCountDownLatchHandler());
    if (selectedOption != JOptionPane.OK_OPTION) {
      return;
    }
    if (game instanceof ServerGame) {
      ((ServerGame) game).stopGame();
    } else {
      game.getMessenger().shutDown();
      ((ClientGame) game).shutDown();
      // an ugly hack, we need a better
      // way to get the main frame
      GameRunner.clientLeftGame();
    }
  }

  public MapSelectionListener mapSelectionListener = new DefaultMapSelectionListener() {
    @Override
    public void mouseEntered(final Territory territory) {
      territoryLastEntered = territory;
      refresh();
    }

    void refresh() {
      final StringBuilder buf = new StringBuilder();
      buf.append(territoryLastEntered == null ? "none" : territoryLastEntered.getName());
      if (territoryLastEntered != null) {
        final TerritoryAttachment ta = TerritoryAttachment.get(territoryLastEntered);
        if (ta != null) {
          final Iterator<TerritoryEffect> iter = ta.getTerritoryEffect().iterator();
          if (iter.hasNext()) {
            buf.append(" (");
          }
          while (iter.hasNext()) {
            buf.append(iter.next().getName());
            if (iter.hasNext()) {
              buf.append(", ");
            } else {
              buf.append(")");
            }
          }
          final int production = ta.getProduction();
          final int unitProduction = ta.getUnitProduction();
          final ResourceCollection resource = ta.getResources();
          if (unitProduction > 0 && unitProduction != production) {
            buf.append(", UnitProd: ").append(unitProduction);
          }
          if (production > 0 || (resource != null && resource.toString().length() > 0)) {
            buf.append(", Prod: ");
            if (production > 0) {
              buf.append(production).append(" PUs");
              if (resource != null && resource.toString().length() > 0) {
                buf.append(", ");
              }
            }
            if (resource != null) {
              buf.append(resource.toString());
            }
          }
        }
      }
      message.setText(buf.toString());
    }
  };

  void clearStatusMessage() {
    if (status == null) {
      return;
    }
    status.setText("");
    status.setIcon(null);
  }

  public void setStatusErrorMessage(final String msg) {
    setStatus(msg, mapPanel.getErrorImage());
  }

  private void setStatus(final String msg, final Optional<Image> image) {
    if (status == null) {
      return;
    }
    status.setText(msg);

    if (!msg.equals("") && image.isPresent()) {
      status.setIcon(new ImageIcon(image.get()));
    } else {
      status.setIcon(null);
    }
  }

  public void setStatusWarningMessage(final String msg) {
    setStatus(msg, mapPanel.getWarningImage());
  }

  public IntegerMap<ProductionRule> getProduction(final PlayerID player, final boolean bid) {
    if (messageAndDialogThreadPool == null) {
      return null;
    }
    messageAndDialogThreadPool.waitForAll();
    actionButtons.changeToProduce(player);
    return actionButtons.waitForPurchase(bid);
  }

  public HashMap<Unit, IntegerMap<RepairRule>> getRepair(final PlayerID player, final boolean bid,
      final Collection<PlayerID> allowedPlayersToRepair) {
    if (messageAndDialogThreadPool == null) {
      return null;
    }
    messageAndDialogThreadPool.waitForAll();
    actionButtons.changeToRepair(player);
    return actionButtons.waitForRepair(bid, allowedPlayersToRepair);
  }

  public MoveDescription getMove(final PlayerID player, final IPlayerBridge bridge, final boolean nonCombat,
      final String stepName) {
    if (messageAndDialogThreadPool == null) {
      return null;
    }
    messageAndDialogThreadPool.waitForAll();
    actionButtons.changeToMove(player, nonCombat, stepName);
    // workaround for panel not receiving focus at beginning of n/c move phase
    if (!getBattlePanel().getBattleFrame().isVisible()) {
      requestWindowFocus();
    }
    return actionButtons.waitForMove(bridge);
  }

  private void requestWindowFocus() {
    SwingAction.invokeAndWait(() -> {
      requestFocusInWindow();
      transferFocus();
    });
  }

  public PlaceData waitForPlace(final PlayerID player, final boolean bid, final IPlayerBridge bridge) {
    if (messageAndDialogThreadPool == null) {
      return null;
    }
    messageAndDialogThreadPool.waitForAll();
    actionButtons.changeToPlace(player);
    return actionButtons.waitForPlace(bid, bridge);
  }

  public void waitForMoveForumPoster(final PlayerID player, final IPlayerBridge bridge) {
    if (actionButtons == null || messageAndDialogThreadPool == null) {
      return;
    }
    // m_messageAndDialogThreadPool.waitForAll();
    actionButtons.changeToMoveForumPosterPanel(player);
    actionButtons.waitForMoveForumPosterPanel(this, bridge);
  }

  public void waitForEndTurn(final PlayerID player, final IPlayerBridge bridge) {
    if (actionButtons == null || messageAndDialogThreadPool == null) {
      return;
    }
    // m_messageAndDialogThreadPool.waitForAll();
    actionButtons.changeToEndTurn(player);
    actionButtons.waitForEndTurn(this, bridge);
  }

  public FightBattleDetails getBattle(final PlayerID player, final Map<BattleType, Collection<Territory>> battles) {
    if (messageAndDialogThreadPool == null) {
      return null;
    }
    messageAndDialogThreadPool.waitForAll();
    actionButtons.changeToBattle(player, battles);
    return actionButtons.waitForBattleSelection();
  }

  /**
   * We do NOT want to block the next player from beginning their turn.
   */
  @Override
  public void notifyError(final String message) {
    final String displayMessage = LocalizeHtml.localizeImgLinksInHtml(message);
    if (messageAndDialogThreadPool == null) {
      return;
    }
    messageAndDialogThreadPool.runTask(() -> EventThreadJOptionPane.showMessageDialogWithScrollPane(TripleAFrame.this,
        displayMessage, "Error", JOptionPane.ERROR_MESSAGE, getUiContext().getCountDownLatchHandler()));
  }

  /**
   * We do NOT want to block the next player from beginning their turn.
   */
  public void notifyMessage(final String message, final String title) {
    if (message == null || title == null) {
      return;
    }
    if (title.indexOf(AbstractConditionsAttachment.TRIGGER_CHANCE_FAILURE) != -1
        && message.indexOf(AbstractConditionsAttachment.TRIGGER_CHANCE_FAILURE) != -1
        && !getUiContext().getShowTriggerChanceFailure()) {
      return;
    }
    if (title.indexOf(AbstractConditionsAttachment.TRIGGER_CHANCE_SUCCESSFUL) != -1
        && message.indexOf(AbstractConditionsAttachment.TRIGGER_CHANCE_SUCCESSFUL) != -1
        && !getUiContext().getShowTriggerChanceSuccessful()) {
      return;
    }
    if (title.equals(AbstractTriggerAttachment.NOTIFICATION) && !getUiContext().getShowTriggeredNotifications()) {
      return;
    }
    if (title.indexOf(AbstractEndTurnDelegate.END_TURN_REPORT_STRING) != -1
        && message.indexOf(AbstractEndTurnDelegate.END_TURN_REPORT_STRING) != -1
        && !getUiContext().getShowEndOfTurnReport()) {
      return;
    }
    final String displayMessage = LocalizeHtml.localizeImgLinksInHtml(message);
    if (messageAndDialogThreadPool != null) {
      messageAndDialogThreadPool.runTask(() -> EventThreadJOptionPane.showMessageDialogWithScrollPane(TripleAFrame.this,
          displayMessage, title, JOptionPane.INFORMATION_MESSAGE, getUiContext().getCountDownLatchHandler()));
    }
  }

  public boolean getOkToLetAirDie(final PlayerID id, final Collection<Territory> airCantLand,
      final boolean movePhase) {
    if (airCantLand == null || airCantLand.isEmpty() || messageAndDialogThreadPool == null) {
      return true;
    }
    messageAndDialogThreadPool.waitForAll();
    final StringBuilder sb = new StringBuilder("<html>Air units cannot land in:<ul> ");
    for (final Territory t : airCantLand) {
      sb.append("<li>").append(t.getName()).append("</li>");
    }
    sb.append("</ul></html>");
    final boolean lhtrProd = AirThatCantLandUtil.isLhtrCarrierProduction(data)
        || AirThatCantLandUtil.isLandExistingFightersOnNewCarriers(data);
    int carrierCount = 0;
    for (final PlayerID p : GameStepPropertiesHelper.getCombinedTurns(data, id)) {
      carrierCount += p.getUnits().getMatches(Matches.unitIsCarrier()).size();
    }
    final boolean canProduceCarriersUnderFighter = lhtrProd && carrierCount != 0;
    if (canProduceCarriersUnderFighter && carrierCount > 0) {
      sb.append("\nYou have ").append(carrierCount).append(" ").append(MyFormatter.pluralize("carrier", carrierCount))
          .append(" on which planes can land");
    }
    final String ok = movePhase ? "End Move Phase" : "Kill Planes";
    final String cancel = movePhase ? "Keep Moving" : "Change Placement";
    final String[] options = {cancel, ok};
    mapPanel.centerOn(airCantLand.iterator().next());
    final int choice =
        EventThreadJOptionPane.showOptionDialog(this, sb.toString(), "Air cannot land", JOptionPane.YES_NO_OPTION,
            JOptionPane.WARNING_MESSAGE, null, options, cancel, getUiContext().getCountDownLatchHandler());
    return choice == 1;
  }

  public boolean getOkToLetUnitsDie(final Collection<Territory> unitsCantFight, final boolean movePhase) {
    if (unitsCantFight == null || unitsCantFight.isEmpty() || messageAndDialogThreadPool == null) {
      return true;
    }
    messageAndDialogThreadPool.waitForAll();
    final StringBuilder buf = new StringBuilder("Units in the following territories will die: ");
    Joiner.on(' ').appendTo(buf, FluentIterable.from(unitsCantFight).transform(unit -> unit.getName()));
    final String ok = movePhase ? "Done Moving" : "Kill Units";
    final String cancel = movePhase ? "Keep Moving" : "Change Placement";
    final String[] options = {cancel, ok};
    this.mapPanel.centerOn(unitsCantFight.iterator().next());
    final int choice =
        EventThreadJOptionPane.showOptionDialog(this, buf.toString(), "Units cannot fight", JOptionPane.YES_NO_OPTION,
            JOptionPane.WARNING_MESSAGE, null, options, cancel, getUiContext().getCountDownLatchHandler());
    return choice == 1;
  }

  public boolean acceptAction(final PlayerID playerSendingProposal, final String acceptanceQuestion,
      final boolean politics) {
    if (messageAndDialogThreadPool == null) {
      return true;
    }
    messageAndDialogThreadPool.waitForAll();
    final int choice = EventThreadJOptionPane.showConfirmDialog(this, acceptanceQuestion,
        "Accept " + (politics ? "Political " : "") + "Proposal from " + playerSendingProposal.getName() + "?",
        JOptionPane.YES_NO_OPTION, getUiContext().getCountDownLatchHandler());
    return choice == JOptionPane.YES_OPTION;
  }

  public boolean getOk(final String message) {
    if (messageAndDialogThreadPool == null) {
      return true;
    }
    messageAndDialogThreadPool.waitForAll();
    final int choice = EventThreadJOptionPane.showConfirmDialog(this, message, message, JOptionPane.OK_CANCEL_OPTION,
        getUiContext().getCountDownLatchHandler());
    return choice == JOptionPane.OK_OPTION;
  }

  public void notifyTechResults(final TechResults msg) {
    if (messageAndDialogThreadPool == null) {
      return;
    }
    messageAndDialogThreadPool.runTask(() -> {
      final AtomicReference<TechResultsDisplay> displayRef = new AtomicReference<>();
      SwingAction.invokeAndWait(() -> {
        final TechResultsDisplay display = new TechResultsDisplay(msg, uiContext, data);
        displayRef.set(display);
      });
      EventThreadJOptionPane.showOptionDialog(TripleAFrame.this, displayRef.get(), "Tech roll", JOptionPane.OK_OPTION,
          JOptionPane.PLAIN_MESSAGE, null, new String[] {"OK"}, "OK", getUiContext().getCountDownLatchHandler());
    });
  }

  public boolean getStrategicBombingRaid(final Territory location) {
    if (messageAndDialogThreadPool == null) {
      return true;
    }
    messageAndDialogThreadPool.waitForAll();
    final String message =
        (Properties.getRaidsMayBePreceededByAirBattles(data) ? "Bomb/Escort" : "Bomb") + " in "
            + location.getName();
    final String bomb =
        (Properties.getRaidsMayBePreceededByAirBattles(data) ? "Bomb/Escort" : "Bomb");
    final String normal = "Attack";
    final String[] choices = {bomb, normal};
    int choice = -1;
    while (choice < 0 || choice > 1) {
      choice = EventThreadJOptionPane.showOptionDialog(this, message, "Bomb?", JOptionPane.OK_CANCEL_OPTION,
          JOptionPane.INFORMATION_MESSAGE, null, choices, bomb, getUiContext().getCountDownLatchHandler());
    }
    return choice == JOptionPane.OK_OPTION;
  }

  public Unit getStrategicBombingRaidTarget(final Territory territory, final Collection<Unit> potentialTargets,
      final Collection<Unit> bombers) {
    if (potentialTargets.size() == 1 || messageAndDialogThreadPool == null) {
      return potentialTargets.iterator().next();
    }
    messageAndDialogThreadPool.waitForAll();
    final AtomicReference<Unit> selected = new AtomicReference<>();
    final String message = "Select bombing target in " + territory.getName();
    final Tuple<JPanel, JList<Unit>> comps = Util.runInSwingEventThread(() -> {
      final JList<Unit> list = new JList<>(new Vector<>(potentialTargets));
      list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
      list.setSelectedIndex(0);
      list.setCellRenderer(new UnitRenderer());
      final JPanel panel = new JPanel();
      panel.setLayout(new BorderLayout());
      if (bombers != null) {
        panel.add(new JLabel("For Units: " + MyFormatter.unitsToTextNoOwner(bombers)), BorderLayout.NORTH);
      }
      final JScrollPane scroll = new JScrollPane(list);
      panel.add(scroll, BorderLayout.CENTER);
      return Tuple.of(panel, list);
    });
    final JPanel panel = comps.getFirst();
    final JList<?> list = comps.getSecond();
    final String[] options = {"OK"};
    final int selection = EventThreadJOptionPane.showOptionDialog(this, panel, message, JOptionPane.OK_OPTION,
        JOptionPane.PLAIN_MESSAGE, null, options, null, getUiContext().getCountDownLatchHandler());
    if (selection == 0) {
      selected.set((Unit) list.getSelectedValue());
    }
    return selected.get();
  }

  /**
   * Create a unit option with icon and description.
   */
  private class UnitRenderer extends JLabel implements ListCellRenderer<Unit> {

    private static final long serialVersionUID = 1749164256040268579L;

    public UnitRenderer() {
      setOpaque(true);
    }

    @Override
    public Component getListCellRendererComponent(final JList<? extends Unit> list, final Unit unit, final int index,
        final boolean isSelected, final boolean cellHasFocus) {

      setText(unit.toString() + ", damage=" + TripleAUnit.get(unit).getUnitDamage());
      final Optional<ImageIcon> icon = uiContext.getUnitImageFactory().getIcon(unit.getType(), unit.getOwner(),
          Matches.unitHasTakenSomeBombingUnitDamage().match(unit), Matches.unitIsDisabled().match(unit));
      if (icon.isPresent()) {
        setIcon(icon.get());
      }
      setBorder(new EmptyBorder(0, 0, 0, 10));

      // Set selected option to highlighted color
      if (isSelected) {
        setBackground(list.getSelectionBackground());
        setForeground(list.getSelectionForeground());
      } else {
        setBackground(list.getBackground());
        setForeground(list.getForeground());
      }

      return this;
    }
  }

  public int[] selectFixedDice(final int numDice, final int hitAt, final boolean hitOnlyIfEquals, final String title,
      final int diceSides) {
    if (messageAndDialogThreadPool == null) {
      return new int[numDice];
    }
    messageAndDialogThreadPool.waitForAll();
    final DiceChooser chooser =
        Util.runInSwingEventThread(() -> new DiceChooser(getUiContext(), numDice, hitAt, hitOnlyIfEquals, diceSides));
    do {
      EventThreadJOptionPane.showMessageDialog(null, chooser, title, JOptionPane.PLAIN_MESSAGE,
          getUiContext().getCountDownLatchHandler());
    } while (chooser.getDice() == null);
    return chooser.getDice();
  }

  public Territory selectTerritoryForAirToLand(final Collection<Territory> candidates, final Territory currentTerritory,
      final String unitMessage) {
    if (candidates == null || candidates.isEmpty()) {
      return null;
    }
    if (candidates.size() == 1 || messageAndDialogThreadPool == null) {
      return candidates.iterator().next();
    }
    messageAndDialogThreadPool.waitForAll();
    final Tuple<JPanel, JList<Territory>> comps = Util.runInSwingEventThread(() -> {
      mapPanel.centerOn(currentTerritory);
      final JList<Territory> list = new JList<>(new Vector<>(candidates));
      list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
      list.setSelectedIndex(0);
      final JPanel panel = new JPanel();
      panel.setLayout(new BorderLayout());
      final JScrollPane scroll = new JScrollPane(list);
      final JTextArea text = new JTextArea(unitMessage, 8, 30);
      text.setLineWrap(true);
      text.setEditable(false);
      text.setWrapStyleWord(true);
      panel.add(text, BorderLayout.NORTH);
      panel.add(scroll, BorderLayout.CENTER);
      return Tuple.of(panel, list);
    });
    final JPanel panel = comps.getFirst();
    final JList<?> list = comps.getSecond();
    final String[] options = {"OK"};
    final String title = "Select territory for air units to land, current territory is " + currentTerritory.getName();
    EventThreadJOptionPane.showOptionDialog(this, panel, title, JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE,
        null, options, null, getUiContext().getCountDownLatchHandler());
    final Territory selected = (Territory) list.getSelectedValue();
    return selected;
  }

  public Tuple<Territory, Set<Unit>> pickTerritoryAndUnits(final PlayerID player,
      final List<Territory> territoryChoices, final List<Unit> unitChoices, final int unitsPerPick) {
    if (messageAndDialogThreadPool == null) {
      return Tuple.of(territoryChoices.iterator().next(),
          new HashSet<>(Matches.getNMatches(unitChoices, unitsPerPick, Matches.always())));
    }
    // total hacks
    messageAndDialogThreadPool.waitForAll();
    {
      final CountDownLatch latch1 = new CountDownLatch(1);
      SwingUtilities.invokeLater(() -> {
        if (!inGame) {
          showGame();
        }
        if (tabsPanel.indexOfTab("Actions") == -1) {
          // add actions tab
          tabsPanel.insertTab("Actions", null, actionButtons, null, 0);
        }
        tabsPanel.setSelectedIndex(0);
        latch1.countDown();
      });
      try {
        latch1.await();
      } catch (final InterruptedException e) {
        ClientLogger.logQuietly(e);
      }
    }
    actionButtons.changeToPickTerritoryAndUnits(player);
    final Tuple<Territory, Set<Unit>> territoryAndUnits =
        actionButtons.waitForPickTerritoryAndUnits(territoryChoices, unitChoices, unitsPerPick);
    final int index = tabsPanel == null ? -1 : tabsPanel.indexOfTab("Actions");
    if (index != -1 && inHistory) {
      final CountDownLatch latch2 = new CountDownLatch(1);
      SwingUtilities.invokeLater(() -> {
        if (tabsPanel != null) {
          // remove actions tab
          tabsPanel.remove(index);
        }
        latch2.countDown();
      });
      try {
        latch2.await();
      } catch (final InterruptedException e) {
        ClientLogger.logQuietly(e);
      }
    }
    if (actionButtons != null && actionButtons.getCurrent() != null) {
      actionButtons.getCurrent().setActive(false);
    }
    return territoryAndUnits;
  }

  public HashMap<Territory, IntegerMap<Unit>> selectKamikazeSuicideAttacks(
      final HashMap<Territory, Collection<Unit>> possibleUnitsToAttack, final Resource attackResourceToken,
      final int maxNumberOfAttacksAllowed) {
    if (SwingUtilities.isEventDispatchThread()) {
      throw new IllegalStateException("Should not be called from dispatch thread");
    }
    final HashMap<Territory, IntegerMap<Unit>> selection = new HashMap<>();
    if (possibleUnitsToAttack == null || possibleUnitsToAttack.isEmpty() || attackResourceToken == null
        || maxNumberOfAttacksAllowed <= 0 || messageAndDialogThreadPool == null) {
      return selection;
    }
    messageAndDialogThreadPool.waitForAll();
    final CountDownLatch continueLatch = new CountDownLatch(1);
    final Collection<IndividualUnitPanelGrouped> unitPanels = new ArrayList<>();
    SwingUtilities.invokeLater(new Runnable() {
      @Override
      public void run() {
        final HashMap<String, Collection<Unit>> possibleUnitsToAttackStringForm = new HashMap<>();
        for (final Entry<Territory, Collection<Unit>> entry : possibleUnitsToAttack.entrySet()) {
          final List<Unit> units = new ArrayList<>(entry.getValue());
          Collections.sort(units,
              new UnitBattleComparator(false, TuvUtils.getCostsForTuv(units.get(0).getOwner(), data),
                  TerritoryEffectHelper.getEffects(entry.getKey()), data, true, false));
          Collections.reverse(units);
          possibleUnitsToAttackStringForm.put(entry.getKey().getName(), units);
        }
        mapPanel.centerOn(data.getMap().getTerritory(possibleUnitsToAttackStringForm.keySet().iterator().next()));
        final IndividualUnitPanelGrouped unitPanel = new IndividualUnitPanelGrouped(possibleUnitsToAttackStringForm,
            data, uiContext, "Select Units to Suicide Attack using " + attackResourceToken.getName(),
            maxNumberOfAttacksAllowed, true, false);
        unitPanels.add(unitPanel);
        final String optionAttack = "Attack";
        final String optionNone = "None";
        final Object[] options = {optionAttack, optionNone};
        final JOptionPane optionPane = new JOptionPane(unitPanel, JOptionPane.PLAIN_MESSAGE,
            JOptionPane.YES_NO_CANCEL_OPTION, null, options, options[1]);
        final JDialog dialog =
            new JDialog((Frame) getParent(), "Select units to Suicide Attack using " + attackResourceToken.getName());
        dialog.setContentPane(optionPane);
        dialog.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
        dialog.setLocationRelativeTo(getParent());
        dialog.setAlwaysOnTop(true);
        dialog.pack();
        dialog.setVisible(true);
        dialog.requestFocusInWindow();
        optionPane.addPropertyChangeListener(e -> {
          if (!dialog.isVisible()) {
            return;
          }
          final String option = ((String) optionPane.getValue());
          if (option.equals(optionNone)) {
            unitPanels.clear();
            selection.clear();
            dialog.setVisible(false);
            dialog.removeAll();
            dialog.dispose();
            continueLatch.countDown();
          } else if (option.equals(optionAttack)) {
            if (unitPanels.size() != 1) {
              throw new IllegalStateException("unitPanels should only contain 1 entry");
            }
            for (final IndividualUnitPanelGrouped terrChooser : unitPanels) {
              for (final Entry<String, IntegerMap<Unit>> entry : terrChooser.getSelected().entrySet()) {
                selection.put(data.getMap().getTerritory(entry.getKey()), entry.getValue());
              }
            }
            dialog.setVisible(false);
            dialog.removeAll();
            dialog.dispose();
            continueLatch.countDown();
          }
        });

      }
    });
    mapPanel.getUiContext().addShutdownLatch(continueLatch);
    try {
      continueLatch.await();
    } catch (final InterruptedException ex) {
      // ignore interrupted exception
    } finally {
      mapPanel.getUiContext().removeShutdownLatch(continueLatch);
    }
    return selection;
  }

  public HashMap<Territory, Collection<Unit>> scrambleUnitsQuery(final Territory scrambleTo,
      final Map<Territory, Tuple<Collection<Unit>, Collection<Unit>>> possibleScramblers) {
    if (messageAndDialogThreadPool == null) {
      return null;
    }
    messageAndDialogThreadPool.waitForAll();
    if (SwingUtilities.isEventDispatchThread()) {
      throw new IllegalStateException("Should not be called from dispatch thread");
    }
    final CountDownLatch continueLatch = new CountDownLatch(1);
    final HashMap<Territory, Collection<Unit>> selection = new HashMap<>();
    final Collection<Tuple<Territory, UnitChooser>> choosers = new ArrayList<>();
    SwingUtilities.invokeLater(new Runnable() {
      @Override
      public void run() {
        mapPanel.centerOn(scrambleTo);
        final JPanel panel = new JPanel();
        panel.setLayout(new BorderLayout());
        final JLabel whereTo = new JLabel("Scramble To: " + scrambleTo.getName());
        whereTo.setFont(new Font("Arial", Font.ITALIC, 12));
        panel.add(whereTo, BorderLayout.NORTH);
        final JPanel panel2 = new JPanel();
        panel2.setBorder(BorderFactory.createEmptyBorder());
        panel2.setLayout(new FlowLayout());
        for (final Territory from : possibleScramblers.keySet()) {
          final JPanel panelChooser = new JPanel();
          panelChooser.setLayout(new BoxLayout(panelChooser, BoxLayout.Y_AXIS));
          panelChooser.setBorder(BorderFactory.createLineBorder(getBackground()));
          final JLabel whereFrom = new JLabel("From: " + from.getName());
          whereFrom.setHorizontalAlignment(SwingConstants.LEFT);
          whereFrom.setFont(new Font("Arial", Font.BOLD, 12));
          panelChooser.add(whereFrom);
          panelChooser.add(new JLabel(" "));
          final Collection<Unit> possible = possibleScramblers.get(from).getSecond();
          final int maxAllowed =
              Math.min(BattleDelegate.getMaxScrambleCount(possibleScramblers.get(from).getFirst()), possible.size());
          final UnitChooser chooser = new UnitChooser(possible, Collections.emptyMap(), false, uiContext);
          chooser.setMaxAndShowMaxButton(maxAllowed);
          choosers.add(Tuple.of(from, chooser));
          panelChooser.add(chooser);
          final JScrollPane chooserScrollPane = new JScrollPane(panelChooser);
          panel2.add(chooserScrollPane);
        }
        panel.add(panel2, BorderLayout.CENTER);
        final String optionScramble = "Scramble";
        final String optionNone = "None";
        final Object[] options = {optionScramble, optionNone};
        final JOptionPane optionPane = new JOptionPane(panel, JOptionPane.PLAIN_MESSAGE,
            JOptionPane.YES_NO_CANCEL_OPTION, null, options, options[1]);
        final JDialog dialog = new JDialog((Frame) getParent(), "Select units to scramble to " + scrambleTo.getName());
        dialog.setContentPane(optionPane);
        dialog.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
        dialog.setLocationRelativeTo(getParent());
        dialog.setAlwaysOnTop(true);
        dialog.pack();
        dialog.setVisible(true);
        dialog.requestFocusInWindow();
        optionPane.addPropertyChangeListener(e -> {
          if (!dialog.isVisible()) {
            return;
          }
          final String option = ((String) optionPane.getValue());
          if (option.equals(optionNone)) {
            choosers.clear();
            selection.clear();
            dialog.setVisible(false);
            dialog.removeAll();
            dialog.dispose();
            continueLatch.countDown();
          } else if (option.equals(optionScramble)) {
            for (final Tuple<Territory, UnitChooser> terrChooser : choosers) {
              selection.put(terrChooser.getFirst(), terrChooser.getSecond().getSelected());
            }
            dialog.setVisible(false);
            dialog.removeAll();
            dialog.dispose();
            continueLatch.countDown();
          }
        });
      }
    });
    mapPanel.getUiContext().addShutdownLatch(continueLatch);
    try {
      continueLatch.await();
    } catch (final InterruptedException ex) {
      // ignore interrupted exception
    } finally {
      mapPanel.getUiContext().removeShutdownLatch(continueLatch);
    }
    return selection;
  }

  public Collection<Unit> selectUnitsQuery(final Territory current, final Collection<Unit> possible,
      final String message) {
    if (messageAndDialogThreadPool == null) {
      return null;
    }
    messageAndDialogThreadPool.waitForAll();
    if (SwingUtilities.isEventDispatchThread()) {
      throw new IllegalStateException("Should not be called from dispatch thread");
    }
    final CountDownLatch continueLatch = new CountDownLatch(1);
    final Collection<Unit> selection = new ArrayList<>();
    SwingUtilities.invokeLater(new Runnable() {
      @Override
      public void run() {
        mapPanel.centerOn(current);
        final JPanel panel = new JPanel();
        panel.setLayout(new BorderLayout());
        final JLabel messageLabel = new JLabel(message);
        messageLabel.setFont(new Font("Arial", Font.ITALIC, 12));
        panel.add(messageLabel, BorderLayout.NORTH);
        final JPanel panelChooser = new JPanel();
        panelChooser.setLayout(new BoxLayout(panelChooser, BoxLayout.Y_AXIS));
        panelChooser.setBorder(BorderFactory.createLineBorder(getBackground()));
        final JLabel whereFrom = new JLabel("From: " + current.getName());
        whereFrom.setHorizontalAlignment(SwingConstants.LEFT);
        whereFrom.setFont(new Font("Arial", Font.BOLD, 12));
        panelChooser.add(whereFrom);
        panelChooser.add(new JLabel(" "));
        final int maxAllowed = possible.size();
        final UnitChooser chooser = new UnitChooser(possible, Collections.emptyMap(), false, uiContext);
        chooser.setMaxAndShowMaxButton(maxAllowed);
        panelChooser.add(chooser);
        final JScrollPane chooserScrollPane = new JScrollPane(panelChooser);
        panel.add(chooserScrollPane, BorderLayout.CENTER);
        final String optionSelect = "Select";
        final String optionNone = "None";
        final Object[] options = {optionSelect, optionNone};
        final JOptionPane optionPane = new JOptionPane(panel, JOptionPane.PLAIN_MESSAGE,
            JOptionPane.YES_NO_CANCEL_OPTION, null, options, options[1]);
        final JDialog dialog = new JDialog((Frame) getParent(), message);
        dialog.setContentPane(optionPane);
        dialog.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
        dialog.setLocationRelativeTo(getParent());
        dialog.setAlwaysOnTop(true);
        dialog.pack();
        dialog.setVisible(true);
        dialog.requestFocusInWindow();
        optionPane.addPropertyChangeListener(e -> {
          if (!dialog.isVisible()) {
            return;
          }
          final String option = ((String) optionPane.getValue());
          if (option.equals(optionNone)) {
            selection.clear();
            dialog.setVisible(false);
            dialog.removeAll();
            dialog.dispose();
            continueLatch.countDown();
          } else if (option.equals(optionSelect)) {
            selection.addAll(chooser.getSelected());
            dialog.setVisible(false);
            dialog.removeAll();
            dialog.dispose();
            continueLatch.countDown();
          }
        });
      }
    });
    mapPanel.getUiContext().addShutdownLatch(continueLatch);
    try {
      continueLatch.await();
    } catch (final InterruptedException ex) {
      ex.printStackTrace();
    } finally {
      mapPanel.getUiContext().removeShutdownLatch(continueLatch);
    }
    return selection;
  }

  public PoliticalActionAttachment getPoliticalActionChoice(final PlayerID player, final boolean firstRun,
      final IPoliticsDelegate politicsDelegate) {
    if (messageAndDialogThreadPool == null) {
      return null;
    }
    messageAndDialogThreadPool.waitForAll();
    actionButtons.changeToPolitics(player);
    requestWindowFocus();
    return actionButtons.waitForPoliticalAction(firstRun, politicsDelegate);
  }

  public UserActionAttachment getUserActionChoice(final PlayerID player, final boolean firstRun,
      final IUserActionDelegate userActionDelegate) {
    if (messageAndDialogThreadPool == null) {
      return null;
    }
    messageAndDialogThreadPool.waitForAll();
    actionButtons.changeToUserActions(player);
    requestWindowFocus();
    return actionButtons.waitForUserActionAction(firstRun, userActionDelegate);
  }

  public TechRoll getTechRolls(final PlayerID id) {
    if (messageAndDialogThreadPool == null) {
      return null;
    }
    messageAndDialogThreadPool.waitForAll();
    actionButtons.changeToTech(id);
    // workaround for panel not receiving focus at beginning of tech phase
    requestWindowFocus();
    return actionButtons.waitForTech();
  }

  public Territory getRocketAttack(final Collection<Territory> candidates, final Territory from) {
    if (messageAndDialogThreadPool == null) {
      return null;
    }
    messageAndDialogThreadPool.waitForAll();
    mapPanel.centerOn(from);
    final AtomicReference<Territory> selected = new AtomicReference<>();
    SwingAction.invokeAndWait(() -> {
      final JList<Territory> list = new JList<>(new Vector<>(candidates));
      list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
      list.setSelectedIndex(0);
      final JPanel panel = new JPanel();
      panel.setLayout(new BorderLayout());
      final JScrollPane scroll = new JScrollPane(list);
      panel.add(scroll, BorderLayout.CENTER);
      if (from != null) {
        panel.add(BorderLayout.NORTH, new JLabel("Targets for rocket in " + from.getName()));
      }
      final String[] options = {"OK", "Dont attack"};
      final String message = "Select Rocket Target";
      final int selection = JOptionPane.showOptionDialog(TripleAFrame.this, panel, message,
          JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE, null, options, null);
      if (selection == 0) {
        selected.set(list.getSelectedValue());
      }
    });
    return selected.get();
  }

  GameStepListener stepListener = (stepName, delegateName, player1, round1, stepDisplayName) -> updateStep();

  private void updateStep() {
    final IUIContext context = uiContext;
    if (context == null || context.isShutDown()) {
      return;
    }
    data.acquireReadLock();
    try {
      if (data.getSequence().getStep() == null) {
        return;
      }
    } finally {
      data.releaseReadLock();
    }
    // we need to invoke and wait here since
    // if we switch to the history as a result of a history
    // change, we need to ensure that no further history
    // events are run until our historySynchronizer is set up
    if (!SwingUtilities.isEventDispatchThread()) {
      SwingAction.invokeAndWait(() -> updateStep());
      return;
    }
    int round;
    String stepDisplayName;
    PlayerID player;
    data.acquireReadLock();
    try {
      round = data.getSequence().getRound();
      stepDisplayName = data.getSequence().getStep().getDisplayName();
      player = data.getSequence().getStep().getPlayerId();
    } finally {
      data.releaseReadLock();
    }
    this.round.setText("Round:" + round + " ");
    step.setText(stepDisplayName);
    final boolean isPlaying = localPlayers.playing(player);
    if (player != null) {
      this.player.setText((isPlaying ? "" : "REMOTE: ") + player.getName());
    }
    if (player != null && !player.isNull()) {
      this.round.setIcon(new ImageIcon(uiContext.getFlagImageFactory().getFlag(player)));
      lastStepPlayer = currentStepPlayer;
      currentStepPlayer = player;
    }
    // if the game control has passed to someone else and we are not just showing the map
    // show the history
    if (player != null && !player.isNull()) {
      if (isPlaying) {
        if (inHistory) {
          requiredTurnSeries.put(player, true);
          // if the game control is with us
          // show the current game
          showGame();
          // System.out.println("Changing step to " + stepDisplayName + " for " + player.getName());
        }
      } else {
        if (!inHistory && !uiContext.getShowMapOnly()) {
          if (!SwingUtilities.isEventDispatchThread()) {
            throw new IllegalStateException("We should be in dispatch thread");
          }
          showHistory();
        }
      }
    }
  }

  public void requiredTurnSeries(final PlayerID player) {
    if (player == null || !ThreadUtil.sleep(300)) {
      return;
    }
    SwingAction.invokeAndWait(() -> {
      final Boolean play = requiredTurnSeries.get(player);
      if (play != null && play) {
        ClipPlayer.play(SoundPath.CLIP_REQUIRED_YOUR_TURN_SERIES, player);
        requiredTurnSeries.put(player, false);
      }
      // center on capital of player, if it is a new player
      if (!player.equals(lastStepPlayer)) {
        lastStepPlayer = player;
        data.acquireReadLock();
        try {
          mapPanel.centerOn(TerritoryAttachment.getFirstOwnedCapitalOrFirstUnownedCapital(player, data));
        } finally {
          data.releaseReadLock();
        }
      }
    });
  }

  GameDataChangeListener dataChangeListener = new GameDataChangeListener() {
    @Override
    public void gameDataChanged(final Change change) {
      try {
        SwingUtilities.invokeLater(() -> {
          if (uiContext == null) {
            return;
          }
          if (getEditMode()) {
            if (tabsPanel.indexOfComponent(editPanel) == -1) {
              showEditMode();
            }
          } else {
            if (tabsPanel.indexOfComponent(editPanel) != -1) {
              hideEditMode();
            }
          }
          if (uiContext.getShowMapOnly()) {
            hideRightHandSidePanel();
            // display troop movement
            final HistoryNode node = data.getHistory().getLastNode();
            if (node instanceof Renderable) {
              final Object details1 = ((Renderable) node).getRenderingData();
              if (details1 instanceof MoveDescription) {
                final MoveDescription moveMessage = (MoveDescription) details1;
                final Route route = moveMessage.getRoute();
                mapPanel.setRoute(null);
                mapPanel.setRoute(route);
                final Territory terr = route.getEnd();
                if (!mapPanel.isShowing(terr)) {
                  mapPanel.centerOn(terr);
                }
              }
            }
          } else {
            showRightHandSidePanel();
          }
        });
      } catch (final Exception e) {
        ClientLogger.logQuietly(e);
      }
    }
  };

  private KeyListener getArrowKeyListener() {
    return new KeyListener() {
      @Override
      public void keyPressed(final KeyEvent e) {
        isCtrlPressed = e.isControlDown();
        // scroll map according to wasd/arrowkeys
        final int diffPixel = computeScrollSpeed();
        final int x = mapPanel.getXOffset();
        final int y = mapPanel.getYOffset();
        final int keyCode = e.getKeyCode();

        if (keyCode == KeyEvent.VK_RIGHT || keyCode == KeyEvent.VK_D) {
          getMapPanel().setTopLeft(x + diffPixel, y);
        } else if (keyCode == KeyEvent.VK_LEFT || keyCode == KeyEvent.VK_A) {
          getMapPanel().setTopLeft(x - diffPixel, y);
        } else if (keyCode == KeyEvent.VK_DOWN || keyCode == KeyEvent.VK_S) {
          getMapPanel().setTopLeft(x, y + diffPixel);
        } else if (keyCode == KeyEvent.VK_UP || keyCode == KeyEvent.VK_W) {
          getMapPanel().setTopLeft(x, y - diffPixel);
        }
        // I for info
        if (keyCode == KeyEvent.VK_I || keyCode == KeyEvent.VK_V) {
          String unitInfo = "";
          if (unitsBeingMousedOver != null && !unitsBeingMousedOver.isEmpty()) {
            final Unit unit = unitsBeingMousedOver.get(0);
            final UnitAttachment ua = UnitAttachment.get(unit.getType());
            if (ua != null) {
              unitInfo = "<b>Unit:</b><br>" + unit.getType().getName() + ": "
                  + ua.toStringShortAndOnlyImportantDifferences(unit.getOwner(), true, false);
            }
          }
          String terrInfo = "";
          if (territoryLastEntered != null) {
            final TerritoryAttachment ta = TerritoryAttachment.get(territoryLastEntered);
            if (ta != null) {
              terrInfo = "<b>Territory:</b><br>" + ta.toStringForInfo(true, true) + "<br>";
            } else {
              terrInfo = "<b>Territory:</b><br>" + territoryLastEntered.getName() + "<br>Water Territory";
            }
          }
          String tipText = unitInfo;
          if (unitInfo.length() > 0 && terrInfo.length() > 0) {
            tipText = tipText + "<br><br><br><br><br>";
          }
          tipText = tipText + terrInfo;
          if (tipText.length() > 0) {
            final Point currentPoint = MouseInfo.getPointerInfo().getLocation();
            final PopupFactory popupFactory = PopupFactory.getSharedInstance();
            final JToolTip info = new JToolTip();
            info.setTipText("<html>" + tipText + "</html>");
            final Popup popup = popupFactory.getPopup(mapPanel, info, currentPoint.x, currentPoint.y);
            popup.show();
            final Runnable disposePopup = () -> {
              ThreadUtil.sleep(5000);
              popup.hide();
            };
            new Thread(disposePopup, "popup waiter").start();
          }
        }
      }

      @Override
      public void keyTyped(final KeyEvent e) {}

      @Override
      public void keyReleased(final KeyEvent e) {
        isCtrlPressed = e.isControlDown();
      }
    };
  }

  private int computeScrollSpeed() {
    return isCtrlPressed
        ? ClientSetting.ARROW_KEY_SCROLL_SPEED.intValue() * ClientSetting.FASTER_ARROW_KEY_SCROLL_MULTIPLIER.intValue()
        : ClientSetting.ARROW_KEY_SCROLL_SPEED.intValue();
  }

  private void showEditMode() {
    tabsPanel.addTab("Edit", editPanel);
    if (editDelegate != null) {
      tabsPanel.setSelectedComponent(editPanel);
    }
    editModeButtonModel.setSelected(true);
    getGlassPane().setVisible(true);
  }

  private void hideEditMode() {
    if (tabsPanel.getSelectedComponent() == editPanel) {
      tabsPanel.setSelectedIndex(0);
    }
    tabsPanel.remove(editPanel);
    editModeButtonModel.setSelected(false);
    getGlassPane().setVisible(false);
  }

  public void showActionPanelTab() {
    tabsPanel.setSelectedIndex(0);
  }

  public void showRightHandSidePanel() {
    rightHandSidePanel.setVisible(true);
  }

  public void hideRightHandSidePanel() {
    rightHandSidePanel.setVisible(false);
  }

  public HistoryPanel getHistoryPanel() {
    return historyPanel;
  }

  private void showHistory() {
    inHistory = true;
    inGame = false;
    setWidgetActivation();
    final GameData clonedGameData;
    data.acquireReadLock();
    try {
      // we want to use a clone of the data, so we can make changes to it
      // as we walk up and down the history
      clonedGameData = GameDataUtils.cloneGameData(data);
      if (clonedGameData == null) {
        return;
      }
      data.removeDataChangeListener(dataChangeListener);
      clonedGameData.testLocksOnRead();
      if (historySyncher != null) {
        throw new IllegalStateException("Two history synchers?");
      }
      historySyncher = new HistorySynchronizer(clonedGameData, game);
      clonedGameData.addDataChangeListener(dataChangeListener);
    } finally {
      data.releaseReadLock();
    }
    statsPanel.setGameData(clonedGameData);
    economyPanel.setGameData(clonedGameData);
    if (objectivePanel != null && !objectivePanel.isEmpty()) {
      objectivePanel.setGameData(clonedGameData);
    }
    details.setGameData(clonedGameData);
    mapPanel.setGameData(clonedGameData);
    final HistoryDetailsPanel historyDetailPanel = new HistoryDetailsPanel(clonedGameData, mapPanel);
    tabsPanel.removeAll();
    tabsPanel.add("History", historyDetailPanel);
    tabsPanel.add("Stats", statsPanel);
    tabsPanel.add("Economy", economyPanel);
    if (objectivePanel != null && !objectivePanel.isEmpty()) {
      tabsPanel.add(objectivePanel.getName(), objectivePanel);
    }
    tabsPanel.add("Notes", notesPanel);
    tabsPanel.add("Territory", details);
    if (getEditMode()) {
      tabsPanel.add("Edit", editPanel);
    }
    if (actionButtons.getCurrent() != null) {
      actionButtons.getCurrent().setActive(false);
    }
    historyComponent.removeAll();
    historyComponent.setLayout(new BorderLayout());
    // create history tree context menu
    // actions need to clear the history panel popup state when done
    final JPopupMenu popup = new JPopupMenu();
    popup.add(new AbstractAction("Show Summary Log") {
      private static final long serialVersionUID = -6730966512179268157L;

      @Override
      public void actionPerformed(final ActionEvent ae) {
        final HistoryLog historyLog = new HistoryLog();
        historyLog.printRemainingTurn(historyPanel.getCurrentPopupNode(), false, data.getDiceSides(), null);
        historyLog.printTerritorySummary(historyPanel.getCurrentPopupNode(), clonedGameData);
        historyLog.printProductionSummary(clonedGameData);
        historyPanel.clearCurrentPopupNode();
        historyLog.setVisible(true);
      }
    });
    popup.add(new AbstractAction("Show Detailed Log") {
      private static final long serialVersionUID = -8709762764495294671L;

      @Override
      public void actionPerformed(final ActionEvent ae) {
        final HistoryLog historyLog = new HistoryLog();
        historyLog.printRemainingTurn(historyPanel.getCurrentPopupNode(), true, data.getDiceSides(), null);
        historyLog.printTerritorySummary(historyPanel.getCurrentPopupNode(), clonedGameData);
        historyLog.printProductionSummary(clonedGameData);
        historyPanel.clearCurrentPopupNode();
        historyLog.setVisible(true);
      }
    });
    popup.add(new AbstractAction("Export Map Snapshot") {
      private static final long serialVersionUID = 1222760138263428443L;

      @Override
      public void actionPerformed(final ActionEvent ae) {
        ScreenshotExporter.exportScreenshot(TripleAFrame.this, data, historyPanel.getCurrentPopupNode());
        historyPanel.clearCurrentPopupNode();
      }
    });
    popup.add(new AbstractAction("Save Game at this point (BETA)") {
      private static final long serialVersionUID = 1430512376199927896L;

      @Override
      public void actionPerformed(final ActionEvent ae) {
        JOptionPane.showMessageDialog(TripleAFrame.this,
            "Please first left click on the spot you want to save from, Then right click and select 'Save Game From "
                + "History'"
                + "\n\nIt is recommended that when saving the game from the History panel:"
                + "\n * Your CURRENT GAME is at the start of some player's turn, and that no moves have been made and "
                + "no actions taken yet."
                + "\n * The point in HISTORY that you are trying to save at, is at the beginning of a player's turn, "
                + "or the beginning of a round."
                + "\nSaving at any other point, could potentially create errors."
                + "\nFor example, saving while your current game is in the middle of a move or battle phase will "
                + "always create errors in the savegame."
                + "\nAnd you will also get errors in the savegame if you try to create a save at a point in history "
                + "such as a move or battle phase.",
            "Save Game from History", JOptionPane.INFORMATION_MESSAGE);
        data.acquireReadLock();
        try {
          final File f = TripleAMenuBar.getSaveGameLocation(TripleAFrame.this);
          if (f != null) {
            try (FileOutputStream fout = new FileOutputStream(f)) {
              final GameData datacopy = GameDataUtils.cloneGameData(data, true);
              datacopy.getHistory().gotoNode(historyPanel.getCurrentPopupNode());
              datacopy.getHistory().removeAllHistoryAfterNode(historyPanel.getCurrentPopupNode());
              // TODO: the saved current delegate is still the current delegate,
              // rather than the delegate at that history popup node
              // TODO: it still shows the current round number, rather than the round at the history popup node
              // TODO: this could be solved easily if rounds/steps were changes,
              // but that could greatly increase the file size :(
              // TODO: this also does not undo the runcount of each delegate step
              @SuppressWarnings("unchecked")
              final Enumeration<TreeNode> enumeration =
                  ((DefaultMutableTreeNode) datacopy.getHistory().getRoot()).preorderEnumeration();
              enumeration.nextElement();
              int round = 0;
              String stepDisplayName = datacopy.getSequence().getStep(0).getDisplayName();
              PlayerID currentPlayer = datacopy.getSequence().getStep(0).getPlayerId();
              while (enumeration.hasMoreElements()) {
                final HistoryNode node = (HistoryNode) enumeration.nextElement();
                if (node instanceof Round) {
                  round = Math.max(0, ((Round) node).getRoundNo() - datacopy.getSequence().getRoundOffset());
                  currentPlayer = null;
                  stepDisplayName = node.getTitle();
                } else if (node instanceof Step) {
                  currentPlayer = ((Step) node).getPlayerId();
                  stepDisplayName = node.getTitle();
                }
              }
              datacopy.getSequence().setRoundAndStep(round, stepDisplayName, currentPlayer);
              GameDataManager.saveGame(fout, datacopy);
              JOptionPane.showMessageDialog(TripleAFrame.this, "Game Saved", "Game Saved",
                  JOptionPane.INFORMATION_MESSAGE);
            } catch (final IOException e) {
              ClientLogger.logQuietly(e);
            }
          }
        } finally {
          data.releaseReadLock();
        }
        historyPanel.clearCurrentPopupNode();
      }
    });
    final JSplitPane split = new JSplitPane();
    split.setOneTouchExpandable(true);
    split.setDividerSize(8);
    historyPanel = new HistoryPanel(clonedGameData, historyDetailPanel, popup, uiContext);
    split.setLeftComponent(historyPanel);
    split.setRightComponent(gameCenterPanel);
    split.setDividerLocation(150);
    historyComponent.add(split, BorderLayout.CENTER);
    historyComponent.add(gameSouthPanel, BorderLayout.SOUTH);
    getContentPane().removeAll();
    getContentPane().add(historyComponent, BorderLayout.CENTER);
    validate();
  }

  private void showGame() {
    inGame = true;
    uiContext.setShowMapOnly(false);
    // Are we coming from showHistory mode or showMapOnly mode?
    if (inHistory) {
      inHistory = false;
      if (historySyncher != null) {
        historySyncher.deactivate();
        historySyncher = null;
      }
      historyPanel.goToEnd();
      historyPanel = null;
      mapPanel.getData().removeDataChangeListener(dataChangeListener);
      statsPanel.setGameData(data);
      economyPanel.setGameData(data);
      if (objectivePanel != null && !objectivePanel.isEmpty()) {
        objectivePanel.setGameData(data);
      }
      details.setGameData(data);
      mapPanel.setGameData(data);
      data.addDataChangeListener(dataChangeListener);
      tabsPanel.removeAll();
    }
    setWidgetActivation();
    tabsPanel.add("Action", actionButtons);
    tabsPanel.add("Stats", statsPanel);
    tabsPanel.add("Economy", economyPanel);
    if (objectivePanel != null && !objectivePanel.isEmpty()) {
      tabsPanel.add(objectivePanel.getName(), objectivePanel);
    }
    tabsPanel.add("Notes", notesPanel);
    tabsPanel.add("Territory", details);
    if (getEditMode()) {
      tabsPanel.add("Edit", editPanel);
    }
    if (actionButtons.getCurrent() != null) {
      actionButtons.getCurrent().setActive(true);
    }
    gameMainPanel.removeAll();
    gameMainPanel.setLayout(new BorderLayout());
    gameMainPanel.add(gameCenterPanel, BorderLayout.CENTER);
    gameMainPanel.add(gameSouthPanel, BorderLayout.SOUTH);
    getContentPane().removeAll();
    getContentPane().add(gameMainPanel, BorderLayout.CENTER);
    mapPanel.setRoute(null);
    validate();
  }

  private void showMapOnly() {
    // Are we coming from showHistory mode or showGame mode?
    if (inHistory) {
      inHistory = false;
      if (historySyncher != null) {
        historySyncher.deactivate();
        historySyncher = null;
      }
      historyPanel.goToEnd();
      historyPanel = null;
      mapPanel.getData().removeDataChangeListener(dataChangeListener);
      mapPanel.setGameData(data);
      data.addDataChangeListener(dataChangeListener);
      gameMainPanel.removeAll();
      gameMainPanel.setLayout(new BorderLayout());
      gameMainPanel.add(mapAndChatPanel, BorderLayout.CENTER);
      gameMainPanel.add(rightHandSidePanel, BorderLayout.EAST);
      gameMainPanel.add(gameSouthPanel, BorderLayout.SOUTH);
      getContentPane().removeAll();
      getContentPane().add(gameMainPanel, BorderLayout.CENTER);
      mapPanel.setRoute(null);
    } else {
      inGame = false;
    }
    uiContext.setShowMapOnly(true);
    setWidgetActivation();
    validate();
  }



  private void setWidgetActivation() {
    if (!SwingUtilities.isEventDispatchThread()) {
      SwingUtilities.invokeLater(() -> setWidgetActivation());
      return;
    }
    if (showHistoryAction != null) {
      showHistoryAction.setEnabled(!(inHistory || uiContext.getShowMapOnly()));
    }
    if (showGameAction != null) {
      showGameAction.setEnabled(!inGame);
    }
    if (showMapOnlyAction != null) {
      // We need to check and make sure there are no local human players
      boolean foundHuman = false;
      for (final IGamePlayer gamePlayer : localPlayers.getLocalPlayers()) {
        if (gamePlayer instanceof TripleAPlayer) {
          foundHuman = true;
        }
      }
      if (!foundHuman) {
        showMapOnlyAction.setEnabled(inGame || inHistory);
      } else {
        showMapOnlyAction.setEnabled(false);
      }
    }
    if (editModeButtonModel != null) {
      if (editDelegate == null || uiContext.getShowMapOnly()) {
        editModeButtonModel.setEnabled(false);
      } else {
        editModeButtonModel.setEnabled(true);
      }
    }
  }

  // setEditDelegate is called by TripleAPlayer at the start and end of a turn
  public void setEditDelegate(final IEditDelegate editDelegate) {
    this.editDelegate = editDelegate;
    // force a data change event to update the UI for edit mode
    dataChangeListener.gameDataChanged(ChangeFactory.EMPTY_CHANGE);
    setWidgetActivation();
  }

  public IEditDelegate getEditDelegate() {
    return editDelegate;
  }

  public ButtonModel getEditModeButtonModel() {
    return editModeButtonModel;
  }

  public ButtonModel getShowCommentLogButtonModel() {
    return showCommentLogButtonModel;
  }

  private boolean getEditMode() {
    final boolean isEditMode;
    // use GameData from mapPanel since it will follow current history node
    mapPanel.getData().acquireReadLock();
    try {
      isEditMode = BaseEditDelegate.getEditMode(mapPanel.getData());
    } finally {
      mapPanel.getData().releaseReadLock();
    }
    return isEditMode;
  }

  private final AbstractAction showHistoryAction = new AbstractAction("Show history") {
    private static final long serialVersionUID = -3960551522512897374L;

    @Override
    public void actionPerformed(final ActionEvent e) {
      showHistory();
      dataChangeListener.gameDataChanged(ChangeFactory.EMPTY_CHANGE);
    }
  };

  private final AbstractAction showGameAction = new AbstractAction("Show current game") {
    private static final long serialVersionUID = -7551760679570164254L;

    {
      setEnabled(false);
    }

    @Override
    public void actionPerformed(final ActionEvent e) {
      showGame();
      dataChangeListener.gameDataChanged(ChangeFactory.EMPTY_CHANGE);
    }
  };

  private final AbstractAction showMapOnlyAction = new AbstractAction("Show map only") {
    private static final long serialVersionUID = -6621157075878333141L;

    @Override
    public void actionPerformed(final ActionEvent e) {
      showMapOnly();
      dataChangeListener.gameDataChanged(ChangeFactory.EMPTY_CHANGE);
    }
  };

  public Collection<Unit> moveFightersToCarrier(final Collection<Unit> fighters, final Territory where) {
    if (messageAndDialogThreadPool == null) {
      return null;
    }
    messageAndDialogThreadPool.waitForAll();
    mapPanel.centerOn(where);
    final AtomicReference<JScrollPane> panelRef = new AtomicReference<>();
    final AtomicReference<UnitChooser> chooserRef = new AtomicReference<>();
    SwingAction.invokeAndWait(() -> {
      final UnitChooser chooser = new UnitChooser(fighters, Collections.emptyMap(), false, uiContext);
      final Dimension screenResolution = Toolkit.getDefaultToolkit().getScreenSize();
      final int availHeight = screenResolution.height - 120;
      final int availWidth = screenResolution.width - 40;
      final JScrollPane scroll = new JScrollPane(chooser);
      scroll.setBorder(BorderFactory.createEmptyBorder());
      scroll.setPreferredSize(new Dimension(
          (scroll.getPreferredSize().width > availWidth ? availWidth
              : (scroll.getPreferredSize().width + (scroll.getPreferredSize().height > availHeight ? 20 : 0))),
          (scroll.getPreferredSize().height > availHeight ? availHeight
              : (scroll.getPreferredSize().height + (scroll.getPreferredSize().width > availWidth ? 26 : 0)))));
      panelRef.set(scroll);
      chooserRef.set(chooser);
    });
    final int option = EventThreadJOptionPane.showOptionDialog(this, panelRef.get(),
        "Move air units to carrier", JOptionPane.PLAIN_MESSAGE, JOptionPane.OK_CANCEL_OPTION, null,
        new String[] {"OK", "Cancel"}, "OK", getUiContext().getCountDownLatchHandler());
    if (option == JOptionPane.OK_OPTION) {
      return chooserRef.get().getSelected();
    }
    return new ArrayList<>();
  }

  public BattlePanel getBattlePanel() {
    return actionButtons.getBattlePanel();
  }

  public Action getShowGameAction() {
    return showGameAction;
  }

  public Action getShowHistoryAction() {
    return showHistoryAction;
  }

  public Action getShowMapOnlyAction() {
    return showMapOnlyAction;
  }

  public IUIContext getUiContext() {
    return uiContext;
  }

  public MapPanel getMapPanel() {
    return mapPanel;
  }

  @Override
  public JComponent getMainPanel() {
    return mapPanel;
  }

  // Beagle Code Called to Change Mapskin
  public void updateMap(final String mapdir) {
    uiContext.setMapDir(data, mapdir);
    // when changing skins, always show relief images
    if (uiContext.getMapData().getHasRelief()) {
      TileImageFactory.setShowReliefImages(true);
    }
    mapPanel.setGameData(data);
    // update mappanels to use new image
    mapPanel.changeImage(uiContext.getMapData().getMapDimensions());
    final Image small = uiContext.getMapImage().getSmallMapImage();
    smallView.changeImage(small);
    mapPanel.changeSmallMapOffscreenMap();
    // redraw territories
    mapPanel.resetMap();
  }

  @Override
  public IGame getGame() {
    return game;
  }

  @Override
  public void setShowChatTime(final boolean showTime) {
    chatPanel.setShowChatTime(showTime);
  }

  public Optional<InGameLobbyWatcherWrapper> getInGameLobbyWatcher() {
    if (ServerGame.class.isAssignableFrom(getGame().getClass())) {
      final ServerGame serverGame = (ServerGame) getGame();
      return Optional.ofNullable(serverGame.getInGameLobbyWatcher());
    } else {
      return Optional.empty();
    }
  }
}
