/*
* $Id$
*
* Copyright (c) 2000-2003 by Rodney Kinney
*
* 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.counters;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.geom.Area;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import VASSAL.build.BadDataReport;
import VASSAL.build.GameModule;
import VASSAL.build.module.GameState;
import VASSAL.build.module.Map;
import VASSAL.build.module.map.StackMetrics;
import VASSAL.command.Command;
import VASSAL.tools.ArrayUtils;
import VASSAL.tools.EnumeratedIterator;
import VASSAL.tools.ErrorDialog;
import VASSAL.tools.SequenceEncoder;
/**
* A collection of GamePieces which can be moved as a single unit
*/
public class Stack implements GamePiece, StateMergeable {
public static final String TYPE = "stack";
protected static final int INCR = 5;
protected GamePiece[] contents = new GamePiece[INCR];
protected int pieceCount = 0;
protected Point pos = new Point(0, 0);
private String id;
private boolean expanded = false;
protected Map map;
private static StackMetrics defaultMetrics;
public Stack() {
this(null);
}
public Stack(GamePiece p) {
if (p != null) {
setMap(p.getMap());
setPosition(new Point(p.getPosition()));
add(p);
}
}
public Iterator<GamePiece> getPiecesIterator() {
return new AllPieceIterator();
}
/**
* @return an Enumeration of the pieces in the stack, from the bottom up This
* is a clone of the contents so add/remove operations during read
* won't affect it.
* @deprecated
*/
@Deprecated
public Enumeration<GamePiece> getPieces() {
return new EnumeratedIterator<GamePiece>(new AllPieceIterator());
// return new AllPieceEnum();
}
public Iterator<GamePiece> getPiecesReverseIterator() {
return new ReversePieceIterator();
}
/**
* Return an enumeration of the pieces in the start, from the top down
*
* @return
* @deprecated
*/
@Deprecated
public Enumeration<GamePiece> getPiecesInReverseOrder() {
return new EnumeratedIterator<GamePiece>(new ReversePieceIterator());
// return new ReversePieceEnum();
}
public Iterator<GamePiece> getPiecesInVisibleOrderIterator() {
return new VisibleOrderIterator();
}
/**
* Returns pieces in the order in which they are visible to the player --
* topmost first In other words, selected pieces first, then unselected pieces
* from the top to the bottom.
* @deprecated
*/
@Deprecated
public Enumeration<GamePiece> getPiecesInVisibleOrder() {
return new EnumeratedIterator<GamePiece>(new VisibleOrderIterator());
// return new VisibleOrderEnum();
}
public void remove(GamePiece p) {
removePieceAt(indexOf(p));
p.setParent(null);
if (getMap() != null) {
getMap().repaint();
}
}
protected void removePieceAt(int index) {
if (index >= 0 && index < pieceCount) {
pieceCount--;
for (int i = index; i < pieceCount; ++i) {
contents[i] = contents[i + 1];
}
expanded = expanded && pieceCount > 1;
}
}
/**
* Perform some action on a GamePiece that has just been removed this Stack
* @param p
* @return a {@link Command} that performs the equivalent action when executed
*/
public Command pieceRemoved(GamePiece p) {
return null;
}
protected void insertPieceAt(GamePiece p, int index) {
if (index < 0) {
index = 0;
}
else if (index > pieceCount) {
index = pieceCount;
}
if (pieceCount >= contents.length) {
GamePiece[] newContents = new GamePiece[contents.length + INCR];
System.arraycopy(contents, 0, newContents, 0, pieceCount);
contents = newContents;
}
for (int i = pieceCount; i > index; --i) {
contents[i] = contents[i - 1];
}
contents[index] = p;
pieceCount++;
}
public void removeAll() {
pieceCount = 0;
expanded = false;
}
public int indexOf(GamePiece p) {
int index = -1;
for (int i = 0; i < pieceCount; ++i) {
if (p == contents[i]) {
index = i;
break;
}
}
return index;
}
public GamePiece getPieceAt(int index) {
return contents[index];
}
/**
* Adds a piece to the stack. If the piece already exists in the stack, moves
* it to the top
*
* @param c
*/
public void add(GamePiece c) {
insert(c, pieceCount);
}
/**
* Adds a GamePiece to this Stack. Slightly more efficient than
* {@link #insert} because it assumes the piece does not already belong to
* this Stack.
*
* @param child
* @param index
*/
public void insertChild(GamePiece child, int index) {
if (child.getParent() != null) {
child.getParent().remove(child);
}
else if (child.getMap() != null) {
child.getMap().removePiece(child);
}
child.setParent(this);
insertPieceAt(child, index);
}
public int getPieceCount() {
return pieceCount;
}
/**
* Return the number of pieces that could possible be drawn in the stack, regardless of visibility to any particular player
* @return
*/
public int getMaximumVisiblePieceCount() {
return pieceCount;
}
/**
* Inserts a child GamePiece at a given index. If the child piece already
* belongs to this Stack, it will be repositioned to the given index.
*
* @param p
* @param pos
*/
public void insert(GamePiece p, int pos) {
if (p == null) {
return;
}
pos = Math.max(pos, 0);
pos = Math.min(pos, pieceCount);
int index = indexOf(p);
if (index >= 0) {
final boolean origExpanded = isExpanded(); // Bug #2766794
if (pos > index) {
insertPieceAt(p, pos + 1);
removePieceAt(index);
}
else {
removePieceAt(index);
insertPieceAt(p, pos);
}
setExpanded(origExpanded);
}
else {
insertChild(p, pos);
}
}
/**
* Perform some action on a GamePiece that has just been added to this Stack
* @param p
* @return a {@link Command} that performs the equivalent action when executed
*/
public Command pieceAdded(GamePiece p) {
return null;
}
/**
* If the <code>obs</code> parameter is a {@link Map}, delegate drawing of
* this Stack to the {@link StackMetrics} of that Map. If <code>obs</code>
* is not a Map, use the default StackMetrics
*
* @see StackMetrics#draw
* @see #getDefaultMetrics
*/
public void draw(Graphics g, int x, int y, Component obs, double zoom) {
if (obs instanceof Map.View) {
((Map.View) obs).getMap().getStackMetrics().draw(this, g, x, y, obs, zoom);
}
else {
getDefaultMetrics().draw(this, g, x, y, obs, zoom);
}
}
/**
* Return a comma-separated list of the names of the pieces in this Stack
*/
public String getName(boolean localized) {
final StringBuilder val = new StringBuilder();
final PieceIterator visibleFilter =
PieceIterator.visible(getPiecesReverseIterator());
while (visibleFilter.hasMoreElements()) {
final GamePiece p = visibleFilter.nextPiece();
val.append(localized ? p.getLocalizedName() : p.getName());
if (val.length() > 0 && visibleFilter.hasMoreElements()) {
val.append(", ");
}
}
return val.toString();
}
public String getName() {
return getName(false);
}
public String getLocalizedName() {
return getName(true);
}
public Rectangle boundingBox() {
final Rectangle r = new Rectangle();
final Rectangle[] childBounds = new Rectangle[getPieceCount()];
getMap().getStackMetrics().getContents(this, null, null, childBounds, 0, 0);
final PieceIterator visibleFilter =
PieceIterator.visible(getPiecesIterator());
while (visibleFilter.hasMoreElements()) {
final GamePiece p = visibleFilter.nextPiece();
r.add(childBounds[indexOf(p)]);
}
return r;
}
public Shape getShape() {
Area a = new Area();
Shape[] childBounds = new Shape[getPieceCount()];
StackMetrics metrics = getMap() == null ? getDefaultMetrics() : getMap().getStackMetrics();
metrics.getContents(this, null, childBounds, null, 0, 0);
final PieceIterator visibleFilter =
PieceIterator.visible(getPiecesIterator());
while (visibleFilter.hasMoreElements()) {
GamePiece p = visibleFilter.nextPiece();
a.add(new Area(childBounds[indexOf(p)]));
}
return a;
}
public void selectNext(GamePiece c) {
KeyBuffer.getBuffer().remove(c);
if (pieceCount > 1 && indexOf(c) >= 0) {
int newSelectedIndex = indexOf(c) == pieceCount - 1 ? pieceCount - 2 : indexOf(c) + 1;
for (int i = 0; i < pieceCount; ++i) {
if (indexOf(contents[i]) == newSelectedIndex) {
KeyBuffer.getBuffer().add(contents[i]);
return;
}
}
}
}
public GamePiece getPieceBeneath(GamePiece p) {
int index = indexOf(p);
while (index-- > 0) {
if (!Boolean.TRUE.equals(contents[index].getProperty(Properties.INVISIBLE_TO_ME))) {
return contents[index];
}
}
return null;
}
public GamePiece getPieceAbove(GamePiece p) {
int index = indexOf(p);
while (++index < getPieceCount()) {
if (!Boolean.TRUE.equals(contents[index].getProperty(Properties.INVISIBLE_TO_ME))) {
return contents[index];
}
}
return null;
}
/** @return the top visible piece in this stack */
public GamePiece topPiece() {
for (int i = pieceCount - 1; i >= 0; --i) {
if (!Boolean.TRUE.equals(contents[i].getProperty(Properties.INVISIBLE_TO_ME))) {
return contents[i];
}
}
return null;
}
/**
* @return the top piece in this stack that is visible to the player with the
* given id
* @param playerId
* @see GameModule#getUserId
*/
public GamePiece topPiece(String playerId) {
for (int i = pieceCount - 1; i >= 0; --i) {
String hiddenBy = (String) contents[i].getProperty(Properties.HIDDEN_BY);
if (hiddenBy == null || hiddenBy.equals(playerId)) {
return contents[i];
}
}
return null;
}
/**
* @return the bottom piece in this stack that is visible to the player with
* the given id
* @param playerId
* @see GameModule#getUserId
*/
public GamePiece bottomPiece(String playerId) {
for (int i = 0; i < pieceCount; ++i) {
String hiddenBy = (String) contents[i].getProperty(Properties.HIDDEN_BY);
if (hiddenBy == null || hiddenBy.equals(playerId)) {
return contents[i];
}
}
return null;
}
/** @return the bottom visible piece in this stack */
public GamePiece bottomPiece() {
for (int i = 0; i < pieceCount; ++i) {
if (!Boolean.TRUE.equals(contents[i].getProperty(Properties.INVISIBLE_TO_ME))) {
return contents[i];
}
}
return null;
}
/**
* @return Number of GamePieces that are visible to me
*/
protected int nVisible() {
int nv = 0;
final PieceIterator visibleFilter =
PieceIterator.visible(getPiecesIterator());
while (visibleFilter.hasMoreElements()) {
visibleFilter.nextPiece();
nv++;
}
return nv;
}
public Command keyEvent(javax.swing.KeyStroke stroke) {
GamePiece p = topPiece();
if (p != null) {
return p.keyEvent(stroke);
}
else {
return null;
}
}
public boolean isExpanded() {
return expanded;
}
public void setExpanded(boolean b) {
expanded = b && getPieceCount() > 1;
}
public String getState() {
SequenceEncoder se = new SequenceEncoder(';');
se.append(getMap() == null ? "null" : getMap().getIdentifier()).append(getPosition().x).append(getPosition().y);
for (int i = 0; i < pieceCount; ++i) {
se.append(contents[i].getId());
}
return se.getValue();
}
public void setState(String s) {
final SequenceEncoder.Decoder st = new SequenceEncoder.Decoder(s, ';');
final String mapId = st.nextToken();
setPosition(new Point(st.nextInt(0), st.nextInt(0)));
pieceCount = 0;
final GameState gs = GameModule.getGameModule().getGameState();
while (st.hasMoreTokens()) {
final GamePiece child = gs.getPieceForId(st.nextToken());
if (child != null) insertChild(child, pieceCount);
}
Map m = null;
if (!"null".equals(mapId)) {
m = Map.getMapById(mapId);
if (m == null) {
ErrorDialog.dataError(new BadDataReport("Could not find map",mapId,null));
}
}
if (m != getMap()) {
if (m != null) {
m.addPiece(this);
}
else {
setMap(null);
}
}
}
/**
* Compute the difference between <code>newState</code> and
* <code>oldState</code> and appy that difference to the current state
*
* @param newState
* @param oldState
*/
public void mergeState(String newState, String oldState) {
String mergedState = newState;
if (!oldState.equals(getState())) {
SequenceEncoder.Decoder stNew = new SequenceEncoder.Decoder(newState, ';');
SequenceEncoder.Decoder stOld = new SequenceEncoder.Decoder(oldState, ';');
SequenceEncoder merge = new SequenceEncoder(';');
merge.append(stNew.nextToken());
stOld.nextToken();
merge.append(stNew.nextToken());
stOld.nextToken();
merge.append(stNew.nextToken());
stOld.nextToken();
ArrayList<String> newContents = new ArrayList<String>();
while (stNew.hasMoreTokens()) {
newContents.add(stNew.nextToken());
}
ArrayList<String> oldContents = new ArrayList<String>();
while (stOld.hasMoreTokens()) {
oldContents.add(stOld.nextToken());
}
for (int i = 0, j = getPieceCount(); i < j; ++i) {
String id = getPieceAt(i).getId();
if (!newContents.contains(id) && !oldContents.contains(id)) {
int index = i == 0 ? -1 :
newContents.indexOf(getPieceAt(i - 1).getId());
newContents.add(index + 1, id);
}
}
for (String s : newContents) {
merge.append(s);
}
mergedState = merge.getValue();
}
setState(mergedState);
}
public String getType() {
return TYPE;
}
public void setProperty(Object key, Object val) {
}
public String toString() {
return super.toString()+"["+getName()+"]";
}
/**
* Calls setProperty() on each piece in this stack
*
* @param key
* @param val
*/
public void setPropertyOnContents(Object key, Object val) {
for (Enumeration<GamePiece> e = getPieces(); e.hasMoreElements();) {
e.nextElement().setProperty(key, val);
}
}
public Object getProperty(Object key) {
return null;
}
public Object getLocalizedProperty(Object key) {
return getProperty(key);
}
public void setMap(Map map) {
this.map = map;
}
public Map getMap() {
return map;
}
public Point getPosition() {
return new Point(pos);
}
public void setPosition(Point p) {
pos = p;
}
public Stack getParent() {
return null;
}
public void setParent(Stack s) {
if (s != null) {
ErrorDialog.dataError(new BadDataReport("Cannot add stack to another stack",toString(),null));
}
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public static void setDefaultMetrics(StackMetrics s) {
defaultMetrics = s;
}
public StackMetrics getStackMetrics(Map m) {
return m == null ? getDefaultMetrics() : m.getStackMetrics();
}
public StackMetrics getStackMetrics() {
return getStackMetrics(getMap());
}
public StackMetrics getDefaultMetrics() {
if (defaultMetrics == null) {
setDefaultMetrics(new StackMetrics());
}
return defaultMetrics;
}
private class VisibleOrderIterator implements Iterator<GamePiece> {
private GamePiece next;
private int index = pieceCount - 1;
private boolean doingSelected = true;
public VisibleOrderIterator() {
next = findNext();
}
public boolean hasNext() {
return next != null;
}
public GamePiece next() {
final GamePiece ret = next;
next = findNext();
return ret;
}
private GamePiece findNext() {
GamePiece ret = null;
while (index >= 0) {
final GamePiece p = getPieceAt(index--);
if (doingSelected ^ !Boolean.TRUE.equals(
p.getProperty(Properties.SELECTED))) {
ret = p;
break;
}
}
if (ret == null && doingSelected) {
doingSelected = false;
index = pieceCount - 1;
ret = findNext();
}
return ret;
}
public void remove() {
throw new UnsupportedOperationException();
}
}
private class AllPieceIterator implements Iterator<GamePiece> {
private int index = 0;
private final GamePiece[] p;
public AllPieceIterator() {
p = ArrayUtils.copyOf(contents, pieceCount);
}
public boolean hasNext() {
return index < p.length;
}
public GamePiece next() {
return p[index++];
}
public void remove() {
throw new UnsupportedOperationException();
}
}
private class ReversePieceIterator implements Iterator<GamePiece> {
private int index = pieceCount - 1;
private final GamePiece[] p;
public ReversePieceIterator() {
p = ArrayUtils.copyOf(contents, pieceCount);
}
public boolean hasNext() {
return index >= 0;
}
public GamePiece next() {
return p[index--];
}
public void remove() {
throw new UnsupportedOperationException();
}
}
}