package the8472.mldht;

import static the8472.bencode.Utils.str2buf;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReferenceArray;

import lbms.plugins.mldht.kad.DHT;
import lbms.plugins.mldht.kad.Key;
import lbms.plugins.mldht.kad.messages.GetPeersRequest;
import lbms.plugins.mldht.kad.messages.MessageBase;
import lbms.plugins.mldht.utils.NIOConnectionManager;
import lbms.plugins.mldht.utils.Selectable;
import the8472.utils.ConfigReader;

public class Firehose implements Component {
	
	List<Connection> connections = new CopyOnWriteArrayList<>();
	
	@Override
	public void start(Collection<DHT> dhts, ConfigReader config) {
		selector = new NIOConnectionManager("firehose");
		dhts.forEach(d -> {
			d.addIncomingMessageListener(this::incomingMessage);
		});
		
		try {
			selector.register(new Server());
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	NIOConnectionManager selector;
	
	class Server implements Selectable {
		
		public Server() throws IOException {
			chan = ServerSocketChannel.open();
			chan.configureBlocking(false);
			// listen on [::0]:35465
			chan.bind(new InetSocketAddress(InetAddress.getByAddress(new byte[16]), 35465));
		}
		
		ServerSocketChannel chan ;

		@Override
		public SelectableChannel getChannel() {
			return chan;
		}

		@Override
		public void registrationEvent(NIOConnectionManager manager, SelectionKey key) throws IOException {}

		@Override
		public void selectionEvent(SelectionKey key) throws IOException {
			SocketChannel connection;
			while((connection = chan.accept()) != null) {
				new Connection(connection);
			}
		}

		@Override
		public void doStateChecks(long now) throws IOException {}

		@Override
		public int calcInterestOps() {
			return SelectionKey.OP_ACCEPT;
		}
		
	};
	
	class Connection implements Selectable {
		
		final SocketChannel chan;
		
		AtomicInteger writePointer = new AtomicInteger();
		AtomicInteger readPointer = new AtomicInteger();
		AtomicReferenceArray<ByteBuffer> toWrite = new AtomicReferenceArray<>(1024);
		volatile boolean empty = true;
		
		
		void add(ByteBuffer buf) {
			int current = writePointer.getAndIncrement();
			toWrite.set(current % toWrite.length(), buf);
			if(empty) {
				readPointer.set(current);
				empty = false;
				selector.interestOpsChanged(this);
			}
		}
		
		ByteBuffer poll() {
			if(currentBuf != null && currentBuf.remaining() > 0)
				return currentBuf;
			int current = readPointer.getAndIncrement();
			currentBuf = toWrite.getAndSet(current % toWrite.length(), null);
			if(currentBuf == null) {
				empty = true;
				selector.interestOpsChanged(this);
			}
			return currentBuf;
		}
		
		public Connection(SocketChannel chan) throws IOException {
			this.chan = chan;
			chan.configureBlocking(false);
			selector.register(this);
			connections.add(this);
		}

		@Override
		public SelectableChannel getChannel() {
			return chan;
		}

		@Override
		public void registrationEvent(NIOConnectionManager manager, SelectionKey key) throws IOException {}

		@Override
		public void selectionEvent(SelectionKey key) throws IOException {
			if(key.isValid() && key.isReadable())
				read();
			if(key.isValid() && key.isWritable())
				write();
		}
		
		ByteBuffer readBuf = ByteBuffer.allocateDirect(4096);
		
		void read() throws IOException {
			try {
				while(true) {
					int read = chan.read(readBuf);
					readBuf.rewind();
					if(read < 0)
						chan.close();
					if(read == 0)
						break;
				}
			} catch(ClosedChannelException ex) {
				chan.close();
			}
		}
		
		ByteBuffer currentBuf;
		
		void write() throws IOException {
			while(true) {
				ByteBuffer buf = poll();
				if(buf == null)
					break;
				if(chan.write(buf) == 0)
					break;
			}
		}

		@Override
		public void doStateChecks(long now) throws IOException {
			if(!chan.isOpen())
				connections.remove(this);
		}

		@Override
		public int calcInterestOps() {
			int ops = SelectionKey.OP_READ;
			if(!empty)
				ops |= SelectionKey.OP_WRITE;
			return ops;
		}
		
	}
	
	void incomingMessage(DHT dht, MessageBase msg) {
		if(msg.getType() != MessageBase.Type.REQ_MSG || msg.getMethod() != MessageBase.Method.GET_PEERS)
			return;
		
		GetPeersRequest req = (GetPeersRequest) msg;
		
		StringBuilder b = new StringBuilder();
		Key k = req.getInfoHash();
		String addr = req.getOrigin().getAddress().getHostAddress();
		long now = System.currentTimeMillis();
		
		b.append(now).append('\t').append(k.toString(false)).append('\t').append(addr).append('\n');
		
		ByteBuffer buf = str2buf(b.toString());
		
		connections.forEach(c -> {
			c.add(buf.duplicate());
		});
	}
	
	
	@Override
	public void stop() {
		connections.forEach(c -> {
			try {
				c.chan.close();
			} catch (Exception e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		});

	}

}

@@ -2451,32 +2451,63 @@
 tomicInteger();%0A
+%09%09// length must be power of 2%0A
 %09%09AtomicReferenc
@@ -2701,34 +2701,35 @@
 ite.set(current 
-%25 
+& (
 toWrite.length()
@@ -2728,16 +2728,21 @@
 length()
+ - 1)
 , buf);%0A
@@ -3051,10 +3051,11 @@
 ent 
-%25 
+& (
 toWr
@@ -3066,16 +3066,21 @@
 length()
+ - 1)
 , null);

handle int overflow