/*
 * $Id: Inventory.java 4561 2008-11-28 15:57:51Z uckelman $
 *
 * Copyright (c) 2005-2006 by Rodney Kinney, Brent Easton,
 * Torsten Spindler, and Scot McConnachie
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License (LGPL) as published by the Free Software Foundation.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, copies are available 
 * at http://www.opensource.org.
 */
package VASSAL.build.module;

import java.awt.Component;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;

import VASSAL.build.AbstractConfigurable;
import VASSAL.build.AutoConfigurable;
import VASSAL.build.Buildable;
import VASSAL.build.GameModule;
import VASSAL.build.module.documentation.HelpFile;
import VASSAL.build.module.map.MenuDisplayer;
import VASSAL.build.module.properties.PropertySource;
import VASSAL.command.Command;
import VASSAL.command.NullCommand;
import VASSAL.configure.Configurer;
import VASSAL.configure.ConfigurerFactory;
import VASSAL.configure.GamePieceFormattedStringConfigurer;
import VASSAL.configure.HotKeyConfigurer;
import VASSAL.configure.IconConfigurer;
import VASSAL.configure.PropertyExpression;
import VASSAL.configure.StringArrayConfigurer;
import VASSAL.configure.StringEnumConfigurer;
import VASSAL.configure.VisibilityCondition;
import VASSAL.counters.BoundsTracker;
import VASSAL.counters.GamePiece;
import VASSAL.counters.PieceCloner;
import VASSAL.counters.PieceFilter;
import VASSAL.counters.PieceIterator;
import VASSAL.counters.Properties;
import VASSAL.counters.PropertiesPieceFilter;
import VASSAL.counters.Stack;
import VASSAL.i18n.Resources;
import VASSAL.i18n.TranslatableConfigurerFactory;
import VASSAL.preferences.PositionOption;
import VASSAL.tools.FormattedString;
import VASSAL.tools.LaunchButton;
import VASSAL.tools.ScrollPane;
import VASSAL.tools.WriteErrorDialog;
import VASSAL.tools.filechooser.FileChooser;
import VASSAL.tools.io.IOUtils;

public class Inventory extends AbstractConfigurable
                       implements GameComponent,
                                  PlayerRoster.SideChangeListener {
  protected LaunchButton launch;
  protected CounterInventory results;
  protected JTree tree;

  public static final String VERSION = "2.1"; //$NON-NLS-1$
  public static final String HOTKEY = "hotkey"; //$NON-NLS-1$
  public static final String BUTTON_TEXT = "text"; //$NON-NLS-1$
  public static final String NAME = "name"; //$NON-NLS-1$
  public static final String ICON = "icon"; //$NON-NLS-1$
  public static final String TOOLTIP = "tooltip"; //$NON-NLS-1$
  // public static final String DEST = "destination";

  /*
   * For use in formatted text output.
   */
  protected String mapSeparator = "\n"; //$NON-NLS-1$
  protected String groupSeparator = "   "; //$NON-NLS-1$

  /*
   * Options Destination - Chat, Dialog, File.
   */
//  public static final String DEST_CHAT = "Chat Window";
//  public static final String DEST_DIALOG = "Dialog Window";
//  public static final String DEST_TREE = "Tree Window";
//  public static final String[] DEST_OPTIONS = { DEST_CHAT, DEST_DIALOG, DEST_TREE }; 
//  protected String destination = DEST_TREE;

  public static final String FILTER = "include"; //$NON-NLS-1$
  protected PropertyExpression piecePropertiesFilter = new PropertyExpression(); //$NON-NLS-1$

  public static final String GROUP_BY = "groupBy"; //$NON-NLS-1$
  protected String[] groupBy = {""}; //$NON-NLS-1$

  public static final String NON_LEAF_FORMAT = "nonLeafFormat"; //$NON-NLS-1$
  protected String nonLeafFormat = "$PropertyValue$"; //$NON-NLS-1$

  public static final String CENTERONPIECE = "centerOnPiece"; //$NON-NLS-1$
  protected boolean centerOnPiece = true;

  public static final String FORWARD_KEYSTROKE = "forwardKeystroke"; //$NON-NLS-1$
  protected boolean forwardKeystroke = true;

  public static final String SHOW_MENU = "showMenu"; //$NON-NLS-1$
  protected boolean showMenu = true;

  public static final String SIDES = "sides"; //$NON-NLS-1$
  protected String[] sides = null;

  public static final String KEYSTROKE = "keystroke"; //$NON-NLS-1$
  protected KeyStroke keyStroke = null;

  public static final String CUTBELOWROOT = "cutRoot"; //$NON-NLS-1$
  protected int cutBelowRoot = 0;

  public static final String CUTABOVELEAVES = "cutLeaves"; //$NON-NLS-1$
  protected int cutAboveLeaves = 0;

  public static final String LEAF_FORMAT = "leafFormat"; //$NON-NLS-1$
  protected String pieceFormat = "$PieceName$"; //$NON-NLS-1$

  public static final String PIECE_ZOOM = "pieceZoom"; //$NON-NLS-1$
  protected double pieceZoom = .25;

  public static final String DRAW_PIECES = "drawPieces"; //$NON-NLS-1$
  protected boolean drawPieces = true;

  public static final String FOLDERS_ONLY = "foldersOnly"; //$NON-NLS-1$
  protected boolean foldersOnly = false;
  
  public static final String SORT_PIECES = "sortPieces"; //$NON-NLS-1$
  protected boolean sortPieces = true;

  public static final String SORT_FORMAT = "sortFormat"; //$NON-NLS-1$
  protected String sortFormat = "$PieceName$"; //$NON-NLS-1$

  public static final String ALPHA = "alpha"; //$NON-NLS-1$
  public static final String LENGTHALPHA = "length"; //$NON-NLS-1$
  public static final String NUMERIC = "numeric"; //$NON-NLS-1$
  public static final String[] SORT_OPTIONS = { ALPHA, LENGTHALPHA, NUMERIC };

  protected String sortStrategy = ALPHA;
  
  public static final String SORTING = "sorting"; //$NON-NLS-1$
  
  protected JDialog frame;

  public Inventory() {
    ActionListener al = new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        launch();
      }
    };
    launch = new LaunchButton(null, TOOLTIP, BUTTON_TEXT, HOTKEY, ICON, al);
    setAttribute(NAME, Resources.getString("Inventory.inventory")); //$NON-NLS-1$
    setAttribute(BUTTON_TEXT, Resources.getString("Inventory.inventory")); //$NON-NLS-1$
    setAttribute(TOOLTIP, Resources.getString("Inventory.show_inventory")); //$NON-NLS-1$
    setAttribute(ICON, "/images/inventory.gif"); //$NON-NLS-1$
    launch.setEnabled(false);
    launch.setVisible(false);
  }

  public static String getConfigureTypeName() {
    return "Game Piece Inventory Window";
  }

  public void addTo(Buildable b) {
  // Support for players changing sides
  PlayerRoster.addSideChangeListener(this);
    launch.setAlignmentY(0.0F);
    GameModule.getGameModule().getToolBar().add(getComponent());
    GameModule.getGameModule().getGameState().addGameComponent(this);
    frame = new JDialog(GameModule.getGameModule().getFrame());
    frame.setTitle(getConfigureName());
    String key = "Inventory." + getConfigureName(); //$NON-NLS-1$
    GameModule.getGameModule().getPrefs().addOption(new PositionOption(key, frame));
    frame.setLayout(new BoxLayout(frame.getContentPane(), BoxLayout.Y_AXIS));
    frame.add(initTree());
    frame.add(initButtons());
    frame.setSize(250, 350);
  }

  /**
   * Construct an explorer like interface for the selected counters
   */
  protected Component initTree() {
    // Initialize the tree to be displayed from the results tree
    tree = new JTree();
    tree.setRootVisible(false);
    tree.setShowsRootHandles(true);
    tree.setCellRenderer(initTreeCellRenderer());
    tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
    // If wanted center on a selected counter
    tree.addTreeSelectionListener(new TreeSelectionListener() {
      public void valueChanged(TreeSelectionEvent e) {
        if (centerOnPiece) {
          GamePiece piece = getSelectedCounter();
          if (piece != null && piece.getMap() != null)
            piece.getMap().centerAt(piece.getPosition());
        }
      }
    });
    tree.addMouseListener(new MouseAdapter() {
      public void mouseReleased(MouseEvent e) {
        if (showMenu && e.isMetaDown()) {
          final TreePath path = tree.getPathForLocation(e.getX(), e.getY());
          if (path != null) {
            if (path.getLastPathComponent() instanceof CounterNode) {
              final CounterNode node = (CounterNode) path.getLastPathComponent();
              final GamePiece piece = node.getCounter().getPiece();
              if (piece != null) {
                JPopupMenu menu = MenuDisplayer.createPopup(piece);
                menu.addPropertyChangeListener("visible", new PropertyChangeListener() { //$NON-NLS-1$
                  public void propertyChange(PropertyChangeEvent evt) {
                    if (Boolean.FALSE.equals(evt.getNewValue())) {
                      SwingUtilities.invokeLater(new Runnable() {
                        public void run() {
                          refresh();
                        }
                      });
                    }
                  }
                });
                menu.show(tree, e.getX(), e.getY());
              }
            }
          }
        }
      }
    });
    tree.addKeyListener(new HotKeySender());

    JScrollPane scrollPane = new ScrollPane(tree,
      JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
      JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
    refresh();
    return scrollPane;
  }

  protected TreeCellRenderer initTreeCellRenderer() {
    return new DefaultTreeCellRenderer() {

      private static final long serialVersionUID = -250332615261355856L;

      public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) {
        super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf && !foldersOnly, row, hasFocus);
        if (value instanceof CounterNode) {
          final GamePiece piece = ((CounterNode) value).getCounter().getPiece();
          if (piece != null) {
            final Rectangle r = piece.getShape().getBounds();
            r.x = (int) Math.round(r.x * pieceZoom);
            r.y = (int) Math.round(r.y * pieceZoom);
            r.width = (int) Math.round(r.width * pieceZoom);
            r.height = (int) Math.round(r.height * pieceZoom);
            setIcon(new Icon() {

              public int getIconHeight() {
                return r.height;
              }

              public int getIconWidth() {
                return r.width;
              }

              public void paintIcon(Component c, Graphics g, int x, int y) {
                piece.draw(g, -r.x, -r.y, c, pieceZoom);
              }

            });
          }
        }
        return this;
      }

    };
  }

  protected Component initButtons() {
    Box buttonBox = Box.createHorizontalBox();
    // Written by Scot McConnachie.
    JButton writeButton = new JButton(Resources.getString(Resources.SAVE));
    writeButton.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        inventoryToText();
      }
    });
    buttonBox.add(writeButton);
    JButton refreshButton = new JButton(Resources.getString(Resources.REFRESH));
    refreshButton.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        refresh();
      }
    });
    buttonBox.add(refreshButton);
    JButton closeButton = new JButton(Resources.getString(Resources.CLOSE));
    closeButton.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        frame.setVisible(false);
      }
    });
    buttonBox.add(closeButton);
    return buttonBox;
  }

  /**
   * @author Scot McConnachie. 
   * Writes the inventory text data to a user selected file.
   * This allows a module designer to use Inventory to create customized text 
   * reports from the game.
   * @author spindler
   * Changed FileChooser to use the new Vassal.tool.FileChooser
   * Changed Separator before getResultString call
   * TODO add check for existing file
   * TODO rework text display of Inventory
   */
  protected void inventoryToText() {
    final StringBuilder output = new StringBuilder(""); //$NON-NLS-1$
    FileChooser fc = GameModule.getGameModule().getFileChooser();
    if (fc.showSaveDialog() == FileChooser.CANCEL_OPTION) return;

    final File file = fc.getSelectedFile();

    // TODO replace this hack
    mapSeparator = System.getProperty("line.separator"); //$NON-NLS-1$
    // groupSeparator = mapSeparator + "  ";
    // groupSeparator = " ";
    output.append(results.getResultString()); 
    // .substring(1).replaceAll(
  //      mapSeparator, System.getProperty("line.separator"));

    PrintWriter p = null; 
    try {
      p = new PrintWriter(new BufferedWriter(new FileWriter(file)));

      p.print(output);
      p.close();     
 
      final Command c = new Chatter.DisplayText(
        GameModule.getGameModule().getChatter(),
        Resources.getString("Inventory.wrote", file)  //$NON-NLS-1$
      );
      c.execute();
    }
    catch (IOException e) {
      WriteErrorDialog.error(e, file);
    }
    finally {
      IOUtils.closeQuietly(p);
    }
  }

  public GamePiece getSelectedCounter() {
    GamePiece piece = null;
    CounterNode node = (CounterNode) tree.getLastSelectedPathComponent();
    if (node != null && node.isLeaf()) {
      piece = node.getCounter().getPiece();
    }
    return piece;
  }

  protected Component getComponent() {
    return launch;
  }

  public void removeFrom(Buildable b) {
    GameModule.getGameModule().getToolBar().remove(getComponent());
    GameModule.getGameModule().getGameState().removeGameComponent(this);
  }

  public void add(Buildable b) {
  }

  public void remove(Buildable b) {
  }

  protected void launch() {
    refresh();
    frame.setVisible(true);
  }

  private void buildTreeModel() {
    // Initialize all pieces with CurrentBoard correctly.
    for (VASSAL.build.module.Map m : VASSAL.build.module.Map.getMapList()) {
      m.getPieces();
    }
    
    final ArrayList<String> path = new ArrayList<String>();
    for (int i = 0; i < groupBy.length; i++)
      path.add(groupBy[i]);
    results = new CounterInventory(
      new Counter(this.getConfigureName()), path, sortPieces);

    final PieceIterator pi = new PieceIterator(
      GameModule.getGameModule().getGameState().getAllPieces().iterator(),
      piecePropertiesFilter
    );

    while (pi.hasMoreElements()) {
      final ArrayList<String> groups = new ArrayList<String>();
      final GamePiece p = pi.nextPiece();

      for (int i = 0; i < groupBy.length; i++) {
        if (groupBy[i].length() > 0) {
          String prop = (String) p.getProperty(groupBy[i]);
          if (prop != null)
            groups.add(prop);
        }
      }

      int count = 1;
      if (nonLeafFormat.length() > 0)
        count = getTotalValue(p);

      final Counter c = new Counter(p, groups, count, pieceFormat, sortFormat);
      // Store
      results.insert(c);
    }
  }

  protected int getTotalValue(GamePiece p) {
    String s = (String) p.getProperty(nonLeafFormat);
    int count = 1;
    try {
      count = Integer.parseInt(s);
    }
    catch (NumberFormatException e) {
      // Count each piece as 1 if the property isn't a number
      count = 1;
    }

    return count;
  }

  public VASSAL.build.module.documentation.HelpFile getHelpFile() {
    return HelpFile.getReferenceManualPage("Inventory.htm"); //$NON-NLS-1$
  }
  
  public Class<?>[] getAllowableConfigureComponents() {
    return new Class<?>[0];
  }

  public String[] getAttributeDescriptions() {
    return new String[] {
      "Name:  ",
      "Button text:  ",
      "Tooltip text:  ",
      "Button icon:  ",
      "Hotkey:  ",
      // "Display",
      "Show only pieces matching these properties:  ",
      "Sort and Group By Properties",
      "Label for folders:  ",
      "Show only folders?",
      "Label for pieces:  ",
      "Sort?",
      "Label for sort:  ",
      "Sorting method:  ",
      "Center on selected piece?",
      "Forward key strokes to selected piece?",
      "Show right-click menu of piece?",
      "Draw piece images?",
      "Zoom factor:  ",
      "Available to these sides"
    };
  }

  public Class<?>[] getAttributeTypes() {
    return new Class<?>[] {
      String.class,
      String.class,
      String.class,
      IconConfig.class,
      KeyStroke.class,
      // DestConfig.class, 
      PropertyExpression.class,
      String[].class,
      String.class,
      Boolean.class,
      PieceFormatConfig.class,
      Boolean.class,
      PieceFormatConfig.class, 
      SortConfig.class,
      Boolean.class,
      Boolean.class,
      Boolean.class,
      Boolean.class, 
      Double.class,
      String[].class
    };
  }

  public String[] getAttributeNames() {
    return new String[] {
      NAME,
      BUTTON_TEXT,
      TOOLTIP,
      ICON,
      HOTKEY,
   // DEST, 
      FILTER,
      GROUP_BY,
      NON_LEAF_FORMAT,
      FOLDERS_ONLY,
      LEAF_FORMAT,
      SORT_PIECES,
      SORT_FORMAT,  
      SORTING,
      CENTERONPIECE,
      FORWARD_KEYSTROKE,
      SHOW_MENU,
      DRAW_PIECES,
      PIECE_ZOOM,
      SIDES
    };
  }

  public static class IconConfig implements ConfigurerFactory {
    public Configurer getConfigurer(AutoConfigurable c, String key, String name) {
      return new IconConfigurer(key, name, "/images/inventory.gif"); //$NON-NLS-1$
    }
  }

  public static class PieceFormatConfig implements TranslatableConfigurerFactory {
    public Configurer getConfigurer(AutoConfigurable c, String key, String name) {
      return new GamePieceFormattedStringConfigurer(key, name);
    }
  }
//  public static class DestConfig implements ConfigurerFactory {
//    public Configurer getConfigurer(AutoConfigurable c, String key, String name) {
//      return new StringEnumConfigurer(key, name, DEST_OPTIONS);
//    }
//  }

  public static class SortConfig implements ConfigurerFactory {
    public Configurer getConfigurer(AutoConfigurable c, String key, String name) {
      return new StringEnumConfigurer(key,name,SORT_OPTIONS);
    }
  }

  public void setAttribute(String key, Object o) {
    if (NAME.equals(key)) {
      setConfigureName((String) o);
    }
    else if (FILTER.equals(key)) {
      piecePropertiesFilter.setExpression((String) o);
    }
    else if (GROUP_BY.equals(key)) {
      if (o instanceof String) {
        o = StringArrayConfigurer.stringToArray((String) o);
      }
      groupBy = (String[]) o;
    }
    else if (NON_LEAF_FORMAT.equals(key)) {
      nonLeafFormat = (String) o;
    }
    else if (LEAF_FORMAT.equals(key)) {
      pieceFormat = (String) o;
    }
    else if (CENTERONPIECE.equals(key)) {
      centerOnPiece = getBooleanValue(o);
    }
    else if (SHOW_MENU.equals(key)) {
      showMenu = getBooleanValue(o);
    }
    else if (DRAW_PIECES.equals(key)) {
      drawPieces = getBooleanValue(o);
    }
    else if (FOLDERS_ONLY.equals(key)) {
      foldersOnly = getBooleanValue(o);
      cutAboveLeaves = foldersOnly ? 1 : 0;
    }
    else if (PIECE_ZOOM.equals(key)) {
      if (o instanceof String) {
        o = Double.valueOf((String) o);
      }
      pieceZoom = ((Double) o).doubleValue();
    }
    else if (FORWARD_KEYSTROKE.equals(key)) {
      forwardKeystroke = getBooleanValue(o);
    }
    else if (SIDES.equals(key)) {
      if (o instanceof String) {
        o = StringArrayConfigurer.stringToArray((String) o);
      }
      sides = (String[]) o;
    }
    else if (KEYSTROKE.equals(key)) {
      if (o instanceof String) {
        o = HotKeyConfigurer.decode((String) o);
      }
      keyStroke = (KeyStroke) o;
    }
    else if (CUTBELOWROOT.equals(key)) {
      if (o instanceof String)
        cutBelowRoot = Integer.parseInt((String) o);
      else {
        cutBelowRoot = ((Integer) o).intValue();
      }
    }
    else if (CUTABOVELEAVES.equals(key)) {
      if (o instanceof String)
        cutAboveLeaves = Integer.parseInt((String) o);
      else {
        cutAboveLeaves = ((Integer) o).intValue();
      }
    }
    else if (SORT_PIECES.equals(key)) {
        sortPieces = getBooleanValue(o);
    }
    else if (SORT_FORMAT.equals(key)) {
      sortFormat = (String) o;
    }
    else if (SORTING.equals(key)) {
      sortStrategy = (String) o;
    }
//    else if (DEST.equals(key)) {
//      destination = (String) o;
//    }

    else {
      launch.setAttribute(key, o);
    }
  }

  private VisibilityCondition piecesVisible = new VisibilityCondition() {
    public boolean shouldBeVisible() {
      return !foldersOnly;
    }
  };

  public VisibilityCondition getAttributeVisibility(String name) {
    if (PIECE_ZOOM.equals(name)) {
      return new VisibilityCondition() {
        public boolean shouldBeVisible() {
          return drawPieces && !foldersOnly;
        }
      };
    }
    else if (LEAF_FORMAT.equals(name) || CENTERONPIECE.equals(name) || FORWARD_KEYSTROKE.equals(name) || SHOW_MENU.equals(name) || DRAW_PIECES.equals(name)) {
      return piecesVisible;
    }
    else {
      return super.getAttributeVisibility(name);
    }
  }

  /**
   * @param o
   */
  protected boolean getBooleanValue(Object o) {
    if (o instanceof String) {
      o = Boolean.valueOf((String) o);
    }
    return ((Boolean) o).booleanValue();
  }

  public String getAttributeValueString(String key) {
    if (NAME.equals(key)) {
      return getConfigureName();
    }
    else if (FILTER.equals(key)) {
      return piecePropertiesFilter.getExpression();
    }
    else if (GROUP_BY.equals(key)) {
      return StringArrayConfigurer.arrayToString(groupBy);
    }
    else if (NON_LEAF_FORMAT.equals(key)) {
      return nonLeafFormat;
    }
    else if (LEAF_FORMAT.equals(key)) {
      return pieceFormat;
    }
    else if (CENTERONPIECE.equals(key)) {
      return String.valueOf(centerOnPiece);
    }
    else if (FORWARD_KEYSTROKE.equals(key)) {
      return String.valueOf(forwardKeystroke);
    }
    else if (SHOW_MENU.equals(key)) {
      return String.valueOf(showMenu);
    }
    else if (DRAW_PIECES.equals(key)) {
      return String.valueOf(drawPieces);
    }
    else if (FOLDERS_ONLY.equals(key)) {
      return String.valueOf(foldersOnly);
    }
    else if (PIECE_ZOOM.equals(key)) {
      return String.valueOf(pieceZoom);
    }
    else if (SIDES.equals(key)) {
      return StringArrayConfigurer.arrayToString(sides);
    }
    else if (KEYSTROKE.equals(key)) {
      return HotKeyConfigurer.encode(keyStroke);
    }
    else if (CUTBELOWROOT.equals(key)) {
      return cutBelowRoot + ""; //$NON-NLS-1$
    }
    else if (CUTABOVELEAVES.equals(key)) {
      return cutAboveLeaves + ""; //$NON-NLS-1$
    }
    else if (SORT_PIECES.equals(key)) {
        return sortPieces + ""; //$NON-NLS-1$
    }
    else if (SORT_FORMAT.equals(key)) {
      return sortFormat;
    }
    else if (SORTING.equals(key)) {
      return sortStrategy;
    }
//    else if (DEST.equals(key)) {
//      return destination;
//    }

    else {
      return launch.getAttributeValueString(key);
    }
  }

  public Command getRestoreCommand() {
    return null;
  }

  public void setup(boolean gameStarting) {
    launch.setEnabled(gameStarting && enabledForPlayersSide());
    if (gameStarting) 
      setupLaunch();
  }
  
  protected void setupLaunch() {
    launch.setEnabled(enabledForPlayersSide());
    launch.setVisible(launch.isEnabled());
  }
  
  /**
   * Update inventory according to change of side.
   */
  public void sideChanged(String oldSide, String newSide) {
    setupLaunch();
  }

  protected boolean enabledForPlayersSide() {
    if (sides == null || sides.length == 0)
      return true;
    for (int i = 0; i < sides.length; i++) {
      if (sides[i].equalsIgnoreCase(PlayerRoster.getMySide()))
        return true;
    }
    return false;
  }

//  protected void executeCommand() {
//    if (destination.equals(DEST_CHAT)) {
//      Command c = new NullCommand();
//      String res[] = results.getResultStringArray();
//
//      for (int i = 0; i < res.length; i++) {
//        c.append(new Chatter.DisplayText(GameModule.getGameModule().getChatter(), res[i]));
//      }
//      c.execute();
//      GameModule.getGameModule().sendAndLog(c);
//    }
//    else if (destination.equals(DEST_DIALOG)) {
//      String res[] = results.getResultStringArray();
//      String text = "";
//      for (int i = 0; i < res.length; i++) {
//        text += res[i] + "\n";
//      }
//      JTextArea textArea = new JTextArea(text);
//      textArea.setEditable(false);
//
//      JScrollPane scrollPane = new ScrollPane(textArea,
//         JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
//         JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
//
//      JOptionPane.showMessageDialog(GameModule.getGameModule().getFrame(), scrollPane, getConfigureName(), JOptionPane.PLAIN_MESSAGE);
//    }
//    else if (destination.equals(DEST_TREE)) {
//      initTree();
//    }
//  }

  /**
   * @return Command which only has some text in. The actual processing is done
   *         within the pieces.
   */
  protected Command sendHotKeyToPieces(final KeyStroke keyStroke) {
    Command c = new NullCommand();
    final TreePath[] tp = tree.getSelectionPaths();
    // set to not get duplicates
    HashSet<GamePiece> pieces = new HashSet<GamePiece>();
    for (int i = 0; i < tp.length; i++) {
      CounterNode node = (CounterNode) tp[i].getLastPathComponent();
      if (node.isLeaf()) {
        pieces.add(node.getCounter().getPiece());
      }
      else {
        for (Iterator<CounterNode> j = node.iterator(); j.hasNext();) {
          final CounterNode childNode = j.next();
          if (childNode.isLeaf())
            pieces.add(childNode.getCounter().getPiece());
        }
      }
    }

    for (GamePiece piece : pieces) {
      GameModule.getGameModule().sendAndLog(piece.keyEvent(keyStroke));
    }
    return c;
  }

  protected Command myUndoCommand() {
    return null;
  }

  private void refresh() {
    // Make an attempt to keep the same nodes expanded
    HashSet<String> expanded = new HashSet<String>();
    for (int i = 0, n = tree.getRowCount(); i < n; ++i) {
      if (tree.isExpanded(i)) {
        expanded.add(tree.getPathForRow(i).getLastPathComponent().toString());
      }
    }
    buildTreeModel();
    tree.setModel(results);

    for (int i = 0; i < tree.getRowCount(); ++i) {
      if (expanded.contains(
            tree.getPathForRow(i).getLastPathComponent().toString())) {
        tree.expandRow(i);
      }
    }
  }

  public class HotKeySender implements KeyListener {
    BoundsTracker tracker;

    public void keyCommand(KeyStroke stroke) {
      if (forwardKeystroke) {
        CounterNode node = (CounterNode) tree.getLastSelectedPathComponent();
        if (node != null) {
          Command comm = getCommand(node, stroke);
          if (comm != null && !comm.isNull()) {
            tracker.repaint();
            GameModule.getGameModule().sendAndLog(comm);
            tracker = null;
            refresh();
          }
        }
      }
    }

    protected Command getCommand(CounterNode node, KeyStroke stroke) {
      GamePiece p = node.getCounter() == null ? null : node.getCounter().getPiece();
      Command comm = null;
      if (p != null) {
        // Save state first
        p.setProperty(Properties.SNAPSHOT, PieceCloner.getInstance().clonePiece(p));
        if (tracker == null) {
          tracker = new BoundsTracker();
          tracker.addPiece(p);
        }
        comm = p.keyEvent(stroke);
      }
      else {
        comm = new NullCommand();
        for (int i = 0, n = node.getChildCount(); i < n; ++i) {
          comm = comm.append(getCommand((CounterNode) node.getChild(i), stroke));
        }
      }
      return comm;
    }

    public void keyPressed(KeyEvent e) {
      keyCommand(KeyStroke.getKeyStrokeForEvent(e));
    }

    public void keyReleased(KeyEvent e) {
      keyCommand(KeyStroke.getKeyStrokeForEvent(e));
    }

    public void keyTyped(KeyEvent e) {
      keyCommand(KeyStroke.getKeyStrokeForEvent(e));
    }

  }
  
//  public static class Dest extends StringEnum {
//    public String[] getValidValues(AutoConfigurable target) {
//      return new String[] {DEST_CHAT, DEST_DIALOG, DEST_TREE};
//    }
//  }
//  public static class SortStrategy extends StringEnum {
//    public String[] getValidValues(AutoConfigurable target) {
//      return new String[] {ALPHA, LENGTHALPHA, NUMERIC};
//    }
//  }

  /**
   * Holds static information of and a reference to a gamepiece. Pay attention
   * to the equals method. It checks if two pieces can be found under the same
   * path!
   * 
   * @author Brent Easton and Torsten Spindler
   * 
   */
  public class Counter implements PropertySource {
    // The gamepiece is stored here to allow dynamic changes of name, location
    // and so forth
    protected GamePiece piece;
    protected List<String> groups;
    protected int value;
    // Only used when no piece is defined
    protected String localName;
    protected FormattedString format;
    protected FormattedString sortingFormat;
    protected CounterNode node;

    public Counter(String name) {
      this(null, null, 0, nonLeafFormat, sortFormat);
      this.localName = name;
    }

    public Counter(GamePiece piece, List<String> groups, int value,
                   String format, String sortFormat) {
      this.piece = piece;
      this.value = value;
      this.groups = groups;
      this.format = new FormattedString(format);
      this.sortingFormat = new FormattedString(sortFormat);
    }

    // piece can be null, so provide a alternate name
    public String getName() {
      if (piece != null)
        return piece.getName();
      return localName;
    }

    public int hashCode() {
      return getName().hashCode();
    }

    public String toString() {
      return format.getLocalizedText(this);
    }
    public String toSortKey() {
      return sortingFormat.getLocalizedText(this);
    }

    public String[] getPath() {
      return groups.toArray(new String[groups.size()]);
    }

    public int getValue() {
      return value;
    }

    public void setValue(int value) {
      this.value = value;
    }

    public GamePiece getPiece() {
      return piece;
    }

    public void setPiece(GamePiece piece) {
      this.piece = piece;
    }

    public boolean equals(Object o) {
      if (!(o instanceof Counter))
        return false;
      Counter c = (Counter) o;
      return getPath().equals(c.getPath());
    }

    public Object getProperty(Object key) {
      Object value = null;
      String s = (String) key;
      if (s.startsWith("sum_")) { //$NON-NLS-1$
        if (piece != null) {
          value = piece.getProperty(s.substring(4));
        }
        else {
          int sum = 0;
          int n = results.getChildCount(node);
          for (int i = 0; i < n; ++i) {
            try {
              CounterNode childNode = (CounterNode) results.getChild(node, i);
              sum += Integer.parseInt((String) (childNode.getCounter()).getProperty(key));
            }
            catch (NumberFormatException e) {
              // Count each piece as 1 if property isn't a number
              sum++;
            }
          }
          value = String.valueOf(sum);
        }
      }
      else if ("PropertyValue".equals(s)) { //$NON-NLS-1$
        return localName;
      }
      else if (piece != null) {
        value = piece.getProperty(key);
      }
      return value;
    }

    public Object getLocalizedProperty(Object key) {
      return getProperty(key);
    }
    
    public void setNode(CounterNode node) {
      this.node = node;
    }

  }

  /**
   * Filter to select pieces required
   * 
   * @author Brent Easton
   */
  protected class Selector implements PieceFilter {

    protected PieceFilter filter;

    public Selector(String include) {
      if (include != null && include.length() > 0) {
        filter = PropertiesPieceFilter.parse(include);
      }
    }

    public boolean accept(GamePiece piece) {
      // Honor visibility

      if (Boolean.TRUE.equals(piece.getProperty(Properties.INVISIBLE_TO_ME)))
        return false;

      // Ignore Stacks, pieces are reported individually from GameState
      if (piece instanceof Stack)
        return false;

      // Don't report pieces with no map
      if (piece.getMap() == null)
        return false;

      // Check for marker
      if (filter != null) {
        return filter.accept(piece);
      }

      // Default Accept piece
      return true;
    }

  }

  /**
   * CounterNode for the result tree.
   * 
   * @author spindler
   * 
   */
  public class CounterNode implements Comparable<CounterNode> {
    protected final String entry;
    protected final Counter counter;
    protected List<CounterNode> children;
    protected int level;

    // protected int depth;

    public CounterNode(final String entry,
                       final Counter counter,
                       final int level) { 
      this(entry, counter);
      this.level = level;
    }

    protected CounterNode(String entry, Counter counter) {
      this.level = 0;
      // this.depth = 0;
      this.entry = entry;
      this.counter = counter;
      counter.setNode(this);
      children = new ArrayList<CounterNode>();
    }

    public String toString() {
      if (counter != null)
        return counter.toString();
      return getEntry();
    }

    /**
     * Places a separator between elements.
     * The separator consists of an indent and a linebreak.
     * @return
     */
    protected String separator() {
      final StringBuilder sep = new StringBuilder();

      if (getLevel() > 0)
        sep.append(mapSeparator);
      for (int i = 0; i < getLevel(); i++)
        sep.append(groupSeparator);
      return sep.toString();
    }

    public String toResultString() {
      final StringBuilder name = new StringBuilder();

      name.append(separator());

      if (counter != null)
        name.append(counter.toString());
      else
        name.append(getEntry());

      for (CounterNode child : children) {
        name.append(child.toResultString());
      }
      return name.toString();
    }

    public String getEntry() {
      return entry;
    }

    public Counter getCounter() {
      return counter;
    }

    public void addChild(final CounterNode counterNode, boolean sort) {
      children.add(counterNode);
      if (sort)
        sortChildren();
    }

    public void addChild(final int i,
                         final CounterNode counterNode, boolean sort) {
      children.add(i, counterNode);
      if (sort)
        sortChildren();
    }

    protected void sortChildren() {
      if (sortStrategy.equals(ALPHA)) 
        Collections.sort(children);
      else if (sortStrategy.equals(LENGTHALPHA))
        Collections.sort(children, new LengthAlpha());
      else if (sortStrategy.equals(NUMERIC))
        Collections.sort(children, new Numerical());
      else 
        Collections.sort(children);
    }

    public void removeChild(CounterNode child) {
      children.remove(child);
    }

    public int getChildCount() {
      return children.size();
    }

    public boolean isLeaf() {
      return children.isEmpty();
    }

    public Object getChild(final int index) {
      return children.get(index);
    }

    public int getIndexOfChild(final Object child) {
      return children.indexOf(child);
    }

    public int getLevel() {
      return level;
    }

    public void setLevel(final int level) {
      this.level = level;
    }

    public int updateValues() {
      int value = 0;
      if (counter != null)
        value = counter.getValue();

      // inform children about update
      for (CounterNode child : children) {
        value += child.updateValues();
      }

      // save new value in counter
      counter.setValue(value);
      return counter.getValue();
    }

    public Iterator<CounterNode> iterator() {
      return children.iterator();
    }

    public void cutLevel(int cut) {
      if (cut == 0) {
        children.clear();
        return;
      }

      for (CounterNode child : children) {
        child.cutLevel(cut - 1);
      }
    }

    public void cutLeaves() {
     ArrayList<CounterNode> toBeRemoved = new ArrayList<CounterNode>();
     for (CounterNode child : children) {
        if (child.isLeaf())
          toBeRemoved.add(child);
        else
          child.cutLeaves();
      }
      children.removeAll(toBeRemoved);
    }

    /**
     * Compare this CounterNode to another one based on the respective SortKeys.
     */
    public int compareTo(CounterNode node) {
      return this.toSortKey().compareTo(node.toSortKey());
    }

    /**
     * Sort this CounterNode by the counters key, if no counter use the label.
     * If no children, use the name of the counterNode, probably could be
     * $PropertyValue$ as well?
     *
     * @return key as String
     */
    protected String toSortKey() {
      String sortKey = getEntry();
      if (counter != null)
        sortKey = counter.toSortKey();
      if (!children.isEmpty())
        sortKey = toString();
      return sortKey;
    }

    /**
     * Base class for comparing two CounterNodes. Contains methods for
     * sanity checking of arguments and comparing non-sane arguments.
     *
     * @author spindler
     */
    protected class CompareCounterNodes {
      /**
       * Sanity check for arguments.
       * @param arg0
       * @param arg1
       * @return true if arguments looks processable, false else
       */
      protected boolean argsOK(Object arg0, Object arg1) {
        return (arg0 != null && 
                arg1 != null && 
                arg0 instanceof CounterNode && 
                arg1 instanceof CounterNode);
        }
    
      protected int compareStrangeArgs(Object arg0, Object arg1) {
        if (arg1.equals(arg1))
          return 0;
  
        if (arg0 == null)
          return 1;
        if (arg1 == null)
          return -1;
        if (arg0 instanceof CounterNode && !(arg1 instanceof CounterNode))
          return -1;
        if (arg1 instanceof CounterNode && !(arg0 instanceof CounterNode))
          return 1;
   
        throw new IllegalArgumentException(
          "These CounterNodes are not strange!");
      }
    }
  
    /**
     * Compare two CounterNodes based on the alphanumerical order of their
     * SortKeys.
     *
     * @author spindler
     */
    protected class Alpha extends CompareCounterNodes
                          implements Comparator<CounterNode> {

      public int compare(CounterNode left, CounterNode right) {
        if (!argsOK(left, right))
          return compareStrangeArgs(left, right);
  
        return left.compareTo(right);
      }  
    }

    /**
     * Compare two CounterNodes based on the first integer value found in
     * their SortKeys. If a CounterNodes SortKey does not contain an integer
     * at all it is assigned the lowest available integer. 
     *
     * @author spindler
     */
    protected class Numerical extends CompareCounterNodes
                              implements Comparator<CounterNode> {
      protected final String regex =  "\\d+"; //$NON-NLS-1$
      protected final Pattern p = Pattern.compile(regex);

      /**
       * Get first integer in key, if any. Otherwise return lowest possible
       * integer. 
       *
       * @param key is a string that may or may not contain an integer value
       * @return the value of the integer found, min(Integer) otherwise
       * 
       */
      protected int getInt(String key) {
        int found = Integer.MIN_VALUE;
        Matcher match = p.matcher(key);

        if (!match.find()) {
          // return minimum value
          return found;
        }
        int start = match.start();
        found = Integer.parseInt(key.substring(start, match.end()));
 
        // Check for sign
        if ( (start > 0) && (key.charAt(start-1) == '-') ) {
          // negative integer found
          // FIXME: Is this a safe operation? What happens when
          // MAX_VALUE * -1 < MIN_VALUE?
          found *= -1;
        }
        return found;
      }

      /**
       * Compare two CounterNodes based on the first integer found in their
       * SortKeys.
       */
      public int compare(CounterNode left, CounterNode right) {
        if (!argsOK(left, right))
          return compareStrangeArgs(left, right);
  
         int l = getInt(left.toSortKey());
         int r = getInt(right.toSortKey());

        if (l < r)
          return -1;
        if (l > r)
          return 1;

        return 0;
      }
    }

    /**
     * Compare two CounterNodes based on the length of their SortKeys and
     * alphanumerical sorting.
     *
     * @author spindler
     */
    protected class LengthAlpha extends CompareCounterNodes
                                implements Comparator<CounterNode> {
      public int compare(CounterNode left, CounterNode right) {
        if (!argsOK(left, right))
          return compareStrangeArgs(left, right);
        
        int leftLength = left.toSortKey().length();
        int rightLength = right.toSortKey().length();
        if (leftLength < rightLength)
          return -1;
        if (leftLength > rightLength)
          return 1;
        // Native comparison
        return (left.compareTo(right));
      }
    }
  }

  public class CounterInventory implements TreeModel {
    // Needed for TreeModel
    protected List<TreeModelListener> treeModelListeners =
      new ArrayList<TreeModelListener>();
    // This contains shortcuts to the nodes of the tree
    protected Map<String,CounterNode> inventory;
    // The start of the tree
    protected CounterNode root;
    // Text view of the tree
    protected String resultString;
    // The path determines where a counter is found in the tree
    protected List<String> path;
    // Small speed up, only update values in tree when something has changed
    protected boolean changed;
    // Sort the tree
    protected boolean sort;

    public CounterInventory(Counter c, List<String> path, boolean sort) {
      this.root = new CounterNode(c.getName(), c);
      this.path = path;
      this.inventory = new HashMap<String,CounterNode>();
      this.sort = sort;
      changed = true;
    }

    /**
     * insert counter into the tree. It is not sorted in any way.
     * 
     * @param counter
     */
    public void insert(Counter counter) {
      final String[] path = counter.getPath();
      final StringBuilder hash = new StringBuilder();

      CounterNode insertNode = root;
      CounterNode newNode = null;
      for (int j = 0; path != null && j < path.length; j++) {
        hash.append(path[j]);
        if (inventory.get(hash.toString()) == null) {
          newNode = new CounterNode(
            path[j], new Counter(path[j]), insertNode.getLevel() + 1);
          inventory.put(hash.toString(), newNode);
          insertNode.addChild(newNode, sort);
        }
        insertNode = inventory.get(hash.toString());
      }
      newNode = new CounterNode(
        counter.toString(), counter, insertNode.getLevel() + 1);
      insertNode.addChild(newNode, sort);
      changed = true;
    }

    private void updateEntries() {
      root.updateValues();
    }

    /**
     * Deliver information of the tree as text.
     * 
     * @return String
     */
    public String getResultString() {
      if (changed)
        updateTree();
      changed = false;
      return root.toResultString();
    }

    /**
     * Compatibility for DisplayResults class.
     * 
     * @return String[]
     */
    public String[] getResultStringArray() {
      return new String[] {getResultString()};
    }

    public Object getRoot() {
      if (changed)
        updateTree();
      return root;
    }

    private void updateTree() {
      updateEntries();
      if (cutBelowRoot > 0)
        root.cutLevel(cutBelowRoot);
      for (int i = cutAboveLeaves; i > 0; i--)
        root.cutLeaves();
      changed = false;
    }

    public int getChildCount(Object parent) {
      CounterNode counter = (CounterNode) parent;
      return counter.getChildCount();
    }

    public boolean isLeaf(Object node) {
      CounterNode counter = (CounterNode) node;
      return counter.isLeaf();
    }

    public void addTreeModelListener(TreeModelListener l) {
      treeModelListeners.add(l);
    }

    public void removeTreeModelListener(TreeModelListener l) {
      treeModelListeners.remove(l);
    }

    public void fireNodesRemoved(Object[] path, int[] childIndices,
                                 Object[] children) {
      TreeModelEvent e = new TreeModelEvent(this, path, childIndices, children);
      for (TreeModelListener l : treeModelListeners) {
        l.treeNodesRemoved(e);
      }
    }

    public Object getChild(Object parent, int index) {
      CounterNode counter = (CounterNode) parent;
      return counter.getChild(index);
    }

    public int getIndexOfChild(Object parent, Object child) {
      CounterNode counter = (CounterNode) parent;
      return counter.getIndexOfChild(child);
    }

    public void valueForPathChanged(TreePath path, Object newValue) {
      throw new UnsupportedOperationException();
    }
  }
}
