package org.btcollider.cnc.keysrv;

import java.util.BitSet;

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) {
		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);
	}

	public static void init(int index, int depth) {
		instance = new KeyServer(index, depth);
	}

	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(keyRange.getStart());
	}

	public void setInWork(KeyRange keyRange) {
		markInWork(keyRange.getStart());
	}

	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 searched too, check next level
		parent.setInWork(Long.max(parent.getLeft().inWorkSince(), parent.getRight().inWorkSince()));

		recMarkSearchedParents(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() && !left.isSearched()) {
			keyStart.set(curIndex - 1, left.getValue());
			return;
		} else if (right.isLeaf() && !right.isSearched()) {
			keyStart.set(curIndex - 1, right.getValue());
			return;
		}

		// Recursive depth-first traversal
		if (!left.isSearched())
			recCollectKey(left, keyStart, curIndex - 1);
		else
			recCollectKey(right, keyStart, curIndex - 1);
	}
}
