package magic.ui.widget.deck;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.event.ActionEvent;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import javax.swing.AbstractAction;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.ScrollPaneConstants;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import magic.data.DeckType;
import magic.data.stats.MagicStats;
import magic.exception.InvalidDeckException;
import magic.firemind.FiremindJsonReader;
import magic.model.MagicDeck;
import magic.translate.MText;
import magic.ui.FontsAndBorders;
import magic.ui.dialog.DecksFilterDialog;
import magic.ui.screen.interfaces.IDeckConsumer;
import magic.ui.widget.duel.viewer.CardViewer;
import magic.utility.DeckUtils;
import net.miginfocom.swing.MigLayout;
@SuppressWarnings("serial")
public class DeckPicker extends JPanel {
// translatable strings
private static final String _S1 = "All Decks (%d)";
private static final String _S2 = "Filtered Decks (%d)";
// ui components
private final MigLayout migLayout = new MigLayout();
private final JComboBox<DeckType> deckTypeJCombo = new JComboBox<>();
private final JList<MagicDeck> decksJList = new JList<>();
private final JScrollPane scroller = new JScrollPane();
private final FilterPanel filterPanel = new FilterPanel();
// properties
private DeckType selectedDeckType = DeckType.Preconstructed;
private final List<IDeckConsumer> listeners = new ArrayList<>();
private ListSelectionListener listSelectionListener;
private DeckFilter deckFilter = null;
public DeckPicker() {
setLookAndFeel();
refreshLayout();
setListeners();
refreshContent();
}
public void addListener(final IDeckConsumer listener) {
if (!listeners.contains(listener)) {
listeners.add(listener);
}
}
private void setLookAndFeel() {
setOpaque(false);
setBackground(FontsAndBorders.TRANSLUCENT_WHITE_STRONG);
setLayout(migLayout);
setMaximumSize(CardViewer.getSidebarImageSize());
setPreferredSize(CardViewer.getSidebarImageSize());
// deck types combo
deckTypeJCombo.setLightWeightPopupEnabled(false);
deckTypeJCombo.setFocusable(false);
deckTypeJCombo.setFont(FontsAndBorders.FONT2);
// decks list
decksJList.setOpaque(false);
decksJList.setBackground(new Color(0, 0, 0, 1));
decksJList.setForeground(Color.BLACK);
decksJList.setFocusable(true);
decksJList.setCellRenderer(new DecksListCellRenderer());
// scroll pane for deck names list
scroller.setViewportView(decksJList);
scroller.setBorder(null);
scroller.setOpaque(false);
scroller.getViewport().setOpaque(false);
scroller.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
}
private void refreshLayout() {
migLayout.setLayoutConstraints("insets 0, gap 0, flowy");
migLayout.setColumnConstraints("[fill, grow]");
migLayout.setRowConstraints("[][fill, grow]");
add(getDeckFilterPanel());
add(scroller);
}
private JPanel getDeckFilterPanel() {
final JPanel panel = new JPanel(new MigLayout("insets 4, gap 0, flowy"));
panel.setOpaque(false);
panel.add(deckTypeJCombo, "w 100%, h 30!");
panel.add(filterPanel, "w 100%");
return panel;
}
private void setListeners() {
// deck types combo
deckTypeJCombo.addItemListener(new ItemListener() {
@Override
public void itemStateChanged(final ItemEvent e) {
if (e.getStateChange() == ItemEvent.SELECTED) {
// ensures dropdown list closes on click *before* refreshing decks list.
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
selectedDeckType = (DeckType) e.getItem();
refreshDecksList();
setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
}
});
}
}
});
// deck names list : need to be able to add/remove to prevent multiple events.
listSelectionListener = getListSelectionListener();
}
final public void refreshContent() {
// deck types combo
final DeckType deckTypes[] = DeckType.getPredefinedDecks().toArray(new DeckType[0]);
deckTypeJCombo.setModel(new DefaultComboBoxModel<>(deckTypes));
deckTypeJCombo.setSelectedIndex(0);
refreshDecksList();
}
private void refreshDecksList() {
// ignore list selection events while setting list data.
decksJList.removeListSelectionListener(listSelectionListener);
decksJList.setListData(getDecksListData());
decksJList.addListSelectionListener(listSelectionListener);
if (decksJList.getModel().getSize() > 0) {
decksJList.setSelectedIndex(0);
filterPanel.setDecksCount(decksJList.getModel().getSize());
} else {
filterPanel.setDecksCount(0);
for (IDeckConsumer listener : listeners) {
listener.setDeck(new MagicDeck());
}
}
}
private MagicDeck[] getPopularDecks() {
return MagicStats.getMostPlayedDecks().toArray(new MagicDeck[0]);
}
private MagicDeck[] getWinningDecks() {
return MagicStats.getTopWinningDecks().toArray(new MagicDeck[0]);
}
private MagicDeck[] getRecentDecks() {
return MagicStats.getRecentlyPlayedDecks().toArray(new MagicDeck[0]);
}
private MagicDeck[] getDecksListData() {
switch (selectedDeckType) {
case Preconstructed:
return getFilteredDecksListData(DeckUtils.getPrebuiltDecksFolder());
case Custom:
return getFilteredDecksListData(DeckUtils.getDecksFolder());
case Firemind:
FiremindJsonReader.refreshTopDecks();
return getFilteredDecksListData(DeckUtils.getFiremindDecksFolder());
case PopularDecks:
return getPopularDecks();
case WinningDecks:
return getWinningDecks();
case RecentDecks:
return getRecentDecks();
default:
return new MagicDeck[0];
}
}
private MagicDeck[] getFilteredDecksListData(final Path decksPath) {
try (DirectoryStream<Path> ds = Files.newDirectoryStream(decksPath, "*.{dec}")) {
final List<MagicDeck> decks = loadDecks(ds);
sortDecksByFilename(decks);
return decks.toArray(new MagicDeck[0]);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* In Windows, DirectoryStream returns a list of files already sorted by
* filename. In Linux it does not, so need to specifically sort list.
*/
private void sortDecksByFilename(final List<MagicDeck> decks) {
Collections.sort(decks, new Comparator<MagicDeck>() {
@Override
public int compare(MagicDeck o1, MagicDeck o2) {
return o1.getFilename().compareToIgnoreCase(o2.getFilename());
}
});
}
private List<MagicDeck> loadDecks(final DirectoryStream<Path> ds) {
final List<MagicDeck> decks = new ArrayList<>();
for (final Path filePath : ds) {
final MagicDeck deck = loadDeck(filePath);
if (isValidFilteredDeck(deck)) {
decks.add(deck);
}
}
return decks;
}
private MagicDeck loadDeck(final Path deckFilePath) {
try {
return DeckUtils.loadDeckFromFile(deckFilePath);
} catch (InvalidDeckException ex) {
// Instead of prompting user with an error dialog for each
// invalid deck found, create an empty deck flagged as invalid
// with its description set to the error message. Invalid decks
// will have their names scored out in the decks list.
final MagicDeck deck = new MagicDeck();
deck.setFilename(deckFilePath.getFileName().toString());
deck.setInvalidDeck(ex.toString());
return deck;
}
}
private boolean isValidFilteredDeck(final MagicDeck deck) {
if (deckFilter != null) {
return deckFilter.isDeckValid(deck);
} else {
return true;
}
}
private ListSelectionListener getListSelectionListener() {
return new ListSelectionListener() {
@Override
public void valueChanged(final ListSelectionEvent e) {
if (!e.getValueIsAdjusting()) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
final MagicDeck deck = decksJList.getSelectedValue();
for (IDeckConsumer listener : listeners) {
if (selectedDeckType == DeckType.Random) {
listener.setDeck(deck.getName(), selectedDeckType);
} else if (selectedDeckType == DeckType.PopularDecks) {
listener.setDeck(deck);
} else if (selectedDeckType == DeckType.WinningDecks) {
listener.setDeck(deck);
} else if (selectedDeckType == DeckType.RecentDecks) {
listener.setDeck(deck);
} else {
listener.setDeck(deck, getDeckPath(deck.getName(), selectedDeckType));
}
}
setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
}
});
}
}
private Path getDeckPath(final String deckName, final DeckType deckType) {
switch (deckType) {
case Preconstructed:
return DeckUtils.getPrebuiltDecksFolder().resolve(deckName + ".dec");
case Custom:
return DeckUtils.getDecksFolder().resolve(deckName + ".dec");
case Firemind:
return DeckUtils.getDecksFolder().resolve("firemind").resolve(deckName + ".dec");
default:
throw new RuntimeException("getDeckPath() not implemented for decktype: " + deckType);
}
}
};
}
private class FilterPanel extends JPanel {
// ui components
private final MigLayout migLayout = new MigLayout();
private final JButton filterButton = new JButton();
public FilterPanel() {
setLookAndFeel();
refreshLayout();
setListeners();
// refreshContent();
}
private void setLookAndFeel() {
setOpaque(false);
setLayout(migLayout);
// filter button
filterButton.setFont(FontsAndBorders.FONT1);
filterButton.setFocusable(false);
filterButton.setHorizontalAlignment(SwingConstants.LEFT);
}
private void refreshLayout() {
removeAll();
migLayout.setLayoutConstraints("insets 0");
add(filterButton, "w 100%");
}
private void setListeners() {
// deck types combo
filterButton.addActionListener(new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
final DecksFilterDialog dialog = new DecksFilterDialog();
dialog.setVisible(true);
if (!dialog.isCancelled()) {
deckFilter = dialog.getDeckFilter();
refreshDecksList();
}
}
});
}
public void setDecksCount(final int deckCount) {
filterButton.setText(MText.get(deckFilter == null ? _S1 : _S2, deckCount));
}
}
}