module simple;

// Importing stuff.
{
	function loadMod(name, ns)
	{
		assert(name == "mod");
		
		ns.x = "I'm x";
	
		ns.foo = function foo()
		{
			writefln("foo");
		};
	
		ns.bar = function bar(x)
		{
			return x[0];
		};
	
		ns.baz = function baz()
		{
			writefln(x);
		};
		
		foreach(k, v; ns)
			if(isFunction(v))
				v.environment(ns);
	}
	
	setModuleLoader("mod", loadMod);
	
	import mod : foo, bar;
	foo();
	writefln(bar([5]));
	mod.baz();

	writefln();
}

// Super calls.
{
	class Base
	{
		function fork()
		{
			writefln("Base fork.");
		}
	}

	class Derived : Base
	{
		function fork()
		{
			writefln("Derived fork!");
			super.fork();
		}
	}
	
	local d = Derived();
	d.fork();
	
	writefln();
}

// Coroutines and coroutine iteration.
{
	local countDown = coroutine function countDown(x)
	{
		yield();
		
		while(x > 0)
		{
			yield(x);
			x--;
		}
	};
	
	foreach(v; countDown, 5)
		writefln(v);
		
	writefln();
	
	local forEach = coroutine function forEach(t)
	{
		yield();
	
		foreach(k, v; t)
			yield(k, v);
	};
	
	foreach(_, k, v; forEach, {hi = 1, bye = 2})
		writefln("key: ", k, ", value: ", v);
	
	writefln();
}

// Testing tailcalls.
{
	function recurse(x)
	{
		writefln("recurse: ", x);
	
		if(x == 0)
			return toString(x);
		else
			return recurse(x - 1);
	}
	
	writefln(recurse(5));
	writefln();
	
	class A
	{
		function f(x)
		{
			writefln("A.f: ", x);

			if(x == 0)
				return toString(x);
			else
				return this.f(x - 1); // call it as this.f to force a 'method' instruction to be generated
		}
	}
	
	local a = A();
	writefln(a.f(5));
	writefln();
}

{
	// A function which lets us define properties for a class.
	// The varargs should be a bunch of tables, each with a 'name' field, and 'getter' and/or 'setter' fields.
	function mixinProperties(classType, vararg)
	{
		classType.mProps = { };
	
		classType.opIndex = function opIndex(key)
		{
			local prop = mProps[key];
	
			if(prop is null)
				throw format(classType, ".opIndex() - Property '%s' does not exist", key);
	
			local getter = prop.getter;
	
			if(getter is null)
				throw format(classType, ".opIndex() - Property '%s' has no getter", key);
	
			return getter(with this);
		};
	
		classType.opIndexAssign = function opIndexAssign(key, value)
		{
			local prop = mProps[key];
	
			if(prop is null)
				throw format(classType, ".opIndexAssign() - Property '%s' does not exist", key);
	
			local setter = prop.setter;
	
			if(setter is null)
				throw format(classType, ".opIndexAssign() - Property '%s' has no setter", key);
	
			setter(with this, value);
		};
	
		foreach(i, prop; [vararg])
		{
			if(!isTable(prop))
				throw format("mixinProperties() - property ", i, " is not a table");
	
			if(prop.name is null)
				throw format("mixinProperties() - property ", i, " has no name");
	
			if(prop.setter is null && prop.getter is null)
				throw format("mixinProperties() - property '%s' has no getter or setter", prop.name);
	
			classType.mProps[prop.name] = prop;
		}
	}
	
	// Create a class to test out.
	class PropTest
	{
		mX = 0;
		mY = 0;
		mName = "";
		
		function constructor(name)
		{
			mName = name;
		}
	
		function toString()
		{
			return format("name = '", mName, "' x = ", mX, " y = ", mY);
		}
	}
	
	// Mix in the properties.
	mixinProperties
	(
		PropTest,
	
		{
			name = "x",
			
			function setter(value)
			{
				mX = value;
			}
	
			function getter()
			{
				return mX;
			}
		},
	
		{
			name = "y",
	
			function setter(value)
			{
				mY = value;
			}
	
			function getter()
			{
				return mY;
			}
		},
	
		{
			name = "name",
	
			function getter()
			{
				return mName;
			}
		}
	);
	
	// Create an instance and try it out.
	local p = PropTest("hello");
	
	writefln(p);
	p.x = 46;
	p.y = 123;
	p.x = p.x + p.y;
	writefln(p);
	
	// Try to access a nonexistent property.
	try
		p.name = "crap";
	catch(e)
	{
		writefln("caught: ", e);
		writefln(getTraceback());
	}
	
	writefln();
}

// Some container classes.
{
	class PQ
	{
		mData;
		mLength = 0;
	
		function constructor()
		{
			mData = array.new(15);
		}
	
		function insert(data)
		{
			resizeArray();
			mData[mLength] = data;
	
			local index = mLength;
			local parentIndex = (index - 1) / 2;
	
			while(index > 0 && mData[parentIndex] > mData[index])
			{
				local temp = mData[parentIndex];
				mData[parentIndex] = mData[index];
				mData[index] = temp;
	
				index = parentIndex;
				parentIndex = (index - 1) / 2;
			}
			
			mLength += 1;
		}
	
		function remove()
		{
			if(mLength == 0)
				throw "PQ.remove() - No items to remove";
	
			local data = mData[0];
			mLength -= 1;
			mData[0] = mData[mLength];
			
			local index = 0;
			local left = 1;
			local right = 2;
	
			while(index < mLength)
			{
				local smaller;
				
				if(left >= mLength)
				{
					if(right >= mLength)
						break;
					else
						smaller = right;
				}
				else
				{
					if(right >= mLength)
						smaller = left;
					else
					{
						if(mData[left] < mData[right])
							smaller = left;
						else
							smaller = right;
					}
				}
	
				if(mData[index] > mData[smaller])
				{
					local temp = mData[index];
					mData[index] = mData[smaller];
					mData[smaller] = temp;
					
					index = smaller;
					left = (index * 2) + 1;
					right = left + 1;
				}
				else
					break;
			}
			
			return data;
		}
		
		function resizeArray()
		{
			if(mLength >= #mData)
				mData.length((#mData + 1) * 2 - 1);
		}
		
		function hasData()
		{
			return mLength != 0;
		}
	}
	
	class Stack
	{
		mHead = null;
	
		function push(data)
		{
			local t = { data = data, next = mHead };
			mHead = t;
		}
		
		function pop()
		{
			if(mHead is null)
				throw "Stack.pop() - No items to pop";
	
			local item = mHead;
			mHead = mHead.next;
			
			return item.data;
		}
	
		function hasData()
		{
			return mHead !is null;
		}
	}
	
	class Queue
	{
		mHead = null;
		mTail = null;
	
		function push(data)
		{
			local t = { data = data, next = null };
	
			if(mTail is null)
			{
				mHead = t;
				mTail = t;
			}
			else
			{
				mTail.next = t;
				mTail = t;
			}
		}
		
		function pop()
		{
			if(mTail is null)
				throw "Queue.pop() - No items to pop";
				
			local item = mHead;
			mHead = mHead.next;
			
			if(mHead is null)
				mTail = null;
				
			return item.data;
		}
		
		function hasData()
		{
			return mHead !is null;
		}
	}
	
	writefln("Priority queue (heap)");
	
	local prioQ = PQ();
	
	for(i : 0 .. 10)
		prioQ.insert(math.rand(0, 20));
	
	while(prioQ.hasData())
		writefln(prioQ.remove());
		
	writefln();
	writefln("Stack");
	
	local stack = Stack();
	
	for(i : 0 .. 5)
		stack.push(i + 1);
	
	while(stack.hasData())
		writefln(stack.pop());
	
	writefln();
	writefln("Queue");
	
	local queue = Queue();
	
	for(i : 0 .. 5)
		queue.push(i + 1);
	
	while(queue.hasData())
		writefln(queue.pop());
	
	writefln();
}

// opApply tests.
{
	class Test
	{
		mData = [4, 5, 6];
	
		function opApply(extra)
		{
			if(isString(extra) && extra == "reverse")
			{
				local function iterator_reverse(index)
				{
					index--;
					
					if(index < 0)
						return;
						
					return index, mData[index];
				}
	
				return iterator_reverse, this, #mData;
			}
			else
			{
				local function iterator(index)
				{
					index++;
	
					if(index >= #mData)
						return;
		
					return index, mData[index];
				}
	
				return iterator, this, -1;
			}
		}
	}
	
	local test = Test();
	
	foreach(k, v; test)
		writefln("test[", k, "] = ", v);
	
	writefln();
	
	foreach(k, v; test, "reverse")
		writefln("test[", k, "] = ", v);
		
	writefln();
	
	test =
	{
		fork = 5,
		knife = 10,
		spoon = "hi"
	};
	
	foreach(k, v; test)
		writefln("test[", k, "] = ", v);
	
	test = [5, 10, "hi"];
	
	writefln();
	
	foreach(k, v; test)
		writefln("test[", k, "] = ", v);
	
	writefln();
	
	foreach(k, v; test, "reverse")
		writefln("test[", k, "] = ", v);
	
	writefln();
	
	foreach(k, v; "hello")
		writefln("str[", k, "] = ", v);
	
	writefln();
	
	foreach(k, v; "hello", "reverse")
		writefln("str[", k, "] = ", v);
	
	writefln();
}

// Testing upvalues in for loops.
{
	local arr = array.new(10);
	
	for(i : 0 .. 10)
		arr[i] = function() { return i; };
	
	writefln("This should be the values 0 through 9:");
	
	foreach(func; arr)
		writefln(func());
	
	writefln();
}

// Testing nested functions.
{
	function outer()
	{
		local x = 3;
	
		function inner()
		{
			x++;
			writefln("inner x: ", x);
		}
	
		writefln("outer x: ", x);
		inner();
		writefln("outer x: ", x);
	
		return inner;
	}
	
	local func = outer();
	func();
	
	writefln();
}

// Testing Exceptions.
{
	function thrower(x)
	{
		if(x >= 3)
			throw "Sorry, x is too big for me!";
	}
	
	function tryCatch(iterations)
	{
		try
		{
			for(i : 0 .. iterations)
			{
				writefln("tryCatch: ", i);
				thrower(i);
			}
		}
		catch(e)
		{
			writefln("tryCatch caught: ", e);
			throw e;
		}
		finally
			writefln("tryCatch finally");
	}
	
	try
	{
		tryCatch(2);
		tryCatch(5);
	}
	catch(e)
		writefln("caught: ", e);
	
	writefln();
}

// Testing arrays.
{
	local array = [7, 9, 2, 3, 6];
	
	array.sort();
	
	foreach(i, v; array)
		writefln("arr[", i, "] = ", v);
	
	array ~= ["foo", "far"];
	
	writefln();
	
	foreach(i, v; array)
		writefln("arr[", i, "] = ", v);
	
	writefln();
}

// Testing vararg functions.
{
	function vargs(vararg)
	{
		local args = [vararg];
	
		writefln("num varargs: ", #args);
	
		foreach(i, v; args)
			writefln("args[", i, "] = ", v);
	}
	
	vargs();
	
	writefln();
	
	vargs(2, 3, 5, "foo", "bar");
	
	writefln();
}

// Testing switches.
{
	foreach(v; ["hi", "bye", "foo"])
	{
		switch(v)
		{
			case "hi":
				writefln("switched to hi");
				break;
				
			case "bye":
				writefln("switched to bye");
				break;
				
			default:
				writefln("switched to something else");
				break;
		}
	}
	
	writefln();
	
	foreach(v; [null, false, 1, 2.3, 'x', "hi"])
	{
		switch(v)
		{
			case null: writefln("null"); break;
			case false: writefln("false"); break;
			case 1: writefln("1"); break;
			case 2.3: writefln("2.3"); break;
			case 'x': writefln("x"); break;
			case "hi": writefln("hi"); break;
		}
	}
	
	writefln();
	
	class A
	{
		mValue;
		
		this(value)
		{
			mValue = value;
		}
	
		function opCmp(other)
		{
			assert(other as A);
			return mValue <=> other.mValue;
		}
	}
	
	local a1 = A(1);
	local a2 = A(2);
	local a3 = A(3);
	
	for(s : 1 .. 4)
	{
		local ss = A(s);
	
		switch(ss)
		{
			case a1:
				writefln(1);
				break;
	
			case a2:
				writefln(2);
				break;
	
			case a3:
				writefln(3);
				break;
		}
	}
}