package the8472.mldht;

import static the8472.bencode.Utils.str2buf;
import static the8472.utils.Functional.tap;
import static the8472.utils.Functional.unchecked;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicReference;

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.kad.messages.MessageBase.Method;
import lbms.plugins.mldht.kad.messages.MessageBase.Type;
import lbms.plugins.mldht.utils.NIOConnectionManager;
import lbms.plugins.mldht.utils.Selectable;
import the8472.utils.ConfigReader;
import the8472.utils.XMLUtils;
import the8472.utils.concurrent.SerializedTaskExecutor;

public class PassiveRedisIndexer implements Component {
	
	private Queue<ByteBuffer> writeQueue = new ConcurrentLinkedQueue<>();
	
	private volatile boolean running = true;
	
	ConfigReader config;
	
	AtomicReference<SocketHandler> ref = new AtomicReference<>();
	
	NIOConnectionManager conMan;
	
	public void start(Collection<DHT> dhts, ConfigReader config)  {
		this.config = config;
		
		conMan = new NIOConnectionManager("redis selector");
		
		dhts.forEach((dht) -> {
			dht.addIncomingMessageListener(this::incomingMessage);
		});
		
	}
	
	
	SocketHandler ensureOpen() {
		SocketHandler handler = ref.get();
		if(handler == null) {
			handler = new SocketHandler();
			if(ref.compareAndSet(null, handler)) {
				handler.open();
			}
		}
		
		return handler;
	}
	
	public void stop() {
		running = false;
	}
	
	private static final String TTL = Integer.toString(2*24*3600);
	
	private void incomingMessage(DHT dht, MessageBase msg) {
		if(!running)
			return;
		
		if(msg.getType() == Type.REQ_MSG && msg.getMethod() == Method.GET_PEERS)
		{
			GetPeersRequest req = (GetPeersRequest) msg;
			long now = System.currentTimeMillis();
			Key k =	req.getTarget();
			String ipAddr = req.getOrigin().getAddress().getHostAddress();
			String key = k.toString(false);
			
			StringBuilder b = new StringBuilder();
			
			
			
			// zadd <hash> <timestamp> <ip>
			b.append("*4\r\n");

			b.append("$4\r\n");
			b.append("ZADD\r\n");

			b.append("$40\r\n");
			b.append(key).append("\r\n");
			
			String intAsString = Long.toString(now);
			b.append('$').append(intAsString.length()).append("\r\n");
			b.append(intAsString).append("\r\n");

			b.append('$').append(ipAddr.length()).append("\r\n");
			b.append(ipAddr).append("\r\n");
			
			// expire <hash> <ttl>
			
			b.append("*3\r\n");
			
			b.append("$6\r\n");
			b.append("EXPIRE\r\n");

			b.append("$40\r\n");
			b.append(key).append("\r\n");
			
			b.append('$').append(TTL.length()).append("\r\n");
			b.append(TTL).append("\r\n");
			
			SocketHandler handler = ensureOpen();

			// to avoid OOM we only fill the queue when we have an open connection
			if(handler.getChannel().isConnected()) {
				writeQueue.add(str2buf(b.toString()));
				handler.tryWrite.run();
			}
			
						
		}
	}
	
	
	static private final Map<String,String> namespaces = tap(new HashMap<>(), m -> m.put("xsi","http://www.w3.org/2001/XMLSchema-instance"));
	
	private InetAddress getAddress() {
		return config.get(XMLUtils.buildXPath("//components/component[@xsi:type='mldht:redisIndexerType']/address",namespaces)).flatMap(unchecked(str -> Optional.of(InetAddress.getByName(str)))).get();
	}

	class SocketHandler implements Selectable {
		
		SocketChannel chan;
		
		void open() {
			try {
				chan = SocketChannel.open();
				chan.configureBlocking(false);
				chan.connect(new InetSocketAddress(getAddress(),6379));
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			
			conMan.register(this);
			
		}
		
		void close() {
			writeQueue.clear();
			ref.compareAndSet(this, null);
			try {
				chan.close();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			
		}
	
		@Override
		public SocketChannel getChannel() {
			return chan;
		}
	
		@Override
		public void registrationEvent(NIOConnectionManager manager, SelectionKey key) throws IOException {
			
			// TODO Auto-generated method stub
			
		}
		
		
	
		@Override
		public void selectionEvent(SelectionKey key) throws IOException {
			if(key.isValid() && key.isConnectable()) {
				chan.finishConnect();
				conMan.interestOpsChanged(this);
			}
				
			if(key.isValid() && key.isReadable())
				read();
			if(key.isValid() && key.isWritable()) {
				awaitingWriteNotification = false;
				tryWrite.run();
				conMan.interestOpsChanged(this);
			}
				
			
		}
		
		volatile boolean awaitingWriteNotification = true;
		ByteBuffer toWrite;
		
		Runnable tryWrite = SerializedTaskExecutor.onceMore(() -> {
			while(!awaitingWriteNotification && !writeQueue.isEmpty()) {
				if(toWrite == null)
					toWrite = writeQueue.poll();
				if(toWrite == null)
					continue;
				
				int written = 0;
				try {
					chan.write(toWrite);
				} catch (IOException e) {
					e.printStackTrace();
				}
				
				if(written < 0) {
					awaitingWriteNotification = true;
					close();
					continue;
				}
				
				if(toWrite.remaining() > 0) {
					awaitingWriteNotification = true;
					conMan.interestOpsChanged(this);
				} else {
					toWrite = null;
				}
			}
		});
		
		ByteBuffer oblivion = ByteBuffer.allocateDirect(4*1024);
		
		void read() throws IOException {
			while(true) {
				oblivion.clear();
				int read = chan.read(oblivion);
				if(read < 0)
					close();
				if(read <= 0)
					break;
			}
		}
	
		@Override
		public void doStateChecks(long now) throws IOException {
			if(!chan.isOpen()) {
				close();
				conMan.deRegister(this);
			}
				
		}
	
		@Override
		public int calcInterestOps() {
			int ops = SelectionKey.OP_READ;
			
			if(chan.isConnectionPending())
				ops |= SelectionKey.OP_CONNECT;
			
			if(awaitingWriteNotification)
				ops |= SelectionKey.OP_WRITE;
				
			return ops;
		}
		
	}

}

@@ -3242,16 +3242,48 @@
 hannel()
+ != null && handler.getChannel()
 .isConne

fix startup NPE