package the8472.bencode;

import static the8472.bencode.Utils.str2buf;

import java.nio.ByteBuffer;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.SortedMap;
import java.util.stream.Stream;

public class BEncoder {
	
	private ByteBuffer buf;
	
	public static class RawData {
		ByteBuffer rawBuf;
		
		public RawData(ByteBuffer b) {
			rawBuf = b;
		}
	}
	
	public static interface StringWriter {
		int length();
		
		void writeTo(ByteBuffer buf);
	}
	
	public ByteBuffer encode(Map<String, Object> toEnc, int maxSize) {
		buf = ByteBuffer.allocate(maxSize);
		encodeMap(toEnc);
		buf.flip();
		return buf;
	}
	
	public void encodeInto(Map<String, Object> toEnc, ByteBuffer target) {
		buf = target;
		encodeMap(toEnc);
		buf.flip();
	}
	
	public ByteBuffer encode(Object toEnc, int maxSize) {
		buf = ByteBuffer.allocate(maxSize);
		encodeInternal(toEnc);
		buf.flip();
		return buf;
	}
	
	private void encodeInternal(Object o) {
		if(o instanceof Map) {
			encodeMap((Map<String, Object>) o);
			return;
		}
		
		if(o instanceof List) {
			encodeList((List<Object>) o);
			return;
		}
		
		
		if(o instanceof String) {
			encodeString((String) o);
			return;
		}

		if(o instanceof byte[]) {
			byte[] b = (byte[]) o;
			encodeInt(b.length, (byte) ':');
			buf.put(b);
			return;
		}
		
		if(o instanceof ByteBuffer) {
			ByteBuffer clone = ((ByteBuffer) o).slice();
			encodeInt(clone.remaining(), (byte) ':');
			buf.put(clone);
			return;
		}
		
		if(o instanceof Integer) {
			buf.put((byte) 'i');
			encodeInt(((Integer) o).intValue(),(byte) 'e');
			return;
		}
		
		if(o instanceof Long) {
			buf.put((byte) 'i');
			encodeLong(((Long) o).longValue(), 'e');
			return;
		}
		
		if(o instanceof RawData) {
			buf.put(((RawData) o).rawBuf);
			return;
		}
		
		if(o instanceof StringWriter) {
			StringWriter w = (StringWriter) o;
			encodeInt(w.length(), (byte)':');
			w.writeTo(buf);
			return;
		}
		
		throw new RuntimeException("unknown object to encode " + o);
	}
	
	private void encodeList(List<Object> l) {
		buf.put((byte) 'l');
		l.forEach(this::encodeInternal);
		buf.put((byte) 'e');
	}
	
	private void encodeString(String str) {
		encodeInt(str.length(), (byte) ':');
		str2buf(str, buf);
	}
	
	private void encodeMap(Map<String, Object> map) {
		buf.put((byte) 'd');
		Stream<Entry<String,Object>> str;
		if(map instanceof SortedMap<?, ?> && ((SortedMap<?, ?>) map).comparator() == null)
			str = map.entrySet().stream();
		else
			str = map.entrySet().stream().sorted(Map.Entry.comparingByKey());
		
		str.forEachOrdered(e -> {
			encodeString(e.getKey());
			encodeInternal(e.getValue());
		});
		buf.put((byte) 'e');
	}
	
	private final static byte[] MIN_INT = str2buf(Integer.toString(Integer.MIN_VALUE)).array();
	
	private void encodeInt(int val, byte terminator) {
		if(val == Integer.MIN_VALUE)
			buf.put(MIN_INT);
		else {
			if(val < 0) {
				buf.put((byte) '-');
				val = -val;
			}
			
			int numChars = 10;
			int probe = 10;
	        for (int i=1; i<10; i++) {
	            if (val < probe) {
	            	numChars = i;
	            	break;
	            }
	            probe = 10*probe;
	        }
	        
	        int pos = buf.position() + numChars;
	        
	        buf.position(pos);
	        
	        for(int i=1; i <= numChars; i++) {
	        	int reduced = val / 10;
	        	int remainder = val - (reduced * 10);
	        	buf.put(pos - i, (byte) ('0' + remainder));
	        	val = reduced;
	        }
	        
		}
		buf.put(terminator);
	}
	
	private void encodeLong(long val, char terminator) {
		str2buf(Long.toString(val), buf);
		buf.put((byte) terminator);
	}
}

@@ -1752,24 +1752,33 @@
 a) %7B%0A%09%09%09
-buf.put(
+ByteBuffer raw = 
 ((RawDat
@@ -1789,16 +1789,44 @@
 ).rawBuf
+;%0A%09%09%09buf.put(raw.duplicate()
 );%0A%09%09%09re

fix accidental source bytebuffer modification when bencoding