package org.royaldev.thehumanity.cards.packs;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.royaldev.thehumanity.TheHumanity;
import org.royaldev.thehumanity.cards.types.BlackCard;
import org.royaldev.thehumanity.cards.types.WhiteCard;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* A parser for CardPack files.
*/
public class CardPackParser {
private static final Pattern cardPackArgumentPattern = Pattern.compile("(\".+?(?<!\\\\)\"|'.+?(?<!\\\\)'|\\S+)");
@NotNull
private final TheHumanity humanity;
public CardPackParser(@NotNull final TheHumanity humanity) {
Preconditions.checkNotNull(humanity, "humanity was null");
this.humanity = humanity;
}
/**
* Takes arguments, usually passed to a command, for a list of CardPacks, then converts them into a list of names.
* This supports quoted card packs to allow spaces and quotes.
* <p>See <a href="http://rubular.com/r/XYxY0EFyLt">this</a> regex.
*
* @param args Array of arguments
* @return List of names
*/
@NotNull
public static List<String> getListOfCardPackNames(final String[] args, final List<String> defaultPacks) {
final List<String> names = Lists.newArrayList();
final String joined = Joiner.on(' ').join(args);
final Matcher m = CardPackParser.cardPackArgumentPattern.matcher(joined);
while (m.find()) {
String name = m.group(1).replaceAll("(\\\\(?!\\s))", "");
if (name.equalsIgnoreCase("default") || name.equalsIgnoreCase("defaults")) {
names.addAll(defaultPacks);
continue;
}
if (name.length() > 1 && (name.startsWith("\"") && name.endsWith("\"") || name.startsWith("'") && name.endsWith("'"))) {
name = name.substring(1, name.length() - 1);
}
names.add(name);
}
return names;
}
/**
* Gets the name of a pack from a file name.
*
* @param fileName File name
* @return Pack name
*/
@NotNull
public static String getNameFromFileName(@NotNull final String fileName) {
Preconditions.checkNotNull(fileName, "fileName was null");
final List<String> parts = Splitter.on('.').splitToList(fileName);
return Joiner.on('.').join(parts.subList(0, parts.size() - 1));
}
/**
* Parses one CardPack given the name of the file that contains it. If there is any IOException while processing, or
* if the file cannot be read, null will be returned.
*
* @param name Name of the file the CardPack is contained in
* @return CardPack or null
*/
@Nullable
public CAHCardPack parseCardPack(@NotNull final String name) {
Preconditions.checkNotNull(name, "name was null");
final File f = new File("cardpacks", name);
if (!f.exists() || !f.isFile()) {
this.humanity.getLogger().warning(f.getName() + " does not exist.");
return null;
}
if (!f.canRead()) {
this.humanity.getLogger().warning("Cannot read " + f.getName() + ".");
return null;
}
final CAHCardPack cp = new MemoryCardPack(getNameFromFileName(f.getName()));
try (final BufferedReader br = new BufferedReader(new FileReader(f))) {
String line;
ParseStage ps = ParseStage.METADATA;
while ((line = br.readLine()) != null) {
line = line.trim();
if (line.isEmpty() || line.startsWith("#")) continue;
ps = ps.parse(cp, line);
}
} catch (final IOException ex) {
this.humanity.getLogger().warning(ex.getMessage());
return null;
}
return cp;
}
/**
* Returns a Collection of CardPacks given an array of their file names. This calls the {@link #parseCardPack}
* method, but all null results are filtered out of the returned Collection.
*
* @param names List of names of files CardPacks are contained in
* @return Collection not containing null
*/
@NotNull
public Collection<CAHCardPack> parseCardPacks(@NotNull final List<String> names) {
Preconditions.checkNotNull(names, "names was null");
return names.stream().map(this::parseCardPack).filter(cp -> cp != null).collect(Collectors.toCollection(ArrayList::new));
}
/**
* Stages of parsing for {@link CardPackParser}.
*/
private enum ParseStage {
/**
* Parsing the metadata section.
*/
METADATA("___METADATA___") {
@NotNull
@Override
ParseStage parseInternal(@NotNull final CAHCardPack cp, @NotNull final String line) {
Preconditions.checkNotNull(cp, "cp was null");
Preconditions.checkNotNull(line, "line was null");
final String[] parts = line.split("\\s*:\\s*");
if (parts.length < 2) return this;
final String key = parts[0];
final String value = Joiner.on(' ').join(Arrays.copyOfRange(parts, 1, parts.length));
switch (key.toLowerCase()) {
case "description":
cp.setDescription(value);
break;
case "author":
cp.setAuthor(value);
break;
}
return this;
}
},
/**
* Parsing the black cards section.
*/
BLACK_CARDS("___BLACK___") {
@NotNull
@Override
ParseStage parseInternal(@NotNull final CAHCardPack cp, @NotNull final String line) {
Preconditions.checkNotNull(cp, "cp was null");
Preconditions.checkNotNull(line, "line was null");
cp.addCard(new BlackCard(cp, line));
return this;
}
},
/**
* Parsing the white cards section.
*/
WHITE_CARDS("___WHITE___") {
@NotNull
@Override
ParseStage parseInternal(@NotNull final CAHCardPack cp, @NotNull final String line) {
Preconditions.checkNotNull(cp, "cp was null");
Preconditions.checkNotNull(line, "line was null");
cp.addCard(new WhiteCard(cp, line));
return this;
}
};
private final String header;
ParseStage(final String header) {
this.header = header;
}
/**
* Gets the matching ParseStage from a given header line. If there is no matching header, null will be returned.
*
* @param line Header line to match
* @return ParseType or null if no matching header
*/
@Nullable
static ParseStage getHeaderType(@NotNull final String line) {
Preconditions.checkNotNull(line, "line was null");
return Arrays.stream(ParseStage.values())
.filter(ps -> ps.getHeader().equals(line))
.findFirst()
.orElse(null);
}
@NotNull
abstract ParseStage parseInternal(@NotNull final CAHCardPack cp, @NotNull final String line);
/**
* Gets the header of this section (e.g. "___BLACK___"). The header declares the start of a new section in a
* CardPack file.
*
* @return Header
*/
@NotNull
String getHeader() {
return this.header;
}
/**
* Parses one line into the given CardPack. This will return the next ParseStage to use for correct parsing.
*
* @param cp CardPack to parse for
* @param line Line to parse
* @return The next ParseStage, never null
*/
@NotNull
ParseStage parse(@NotNull final CAHCardPack cp, @NotNull final String line) {
Preconditions.checkNotNull(cp, "cp was null");
Preconditions.checkNotNull(line, "line was null");
final ParseStage headerType = ParseStage.getHeaderType(line);
if (headerType != null) {
return headerType;
}
return this.parseInternal(cp, line);
}
}
}