package games.strategy.triplea.ui;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Image;
import java.awt.Insets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextArea;
import games.strategy.engine.data.GameData;
import games.strategy.engine.data.Unit;
import games.strategy.triplea.delegate.dataObjects.CasualtyList;
import games.strategy.triplea.util.UnitCategory;
import games.strategy.triplea.util.UnitOwner;
import games.strategy.triplea.util.UnitSeperator;
import games.strategy.ui.ScrollableTextField;
import games.strategy.ui.ScrollableTextFieldListener;
import games.strategy.util.IntegerMap;
import games.strategy.util.Match;
public class UnitChooser extends JPanel {
private static final long serialVersionUID = -4667032237550267682L;
private final List<ChooserEntry> m_entries = new ArrayList<>();
private final Map<Unit, Collection<Unit>> m_dependents;
private JTextArea m_title;
private int m_total = -1;
private final JLabel m_leftToSelect = new JLabel();
private final GameData m_data;
private boolean m_allowMultipleHits = false;
private JButton m_autoSelectButton;
private JButton m_selectNoneButton;
private final IUIContext m_uiContext;
private final Match<Collection<Unit>> m_match;
/** Creates new UnitChooser. */
public UnitChooser(final Collection<Unit> units, final Map<Unit, Collection<Unit>> dependent, final GameData data,
final boolean allowTwoHit, final IUIContext uiContext) {
this(units, Collections.emptyList(), dependent, data, allowTwoHit, uiContext);
}
public UnitChooser(final Collection<Unit> units, final Collection<Unit> defaultSelections,
final Map<Unit, Collection<Unit>> dependent, final GameData data, final boolean allowTwoHit,
final IUIContext uiContext) {
this(units, defaultSelections, dependent, false, false, data, allowTwoHit, uiContext);
}
public UnitChooser(final Collection<Unit> units, final CasualtyList defaultSelections,
final Map<Unit, Collection<Unit>> dependent, final GameData data, final boolean allowMultipleHits,
final IUIContext uiContext) {
m_dependents = dependent;
m_data = data;
m_allowMultipleHits = allowMultipleHits;
m_uiContext = uiContext;
m_match = null;
final List<Unit> combinedList = defaultSelections.getDamaged();
// TODO: this adds it to the default selections list, is this intended?
combinedList.addAll(defaultSelections.getKilled());
createEntries(units, dependent, false, false, combinedList);
layoutEntries();
}
public UnitChooser(final Collection<Unit> units, final Collection<Unit> defaultSelections,
final Map<Unit, Collection<Unit>> dependent, final boolean categorizeMovement,
final boolean categorizeTransportCost, final GameData data, final boolean allowMultipleHits,
final IUIContext uiContext) {
m_dependents = dependent;
m_data = data;
m_allowMultipleHits = allowMultipleHits;
m_uiContext = uiContext;
m_match = null;
createEntries(units, dependent, categorizeMovement, categorizeTransportCost, defaultSelections);
layoutEntries();
}
public UnitChooser(final Collection<Unit> units, final Collection<Unit> defaultSelections,
final Map<Unit, Collection<Unit>> dependent, final boolean categorizeMovement,
final boolean categorizeTransportCost, final GameData data, final boolean allowMultipleHits,
final IUIContext uiContext, final Match<Collection<Unit>> match) {
m_dependents = dependent;
m_data = data;
m_allowMultipleHits = allowMultipleHits;
m_uiContext = uiContext;
m_match = match;
createEntries(units, dependent, categorizeMovement, categorizeTransportCost, defaultSelections);
layoutEntries();
}
/**
* Set the maximum number of units that we can choose.
*/
public void setMax(final int max) {
m_total = max;
m_textFieldListener.changedValue(null);
m_autoSelectButton.setVisible(false);
m_selectNoneButton.setVisible(false);
}
public void setMaxAndShowMaxButton(final int max) {
m_total = max;
m_textFieldListener.changedValue(null);
m_autoSelectButton.setText("Max");
}
public void setTitle(final String title) {
m_title.setText(title);
}
private void updateLeft() {
if (m_total == -1) {
return;
}
Iterator<ChooserEntry> iter;
final int selected = getSelectedCount();
m_leftToSelect.setText("Left to select:" + (m_total - selected));
iter = m_entries.iterator();
while (iter.hasNext()) {
final ChooserEntry entry = iter.next();
entry.setLeftToSelect(m_total - selected);
}
m_leftToSelect.setText("Left to select:" + (m_total - selected));
}
private void checkMatches() {
final Collection<Unit> allSelectedUnits = new ArrayList<>();
for (final ChooserEntry entry : m_entries) {
addToCollection(allSelectedUnits, entry, entry.getTotalHits(), false);
}
// check match against each scroll button
for (final ChooserEntry entry : m_entries) {
final Collection<Unit> newSelectedUnits = new ArrayList<>(allSelectedUnits);
final int totalHits = entry.getTotalHits();
final int totalUnits = entry.getCategory().getUnits().size();
int leftToSelect = 0;
final Iterator<Unit> unitIter = entry.getCategory().getUnits().iterator();
for (int i = 1; i <= totalUnits; i++) {
final Unit unit = unitIter.next();
if (i > totalHits) {
newSelectedUnits.add(unit);
}
if (i >= totalHits) {
if (m_match.match(newSelectedUnits)) {
leftToSelect = i - totalHits;
} else {
break;
}
}
}
entry.setLeftToSelect(leftToSelect);
}
}
private int getSelectedCount() {
int selected = 0;
for (final ChooserEntry entry : m_entries) {
selected += entry.getTotalHits();
}
return selected;
}
private void createEntries(final Collection<Unit> units, final Map<Unit, Collection<Unit>> dependent,
final boolean categorizeMovement, final boolean categorizeTransportCost,
final Collection<Unit> defaultSelections) {
final Collection<UnitCategory> categories =
UnitSeperator.categorize(units, dependent, categorizeMovement, categorizeTransportCost);
final Collection<UnitCategory> defaultSelectionsCategorized =
UnitSeperator.categorize(defaultSelections, dependent, categorizeMovement, categorizeTransportCost);
final IntegerMap<UnitCategory> defaultValues = createDefaultSelectionsMap(defaultSelectionsCategorized);
for (final UnitCategory category : categories) {
addCategory(category, defaultValues.getInt(category));
}
}
private IntegerMap<UnitCategory> createDefaultSelectionsMap(final Collection<UnitCategory> categories) {
final IntegerMap<UnitCategory> defaultValues = new IntegerMap<>();
for (final UnitCategory category : categories) {
final int defaultValue = category.getUnits().size();
defaultValues.put(category, defaultValue);
}
return defaultValues;
}
private void addCategory(final UnitCategory category, final int defaultValue) {
final ChooserEntry entry = new ChooserEntry(category, m_total, m_textFieldListener, m_data, m_allowMultipleHits,
defaultValue, m_uiContext);
m_entries.add(entry);
}
private void layoutEntries() {
this.setLayout(new GridBagLayout());
m_title = new JTextArea("Choose units");
m_title.setBackground(this.getBackground());
m_title.setEditable(false);
m_title.setWrapStyleWord(true);
final Insets nullInsets = new Insets(0, 0, 0, 0);
final Dimension buttonSize = new Dimension(80, 20);
m_selectNoneButton = new JButton("None");
m_selectNoneButton.setPreferredSize(buttonSize);
m_autoSelectButton = new JButton("All");
m_autoSelectButton.setPreferredSize(buttonSize);
add(m_title, new GridBagConstraints(0, 0, 7, 1, 0, 0.5, GridBagConstraints.EAST, GridBagConstraints.HORIZONTAL,
nullInsets, 0, 0));
m_selectNoneButton.addActionListener(e -> selectNone());
m_autoSelectButton.addActionListener(e -> autoSelect());
int yIndex = 1;
for (final ChooserEntry entry : m_entries) {
entry.createComponents(this, yIndex);
yIndex++;
}
add(m_autoSelectButton, new GridBagConstraints(0, yIndex, 7, 1, 0, 0.5, GridBagConstraints.EAST,
GridBagConstraints.NONE, nullInsets, 0, 0));
yIndex++;
add(m_leftToSelect, new GridBagConstraints(0, yIndex, 5, 2, 0, 0.5, GridBagConstraints.WEST,
GridBagConstraints.HORIZONTAL, nullInsets, 0, 0));
if (m_match != null) {
m_autoSelectButton.setVisible(false);
m_selectNoneButton.setVisible(false);
checkMatches();
}
}
public Collection<Unit> getSelected() {
return getSelected(true);
}
/**
* get the units selected.
* If units are two hit enabled, returns those with two hits (ie: those killed).
*/
public List<Unit> getSelected(final boolean selectDependents) {
final List<Unit> selectedUnits = new ArrayList<>();
for (final ChooserEntry entry : m_entries) {
addToCollection(selectedUnits, entry, entry.getFinalHit(), selectDependents);
}
return selectedUnits;
}
/**
* Only applicable if this dialog was constructed using multiple hit points.
*/
public List<Unit> getSelectedDamagedMultipleHitPointUnits() {
final List<Unit> selectedUnits = new ArrayList<>();
final Iterator<ChooserEntry> entries = m_entries.iterator();
while (entries.hasNext()) {
final ChooserEntry chooserEntry = entries.next();
if (chooserEntry.hasMultipleHitPoints()) {
// there may be some units being given multiple hits, while others get a single or no hits
for (int i = 0; i < chooserEntry.size() - 1; i++) {
// here we are counting on the fact that unit category stores the units in a list, so the order is the same
// every time we access
// it.
// this means that in the loop we may select the first 2 units in the list to receive 1 hit, then select the
// first unit the list
// to receive 1 more hit
addToCollection(selectedUnits, chooserEntry, chooserEntry.getHits(i), false);
}
}
}
return selectedUnits;
}
private void selectNone() {
for (final ChooserEntry entry : m_entries) {
entry.selectNone();
}
}
// does not take into account multiple hit points
private void autoSelect() {
if (m_total == -1) {
for (final ChooserEntry entry : m_entries) {
entry.selectAll();
}
} else {
int leftToSelect = m_total - getSelectedCount();
for (final ChooserEntry entry : m_entries) {
final int canSelect = entry.getMax() - entry.getHits(0);
if (leftToSelect >= canSelect) {
entry.selectAll();
leftToSelect -= canSelect;
} else {
entry.set(entry.getHits(0) + canSelect);
leftToSelect = 0;
break;
}
}
}
}
private void addToCollection(final Collection<Unit> addTo, final ChooserEntry entry, final int quantity,
final boolean addDependents) {
final Collection<Unit> possible = entry.getCategory().getUnits();
if (possible.size() < quantity) {
throw new IllegalStateException("Not enough units");
}
final Iterator<Unit> iter = possible.iterator();
for (int i = 0; i < quantity; i++) {
final Unit current = iter.next();
addTo.add(current);
if (addDependents) {
final Collection<Unit> dependents = m_dependents.get(current);
if (dependents != null) {
addTo.addAll(dependents);
}
}
}
}
private final ScrollableTextFieldListener m_textFieldListener = new ScrollableTextFieldListener() {
@Override
public void changedValue(final ScrollableTextField field) {
if (m_match != null) {
checkMatches();
} else {
updateLeft();
}
}
};
}
class ChooserEntry {
private final UnitCategory m_category;
private final ScrollableTextFieldListener m_hitTextFieldListener;
private final GameData m_data;
private final boolean m_hasMultipleHits;
private final List<Integer> m_defaultHits;
private final List<ScrollableTextField> m_hitTexts;
private final List<JLabel> m_hitLabel = new ArrayList<>();
private int m_leftToSelect = 0;
private static Insets nullInsets = new Insets(0, 0, 0, 0);
private final IUIContext m_uiContext;
ChooserEntry(final UnitCategory category, final int leftToSelect, final ScrollableTextFieldListener listener,
final GameData data, final boolean allowTwoHit, final int defaultValue, final IUIContext uiContext) {
m_hitTextFieldListener = listener;
m_data = data;
m_category = category;
m_leftToSelect = leftToSelect < 0 ? category.getUnits().size() : leftToSelect;
m_hasMultipleHits =
allowTwoHit && category.getHitPoints() > 1 && category.getDamaged() < category.getHitPoints() - 1;
m_hitTexts = new ArrayList<>(Math.max(1, category.getHitPoints() - category.getDamaged()));
m_defaultHits = new ArrayList<>(Math.max(1, category.getHitPoints() - category.getDamaged()));
final int numUnits = category.getUnits().size();
int hitsUsedSoFar = 0;
for (int i = 0; i < Math.max(1, category.getHitPoints() - category.getDamaged()); i++) {
// TODO: check if default value includes damaged points or not
final int hitsToUse = Math.min(numUnits, (defaultValue - hitsUsedSoFar));
hitsUsedSoFar += hitsToUse;
m_defaultHits.add(hitsToUse);
}
m_uiContext = uiContext;
}
public void createComponents(final JPanel panel, final int yIndex) {
int gridx = 0;
for (int i =
0; i < (m_hasMultipleHits ? Math.max(1, m_category.getHitPoints() - m_category.getDamaged()) : 1); i++) {
final ScrollableTextField scroll = new ScrollableTextField(0, m_category.getUnits().size());
m_hitTexts.add(scroll);
scroll.setValue(m_defaultHits.get(i));
scroll.addChangeListener(m_hitTextFieldListener);
final JLabel label = new JLabel("x" + m_category.getUnits().size());
m_hitLabel.add(label);
panel.add(new UnitChooserEntryIcon(i > 0, m_uiContext), new GridBagConstraints(gridx++, yIndex, 1, 1, 0, 0,
GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, new Insets(0, (i == 0 ? 0 : 8), 0, 0), 0, 0));
if (i == 0) {
if (m_category.getMovement() != -1) {
panel.add(new JLabel("mvt " + m_category.getMovement()), new GridBagConstraints(gridx, yIndex, 1, 1, 0, 0,
GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, new Insets(0, 4, 0, 4), 0, 0));
}
if (m_category.getTransportCost() != -1) {
panel.add(new JLabel("cst " + m_category.getTransportCost()), new GridBagConstraints(gridx, yIndex, 1, 1, 0,
0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, new Insets(0, 4, 0, 4), 0, 0));
}
gridx++;
}
panel.add(label, new GridBagConstraints(gridx++, yIndex, 1, 1, 0, 0, GridBagConstraints.WEST,
GridBagConstraints.HORIZONTAL, nullInsets, 0, 0));
panel.add(scroll, new GridBagConstraints(gridx++, yIndex, 1, 1, 0, 0, GridBagConstraints.WEST,
GridBagConstraints.HORIZONTAL, new Insets(0, 4, 0, 0), 0, 0));
scroll.addChangeListener(field -> updateLeftToSelect());
}
updateLeftToSelect();
}
public int getMax() {
return m_hitTexts.get(0).getMax();
}
public void set(final int value) {
m_hitTexts.get(0).setValue(value);
}
public UnitCategory getCategory() {
return m_category;
}
public void selectAll() {
m_hitTexts.get(0).setValue(m_hitTexts.get(0).getMax());
}
public void selectAllMultipleHitPoints() {
for (final ScrollableTextField t : m_hitTexts) {
t.setValue(t.getMax());
}
}
public void selectNone() {
m_hitTexts.get(0).setValue(0);
}
public void setLeftToSelect(final int leftToSelect) {
m_leftToSelect = leftToSelect < 0 ? m_category.getUnits().size() : leftToSelect;
updateLeftToSelect();
}
private void updateLeftToSelect() {
int previousMax = m_category.getUnits().size();
for (int i = 0; i < m_hitTexts.size(); i++) {
final int newMax = m_leftToSelect + getHits(i);
final ScrollableTextField text = m_hitTexts.get(i);
if (i > 0 && !m_hasMultipleHits) {
text.setMax(0);
} else {
text.setMax(Math.min(newMax, previousMax));
}
if (text.getValue() < 0 || text.getValue() > text.getMax()) {
text.setValue(Math.max(0, Math.min(text.getMax(), text.getValue())));
}
m_hitLabel.get(i).setText("x" + (i == 0 ? m_category.getUnits().size() : text.getMax()));
previousMax = text.getValue();
}
}
public int getTotalHits() {
int hits = 0;
for (int i = 0; i < m_hitTexts.size(); i++) {
hits += getHits(i);
}
return hits;
}
public int getHits(final int zeroBasedHitsPosition) {
if (zeroBasedHitsPosition < 0 || zeroBasedHitsPosition > m_hitTexts.size() - 1) {
throw new IllegalArgumentException("Index out of range");
}
if (!m_hasMultipleHits && zeroBasedHitsPosition > 0) {
return 0;
}
return m_hitTexts.get(zeroBasedHitsPosition).getValue();
}
public int getFinalHit() {
return getHits(m_hitTexts.size() - 1);
}
public int getAllButFinalHit() {
int hits = 0;
for (int i = 0; i < m_hitTexts.size() - 1; i++) {
hits += getHits(i);
}
return hits;
}
public int size() {
return m_hitTexts.size();
}
public boolean hasMultipleHitPoints() {
return m_hasMultipleHits;
}
private class UnitChooserEntryIcon extends JComponent {
private static final long serialVersionUID = 591598594559651745L;
private final boolean m_forceDamaged;
private final IUIContext uiContext;
UnitChooserEntryIcon(final boolean forceDamaged, final IUIContext uiContext) {
m_forceDamaged = forceDamaged;
this.uiContext = uiContext;
}
@Override
public void paint(final Graphics g) {
super.paint(g);
final Optional<Image> image =
uiContext.getUnitImageFactory().getImage(m_category.getType(), m_category.getOwner(), m_data,
m_forceDamaged || m_category.hasDamageOrBombingUnitDamage(), m_category.getDisabled());
if (image.isPresent()) {
g.drawImage(image.get(), 0, 0, this);
}
final Iterator<UnitOwner> iter = m_category.getDependents().iterator();
int index = 1;
while (iter.hasNext()) {
final UnitOwner holder = iter.next();
final int x = uiContext.getUnitImageFactory().getUnitImageWidth() * index;
final Optional<Image> unitImg =
uiContext.getUnitImageFactory().getImage(holder.getType(), holder.getOwner(), m_data, false, false);
if (unitImg.isPresent()) {
g.drawImage(unitImg.get(), x, 0, this);
}
index++;
}
}
@Override
public int getWidth() {
// we draw a unit symbol for each dependent
return uiContext.getUnitImageFactory().getUnitImageWidth() * (1 + m_category.getDependents().size());
}
@Override
public int getHeight() {
return uiContext.getUnitImageFactory().getUnitImageHeight();
}
@Override
public Dimension getMaximumSize() {
return getDimension();
}
@Override
public Dimension getMinimumSize() {
return getDimension();
}
@Override
public Dimension getPreferredSize() {
return getDimension();
}
public Dimension getDimension() {
return new Dimension(getWidth(), getHeight());
}
}
}