package launcher.serialize.config;
import launcher.LauncherAPI;
import launcher.helper.VerifyHelper;
import launcher.serialize.config.entry.*;
import java.io.IOException;
import java.io.LineNumberReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public final class TextConfigReader
{
private final LineNumberReader reader;
private final boolean ro;
private String skipped;
private int ch = -1;
private TextConfigReader(Reader reader, boolean ro)
{
this.reader = new LineNumberReader(reader);
this.reader.setLineNumber(1);
this.ro = ro;
}
@LauncherAPI
public static BlockConfigEntry read(Reader reader, boolean ro) throws IOException
{
return new TextConfigReader(reader, ro).readBlock(0);
}
private IOException newIOException(String message)
{
return new IOException(message + " (line " + reader.getLineNumber() + ')');
}
private int nextChar(boolean eof) throws IOException
{
ch = reader.read();
if (eof && ch < 0)
{
throw newIOException("Unexpected end of config");
}
return ch;
}
private int nextClean(boolean eof) throws IOException
{
nextChar(eof);
return skipWhitespace(eof);
}
private BlockConfigEntry readBlock(int cc) throws IOException
{
Map<String, ConfigEntry<?>> map = new LinkedHashMap<>(16);
// Read block entries
boolean brackets = ch == '{';
while (nextClean(brackets) >= 0 && (!brackets || ch != '}'))
{
String preNameComment = skipped;
// Read entry name
String name = readToken();
if (skipWhitespace(true) != ':')
{
throw newIOException("Value start expected");
}
String postNameComment = skipped;
// Read entry value
nextClean(true);
String preValueComment = skipped;
ConfigEntry<?> entry = readEntry(4);
if (skipWhitespace(true) != ';')
{
throw newIOException("Value end expected");
}
// Set comments
entry.setComment(0, preNameComment);
entry.setComment(1, postNameComment);
entry.setComment(2, preValueComment);
entry.setComment(3, skipped);
// Try add entry to map
if (map.put(name, entry) != null)
{
throw newIOException(String.format("Duplicate config entry: '%s'", name));
}
}
// Set comment after last entry and return block
BlockConfigEntry block = new BlockConfigEntry(map, ro, cc + 1);
block.setComment(cc, skipped);
nextChar(false);
return block;
}
private ConfigEntry<?> readEntry(int cc) throws IOException
{
// Try detect type by first char
switch (ch)
{
case '"': // String
return readString(cc);
case '[': // List
return readList(cc);
case '{': // Block
return readBlock(cc);
default:
break;
}
// Possibly integer value
if (ch == '-' || ch >= '0' && ch <= '9')
{
return readInteger(cc);
}
// Statement?
String statement = readToken();
switch (statement)
{
case "true":
return new BooleanConfigEntry(Boolean.TRUE, ro, cc);
case "false":
return new BooleanConfigEntry(Boolean.FALSE, ro, cc);
default:
throw newIOException(String.format("Unknown statement: '%s'", statement));
}
}
private ConfigEntry<Integer> readInteger(int cc) throws IOException
{
return new IntegerConfigEntry(Integer.parseInt(readToken()), ro, cc);
}
private ConfigEntry<List<ConfigEntry<?>>> readList(int cc) throws IOException
{
List<ConfigEntry<?>> listValue = new ArrayList<>(16);
// Read list elements
boolean hasNextElement = nextClean(true) != ']';
String preValueComment = skipped;
while (hasNextElement)
{
ConfigEntry<?> element = readEntry(2);
hasNextElement = skipWhitespace(true) != ']';
element.setComment(0, preValueComment);
element.setComment(1, skipped);
listValue.add(element);
// Prepare for next element read
if (hasNextElement)
{
if (ch != ',')
{
throw newIOException("Comma expected");
}
nextClean(true);
preValueComment = skipped;
}
}
// Set in-list comment (if no elements)
boolean additional = listValue.isEmpty();
ConfigEntry<List<ConfigEntry<?>>> list = new ListConfigEntry(listValue, ro, additional ? cc + 1 : cc);
if (additional)
{
list.setComment(cc, skipped);
}
// Return list
nextChar(false);
return list;
}
private ConfigEntry<?> readString(int cc) throws IOException
{
StringBuilder builder = new StringBuilder();
// Read string chars
while (nextChar(true) != '"')
{
switch (ch)
{
case '\r':
case '\n': // String termination
throw newIOException("String termination");
case '\\':
int next = nextChar(true);
switch (next)
{
case 't':
builder.append('\t');
break;
case 'b':
builder.append('\b');
break;
case 'n':
builder.append('\n');
break;
case 'r':
builder.append('\r');
break;
case 'f':
builder.append('\f');
break;
case '"':
case '\\':
builder.append((char) next);
break;
default:
throw newIOException("Illegal char escape: " + (char) next);
}
break;
default: // Normal character
builder.append((char) ch);
break;
}
}
// Return string
nextChar(false);
return new StringConfigEntry(builder.toString(), ro, cc);
}
private String readToken() throws IOException
{
// Read token
StringBuilder builder = new StringBuilder();
while (VerifyHelper.isValidIDNameChar(ch))
{
builder.append((char) ch);
nextChar(false);
}
// Return token as string
String token = builder.toString();
if (token.isEmpty())
{
throw newIOException("Not a token");
}
return token;
}
private void skipComment(StringBuilder skippedBuilder, boolean eof) throws IOException
{
while (ch >= 0 && ch != '\r' && ch != '\n')
{
skippedBuilder.append((char) ch);
nextChar(eof);
}
}
private int skipWhitespace(boolean eof) throws IOException
{
StringBuilder skippedBuilder = new StringBuilder();
while (Character.isWhitespace(ch) || ch == '#')
{
if (ch == '#')
{
skipComment(skippedBuilder, eof);
continue;
}
skippedBuilder.append((char) ch);
nextChar(eof);
}
skipped = skippedBuilder.toString();
return ch;
}
}