#ifndef BINNING_H
#define BINNING_H

#include <vector>
#include <unordered_set>

struct BinningRange {
	float min;
	float max;
	BinningRange(float min, float max) : min(min), max(max) {;}
	float getWidth() const {return max-min;}
};

template <typename Binable> class Binning {

private:

	/** size to use for each dimension's bin */
	std::vector<float> binSizes;

	std::vector<BinningRange> binRanges;

	std::unordered_set<uint64_t> used;

public:

	/** add a new dimension for binning with its corresponding size */
	void setBinSizes(const std::vector<float> binSizes) {
		this->binSizes = binSizes;
	}

	/** manually set min/max range for each dimension */
	void setRanges(const std::vector<BinningRange> ranges) {
		this->binRanges = ranges;
	}

	/** estimate each dimension's min/max from the given entry set */
	void setRanges(const std::vector<Binable>& entries) {

		clearUsed();
		binRanges.clear();

		// process each to-be-binned dimension
		const int numDimensions = binSizes.size();
		for (int dim = 0; dim < numDimensions; ++dim) {

			// determin min and max value for the current dimension
			BinningRange r(+1e30, -1e30);
			for (const Binable& b : entries) {
				const float val = b.getBinValue(dim);
				if(val < r.min) {r.min = val;}
				if(val > r.max) {r.max = val;}
			}

			// remember
			binRanges.push_back(r);

		}

	}

	/** remove all tracked usages */
	void clearUsed() {
		used.clear();
	}

	/** does the given element relate to an used or unsed bin? */
	bool isFree(const Binable& b) const {
		const uint64_t hash = getHash(b);
		return used.find(hash) == used.end();
	}

	/** mark the bin the given element belongs to as used */
	void markUsed(const Binable& b) {
		const uint64_t hash = getHash(b);
		used.insert(hash);
	}

private:

	/** calculate unique-bin-hash for the given element */
	uint64_t getHash(const Binable& b) const {

		uint64_t hash = 0;

		const int numDimensions = binSizes.size();
		for (int dim = 0; dim < numDimensions; ++dim) {

			// get binable's value for the current dimension
			const float val = b.getBinValue(dim);

			// snap value to bin-number
			const int binNr = std::round((val-binRanges[dim].min) / binSizes[dim]);

			// maximum binNr
			const int binNrMax = binRanges[dim].getWidth() / binSizes[dim];

			// sanity check
			if (binNr < 0 || binNr > 255) {throw "bin index out of range!!";}

			// update hash
			hash *= (binNrMax+1);
			hash += binNr;

		}

		// done
		return hash;

	}

};

#endif // BINNING_H
