package org.royaldev.thehumanity.history;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.royaldev.thehumanity.TheHumanity;
import org.royaldev.thehumanity.game.GameSnapshot;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class History {
private final TheHumanity humanity;
private final ObjectMapper objectMapper = new ObjectMapper();
private final Cache<String, GameSnapshot> cache = CacheBuilder.newBuilder()
.expireAfterAccess(1L, TimeUnit.HOURS)
.build();
private final Object saveLock = new Object();
public History(@NotNull final TheHumanity humanity) {
Preconditions.checkNotNull(humanity, "humanity was null");
this.humanity = humanity;
this.createHistorySchema();
}
private boolean createFile(@NotNull final File file) {
Preconditions.checkNotNull(file, "file was null");
try {
return !file.exists() && file.getParentFile().mkdirs() && file.createNewFile();
} catch (final IOException ex) {
throw new RuntimeException(ex);
}
}
private boolean createFolder(@NotNull final File folder) {
Preconditions.checkNotNull(folder, "folder was null");
return !folder.exists() && folder.mkdirs();
}
private void createHistorySchema() {
this.createFolder(this.getHistoryFolder());
}
@Nullable
private String loadGameSnapshotJSON(@NotNull final String channel, final int number) {
Preconditions.checkNotNull(channel, "channel was null");
final File gameLocation = this.getGameSnapshotFile(channel, number);
if (!gameLocation.exists()) return null;
final List<String> lines;
try {
lines = Files.readAllLines(gameLocation.toPath());
} catch (final IOException ex) {
throw new RuntimeException(ex);
}
return Joiner.on('\n').join(lines);
}
public int[] getAllGameNumbers(@NotNull final String channel) {
Preconditions.checkNotNull(channel, "channel was null");
final File channelFolder = this.getChannelFolder(channel);
if (!channelFolder.exists()) return new int[0];
if (!channelFolder.isDirectory()) throw new IllegalStateException("channel folder is not a directory");
return Arrays.stream(channelFolder.list()).mapToInt(name -> {
try {
return Integer.parseInt(name.split("\\.")[0]);
} catch (final NumberFormatException ex) {
return -1;
}
}).filter(number -> number > 0).toArray();
}
@NotNull
public File getChannelFolder(@NotNull final String channel) {
Preconditions.checkNotNull(channel, "channel was null");
return new File(this.getHistoryFolder(), channel.toLowerCase());
}
@NotNull
public File getGameSnapshotFile(@NotNull final String channel, final int number) {
Preconditions.checkNotNull(channel, "channel was null");
return new File(this.getChannelFolder(channel), number + ".json");
}
public File getHistoryFolder() {
return new File("history");
}
public int getLastGameSnapshotNumber(@NotNull final String channel) {
Preconditions.checkNotNull(channel, "channel was null");
return Arrays.stream(this.getAllGameNumbers(channel)).max().orElse(0);
}
/**
* Loads a GameSnapshot from the disk. If the game could not be located, this will return null.
* <p>Snapshots are cached for an hour before they are invalidated and loaded from the disk again.
* <p>If the number given is 0, the current game will be retrieved and a snapshot will be returned from that game.
* If there is no game, null will be returned. If the number is negative, an IllegalArgumentException will be
* thrown.
*
* @param channel Channel to load game from. Ex: "#CAHdev"
* @param number Number (not index) of the game to load. Ex: 5
* @return GameSnapshot or null
* @throws IllegalArgumentException If {@code number} is negative
*/
@Nullable
public GameSnapshot loadGameSnapshot(@NotNull final String channel, final int number) {
Preconditions.checkNotNull(channel, "channel was null");
if (number < 0) {
throw new IllegalArgumentException("Game number was negative");
}
final String cacheKey = channel.toLowerCase() + ":" + number;
final GameSnapshot cached = this.cache.getIfPresent(cacheKey);
if (cached != null) {
return cached;
}
final String json = this.loadGameSnapshotJSON(channel, number);
if (json == null) {
return null;
}
try {
final GameSnapshot gs = this.objectMapper.readValue(json, GameSnapshot.class);
this.cache.put(cacheKey, gs);
return gs;
} catch (final IOException ex) {
throw new RuntimeException(ex);
}
}
public void saveGameSnapshot(@NotNull final GameSnapshot gameSnapshot) {
synchronized (this.saveLock) {
Preconditions.checkNotNull(gameSnapshot, "gameSnapshot was null");
final String channel = gameSnapshot.getChannel();
final File gameLocation = this.getGameSnapshotFile(channel, this.getLastGameSnapshotNumber(channel) + 1);
this.createFile(gameLocation);
try {
Files.write(gameLocation.toPath(), gameSnapshot.toJSON().getBytes(StandardCharsets.UTF_8));
} catch (final IOException ex) {
throw new RuntimeException(ex);
}
}
}
}