package org.btcollider.cnc.keysrv;

import java.util.BitSet;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Random;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import org.btcollider.cnc.CnC;
import org.btcollider.cnc.dto.KeyRange;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class KeyServer {
	final Logger log = LoggerFactory.getLogger(KeyServer.class);

	private static KeyServer instance;

	private KeyTree root;
	private int index;
	private int depth;

	protected KeyServer(int index, int depth, long maxWorkSpan) {
		assert Math.pow(2, CnC.MAX_BITS) <= Long.MAX_VALUE;
		assert index < CnC.MAX_BITS && depth <= CnC.MAX_BITS;

		this.depth = depth;
		this.index = index;
		this.root = generateKeyTree(depth);
		
		log.info("Starting KeyTree concierge");
		ScheduledExecutorService es = Executors.newSingleThreadScheduledExecutor();
		es.scheduleWithFixedDelay(new KTConcierge(root, maxWorkSpan), 0, 30, TimeUnit.SECONDS);
	}

	public static void init(int index, int depth, long maxWorkSpan) {
		if (instance == null)
			instance = new KeyServer(index, depth, maxWorkSpan);
		else
			throw new IllegalAccessError("KeyServer already initialized");
	}

	public static KeyServer getInstance() {
		if (instance == null)
			throw new IllegalAccessError("KeyServer is not initialized");
		return instance;
	}

	public KeyRange getRange() {
		BitSet keyStart = new BitSet(CnC.MAX_BITS);

		recCollectKey(root, keyStart, index);

		BitSet keyEnd = new BitSet(CnC.MAX_BITS);
		keyEnd.or(keyStart); // set keyEnd to keyStart
		keyEnd.flip(0, index + 1 - depth); // reverse last bits

		KeyRange kr = new KeyRange(keyStart, keyEnd);

		return kr;
	}

	public void setSearched(Long from, Long to) {
		assert from >= 0 && to >= 0 && from <= to;

		long step = (long) Math.pow(2, ((index + 1) - depth));

		for (long i = from; i < to; i += step) {
			BitSet searchedKey = BitSet.valueOf(new long[] { i });
			markSearched(searchedKey);
		}
	}

	public void setSearched(KeyRange keyRange) {
		markSearched(BitSet.valueOf(new long[] { keyRange.getStart() }));
	}

	public void setInWork(KeyRange keyRange) {
		markInWork(BitSet.valueOf(new long[] { keyRange.getStart() }));
	}

	public String printDot() {
		StringBuffer buffer = new StringBuffer();
		buffer.append("strict graph { \n");

		Queue<KeyTree> frontier = new LinkedList<>();
		frontier.add(root);

		while (!frontier.isEmpty()) {
			KeyTree node = frontier.poll();
			buffer.append(dotNodeName(node) + ";\n");

			if (node.getParent() != null) {
				buffer.append(dotNodeName(node.getParent()) + " -- " + dotNodeName(node) + ";\n");
			}

			if (node.getLeft() != null)
				frontier.add(node.getLeft());
			if (node.getRight() != null)
				frontier.add(node.getRight());
		}

		buffer.append("\n}");
		return buffer.toString();
	}

	private String dotNodeName(KeyTree node) {
		String nodeName = "\"" + node.hashCode() + "_" + node.getValue();

		if (node.isSearched())
			nodeName += "_s";
		if (node.inWork())
			nodeName += "_w";

		nodeName += "\"";

		return nodeName;
	}

	private void markInWork(BitSet key) {
		KeyTree keyTreePointer = root;

		int curIdx = key.length() - 2; // ignore root bit
		while (curIdx >= 0) {
			if (key.get(curIdx))
				keyTreePointer = keyTreePointer.getRight();
			else
				keyTreePointer = keyTreePointer.getLeft();

			if (keyTreePointer.inWork()) {
				break; // path already marked as inWork
			}

			if (keyTreePointer.isLeaf()) { // end of tree reached
				keyTreePointer.setInWork();
				recMarkInWorkParents(keyTreePointer);
				break;
			}

			curIdx--;
		}
	}

	private void recMarkInWorkParents(KeyTree child) {
		if (child.getParent() == null)
			return;

		KeyTree parent = child.getParent();

		if (!parent.getLeft().inWork() || !parent.getRight().inWork()) {
			return;
		}

		// Mark parent inWork too, check next level
		parent.setInWork(Long.max(parent.getLeft().inWorkSince(), parent.getRight().inWorkSince()));

		recMarkInWorkParents(parent);
	}

	private void markSearched(BitSet key) {
		KeyTree keyTreePointer = root;

		int curIdx = key.length() - 2; // ignore root bit
		while (curIdx >= 0) {
			if (key.get(curIdx))
				keyTreePointer = keyTreePointer.getRight();
			else
				keyTreePointer = keyTreePointer.getLeft();

			if (keyTreePointer.isSearched())
				break; // path already marked as searched

			if (keyTreePointer.isLeaf()) { // end of tree reached
				keyTreePointer.setSearched(true);
				recMarkSearchedParents(keyTreePointer);
				break;
			}

			curIdx--;
		}
	}

	private void recMarkSearchedParents(KeyTree child) {
		if (child.getParent() == null)
			return;

		KeyTree parent = child.getParent();

		if (!parent.getLeft().isSearched() || !parent.getRight().isSearched()) {
			return;
		}

		// Mark parent searched too, check next level
		parent.setSearched(true);
		recMarkSearchedParents(parent);
	}

	private KeyTree generateKeyTree(int depth) {
		assert depth > 0;

		KeyTree root = new KeyTree(true, null);
		recGenKeyTree(root, depth - 1);

		return root;
	}

	private void recGenKeyTree(KeyTree kt, int depth) {
		assert depth >= 0;

		if (depth == 0)
			return;

		KeyTree left = new KeyTree(false, kt);
		KeyTree right = new KeyTree(true, kt);

		kt.setLeft(left);
		kt.setRight(right);

		recGenKeyTree(left, depth - 1);
		recGenKeyTree(right, depth - 1);
	}

	private void recCollectKey(KeyTree kt, BitSet keyStart, int curIndex) {
		assert curIndex > 0;

		keyStart.set(curIndex, kt.getValue());

		KeyTree left = kt.getLeft();
		KeyTree right = kt.getRight();

		// Finish criteria
		if (left.isLeaf() || right.isLeaf()) {
			assert left.isLeaf() && right.isLeaf(); // fully balanced binary tree by construction

			// No choice finish
			if (!right.isFree())
				keyStart.set(curIndex - 1, left.getValue());
			else if (!left.isFree())
				keyStart.set(curIndex - 1, right.getValue());
			else if (left.isLeaf() && right.isLeaf()) { // Randomized finish
				Random r = new Random();
				boolean leftFirst = r.nextBoolean();
				if(leftFirst) keyStart.set(curIndex - 1, left.getValue());
				else keyStart.set(curIndex - 1, right.getValue());
			}
		
			return;
		}

		// No choice depth-first traversal
		if (!left.isFree())
			recCollectKey(right, keyStart, curIndex - 1);
		else if (!right.isFree())
			recCollectKey(left, keyStart, curIndex - 1);
		else { // Randomized depth-first traversal
			Random r = new Random();
			boolean leftFirst = r.nextBoolean();
			if (leftFirst)
				recCollectKey(left, keyStart, curIndex - 1);
			else
				recCollectKey(right, keyStart, curIndex - 1);
		}
	}

	protected KeyTree getKeyTree() {
		return this.root;
	}
}
