package com.faforever.client.replay;
import com.faforever.client.fx.FxmlLoader;
import com.faforever.client.i18n.I18n;
import com.faforever.client.map.MapService;
import com.faforever.client.notification.Action;
import com.faforever.client.notification.DismissAction;
import com.faforever.client.notification.NotificationService;
import com.faforever.client.notification.PersistentNotification;
import com.faforever.client.notification.ReportAction;
import com.faforever.client.notification.Severity;
import com.faforever.client.reporting.ReportingService;
import com.faforever.client.task.TaskService;
import com.faforever.client.util.TimeService;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import javafx.application.Platform;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableMap;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeTableCell;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableRow;
import javafx.scene.control.TreeTableView;
import javafx.scene.control.cell.TextFieldTreeTableCell;
import javafx.scene.image.ImageView;
import javafx.util.StringConverter;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.lang.invoke.MethodHandles;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.CompletionStage;
import java.util.stream.Collectors;
import static java.util.Arrays.asList;
public class ReplayVaultController {
private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
@FXML
TreeTableView<ReplayInfoBean> replayVaultRoot;
@FXML
TreeTableColumn<ReplayInfoBean, Number> idColumn;
@FXML
TreeTableColumn<ReplayInfoBean, String> titleColumn;
@FXML
TreeTableColumn<ReplayInfoBean, String> playersColumn;
@FXML
TreeTableColumn<ReplayInfoBean, Instant> timeColumn;
@FXML
TreeTableColumn<ReplayInfoBean, Duration> durationColumn;
@FXML
TreeTableColumn<ReplayInfoBean, String> gameTypeColumn;
@FXML
TreeTableColumn<ReplayInfoBean, String> mapColumn;
@Resource
NotificationService notificationService;
@Resource
ReplayService replayService;
@Resource
MapService mapService;
@Resource
TaskService taskService;
@Resource
I18n i18n;
@Resource
TimeService timeService;
@Resource
ReportingService reportingService;
@Resource
FxmlLoader fxmlLoader;
@Resource
ApplicationContext applicationContext;
@Resource
Locale locale;
@VisibleForTesting
TreeItem<ReplayInfoBean> localReplaysRoot;
@VisibleForTesting
TreeItem<ReplayInfoBean> onlineReplaysRoot;
@SuppressWarnings("unchecked")
@PostConstruct
void postConstruct() {
localReplaysRoot = new TreeItem<>(new ReplayInfoBean(i18n.get("replays.localReplays")));
localReplaysRoot.setExpanded(true);
onlineReplaysRoot = new TreeItem<>(new ReplayInfoBean(i18n.get("replays.onlineReplays")));
onlineReplaysRoot.setExpanded(true);
TreeItem<ReplayInfoBean> tableRoot = new TreeItem<>(new ReplayInfoBean("invisibleRootItem"));
tableRoot.getChildren().addAll(localReplaysRoot, onlineReplaysRoot);
replayVaultRoot.setRoot(tableRoot);
replayVaultRoot.setRowFactory(param -> replayRowFactory());
replayVaultRoot.getSortOrder().setAll(Collections.singletonList(timeColumn));
idColumn.setCellValueFactory(param -> param.getValue().getValue().idProperty());
idColumn.setCellFactory(this::idCellFactory);
titleColumn.setCellValueFactory(param -> param.getValue().getValue().titleProperty());
timeColumn.setCellValueFactory(param -> param.getValue().getValue().startTimeProperty());
timeColumn.setCellFactory(this::timeCellFactory);
timeColumn.setSortType(TreeTableColumn.SortType.DESCENDING);
gameTypeColumn.setCellValueFactory(param -> param.getValue().getValue().gameTypeProperty());
mapColumn.setCellValueFactory(param -> param.getValue().getValue().mapProperty());
mapColumn.setCellFactory(this::mapCellFactory);
playersColumn.setCellValueFactory(this::playersValueFactory);
durationColumn.setCellValueFactory(this::durationCellValueFactory);
durationColumn.setCellFactory(this::durationCellFactory);
}
@NotNull
private TreeTableRow<ReplayInfoBean> replayRowFactory() {
TreeTableRow<ReplayInfoBean> row = new TreeTableRow<>();
row.setOnMouseClicked(event -> {
// If ID == 0, this isn't an entry but root node
if (event.getClickCount() == 2 && !row.isEmpty() && row.getItem().getId() != 0) {
replayService.runReplay(row.getItem());
}
});
return row;
}
private ObservableValue<String> playersValueFactory(TreeTableColumn.CellDataFeatures<ReplayInfoBean, String> features) {
return new StringBinding() {
@Override
protected String computeValue() {
ReplayInfoBean replayInfoBean = features.getValue().getValue();
ObservableMap<String, List<String>> teams = replayInfoBean.getTeams();
if (teams.isEmpty()) {
return "";
}
ArrayList<String> teamsAsStrings = new ArrayList<>();
for (List<String> playerNames : teams.values()) {
Collections.sort(playerNames);
teamsAsStrings.add(Joiner.on(i18n.get("textSeparator")).join(playerNames));
}
return Joiner.on(i18n.get("vsSeparator")).join(teamsAsStrings);
}
};
}
private TreeTableCell<ReplayInfoBean, Instant> timeCellFactory(TreeTableColumn<ReplayInfoBean, Instant> column) {
TextFieldTreeTableCell<ReplayInfoBean, Instant> cell = new TextFieldTreeTableCell<>();
cell.setConverter(new StringConverter<Instant>() {
@Override
public String toString(Instant object) {
return timeService.lessThanOneDayAgo(object);
}
@Override
public Instant fromString(String string) {
return null;
}
});
return cell;
}
private TreeTableCell<ReplayInfoBean, String> mapCellFactory(TreeTableColumn<ReplayInfoBean, String> column) {
final ImageView imageVew = fxmlLoader.loadAndGetRoot("map_preview_table_cell.fxml", this);
TreeTableCell<ReplayInfoBean, String> cell = new TreeTableCell<ReplayInfoBean, String>() {
@Override
protected void updateItem(String mapName, boolean empty) {
super.updateItem(mapName, empty);
if (empty || mapName == null) {
setText(null);
setGraphic(null);
} else {
imageVew.setImage(mapService.loadSmallPreview(mapName));
setGraphic(imageVew);
setText(mapName);
}
}
};
cell.setGraphic(imageVew);
return cell;
}
private TreeTableCell<ReplayInfoBean, Number> idCellFactory(TreeTableColumn<ReplayInfoBean, Number> column) {
TextFieldTreeTableCell<ReplayInfoBean, Number> cell = new TextFieldTreeTableCell<>();
cell.setConverter(new StringConverter<Number>() {
@Override
public String toString(Number object) {
if (object.intValue() == 0) {
return "";
}
return String.format(locale, "%d", object.intValue());
}
@Override
public Number fromString(String string) {
return null;
}
});
return cell;
}
private TreeTableCell<ReplayInfoBean, Duration> durationCellFactory(TreeTableColumn<ReplayInfoBean, Duration> column) {
TextFieldTreeTableCell<ReplayInfoBean, Duration> cell = new TextFieldTreeTableCell<>();
cell.setConverter(new StringConverter<Duration>() {
@Override
public String toString(Duration object) {
if (object == null) {
return "";
}
return timeService.shortDuration(object);
}
@Override
public Duration fromString(String string) {
return null;
}
});
return cell;
}
@NotNull
private ObservableValue<Duration> durationCellValueFactory(TreeTableColumn.CellDataFeatures<ReplayInfoBean, Duration> param) {
ReplayInfoBean replayInfoBean = param.getValue().getValue();
Instant startTime = replayInfoBean.getStartTime();
Instant endTime = replayInfoBean.getEndTime();
if (startTime == null || endTime == null) {
return new SimpleObjectProperty<>(null);
}
return new SimpleObjectProperty<>(Duration.between(startTime, endTime));
}
public CompletionStage<Void> loadLocalReplaysInBackground() {
LoadLocalReplaysTask task = applicationContext.getBean(LoadLocalReplaysTask.class);
localReplaysRoot.getChildren().clear();
return taskService.submitTask(task).getFuture()
.thenAccept(this::addLocalReplays)
.exceptionally(throwable -> {
logger.warn("Error while loading local replays", throwable);
notificationService.addNotification(new PersistentNotification(
i18n.get("replays.loadingLocalTask.failed"),
Severity.ERROR, asList(new ReportAction(i18n, reportingService, throwable), new DismissAction(i18n))
));
return null;
}
);
}
private void addLocalReplays(Collection<ReplayInfoBean> result) {
Collection<TreeItem<ReplayInfoBean>> items = result.stream()
.map(TreeItem::new).collect(Collectors.toCollection(ArrayList::new));
Platform.runLater(() -> localReplaysRoot.getChildren().addAll(items));
}
public void loadOnlineReplaysInBackground() {
replayService.getOnlineReplays()
.thenAccept(this::addOnlineReplays)
.exceptionally(throwable -> {
logger.warn("Error while loading online replays", throwable);
notificationService.addNotification(new PersistentNotification(
i18n.get("replays.loadingOnlineTask.failed"),
Severity.ERROR,
Collections.singletonList(new Action(i18n.get("report"), event -> reportingService.reportError(throwable)))
));
return null;
});
}
private void addOnlineReplays(Collection<ReplayInfoBean> result) {
Collection<TreeItem<ReplayInfoBean>> items = result.stream()
.map(TreeItem::new).collect(Collectors.toCollection(ArrayList::new));
Platform.runLater(() -> onlineReplaysRoot.getChildren().addAll(items));
}
public Node getRoot() {
return replayVaultRoot;
}
}