package chatty.gui.components.srl;
import chatty.gui.components.menus.ContextMenuListener;
import chatty.util.DateTime;
import chatty.util.srl.Race;
import chatty.util.srl.Race.Entrant;
import chatty.util.srl.SpeedrunsLive;
import chatty.util.srl.SpeedrunsLiveListener;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
/**
* Manages the GUI part of the SRL races stuff. Requests updates of the race
* data, can open the appropriate dialogs etc.
*
* @author tduva
*/
public class SRL {
/**
* The actual update timer, which checks IF a data update should be done and
* updates parts of the GUI.
*/
private final static int UPDATE_DELAY = 4000;
/**
* Update races data automatically at most at this interval.
*/
private final static int AUTO_UPDATE_DELAY = 2 * 60 * 1000;
/**
* After how much time the current race data should be considered old, which
* means it is updated when an active action is performed (like opening the
* dialog, searching for a player). This does not mean it updates fully
* automatic after this time.
*/
private final static int DATA_STALE_DELAY = 30*1000;
private final Window parent;
private final SRLRaces racesDialog;
private final SRLRace raceInfo;
private final SRLRaceFinder raceFinder;
private final SpeedrunsLive srl;
private Collection<Race> currentRaces;
private long lastReceived;
private boolean loading;
private long lastError;
private String errorMessage;
private String entrantToSearch;
public SRL(Window parent, SpeedrunsLive srl, ContextMenuListener contextMenuListener) {
this.srl = srl;
this.parent = parent;
this.racesDialog = new SRLRaces(parent, this);
this.raceInfo = new SRLRace(parent, contextMenuListener);
this.raceFinder = new SRLRaceFinder(parent, this);
raceFinder.addComponentListener(new ComponentAdapter() {
@Override
public void componentHidden(ComponentEvent e) {
cancelSearch();
}
});
Timer timer = new Timer(UPDATE_DELAY, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
update();
}
});
timer.setRepeats(true);
timer.start();
srl.addListener(new MySpeedrunsLiveListener());
}
/**
* Shows the list of current races, requesting a data update if the data is
* old enough.
*/
public void openRaceList() {
if (isDataStale()) {
reload();
}
racesDialog.setLocationRelativeTo(parent);
racesDialog.showDialog();
}
/**
* Search for a race with the given entrant. Opens the race finder as
* appropriate feedback and either request data if it is old or immediately
* start searching in the cached data.
*
* @param stream
*/
public void searchRaceWithEntrant(String stream) {
entrantToSearch = stream;
raceFinder.setLocationRelativeTo(parent);
raceFinder.open(stream);
if (isDataStale()) {
reload();
} else {
searchRaceWithEntrant(currentRaces);
}
}
/**
* Actually search in the current race data for races that contain the
* saved entrant. Loads the found races into the race finder or opens the
* race info directly if only one was found and no race info is open.
*
* @param races
*/
private void searchRaceWithEntrant(Collection<Race> races) {
if (entrantToSearch != null) {
Collection<Race> foundRaces = findRaces(entrantToSearch, races);
if (foundRaces.size() == 1 && !raceInfo.isVisible()) {
raceFinder.close();
openRaceInfo(foundRaces.iterator().next());
} else {
raceFinder.setFoundRaces(foundRaces);
}
entrantToSearch = null;
}
}
/**
* Don't continue the search if requested data is received. This happens
* when the race finder is closed.
*/
private void cancelSearch() {
entrantToSearch = null;
}
/**
* Opens the info dialog for the given race.
*
* @param race
*/
protected void openRaceInfo(Race race) {
if (!raceInfo.isVisible()) {
if (racesDialog.isVisible()) {
raceInfo.setLocationRelativeTo(racesDialog);
} else {
raceInfo.setLocationRelativeTo(parent);
}
}
raceInfo.open(race);
}
protected boolean isAutoUpdating() {
return raceInfo.isVisible();
}
protected boolean isOpen(Race race) {
return raceInfo.isVisible() && race.equals(raceInfo.getRace());
}
private void update() {
if (loading) {
racesDialog.setStatusText("Loading..");
} else {
String infoText = "";
long ago = System.currentTimeMillis() - lastReceived;
long errorAgo = System.currentTimeMillis() - lastError;
if (lastReceived > 0) {
infoText += "Last updated: "
+ DateTime.duration(ago, 1, 0) + " ago";
}
if (isAutoUpdating()) {
infoText += " (auto updating)";
if (ago > AUTO_UPDATE_DELAY
&& (lastError == -1 || errorAgo > AUTO_UPDATE_DELAY)) {
reload();
return;
}
}
if (lastError != -1) {
infoText += String.format(" [%s ago: %s]",
DateTime.duration(errorAgo, 2, 0),
errorMessage);
}
racesDialog.setStatusText(infoText);
}
}
private boolean isDataStale() {
return System.currentTimeMillis() - lastReceived > DATA_STALE_DELAY;
}
protected void reload() {
setLoading(true);
srl.requestRaces();
}
private void setLoading(boolean loading) {
racesDialog.setLoading(loading);
raceFinder.setLoading(loading);
this.loading = loading;
update();
}
public void setRaces(List<Race> newRaces) {
currentRaces = newRaces;
lastError = -1;
racesDialog.setRaces(newRaces);
raceInfo.update(newRaces);
lastReceived = System.currentTimeMillis();
setLoading(false);
searchRaceWithEntrant(newRaces);
}
protected void requestError(String description) {
lastError = System.currentTimeMillis();
errorMessage = description;
raceInfo.error();
raceFinder.error();
setLoading(false);
}
/**
* Receiving data from SRL.
*/
private class MySpeedrunsLiveListener implements SpeedrunsLiveListener {
@Override
public void racesReceived(final List<Race> newRaces) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
setRaces(newRaces);
}
});
}
@Override
public void error(final String description) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
requestError(description);
}
});
}
}
/**
* Help function to find races that contain the entrant with the given
* stream.
*
* @param stream
* @param races
* @return
*/
public static final Collection<Race> findRaces(String stream, Collection<Race> races) {
Collection<Race> result = new ArrayList<>();
for (Race r : races) {
for (Entrant e : r.getEntrants()) {
if (stream.equalsIgnoreCase(e.twitch)) {
result.add(r);
}
}
}
return result;
}
/**
* Return the SRL name for entrant with the given stream, based on the
* given race.
*
* @param stream
* @param race
* @return The SRL name for the given stream or {@code null} if not in this
* race
*/
public static final String findSrlName(String stream, Race race) {
for (Entrant entrant : race.getEntrants()) {
if (stream.equalsIgnoreCase(entrant.twitch)) {
return entrant.name;
}
}
return null;
}
}