/*
* Copyright (c) 2014 tabletoptool.com team.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*
* Contributors:
* rptools.com team - initial implementation
* tabletoptool.com team - further development
*/
package com.t3.model.initiative;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.ListIterator;
import javax.swing.Icon;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import com.t3.client.AppPreferences;
import com.t3.client.TabletopTool;
import com.t3.guid.GUID;
import com.t3.model.Token;
import com.t3.model.Zone;
import com.t3.util.guidreference.NullHelper;
import com.t3.util.guidreference.TokenReference;
import com.t3.util.guidreference.ZoneReference;
import com.t3.xstreamversioned.version.SerializationVersion;
/**
* All of the tokens currently being shown in the initiative list. It includes a reference to all
* the tokens in order, a reference to the current token, a displayable initiative value and a
* hold state for each token.
*
* @author Jay
*/
@SerializationVersion(1)
public class InitiativeList implements Serializable {
/*---------------------------------------------------------------------------------------------
* Instance Variables
*-------------------------------------------------------------------------------------------*/
/**
* The tokens and their order within the initiative
*/
private List<TokenInitiative> tokens = new ArrayList<TokenInitiative>();
/**
* The token in the list which currently has initiative.
*/
private int current = -1;
/**
* The current round for initiative.
*/
private int round = -1;
/**
* Used to add property change support to the round and current values.
*/
private transient PropertyChangeSupport pcs = new PropertyChangeSupport(this);
/**
* The zone that owns this initiative list, used for persistence
*/
private ZoneReference zone;
/**
* Hold the update when this variable is greater than 0. Some methods need to call
* {@link #updateServer()} when they are called, but they also get called by other
* methods that update the server. This keeps it from happening multiple times.
*/
private transient int holdUpdate;
/**
* Flag indicating that a full update is needed.
*/
private boolean fullUpdate;
/**
* Hide all of the NPC's from the players.
*/
private boolean hideNPC = AppPreferences.getInitHideNpcs();
/*---------------------------------------------------------------------------------------------
* Class Variables
*-------------------------------------------------------------------------------------------*/
/**
* Name of the tokens property passed in {@link PropertyChangeEvent}s.
*/
public static final String TOKENS_PROP = "tokens";
/**
* Name of the round property passed in {@link PropertyChangeEvent}s.
*/
public static final String ROUND_PROP = "round";
/**
* Name of the current property passed in {@link PropertyChangeEvent}s.
*/
public static final String CURRENT_PROP = "current";
/**
* Name of the hide NPCs property passed in {@link PropertyChangeEvent}s.
*/
public static final String HIDE_NPCS_PROP = "hideNPCs";
/**
* Name of the owner permission property passed in {@link PropertyChangeEvent}s.
*/
public static final String OWNER_PERMISSIONS_PROP = "ownerPermissions";
/**
* Logger for this class
*/
private static final Logger LOGGER = Logger.getLogger(InitiativeList.class);
/*---------------------------------------------------------------------------------------------
* Constructor
*-------------------------------------------------------------------------------------------*/
/**
* Create an initiative list for a zone.
*
* @param aZone The zone that owns this initiative list.
*/
public InitiativeList(Zone aZone) {
setZone(aZone);
}
/*---------------------------------------------------------------------------------------------
* Instance Methods
*-------------------------------------------------------------------------------------------*/
/**
* Get the token initiative data at the passed index. Allows the other state to be set.
*
* @param index Index of the token initiative data needed.
* @return The token initiative data for the passed index.
*/
public TokenInitiative getTokenInitiative(int index) {
return index < tokens.size() && index >= 0 ? tokens.get(index) : null;
}
/**
* Get the number of tokens in this list.
*
* @return Number of tokens
*/
public int getSize() {
return tokens.size();
}
/**
* Get the token at the passed index.
*
* @param index Index of the token needed.
* @return The token for the passed index.
*/
public Token getToken(int index) {
return index >= 0 && index < tokens.size() ? tokens.get(index).getToken() : null;
}
/**
* Insert a new token into the initiative.
*
* @param index Insert the token here.
* @param token Insert this token.
* @return The token initiative value that holds the token.
*/
public TokenInitiative insertToken(int index, Token token) {
startUnitOfWork();
TokenInitiative currentInitiative = getTokenInitiative(getCurrent()); // Save the currently selected initiative
if (index == -1) {
index = tokens.size();
}
TokenInitiative ti = new TokenInitiative(token);
ti.setInitiativeList(this);
tokens.add(index, ti);
getPCS().fireIndexedPropertyChange(TOKENS_PROP, index, null, ti);
setCurrent(indexOf(currentInitiative)); // Restore current initiative
finishUnitOfWork();
return ti;
}
/**
* Insert a new token into the initiative.
*
* @param tokens Insert these tokens.
*/
public void insertTokens(List<Token> tokens) {
startUnitOfWork();
for (Token token : tokens)
insertToken(-1, token);
finishUnitOfWork();
}
/**
* Find the index of the passed token.
*
* @param token Search for this token.
* @return A list of the indexes found for the listed token
*/
public List<Integer> indexOf(Token token) {
List<Integer> list = new ArrayList<Integer>();
for (int i = 0; i < tokens.size(); i++)
if (token.equals(tokens.get(i).getToken()))
list.add(i);
return list;
}
/**
* Searches for the passed token in the list.
*
* @param token Search for this token.
* @return if this token is contained in the list
*/
public boolean contains(Token token) {
for (int i = 0; i < tokens.size(); i++)
if (token.equals(tokens.get(i).getToken()))
return true;
return false;
}
/**
* Find the index of the passed token initiative.
*
* @param ti Search for this token initiative instance
* @return The index of the token initiative that was found or -1 if the token initiative was not found;
*/
public int indexOf(TokenInitiative ti) {
for (int i = 0; i < tokens.size(); i++)
if (tokens.get(i).equals(ti))
return i;
return -1;
}
/**
* Remove a token from the initiative.
*
* @param index Remove the token at this index.
* @return The token that was removed.
*/
public Token removeToken(int index) {
// If we are deleting the token with initiative, drop back to the previous token, if we're at the beginning, clear current
startUnitOfWork();
TokenInitiative currentInitiative = getTokenInitiative(getCurrent()); // Save the currently selected initiative
int currentInitIndex = indexOf(currentInitiative);
if (currentInitIndex == index) {
if (tokens.size() == 1) {
currentInitiative = null;
} if (index == 0) {
currentInitiative = getTokenInitiative(1);
} else {
currentInitiative = getTokenInitiative(currentInitIndex - 1);
} // endif
} // endif
TokenInitiative ti = tokens.remove(index);
Token old = ti.getToken();
getPCS().fireIndexedPropertyChange(TOKENS_PROP, index, ti, null);
setCurrent(indexOf(currentInitiative)); // Restore current initiative
finishUnitOfWork();
return old;
}
/** @return Getter for current */
public int getCurrent() {
return current;
}
/** @param aCurrent Setter for the current to set */
public void setCurrent(int aCurrent) {
if (current == aCurrent)
return;
startUnitOfWork();
if (aCurrent < 0 || aCurrent >= tokens.size())
aCurrent = -1; // Don't allow bad values
int old = current;
current = aCurrent;
getPCS().firePropertyChange(CURRENT_PROP, old, current);
finishUnitOfWork();
}
/**
* Go to the next token in initiative order.
*/
public void nextInitiative() {
if (tokens.isEmpty())
return;
startUnitOfWork();
int newRound = (round < 0) ? 1 : (current + 1 >= tokens.size()) ? round + 1 : round;
int newCurrent = (current < 0 || current + 1 >= tokens.size()) ? 0 : current + 1;
setCurrent(newCurrent);
setRound(newRound);
finishUnitOfWork();
}
/**
* Go to the previous token in initiative order.
*/
public void prevInitiative() {
if (tokens.isEmpty())
return;
startUnitOfWork();
int newRound = (round < 2) ? 1 : (current - 1 < 0) ? round - 1 : round;
int newCurrent = (current < 1) ? (round < 2 ? 0 : tokens.size() - 1): current - 1;
setCurrent(newCurrent);
setRound(newRound);
finishUnitOfWork();
}
/** @return Getter for round */
public int getRound() {
return round;
}
/** @param aRound Setter for the round to set */
public void setRound(int aRound) {
if (round == aRound)
return;
startUnitOfWork();
int old = round;
round = aRound;
getPCS().firePropertyChange(ROUND_PROP, old, aRound);
finishUnitOfWork();
}
/**
* Add a listener to any property change.
*
* @param listener The listener to be added.
*/
public void addPropertyChangeListener(PropertyChangeListener listener) {
getPCS().addPropertyChangeListener(listener);
}
/**
* Add a listener to the given property name
*
* @param propertyName Add the listener to this property name.
* @param listener The listener to be added.
*/
public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
getPCS().addPropertyChangeListener(propertyName, listener);
}
/**
* Remove a listener for all property changes.
*
* @param listener The listener to be removed.
*/
public void removePropertyChangeListener(PropertyChangeListener listener) {
getPCS().removePropertyChangeListener(listener);
}
/**
* Remove a listener from a given property name
*
* @param propertyName Remove the listener from this property name.
* @param listener The listener to be removed.
*/
public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) {
getPCS().removePropertyChangeListener(propertyName, listener);
}
/**
* Start a new unit of work.
*/
public void startUnitOfWork() {
holdUpdate += 1;
if (holdUpdate == 1)
fullUpdate = false;
LOGGER.debug("startUnitOfWork(): " + holdUpdate + " full: " + fullUpdate);
}
/**
* Finish the current unit of work and update the server.
*/
public void finishUnitOfWork() {
fullUpdate = true;
finishUnitOfWork(null);
}
/**
* Finish the current unit of work on a single initiative item and update the server.
*
* @param ti Only need to update this token initiative.
*/
public void finishUnitOfWork(TokenInitiative ti) {
assert holdUpdate > 0 : "Trying to close unit of work when one is not open.";
holdUpdate -= 1;
LOGGER.debug("finishUnitOfWork(" + (ti == null ? "" : ti.getId().toString()) + "): = " + holdUpdate + " full: " + fullUpdate);
if (holdUpdate == 0) {
if (fullUpdate || ti == null) {
updateServer();
} else {
updateServer(ti);
} // endif
} // endif
}
/**
* Remove all of the tokens from the model and clear round and current
*/
public void clearModel() {
if (current == -1 && round == -1 && tokens.isEmpty())
return;
startUnitOfWork();
setCurrent(-1);
setRound(-1);
if (!tokens.isEmpty()) {
List<TokenInitiative> old = tokens;
tokens = new ArrayList<TokenInitiative>();
getPCS().firePropertyChange(TOKENS_PROP, old, tokens);
} // endif
finishUnitOfWork();
}
/**
* Updates occurred to the tokens.
*/
public void update() {
// No zone, no tokens
if (getZone() == null) {
clearModel();
return;
} // endif
// Remove deleted tokens
startUnitOfWork();
boolean updateNeeded = false;
ListIterator<TokenInitiative> i = tokens.listIterator();
while (i.hasNext()) {
TokenInitiative ti = i.next();
if (!ti.tokenExists()) {
int index = tokens.indexOf(ti);
if (index <= current)
setCurrent(current - 1);
i.remove();
updateNeeded = true;
getPCS().fireIndexedPropertyChange(TOKENS_PROP, index, ti, null);
} // endif
} // endwhile
if (updateNeeded) {
finishUnitOfWork();
} else if (holdUpdate == 1) {
holdUpdate -= 1; // Do no updates.
LOGGER.debug("finishUnitOfWork() - no update");
} // endif
}
/**
* Sort the tokens by their initiative state from largest to smallest. If the initiative state string can be converted into a
* {@link Double} that is done first. All values converted to {@link Double}s are always considered bigger than the {@link String}
* values. The {@link String} values are considered bigger than any <code>null</code> values.
*/
public void sort() {
startUnitOfWork();
TokenInitiative currentInitiative = getTokenInitiative(getCurrent()); // Save the currently selected initiative
Collections.sort(tokens);
getPCS().firePropertyChange(TOKENS_PROP, null, tokens);
setCurrent(indexOf(currentInitiative)); // Restore current initiative
finishUnitOfWork();
}
/** @return Getter for zone */
public Zone getZone() {
return NullHelper.value(zone);
}
/** @return Getter for pcs */
private PropertyChangeSupport getPCS() {
if (pcs == null)
pcs = new PropertyChangeSupport(this);
return pcs;
}
/**
* Move a token from it's current position to the new one.
*
* @param oldIndex Move the token at this index
* @param index To here.
*/
public void moveToken(int oldIndex, int index) {
// Bad index, same index, oldIndex->oldindex+1, or moving the last token to the end of the list do nothing.
if (oldIndex < 0 || oldIndex == index || (oldIndex == tokens.size() - 1 && index == tokens.size()) || oldIndex == (index-1))
return;
// Save the current position, the token moves but the initiative does not.
TokenInitiative newInitiative = null;
TokenInitiative currentInitiative = getTokenInitiative(getCurrent()); // Save the current initiative
if (oldIndex == current) {
newInitiative = getTokenInitiative(oldIndex != 0 ? oldIndex -1 : 1);
current = (oldIndex != 0 ? oldIndex -1 : 1);
}
startUnitOfWork();
current = -1;
TokenInitiative ti = tokens.remove(oldIndex);
ti.setInitiativeList(this);
getPCS().fireIndexedPropertyChange(TOKENS_PROP, oldIndex, ti, null);
// Add it at it's new position
index -= index > oldIndex ? 1 : 0;
tokens.add(index, ti);
getPCS().fireIndexedPropertyChange(TOKENS_PROP, index, null, ti);
// Set/restore proper initiative
if (newInitiative == null)
current = indexOf(currentInitiative);
else
setCurrent(indexOf(newInitiative));
finishUnitOfWork();
}
/**
* Update the server with the new list
*/
public void updateServer() {
if (zone== null)
return;
LOGGER.debug("Full update");
TabletopTool.serverCommand().updateInitiative(this, null);
}
/**
* Update the server with the new Token Initiative
*
* @param ti Item to update
*/
public void updateServer(TokenInitiative ti) {
if (zone == null)
return;
LOGGER.debug("Token Init update: " + ti.getId());
TabletopTool.serverCommand().updateTokenInitiative(zone.getId(), ti.getId(), ti.isHolding(), ti.getRawState(), indexOf(ti));
}
/** @param aZone Setter for the zone */
public void setZone(Zone aZone) {
zone = NullHelper.referenceZone(aZone);
}
/** @return Getter for hideNPC */
public boolean isHideNPC() {
return hideNPC;
}
/** @param hide Setter for hideNPC */
public void setHideNPC(boolean hide) {
if (hide == hideNPC)
return;
startUnitOfWork();
boolean old = hideNPC;
hideNPC = hide;
getPCS().firePropertyChange(HIDE_NPCS_PROP, old, hide);
finishUnitOfWork();
}
/** @return Getter for tokens */
public List<TokenInitiative> getTokens() {
return Collections.unmodifiableList(tokens);
}
/*---------------------------------------------------------------------------------------------
* TokenInitiative Inner Class
*-------------------------------------------------------------------------------------------*/
/**
* This class holds all of the data to describe a token w/in initiative.
*
* @author Jay
*/
@SerializationVersion(1)
public static class TokenInitiative implements Comparable<TokenInitiative> {
/*---------------------------------------------------------------------------------------------
* Instance Variables
*-------------------------------------------------------------------------------------------*/
/**
* The token which is needed for persistence. It is immutable.
*/
private TokenReference token;
/**
* Flag indicating that the token is holding it's initiative.
*/
private boolean holding;
/**
* Optional state that can be displayed in the initiative panel.
*/
private InitiativeValue state;
/**
* Save off the icon so that it can be displayed as needed.
*/
private transient Icon displayIcon;
private InitiativeList initiativeList;
/*---------------------------------------------------------------------------------------------
* Constructors
*-------------------------------------------------------------------------------------------*/
/**
* Create the token initiative for the passed token.
*
* @param aToken Add this token to the initiative.
*/
public TokenInitiative(Token aToken) {
token=NullHelper.referenceToken(aToken);
}
/*---------------------------------------------------------------------------------------------
* Instance Methods
*-------------------------------------------------------------------------------------------*/
public boolean tokenExists() {
return token.isValid();
}
/** @return Getter for token */
public Token getToken() {
return NullHelper.value(token);
}
/** @return Getter for id */
public GUID getId() {
return NullHelper.getId(token);
}
/** @return Getter for holding */
public boolean isHolding() {
return holding;
}
/** @param isHolding Setter for the holding to set */
public void setHolding(boolean isHolding) {
if (holding == isHolding)
return;
initiativeList.startUnitOfWork();
boolean old = holding;
holding = isHolding;
initiativeList.getPCS().fireIndexedPropertyChange(TOKENS_PROP, initiativeList.tokens.indexOf(this), old, isHolding);
initiativeList.finishUnitOfWork(this);
}
/** @return Getter for state */
public Object getState() {
if(state==null)
return null;
else
return state.getValue();
}
/** @return Getter for state */
public InitiativeValue getRawState() {
return state;
}
public void setState(String state) {
this.setState(InitiativeValue.create(state));
}
public void setState(Number state) {
this.setState(InitiativeValue.create(state));
}
/** This method accepts a string as the new initiative value but it will try to convert it into a number first*/
public void setUnparsedState(String state) {
try {
this.setState(Integer.valueOf(state));
} catch(NumberFormatException e) {
try {
this.setState(Double.valueOf(state));
} catch(NumberFormatException e2) {
if(StringUtils.isBlank(state))
this.setState((InitiativeValue)null);
else
this.setState(state);
}
}
}
/** @param aState Setter for the state to set */
public void setState(InitiativeValue aState) {
if (state == aState || (state != null && state.equals(aState)))
return;
initiativeList.startUnitOfWork();
Object old = state;
state = aState;
initiativeList.getPCS().fireIndexedPropertyChange(TOKENS_PROP, initiativeList.tokens.indexOf(this), old, aState);
initiativeList.finishUnitOfWork(this);
}
/** @return Getter for displayIcon */
public Icon getDisplayIcon() {
return displayIcon;
}
/** @param displayIcon Setter for the displayIcon to set */
public void setDisplayIcon(Icon displayIcon) {
this.displayIcon = displayIcon;
}
public void update(boolean isHolding, String aState) {
this.update(isHolding, InitiativeValue.create(aState));
}
public void update(boolean isHolding, Number aState) {
this.update(isHolding, InitiativeValue.create(aState));
}
/**
* Update the internal state w/o firing events. Needed for single token
* init updates.
*
* @param isHolding New holding state
* @param aState New state
*/
public void update(boolean isHolding, InitiativeValue aState) {
boolean old = holding;
holding = isHolding;
Object oldState = state;
state = aState;
initiativeList.getPCS().fireIndexedPropertyChange(TOKENS_PROP, initiativeList.tokens.indexOf(this), old, isHolding);
initiativeList.getPCS().fireIndexedPropertyChange(TOKENS_PROP, initiativeList.tokens.indexOf(this), oldState, aState);
}
public void setInitiativeList(InitiativeList initiativeList) {
this.initiativeList = initiativeList;
}
@Override
public int compareTo(TokenInitiative o) {
if(o==null)
return 1;
else
return state.compareTo(o.state);
}
}
}