package the8472.bencode;
import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
public class PrettyPrinter {
StringBuilder builder;
int nesting;
boolean guessHuman;
CharSequence indent;
public PrettyPrinter() {
builder = new StringBuilder();
}
public PrettyPrinter(StringBuilder b) {
builder = b;
}
public void indent(CharSequence i) {
indent = i;
}
public void guessHumanReadableStringValues(boolean value) {
guessHuman = value;
}
enum LinebreakOmit {
NONE, NEXT, TRY
}
LinebreakOmit previousState = LinebreakOmit.NONE;
private void linebreak(LinebreakOmit state) {
if(indent == null)
return;
if(state == LinebreakOmit.TRY && previousState == LinebreakOmit.NEXT) {
previousState = LinebreakOmit.NONE;
return;
}
builder.append('\n');
for(int i=0;i<nesting;i++)
builder.append(indent);
previousState = state;
}
public PrettyPrinter append(Object o) {
prettyPrintInternal(o);
return this;
}
@Override
public String toString() {
return builder.toString();
}
void prettyPrintInternal(Object o) {
if(o instanceof Map) {
Map<Object,Object> m = (Map<Object, Object>) o;
builder.append("{");
nesting++;
if(m.size() > 0)
linebreak(LinebreakOmit.NONE);
Iterator<Entry<Object,Object>> it = m.entrySet().iterator();
while(it.hasNext()) {
Map.Entry<?,?> e = it.next();
prettyPrintInternal(e.getKey());
builder.append(":");
prettyPrintInternal(e.getValue());
if(it.hasNext()) {
builder.append(", ");
linebreak(LinebreakOmit.NONE);
}
}
nesting--;
if(m.size() > 0)
linebreak(LinebreakOmit.NEXT);
builder.append("}");
return;
}
if(o instanceof List) {
List<?> l = (List<?>) o;
builder.append("[");
nesting++;
if(l.size() > 1)
linebreak(LinebreakOmit.NONE);
Iterator<?> it = l.iterator();
Object prev = null;
while(it.hasNext()) {
Object e = it.next();
if(prev != null) {
builder.append(", ");
boolean omit = (prev instanceof List || prev instanceof Map) && (e instanceof List || e instanceof Map);
linebreak(omit ? LinebreakOmit.TRY : LinebreakOmit.NONE);
}
prettyPrintInternal(e);
prev = e;
}
nesting--;
if(l.size() > 1)
linebreak(LinebreakOmit.NEXT);
builder.append("]");
return;
}
if(o instanceof String) {
builder.append('"');
builder.append(o);
builder.append('"');
return;
}
if(o instanceof Long || o instanceof Integer) {
builder.append(o);
return;
}
if(o instanceof ByteBuffer) {
ByteBuffer buf = ((ByteBuffer) o).slice();
byte[] bytes;
if(buf.hasArray() && buf.arrayOffset() == 0 && buf.capacity() == buf.limit())
bytes = buf.array();
else {
bytes = new byte[buf.remaining()];
buf.get(bytes);
}
o = bytes;
}
if(o instanceof byte[]) {
byte[] bytes = (byte[]) o;
if(bytes.length == 0) {
builder.append("\"\"");
return;
}
if(guessHuman) {
CharsetDecoder dec = StandardCharsets.UTF_8.newDecoder();
dec.onMalformedInput(CodingErrorAction.REPORT);
dec.onUnmappableCharacter(CodingErrorAction.REPORT);
try {
String asString = dec.decode(ByteBuffer.wrap(bytes)).toString();
if(asString.codePoints().noneMatch(i -> i < 32 && i != '\r' && i != '\n')) {
builder.append('"').append(asString).append('"');
return;
}
} catch(CharacterCodingException ignored) {
// do nothing
}
}
builder.append("0x");
Utils.toHex(bytes, builder, 20);
if(bytes.length > 20) {
builder.append('…');
builder.append('(');
builder.append(bytes.length);
builder.append(')');
}
if(bytes.length < 10) {
builder.append('/');
builder.append(Utils.stripToAscii(bytes));
}
return;
}
builder.append("unhandled type(").append(o).append(')');
}
}