module tools.Set;

import std.string;
import std.traits;

/++
 + Template that determines if a function exists with the signature:
 + char[] toString(T);
 + 
 + Params:  T = The type to test.
 + Returns: A bool specifying whether or not the toString(T) function exists.
 +/
private template hasToStringMember(T) {
	const bool hasToStringMember = is(typeof(&T.toString) == char[] function());
}

/++
 + Template that determines if T has a member-function with the signature:
 + char[] toString();
 + 
 + Params:  T = The class or struct to test.
 + Returns: A bool specifying whether or not the T.toString() function exists.
 +/
private template toStringFunctionExists(T) {
	const bool toStringFunctionExists = is(typeof(.toString(cast(T)T.init)));
}

/++
 + Template that checks if a type is a static array type, and if so, returns
 + its dynamic array equivalent.
 + 
 + Params:  T = The type to check.
 + Returns: If the type was not a static array: T itself.
 +          If the type was a static array: the dynamic array equivalent of T.
 +/
private template nonStaticArrayType(T) {
	static if (isStaticArray!(T)) {
		alias typeof(T[0])[] nonStaticArrayType;
	} else {
		alias T nonStaticArrayType;
	}
}

/++
 + Returns the minimum of two values.
 + 
 + Params:  value1 = The first value to compare.
 +          value2 = The second value to compare.
 + Returns: The one with the lowest value, or value1 if both are equal.
 +/
private T min(T)(T value1, T value2) {
	if (value2 < value1) return value2;
	return value1;
}
unittest {
	assert(min(1, 2) == 1);
	assert(min(10, -20) == -20);
	assert(min(42, 42) == 42);
	
	assert(min(1.5, 2.4) == 1.5);
	assert(min(10.3, -20.9) == -20.9);
	assert(min(5.44, 5.44) == 5.44);
}

/++
 + Implementation of a set. The rules:
 + 
 + * Can hold any type that can be used as the key-type of an
 +   associative array.
 + 
 + * Can hold only one of each value.
 + 
 + * Holds no information associated with a value, except the
 +   value itself.
 + 
 + * The elements in a set are not in any order.
 + 
 + * If the type is a static array, the set will actually hold
 +   its dynamic array equivalent.
 + 
 + * Can be used as the key-type of an associative array.
 +   So it is also possible to have a set of sets.
 + 
 + Authors: Michiel Helvensteijn
 + 
 + Bugs: Was not designed for efficiency. If you want to do many set
 +       operations in a loop, this may not be the right class (yet).
 +/
class Set(T)  {
	private:
		
		alias nonStaticArrayType!(T) Type;
		
		static if (isStaticArray!(T)) {
			Type duplicate(Type element) {
				return element.dup;
			}
		} else {
			Type duplicate(T element) {
				return element;
			}
		}
		
	public:
		
		/++
		 + The default constructor that creates an empty set.
		 +/
		this() {
		} // intentionally empty
		
		/++
		 + The copy constructor that creates a copy of 'other'.
		 + 
		 + Params: other = The other set to copy.
		 +/
		this(Set!(T) other) {
			foreach (element ; other) {
				add(element);
			}
		}
		
		/++
		 + Add a new element to the set.
		 + 
		 + Params: element = The new element to add.
		 +/
		void add(Type element) {
			_elements[duplicate(element)] = true;
		}
		unittest {
			auto x = set(1,2,3);
			
			assert(!(4 in x));
			assert(x.size() == 3);
			
			x.add(4);
			
			assert(4 in x);
			assert(x.size() == 4);
			
			x.add(4);
			
			assert(x.size() == 4);
		}
		
		/++
		 + Remove an element from the set.
		 + 
		 + Params: element = The element to remove.
		 +/
		void remove(Type element) {
			_elements.remove(duplicate(element));
		}
		unittest {
			auto x = set(1,2,3,4,5,6,7);
			
			assert(4 in x);
			assert(x.size() == 7);
			
			x.remove(4);
			
			assert(!(4 in x));
			assert(x.size() == 6);
			
			x.remove(4);
			
			assert(x.size() == 6);
		}
		
		/++
		 + Get the number of elements in the set.
		 + 
		 + Returns: The number of elements in the set.
		 +/
		uint size() {
			return _elements.length;
		}
		unittest {
			auto x = set(1,2,3,4,5,6,7);
			
			assert(x.size() == 7);
			
			auto y = new Set!(char[]);
			
			assert(y.size() == 0);
		}
		
		/++
		 + Get an array filled with the elements of the set.
		 + 
		 + Returns: An array filled with the elements of the set in
		 +          undefined order.
		 +/
		Type[] getArray() {
			return _elements.keys;
		}
		unittest {
			auto x = set("one", "two", "three", "fortytwo");
			auto y = x.getArray();
			
			foreach (e ; y) {
				assert(e in x);
			}
		}
		
		/++
		 + Get a textual representation of the content of the set.
		 + For this function to work, the content type (T) of the
		 + set must either be a string or must also have a toString
		 + function, in one of these forms:
		 + * char[] T.toString();
		 + * char[] .toString(T);
		 + 
		 + Returns: A textual representation of the content of the set.
		 +/
		char[] toString() {
			char[] list;
			
			for (uint i = 0; i < _elements.length; ++i) {
			
				static if (is(Type == char[])) {
					list ~= _elements.keys[i];
				} else static if (hasToStringMember!(T)) {
					list ~= _elements.keys[i].toString();
				} else static if(toStringFunctionExists!(T)) {
					list ~= .toString(_elements.keys[i]);
				} else {
					assert(0, "The type " ~ T.stringof ~ " has no toString function defined.");
				}
				
				if (i < _elements.length - 1) {
					list ~= ", ";
				}
			}
			
			return "{" ~ list ~ "}";
		}
		
		/++
		 + Get the hash-value of the set. This function makes it
		 + possible for the set to be the key of an associative array.
		 + 
		 + Returns: The hash-value of the set.
		 +/
		hash_t toHash() {
			hash_t result = 0;
			uint count = 1;
			
			foreach (element ; this) {
				result = (result + typeid(Type).getHash(&element) * count++) % hash_t.max;
			}
			
			return result;
		}
		
		/++
		 + Compare two sets for equality.
		 + 
		 + Params: other = The other set to test against.
		 + Returns: 1 if the two sets are equal. 0 if they are not.
		 +/
		int opEquals(Object other) {
			auto o = cast(Set!(T)) other;
			
			if (!o) return false;
			
			if (_elements.length != o._elements.length) return false;
			
			foreach (element ; o)
				if (!(element in this)) return false;
			
			return true;
		}
		unittest {
			assert(set(1, 2, 3) == set(3, 1, 2));
			assert(set(5, 6, 7) == set(5, 5, 7, 6, 5, 7, 6));
			assert(set(5, 6, 7) != set(5, 6, 8));
		}
		
		/++
		 + Compare two sets for inequality. This function only exists
		 + so that sets can be keys for associative arrays. One set can
		 + not mathematically be less or greater than another set.
		 + 
		 + Params: other = The other set to test against.
		 + Returns:  0 if this = other
		 +          -1 if this < other
		 +           1 if this > other
		 +           The inequality operators return booleans.
		 +/
		int opCmp(Object other) {
			auto o = cast(Set!(T)) other;
			
			if (!o) return -1;
			
			auto thisList = _elements.keys;
			auto otherList = o._elements.keys;
			
			thisList.sort;
			otherList.sort;
			
			for (uint i = 0; i < min(thisList.length, otherList.length); ++i) {
				if (thisList[i] < otherList[i]) return -1;
				if (thisList[i] > otherList[i]) return 1;
			}
			
			if (thisList.length < otherList.length) return -1;
			if (thisList.length > otherList.length) return 1;
			
			return 0;
		}
		
		/++
		 + Check if an element is a member of the set.
		 + 
		 + Params: element = The element to check for.
		 + Returns: true, if the element is in the set
		 +          false, if the element is not in the set
		 +/
		bool opIn_r(Type element) {
			return cast(bool)(duplicate(element) in _elements);
		}
		unittest {
			assert(1 in set(1, 2, 3));
			assert("foo" in set("bleh", "foo", "bar"));
			assert(!(5 in set(1, 2, 3, 4, 6, 7)));
		}
		
		/++
		 + Get the intersection of two sets. An element will be in the
		 + result if and only if it is in both source sets.
		 + 
		 + Params: other = The other set to intersect with this one.
		 + Returns: The intersection of this set with the other.
		 +/
		Set!(T) opAnd(Set!(T) other) {
			auto result = new Set!(T);
			
			foreach (element ; this) {
				if (element in other) {
					result.add(element);
				}
			}
			
			return result;
		}
		unittest {
			auto x = set(1, 2, 3, 4);
			auto y = set(3, 4, 5, 6);
			
			assert((x & y) == set(3, 4));
			assert((y & x) == set(3, 4));
		}
		
		/++
		 + Intersect this set with another set. This set will become the
		 + intersection result. An element will be in the result if and
		 + only if it is in both source sets.
		 + 
		 + Params: other = The other set to intersect with this one.
		 +/
		void opAndAssign(Set!(T) other) {
			foreach (element ; this) {
				if (!(element in other)) {
					remove(element);
				}
			}
		}
		unittest {
			auto x = set(1, 2, 3, 4);
			auto y = set(3, 4, 5, 6);
			
			assert(x == set(1, 2, 3, 4));
			
			x &= y;
			
			assert(x == set(3, 4));
		}
		
		/++
		 + Get the union of two sets. An element will be in the
		 + result if and only if it is in either or both source sets.
		 + 
		 + Params: other = The other set to unite with this one.
		 + Returns: The union of this set with the other.
		 +/
		Set!(T) opOr(Set!(T) other) {
			auto result = new Set!(T)(this);
			
			foreach (element ; other) {
				result.add(element);
			}
			
			return result;
		}
		unittest {
			auto x = set(1, 2, 3, 4);
			auto y = set(3, 4, 5, 6);
			
			assert((x | y) == set(1, 2, 3, 4, 5, 6));
			assert((y | x) == set(1, 2, 3, 4, 5, 6));
		}
		
		/++
		 + Unite this set with another set. This set will become the union
		 + result. An element will be in the result if and only if it is in
		 + either or both source sets.
		 + 
		 + Params: other = The other set to unite with this one.
		 +/
		void opOrAssign(Set!(T) other) {
			foreach (element ; other) {
				add(element);
			}
		}
		unittest {
			auto x = set(1, 2, 3, 4);
			auto y = set(3, 4, 5, 6);
			
			assert(x == set(1, 2, 3, 4));
			
			x |= y;
			
			assert(x == set(1, 2, 3, 4, 5, 6));
		}
		
		/++
		 + Get the symmetric difference of two sets. An element will be in the
		 + result if and only if it is in one, but not both, source sets.
		 + 
		 + Params: other = The other set to xor with this one.
		 + Returns: The symmetric difference of this set with the other.
		 +/
		Set!(T) opXor(Set!(T) other) {
			auto result = new Set!(T)(this);
			
			foreach (element ; other) {
				if (element in this) {
					result.remove(element);
				} else {
					result.add(element);
				}
			}
			
			return result;
		}
		unittest {
			auto x = set(1, 2, 3, 4);
			auto y = set(3, 4, 5, 6);
			
			assert((x ^ y) == set(1, 2, 5, 6));
			assert((y ^ x) == set(1, 2, 5, 6));
		}
		
		/++
		 + Xor this set with another set. This set will become the symmetric
		 + difference result. An element will be in the result if and only if
		 + it is in one, but not both, source sets.
		 + 
		 + Params: other = The other set to unite with this one.
		 +/
		void opXorAssign(Set!(T) other) {
			foreach (element ; other) {
				if (element in this) {
					remove(element);
				} else {
					add(element);
				}
			}
		}
		unittest {
			auto x = set(1, 2, 3, 4);
			auto y = set(3, 4, 5, 6);
			
			assert(x == set(1, 2, 3, 4));
			
			x ^= y;
			
			assert(x == set(1, 2, 5, 6));
		}
		
		/++
		 + Get the relative complement of another set in this one. An element
		 + will be in the result if and only if it is in this set, but not in
		 + the other.
		 + 
		 + Params: other = The other set to complement in this one.
		 + Returns: The relative complement of the other set in this one.
		 +/
		Set!(T) opSub(Set!(T) other) {
			auto result = new Set!(T)(this);
			
			foreach (element ; other) {
				result.remove(element);
			}
			
			return result;
		}
		unittest {
			auto x = set(1, 2, 3, 4);
			auto y = set(3, 4, 5, 6);
			
			assert((x - y) == set(1, 2));
			assert((y - x) == set(5, 6));
		}
		
		/++
		 + Complement another set in this one. This set will become the
		 + relative complement result. An element will be in the result if
		 + and only if it is in this set, but not in the other.
		 + 
		 + Params: other = The other set to complement in this one.
		 +/
		void opSubAssign(Set!(T) other) {
			foreach (element ; other) {
				remove(element);
			}
		}
		unittest {
			auto x = set(1, 2, 3, 4);
			auto y = set(3, 4, 5, 6);
			
			assert(x == set(1, 2, 3, 4));
			
			x -= y;
			
			assert(x == set(1, 2));
		}
		
		/++
		 + The foreach loop on this set. It will visit all elements in the
		 + set in an undefined order.
		 + 
		 + Example:
		 + ------------------------------
		 + auto x = new Set!(int);
		 + x.add(5); x.add(6); x.add(7);
		 + 
		 + foreach (e ; x) {
		 +     assert(e in x); 
		 + }
		 + ------------------------------
		 +/
		int opApply(int delegate(inout Type) dg) {
			int result = 0;
			
			foreach (element; _elements.keys) {
				result = dg(element);
				if (result) break;
			}
			
			return result;
		}
		unittest {
			auto x = set(1, 2, 3, 4, 5, 6);
			
			foreach (e ; x) {
				assert(e in x);
			}
		}
		
		int opApplyReverse(int delegate(inout Type) dg) {
			assert(0, "foreach_reverse has no meaning for a set. Use foreach instead.");
		}
		
	private:
		
		bool[Type] _elements;
}

/++
 + A sort of set literal.
 + 
 + Params:  elements = The elements of the set literal. Must all be of the
 +                     same type and must at least include one value.
 + Returns: The set with the elements specified.
 + 
 + Example:
 + ------------------------------
 + auto x = set(1, 2, 3);
 + ------------------------------
 +/
Set!(T[0]) set(T ...)(T elements) {
	auto result = new Set!(T[0]);
	
	foreach (element; elements) {
		result.add(element);
	}
	
	return result;
}
unittest {
	auto x = set(1, 2, 3);
	
	static assert( is(typeof(x) == Set!(int)) );
	assert(x.size() == 3);
	assert(1 in x);
	assert(2 in x);
	assert(3 in x);
}

